diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 092320f5bb..3d837fbe1e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -97,6 +97,9 @@ jobs: - name: Run core Rust tests run: cargo test --locked -p bitfun-core + - name: Run desktop Rust tests + run: cargo test --locked -p bitfun-desktop + # ── Frontend: build ──────────────────────────────────────────────── frontend-build: name: Frontend Build diff --git a/.gitignore b/.gitignore index 7411dad063..ca5075bb42 100644 --- a/.gitignore +++ b/.gitignore @@ -79,3 +79,5 @@ Cross.toml ASSETS_LICENSES.md external/ +/.bitfun/search/flashgrep-index/ +.agents/ diff --git a/AGENTS-CN.md b/AGENTS-CN.md index 9dbf28d950..0c75337b2a 100644 --- a/AGENTS-CN.md +++ b/AGENTS-CN.md @@ -15,23 +15,28 @@ BitFun 是一个由 Rust workspace 与 React 前端组成的项目。 ## 分层模块索引 -依赖关系按自上而下读取:上层只能依赖下层,同层 crate 也应保持最小依赖。 +依赖关系按自上而下读取。下表是物理 crate 布局,不是完整概念架构;Product Feature、Agent Kernel、 +Execution、Extension、Cross-platform Adapter 的边界以 +[`docs/architecture/core-decomposition.md`](docs/architecture/core-decomposition.md) +为准。同层 crate 也应保持最小依赖。 | # | 层级 | 路径 | 职责 | 模块 / 入口 | 层级文档 | |---|---|---|---|---|---| | 1 | 接口与入口层 | `src/apps/*`, `src/web-ui`, `src/mobile-web`, `BitFun-Installer`, `tests/e2e`, `src/crates/interfaces` | 产品宿主、命令、UI 入口、协议接口和跨形态测试 | desktop、CLI、server、relay、Web UI、mobile web、installer、E2E、`acp` | 最近的本地 `AGENTS.md`;[interfaces](src/crates/interfaces/AGENTS.md) | | 2 | 产品组装层 | `src/crates/assembly` | 兼容导出、产品能力选择、product-full 接线和 adapter/service 注册 | `core`, `product-capabilities` | [AGENTS.md](src/crates/assembly/AGENTS.md) | | 3 | 适配层 | `src/crates/adapters` | AI/API/transport/WebDriver 协议 adapter 和外部 provider 转换 | `ai-adapters`, `api-layer`, `transport`, `webdriver` | [AGENTS.md](src/crates/adapters/AGENTS.md) | -| 4 | 服务实现层 | `src/crates/services` | 可复用 OS、filesystem、terminal、MCP、remote、git、watch、process、session persistence primitives、network 和 MiniApp runtime IO 实现 | `services-core`, `services-integrations`, `terminal` | [AGENTS.md](src/crates/services/AGENTS.md) | +| 4 | 服务实现层 | `src/crates/services` | 可复用 OS、filesystem、terminal、MCP、remote、git、watch、process、LSP plugin registry、session persistence primitives、network 和 MiniApp runtime IO 实现 | `services-core`, `services-integrations`, `terminal` | [AGENTS.md](src/crates/services/AGENTS.md) | | 5 | 执行原语层 | `src/crates/execution` | 可移植 agent、harness、stream、DeepReview policy/report、typed-service、tool-contract、tool-group 和 tool-execution 构件 | `agent-runtime`, `agent-stream`, `tool-contracts`, `harness`, `runtime-services`, `tool-provider-groups`, `tool-execution` | [AGENTS.md](src/crates/execution/AGENTS.md) | -| 6 | 稳定契约与产品领域层 | `src/crates/contracts` | 跨层共享 DTO、事件形状、runtime port、产品领域契约和策略 | `core-types`, `events`, `runtime-ports`, `product-domains` | [AGENTS.md](src/crates/contracts/AGENTS.md) | +| 6 | 稳定契约与产品领域层 | `src/crates/contracts` | 跨层共享 DTO、事件形状、runtime port、LSP protocol/plugin DTO、产品领域契约和策略 | `core-types`, `events`, `runtime-ports`, `product-domains` | [AGENTS.md](src/crates/contracts/AGENTS.md) | 边界规则: - 接口与入口层暴露选定产品行为;可复用行为应下移。 - 组装层只接线下层并选择产品能力事实,不实现具体 adapter、OS 或 service 细节。 -- 适配层翻译协议和外部系统,不拥有产品能力选择或可复用 OS service 行为。 -- 服务实现层负责可复用的 OS、process、terminal、MCP、remote、git、filesystem、session persistence primitives 和 MiniApp runtime IO 能力。 +- 产品特性只在内核能力之上组装用户侧命令、UI contribution、设置和默认策略;长程任务、scheduler、permission、session/workspace、memory、DFX、hook 和 event 事实属于 Agent Kernel owner。 +- 适配层翻译协议和外部 provider 形状,不拥有产品能力选择或可复用 OS service 行为。 +- 服务实现层负责可复用的 OS、process、terminal、MCP、remote、git、filesystem、LSP plugin registry、session persistence primitives 和 MiniApp runtime IO 能力。 +- 外部系统是边界外资源,不是仓库内层级。只有已注册的 adapter、service 或 app-local provider 应调用它们;其他层消费 port 和稳定契约。 - 执行原语层只放可移植运行时构件,不拥有宿主或交付形态。 - 契约层保持轻行为,不得向上依赖。 diff --git a/AGENTS.md b/AGENTS.md index b766a3735f..1d2a93d128 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -15,24 +15,29 @@ Repository rule: **keep product logic platform-agnostic, then expose it through ## Layered Module Index -Dependencies flow top to bottom. A layer may depend on lower layers only; keep -crate dependencies inside each layer to the smallest set needed. +Dependencies flow top to bottom. This table is the physical crate layout, not +the full conceptual architecture. For Product Feature / Agent Kernel / +Execution / Extension / Cross-platform Adapter boundaries, read +[`docs/architecture/core-decomposition.md`](docs/architecture/core-decomposition.md). +Keep crate dependencies inside each layer to the smallest set needed. | # | Layer | Path | Owns | Modules / entries | Layer doc | |---|---|---|---|---|---| | 1 | Interfaces and entrypoints | `src/apps/*`, `src/web-ui`, `src/mobile-web`, `BitFun-Installer`, `tests/e2e`, `src/crates/interfaces` | Product hosts, commands, UI entrypoints, protocol interfaces, and cross-surface tests | desktop, CLI, server, relay, Web UI, mobile web, installer, E2E, `acp` | nearest local `AGENTS.md`; [interfaces](src/crates/interfaces/AGENTS.md) | | 2 | Product assembly | `src/crates/assembly` | Compatibility exports, product capability selection, product-full wiring, and adapter/service registration | `core`, `product-capabilities` | [AGENTS.md](src/crates/assembly/AGENTS.md) | | 3 | Adapters | `src/crates/adapters` | AI/API/transport/WebDriver protocol adapters and external-provider translation | `ai-adapters`, `api-layer`, `transport`, `webdriver` | [AGENTS.md](src/crates/adapters/AGENTS.md) | -| 4 | Services | `src/crates/services` | Reusable OS, filesystem, terminal, MCP, remote, git, watch, process, session persistence primitives, MiniApp runtime IO, and network implementations | `services-core`, `services-integrations`, `terminal` | [AGENTS.md](src/crates/services/AGENTS.md) | +| 4 | Services | `src/crates/services` | Reusable OS, filesystem, terminal, MCP, remote, git, watch, process, LSP plugin registry, session persistence primitives, MiniApp runtime IO, and network implementations | `services-core`, `services-integrations`, `terminal` | [AGENTS.md](src/crates/services/AGENTS.md) | | 5 | Execution primitives | `src/crates/execution` | Portable agent, harness, stream, DeepReview policy/report, typed-service, tool-contract, tool-group, and tool-execution building blocks | `agent-runtime`, `agent-stream`, `tool-contracts`, `harness`, `runtime-services`, `tool-provider-groups`, `tool-execution` | [AGENTS.md](src/crates/execution/AGENTS.md) | -| 6 | Stable contracts and product domains | `src/crates/contracts` | Shared DTOs, event shapes, runtime ports, and product domain contracts/policies | `core-types`, `events`, `runtime-ports`, `product-domains` | [AGENTS.md](src/crates/contracts/AGENTS.md) | +| 6 | Stable contracts and product domains | `src/crates/contracts` | Shared DTOs, event shapes, runtime ports, LSP protocol/plugin DTOs, and product domain contracts/policies | `core-types`, `events`, `runtime-ports`, `product-domains` | [AGENTS.md](src/crates/contracts/AGENTS.md) | Boundary rules: - Interfaces and app entrypoints expose selected product behavior; reusable behavior moves down. - Assembly wires lower layers and selects product capability facts; it must not implement concrete adapter, OS, or service details. -- Adapters translate protocols and external systems; they should not own product capability selection or reusable OS service behavior. -- Services implement reusable concrete OS, process, terminal, MCP, remote, git, filesystem, and MiniApp runtime IO capabilities. +- Product features assemble user-facing commands, UI contributions, settings, and default policy on top of kernel capabilities; long-running task, scheduler, permission, session/workspace, memory, DFX, hook, and event facts stay in Agent Kernel owners. +- Adapters translate protocols and external-provider shapes; they should not own product capability selection or reusable OS service behavior. +- Services implement reusable concrete OS, process, terminal, MCP, remote, git, filesystem, LSP plugin registry, and MiniApp runtime IO capabilities. +- External systems are boundary resources, not repository layers. Only registered adapters/services/app-local providers should call them; other layers consume ports and stable contracts. - Execution crates are portable runtime building blocks, not host-specific or delivery-profile owners. - Contracts stay behavior-light and must not depend upward. diff --git a/BitFun-Installer/package-lock.json b/BitFun-Installer/package-lock.json index aacce26030..9cc077cb9d 100644 --- a/BitFun-Installer/package-lock.json +++ b/BitFun-Installer/package-lock.json @@ -1,12 +1,12 @@ { "name": "bitfun-installer", - "version": "0.2.10", + "version": "0.2.12", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "bitfun-installer", - "version": "0.2.10", + "version": "0.2.12", "dependencies": { "@tauri-apps/api": "^2.10.1", "@tauri-apps/plugin-dialog": "^2.6.0", diff --git a/BitFun-Installer/package.json b/BitFun-Installer/package.json index a2b650f98d..6948bc4e85 100644 --- a/BitFun-Installer/package.json +++ b/BitFun-Installer/package.json @@ -1,6 +1,6 @@ { "name": "bitfun-installer", - "version": "0.2.10", + "version": "0.2.12", "private": true, "type": "module", "description": "BitFun Custom Installer - Modern branded installation experience", diff --git a/BitFun-Installer/src-tauri/Cargo.toml b/BitFun-Installer/src-tauri/Cargo.toml index ef8b293462..ed52c8893e 100644 --- a/BitFun-Installer/src-tauri/Cargo.toml +++ b/BitFun-Installer/src-tauri/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "bitfun-installer" -version = "0.2.10" +version = "0.2.12" authors = ["BitFun Team"] edition = "2021" description = "BitFun Custom Installer - Modern branded installation experience" diff --git a/BitFun-Installer/src-tauri/src/installer/types.rs b/BitFun-Installer/src-tauri/src/installer/types.rs index 03c7933e63..0b692ccb48 100644 --- a/BitFun-Installer/src-tauri/src/installer/types.rs +++ b/BitFun-Installer/src-tauri/src/installer/types.rs @@ -70,6 +70,9 @@ impl From for RemoteModelInfo { pub enum ConnectionTestMessageCode { ToolCallsNotDetected, ImageInputCheckFailed, + TlsOrCertificateIssue, + ProxyIssue, + NetworkIssue, } impl From for ConnectionTestMessageCode { @@ -81,6 +84,13 @@ impl From for ConnectionTe bitfun_ai_adapters::types::ConnectionTestMessageCode::ImageInputCheckFailed => { Self::ImageInputCheckFailed } + bitfun_ai_adapters::types::ConnectionTestMessageCode::TlsOrCertificateIssue => { + Self::TlsOrCertificateIssue + } + bitfun_ai_adapters::types::ConnectionTestMessageCode::ProxyIssue => Self::ProxyIssue, + bitfun_ai_adapters::types::ConnectionTestMessageCode::NetworkIssue => { + Self::NetworkIssue + } } } } diff --git a/BitFun-Installer/src/pages/LanguageSelect.tsx b/BitFun-Installer/src/pages/LanguageSelect.tsx index 4cc3a1bb98..f8836ead01 100644 --- a/BitFun-Installer/src/pages/LanguageSelect.tsx +++ b/BitFun-Installer/src/pages/LanguageSelect.tsx @@ -44,13 +44,13 @@ export function LanguageSelect({ onSelect }: LanguageSelectProps) { BitFun

BitFun

@@ -72,7 +72,7 @@ export function LanguageSelect({ onSelect }: LanguageSelectProps) { opacity: 0.6, letterSpacing: '0.5px', }}> - Version 0.2.10 + Version 0.2.12 @@ -106,9 +106,9 @@ export function LanguageSelect({ onSelect }: LanguageSelectProps) { style={{ display: 'flex', alignItems: 'center', gap: 12, padding: '14px 16px', width: '100%', - background: isSelected ? 'rgba(96, 165, 250, 0.14)' : 'rgba(148, 163, 184, 0.08)', + background: isSelected ? 'var(--color-accent-100)' : 'var(--element-bg-subtle)', border: 'none', - borderRadius: 'var(--radius-sm)', + borderRadius: 'var(--size-radius-sm)', cursor: 'pointer', textAlign: 'left', transition: 'all 0.25s ease', outline: 'none', @@ -117,12 +117,12 @@ export function LanguageSelect({ onSelect }: LanguageSelectProps) { }} onMouseEnter={(e) => { if (!isSelected) { - e.currentTarget.style.background = 'rgba(148, 163, 184, 0.14)'; + e.currentTarget.style.background = 'var(--element-bg-soft)'; } }} onMouseLeave={(e) => { if (!isSelected) { - e.currentTarget.style.background = 'rgba(148, 163, 184, 0.08)'; + e.currentTarget.style.background = 'var(--element-bg-subtle)'; } }} > diff --git a/BitFun-Installer/src/pages/Options.tsx b/BitFun-Installer/src/pages/Options.tsx index accb3cad26..36674bc253 100644 --- a/BitFun-Installer/src/pages/Options.tsx +++ b/BitFun-Installer/src/pages/Options.tsx @@ -107,7 +107,7 @@ export function Options({ ) : null} {!existingInstall.mainBinaryPresent ? ( -
+
{t('options.existingInstallBinaryMissing')}
) : null} diff --git a/BitFun-Installer/src/pages/ThemeSetup.tsx b/BitFun-Installer/src/pages/ThemeSetup.tsx index 200ffc25d9..92ecae9a52 100644 --- a/BitFun-Installer/src/pages/ThemeSetup.tsx +++ b/BitFun-Installer/src/pages/ThemeSetup.tsx @@ -29,7 +29,7 @@ export function ThemeSetup({ options, setOptions, onLaunch, onClose }: ThemeSetu width: '100%', borderRadius: 12, padding: 8, - background: active ? 'rgba(96, 165, 250, 0.14)' : 'rgba(148, 163, 184, 0.08)', + background: active ? 'var(--color-accent-100)' : 'var(--element-bg-subtle)', border: 'none', cursor: 'pointer', transition: 'background 0.2s ease', @@ -106,7 +106,7 @@ export function ThemeSetup({ options, setOptions, onLaunch, onClose }: ThemeSetu
-
+
diff --git a/BitFun-Installer/src/styles/global.css b/BitFun-Installer/src/styles/global.css index 86ba0d0230..111b92f9ac 100644 --- a/BitFun-Installer/src/styles/global.css +++ b/BitFun-Installer/src/styles/global.css @@ -27,7 +27,7 @@ html, body, #root { display: flex; align-items: center; justify-content: space-between; padding: 0 16px; background: var(--color-bg-primary); - border-bottom: 1px solid rgba(148, 163, 184, 0.12); + border-bottom: 1px solid var(--border-subtle); -webkit-app-region: drag; flex-shrink: 0; position: relative; z-index: 10; } @@ -58,20 +58,20 @@ html, body, #root { width: 32px; height: 32px; padding: 0; background: transparent; border: 1px solid transparent; - border-radius: var(--radius-sm); - color: #8a8a8a; + border-radius: var(--size-radius-sm); + color: var(--color-text-muted); cursor: pointer; outline: none; transition: background var(--motion-fast) ease, color var(--motion-fast) ease; } .window-controls__btn:hover { - background: rgba(255, 255, 255, 0.06); - color: #b8b8b8; + background: var(--element-bg-subtle); + color: var(--color-text-secondary); } .window-controls__btn:active { opacity: 0.8; } .window-controls__btn--close:hover { - background: rgba(255, 255, 255, 0.06); - color: #ef4444; + background: var(--element-bg-subtle); + color: var(--color-error); } .installer-content { @@ -115,7 +115,7 @@ html, body, #root { gap: 10px; flex-wrap: wrap; padding: 10px 22px 12px; - border-top: 1px solid rgba(148, 163, 184, 0.12); + border-top: 1px solid var(--border-subtle); background: var(--color-bg-primary); flex-shrink: 0; } @@ -135,7 +135,7 @@ html, body, #root { min-height: 28px; background: transparent; border: 1px dashed var(--border-base); - border-radius: var(--radius-sm); + border-radius: var(--size-radius-sm); font-family: var(--font-sans); font-size: 13px; font-weight: 500; color: var(--color-text-muted); @@ -204,14 +204,14 @@ html, body, #root { width: 100%; background: transparent; border: 1px dashed var(--border-base); - border-radius: var(--radius-sm); + border-radius: var(--size-radius-sm); cursor: pointer; text-align: left; box-shadow: none; transition: background 0.25s ease, transform 0.2s ease, border-color 0.25s ease, border-style 0.25s ease; position: relative; overflow: hidden; } .card-btn:hover { - background: rgba(148, 163, 184, 0.08); + background: var(--element-bg-subtle); border-style: solid; border-color: var(--border-medium); transform: translateY(-1px); @@ -220,17 +220,17 @@ html, body, #root { .card-btn:active { transform: translateY(0); } .card-btn:focus-visible { outline: none; - background: rgba(148, 163, 184, 0.14); + background: var(--element-bg-soft); box-shadow: none; } -.card-btn:hover .card-btn__icon { background: rgba(148, 163, 184, 0.16); } +.card-btn:hover .card-btn__icon { background: var(--element-bg-soft); } .card-btn:hover .card-btn__icon svg { color: var(--color-accent-600); } .card-btn:hover .card-btn__arrow { opacity: 0.8; transform: translateX(0); } .card-btn__icon { flex-shrink: 0; width: 40px; height: 40px; display: flex; align-items: center; justify-content: center; - background: rgba(148, 163, 184, 0.1); + background: var(--element-bg-subtle); border-radius: 10px; transition: all 0.25s ease; } @@ -252,7 +252,7 @@ html, body, #root { flex: 1; padding: 10px 14px; background: var(--element-bg-subtle); border: 1px solid var(--border-subtle); - border-radius: var(--radius-sm); + border-radius: var(--size-radius-sm); color: var(--color-text-primary); font-size: 12px; font-family: var(--font-mono); outline: none; transition: all 0.25s ease; @@ -275,7 +275,7 @@ html, body, #root { gap: 8px; background: var(--element-bg-subtle); border: 1px solid var(--border-subtle); - border-radius: var(--radius-sm); + border-radius: var(--size-radius-sm); color: var(--color-text-primary); cursor: pointer; text-align: left; @@ -326,8 +326,8 @@ html, body, #root { padding: 4px; background: var(--color-bg-secondary); border: 1px solid var(--border-medium); - border-radius: var(--radius-sm); - box-shadow: 0 8px 18px rgba(0, 0, 0, 0.25); + border-radius: var(--size-radius-sm); + box-shadow: none; } .bf-select-search { @@ -343,7 +343,7 @@ html, body, #root { min-height: 30px; padding: 6px 8px; border: 1px solid var(--border-subtle); - border-radius: var(--radius-sm); + border-radius: var(--size-radius-sm); background: var(--element-bg-subtle); color: var(--color-text-primary); font-size: 12px; @@ -366,7 +366,7 @@ html, body, #root { background: transparent; color: var(--color-text-primary); padding: 8px; - border-radius: var(--radius-sm); + border-radius: var(--size-radius-sm); text-align: left; display: flex; flex-direction: column; @@ -538,7 +538,7 @@ html, body, #root { gap: 10px; flex-wrap: wrap; padding: 10px 22px 12px; - border-top: 1px solid rgba(148, 163, 184, 0.12); + border-top: 1px solid var(--border-subtle); background: var(--color-bg-primary); flex-shrink: 0; } @@ -558,7 +558,7 @@ html, body, #root { .checkbox-item { display: flex; align-items: center; gap: 10px; cursor: pointer; padding: 8px 10px; - border-radius: var(--radius-sm); + border-radius: var(--size-radius-sm); transition: background var(--motion-fast) ease; } .checkbox-item:hover { background: var(--element-bg-subtle); } @@ -571,7 +571,7 @@ html, body, #root { transition: all var(--motion-fast) ease; flex-shrink: 0; } .checkbox-box.checked { background: var(--color-accent-500); border-color: var(--color-accent-500); } -.checkbox-box svg { width: 10px; height: 10px; color: white; opacity: 0; transition: opacity var(--motion-fast) ease; } +.checkbox-box svg { width: 10px; height: 10px; color: var(--color-bg-secondary); opacity: 0; transition: opacity var(--motion-fast) ease; } .checkbox-box.checked svg { opacity: 1; } .checkbox-label { font-size: 13px; color: var(--color-text-secondary); transition: color var(--motion-fast) ease; } @@ -590,7 +590,7 @@ html, body, #root { } .progress-bar-fill::after { content: ''; position: absolute; inset: 0; - background: linear-gradient(90deg, transparent, rgba(255,255,255,0.2), transparent); + background: linear-gradient(90deg, transparent, var(--color-accent-200), transparent); background-size: 200% 100%; animation: shimmer 2s linear infinite; } diff --git a/BitFun-Installer/src/styles/variables.css b/BitFun-Installer/src/styles/variables.css index ff5e39d44c..cfd9e8f718 100644 --- a/BitFun-Installer/src/styles/variables.css +++ b/BitFun-Installer/src/styles/variables.css @@ -1,17 +1,10 @@ :root { --color-bg-primary: #121214; - --color-bg-secondary: #18181a; - --color-bg-tertiary: #121214; - --color-bg-quaternary: #202024; - --color-bg-elevated: #18181a; - --color-bg-workbench: #121214; - --color-bg-flowchat: #121214; - --color-bg-tooltip: rgba(30, 30, 32, 0.92); + --color-bg-secondary: #1a1c1e; --color-text-primary: #e8e8e8; --color-text-secondary: #b0b0b0; --color-text-muted: #858585; - --color-text-disabled: #555555; --color-accent-50: rgba(96, 165, 250, 0.04); --color-accent-100: rgba(96, 165, 250, 0.08); @@ -20,42 +13,21 @@ --color-accent-400: rgba(96, 165, 250, 0.4); --color-accent-500: #60a5fa; --color-accent-600: #3b82f6; - --color-accent-700: rgba(59, 130, 246, 0.8); - --color-accent-800: rgba(59, 130, 246, 0.9); - - --color-purple-50: rgba(139, 92, 246, 0.04); - --color-purple-100: rgba(139, 92, 246, 0.08); - --color-purple-200: rgba(139, 92, 246, 0.15); - --color-purple-300: rgba(139, 92, 246, 0.25); - --color-purple-400: rgba(139, 92, 246, 0.4); - --color-purple-500: #8b5cf6; - --color-purple-600: #7c3aed; - --color-purple-700: rgba(124, 58, 237, 0.8); - --color-purple-800: rgba(124, 58, 237, 0.9); --color-success: #34d399; --color-warning: #f59e0b; --color-error: #ef4444; - --color-info: #E1AB80; - --color-highlight: #d4a574; - --color-highlight-bg: rgba(212, 165, 116, 0.15); --element-bg-subtle: rgba(255, 255, 255, 0.06); - --element-bg-soft: rgba(255, 255, 255, 0.10); - --element-bg-base: rgba(255, 255, 255, 0.13); - --element-bg-medium: rgba(255, 255, 255, 0.17); - --element-bg-strong: rgba(255, 255, 255, 0.21); - --element-bg-elevated: rgba(255, 255, 255, 0.25); + --element-bg-soft: rgba(255, 255, 255, 0.12); + --element-bg-medium: rgba(255, 255, 255, 0.18); + --element-bg-strong: rgba(255, 255, 255, 0.24); --border-subtle: rgba(255, 255, 255, 0.12); --border-base: rgba(255, 255, 255, 0.18); --border-medium: rgba(255, 255, 255, 0.24); - --border-strong: rgba(255, 255, 255, 0.32); - --border-prominent: rgba(225, 171, 128, 0.50); - --radius-sm: 6px; - --radius-base: 8px; - --radius-lg: 12px; + --size-radius-sm: 6px; --font-sans: 'Noto Sans SC', 'PingFang SC', 'Microsoft YaHei', -apple-system, BlinkMacSystemFont, 'Segoe UI', 'SF Pro Display', diff --git a/BitFun-Installer/src/theme/installerThemeRuntime.ts b/BitFun-Installer/src/theme/installerThemeRuntime.ts index c438c90ccd..c0f4e29def 100644 --- a/BitFun-Installer/src/theme/installerThemeRuntime.ts +++ b/BitFun-Installer/src/theme/installerThemeRuntime.ts @@ -3,6 +3,8 @@ import { SYSTEM_THEME_ID, type ThemeId, type ThemePreferenceId } from '../types/ import type { InstallerTheme } from './installerThemesData'; import { findInstallerThemeById } from './installerThemesData'; +const ACCENT_STOPS = ['50', '100', '200', '300', '400', '500', '600'] as const; + /** Same rule as main app `getSystemPreferredDefaultThemeId`: dark -> bitfun-dark, else bitfun-light. */ export function getSystemPreferredBuiltinThemeId(): ThemeId { if (typeof window === 'undefined' || typeof window.matchMedia !== 'function') { @@ -17,44 +19,24 @@ export function applyInstallerThemeToDocument(theme: InstallerTheme): void { root.style.setProperty('--color-bg-primary', colors.background.primary); root.style.setProperty('--color-bg-secondary', colors.background.secondary); - root.style.setProperty('--color-bg-tertiary', colors.background.tertiary); - root.style.setProperty('--color-bg-quaternary', colors.background.quaternary); - root.style.setProperty('--color-bg-elevated', colors.background.elevated); - root.style.setProperty('--color-bg-workbench', colors.background.workbench); - root.style.setProperty('--color-bg-flowchat', colors.background.flowchat); - root.style.setProperty('--color-bg-tooltip', colors.background.tooltip ?? colors.background.elevated); root.style.setProperty('--color-text-primary', colors.text.primary); root.style.setProperty('--color-text-secondary', colors.text.secondary); root.style.setProperty('--color-text-muted', colors.text.muted); - root.style.setProperty('--color-text-disabled', colors.text.disabled); root.style.setProperty('--element-bg-subtle', colors.element.subtle); root.style.setProperty('--element-bg-soft', colors.element.soft); - root.style.setProperty('--element-bg-base', colors.element.base); root.style.setProperty('--element-bg-medium', colors.element.medium); root.style.setProperty('--element-bg-strong', colors.element.strong); - root.style.setProperty('--element-bg-elevated', colors.element.elevated); root.style.setProperty('--border-subtle', colors.border.subtle); root.style.setProperty('--border-base', colors.border.base); root.style.setProperty('--border-medium', colors.border.medium); - root.style.setProperty('--border-strong', colors.border.strong); - root.style.setProperty('--border-prominent', colors.border.prominent); root.style.setProperty('--color-success', colors.semantic.success); root.style.setProperty('--color-warning', colors.semantic.warning); root.style.setProperty('--color-error', colors.semantic.error); - root.style.setProperty('--color-info', colors.semantic.info); - root.style.setProperty('--color-highlight', colors.semantic.highlight); - root.style.setProperty('--color-highlight-bg', colors.semantic.highlightBg); - Object.entries(colors.accent).forEach(([key, value]) => { - root.style.setProperty(`--color-accent-${key}`, value); + ACCENT_STOPS.forEach((key) => { + root.style.setProperty(`--color-accent-${key}`, colors.accent[key]); }); - if (colors.purple) { - Object.entries(colors.purple).forEach(([key, value]) => { - root.style.setProperty(`--color-purple-${key}`, value); - }); - } - root.setAttribute('data-theme', theme.id); root.setAttribute('data-theme-type', theme.type); } diff --git a/BitFun-Installer/src/theme/installerThemesData.ts b/BitFun-Installer/src/theme/installerThemesData.ts index aae24b61db..50f8959a56 100644 --- a/BitFun-Installer/src/theme/installerThemesData.ts +++ b/BitFun-Installer/src/theme/installerThemesData.ts @@ -1,5 +1,14 @@ import type { ThemeId } from '../types/installer'; +type AccentStop = '50' | '100' | '200' | '300' | '400' | '500' | '600'; +type AccentRamp = Record; +type RampAlphas = readonly [string, string, string, string, string]; +type BackgroundColors = InstallerTheme['colors']['background']; +type TextColors = InstallerTheme['colors']['text']; +type SemanticColors = InstallerTheme['colors']['semantic']; +type BorderColors = InstallerTheme['colors']['border']; +type ElementColors = InstallerTheme['colors']['element']; + export type InstallerTheme = { id: ThemeId; name: string; @@ -8,160 +17,259 @@ export type InstallerTheme = { background: { primary: string; secondary: string; - tertiary: string; - quaternary: string; - elevated: string; - workbench: string; - flowchat: string; - tooltip: string; }; text: { primary: string; secondary: string; muted: string; - disabled: string; }; - accent: Record<'50' | '100' | '200' | '300' | '400' | '500' | '600' | '700' | '800', string>; - purple: Record<'50' | '100' | '200' | '300' | '400' | '500' | '600' | '700' | '800', string>; + accent: AccentRamp; semantic: { success: string; warning: string; error: string; - info: string; - highlight: string; - highlightBg: string; }; border: { subtle: string; base: string; medium: string; - strong: string; - prominent: string; }; element: { subtle: string; soft: string; - base: string; medium: string; strong: string; - elevated: string; }; }; }; +const DEFAULT_RAMP_ALPHAS: RampAlphas = ['0.04', '0.08', '0.15', '0.25', '0.4']; +const DEFAULT_BLUE_RGB = '96, 165, 250'; +const DEFAULT_BLUE_500 = '#60a5fa'; +const DEFAULT_BLUE_600 = '#3b82f6'; +const DARK_CARD_BACKGROUND = '#121214'; +const DARK_CARD_SURFACE = '#1a1c1e'; +const MIDNIGHT_CARD_BACKGROUND = '#2b2d30'; + +function alpha(rgb: string, opacity: string): string { + return `rgba(${rgb}, ${opacity})`; +} + +function createAccentRamp( + rgb: string, + solid500: string, + solid600: string, + alphas: RampAlphas = DEFAULT_RAMP_ALPHAS, +): AccentRamp { + return { + '50': alpha(rgb, alphas[0]), + '100': alpha(rgb, alphas[1]), + '200': alpha(rgb, alphas[2]), + '300': alpha(rgb, alphas[3]), + '400': alpha(rgb, alphas[4]), + '500': solid500, + '600': solid600, + }; +} + +type TonePreset = { + text: TextColors; + semantic: SemanticColors; + border: BorderColors; + element: ElementColors; +}; + +type ThemeSeed = { + id: ThemeId; + name: string; + type: 'dark' | 'light'; + background: { + primary: string; + secondary?: string; + }; + accentRgb: string; + accent500: string; + accent600: string; + semantic?: Partial; +}; + +function createBackground(seed: ThemeSeed['background']): BackgroundColors { + const secondary = seed.secondary ?? seed.primary; + return { + primary: seed.primary, + secondary, + }; +} + +function createBorderRamp(rgb: string, alphas: readonly [string, string, string]): BorderColors { + return { + subtle: alpha(rgb, alphas[0]), + base: alpha(rgb, alphas[1]), + medium: alpha(rgb, alphas[2]), + }; +} + +function createElementRamp(rgb: string): ElementColors { + return { + subtle: alpha(rgb, '0.06'), + soft: alpha(rgb, '0.12'), + medium: alpha(rgb, '0.18'), + strong: alpha(rgb, '0.24'), + }; +} + +const DARK_TONE: TonePreset = { + text: { primary: '#e8e8e8', secondary: '#b0b0b0', muted: '#858585' }, + semantic: { + success: '#34d399', + warning: '#f59e0b', + error: '#ef4444', + }, + border: createBorderRamp('255, 255, 255', ['0.12', '0.18', '0.24']), + element: createElementRamp('255, 255, 255'), +}; + +const LIGHT_TONE: TonePreset = { + text: { primary: '#1e293b', secondary: '#3d4f66', muted: '#64748b' }, + semantic: { + success: '#5b9a6f', + warning: '#c08c42', + error: '#c26565', + }, + border: createBorderRamp('100, 116, 139', ['0.15', '0.22', '0.32']), + element: createElementRamp('71, 102, 143'), +}; + +function createInstallerTheme(seed: ThemeSeed): InstallerTheme { + const accent = createAccentRamp(seed.accentRgb, seed.accent500, seed.accent600); + const tone = seed.type === 'light' ? LIGHT_TONE : DARK_TONE; + + return { + id: seed.id, + name: seed.name, + type: seed.type, + colors: { + background: createBackground(seed.background), + text: { ...tone.text }, + accent, + semantic: { + success: tone.semantic.success, + warning: tone.semantic.warning, + error: tone.semantic.error, + ...seed.semantic, + }, + border: { ...tone.border }, + element: { ...tone.element }, + }, + }; +} + export const THEMES: InstallerTheme[] = [ - { + createInstallerTheme({ id: 'bitfun-dark', name: 'Dark', type: 'dark', - colors: { - background: { primary: '#121214', secondary: '#18181a', tertiary: '#121214', quaternary: '#202024', elevated: '#18181a', workbench: '#121214', flowchat: '#121214', tooltip: 'rgba(30, 30, 32, 0.92)' }, - text: { primary: '#e8e8e8', secondary: '#b0b0b0', muted: '#858585', disabled: '#555555' }, - accent: { '50': 'rgba(96, 165, 250, 0.04)', '100': 'rgba(96, 165, 250, 0.08)', '200': 'rgba(96, 165, 250, 0.15)', '300': 'rgba(96, 165, 250, 0.25)', '400': 'rgba(96, 165, 250, 0.4)', '500': '#60a5fa', '600': '#3b82f6', '700': 'rgba(59, 130, 246, 0.8)', '800': 'rgba(59, 130, 246, 0.9)' }, - purple: { '50': 'rgba(139, 92, 246, 0.04)', '100': 'rgba(139, 92, 246, 0.08)', '200': 'rgba(139, 92, 246, 0.15)', '300': 'rgba(139, 92, 246, 0.25)', '400': 'rgba(139, 92, 246, 0.4)', '500': '#8b5cf6', '600': '#7c3aed', '700': 'rgba(124, 58, 237, 0.8)', '800': 'rgba(124, 58, 237, 0.9)' }, - semantic: { success: '#34d399', warning: '#f59e0b', error: '#ef4444', info: '#E1AB80', highlight: '#d4a574', highlightBg: 'rgba(212, 165, 116, 0.15)' }, - border: { subtle: 'rgba(255, 255, 255, 0.12)', base: 'rgba(255, 255, 255, 0.18)', medium: 'rgba(255, 255, 255, 0.24)', strong: 'rgba(255, 255, 255, 0.32)', prominent: 'rgba(225, 171, 128, 0.50)' }, - element: { subtle: 'rgba(255, 255, 255, 0.06)', soft: 'rgba(255, 255, 255, 0.10)', base: 'rgba(255, 255, 255, 0.13)', medium: 'rgba(255, 255, 255, 0.17)', strong: 'rgba(255, 255, 255, 0.21)', elevated: 'rgba(255, 255, 255, 0.25)' }, + background: { + primary: DARK_CARD_BACKGROUND, + secondary: DARK_CARD_SURFACE, }, - }, - { + accentRgb: DEFAULT_BLUE_RGB, + accent500: DEFAULT_BLUE_500, + accent600: DEFAULT_BLUE_600, + }), + createInstallerTheme({ id: 'bitfun-light', name: 'Light', type: 'light', - colors: { - background: { primary: '#f7f8fa', secondary: '#ffffff', tertiary: '#f3f5f8', quaternary: '#ebeef3', elevated: '#ffffff', workbench: '#f7f8fa', flowchat: '#f7f8fa', tooltip: 'rgba(255, 255, 255, 0.98)' }, - text: { primary: '#1e293b', secondary: '#3d4f66', muted: '#64748b', disabled: '#94a3b8' }, - accent: { '50': 'rgba(71, 102, 143, 0.04)', '100': 'rgba(71, 102, 143, 0.08)', '200': 'rgba(71, 102, 143, 0.14)', '300': 'rgba(71, 102, 143, 0.22)', '400': 'rgba(71, 102, 143, 0.36)', '500': '#5a7bb2', '600': '#4a6694', '700': 'rgba(74, 102, 148, 0.8)', '800': 'rgba(74, 102, 148, 0.9)' }, - purple: { '50': 'rgba(107, 90, 137, 0.04)', '100': 'rgba(107, 90, 137, 0.08)', '200': 'rgba(107, 90, 137, 0.14)', '300': 'rgba(107, 90, 137, 0.22)', '400': 'rgba(107, 90, 137, 0.36)', '500': '#7c6b99', '600': '#655680', '700': 'rgba(101, 86, 128, 0.8)', '800': 'rgba(101, 86, 128, 0.9)' }, - semantic: { success: '#5b9a6f', warning: '#c08c42', error: '#c26565', info: '#5a7bb2', highlight: '#b8863a', highlightBg: 'rgba(184, 134, 58, 0.12)' }, - border: { subtle: 'rgba(100, 116, 139, 0.15)', base: 'rgba(100, 116, 139, 0.22)', medium: 'rgba(100, 116, 139, 0.32)', strong: 'rgba(100, 116, 139, 0.42)', prominent: 'rgba(100, 116, 139, 0.52)' }, - element: { subtle: 'rgba(71, 102, 143, 0.05)', soft: 'rgba(71, 102, 143, 0.08)', base: 'rgba(71, 102, 143, 0.11)', medium: 'rgba(71, 102, 143, 0.15)', strong: 'rgba(71, 102, 143, 0.20)', elevated: 'rgba(255, 255, 255, 0.92)' }, - }, - }, - { + background: { primary: '#f7f8fa', secondary: '#ffffff' }, + accentRgb: '71, 102, 143', + accent500: '#5a7bb2', + accent600: '#4a6694', + }), + createInstallerTheme({ id: 'bitfun-midnight', name: 'Midnight', type: 'dark', - colors: { - background: { primary: '#2b2d30', secondary: '#1e1f22', tertiary: '#313335', quaternary: '#3c3f41', elevated: '#2b2d30', workbench: '#212121', flowchat: '#2b2d30', tooltip: 'rgba(43, 45, 48, 0.94)' }, - text: { primary: '#bcbec4', secondary: '#9da0a8', muted: '#6f737a', disabled: '#4e5157' }, - accent: { '50': 'rgba(88, 166, 255, 0.04)', '100': 'rgba(88, 166, 255, 0.08)', '200': 'rgba(88, 166, 255, 0.15)', '300': 'rgba(88, 166, 255, 0.25)', '400': 'rgba(88, 166, 255, 0.4)', '500': '#58a6ff', '600': '#3b82f6', '700': 'rgba(59, 130, 246, 0.8)', '800': 'rgba(59, 130, 246, 0.9)' }, - purple: { '50': 'rgba(156, 120, 255, 0.04)', '100': 'rgba(156, 120, 255, 0.08)', '200': 'rgba(156, 120, 255, 0.15)', '300': 'rgba(156, 120, 255, 0.25)', '400': 'rgba(156, 120, 255, 0.4)', '500': '#9c78ff', '600': '#8b5cf6', '700': 'rgba(139, 92, 246, 0.8)', '800': 'rgba(139, 92, 246, 0.9)' }, - semantic: { success: '#6aab73', warning: '#e0a055', error: '#cc7f7a', info: '#58a6ff', highlight: '#d4a574', highlightBg: 'rgba(212, 165, 116, 0.15)' }, - border: { subtle: 'rgba(255, 255, 255, 0.08)', base: 'rgba(255, 255, 255, 0.14)', medium: 'rgba(255, 255, 255, 0.20)', strong: 'rgba(255, 255, 255, 0.26)', prominent: 'rgba(255, 255, 255, 0.35)' }, - element: { subtle: 'rgba(255, 255, 255, 0.04)', soft: 'rgba(255, 255, 255, 0.06)', base: 'rgba(255, 255, 255, 0.09)', medium: 'rgba(255, 255, 255, 0.12)', strong: 'rgba(255, 255, 255, 0.15)', elevated: 'rgba(255, 255, 255, 0.18)' }, + background: { primary: MIDNIGHT_CARD_BACKGROUND, secondary: DARK_CARD_SURFACE }, + accentRgb: DEFAULT_BLUE_RGB, + accent500: DEFAULT_BLUE_500, + accent600: DEFAULT_BLUE_600, + semantic: { + success: '#6aab73', + warning: '#e0a055', + error: '#cc7f7a', }, - }, - { + }), + createInstallerTheme({ id: 'bitfun-china-style', name: 'Ink Charm', type: 'light', - colors: { - background: { primary: '#faf8f0', secondary: '#f5f3e8', tertiary: '#f0ede0', quaternary: '#ebe8d8', elevated: '#ebe9e3', workbench: '#faf8f0', flowchat: '#faf8f0', tooltip: 'rgba(250, 248, 240, 0.96)' }, - text: { primary: '#1a1a1a', secondary: '#3d3d3d', muted: '#6a6a6a', disabled: '#9a9a9a' }, - accent: { '50': 'rgba(46, 94, 138, 0.04)', '100': 'rgba(46, 94, 138, 0.08)', '200': 'rgba(46, 94, 138, 0.15)', '300': 'rgba(46, 94, 138, 0.25)', '400': 'rgba(46, 94, 138, 0.4)', '500': '#2e5e8a', '600': '#234a6d', '700': 'rgba(35, 74, 109, 0.8)', '800': 'rgba(35, 74, 109, 0.9)' }, - purple: { '50': 'rgba(126, 176, 155, 0.04)', '100': 'rgba(126, 176, 155, 0.08)', '200': 'rgba(126, 176, 155, 0.15)', '300': 'rgba(126, 176, 155, 0.25)', '400': 'rgba(126, 176, 155, 0.4)', '500': '#7eb09b', '600': '#5a9078', '700': 'rgba(90, 144, 120, 0.8)', '800': 'rgba(90, 144, 120, 0.9)' }, - semantic: { success: '#52ad5a', warning: '#f0a020', error: '#c8102e', info: '#2e5e8a', highlight: '#f0a020', highlightBg: 'rgba(240, 160, 32, 0.12)' }, - border: { subtle: 'rgba(106, 92, 70, 0.12)', base: 'rgba(106, 92, 70, 0.20)', medium: 'rgba(106, 92, 70, 0.28)', strong: 'rgba(106, 92, 70, 0.36)', prominent: 'rgba(106, 92, 70, 0.48)' }, - element: { subtle: 'rgba(46, 94, 138, 0.03)', soft: 'rgba(46, 94, 138, 0.06)', base: 'rgba(46, 94, 138, 0.10)', medium: 'rgba(46, 94, 138, 0.14)', strong: 'rgba(46, 94, 138, 0.18)', elevated: 'rgba(255, 255, 255, 0.85)' }, + background: { primary: '#faf8f0', secondary: '#f5f3e8' }, + accentRgb: '46, 94, 138', + accent500: '#2e5e8a', + accent600: '#234a6d', + semantic: { + success: '#52ad5a', + warning: '#f0a020', + error: '#c8102e', }, - }, - { + }), + createInstallerTheme({ id: 'bitfun-china-night', name: 'Ink Night', type: 'dark', - colors: { - background: { primary: '#1a1814', secondary: '#212019', tertiary: '#262420', quaternary: '#2d2926', elevated: '#2d2926', workbench: '#1a1814', flowchat: '#1a1814', tooltip: 'rgba(26, 24, 20, 0.95)' }, - text: { primary: '#e8e6e1', secondary: '#c5c3be', muted: '#928f89', disabled: '#5f5d59' }, - accent: { '50': 'rgba(115, 165, 204, 0.04)', '100': 'rgba(115, 165, 204, 0.08)', '200': 'rgba(115, 165, 204, 0.15)', '300': 'rgba(115, 165, 204, 0.25)', '400': 'rgba(115, 165, 204, 0.4)', '500': '#73a5cc', '600': '#5a8bb3', '700': 'rgba(90, 139, 179, 0.8)', '800': 'rgba(90, 139, 179, 0.9)' }, - purple: { '50': 'rgba(150, 198, 180, 0.04)', '100': 'rgba(150, 198, 180, 0.08)', '200': 'rgba(150, 198, 180, 0.15)', '300': 'rgba(150, 198, 180, 0.25)', '400': 'rgba(150, 198, 180, 0.4)', '500': '#96c6b4', '600': '#7aab98', '700': 'rgba(122, 171, 152, 0.8)', '800': 'rgba(122, 171, 152, 0.9)' }, - semantic: { success: '#6bc072', warning: '#f5b555', error: '#e85555', info: '#73a5cc', highlight: '#e6a84a', highlightBg: 'rgba(230, 168, 74, 0.15)' }, - border: { subtle: 'rgba(232, 230, 225, 0.10)', base: 'rgba(232, 230, 225, 0.16)', medium: 'rgba(232, 230, 225, 0.22)', strong: 'rgba(232, 230, 225, 0.28)', prominent: 'rgba(232, 230, 225, 0.38)' }, - element: { subtle: 'rgba(115, 165, 204, 0.06)', soft: 'rgba(115, 165, 204, 0.09)', base: 'rgba(115, 165, 204, 0.12)', medium: 'rgba(115, 165, 204, 0.16)', strong: 'rgba(115, 165, 204, 0.20)', elevated: 'rgba(45, 41, 38, 0.95)' }, + background: { primary: '#1a1814', secondary: DARK_CARD_SURFACE }, + accentRgb: '115, 165, 204', + accent500: '#73a5cc', + accent600: '#5a8bb3', + semantic: { + success: '#6bc072', + warning: '#f5b555', + error: '#e85555', }, - }, - { + }), + createInstallerTheme({ id: 'bitfun-cyber', name: 'Cyber', type: 'dark', - colors: { - background: { primary: '#101010', secondary: '#151515', tertiary: '#1a1a1a', quaternary: '#1f1f1f', elevated: '#0d0d0d', workbench: '#101010', flowchat: '#101010', tooltip: 'rgba(16, 16, 16, 0.95)' }, - text: { primary: '#e0f2ff', secondary: '#c7e7ff', muted: '#7fadcc', disabled: '#4a5a66' }, - accent: { '50': 'rgba(0, 230, 255, 0.05)', '100': 'rgba(0, 230, 255, 0.1)', '200': 'rgba(0, 230, 255, 0.18)', '300': 'rgba(0, 230, 255, 0.3)', '400': 'rgba(0, 230, 255, 0.45)', '500': '#00e6ff', '600': '#00ccff', '700': 'rgba(0, 204, 255, 0.85)', '800': 'rgba(0, 204, 255, 0.95)' }, - purple: { '50': 'rgba(138, 43, 226, 0.05)', '100': 'rgba(138, 43, 226, 0.1)', '200': 'rgba(138, 43, 226, 0.18)', '300': 'rgba(138, 43, 226, 0.3)', '400': 'rgba(138, 43, 226, 0.45)', '500': '#8a2be2', '600': '#7928ca', '700': 'rgba(121, 40, 202, 0.85)', '800': 'rgba(121, 40, 202, 0.95)' }, - semantic: { success: '#00ff9f', warning: '#ffcc00', error: '#ff0055', info: '#00e6ff', highlight: '#ffdd44', highlightBg: 'rgba(255, 221, 68, 0.15)' }, - border: { subtle: 'rgba(0, 230, 255, 0.14)', base: 'rgba(0, 230, 255, 0.20)', medium: 'rgba(0, 230, 255, 0.28)', strong: 'rgba(0, 230, 255, 0.36)', prominent: 'rgba(0, 230, 255, 0.50)' }, - element: { subtle: 'rgba(0, 230, 255, 0.06)', soft: 'rgba(0, 230, 255, 0.09)', base: 'rgba(0, 230, 255, 0.13)', medium: 'rgba(0, 230, 255, 0.17)', strong: 'rgba(0, 230, 255, 0.22)', elevated: 'rgba(0, 230, 255, 0.27)' }, + background: { primary: '#0e0e10', secondary: DARK_CARD_SURFACE }, + accentRgb: '0, 230, 255', + accent500: '#00e6ff', + accent600: '#00ccff', + semantic: { + success: '#00ff9f', + warning: '#ffcc00', + error: '#ff0055', }, - }, - { + }), + createInstallerTheme({ id: 'bitfun-tokyo-night', name: 'Tokyo Night', type: 'dark', - colors: { - background: { primary: '#1a1b26', secondary: '#16161e', tertiary: '#14141b', quaternary: '#1e202e', elevated: '#20222c', workbench: '#16161e', flowchat: '#1a1b26', tooltip: 'rgba(22, 22, 30, 0.94)' }, - text: { primary: '#c0caf5', secondary: '#a9b1d6', muted: '#787c99', disabled: '#545c7e' }, - accent: { '50': 'rgba(122, 162, 247, 0.05)', '100': 'rgba(122, 162, 247, 0.08)', '200': 'rgba(122, 162, 247, 0.15)', '300': 'rgba(122, 162, 247, 0.25)', '400': 'rgba(122, 162, 247, 0.4)', '500': '#7aa2f7', '600': '#6183bb', '700': 'rgba(97, 131, 187, 0.85)', '800': 'rgba(97, 131, 187, 0.95)' }, - purple: { '50': 'rgba(187, 154, 247, 0.05)', '100': 'rgba(187, 154, 247, 0.08)', '200': 'rgba(187, 154, 247, 0.15)', '300': 'rgba(187, 154, 247, 0.25)', '400': 'rgba(187, 154, 247, 0.4)', '500': '#bb9af7', '600': '#9d7cd8', '700': 'rgba(157, 124, 216, 0.85)', '800': 'rgba(157, 124, 216, 0.95)' }, - semantic: { success: '#9ece6a', warning: '#e0af68', error: '#f7768e', info: '#7dcfff', highlight: '#e0af68', highlightBg: 'rgba(224, 175, 104, 0.15)' }, - border: { subtle: 'rgba(54, 59, 84, 0.45)', base: 'rgba(54, 59, 84, 0.6)', medium: 'rgba(54, 59, 84, 0.72)', strong: 'rgba(54, 59, 84, 0.85)', prominent: 'rgba(122, 162, 247, 0.45)' }, - element: { subtle: 'rgba(122, 162, 247, 0.06)', soft: 'rgba(122, 162, 247, 0.08)', base: 'rgba(122, 162, 247, 0.11)', medium: 'rgba(122, 162, 247, 0.14)', strong: 'rgba(122, 162, 247, 0.18)', elevated: 'rgba(122, 162, 247, 0.22)' }, + background: { primary: '#1a1b26', secondary: DARK_CARD_SURFACE }, + accentRgb: '122, 162, 247', + accent500: '#7aa2f7', + accent600: '#6183bb', + semantic: { + success: '#9ece6a', + warning: '#e0af68', + error: '#f7768e', }, - }, - { + }), + createInstallerTheme({ id: 'bitfun-slate', name: 'Slate', type: 'dark', - colors: { - background: { primary: '#1a1c1e', secondary: '#1a1c1e', tertiary: '#1a1c1e', quaternary: '#32363a', elevated: '#1a1c1e', workbench: '#1a1c1e', flowchat: '#1a1c1e', tooltip: 'rgba(42, 45, 48, 0.96)' }, - text: { primary: '#eef0f3', secondary: '#c8ccd2', muted: '#9ea4ab', disabled: '#65696f' }, - accent: { '50': 'rgba(122, 176, 238, 0.04)', '100': 'rgba(122, 176, 238, 0.08)', '200': 'rgba(122, 176, 238, 0.15)', '300': 'rgba(122, 176, 238, 0.25)', '400': 'rgba(122, 176, 238, 0.4)', '500': '#7ab0ee', '600': '#689ad8', '700': 'rgba(104, 154, 216, 0.8)', '800': 'rgba(104, 154, 216, 0.9)' }, - purple: { '50': 'rgba(184, 198, 255, 0.04)', '100': 'rgba(184, 198, 255, 0.08)', '200': 'rgba(184, 198, 255, 0.15)', '300': 'rgba(184, 198, 255, 0.25)', '400': 'rgba(184, 198, 255, 0.4)', '500': '#b8c4ff', '600': '#9dacf5', '700': 'rgba(157, 172, 245, 0.8)', '800': 'rgba(157, 172, 245, 0.9)' }, - semantic: { success: '#7fb899', warning: '#d4a574', error: '#c9878d', info: '#7ab0ee', highlight: '#e2e4e7', highlightBg: 'rgba(212, 214, 216, 0.12)' }, - border: { subtle: 'rgba(255, 255, 255, 0.12)', base: 'rgba(255, 255, 255, 0.18)', medium: 'rgba(255, 255, 255, 0.24)', strong: 'rgba(255, 255, 255, 0.32)', prominent: 'rgba(255, 255, 255, 0.45)' }, - element: { subtle: 'rgba(255, 255, 255, 0.06)', soft: 'rgba(255, 255, 255, 0.10)', base: 'rgba(255, 255, 255, 0.13)', medium: 'rgba(255, 255, 255, 0.17)', strong: 'rgba(255, 255, 255, 0.21)', elevated: 'rgba(255, 255, 255, 0.25)' }, + background: { primary: DARK_CARD_SURFACE }, + accentRgb: '122, 176, 238', + accent500: '#7ab0ee', + accent600: '#689ad8', + semantic: { + success: '#7eb09b', + warning: '#f59e0b', + error: '#c9878d', }, - }, + }), ]; export const THEME_DISPLAY_ORDER: ThemeId[] = [ @@ -176,7 +284,7 @@ export const THEME_DISPLAY_ORDER: ThemeId[] = [ ]; export function findInstallerThemeById(id: ThemeId): InstallerTheme { - return THEMES.find((t) => t.id === id) - ?? THEMES.find((t) => t.id === 'bitfun-light') + return THEMES.find((theme) => theme.id === id) + ?? THEMES.find((theme) => theme.id === 'bitfun-light') ?? THEMES[0]; } diff --git a/BitFun-Installer/src/types/installer.ts b/BitFun-Installer/src/types/installer.ts index 07343176c1..646890aa7f 100644 --- a/BitFun-Installer/src/types/installer.ts +++ b/BitFun-Installer/src/types/installer.ts @@ -56,7 +56,12 @@ export interface ModelConfig { } /** Matches backend `ConnectionTestMessageCode` (camelCase JSON). */ -export type ConnectionTestMessageCode = 'toolCallsNotDetected' | 'imageInputCheckFailed'; +export type ConnectionTestMessageCode = + | 'toolCallsNotDetected' + | 'imageInputCheckFailed' + | 'tlsOrCertificateIssue' + | 'proxyIssue' + | 'networkIssue'; export interface ConnectionTestResult { success: boolean; diff --git a/Cargo.toml b/Cargo.toml index 30d1f629d6..fc651be44d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -35,7 +35,7 @@ resolver = "2" # Shared package metadata — single source of truth for version [workspace.package] -version = "0.2.10" # x-release-please-version +version = "0.2.12" # x-release-please-version authors = ["BitFun Team"] edition = "2021" @@ -161,6 +161,7 @@ atspi = "0.29" leptess = "0.14" core-foundation = "0.9" core-graphics = { version = "0.23", features = ["elcapitan", "highsierra"] } +foreign-types = "0.5" dispatch = "0.2" block2 = "0.6" objc2 = "0.6" diff --git a/README.md b/README.md index c55525a020..e5df2b0fdc 100644 --- a/README.md +++ b/README.md @@ -7,6 +7,8 @@
+[![Trendshift](https://trendshift.io/api/badge/repositories/44672)](https://trendshift.io/repositories/44672) + [![GitHub release](https://img.shields.io/github/v/release/GCWing/BitFun?style=flat-square&color=blue)](https://github.com/GCWing/BitFun/releases) [![Website](https://img.shields.io/badge/Website-openbitfun.com-6f42c1?style=flat-square)](https://openbitfun.com/) [![License: MIT](https://img.shields.io/badge/License-MIT-yellow?style=flat-square)](https://github.com/GCWing/BitFun/blob/main/LICENSE) diff --git a/README.zh-CN.md b/README.zh-CN.md index d78961b5e5..b7853bc78b 100644 --- a/README.zh-CN.md +++ b/README.zh-CN.md @@ -7,6 +7,8 @@
+[![Trendshift](https://trendshift.io/api/badge/repositories/44672)](https://trendshift.io/repositories/44672) + [![GitHub release](https://img.shields.io/github/v/release/GCWing/BitFun?style=flat-square&color=blue)](https://github.com/GCWing/BitFun/releases) [![Website](https://img.shields.io/badge/Website-openbitfun.com-6f42c1?style=flat-square)](https://openbitfun.com/) [![License: MIT](https://img.shields.io/badge/License-MIT-yellow?style=flat-square)](https://github.com/GCWing/BitFun/blob/main/LICENSE) diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 0000000000..b817b1b6e8 --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,59 @@ +# Security Policy + +[中文版](./SECURITY_CN.md) + +BitFun is a desktop-grade Agent runtime (Rust core + Tauri shell) that runs on your own machine with broad capabilities—filesystem, terminal, Git, MCP/LSP, and remote control. Because of this reach, we take security reports seriously and appreciate the community's help in keeping users safe. + +## Supported Versions + +BitFun is currently in active `0.x` development and ships as a rolling release. Security fixes land on the latest release; older versions are not patched separately. + +| Version | Supported | +| ------- | --------- | +| Latest release (`main`) | ✅ | +| Older releases | ❌ | + +Please upgrade to the latest [release](https://github.com/GCWing/BitFun/releases) before reporting an issue to confirm it still reproduces. + +## Reporting a Vulnerability + +**Please do not open a public issue, discussion, or pull request for security vulnerabilities.** Public disclosure before a fix is available puts users at risk. + +Instead, report privately through GitHub Security Advisories: + +➡️ **[Report a vulnerability](https://github.com/GCWing/BitFun/security/advisories/new)** + +This opens a private channel visible only to the maintainers. If you are unable to use GitHub Security Advisories, open a minimal public issue that says only "I'd like to report a security issue privately"—without any details—and a maintainer will follow up with a private channel. + +To help us triage quickly, please include where you can: + +- A clear description of the vulnerability and its impact +- The affected component (Rust core, desktop/Tauri, web UI, mobile-web pairing, server/relay, CLI, installer, etc.) +- Step-by-step reproduction instructions or a proof of concept +- Affected version(s), operating system, and configuration +- Any suggested mitigation or fix, if you have one + +## Disclosure Process + +- We aim to acknowledge new reports within **5 business days**. +- We will work with you to confirm the issue, assess severity, and determine a fix timeline, keeping you updated on progress. +- Once a fix is released, we will publish a security advisory and credit the reporter unless you prefer to remain anonymous. +- We follow coordinated disclosure: please give us a reasonable window to ship a fix before any public disclosure. + +## Scope + +In scope: + +- The BitFun runtime, official Agents, desktop/CLI/server apps, web UI, and the mobile-web pairing/remote-control flow in this repository. + +Out of scope: + +- Issues in third-party dependencies (please report those upstream; let us know if a BitFun update is needed). +- Vulnerabilities that require a pre-compromised machine, physical access, or already-elevated privileges. +- Risks inherent to running an autonomous Agent with capabilities you explicitly grant it (e.g., a tool you authorized acting within its granted permissions). + +## Safe Harbor + +We will not pursue or support legal action against researchers who, in good faith, discover and report vulnerabilities in accordance with this policy and who avoid privacy violations, data destruction, and service disruption during testing. + +Thank you for helping keep BitFun and its users safe. diff --git a/SECURITY_CN.md b/SECURITY_CN.md new file mode 100644 index 0000000000..900186ea55 --- /dev/null +++ b/SECURITY_CN.md @@ -0,0 +1,59 @@ +# 安全策略 + +[English](./SECURITY.md) + +BitFun 是一个运行在你本机的桌面级 Agent 运行时(Rust 内核 + Tauri 外壳),具备文件系统、终端、Git、MCP/LSP 以及远程控制等广泛能力。正因为权限范围较大,我们非常重视安全问题,并感谢社区帮助我们保护用户安全。 + +## 受支持的版本 + +BitFun 目前处于 `0.x` 活跃开发阶段,采用滚动发布。安全修复只会合入最新发布版本,旧版本不会单独打补丁。 + +| 版本 | 是否支持 | +| ---- | -------- | +| 最新发布版(`main`) | ✅ | +| 较旧的发布版 | ❌ | + +在上报问题前,请先升级到最新[发布版](https://github.com/GCWing/BitFun/releases),确认问题仍可复现。 + +## 如何上报漏洞 + +**请不要通过公开的 Issue、Discussion 或 Pull Request 来上报安全漏洞。** 在修复发布之前公开披露会让用户面临风险。 + +请通过 GitHub Security Advisories 私密上报: + +➡️ **[上报漏洞](https://github.com/GCWing/BitFun/security/advisories/new)** + +该入口会开启一个仅维护者可见的私密渠道。如果你无法使用 GitHub Security Advisories,可以新建一个极简的公开 Issue,仅写明「我想私下上报一个安全问题」——不要附带任何细节——维护者会与你建立私密沟通渠道。 + +为便于我们快速分诊,请尽量提供以下信息: + +- 漏洞的清晰描述及其影响 +- 受影响的组件(Rust 内核、桌面端/Tauri、Web UI、移动端配对、服务端/中继、CLI、安装器等) +- 复现步骤或概念验证(PoC) +- 受影响的版本、操作系统及相关配置 +- 如有缓解措施或修复建议,也欢迎一并提供 + +## 披露流程 + +- 我们会争取在 **5 个工作日内**确认收到新的上报。 +- 我们会与你一起确认问题、评估严重程度并确定修复时间线,并持续向你同步进展。 +- 修复发布后,我们会发布安全公告,并在你愿意的情况下致谢上报者(若你希望匿名,我们将予以尊重)。 +- 我们遵循协调披露原则:请在公开披露前给我们留出合理的修复窗口期。 + +## 适用范围 + +适用范围内: + +- 本仓库中的 BitFun 运行时、官方 Agent、桌面端/CLI/服务端应用、Web UI,以及移动端配对/远程控制流程。 + +不在适用范围内: + +- 第三方依赖中的问题(请向上游上报;如需 BitFun 侧更新请告知我们)。 +- 需要事先攻陷机器、物理访问或已提权环境才能触发的漏洞。 +- 运行自主 Agent 时由你明确授予的能力所固有的风险(例如某个你已授权的工具在其权限范围内执行操作)。 + +## 安全港(Safe Harbor) + +对于善意发现并按照本策略上报漏洞,且在测试过程中避免侵犯隐私、破坏数据与中断服务的研究者,我们不会对其采取或支持任何法律行动。 + +感谢你帮助保障 BitFun 及其用户的安全。 diff --git a/docs/architecture/agent-runtime-services-design.md b/docs/architecture/agent-runtime-services-design.md index 84de76283d..c0ea18968a 100644 --- a/docs/architecture/agent-runtime-services-design.md +++ b/docs/architecture/agent-runtime-services-design.md @@ -1,16 +1,19 @@ -# Agent Runtime SDK 与 Runtime Services 设计 +# Agent Kernel、Runtime Services 与 Extension API 设计 -本文是 [`core-decomposition.md`](core-decomposition.md) 的开发设计文档,描述目标模块、 -接口、crate 内部结构和行为保护。本文只记录设计约束,不记录实现过程或验证记录。 +本文是 [`core-decomposition.md`](core-decomposition.md) 的开发设计文档,描述目标模块、接口、 +crate 内部结构和行为保护。本文只记录设计约束,不记录实现过程或验证记录。插件运行时主机、 +生态兼容适配层、IPC 和候选效果契约见 +[`plugin-runtime-host-design.md`](plugin-runtime-host-design.md)。 ## 1. 设计目标与边界 -- Agent Runtime SDK 可被 Desktop、CLI、Server、Remote、ACP 等产品形态嵌入。 -- Agent Runtime SDK 对外提供稳定、窄口径的 runtime API,而不是暴露 `bitfun-core`、产品命令路径或 concrete manager。 -- Runtime 不感知平台差异、工具实现差异和构建形态差异。 -- Tool 使用通用接口和 provider group 注册,不绑定底层实现。 -- 具体 adapter 与 service 实现由上层 Product Assembly 注入。 -- Harness 可扩展,新增 SDD 等工作流不侵入 runtime kernel。 +- Agent Kernel 可被 Desktop、CLI、Server、Remote、ACP、Web 和独立 SDK 形态嵌入。 +- Agent Kernel 对外提供稳定、窄口径的 Rust runtime API,而不是暴露 `bitfun-core`、产品命令路径或 concrete manager。 +- Product Feature 把内核能力组装为用户侧能力,可能同时触达 Rust 和 UI,但不拥有内核状态机或平台实现。 +- Product API 同时包含 Rust Kernel API 与 UI Extension Contract;OpenCode / ACP / plugin adapter 仅承担映射和注册。 +- Agent Kernel 不感知平台差异、工具实现差异、UI host 差异和构建形态差异。 +- Tool、Skill、MCP、Harness 和 extension 使用通用接口和 provider / contribution 注册,不绑定底层实现。 +- 具体 service、UI host、Plugin Runtime Host binding 和 adapter manifest set 由上层 Product Assembly 注入。 - 每个 crate 只依赖最小稳定集合,依赖方向可检查。 ### 1.1 SDK 发布边界 @@ -37,93 +40,100 @@ Agent Runtime SDK 的发布边界以调用方能力为准,而不是以物理 c - 完整产品能力只能通过 Product Assembly 或兼容 `bitfun-core/product-full` 组装,不反向污染 SDK API。 SDK 公共 API 以 `AGENT_RUNTIME_SDK_API_VERSION` 标记兼容边界。当前 API version 为 v1 preview: -小版本更新可以增加可选 builder hook、DTO 字段或 registry 查询能力,但不得改变既有端口语义、 +小版本更新允许增加可选 builder hook、DTO 字段或 registry 查询能力,但不得改变既有端口语义、 错误分类、session / turn 标识含义或默认 feature 依赖。任何需要调用方改写现有嵌入代码的变更, 必须提升 API version 并提供兼容迁移路径。 只要外部调用方仍必须导入 `bitfun-core`、启用 `product-full`、持有 concrete service manager、读取产品命令 registry 或依赖全局 mutable state,SDK 发布边界就不成立。 -### 1.2 crate 划分 +### 1.2 内核与特性的分界 -```text -bitfun-core-types -bitfun-events -bitfun-runtime-ports -bitfun-runtime-services # typed service bundle / capability availability -tool-contracts # Cargo package: bitfun-agent-tools -tool-provider-groups # Cargo package: bitfun-tool-packs -tool-execution # Cargo package: tool-runtime -bitfun-agent-runtime # agent kernel contracts and portable runtime decisions -bitfun-harness # workflow descriptor / provider / registry contracts -bitfun-services-core -bitfun-services-integrations -bitfun-product-domains -bitfun-acp -bitfun-core -apps/* -``` +内核能力和产品特性必须分开判断: -目标依赖: +| 领域 | 属于内核 | 属于产品特性 | +|---|---|---| +| 长程任务 | task identity、queue、resume、cancel、event、persistence facts | `/goal` 命令、默认目标模板、UI 展示、设置项和产品文案 | +| 权限 | permission facts、source identity、decision request、audit event | 桌面弹窗、CLI 提示、Web 状态投影和产品默认选项 | +| 上下文 | session/workspace facts、context assembly contract、memory port | 具体入口的上下文展示、快捷命令和 feature 默认配置 | +| 模型调度 | provider-neutral model route request、usage/cost/cache facts | 产品形态默认模型、设置入口和降级文案 | +| Hook / Event | event schema、hook order、timeout、error policy | 哪些 feature 注册 hook、UI 如何展示 hook 结果 | -```text -apps/* - -> bitfun-core 或 Product Assembly crate - -> 按需依赖 bitfun-acp / transport / api-layer - -Product Assembly - -> product capability packs - -> bitfun-agent-runtime - -> bitfun-harness - -> tool-contracts / tool-provider-groups / tool-execution - -> bitfun-runtime-services - -> adapters / services - -Product Capability packs - -> bitfun-harness - -> bitfun-agent-runtime - -> tool-provider-groups - -> bitfun-product-domains +判断标准: -bitfun-agent-runtime - -> bitfun-runtime-ports - -> bitfun-events - -> bitfun-agent-stream - -> tool-contracts - -> bitfun-runtime-services +- 在 Desktop、CLI、Web、ACP 和 SDK 中都可复用,且不依赖 UI 或平台 concrete 的能力,优先归 Agent Kernel。 +- 会改变用户入口、命令、设置、UI contribution、默认策略或产品文案的能力,归 Product Feature。 +- 会接触 OS、network、terminal、filesystem、remote host、MCP server 或 AI provider concrete 的能力,归 Cross-platform + Adapter 或 protocol adapter。 +- 来自外部插件、OpenCode、ACP、external skill 或第三方 package 的能力,先进入 Extension Layer,再由 Product + Assembly 注册到 feature / kernel / execution 的稳定接口。 -tool-execution - -> tool-contracts - -> bitfun-runtime-ports - -> bitfun-events +### 1.3 Product API 与 Extension API -bitfun-runtime-services - -> bitfun-runtime-ports - -> bitfun-core-types / bitfun-events(仅当 service DTO 或 event contract 需要时引入) - -adapters / services - -> bitfun-runtime-ports - -> bitfun-core-types - -> 允许的 third-party 依赖 - -> External Systems -``` +Product API 不应被定义为单一 Rust 后端 API。目标 API 分为三类: + +| API | 作用 | 主要消费者 | 约束 | +|---|---|---|---| +| Rust Kernel API | 创建 runtime、提交 turn、消费 event、注册 ports/providers/hooks | Product Assembly、SDK、extension adapter | 不暴露 `bitfun-core/product-full` 或 concrete manager | +| UI Extension Contract | 描述 UI contribution、命令入口、设置项、panel、状态投影 | Web UI、Desktop UI、插件适配层 | 只传 descriptor 和 stable DTO,不 import React/Tauri implementation | +| Capability / Effect API | 声明 tool、MCP、plugin、hook、network、file、shell 等能力和副作用 | Plugin Runtime Host、Security Boundary、Product Assembly | 未声明或超声明的能力默认受限 | + +OpenCode adapter、ACP bridge 和未来 plugin runtime 必须把外部 API 映射为上述三类 API,再由 Product Assembly +统一注册。它们不能直接写 Agent Kernel 权威状态,也不能绕过 permission、sandbox、audit 或 UI host 的渲染边界。 + +Agentic frontend event projection 属于稳定 event contract:Agent Kernel 产生 provider-neutral `AgenticEvent`, +契约层给出 event name、event type 和 payload;Tauri、WebSocket、OpenCode adapter 或 UI host +只选择 delivery/profile,不重新定义字段映射。 + +Extension registration contract 属于稳定扩展契约,不属于 Product Assembly 的具体实现。Plugin Runtime Host 和 +compatibility adapter 只产出可注册的 descriptor、provider 和 contribution;Product Assembly 消费这些 contract 并完成 +产品形态内的注册,避免 Extension 层反向依赖 assembly crate。 + +### 1.4 API 与 crate 边界 + +本设计按 API owner 划分 crate,而不是按调用方或产品形态划分。一个 crate 只能拥有一类稳定边界;如果同一文件同时 +处理 UI 入口、产品策略、内核状态和 OS I/O,应拆到对应 owner。 + +| API / owner | 主要 crate | 允许依赖 | 不允许依赖 | 对外承诺 | +|---|---|---|---|---| +| Product Assembly API | `src/crates/assembly/*` | feature packs、Kernel API、Execution API、Runtime Services、platform providers | Agent 内部状态机、具体 UI 组件实现作为下层依赖 | 按产品形态组装能力,输出 typed runtime parts | +| Product Feature API | `product-capabilities`、`product-domains`、对应 UI contribution owner | Kernel API、UI Extension Contract、Capability/Effect contract、domain contract | OS concrete、Tauri handle、Execution concrete、permission 最终策略 | 把内核能力和稳定 descriptor 映射为用户功能和默认策略 | +| Rust Kernel API | `agent-runtime`、`agent-stream`、`runtime-services`、`runtime-ports`、`events`、`core-types` | stable contracts、tool/harness registry、typed services | `bitfun-core`、Tauri、Web UI、ACP protocol、provider concrete | session / turn / event / permission / scheduler / context 等 SDK 候选接口 | +| Execution API | `tool-contracts`、`tool-provider-groups`、`tool-execution`、`harness` | stable contracts、runtime ports、注入的 service ports | product registry、UI、具体 filesystem/Git/terminal/MCP client | tool、skills、MCP tool bridge、sandbox、harness 执行语义 | +| Extension API | Plugin Runtime Host / OpenCode compatibility / ACP adapter owner | Rust Kernel API contract、UI Extension Contract、Capability/Effect contract | Web UI React implementation、Tauri state、kernel 权威状态写入 | 把外部生态能力转换为 descriptor、provider 和 candidate effect | +| Platform / Provider Adapter API | `services/*`、`adapters/*`、app-local provider | runtime ports、stable DTO、允许的第三方库 | Product Feature、Agent Kernel 状态机、UI command | 实现 filesystem、terminal、network、remote、Git、MCP transport、AI provider 等边界外 I/O | +| Stable Contract API | `contracts/*` | 低层无行为依赖或标准序列化依赖 | 上层 crate、concrete manager、UI rendering | DTO、event、port、capability/effect、permission、sandbox、audit、typed error | 禁止依赖: -- `bitfun-runtime-ports` -> `bitfun-core` -- `tool-contracts` -> 具体 service crate -- `tool-execution` -> 产品 registry / permission policy / 具体 tool 实现 crate -- `bitfun-agent-runtime` -> `bitfun-core` -- `bitfun-agent-runtime` -> Tauri / CLI / ACP protocol / Web UI -- `bitfun-harness` -> 具体 filesystem / Git / terminal manager +- `contracts/*` 或 `runtime-ports` 依赖 `bitfun-core`、assembly、apps、UI 或 concrete service。 +- `agent-runtime` 依赖 `bitfun-core`、Tauri、Web UI、ACP protocol、AI provider concrete、MCP client concrete 或 OS service manager。 +- `tool-contracts` 依赖具体 service crate;`tool-execution` 依赖产品 registry、产品 permission policy 或具体 UI。 +- `harness` 依赖具体 filesystem/Git/terminal manager;它只通过 ports 和 provider contract 获取能力。 +- Plugin Runtime Host 不能依赖 Web UI React component implementation、Tauri app state 或 concrete core manager。 +- Product Feature 直接依赖 platform adapter concrete、execution concrete、全局 mutable runtime state 或边界外资源 client。 + +接口暴露原则: -目标 crate 创建或继续扩展准入: +- 对外 API 按层拆分:Rust Kernel API、UI Extension Contract、Capability/Effect API、Product Assembly API 分别定义。 +- 下层不暴露上层对象。Kernel 不返回 UI command;Execution 不返回 React descriptor 以外的 UI 实现;Platform Adapter 不返回产品命令。 +- 注册接口接收 typed provider / descriptor / policy,不接收 `Any`、无类型 service name 或全局 mutable registry。 +- 兼容 facade 可以保留旧路径导出,但旧路径不得成为新 API 的真实 owner。 -- 只有当 owner 边界、旧路径兼容、focused tests、依赖收益和 boundary check 都能同时落地时,才创建新的目标 crate。 -- `bitfun-runtime-services` 的扩展必须保持 typed builder、本地 service、remote service 和 fake provider 三类注入路径可测试。 -- `bitfun-agent-runtime` 的扩展必须保持旧路径 facade、focused tests 和 boundary check,且不得吸收 concrete service、product surface 或平台实现。 -- `bitfun-harness` 的扩展必须保持 descriptor / registry、旧路径兼容、focused tests 和 boundary check,且不得把 provider 注册误写成 concrete workflow execution。 -- 若目标 crate 只能承接单个 helper 或只能通过 `bitfun-core` 才能测试,应继续留在初始兼容 facade,不提前拆 crate。 +### 1.5 平台适配与边界外资源 + +Platform / Provider Adapter 是仓库内实现层,负责把稳定 port 转换为 OS、network、terminal、remote、MCP +transport、AI provider、browser runtime 或第三方库调用。边界外资源不是 crate、不是逻辑层,也不是所有模块可依赖的 +基础设施。 + +实现规则: + +- Product Assembly 是唯一可以选择具体 platform provider 的位置;选择结果以 typed runtime parts 注入。 +- Kernel、Execution、Extension 和 Product Feature 只消费 stable contract、port handle 或 descriptor,不导入具体 + provider crate。 +- Platform adapter 不读取 delivery profile、feature pack 或 UI command;形态差异由 Product Assembly 注入。 +- 外部资源错误必须在 adapter 边界转换为 typed error、unsupported/unavailable 或 capability/effect fact,不能泄漏为 + 产品层专用分支。 ## 2. 稳定接口与运行时服务 @@ -261,9 +271,50 @@ Remote ports 的边界: - 不在 runtime services 中创建 concrete manager;创建发生在 Product Assembly。 - `RuntimeServices` 是运行时依赖集合,不是全局 mutable app state。 -## 3. Runtime / Tool / Harness 内核 +### 2.3 安全控制面接口 + +安全控制面把 tool、MCP、skills、plugin、hook、shell、network、file、browser/desktop 和 remote 动作归一为 +capability/effect/security decision。它跨越 Kernel、Execution、Extension、Cross-platform Adapter 和 UI projection, +但最终决策必须由 Product Assembly 注入的确定性策略实现和内核事实共同约束。该接口定义的是跨层 contract, +不是 contracts crate 内部的具体策略实现。 + +建议接口: -### 3.1 Agent Runtime SDK +```rust +pub struct CapabilityEffectDeclaration { + pub capability: CapabilityId, + pub source: CapabilitySource, + pub targets: Vec, + pub data_classes: Vec, + pub side_effects: Vec, + pub execution_domain: ExecutionDomain, +} + +pub struct SecurityDecisionRequest { + pub session: SessionIdentity, + pub turn: Option, + pub agent: AgentIdentity, + pub source: CapabilitySource, + pub effect: CapabilityEffectDeclaration, + pub proposed_action: ProposedAction, +} + +pub trait SecurityDecisionPort: Send + Sync { + fn decide(&self, request: SecurityDecisionRequest) -> SecurityDecisionFuture; +} +``` + +约束: + +- UI 只展示 decision 和 user options,不成为最终授权来源。 +- Plugin Runtime Host 必须声明 capability/effect,未知或超声明能力默认受限。 +- `allow_in_sandbox` 只能在实际 sandbox 或隔离路径存在时返回。 +- 远程、ACP、MCP、plugin、browser/desktop 和 cloud task 必须携带 execution domain。 +- 模型输出只能辅助解释和候选判断,不能直接写 permission、audit 或 policy state。 + +## 3. Kernel / Tool / Harness 内核 + +### 3.1 Agent Kernel / Runtime SDK 目标 owner crate:`bitfun-agent-runtime`。 @@ -271,13 +322,17 @@ Remote ports 的边界: - session 生命周期。 - dialog turn / model round 生命周期。 +- long-running task 生命周期、resume/checkpoint fact 和 result delivery。 - scheduler / queue / cancellation。 +- permission coordination 和安全事实投递。 +- model routing / usage / cost / cache facts。 - prompt loop 和 context assembly。 - prompt cache 协调。 +- memory / workspace facts。 +- DFX / telemetry / audit facts。 - agent definition registry、subagent registry 查询和 delegation policy。 - fork context seeding。 - tool call 调度。 -- permission 协调。 - runtime events。 - post-turn processor。 @@ -327,18 +382,28 @@ impl AgentRuntime { 该 facade 是目标 API 形态。它必须只接收已组装的 typed parts,不负责创建 filesystem、terminal、MCP、AI client、Remote provider 或产品命令。 -当前 v1 preview API 以 message / attachment / metadata 作为最小输入形态;若后续需要把 +当前 v1 preview API 以 message / attachment / metadata 作为最小输入形态;若把 model-round cancellation token、structured AgentInput 或更复杂的 event cursor 纳入公开 SDK, 必须提升 SDK API version 并保留旧路径兼容。 -旧路径兼容约束: +产品特性边界: + +- `/goal`、slash command、输入框按钮、设置项、UI panel 和默认文案不进入 Agent Kernel。 +- 内核只提供 goal / long-running task 所需的任务 identity、生命周期、队列、resume/cancel、event 和 persistence fact。 +- Product Feature 负责把这些内核事实映射为 `/goal` 命令、可见状态、快捷操作和默认策略。 +- 若某个 feature 需要修改 Rust 和 UI,必须以 feature pack 同时声明 Rust runtime request、UI contribution 和 + capability/effect,不得仅在单侧隐式扩展。 + +兼容边界: - `bitfun-agent-runtime` 只能依赖稳定契约、Tool Runtime、Runtime Services 接口和注入的 provider。 - concrete scheduler 生命周期、session metadata store、token subscriber、event delivery、product `Tool` handler、concrete prompt assembly、workspace / remote / config IO、custom subagent file IO 和平台 adapter 在行为等价未证明前不得下沉到 runtime kernel。 -- prompt、event、thread goal、scheduler 或 subagent 的纯事实如果进入 Agent Runtime SDK,必须同时删除旧 owner - 实现主体,保留旧路径兼容,并具备 focused contract test 与 boundary check。 +- product feature command、UI state、settings persistence、plugin UI rendering 和 delivery-profile 默认策略不得下沉到 + runtime kernel。 +- prompt、event、thread goal、scheduler 或 subagent 的纯事实如果进入 Agent Runtime SDK,旧 owner 只能保留兼容入口; + 行为等价需要有 contract test 和 boundary guard 证明。 建议内部模块: @@ -519,9 +584,9 @@ pub struct ToolExecutionContext { - workspace service、path policy、runtime artifact reference、remote path containment 和 tool context facts 的 稳定 contract。 -旧路径兼容约束: +兼容边界: -- core 可以保留旧路径 facade、concrete tool adapter、state update、registry lookup、confirmation、actual +- core 允许保留旧路径 facade、concrete tool adapter、state update、registry lookup、confirmation、actual execution 和 filesystem persistence;目标状态要求只有在等价测试保护下才能移动这些行为。 - workspace file/shell contract 保留既有错误与取消语义;不得把错误分类、取消语义或产品 tool exposure 变更混入 owner 边界移动。 @@ -606,7 +671,7 @@ pub struct HarnessExecutionContext { 设计约束: -- harness 可以编排 runtime/tool,但不拥有 session manager internals。 +- harness 允许编排 runtime/tool,但不拥有 session manager internals。 - harness 不直接访问 concrete filesystem / Git / terminal。 - 产品命令只映射到 harness capability,不把命令展示逻辑下沉。 - 新 harness 通过 provider 注册,不改 Agent Runtime SDK 内核。 @@ -618,19 +683,23 @@ pub struct HarnessExecutionContext { ### 4.1 Product Assembly Product Assembly 是 composition root。初始状态可由 `bitfun-core` 兼容 facade 承载;目标状态可拆成独立 -Product Assembly crate。 +Product Assembly crate。它按 feature bundle 触发组装,同时连接 UI host、Rust Kernel、Execution、Plugin Runtime +Host 和 Cross-platform Adapter。 职责: -- 创建或接收具体 adapter / service 实现。 +- 创建或接收具体 service 实现、Plugin Runtime Host binding 和 adapter manifest set。 - 构建 `RuntimeServices`。 - 注册 tool provider groups。 - 注册 harness providers。 - 注册 agent definitions、subagents、skills、prompt modules。 +- 注册 plugin runtime binding、adapter manifest set、ACP bridge 和 UI contribution provider。 - 建立产品 feature matrix。 - 把 interface 命令映射到 capability / harness / runtime request。 -- 根据交付形态选择 `DeliveryProfile`、`CapabilitySet`、adapter 和 service provider 集合。 +- 把 feature bundle 映射为 Rust runtime request、UI contribution、plugin capability 和安全策略。 +- 根据交付形态选择 `DeliveryProfile`、`CapabilitySet`、adapter manifest set 和 service provider 集合。 - 对不支持能力返回 typed unsupported / unavailable 错误,而不是让下层 runtime 判断产品形态。 +- 通过 typed `PluginRuntimeBinding` 向 Kernel 注入插件运行时 client 或 disabled stub,不使用全局 registry。 建议模块: @@ -639,12 +708,15 @@ product-assembly full.rs delivery_profile.rs capability_set.rs + feature_bundle.rs desktop.rs cli.rs server.rs remote.rs acp.rs feature_matrix.rs + ui_contributions.rs + extensions.rs commands.rs ``` @@ -652,12 +724,15 @@ product-assembly ```rust pub enum DeliveryProfile { + ProductFull, Desktop, Cli, Server, Remote, Acp, Web, + MobileWeb, + Sdk, } pub struct CapabilitySet { @@ -666,12 +741,39 @@ pub struct CapabilitySet { pub harness_packs: Vec, pub service_capabilities: Vec, pub command_providers: Vec, + pub ui_contributions: Vec, + pub extensions: ExtensionCapabilitySet, } pub struct ProductAssemblyPlan { pub profile: DeliveryProfile, pub capabilities: CapabilitySet, pub feature_groups: Vec, + pub feature_bundles: Vec, +} + +pub struct ExtensionCapabilitySet { + pub plugin_runtime: PluginRuntimeAvailability, + pub adapters: Vec, + pub ui: UiExtensionAvailability, +} + +pub enum PluginRuntimeAvailability { + Disabled { reason: UnsupportedReason }, + ProjectionOnly { reason: UnsupportedReason }, + Enabled { + compat_level: PluginCompatibilityLevel, + execution: PluginExecutionLocation, + trust_policy: PluginTrustPolicyId, + fallback: PluginFallbackMode, + }, + Unavailable { reason: UnsupportedReason }, +} + +pub enum PluginRuntimeBinding { + Disabled(DisabledPluginRuntimeClient), + ProjectionOnly(ProjectionOnlyPluginRuntimeClient), + Client(Arc), } pub trait ProductAssembler { @@ -680,6 +782,8 @@ pub trait ProductAssembler { } ``` +`ExtensionCapabilitySet` 是产品扩展能力聚合;它不表示旧的扩展主机,也不暴露具体 adapter object。 + 实现注册方式: ```rust @@ -690,6 +794,8 @@ pub struct ProductAssemblyInput { pub harness_providers: Vec>, pub agents: Arc, pub commands: Vec, + pub ui_contributions: Vec, + pub plugin_runtime: PluginRuntimeBinding, pub hooks: RuntimeHookRegistry, } @@ -699,6 +805,8 @@ pub struct ProductRuntimeParts { pub harnesses: Arc, pub agents: Arc, pub commands: ProductCommandRegistry, + pub ui_contributions: UiContributionRegistry, + pub plugin_runtime: PluginRuntimeBinding, pub hooks: RuntimeHookRegistry, } ``` @@ -710,6 +818,9 @@ pub struct ProductRuntimeParts { - harness provider 只注册到 `HarnessRegistryBuilder`。 - agent、subagent、prompt、skill 只注册到 `AgentDefinitionRegistry` 或对应 registry。 - 输入框命令、审核入口、MiniApp 入口只注册到 `ProductCommandRegistry`,再映射到 capability 或 harness。 +- UI panel、command palette item、settings entry 和状态投影只注册为 `UiContributionDescriptor`,由对应 UI host 渲染。 +- Adapter manifest/capability set 由 Product Assembly 选择;具体 adapter object 只由 Plugin Runtime Host 内部加载。 + OpenCode / plugin adapter 只能产出候选效果;ACP bridge 仍按协议 adapter 进入对应入口。 - unsupported / unavailable 能力在 `CapabilityAvailability` 中表达,不让 runtime kernel 读取产品形态。 示例构建流程: @@ -728,10 +839,13 @@ pub fn build_desktop_runtime(input: DesktopAssemblyInput) -> Result Result Vec; fn agent_definitions(&self) -> Vec; fn command_providers(&self) -> Vec; + fn ui_contributions(&self) -> Vec; + fn extension_capabilities(&self) -> Vec; } ``` 分层规则: -- Code Agent pack 可以声明 agent modes、tool packs、prompt modules,但不拥有 tool execution。 -- Deep Review pack 可以声明 harness provider、report artifact contract、queue/retry policy, +- Code Agent pack 允许声明 agent modes、tool packs、prompt modules,但不拥有 tool execution。 +- Deep Review pack 允许声明 harness provider、report artifact contract、queue/retry policy, 但 target resolution 和 UI construction 留在 surface。 -- MiniApp pack 可以声明 MiniApp harness、domain ports、artifact policy,但 worker process 和 +- MiniApp pack 允许声明 MiniApp harness、domain ports、artifact policy,但 worker process 和 filesystem IO 通过 Runtime Services provider。 -- MCP App pack 可以声明 MCP tool/resource/prompt capability,但 MCP transport 属于 +- MCP App pack 允许声明 MCP tool/resource/prompt capability,但 MCP transport 属于 `bitfun-services-integrations`。 - Input command pack 只声明 command 到 capability/harness/runtime request 的映射,不共享具体 UI。 +- Long-running task pack 只声明任务入口、默认 policy、UI contribution 和 command 映射;任务生命周期属于 Agent Kernel。 +- Plugin extension pack 只声明插件能力、UI contribution 和外部 API 映射;安全决策和最终状态写入属于 Kernel / Security Boundary。 + +### 4.4 Plugin Runtime Host 与 Compatibility Adapter + +Plugin Runtime Host 是插件层运行时,不是第二个 Agent Kernel。它负责插件兼容层生命周期、主机通信、项目执行域、 +隔离、健康、deadline、幂等、adapter registry 和候选效果路由。Compatibility Adapter 运行在该 Host 内部, +只把 OpenCode、Claude Code、Codex 插件或 BitFun 原生插件 API 映射为 BitFun 稳定合同。 + +主体进程只能依赖 `PluginRuntimeClient`、`PluginRuntimeBinding`、dispatch/response envelope、candidate effect、 +trust policy 和 UI descriptor。主体进程不得按具体 adapter 类型分支,也不得读取插件 worker、subprocess、 +package manager 或生态原始对象。 + +核心合同分为六组: + +| 合同 | 作用 | 不能暴露 | +|---|---|---| +| Runtime binding | Product Assembly 注入 enabled、projection-only 或 disabled 插件运行时 | 具体生态 adapter、worker handle、全局 registry | +| Adapter manifest | 描述生态、来源、版本、capability/effect、兼容等级 | package manager 具体实现、下载路径、secret | +| Lifecycle | initialize、activate、deactivate、reload、dispose、failure quarantine | kernel state、UI implementation、concrete manager | +| Contribution set | tool、hook、workflow、UI descriptor、config/provider transform | React/Tauri object、完整 `RuntimeServices` | +| Security bridge | permission/effect request、sandbox facts、audit candidate | 直接授权写入、policy state mutation | +| Compatibility mapping | 外部生态 API 到 BitFun contract 的映射 | 作为内部真实 owner 的 OpenCode/Claude/Codex 类型 | + +主体进程可见接口: + +```rust +pub trait PluginRuntimeClient: Send + Sync { + fn availability(&self) -> PluginRuntimeAvailability; + async fn dispatch(&self, envelope: PluginDispatchEnvelope) -> Result; + async fn refresh_project(&self, update: ProjectRuntimeRefresh) -> Result<(), PluginRuntimeError>; + async fn shutdown(&self, request: PluginRuntimeShutdown) -> Result<(), PluginRuntimeError>; +} +``` + +Host 内部建议接口: + +```rust +pub(crate) trait CompatibilityAdapter: Send + Sync { + fn manifest(&self) -> PluginAdapterManifest; + fn capabilities(&self) -> Vec; + async fn map_event(&self, ctx: AdapterDispatchContext) -> Result, PluginRuntimeError>; + async fn dispose(&self) -> Result<(), PluginRuntimeError>; +} + +pub struct PluginAdapterManifest { + pub adapter_id: PluginAdapterId, + pub ecosystem: PluginEcosystem, + pub version: PluginAdapterVersion, + pub supported_levels: Vec, + pub effects: Vec, +} +``` + +`PluginEffectCandidate`、`PluginDispatchEnvelope`、`PluginResponseEnvelope` 和 `UiContributionDescriptor` 的权威 schema +由 [`plugin-runtime-host-design.md`](plugin-runtime-host-design.md) 定义。本文不维护第二套字段。 +`Arc`、`RuntimeHookProviderRef`、`Arc` 等内部 provider trait 只能在 +Product Assembly 内部 materialize,不能成为插件或 SDK 的公开返回值。 +`CompatibilityAdapter` 是 Host-internal 接口;Product Assembly 只选择 adapter manifest/capability set 和 +`PluginRuntimeBinding`,不持有具体 adapter object。 + +OpenCode adapter 的边界: + +- OpenCode server plugin 映射到 tool provider candidate、runtime hook candidate、permission candidate hook、 + config/provider transform 和 event subscription。 +- OpenCode TUI plugin 映射到 UI contribution descriptor,包括 slot、route、command/keymap、prompt augmentation、 + dialog/toast、theme 和只读 state view。 +- OpenCode `permission.ask` 只能映射为 candidate decision;最终 permission decision、audit result 和 policy write + 仍由 Security Boundary 完成。 +- OpenCode tool 映射到 BitFun Tool ABI 时必须进入 materialized snapshot,携带 provider identity、schema、 + permission/effect declaration、cancellation 和 stale call guard。 +- OpenCode event subscription 只能消费 BitFun public event manifest;不得读取 session、turn、tool 或 UI host + 的内部结构。 +- OpenCode config/provider/model/skill/MCP transform 必须逐项进入 support matrix。未支持能力返回 typed unsupported。 + +UI Extension Contract: + +```rust +pub struct UiExtensionContract { + pub slots: Vec, + pub routes: Vec, + pub commands: Vec, + pub prompt_contributions: Vec, + pub state_views: Vec, +} + +pub struct UiSlotDescriptor { + pub id: UiSlotId, + pub surface: UiSurfaceId, + pub payload_contract: UiPayloadContractRef, + pub fallback: UiUnsupportedFallback, +} +``` + +设计约束: + +- UI descriptor 只描述可渲染贡献,不包含 React component、Tauri window、DOM node 或 renderer handle。 +- Desktop、Web、CLI 可以对同一 descriptor 提供不同 host 实现;不支持时必须返回 unsupported/unavailable。 +- UI state view 必须是只读 projection,不能成为插件写 kernel state 的旁路。 +- Plugin lifecycle 的 dispose 必须撤销 tool、hook、workflow、UI contribution、event subscription 和 transform。 +- Product Assembly 只能通过 support matrix 暴露已实现能力;未实现能力必须返回 typed unsupported。 + +风险与保护: + +| 风险 | 保护方式 | +|---|---| +| 外部生态 API 反向成为内部 owner | BitFun 先定义 Plugin Runtime Host、Tool ABI、Event Manifest、Permission/Effect 和 UI descriptor | +| 主体进程直接感知具体 adapter | 主体进程只依赖 `PluginRuntimeClient` 和规范 envelope;adapter 类型仅在 Host 内部出现 | +| 插件越权修改权限或状态 | hook 只产出候选效果;最终 decision / audit / state write 由 Security Boundary 完成 | +| UI contribution 绑定具体前端实现 | descriptor-only contract;host adapter 渲染;unsupported 显式返回 | +| Tool ABI 与 built-in/MCP/plugin 分裂 | 统一 materialized snapshot、provider identity、permission/effect filter 和 stale call guard | +| 插件生命周期泄漏 | 注册 token + dispose finalizer;reload 必须撤销旧 contribution 后再注册 | +| 远程/SDK 形态能力漂移 | contribution 必须声明 execution domain、workspace identity、required capabilities 和 fallback | +| 外部 JS/TS runtime 安全不可控 | 默认目标态只承诺受控 Host 和 compatibility adapter;可写 runtime 需要单独安全评审 | -### 4.4 ACP 扩展方式 +### 4.5 ACP 扩展方式 `bitfun-acp` 保持 integration owner。 @@ -838,7 +1074,7 @@ pub trait ExternalToolProvider: Send + Sync { Agent Runtime SDK 只能看到 external agent/tool capability,不感知 ACP protocol、进程管理、 remote probing 或 startup timeout。 -### 4.5 Skills / Prompt / Subagent +### 4.6 Skills / Prompt / Subagent 建议归属: @@ -855,7 +1091,7 @@ remote probing 或 startup timeout。 - prompt module 只声明可组合内容,不执行 IO。 - skill resource 访问通过 filesystem/workspace port。 -### 4.6 Hook 与 Event 设计 +### 4.7 Hook 与 Event 设计 事件: @@ -981,12 +1217,20 @@ Product 测试: - Remote workspace 行为。 - MCP dynamic tool catalog。 - MiniApp 与 review workflow。 +- UI contribution descriptor round-trip 和 host fallback。 +- SDK minimal feature / no-default-features 嵌入验证。 +- OpenCode / plugin adapter 的 capability/effect 声明与安全决策测试。 ### 5.4 目标态判定口径 - `bitfun-agent-runtime` 能在不依赖 `bitfun-core` 的情况下构建 runtime kernel。 - Agent Runtime SDK facade 能通过 fake model provider、fake runtime services、fake tool provider 和 fake harness provider 完成最小 session / turn / event stream 流程。 +- 长程任务、scheduler、permission、context、session/workspace、memory、DFX、hook/event 能作为内核能力被产品特性复用。 +- `/goal`、DeepReview、MiniApp、输入框命令、settings 和 UI panel 通过 feature pack / Product Assembly 组装,不进入内核。 +- Product API 同时包含 Rust Kernel API、UI Extension Contract 和 Capability/Effect API。 +- Plugin Runtime Host / compatibility adapter 能把外部 plugin API 映射为 BitFun 的 tool、hook、workflow、UI contribution 和 + capability/effect declaration,并受安全控制面约束。 - `bitfun-runtime-services` 提供 typed service injection,并由 boundary check 保护。 - `tool-contracts`、`tool-provider-groups` 和 `tool-execution` 分别承担 tool contract、provider group plan 和低层 execution helper;具体 tool 通过 Product Assembly 注册。 - `bitfun-harness` 支持工作流 provider 扩展。 diff --git a/docs/architecture/core-decomposition.md b/docs/architecture/core-decomposition.md index 660e258406..affdea9887 100644 --- a/docs/architecture/core-decomposition.md +++ b/docs/architecture/core-decomposition.md @@ -1,53 +1,58 @@ # BitFun Core 拆解架构 -本文概括 BitFun core runtime 拆解的两个稳定设计维度:**初始状态**和**目标状态**。 -初始状态描述设计建立时的事实架构、耦合关系和主要问题;目标状态描述期望分层、稳定接口、 -实现归属、组装边界、依赖方向和风险约束。 - -本文聚焦设计结论。详细接口、crate 内部模块和测试设计见 -[`agent-runtime-services-design.md`](agent-runtime-services-design.md)。 +本文描述 BitFun core runtime 拆解的稳定设计:先说明设计建立时的初始状态,再定义目标状态的逻辑分层、 +接口与实现分离、扩展方式、安全边界和风险约束。本文只描述架构设计本身,不记录阶段进度;执行计划见 +[`../plans/core-decomposition-plan.md`](../plans/core-decomposition-plan.md),详细接口和 crate 内部设计见 +[`agent-runtime-services-design.md`](agent-runtime-services-design.md),插件运行时主机、生态兼容适配层和 +主进程通信边界见 [`plugin-runtime-host-design.md`](plugin-runtime-host-design.md)。 ## 1. 背景与目标 设计建立时,BitFun 已经从 `bitfun-core` 中抽出了若干 owner crate,但 `bitfun-core` 仍承担兼容 facade、 -完整产品 runtime 组装、agent loop、service 接线、tool materialization 和部分 product domain -adapter。这个形态在功能上可运行,但会让 runtime 拆解持续面临三个问题: +完整产品 runtime 组装、agent loop、service 接线、tool materialization、产品命令适配和部分 product domain +adapter。该初始形态具备运行能力,但会让架构演进持续面临三个问题: -- 产品逻辑、平台接入和具体 service 实现边界不够稳定。 -- Desktop、CLI、Server、Remote、ACP、Web 等产品形态容易被完整 `bitfun-core` 牵引。 -- Tool、MCP、ACP、subagent、skills、harness 等扩展点缺少统一的分层归属。 +- 产品特性、Agent 内核、平台实现和具体 service 接线混在同一条路径中。 +- Desktop、CLI、Server、Remote、ACP、Web 和未来 SDK 容易被完整 `bitfun-core/product-full` 牵引。 +- Tool、MCP、ACP、subagent、skills、harness、plugin、hook 和 UI 扩展缺少统一分层归属。 -目标形态不是在 `bitfun-core` 内继续扩张完整 `AgentRuntime`,而是形成可独立嵌入的 -Agent Runtime SDK。稳定契约定义上层可依赖的接口,Product Assembly 负责注册具体实现, -Runtime Services、Tool primitives 和 Harness Layer 分别隔离 service、tool、工作流和产品形态差异。 +目标状态不是将所有能力迁入一个新的巨型 `AgentRuntime` crate,而是将 BitFun 拆成可组合的产品架构: -Agent Runtime SDK 在本文中不是某个 crate 的简单重命名,而是一组可对外稳定承诺的运行时能力边界。 -目标状态下,调用方应能通过稳定 API 创建 runtime、提交 turn、消费事件流、注册 tool / harness / service -provider、处理 permission / cancellation / persistence / telemetry,而不需要依赖 `bitfun-core`、app crate、 -Tauri handle 或任何产品形态的 concrete manager。在该目标达成前,`execution` 层只能称为执行原语集合, -不能对外宣称为完整 SDK。 +- **Agent Kernel** 提供稳定、可嵌入、平台无关的 Agent 能力,包括 session / workspace、长程任务、调度、 + 权限、模型调度、上下文、记忆、DFX、hook 和 event。 +- **Product Feature** 负责把内核能力组装为用户可见功能。例如长程任务支持属于内核,`/goal` 命令、设置项、 + UI 展示和默认策略属于产品特性。 +- **Execution / Extension / Platform Adapter** 分别承接工具执行、生态扩展和跨平台实现,避免内核直接依赖 + Tauri、UI、MCP client、SSH、filesystem manager 或 provider concrete。 +- **Product Assembly** 是 composition root,按 Desktop、CLI、Web、ACP、Remote、SDK 等形态选择 feature、 + Rust kernel provider、UI contribution、插件扩展和平台实现。 -目标状态必须保持产品行为、默认能力集合、权限语义、工具曝光、事件语义和 release 构建形态等价。 +目标状态必须保持产品行为、默认能力集合、权限语义、工具曝光、事件语义、session 生命周期、remote 行为和 +release 构建形态等价。任何会改变功能边界的设计变更都必须先单独评审。 ## 2. 架构原则 -- 依赖只能从产品入口 / 产品组装流向产品能力、具体适配、服务和执行原语,再流向稳定契约;下层不得感知上层产品形态。 -- 接口和实现必须分开:接口属于稳定契约、Runtime Services、Tool primitives 或 Harness contract; - 具体实现属于 Product Assembly 的注册边界、Adapters 或 Services。 -- Product interface 可以有差异,capability contract 必须收敛。不同产品入口可以选择不同能力集合, - 但不能通过下沉 UI、命令或协议逻辑来换取复用。 -- `bitfun-core` 保留兼容 facade 和 `product-full` 组装边界;新 owner crate 不得依赖回 - `bitfun-core`。 -- 对外 SDK API 必须是稳定、窄口径、可版本化的 facade,不得把 `bitfun-core`、`product-full`、全量 - service bundle 或产品内部 manager 暴露给调用方。 -- Hook 是受控扩展点,Event 是事实通知。能改变行为的 hook 必须有顺序、timeout、错误策略和等价保护。 -- feature group 是构建边界,CapabilitySet 是产品运行时能力边界;两者必须由 Product Assembly - 显式映射。 +- **内核与特性分离**:Agent Kernel 只拥有平台无关的固化 Agent 逻辑和扩展机制;用户侧命令、UI、设置、 + feature 默认策略和产品文案属于 Product Feature / Product Assembly。 +- **依赖单向流动**:产品入口和组装允许依赖下层,内核、执行、扩展、跨平台适配和契约层不得反向读取产品入口、 + UI state、Tauri handle 或 delivery profile。 +- **接口与实现分离**:接口属于稳定契约、Kernel API、Runtime Services、Tool / Harness contract 或 UI + extension contract;具体实现属于 Product Assembly 注册边界、Plugin Runtime Host、protocol adapter 或 + cross-platform adapter。 +- **Product API 覆盖 Rust 与 UI**:BitFun API 不应仅指 Rust 后端 API。目标状态需要同时提供 Rust Kernel API + 和 UI Extension Contract,再由 OpenCode / ACP / plugin adapter 映射到外部生态 API。 +- **扩展只产出受控候选效果**:plugin、OpenCode adapter、MCP、skills 和 hooks 通过声明式契约贡献事件订阅、UI + contribution、tool、prompt、permission candidate 或 workflow provider,但授权、状态写入、审计和最终执行 + 仍由内核事实与安全控制面决定。 +- **外部系统只在边界被调用**:OS、Git、MCP server、AI provider、remote host、browser/desktop 环境是仓库外部 + 系统。它们由跨平台或协议 adapter 调用,不作为服务层上游依赖写入核心逻辑视图。 +- **feature group 不等于产品能力**:feature group 是 Cargo 构建边界,CapabilitySet 是运行时产品能力边界。 + 两者必须由 Product Assembly 显式映射。 ## 3. 初始状态逻辑视图 -初始状态的核心事实是:多个 crate 已经承接了稳定类型、事件、stream、tool contract、部分 service -helper 和 product domain 纯逻辑,但完整运行时仍以 `bitfun-core` 为中心。 +初始状态的核心事实是:多个 crate 已经承接稳定类型、事件、stream、tool contract、部分 service helper 和 +product domain 纯逻辑,但完整运行时仍以 `bitfun-core` 为中心。 ```mermaid flowchart TB @@ -92,266 +97,375 @@ flowchart TB Ai --> External ``` -初始状态主要模块范围: +初始状态主要问题: -| 模块 | 初始定位 | 架构影响 | -|---|---|---| -| `bitfun-core` | 兼容 facade、agent runtime、tool runtime 组装、service 接线和完整产品能力集合 | 仍是事实上的 runtime owner,拆解必须先保护行为等价 | -| `bitfun-runtime-ports` | 面向 runtime/service 边界的 DTO 和 trait | 只定义 contract,不拥有 runtime 实现 | -| `tool-contracts` / `bitfun-agent-tools` | provider-neutral tool DTO、manifest、path/result policy、catalog contract 和 deterministic execution admission gate | 适合承接纯 tool contract 策略,但不应拥有具体 IO tool | -| `tool-execution` / `tool-runtime` | 既有低层工具执行 helper crate | 目标是只承接低层 file/search/tool execution helper,不拥有产品 registry 或 permission policy | -| `bitfun-services-core` | 基础 service helper、本地 filesystem facade、部分通用 service 逻辑 | 适合作为本地基础 service owner,但不能吸收产品 runtime 语义 | -| `bitfun-services-integrations` | MCP、Git、remote-connect、remote-SSH 等 integration helper | 适合拥有外部协议和重依赖 service implementation,不应反向感知产品 interface | -| `bitfun-product-domains` | MiniApp、function-agent 等纯状态、策略、port 和部分决策逻辑 | 适合承接 pure domain,不应直接执行 filesystem/Git/AI concrete call | -| `bitfun-acp` | ACP protocol interface 和 client behavior | 应保持产品协议入口,不下沉到 Agent Runtime | -| `transport` / `api-layer` | surface 到 runtime 的 API/transport adapter | 应保持传输层,不拥有 runtime owner | +- `bitfun-core` 仍是事实上的 runtime owner,产品命令、service wiring、tool registry、manager handle 和兼容导出 + 交叉存在。 +- 产品特性没有与内核能力分层。例如 `/goal`、DeepReview、MiniApp、custom agent、tool exposure 同时触达 UI、 + Rust runtime、tool、service 和权限。 +- 现有六层物理目录有助于控制依赖,但不能直接表达 Product Feature、Agent Kernel、Plugin Runtime Host 和 + Cross-platform Adapter 的目标关系。 +- 适配层与服务层描述偏实现目录,不能清楚区分 protocol/provider translation、OS/service implementation、 + UI Extension Contract 和外部生态 adapter。 +- 安全边界散落在 SDLC Harness、tool permission、MCP、hook、shell、网络和平台执行路径中,缺少贯穿内核、 + 执行、扩展和 UI 投影的一致控制面。 +- `product-full` 是完整产品能力的安全网,不是最终按产品形态和 feature bundle 拆分的能力矩阵。 +- Agent Runtime SDK 仍缺少可对外稳定承诺的统一 facade、事件流协议、provider 注册边界、UI/API 扩展契约和 + 最小依赖构建形态。 -## 4. 初始状态主要问题 +## 4. 对照分析 -### 4.1 分层不清晰 +本节只提炼对 BitFun 分层有用的架构信号,不把其他项目的实现形态直接复制到 BitFun。 -同一能力经常同时包含 UI/command、runtime orchestration、tool execution、service IO 和 domain -decision。初始状态代码中这些部分仍大量通过 `bitfun-core` 串联,导致拆解时难以判断“移动的是接口、 -实现、组装逻辑还是产品行为”。 +### 4.1 Claude Code -### 4.2 接口与实现边界不稳定 +Claude Code 官方文档把 Agent SDK、hooks、slash commands、subagents、MCP、permissions 和 sessions 作为可组合 +能力暴露;hooks 在生命周期事件上触发,permission / settings 管理工具使用和团队配置,slash commands 通过 SDK +控制 session。该结构表明,用户可见命令、Agent Kernel、权限、MCP、skills 和 hooks 能够通过稳定 API 组合,而不是混在 +同一个 runtime owner 中。 -已有 `runtime-ports` 和若干 contract crate,但许多 call site 仍依赖 concrete manager、 -core-owned context 或完整 product runtime snapshot。接口没有稳定到足以让 runtime 与具体 service -实现独立演进。 +对 BitFun 的设计结论: -### 4.3 产品形态被完整 core 牵引 +- 长程任务、session、权限、事件、hook 和 tool 调度属于内核能力;`/goal`、`/plan`、设置项和 UI 展示属于特性组装。 +- SDK 边界必须暴露稳定 builder / runner / event stream / permission / session API,而不是要求调用方理解产品命令。 +- hooks 能改变行为时必须有顺序、timeout、错误策略和安全审计;不得只是事件通知。 -Desktop、CLI、Server、Remote、ACP 和 Web 的入口差异较大,但初始状态下大多仍通过完整 `bitfun-core` -获得能力。这会让轻量交付形态继承不必要的 tool、service、UI 或平台依赖。 +### 4.2 OpenCode -### 4.4 Tool contract 与 tool execution 混合 +OpenCode 的产品结构展示了 terminal、desktop、IDE、server 和 SDK 多入口;agents / subagents 可配置 prompt、model 和 +tool access;tools 通过 permission 控制并可由 custom tools 或 MCP 扩展;plugins 同时覆盖 server hooks 和 TUI +contribution。其代码层面还体现了几个关键边界:server plugin API 与 TUI plugin API 分离;tool registry 在执行前 +materialize 当前可用工具快照并按 permission 过滤;event service 使用 manifest、version、aggregate sequence 和 +durable replay;permission service 保持最终权威,插件 hook 只参与候选决策。 -provider-neutral manifest、path policy、result policy、`ToolUseContext` runtime handle、collapsed unlock -lifecycle、runtime artifact persistence 和 product registry materialization 在初始状态下与 concrete tool -execution 交织在 core 及其兼容路径中。目标状态下,tool contracts 应拥有 provider-neutral manifest / -catalog / permission / result / artifact contract,core、services 或 adapter 只保留实际 IO tool adapter、 -state update、旧路径 facade 和有等价保护的拆解边界。工具 owner 拆解如果没有快照保护,容易改变 -prompt-visible manifest、`GetToolSpec`、MCP/ACP catalog 或 oversized result 行为。 +对 BitFun 的设计结论: -### 4.5 Service、MCP、ACP 与 runtime kernel 容易交叉 +- OpenCode adapter 不应仅绑定 Rust 内核。它需要映射 BitFun 的 Rust Kernel API、UI Extension Contract、tool / MCP / + skills / plugin contract 和安全能力声明。 +- 插件扩展范围覆盖 UI 和内核能力,但只能通过 Product Assembly 注册到明确的 feature / capability;不能直接持有产品状态或 + 绕过安全边界。 +- API 需要按层暴露,避免把所有能力集中到单一 `bitfun-core` 或单一后端 API crate。 +- OpenCode API 不应直接成为 BitFun 内部 owner。BitFun 先定义自己的 Plugin Runtime Host、Tool ABI、Event Manifest 和 + Permission/Effect control plane,再由 adapter 做兼容映射。 -MCP 和 ACP 是外部协议/能力接入,不应变成 Agent Runtime SDK 的内部协议依赖。Runtime kernel 只应看见 -external capability、tool provider 或 service port;连接生命周期、鉴权、transport 和 timeout 策略应由 -Adapters、Services 或 Product Assembly 管理。 +### 4.3 Claw Code -### 4.6 扩展点缺少统一语义 +Claw Code 的 Rust workspace 将完整 CLI、轻量 automation harness、RAG service、runtime、tools、commands 和 plugins +分成不同产品能力。其设计强调权限模式、workspace 边界、结构化输出、session 和可测试的 tool contract。该结构对 +BitFun 的价值在于:完整产品形态和轻量 SDK / automation 形态应共享稳定能力,而不是共享全量产品入口。 -agent definitions、subagents、skills、prompt modules、tool providers、MCP providers、hooks 和 -product commands 都是扩展点,但目前没有统一表达它们分别属于哪一层、如何注册、是否允许改变行为、 -以及如何做权限和测试保护。 +对 BitFun 的设计结论: -### 4.7 feature graph 还不是产品能力矩阵 +- SDK、CLI、Web、ACP、Remote 等形态应通过 capability matrix 选择能力,不应被 `product-full` 全量牵引。 +- 轻量形态需要可解释的最小 API 和安全默认值;完整形态可以通过 Product Assembly 注入更多 provider。 +- runtime / tools / commands 的公共导出面需要控制,否则长期会退化为难以承诺的 workspace-internal API。 -初始状态下,`product-full` 是完整产品能力的安全网,不是最终按产品拆分的 feature matrix。直接减轻默认 feature -或把 feature group 当成产品能力边界,都会引入构建形态和发布能力漂移。 +### 4.4 Codex -### 4.8 构建与测试牵引过大 +Codex 官方文档把 sandbox、approval、network control 和 auto-review 放在 agent 操作安全模型中,而不是把所有确认 +都交给 UI 或模型判断。对 BitFun 而言,权限确认、沙箱、网络、凭据、插件执行域和远程执行域必须由稳定控制面表达, +再由产品入口投影给用户。 -重依赖和完整 runtime 聚合在 `bitfun-core` 周围,导致局部测试、owner crate 测试和轻量产品入口容易被 -不相关依赖拖入编译和链接路径。目标状态必须让依赖收益可度量,同时不能以牺牲功能等价换取构建收益。 +### 4.5 对照结论 -### 4.9 SDK 发布边界不足 +Claude Code、OpenCode、Claw Code 和 Codex 的共同信号不是“所有能力放入一个 runtime”,而是把 agent 内核、安全控制、 +扩展声明、工具执行和平台 provider 分开。用户可见功能通过命令、设置、UI contribution 或 SDK API 组装;外部插件 +通过 descriptor、hook、tool provider 或 MCP/ACP bridge 接入;OS、network、remote、browser 和 provider client +留在边界 adapter 中。BitFun 的目标架构也应遵循这一点:Product Assembly 负责组装 concrete provider,普通层级 +只消费稳定 API、port 和 capability/effect contract。 -已有 `bitfun-agent-runtime`、`bitfun-runtime-services`、`tool-contracts`、`tool-execution`、`bitfun-harness` -和 `runtime-ports` 等 SDK 候选原语,但缺少可对外承诺的统一 runtime facade、稳定错误模型、事件流协议、 -provider 注册边界、持久化/恢复契约和最小依赖构建形态。如果外部调用方仍需要直接理解 `bitfun-core`、 -`product-full`、concrete service manager 或产品命令路径,说明 SDK 边界尚未完成。 +## 5. 目标逻辑视图 -## 5. 对照分析 +目标逻辑按概念层表达。概念层决定职责、接口和依赖方向;物理目录决定 crate 的放置位置。二者必须一致,但不要求 +一层只对应一个 crate。 -本节只提炼对 BitFun 分层有用的架构信号,不把其他项目的实现形态直接复制到 BitFun。 +本图区分两类关系: -### 5.1 Claude Code 相关实现参考 +- **编译依赖**:普通模块只能依赖本层公开 API、下层稳定契约或被注入的 port,不直接依赖更底层 concrete 实现。 +- **组装依赖**:Product Assembly 是 composition root,允许在构建期认识具体 feature、kernel、execution、 + extension 和 platform provider,并把它们装配为 typed runtime parts。 -Claude Code 相关 Rust 实现参考中,workspace 将 CLI binary、provider API、runtime、tools、 -commands、plugins、telemetry 和 mock harness 拆成不同 crate。其 `runtime` 负责 session、config、 -permission、MCP、prompt 和 runtime loop;`tools` 负责 tool specs 与执行;`commands` 负责 slash command -registry;`plugins` 负责 plugin metadata、hook 和 install/enable/disable surfaces。该结构说明: +外部系统不是仓库内层级。OS、Git、MCP server、AI provider、remote host、browser/desktop runtime 和 plugin +package 都是边界外资源,只能由被注入的跨平台或协议 adapter 调用。 -- 工具规格、命令 surface、plugin/hook 和 runtime loop 可以分开演进。 -- permission、MCP lifecycle、task registry、LSP registry 等可作为 runtime/service owner 管理,而不是散落在 UI。 -- 如果 runtime crate 同时吸收 session、MCP、permission、prompt 和 tool bridge,也会变成新的重聚合点。 +```mermaid +flowchart TB + Surface["产品入口(Product Surfaces)
Desktop / CLI / Web / ACP / Remote / SDK / UI host"] + Assembly["产品组装层(Product Assembly)
delivery profile / feature bundle / provider registration / capability matrix"] + Feature["产品特性层(Product Feature Layer)
/goal / DeepReview / MiniApp / commands / settings / product workflows"] + Kernel["核心层(Agent Kernel)
session / workspace / scheduler / permission / model routing / context / memory / DFX / hook-event bus"] + Execution["执行层(Execution Layer)
tool runtime / skills / MCP tool bridge / sandbox / harness / artifact semantics"] + Extension["扩展层(Extension Layer)
Plugin Runtime Host / compatibility adapters / ACP bridge / external skills / UI contribution descriptors"] + Platform["跨平台适配实现(Platform and Provider Adapters)
filesystem / network / process / terminal / remote / Git / MCP transport / AI provider"] + Contracts["稳定契约与安全控制面(Stable Contracts and Security Control Plane)
DTO / ports / events / capability-effect / permission / audit / UI extension contract"] + External[("边界外资源(External Resources)
OS / Git binary / MCP server / AI provider / remote host / browser runtime / plugin package")] + + Surface --> Assembly + Assembly --> Feature + Assembly --> Kernel + Assembly --> Execution + Assembly --> Extension + Assembly --> Platform + Feature --> Kernel + Feature --> Contracts + Kernel --> Contracts + Execution --> Contracts + Extension --> Contracts + Platform --> Contracts + Kernel -. "通过注入的 execution port 调用" .-> Execution + Execution -. "通过注入的 platform port 调用" .-> Platform + Assembly -. "注册 extension descriptor/provider" .-> Extension + Platform -. "边界调用,不是仓库依赖层" .-> External +``` -总结:拆分 crate 不是目标本身,关键是让 CLI/TUI、commands、tools、plugins、runtime 和 -service integrations 通过稳定 contract 组合,避免把 `bitfun-core` 的聚合问题搬到新的 runtime crate。 +普通模块的依赖方向只允许流向稳定接口,而不是直接认识所有下层实现: + +```text +Product Surfaces + -> Product Assembly API +Product Assembly + -> Product Feature API / Agent Kernel API / Execution API / Extension API / Platform Provider API +Product Feature + -> Agent Kernel API / Stable Contracts and Security Control Plane +Agent Kernel + -> Stable Contracts and Security Control Plane +Execution + -> Stable Contracts and Security Control Plane +Extension + -> Stable Contracts and Security Control Plane +Platform and Provider Adapters + -> Stable Contracts and Security Control Plane +``` -### 5.2 Opencode +运行时调用可以通过 Product Assembly 注入的 typed port 从 Kernel 进入 Execution、从 Execution 进入 Platform +provider,但这不是对 concrete crate 的编译依赖。除 Product Assembly 外,任何层都不应为了“方便调用”直接依赖下层 +concrete implementation。外部系统只存在于 adapter 的 I/O 边界,不参与仓库内层级依赖。 -Opencode 官方文档展示了更偏产品化的扩展模型:同一个 agent 可以运行在 terminal、desktop 或 IDE; -agents 分为 primary agents 和 subagents,可配置 prompt、model 与 tool access;tools 通过 permission 控制, -并可通过 custom tools 或 MCP servers 扩展;plugins 订阅 command、file、permission、session、tool、TUI -等事件;skills 通过独立目录按需发现和加载。 +同层 crate 之间也必须按 owner 最小化依赖,禁止为了复用 helper 形成循环依赖或让下层读取产品形态。 -总结: +## 6. 层级放置规则 -- Agent、Tool、MCP、Plugin/Hook、Skill 和 Product Surface 应该是互相连接的扩展面,而不是同一个模块内部的分支。 -- 权限和工具可见性必须是 runtime 可观测的 contract,不能只存在于 UI 或 prompt 拼接中。 -- 多产品形态需要 Product Assembly 做 capability/provider 选择,而不是让 Agent Runtime SDK 判断调用来自 - Desktop、CLI、Remote 还是 ACP。 +### 6.1 产品组装与接口层(Product Assembly and Interfaces) -## 6. 目标逻辑视图 +放置 Desktop、CLI、Server、Remote、ACP、Web、Mobile Web、Installer、E2E 和 SDK 入口相关代码。该层负责选择 +`DeliveryProfile`、feature bundle、capability matrix、Rust provider、UI contribution、Plugin Runtime Host binding 和 +platform provider,并把它们组装成产品可用能力。 -目标架构以六个物理 owner 分区表达依赖方向。`interfaces` 只承载协议和宿主入口;`assembly` 负责产品能力选择与注册;`adapters` 负责协议、transport 和外部 provider 转换;`services` 负责本地系统与 runtime infrastructure 的可复用具体实现;`execution` 只放可移植执行原语;`contracts` 提供稳定事实、port 和产品领域规则。这样可以同时区分“协议适配”和“服务实现”,也避免把 execution 误解为完整运行时实现层。 +当前主要路径:`src/apps/*`、`src/web-ui`、`src/mobile-web`、`BitFun-Installer`、`tests/e2e`、 +`src/crates/interfaces`、`src/crates/assembly/core` 和 `src/crates/assembly/product-capabilities`。 -```mermaid -flowchart TB - Interfaces["接口与入口层(Interfaces and Entrypoints)
UI / command / protocol interface / delivery profile"] - Assembly["产品组装层(Product Assembly)
compatibility facade / capability selection / adapter and service registration"] - Adapters["适配层(Adapters)
AI / API / transport / WebDriver / external provider translation"] - Services["服务实现层(Services)
filesystem / git / terminal / MCP / remote / process / OS integration"] - Execution["执行原语层(Execution Primitives)
agent / harness / stream / typed-service / tool primitives"] - Contracts["稳定契约与产品领域层(Stable Contracts and Product Domains)
DTO / event / runtime port / product domain policy"] - External["外部系统(External Systems)
OS / Git / MCP server / ACP client / AI provider / remote host"] - - Interfaces --> Assembly - Interfaces --> Adapters - Assembly --> Adapters - Assembly --> Services - Assembly --> Execution - Assembly --> Contracts - Adapters --> Services - Adapters --> Execution - Adapters --> Contracts - Services --> Execution - Services --> Contracts - Execution --> Contracts - Adapters --> External - Services --> External -``` +应该放这里: -依赖方向只允许从上到下。接口与入口层暴露产品形态;组装层选择能力集合并注册 adapter/service;适配层翻译协议和外部 provider;服务实现层接触 OS、process、filesystem、git、terminal、MCP 和 remote;执行原语层提供可复用 runtime building blocks;契约层提供稳定事实、port 和产品领域规则。任何下层 crate 反向读取产品入口、组装配置或 host state 都视为边界违规。 +- 产品入口、host adapter、UI host、Tauri command、CLI command、HTTP/transport entrypoint、ACP surface。 +- 产品形态选择、feature bundle 选择、capability matrix、provider 注册和 unsupported/unavailable 映射。 +- UI contribution 的渲染 host、输入框命令入口、产品命令到 runtime request 的映射。 -## 7. 目标层级 +不应该放这里: -目标层级以物理 owner 分区为入口。每个分区可以包含多个 crate,但 crate 内部职责必须能够通过依赖、测试和边界脚本独立验证。 +- Agent session/turn/scheduler 的状态机。 +- Tool、MCP、filesystem、terminal、remote、AI provider 的通用实现。 +- 可被 SDK 独立复用的内核 API 语义。 -### 7.1 接口与入口层(Interfaces and Entrypoints) +### 6.2 产品特性层(Product Feature Layer) -接口与入口层是用户、协议或外部系统进入 BitFun 的入口,负责 UI、命令、路由、协议接口、交付形态选择和 host integration。对应范围包括 `src/apps/*`、`src/web-ui`、`src/mobile-web`、`BitFun-Installer`、`tests/e2e` 和 `src/crates/interfaces`。入口层可以选择 `DeliveryProfile` 并调用 assembly 或 adapter API,但不拥有共享 runtime 行为。 +放置把底层能力组合成用户功能的逻辑。产品特性可以同时有 Rust 编排、UI contribution、命令入口和默认策略, +但它只编排能力,不拥有内核状态机或平台实现。 -### 7.2 产品组装层(Product Assembly) +当前主要路径:`src/crates/assembly/product-capabilities`、`src/crates/contracts/product-domains`、部分 +`src/web-ui` feature host、app command adapter,以及迁移期 `bitfun-core` 兼容路径。 -产品组装层负责兼容导出、完整产品能力选择、feature group 到 capability set 的映射、adapter/service 注册和 product-full 接线。物理位置是 `src/crates/assembly`,当前包含 `bitfun-core` 兼容门面和 `bitfun-product-capabilities` 能力模型。`product-capabilities` 只描述 capability id、tool group、service requirement 和 harness selection,不执行 IO,也不承载产品领域状态机。 +应该放这里: -### 7.3 适配层(Adapters) +- `/goal`、DeepReview、DeepResearch、MiniApp、input command、settings、review UI、custom agent/mode 的产品编排。 +- feature pack / capability pack、默认策略、UI contribution descriptor、命令到 kernel request 或稳定 descriptor 的映射。 +- 特性级 DTO 和纯规则,例如 MiniApp domain contract、review report domain contract。 -适配层负责协议、transport、外部 provider 和宿主通信转换,物理位置是 `src/crates/adapters`。其中 `ai-adapters` 负责 AI provider 请求/响应映射和 provider stream 协议解析,解析结果应转换为 execution 层拥有的统一 stream 契约;`api-layer` 负责产品宿主共用的后端 API adapter,`transport` 负责事件投递和 host transport adapter,`webdriver` 负责 WebDriver 协议和浏览器自动化 adapter。适配层不拥有产品能力选择,也不承载可复用 OS service 实现。 +不应该放这里: -### 7.4 服务实现层(Services) +- 长程任务生命周期、session/workspace 状态、scheduler、permission decision、event queue。 +- Tool/MCP/sandbox 的实际执行、filesystem/terminal/remote 的具体实现。 +- 依赖某个产品形态的 UI 组件实现;特性只声明 descriptor,由入口层渲染。 -服务实现层负责接触本地系统和 runtime infrastructure 的可复用具体实现,物理位置是 `src/crates/services`。其中 `services-core` 承载轻量 service primitive,`services-integrations` 承载 MCP、Git、remote、file watch 和产品领域 port 的具体实现,`terminal` 承载 PTY、shell integration 和 terminal session infrastructure。服务实现层可以实现 `contracts`、`execution` 或 `product-domains` 定义的 port,但不选择产品 profile,也不直接暴露 UI/协议入口。 +### 6.3 核心层(Agent Kernel) -### 7.5 执行原语层(Execution Primitives) +放置平台无关、产品形态无关的 Agent 固化逻辑。该层是未来 Agent Runtime SDK 的核心候选边界。 -执行原语层提供 provider-neutral 的 runtime building blocks,物理位置是 `src/crates/execution`。`agent-runtime`、`agent-stream`、`harness`、`runtime-services`、`tool-contracts`、`tool-provider-groups` 和 `tool-execution` 分别定义 agent loop facts、统一 stream DTO / tool-call 累积 / replay 契约、workflow descriptor、typed service bundle、tool manifest / permission / result policy、tool group facts 和低层 tool execution helper。当前 Cargo package / lib 名保持兼容,但物理目录按职责命名。它们只能依赖稳定契约或明确的 provider-neutral DTO,不直接创建 Tauri handle、filesystem manager、Git provider、MCP client、AI client 或 host process。 +当前主要路径:`src/crates/execution/agent-runtime`、`src/crates/execution/agent-stream`、 +`src/crates/execution/runtime-services`、`src/crates/contracts/runtime-ports`、`src/crates/contracts/events` 和 +`src/crates/contracts/core-types`。 -### 7.6 稳定契约与产品领域层(Stable Contracts and Product Domains) +应该放这里: -稳定契约与产品领域层是最低层,物理位置是 `src/crates/contracts`。它包含 `core-types`、`events`、`runtime-ports` 和 `product-domains`。`product-domains` 是 Product Domain Model,负责 MiniApp、function-agent 等领域 DTO、纯策略、状态规则和窄 port;具体 Git、filesystem、AI 或 worker execution 实现在 services、adapters 或 assembly/core 的兼容路径中,不得回流到 contracts。 +- session/workspace facts、turn/model round 生命周期、long-running task、scheduler、cancellation。 +- permission coordination、model route request、context assembly、memory、DFX、event queue/router。 +- hook registry、post-turn processor、agent/subagent registry 查询和 SDK-facing runtime facade。 -### 7.7 扩展点归属 +不应该放这里: -- AI、API、transport 和 WebDriver 的协议转换属于 Adapters。 -- MCP、terminal、filesystem、git、remote 和 file watch 的可复用具体实现属于 Services。 -- Tool manifest、permission、execution admission、result / artifact policy 属于 Execution Primitives 的 `tool-contracts`。 -- Tool provider group facts 属于 Execution Primitives 的 `tool-provider-groups`;低层 filesystem/search helper 属于 `tool-execution`。 -- Agent、subagent、prompt module、scheduler、session / turn facts 和 hook routing 属于 Execution Primitives。 -- Harness workflow descriptor 和 route plan 属于 Execution Primitives;具体工作流 IO 留在 Services、Adapters 或兼容路径,直到有等价保护后再迁移。 -- Capability pack、delivery profile、adapter/service selection 和 product-full assembly 属于 Product Assembly。 -- 产品领域状态、规则、port 和 domain policy 属于 Stable Contracts and Product Domains。 +- `/goal`、DeepReview、MiniApp、settings、UI panel、产品默认文案。 +- Tauri、Web UI、CLI TUI、ACP protocol、filesystem/Git/terminal/MCP/AI/remote provider concrete。 +- 具体 tool 实现或具体 platform service manager。 -## 8. 接口与实现关系 +### 6.4 执行层(Execution Layer) -接口由稳定契约、Runtime Services、Tool Contracts 或 Harness contract 定义;具体实现由 adapter、service 或产品入口创建;注册动作只能发生在 Product Assembly。Agent Runtime、tool contracts、tool execution 和 Harness 只接收已经组装好的接口或 provider registry,不直接创建平台实现。 +放置可被 Agent Kernel 调度的执行原语。该层定义 tool、skills、MCP tool bridge、sandbox、local/remote tool +runtime 和 harness 的执行语义,但不决定产品形态。 -```mermaid -flowchart TB - Interface["接口与入口层(Interfaces and Entrypoints)
选择入口和 DeliveryProfile"] - Assembly["产品组装层(Product Assembly)
唯一注册点"] - ServiceBuilder["运行时服务层(Runtime Services)
RuntimeServicesBuilder"] - ToolBuilder["工具执行原语(Tool Primitives)
tool contracts / groups / execution"] - HarnessBuilder["工作流编排层(Harness Layer)
HarnessRegistryBuilder"] - AgentRegistry["Agent 执行原语(Agent Runtime)
AgentDefinitionRegistry"] - CommandRegistry["接口 / 产品组装层
ProductCommandRegistry"] - Runtime["Agent / Tool / Harness primitives
只消费接口"] - Adapters["适配层(Adapters)
AI / API / transport / WebDriver adapters"] - Services["服务实现层(Services)
OS / filesystem / Git / terminal / MCP / remote services"] - Contracts["稳定契约与产品领域层(Stable Contracts and Product Domains)
DTO / event / port trait"] - - Interface --> Assembly - Assembly --> ServiceBuilder - Assembly --> ToolBuilder - Assembly --> HarnessBuilder - Assembly --> AgentRegistry - Assembly --> CommandRegistry - Assembly --> Adapters - Assembly --> Services - ServiceBuilder --> Runtime - ToolBuilder --> Runtime - HarnessBuilder --> Runtime - AgentRegistry --> Runtime - CommandRegistry --> Interface - Runtime --> Contracts - Adapters --> Contracts - Services --> Contracts - Adapters --> Services -``` +当前主要路径:`src/crates/execution/tool-contracts`、`src/crates/execution/tool-provider-groups`、 +`src/crates/execution/tool-execution`、`src/crates/execution/harness`,以及迁移期 core tool/harness 兼容路径。 + +应该放这里: + +- tool manifest、permission request shape、tool result/artifact policy、tool group plan。 +- built-in tool provider 的 provider-neutral 部分、MCP tool bridge、skills execution contract。 +- harness descriptor、route plan、workflow provider contract、sandbox execution contract。 + +不应该放这里: + +- MCP transport/client concrete、remote SSH/SFTP/PTY、filesystem manager、terminal PTY、network client。 +- 产品命令、UI 展示、产品能力选择。 +- 权限最终策略实现;执行层只消费安全控制面的 decision / facts。 + +### 6.5 扩展层(Extension Layer) + +放置外部生态接入逻辑。扩展层把 OpenCode、ACP、plugin、external skills、external tools 和 hook adapter +转换为 BitFun 的稳定契约。 + +当前主要路径:`src/crates/interfaces/acp`、OpenCode / plugin 相关设计文档、部分 +`src/crates/services/services-integrations`、app command adapter 和扩展 host owner。 + +应该放这里: + +- Plugin Runtime Host、compatibility adapter、ACP bridge、external skill/plugin 的注册与映射。 +- UI contribution descriptor、external command/tool/hook/workflow provider descriptor。 +- capability/effect declaration、plugin source identity、插件能力声明和外部 API 映射。 + +不应该放这里: + +- Web UI React 组件实现、Tauri app state、kernel 权威状态。 +- permission decision、audit result、sandbox policy 的最终写入。 +- 绕过 Product Assembly 直接注入产品功能。 + +### 6.6 跨平台适配层(Cross-platform Adapter Layer) + +放置仓库内与边界外资源交互的具体实现。该层不是“外部系统层”,也不是所有模块都应依赖的底座;它只是实现稳定 +ports 的 provider / driver / protocol adapter。Product Assembly 按产品形态选择并注册具体实现,Kernel、Execution、 +Extension 和 Product Feature 只通过稳定 port 或 provider contract 消费能力。 + +外部系统指仓库外资源,例如 OS API、Git binary、MCP server、AI provider、remote host、browser/desktop runtime、 +plugin package 或 cloud service。它们不属于仓库内依赖层,不能被普通模块作为上游依赖建模。 + +当前主要路径:`src/crates/services/services-core`、`src/crates/services/services-integrations`、 +`src/crates/services/terminal`、`src/crates/adapters/ai-adapters`、`src/crates/adapters/api-layer`、 +`src/crates/adapters/transport`、`src/crates/adapters/webdriver` 和 app-local provider。 + +应该放这里: + +- filesystem、network、process/thread/time、terminal、remote、Git、MCP transport、AI/provider protocol。 +- browser/desktop automation、WebDriver、HTTP/transport adapter、OS-specific permission/projection provider。 +- local/remote provider 的 concrete implementation、第三方库适配、外部协议错误映射和不可用状态转换。 + +不应该放这里: + +- 产品能力选择、feature pack、UI 命令、Agent Kernel 状态机。 +- Tool manifest 的产品曝光策略或 permission 最终策略。 +- 直接依赖上层产品入口来决定行为;形态差异由 Product Assembly 注入。 +- 被其他层当作“必须依赖的基础层”。除组装层外,调用方应依赖 port / contract,而不是依赖 concrete adapter。 + +### 6.7 稳定契约与安全控制面(Stable Contracts and Security Control Plane) + +放置跨层稳定语义。该层定义 DTO、event、port、capability/effect model、permission facts、sandbox facts、 +audit facts、UI extension contract、artifact ref、unsupported/unavailable 错误和产品领域纯规则。 + +当前主要路径:`src/crates/contracts/*`、`docs/sdlc-harness/*` 中的安全/治理契约,以及下层 crate 中的 +provider-neutral contract。 + +应该放这里: + +- 可序列化 DTO、identity、event、port trait、artifact ref、typed error。 +- 跨框架 frontend event projection:把内核事件映射为稳定 event name / event type / payload,不包含 Tauri、 + React、WebSocket delivery 或 OpenCode adapter 实现。 +- capability/effect、permission、sandbox、execution domain、audit facts。 +- UI extension descriptor contract 和跨产品领域的纯规则。 + +不应该放这里: + +- 具体策略实现、具体 service manager、具体 provider、UI rendering、runtime state machine。 +- 依赖上层 crate 的 helper。稳定契约只能向下保持 behavior-light。 + +本文中的安全控制面指稳定契约和注入式策略边界,不表示具体策略实现必须位于 contracts crate。具体策略实现由 +Product Assembly 注入,通过稳定 port 供 Kernel、Execution、Extension、Platform Adapter 和 UI projection 消费。 +最终授权和审计不能由模型输出、外部插件或平台 adapter 直接写入。 + +## 7. 接口与实现关系 + +接口定义在稳定 owner;实现由 Product Assembly 或产品宿主在组装边界注入。注册发生在构建期,运行时热路径应消费 +typed parts 和 port handle,而不是通过无类型 service locator 动态查找。 + +除 Product Assembly 外,普通层级不应同时依赖接口和实现。Kernel 只依赖 kernel/runtime contracts;Execution 只依赖 +tool/harness/sandbox contracts 和被注入的 platform ports;Extension 只产出 descriptor、provider 和 candidate effect; +Platform adapter 只实现 ports 并调用边界外资源。 + +| 接口 / API | 定义 owner | 实现 owner | 注册者 | 消费者 | 边界 | +|---|---|---|---|---|---| +| `ProductAssembler` / `ProductAssemblyPlan` | Product Assembly | 产品入口或 assembly crate | 产品入口 | Desktop / CLI / Server / Remote / Web / Mobile Web / SDK / ACP | 选择能力,不写 Agent 状态机 | +| `ProductFeaturePack` | Product Feature | feature owner | Product Assembly | 产品入口、Kernel API、UI host | 编排 feature,不拥有 OS concrete 或 Plugin Runtime Host | +| `AgentRuntimeBuilder` / Kernel API | Agent Kernel | `bitfun-agent-runtime` | Product Assembly / SDK host | Product Feature、SDK、Extension adapter | 接收 typed parts,不创建 concrete manager | +| `ToolRuntimeBuilder` / Tool contracts | Execution | tool/execution owner | Product Assembly | Agent Kernel 通过 execution port、Harness | 执行 tool,不选择产品形态 | +| `HarnessRegistryBuilder` | Execution | harness owner | Product Assembly | Agent Kernel 通过 registry contract、Product Feature | 注册 workflow provider,不执行 UI 展示 | +| `PluginRuntimeHost` / compatibility adapter | Extension | plugin runtime host / adapter | Product Assembly | Product Assembly、UI host、Execution provider registry | 产出 descriptor/provider,不写权威状态 | +| `RuntimeServicesBuilder` / platform providers | Cross-platform Adapter | services/adapters/app provider | Product Assembly | Kernel/Execution/Harness 通过 port handle | 实现边界外 I/O,不读取 product profile | +| `SecurityDecisionPort` / `CapabilityEffectPolicy` | Stable Contracts and Security Control Plane | 注入式策略 owner | Product Assembly | Kernel、Execution、Extension、UI projection | 决策可审计,模型/插件不能直接授权 | +| `AgenticFrontendEvent` / frontend event projection | Stable Contracts and Security Control Plane | `bitfun-events` | 无运行时注册 | Tauri/WebSocket/UI extension/OpenCode adapter | 只定义 event name/type/payload;delivery 由 adapter 执行 | + +典型调用链: + +1. 产品入口选择 `DeliveryProfile` 和 feature bundle。 +2. Product Assembly 创建或接收 concrete providers,并构建 `RuntimeServices`、tool registry、harness registry、 + Plugin Runtime Host binding、UI contribution registry 和 security policy。 +3. Product Feature 把 command、settings 和 UI contribution 映射为 kernel request、feature policy 或稳定 descriptor。 +4. Agent Kernel 消费 typed runtime parts 和稳定 contract,产生 event、permission request、tool request 和 task facts。 +5. Execution 通过 execution contract 处理 tool/harness/sandbox/MCP tool bridge;具体 I/O 只通过被注入的 platform + port 完成。 +6. Platform adapter 实现 platform/provider ports 并调用边界外资源;它不读取产品 profile,也不决定产品能力。 +7. Extension 只产出 descriptor、provider 和 candidate effects;最终授权、状态写入和审计仍走安全控制面。 + +## 8. 安全与鲁棒性约束 -注册器与前文目标层级的对应关系如下: - -| 注册器 / 组装点 | 所属目标层级 | 初始承载与目标承载 | 注册内容 | -|---|---|---|---| -| `ProductAssembler` / `ProductAssemblyPlan` | 产品组装层(Product Assembly) | 初始可在 `bitfun-core` facade 或产品入口;目标可收敛为 assembly owner | `DeliveryProfile`、`CapabilitySet`、feature group、adapter/service 选择 | -| `RuntimeServicesBuilder` | 执行原语层(Execution Primitives)与服务实现层(Services)的边界 | 目标在 `bitfun-runtime-services`;连接 `bitfun-runtime-ports`、`bitfun-services-*` 和初始 service wiring | filesystem、workspace、session store、Git、terminal、network、MCP catalog、remote connection / workspace / projection port | -| `ToolRuntimeBuilder` | 执行原语层(Execution Primitives) | `tool-execution`、`tool-contracts`、`tool-provider-groups`;Cargo package 名保持兼容 | tool provider、tool group、manifest、permission gate、tool hook | -| `HarnessRegistryBuilder` | 工作流编排层(Harness Layer) | 目标在 `bitfun-harness`;初始可由 `bitfun-core::agentic::harness` 注册 legacy-facade provider | SDD、Deep Review、DeepResearch、MiniApp 等 harness provider | -| `AgentDefinitionRegistry` | 执行原语层(Execution Primitives) | 目标在 `bitfun-agent-runtime`;初始可由 `bitfun-core` agent definition 代码承载 | agent、subagent、prompt module、skill definition | -| `ProductCommandRegistry` | 接口与入口层(Interfaces and Entrypoints)与产品组装层(Product Assembly)的边界 | 产品入口或 assembly 模块 | 输入框命令、审核入口、MiniApp 入口到 capability / harness / runtime request 的映射 | -| adapter set | 适配层(Adapters) | `bitfun-ai-adapters`、`bitfun-api-layer`、`bitfun-transport`、`bitfun-webdriver`、app adapters | AI、API、transport、WebDriver 等协议或外部 provider adapter | -| service set | 服务实现层(Services) | `bitfun-services-*`、`terminal-core` 和具体 app service implementations | OS、filesystem、Git、terminal、MCP、remote 的具体 service;Remote service 内部继续区分 SSH、relay、本地隧道、远端 OS 支持 | - -注册路径必须是显式、typed、可测试的: - -- 接口与入口层(Interfaces and Entrypoints)只选择 `DeliveryProfile` 和产品配置,不直接把 concrete manager 传入 runtime。 -- 产品组装层(Product Assembly)根据产品形态创建或接收 adapter/service,并调用 typed builder 完成注册。 -- Tool、OS、Remote、Protocol provider 分别留在对应 app、Adapters 或 Services 中,通过同一组 port 暴露。 -- Tauri 只能出现在 Desktop app、transport/API adapter 或产品入口命令外观中;Agent Runtime、 - Tool primitives、Harness、Runtime Services contract 和 Product Capabilities 不得依赖 Tauri handle、 - window、command macro 或 desktop app state。 -- Remote provider 必须拆分稳定连接接口和具体远端 OS / transport 实现,避免把 SSH、relay 或远端平台差异泄漏到 runtime。 -- 不支持的能力在 assembly 的 capability availability 中显式返回 unsupported / unavailable,不在 execution primitive 内写产品分支。 -- 禁止使用无类型 `Any` service locator、全局 mutable registry 或下层 crate 反向读取产品配置。 +- 安全边界是跨层控制面,不是 UI 弹窗或 tool 名称规则。每次 tool、MCP、plugin、hook、shell、network、file、 + browser/desktop 或 remote 动作都必须能归一为 capability/effect/security decision。 +- Agent Kernel 负责维护可审计事实:session、turn、agent/subagent、permission source、execution domain、event + sequence、cancellation、resume/checkpoint 和 DFX facts。 +- Execution 层必须在 tool / MCP / skills / harness 执行前消费 permission、sandbox 和 capability facts。 +- Extension 层必须声明来源、hash、能力、数据类别、副作用、执行域和 UI contribution 范围;未知或声明不完整的能力默认受限。 +- Cross-platform Adapter 必须表达执行位置和降级原因,例如 local host、remote SSH、container、ACP client、MCP server、 + plugin domain、browser/desktop 或 cloud task。 +- UI 只投影安全状态和用户选项,不成为最终授权来源;组织策略、安全拒绝和凭据保护不能被本地确认绕过。 +- 性能保护要求 registry 构建期注入优先,热路径避免无类型 service lookup、全局 mutable registry 和高成本同步扫描。 ## 9. 风险 | 风险 | 保护方式 | |---|---| -| 产品组装层(Product Assembly)膨胀为新的全局状态中心 | assembly 只做构建期注册,输出不可变 runtime parts;产品状态仍归 surface 或 runtime owner | -| 接口拆得过细,导致复杂度和动态分发成本上升 | 以 capability 和稳定用例定义 port 粒度,热路径避免运行时 map lookup,优先 builder-time 注入 | -| 平台实现泄漏到 Agent、Tool 或 Harness execution primitives | 依赖检查禁止 execution owner 依赖 app crate、Tauri、CLI TUI、ACP protocol 和 concrete service crate | -| core 拆分后仍隐式绑定 Tauri | Tauri 只允许在 Desktop app 或明确 feature-gated adapter;向下层传递 typed port、DTO、event fact 和 capability availability | -| 不同产品形态能力矩阵漂移 | Product Assembly 维护 capability matrix;减少或替换能力时补产品入口验证和 unsupported 行为测试 | -| Tool、MCP、ACP 的 manifest、permission 或事件语义拆解后不等价 | 保留旧路径兼容 facade,增加 manifest snapshot、permission 决策和事件映射等价测试 | -| Harness provider 只做注册但被误认为已经拥有执行语义 | descriptor-only / legacy-facade provider 只能生成 route plan;执行语义移动必须单独证明行为等价 | -| `bitfun-core` 只是改名为新的巨型 runtime crate | 新 owner crate 必须有单一职责和最小依赖;产品能力、harness、service 实现不得继续堆入 agent kernel | -| 目标 crate 先行创建但没有真实 owner | 只有 owner 边界、旧路径兼容、focused tests、依赖收益和 boundary check 同时成立时才创建 crate;否则继续留在 facade | +| 新 Agent Kernel 膨胀为新的巨型 core | Kernel 只拥有平台无关 Agent 状态机;feature、tool concrete、platform adapter 和 UI 扩展必须留在对应层 | +| 产品特性下沉到内核 | 用 feature pack / capability matrix 表达 `/goal`、MiniApp、DeepReview 等产品功能;内核只暴露通用能力 | +| Product Assembly 变成全局状态中心 | assembly 只做构建期注册,输出不可变 runtime parts;产品状态归 surface、feature 或 kernel owner | +| Product Assembly 之外的层直接依赖 concrete provider | 将 concrete provider 依赖限制在组装边界;普通调用方只消费 typed port、descriptor 或 stable contract | +| Plugin Runtime Host 绕过安全边界 | 插件只能产出候选效果和 contribution;授权、审计和状态写入由内核事实与安全控制面决定 | +| UI API 和 Rust API 分裂 | Product API 必须同时定义 Rust Kernel API 与 UI Extension Contract,并由 assembly 统一注册 | +| Adapter / Services 边界继续模糊 | 协议/provider translation 和 OS/service implementation 都属于平台/provider adapter 实现边界;不得拥有产品能力选择 | +| 外部系统被误建模为底层依赖层 | 外部资源只在 adapter I/O 边界出现;架构图和依赖规则不得要求 Kernel、Execution、Feature 直接依赖外部系统 | +| 多产品形态能力漂移 | capability matrix、unsupported/unavailable contract、Desktop/CLI/Web/SDK/ACP/Remote 验证矩阵同步维护 | +| 权限、工具、MCP、ACP 语义迁移后不等价 | 保留兼容 facade,补 manifest snapshot、permission decision、event mapping 和 product shape focused tests | +| 抽象只增不替换 owner | 设计不能只新增 facade 或空接口;新 owner 必须能承接真实职责,并让旧主体路径退化为兼容入口 | ## 10. 目标状态判定 -- `bitfun-core` 不再是事实上的完整 runtime owner,而是兼容 facade 和 `product-full` 组装边界。 -- Agent Runtime SDK 可在不依赖 `bitfun-core`、app crate 或 Tauri 的情况下被嵌入,并通过稳定 builder / - runner / event stream / registry API 提供 agent 能力。 -- Agent Runtime、Tool Contracts / Tool Provider Groups / Tool Execution、Runtime Services、Harness 和 Product Capabilities 分别拥有可审查的职责边界。 -- 稳定契约和各 execution owner 定义接口;具体 Tool、OS、Remote service 留在 Services,协议和外部 provider 转换留在 Adapters。 -- 产品组装层(Product Assembly)是唯一注册点,通过 typed builder / registry 连接接口和具体实现。 -- Tauri 只属于 Desktop app 或明确 feature-gated adapter,不进入 core、execution owner 或 contract crate。 -- runtime 只依赖 remote connection、remote workspace、remote projection 和 capability facts 等 port;SSH、relay、 - 本地隧道、远端 OS 差异和认证方式属于具体 Remote provider。 -- 产品形态差异通过 capability matrix 和 Product Assembly 表达,不通过下沉 UI、命令、协议或平台实现表达。 -- 权限、工具曝光、事件、session、remote workspace 和 release 构建形态必须保持功能等价。 +- `bitfun-core` 不再是事实上的完整 runtime owner,而是 compatibility facade、`product-full` 组装边界和少量迁移期 adapter。 +- Agent Kernel 能够在不依赖 `bitfun-core`、app crate、Tauri 或 Web UI 的情况下完成最小 session / turn / event stream。 +- 产品特性通过 feature pack / capability matrix 组装,`/goal` 等命令只配置和外放内核能力,不拥有内核状态机。 +- Product API 同时包含 Rust Kernel API 和 UI Extension Contract;OpenCode / ACP / plugin adapter 通过扩展层映射到这些 API。 +- Tool、MCP、skills、sandbox、local/remote runtime 和 harness 执行原语归执行层;具体 OS / provider / remote 实现归跨平台适配层。 +- 权限、sandbox、capability/effect、audit、unsupported/unavailable 和 execution domain 由稳定控制面表达。 +- Desktop、CLI、Web、ACP、Remote 和独立 SDK 都通过 Product Assembly 显式选择能力,不通过下层 `if desktop/cli/web` 分支表达差异。 +- 所有高风险迁移都有行为等价保护、依赖边界检查、产品形态验证和必要的性能/构建影响说明。 diff --git a/docs/architecture/opencode-plugin-surface-audit.md b/docs/architecture/opencode-plugin-surface-audit.md new file mode 100644 index 0000000000..d26c23abd9 --- /dev/null +++ b/docs/architecture/opencode-plugin-surface-audit.md @@ -0,0 +1,225 @@ +# OpenCode 插件兼容暴露面审计 + +本文独立审视 BitFun 当前核心迁移、公共 API 暴露面和未来受控接入 OpenCode +插件生态的风险。本文不替代 `core-decomposition.md` 和 +`agent-runtime-services-design.md`,也不记录单次 PR 进度或维护独立执行路线图。 + +## 1. 复核方式 + +本文记录架构风险和设计结论,不固化本机路径或临时分支快照。复核时应重新执行以下最小证据检查: + +- `git fetch gcwing main` 后对照 `gcwing/main` 检查 BitFun 当前 public surface、owner crate 和旧路径。 +- 在 OpenCode 工作区对照当前插件、tool registry、event、permission 和 TUI contribution 设计。 +- 在 Claw Code / Claude Code 类实现中对照 full product、lean automation / SDK 和 service 形态拆分。 +- 使用 `cargo metadata --no-deps`、`cargo tree`、workspace boundary checks 和目标文档 diff 验证依赖边界。 + +审计目标: + +1. 校验已有迁移是否真正形成 owner 边界,而不是只移动文件或新增 facade。 +2. 校验当前公共 API 是否适合作为 Agent Runtime SDK、插件 host 或 OpenCode adapter 的基础。 +3. 识别 OpenCode 分级适配前必须提前纳入计划的高风险工作。 +4. 区分必须立即收敛的架构债务和不应过早稳定的外部兼容承诺。 + +## 2. 复核结论 + +上一次审计中的核心担忧成立:BitFun 已经拆出多个 owner crate,但公共暴露面、 +旧路径兼容层和部分合同对象仍然偏宽,迁移完成度不能只按物理目录判断。 + +需要修正的判断是:代码行数、`pub mod` 数量或 `cargo tree` 依赖数量本身不是错误。 +它们只是风险信号。真正需要阻断后续迁移的是以下情况: + +- 新 owner 已存在,但上层仍把旧 owner 的具体对象当作主 API。 +- 稳定合同同时承载多个领域的内部 wire shape,导致调用方被迫依赖过宽语义。 +- Product Assembly 之外的模块同时认识接口和 concrete provider。 +- 插件或 SDK 入口需要直接导入 `bitfun-core/product-full`、产品命令 registry 或完整 + `RuntimeServices` bundle 才能工作。 + +因此,后续计划不应追求“马上重写成 OpenCode 插件系统”,而应先做公共面收口: +把 stable external API、workspace-internal API 和 compatibility API 分开,再基于 BitFun +自己的 Plugin Runtime Host 承接 OpenCode adapter。 + +## 3. 竞品结构信号 + +### 3.1 OpenCode + +OpenCode 的关键设计不是某个单独 crate 或包名,而是公共面分层: + +- Server plugin API 和 TUI plugin API 分离。服务端插件拿到 project、worktree、 + client、tool、permission、hook 等能力;TUI 插件拿到 route、slot、keymap、 + dialog、toast、state、theme 等 UI 能力。 +- Plugin host 通过稳定 handle、transform、registration API 连接内部服务,不把 + manager、registry 或内部状态对象直接暴露给插件。 +- Tool definition 是不透明值对象;tool registry 在执行前 materialize 当前可用工具 + 快照,并按 permission 过滤,执行时可以识别 stale tool call。 +- Event 服务有 manifest、version、aggregate sequence、durable replay 和 listener + 隔离。插件消费事件合同,而不是直接读取 session 内部结构。 +- Permission 服务保持权威;插件 hook 可参与 ask 流程,但不能绕过最终安全控制面。 + +对 BitFun 的结论:OpenCode adapter 不应成为 BitFun 内部真实 owner。 +BitFun 需要先有 Rust Kernel API、UI Extension Contract、Tool ABI、Event Manifest 和 +Permission/Effect Control Plane,再把 OpenCode API 映射到这些合同。 + +### 3.2 Claw Code + +Claw Code 的产品拆分提供了另一个有用信号:完整 CLI、轻量 automation harness 和 +独立 RAG service 被分成不同产品能力;安全、权限、NDJSON 输出、session 和 tool +contract 被明确约束。它也暴露了一个反例风险:runtime / tools / commands 的公共 +导出面很容易随功能增加而变宽,长期会降低 SDK 边界的可解释性。 + +对 BitFun 的结论:完整产品、SDK、CLI、Web、ACP、Remote 不应共享同一套全量公开面。 +轻量形态需要窄 API 和明确能力矩阵;完整形态可以由 Product Assembly 注入更多 provider。 + +### 3.3 Codex / Claude Code 类产品 + +同类产品的共同趋势是:Agent 内核、安全控制、工具执行、MCP/插件扩展、UI/命令入口和 +平台 provider 分开演进。用户可见命令和设置负责把能力外放;内核保持 session、 +permission、event、tool request、model routing 等通用事实;外部生态通过 descriptor、 +hook、tool provider 或协议 bridge 接入。 + +对 BitFun 的结论:生态插件能力不应从 `/goal`、DeepReview、MiniApp 或某个产品命令 +反推出来,而应由统一 extension contract 向产品特性注册 contribution。 + +## 4. BitFun 当前暴露面复核 + +### 4.1 `bitfun-core` + +结论:当前 `bitfun-core` 仍是 compatibility facade 与 product-full assembly 的混合体。 +它可以作为过渡层存在,但不能继续成为新功能主入口。 + +风险信号: + +- `bitfun-core` 仍 re-export `ExecutionEngine`、`StreamProcessor`、`ToolPipeline`、 + `ToolRegistry`、`BackendEventManager`、`ConfigManager`、`WorkspaceManager` 等具体对象。 +- no-default 构建仍牵引多个 runtime/service/transport crate 和若干三方库。该事实不等于错误, + 但说明它还不是可对外承诺的薄 SDK facade。 +- core 中大量旧路径 re-export 合理用于兼容,但如果新代码继续依赖这些路径,迁移会回流。 + +准出要求: + +- 新调用方不得新增 `bitfun-core::agentic::*`、`bitfun-core::service::*` 作为主依赖。 +- 旧路径必须标记为 compatibility API;真实 owner 应在对应 crate 的 `api` / `prelude` + 或模块级合同中公开。 +- `bitfun-core` 只能选择、组装或转发,不保留新 owner 的核心状态机或 concrete provider。 + +### 4.2 `bitfun-agent-runtime` + +结论:它已承接大量 Agent 事实和决策逻辑,但顶层公共面仍像 workspace-internal 实现集合, +不适合直接作为外部 Agent Runtime SDK。 + +合理部分: + +- 在 workspace 内迁移期,多个 `pub mod` 有助于从 core 旧路径转发并保持测试可见性。 +- 当前 API version 仍是 preview,不应为了外部发布过早做破坏性收口。 + +需要补齐: + +- 区分 `sdk` / `api` / `prelude` 的稳定外部面,与 workspace-internal 模块。 +- DeepReview、custom agent、skills、thread goal 等产品或策略模块不能自然变成 SDK 顶层合同。 +- SDK 调用方应只看到 builder、runner、request/response、event stream、typed error、 + registry/provider handle,而不是 session manager 内部结构。 + +### 4.3 `runtime-ports` + +结论:`runtime-ports` 当前承担了过多领域合同。问题不是文件长度,而是不同领域 DTO 和 +port 的版本、依赖和安全语义被绑在一起。 + +高风险混合领域: + +- OS/service port、workspace filesystem/shell、terminal execution。 +- permission、runtime event、remote workspace/projection/capability。 +- agent dialog/session/thread goal/dynamic tool/transcript。 + +建议方向: + +- 先按模块分组和导出面分类,避免立即大规模 crate 拆分带来 churn。 +- 对外稳定面按 service ports、agent lifecycle、tool ABI、remote/session workspace、 + permission/effect、event manifest 分域。 +- 合同对象避免泄漏当前实现字段。新字段必须有默认值、版本策略和兼容测试。 + +### 4.4 `RuntimeServices` + +结论:`RuntimeServices` 适合作为 Product Assembly 构建出的内部 typed bundle,但不适合 +直接作为插件或外部 SDK 的公共 API。 + +风险: + +- 公开字段让调用方天然知道全部底层能力,容易退化为 service locator。 +- 插件如果拿到完整 bundle,会绕过 capability-scoped handle 和 permission/effect 声明。 + +建议方向: + +- Product Assembly 可以持有完整 bundle。 +- Kernel、Execution、Extension、Plugin 只拿到能力子集 view,例如 workspace、tool、 + permission、event、artifact、remote facts。 +- SDK API 暴露 builder 注入点和 narrow runtime context,不暴露完整 bundle 字段。 + +### 4.5 Tool contract + +结论:当前 `tool-contracts` 已经承接了很多 provider-neutral 工具语义,但通用 ABI 和 +BitFun 产品策略仍有混合。 + +应保留在 Tool ABI 的内容: + +- tool name、schema、description、execute context、result、attachment、metadata。 +- permission/effect 声明、readonly/concurrency facts、artifact reference、cancellation。 +- materialized snapshot、stale call guard、provider identity。 + +应迁出或作为 decorator 的内容: + +- collapsed tool 的产品提示策略。 +- MiniApp headless restriction。 +- delegation policy 对具体 tool 名称的默认拦截。 +- 特定产品功能的 tool 排序和 manifest 文案。 + +### 4.6 Product capability / Harness + +结论:`legacy_facade` 是合理过渡标记,不应被写成“已迁移完成”。它只说明路由计划已归档, +不说明 concrete workflow execution 已归 owner。 + +建议方向: + +- capability pack 描述 service、tool group、harness provider、UI contribution、extension + capability 的组合关系。 +- 当 concrete execution 迁移后,capability pack 只引用 owner provider id。 +- 对 DeepReview、MiniApp、DeepResearch 等复杂功能,迁移完成必须包含执行主体迁移、 + UI/command descriptor、event/permission 等价和旧路径收敛。 + +## 5. OpenCode 适配的未计划高风险项 + +以下事项必须进入后续计划,否则 OpenCode 分级适配会把当前边界债务固化为外部合同。 + +| 风险项 | 风险 | 解决方法 | +|---|---|---| +| 插件生命周期 | 插件 install / activate / deactivate / reload / dispose 不清晰,容易泄漏状态或无法回滚 | 建立 BitFun Plugin Runtime Host lifecycle,所有 provider/contribution 注册必须可撤销 | +| Rust / UI API 混用 | OpenCode server plugin 和 TUI plugin 能力不同,混用会让 UI 依赖进入内核 | 通过 Rust Kernel API / Plugin Runtime Host contract 与 UI Extension Contract 分别承接,再由 Product Assembly 统一注册 | +| Tool ABI 不稳定 | 插件 tool、MCP tool、built-in tool 走不同路径,permission 与 stale call 行为不一致 | 建立统一 materialized tool snapshot、provider identity、permission/effect filter 和 stale call guard | +| Event 无版本合同 | 插件消费内部事件字段后,后续重构会破坏生态 | 定义 public event manifest、version、aggregate identity、durable/replay 口径和 UI projection | +| Permission hook 越权 | 插件 hook 可能绕过最终授权或写 audit state | hook 只能产出 candidate decision;最终 decision、audit、policy write 由安全控制面完成 | +| UI contribution 缺口 | 没有 slots/routes/keymap/dialog/prompt/state view,OpenCode TUI 插件无法等价映射 | 先定义 descriptor-only UI host contract,再按 Desktop/Web/CLI 能力逐步实现 | +| Workspace/Remote 不一致 | 插件假设本地路径会破坏 remote、relay、web、SDK 形态 | 暴露 workspace identity、logical path、artifact URI、remote capability facts,不暴露本地绝对路径 | +| 外部包安全 | JS/TS 插件 runtime 涉及包来源、权限、secret、网络和崩溃隔离 | 第一阶段只做 native extension contract 和受限 adapter;JS runtime 需独立安全评审 | +| Config/provider transform | 直接开放 provider/model/config transform 会影响全局行为 | 采用 support matrix,先开放只读或 scoped transform,再补审计与回滚 | +| 产品能力漂移 | Desktop、CLI、Web、ACP、SDK 对插件能力支持不同 | 在 Product Assembly 维护 capability matrix 和 unsupported/unavailable contract | + +## 6. 与实施计划的关系 + +后续执行节奏只由 [`core-decomposition-plan.md`](../plans/core-decomposition-plan.md) 维护。 +本文只提供风险排序和计划映射,避免审计文档成为第二份路线图。 + +| 风险主题 | 计划映射 | 必须保留的准出要求 | +|---|---|---| +| 旧 public surface 过宽 | Stage A:Public API Closure | stable external、workspace-internal、compatibility API 明确分层,并阻断旧 core 路径回流 | +| Tool ABI / runtime context 混合 | Stage B:Tool ABI、Event Manifest 与 Security Control Plane | materialized snapshot、provider identity、permission/effect filter、stale call guard、public event manifest、version、aggregate identity、replay/retention 具备测试 | +| Plugin Runtime Host 生命周期和安全桥接 | Stage C:Plugin Runtime Host Foundation | contribution 以 descriptor 暴露,Product Assembly 内部 materialize provider;注册可撤销,候选效果不能写权威状态 | +| UI 扩展合同缺口 | Stage D:UI Extension Contract 与产品形态矩阵 | descriptor-only、只读 state view、入口 fallback 和 unsupported/unavailable 行为具备 round-trip 测试 | +| OpenCode 分级适配 | Stage E:OpenCode Compatibility Adapter | support matrix、typed unsupported、permission/effect、event manifest、UI contribution 和远程/workspace 映射全部可验证 | +| 剩余 concrete owner | Stage F:剩余 Concrete Owner 与 SDK Readiness | Product Assembly 选择 concrete provider;普通层级只依赖 port、descriptor 或 stable contract | + +## 7. 执行准则 + +- 不把 OpenCode API 直接稳定成 BitFun 内部 API。 +- 不把 full `RuntimeServices` bundle 或 `bitfun-core/product-full` 暴露给插件或 SDK。 +- 不把 UI implementation、Tauri state、React component 或具体 provider handle 下沉到内核。 +- 不接受只新增抽象、不删除或收敛旧路径的迁移 PR。 +- 不在安全控制面未完成前开放可写插件 hook。 +- 任何会改变工具曝光、权限语义、事件字段、remote 行为或产品能力矩阵的变更必须单独评审。 diff --git a/docs/architecture/plugin-runtime-host-design.md b/docs/architecture/plugin-runtime-host-design.md new file mode 100644 index 0000000000..641216a04d --- /dev/null +++ b/docs/architecture/plugin-runtime-host-design.md @@ -0,0 +1,471 @@ +# 插件运行时主机与生态兼容适配层设计 + +本文补充 [`core-decomposition.md`](core-decomposition.md)、 +[`agent-runtime-services-design.md`](agent-runtime-services-design.md) 和 +[`../sdlc-harness/features/opencode-compatibility.md`](../sdlc-harness/features/opencode-compatibility.md)。 +本文只描述目标架构、合同和风险边界,不记录实施进度。 + +## 1. 方案判断 + +插件扩展能力需要独立于 Agent Kernel、Product Assembly 和具体生态适配器。合理的目标模型是: + +- **Plugin Runtime Host** 管理插件兼容层生命周期、项目执行域、IPC、隔离、健康、超时、幂等和候选效果路由。 +- **Compatibility Adapter** 只负责把 OpenCode、Claude Code、Codex 插件或 BitFun 原生插件 API 映射为 BitFun 稳定合同。 +- **BitFun 主体进程** 只依赖 `PluginRuntimeClient`、规范信封、候选效果、能力声明和 UI descriptor,不依赖任何具体生态适配器类型。 + +该模型是合理的,因为它把“插件运行时治理”和“生态 API 翻译”拆开:主体进程可替换适配器,插件失败可降级,OpenCode +兼容不会反向成为 BitFun 内部 owner。若主体进程需要按 `OpenCodeAdapter`、`ClaudeCodeAdapter` 等类型分支, +或需要读取插件运行单元内部对象,则设计不成立。 + +## 2. 目标与非目标 + +目标: + +- 在 Agent Kernel 外提供受控 Plugin Runtime Host,承载 OpenCode 等生态兼容适配层。 +- 让 plugin、hook、自定义工具、事件订阅和 UI contribution 统一映射到 BitFun 的事件、Tool ABI、 + Permission/Effect 和 UI Extension Contract。 +- 保持 Kernel 是任务状态、事件和审计事实的权威源;Execution 是工具结果权威源;Security Boundary 是权限权威源。 + 插件只能返回候选效果。 +- 让 Desktop、CLI、Server、Remote、ACP、Web、Mobile Web 和 SDK 显式启用、禁用或降级插件能力。 + +非目标: + +- 不复制完整 OpenCode runtime,不承诺任意社区插件无修改运行。 +- 不把 JS/TS runtime、worker、WebView 或子进程视为安全边界。 +- 不允许插件直接写通过、失败、阻断、授权、审计、工具结果或产品状态。 +- 不把插件 API 暴露成无约束 localhost 服务;默认使用受控 IPC。 +- 不把插件运行时主机塞进 Agent Kernel、Tool Runtime、Harness 或 Product Assembly 的内部实现。 + +## 3. 目标逻辑视图 + +```mermaid +flowchart TB + Surface["产品入口与 UI
Desktop / CLI / Server / Remote / ACP / Web / Mobile Web / SDK"] + Assembly["Product Assembly
delivery profile / capability matrix / runtime binding"] + Kernel["Agent Kernel
event facts / permission coordination / task state / audit facts"] + Execution["Execution
Tool ABI / tool runtime / harness / sandbox"] + UIExt["UI Extension Contract
descriptor validation / fallback / projection"] + Client["PluginRuntimeClient
typed boundary in main process"] + Host["Plugin Runtime Host
lifecycle / IPC / adapter registry / project domain / effect router"] + Registry["Compatibility Adapter Registry"] + OpenCode["OpenCode Compatibility Adapter"] + Claude["Claude Code Compatibility Adapter"] + Codex["Codex Plugin Compatibility Adapter"] + Native["BitFun Native Plugin Adapter"] + Unit["Plugin Execution Unit
cell / worker / subprocess / sandbox"] + + Surface --> Assembly + Assembly --> Kernel + Assembly --> Client + Kernel --> Execution + Kernel --> UIExt + Kernel <--> Client + Client <--> Host + Host --> Registry + Registry --> OpenCode + Registry --> Claude + Registry --> Codex + Registry --> Native + OpenCode --> Unit + Claude --> Unit + Codex --> Unit + Native --> Unit + Host -. "PluginEffectCandidate" .-> Client + UIExt -. "safe descriptor" .-> Surface +``` + +依赖规则: + +- Product Assembly 选择是否启用插件运行时、选择适配器集合、注入 trust policy、execution domain 和 disabled reason。 +- Kernel 只通过 `PluginRuntimeClient` 通信;不加载插件代码,不读取 OpenCode 或其他生态内部对象。 +- Plugin Runtime Host 负责运行时治理和主机通信;不写权威状态,不绕过 Kernel 调用 service。 +- Compatibility Adapter 负责生态 API 到 BitFun 合同的转换;不拥有工具执行、权限决策、审计或 UI state。 +- UI 只消费经 UI Extension Contract 校验后的 descriptor,不执行插件返回的 UI 代码。 + +## 4. 主体进程 API 暴露面 + +BitFun 主体进程的插件相关 API 只允许包含下列稳定概念: + +| 概念 | 所属 | 作用 | +|---|---|---| +| `PluginRuntimeClient` | Kernel / Product Assembly 可见 | 主体进程调用插件运行时主机的 typed client | +| `PluginRuntimeBinding` | Product Assembly 可见 | enabled client、projection-only client 或 disabled stub | +| `PluginRuntimeAvailability` | Product Assembly 可见 | 表达交付形态、兼容等级、降级和不可用原因 | +| `PluginDispatchEnvelope` / `PluginResponseEnvelope` | Kernel 与 Host 边界 | 事件投递、响应、epoch、deadline 和 diagnostics | +| `PluginEffectCandidate` | Kernel / Security Boundary 可见 | 插件候选效果;必须重新校验后才能应用 | +| `UiContributionDescriptor` | UI host 可见 | 声明式 UI contribution;不携带可执行 UI | +| `PluginTrustPolicy` / `PluginSourceRef` | Security Boundary 可见 | 来源、hash、信任状态、能力和执行域 | + +禁止暴露: + +- `OpenCodeCompatibilityAdapter`、`ClaudeCodeCompatibilityAdapter`、`CodexPluginCompatibilityAdapter` 等具体适配器对象。 +- 插件进程句柄、worker handle、WebView、package manager client、解释器实例或内部 module object。 +- full `RuntimeServices`、concrete provider handle、session manager、UI store、Tauri handle 或产品命令 registry。 +- 生态原始 payload 作为公共 DTO;原始 payload 只能存在于对应 Compatibility Adapter 内部。 + +## 5. Product Assembly 能力模型 + +插件能力进入 Product Assembly 的 typed capability matrix,不隐藏在 hook、service locator 或全局 registry 中。 +这里的 `ExtensionCapabilitySet` 表示产品扩展能力聚合,不表示旧的扩展主机类型;插件运行时边界由 +`PluginRuntimeAvailability` 和 `PluginRuntimeBinding` 表达。 + +```rust +pub struct ExtensionCapabilitySet { + pub plugin_runtime: PluginRuntimeAvailability, + pub adapters: Vec, + pub ui: UiExtensionAvailability, +} + +pub enum PluginRuntimeAvailability { + Disabled { reason: UnsupportedReason }, + ProjectionOnly { reason: UnsupportedReason }, + Enabled { + compat_level: PluginCompatibilityLevel, + execution: PluginExecutionLocation, + trust_policy: PluginTrustPolicyId, + fallback: PluginFallbackMode, + }, + Unavailable { reason: UnsupportedReason }, +} + +pub enum PluginRuntimeBinding { + Disabled(DisabledPluginRuntimeClient), + ProjectionOnly(ProjectionOnlyPluginRuntimeClient), + Client(Arc), +} +``` + +SDK minimal feature 只能依赖 disabled stub 或测试 fake,不得隐式启动 JS/TS runtime。`DeliveryProfile` 只能影响 +binding 和 capability 选择,不得让 Agent Kernel 出现 `if desktop`、`if cli` 或 `if opencode` 分支。 + +## 6. 领域模型 + +| 领域对象 | 定义 | 关键约束 | +|---|---|---| +| `PluginRuntimeHost` | 管理插件兼容层生命周期、通信和隔离的运行时主机 | 不写权威状态,不执行产品策略 | +| `PluginRuntimeClient` | 主体进程调用 Host 的窄接口 | 不暴露具体生态适配器或运行单元句柄 | +| Host-internal `CompatibilityAdapter` | 生态 API 映射器 | 只做翻译,不拥有权限、工具结果或 UI state | +| `PluginExecutionUnit` | cell、worker、subprocess 或 sandbox | 只能访问 Host facade 白名单 | +| `ProjectExecutionDomain` | workspace、trust、permission、tool registry 和 event subscription 的隔离域 | local/remote 必须用 logical path 和 execution domain 表达 | +| `PluginSourceRef` | 插件来源、版本、hash、签名和作用域 | hash 或来源变化必须重新信任 | +| `PluginTrustPolicy` | 信任状态、能力范围、撤销策略和执行等级 | 默认拒绝未知能力 | +| `PluginEffectCandidate` | 建议、证据、工具候选、权限候选或 UI contribution 候选 | 不能表示最终授权、审计或工具结果 | +| `UiContributionDescriptor` | UI contribution 的声明式描述 | 不能包含 React、DOM、Tauri 或可执行代码 | + +## 7. 关键模块开发视图 + +目标放置原则: + +```text +src/crates/contracts + core-types / events / runtime-ports + plugin runtime DTO、event、capability/effect、trust、UI descriptor contract + +src/crates/execution + agent-runtime + consumes PluginRuntimeClient through typed runtime parts + tool-contracts / tool-execution + materializes accepted tool provider candidates through Tool ABI + +src/crates/adapters + opencode-compatibility-adapter + maps OpenCode API to BitFun canonical envelopes and candidates + future compatibility adapters + map other plugin ecosystems through the same adapter trait + +src/crates/services + services-integrations / terminal / services-core + provide process、IPC、filesystem、network、remote 和 sandbox primitives behind ports + +src/crates/assembly + product-capabilities / core + selects DeliveryProfile, PluginRuntimeBinding, adapter set and fallback policy + +src/apps/* / src/web-ui / src/mobile-web + UI host and product entrypoints + render validated descriptors or typed unsupported states +``` + +开发约束: + +- Compatibility Adapter 可以依赖规范合同和必要 protocol parser;不能依赖 `bitfun-core/product-full`、UI implementation + 或 concrete service manager。 +- Host facade 由插件运行时主机提供;插件运行单元不能直接调用 OS、shell、network、filesystem 或 credentials。 +- Tool provider candidate 只有被 Kernel、Security Boundary 和 Tool ABI 接受后,才能 materialize 为可执行 provider。 +- UI contribution 只能通过 descriptor 进入 UI host;前端渲染实现位于对应产品入口。 + +## 8. 运行时与通信模型 + +默认模型是一个受 Product Assembly 选择的 Plugin Runtime Host。Host 可以承载多个项目执行域,但每个执行域必须隔离: + +- workspace、worktree、execution host、logical path。 +- trust record、permission scope、tool override table、event subscription。 +- 插件状态、环境变量、依赖缓存、审计流和资源预算。 + +Host 内部按风险选择执行单元: + +| 等级 | 适用场景 | 约束 | +|---|---|---| +| cell | 受信任的观察、建议和只读 hook | 无 shell、无网络、无凭据;必须有 deadline | +| worker | 轻量 guard、格式化建议、低风险工具候选 | 只能调用 Host facade 白名单 | +| subprocess | 高风险依赖、工具复写、崩溃隔离需求 | 独立进程、环境白名单、资源预算、工作目录限制 | +| sandbox | 未知来源或强隔离场景 | 无凭据、受控网络、临时或只读 worktree、可审计 | + +Remote workspace 场景下,Host 应靠近实际执行域运行。UI 端只接收 logical path、descriptor、diagnostic 和审计摘要; +不得把远端 absolute path、SSH 细节或远端 OS 差异泄漏给插件 API 消费方。 + +## 9. IPC 与候选效果契约 + +传输可以用 JSON-RPC、framed protobuf 或 gRPC over pipe 实现;稳定合同只承诺 schema、语义和错误模型。 +Host 生命周期由 Product Assembly 或 Host supervisor 管理;Kernel 只通过 `PluginRuntimeClient` 投递事件、刷新快照并消费响应。 + +| 方法 | 方向 | 语义 | +|---|---|---| +| `runtime.initialize` | Assembly / Supervisor -> Host | 传入 runtime version、adapter manifest set、policy snapshot 和 transport capability | +| `runtime.health` | Assembly / Supervisor -> Host | 健康、负载、执行单元状态和降级原因 | +| `runtime.shutdown` | Assembly / Supervisor -> Host | 关闭执行域、适配器或运行时主机 | +| `project.open` | Assembly / Supervisor -> Host | 创建或恢复项目执行域 | +| `project.refresh` | Kernel / Assembly -> Host | 更新 workspace、trust、permission、tool registry 或 policy epoch | +| `plugin.discover` | Assembly / Supervisor -> Host | 发现配置、插件来源、hash、声明能力和兼容等级 | +| `event.dispatch` | Kernel -> Client -> Host | 投递规范事件并等待候选效果或超时 | + +```ts +interface PluginDispatchEnvelope { + envelope_version: 1; + event_id: string; + event_type: string; + event_version: string; + project_domain_id: string; + workspace: WorkspaceRef; + source: PluginSourceRef; + correlation_id: string; + causation_id?: string; + idempotency_key: string; + deadline_ms: number; + project_epoch: number; + trust_epoch: number; + policy_epoch: number; + tool_registry_epoch?: number; + payload_ref?: PayloadRef; + payload?: CanonicalPayload; +} + +interface CanonicalPayload { + schema_version: string; + data_classification: DataClassification; + redaction: "none" | "partial" | "full"; + value: unknown; +} + +interface PluginResponseEnvelope { + envelope_version: 1; + request_event_id: string; + project_domain_id: string; + adapter_id: string; + plugin_id?: string; + completed_at_ms: number; + effects: PluginEffectCandidate[]; + diagnostics: PluginDiagnostic[]; + observed_epochs: { + project_epoch: number; + trust_epoch: number; + policy_epoch: number; + tool_registry_epoch?: number; + }; +} + +interface PluginEffectBase { + effect_id: string; + schema_version: string; + declared_capability: CapabilityId; + target_ref: TargetRef; + data_classification: DataClassification; + risk_level: "low" | "medium" | "high"; + requires_permission: boolean; + source_ref: PluginSourceRef; +} + +type PluginEffectCandidate = + | (PluginEffectBase & { kind: "suggestion"; body: CanonicalPayload | PayloadRef }) + | (PluginEffectBase & { kind: "evidence_candidate"; body: CanonicalPayload | PayloadRef }) + | (PluginEffectBase & { kind: "tool_input_patch_candidate"; tool_call_id: string; patch: CanonicalPayload | PayloadRef }) + | (PluginEffectBase & { kind: "tool_provider_candidate"; manifest: ToolProviderDescriptor; source: PluginSourceRef }) + | (PluginEffectBase & { kind: "post_tool_evidence_candidate"; tool_call_id: string; body: CanonicalPayload | PayloadRef }) + | (PluginEffectBase & { kind: "permission_candidate"; request_id: string; recommendation: "allow" | "deny"; reason: string }) + | (PluginEffectBase & { kind: "ui_contribution_candidate"; descriptor: UiContributionDescriptor }) + | (PluginEffectBase & { kind: "event_subscription_candidate"; events: string[] }) + | (PluginEffectBase & { kind: "unsupported"; capability: string; reason: string }); + +interface UiContributionDescriptor { + descriptor_version: 1; + contribution_id: string; + source_ref: PluginSourceRef; + surfaces: Array<"desktop" | "web" | "mobile_web" | "cli" | "server" | "remote" | "acp" | "sdk">; + slot: "panel" | "command_palette" | "settings" | "notification" | "status" | "text_projection"; + component_kind: "text" | "summary" | "form" | "list" | "status_badge" | "command"; + data_refs: PayloadRef[]; + allowed_actions: ProductCommandRef[]; + required_capabilities: CapabilityId[]; + permission_prompt?: PermissionPromptDescriptor; + fallback: { + mode: "hide" | "disabled" | "text_projection" | "unsupported_message"; + message_key?: string; + }; + accessibility: { + label_key: string; + description_key?: string; + }; + i18n_namespaces: string[]; +} +``` + +`unknown` 只能出现在带 `schema_version`、`data_classification` 和 `redaction` 的 canonical payload 内部。不得在公共 +API 中绕过 schema 传递裸 JSON。 + +`UiContributionDescriptor` 不得包含 React component、HTML script、DOM selector、Tauri command、store mutation +或任意可执行代码。未知 slot、未知 action 或缺失 capability 时,入口必须按 fallback 降级。 + +`tool_result`、`permission_granted`、`audit_written` 和 `state_changed` 不允许作为插件响应类型。真实工具结果由 +Execution 写入;真实权限状态由 Security Boundary 写入;审计事实由 Kernel 写入。 + +## 10. 关键时序 + +### 10.1 插件运行时启动 + +```mermaid +sequenceDiagram + participant Surface as Product Surface + participant Assembly as Product Assembly + participant Kernel as Agent Kernel + participant Host as Plugin Runtime Host + participant Adapter as Compatibility Adapter + + Surface->>Assembly: select DeliveryProfile + Assembly->>Assembly: build ExtensionCapabilitySet + Assembly->>Host: runtime.initialize(adapter manifest set, policy snapshot) + Host->>Adapter: load adapter descriptors + Adapter-->>Host: supported capabilities + Host-->>Assembly: PluginRuntimeBinding::Client or disabled reason + Assembly->>Kernel: inject PluginRuntimeClient +``` + +### 10.2 事件分发与候选效果 + +```mermaid +sequenceDiagram + participant Kernel as Agent Kernel + participant Client as PluginRuntimeClient + participant Host as Plugin Runtime Host + participant Adapter as Compatibility Adapter + participant Security as Security Boundary + + Kernel->>Client: dispatch PluginDispatchEnvelope + Client->>Host: event.dispatch + Host->>Adapter: map canonical event to ecosystem API + Adapter-->>Host: candidate effects + Host-->>Client: PluginResponseEnvelope + Client-->>Kernel: response with observed epochs + Kernel->>Kernel: check epoch, idempotency and capability + Kernel->>Security: validate permission/effect + Security-->>Kernel: accept, reject or typed unsupported +``` + +### 10.3 UI contribution 投影 + +```mermaid +sequenceDiagram + participant Adapter as Compatibility Adapter + participant Host as Plugin Runtime Host + participant Kernel as Agent Kernel + participant UIExt as UI Extension Contract + participant Surface as UI Host + + Adapter-->>Host: ui_contribution_candidate + Host-->>Kernel: PluginResponseEnvelope + Kernel->>UIExt: validate descriptor and capability + UIExt-->>Surface: safe descriptor or fallback + Surface->>Surface: render host-native UI +``` + +## 11. OpenCode 兼容映射 + +OpenCode Compatibility Adapter 是 Plugin Runtime Host 内部适配器,不是 BitFun 内部插件模型。 + +| OpenCode 能力 | BitFun 映射 | 约束 | +|---|---|---| +| workspace / global plugin 配置 | `plugin.discover` 的来源、hash、声明能力和信任候选 | 默认发现但不执行 | +| hooks object | 规范事件订阅和 `event.dispatch` | hook 只返回候选效果 | +| custom tools | `tool_provider_candidate` | 启用后仍走 Tool ABI 和权限 gate | +| tool execute before | `tool_input_patch_candidate`、`permission_candidate`、`suggestion` | 不能直接改写执行结果 | +| tool execute after | `post_tool_evidence_candidate` | 不能伪造 tool result | +| permission hooks | `permission_candidate` | 不能直接 approve | +| client / server API | 受限 adapter facade | 只暴露规范事件、只读状态和候选提交 | +| shell helper | 默认禁用;可映射为受控 tool request 候选 | 必须经过 shell 权限和 sandbox 策略 | +| SSE event stream | Kernel 规范事件订阅 | 不暴露 Host 内部事件作为权威流 | +| TUI/UI contribution | `ui_contribution_candidate` | 只允许 descriptor,不允许直接操作 UI state | + +OpenCode payload 只能存在于 Adapter 内部。进入 Kernel、Execution、UI 或质量数据面的对象必须先转换为 BitFun 规范 +DTO。 + +## 12. 产品形态与降级 + +| 产品形态 | 插件运行时策略 | UI 策略 | 失败语义 | +|---|---|---|---| +| Desktop / product-full | 可启用本地 Host;高风险能力按 trust policy 提权 | 渲染已校验 descriptor | host crash 或 timeout 不影响默认任务 | +| CLI | 可启用本地 Host 或只读投影 | 文本 descriptor / warning | unsupported 明确输出,不静默忽略 | +| Server | 默认受控启用或 disabled stub,取决于部署策略 | API 返回 typed unsupported 或 descriptor projection | 不自动启动本地 JS/TS runtime | +| Remote | Host 靠近远端执行域 | UI 只接收 logical path 和 descriptor | 不回落到本地路径执行 | +| ACP | 以 protocol adapter 暴露可组合 capability | 以 ACP 能力或 unsupported 表达 | 不把插件失败解释为 agent 失败 | +| Web / Mobile Web | 不启动本地 Host | 只消费后端投影 descriptor | 不持有插件执行单元 | +| SDK minimal | disabled stub 或调用方注入 fake/client | 无默认 UI host | 不牵引 product-full 或 concrete provider | + +能力矩阵: + +| 能力 | Desktop / product-full | CLI | Server | Remote | ACP | Web / Mobile Web | SDK minimal | +|---|---|---|---|---|---|---|---| +| discovery | 支持本地/项目发现 | 支持本地/项目发现 | 由部署策略启用 | 在远端执行域发现 | 通过 ACP capability 暴露 | 只消费后端投影 | 调用方注入或 disabled | +| read-only event hook | 支持候选建议/证据 | 支持文本诊断 | 支持 API diagnostic | 靠近远端 Host 执行 | 映射为 ACP event/capability | 只展示投影 | fake/client 可选 | +| tool provider candidate | 支持,必须经 Tool ABI | 支持,必须经 Tool ABI | 默认受部署策略限制 | 绑定远端 execution domain | 只作为 external tool capability | 不执行 | 默认 disabled | +| permission candidate | 只作建议,最终由安全边界决策 | 只作建议 | 只作 API 候选 | 绑定远端 trust/policy epoch | 映射为 ACP permission bridge | 只展示状态 | 默认 disabled | +| UI contribution | descriptor 渲染 | 文本或命令投影 | API projection / unsupported | 只使用 logical path | ACP 能力或 unsupported | descriptor 投影 | 默认无 UI host | +| shell helper | 默认禁用,可映射为 tool request 候选 | 默认禁用 | 默认禁用 | 只允许远端策略批准 | 不直接暴露 shell | 不执行 | disabled | +| read-only state view | 支持脱敏投影 | 支持文本投影 | 支持 API 投影 | 支持远端脱敏投影 | 映射为 protocol state | 支持只读投影 | fake/client 可选 | +| writable JS/TS runtime | 非默认能力,需独立安全评审 | 非默认能力 | 非默认能力 | 非默认能力 | 不作为默认能力 | 不执行 | disabled | + +产品入口必须区分“未启用”“只读建议”“能力不可用”“候选被接受”。插件降级不能被解释为任务成功保障。 + +## 13. 安全与供应链 + +插件能力必须绑定: + +- 来源:project、global、enterprise registry、signed bundle 或 remote source。 +- 身份:adapter id、plugin id、版本、hash、签名状态。 +- 能力声明:事件、tool、文件、网络、凭据、UI contribution 和执行域。 +- 信任状态:未信任、只读信任、项目级信任、组织级信任、撤销。 +- 执行位置:local、remote、worker、subprocess、sandbox、container。 + +安全规则: + +- 未信任插件只能发现和展示,不能执行。 +- 凭据、网络、shell、process spawn、package install 和动态 import 默认拒绝。 +- 文件访问使用 logical path 和 workspace policy,不能暴露 host absolute path。 +- 配置、插件源码或 package hash 变化后必须重新信任。 +- 插件失败、崩溃、超时和旧 epoch 响应必须可诊断、可丢弃、可审计。 + +## 14. 验证矩阵 + +| 验证面 | 必须覆盖 | +|---|---| +| 主体进程 API | 只暴露 runtime client、binding、envelope、candidate、trust 和 descriptor | +| schema | dispatch / response / effect candidate serialization round-trip | +| 时序 | initialize、deadline、cancel、stale epoch、idempotency key | +| 权限 | plugin 不能 approve、不能写审计、不能伪造 tool result | +| 工具 | tool provider candidate 经 Tool ABI 和 permission gate 后才能 materialize | +| UI | descriptor 校验、未知 contribution fallback、无直接 UI state mutation | +| 安全 | 未信任插件不执行、hash 变化重新信任、secret/network/shell 默认拒绝 | +| 崩溃 | host、worker、subprocess 或 adapter failure 不影响默认任务 | +| 远程 | logical path、remote execution domain 和权限范围不泄漏本地路径 | +| 产品形态 | Desktop、CLI、Server、Remote、ACP、Web、Mobile Web、SDK 的 unsupported 行为明确 | + +通过标准是:插件运行时缺失、适配器不支持某能力、插件失败或外部插件返回非法结果时,默认 BitFun 任务行为、 +权限语义、工具执行和审计事实保持等价。 diff --git a/docs/architecture/theme-token-optimization.md b/docs/architecture/theme-token-optimization.md index 8defb48b40..d4d81d885c 100644 --- a/docs/architecture/theme-token-optimization.md +++ b/docs/architecture/theme-token-optimization.md @@ -1,6 +1,9 @@ # 主题与颜色 Token 优化方案 -> 当前基线:`gcwing/main` 的 `8d85e236`,扫描日期为 2026-06-18。 +> 当前治理基线以 `scripts/theme-color-governance-baseline.json`、 +> `scripts/theme-color-governance-baseline.mobile-web.json`、 +> `scripts/theme-color-governance-baseline.installer.json`、 +> `scripts/theme-color-governance-baseline.cli.json` 和审计脚本输出为准。 本文档用于梳理 BitFun 前端主题、硬编码颜色、重复 token、近似色冗余、 命名漂移和后续治理方案。目标不是把所有看起来相近的颜色都合并,而是让 @@ -13,7 +16,11 @@ - `src/web-ui` 中的主题预设、运行时 CSS 变量注入和共享样式 token。 - `src/web-ui/src/component-library/styles` 下的 token 定义。 +- `src/mobile-web` 中的独立 mobile theme preset、运行时 CSS 变量和移动端组件样式。 +- `BitFun-Installer/src` 中的安装器主题数据、运行时变量注入、静态变量和安装流程组件样式。 +- `src/apps/cli` 中的 CLI/TUI 主题 preset JSON、terminal ANSI 适配和外部主题兼容 fallback。 - 组件 SCSS/CSS/TSX 中的硬编码颜色、fallback 色值和局部 token。 +- Rust/desktop 侧由 TS 主题预设生成的最小投影 manifest。 - 旧 token 名称到新规范名称的兼容别名。 - 后续防止新增硬编码颜色的审计和约束规则。 @@ -51,13 +58,118 @@ - 对每个页面做像素级重设计。 - 在所有调用方迁移完成前移除兼容别名。 +## 主题扩展职责边界 + +主题扩展必须把“持久化/首屏引导”和“完整渲染契约”拆开维护,避免 Rust 与 +TS 各自增长一套主题定义。参考经验: + +- [OpenCode theme](https://opencode.ai/docs/themes/) 采用可扩展 JSON schema、内置/用户/项目多层加载、 + 可复用色值定义和 light/dark variant;扩展点贴近 TUI 渲染层,host 只负责加载顺序和选择。 +- [Claude Code theme](https://code.claude.com/docs/en/terminal-config) 通过 `/theme`、theme picker 和 auto + light/dark 匹配终端背景,同时明确不接管终端自身配色;host 只适配环境,不复制完整 palette。 +- [Codex config](https://developers.openai.com/codex/config-basic) 使用分层 config 和明确优先级; + 对 BitFun 的启发是:Rust/host 可以读取偏好与可信配置层,但不应成为 UI token 语义 owner。 + +OpenCode 的 agent/plugin 色彩入口保持为小型语义集合: +`primary`、`secondary`、`accent`、`success`、`warning`、`error`、`info`。 +BitFun 后续适配插件体系时也必须遵循同一边界:插件消费 TS 侧生成的 compact projection, +不得直接读取完整 `ThemeConfig`、runtime CSS var 列表或 generated widget payload allowlist。 +当前 `src/web-ui/src/infrastructure/theme/pluginThemeProjection.ts` 是插件语义色映射入口, +只把完整主题投影成 7 个外部可扩展 key;如插件未来确实需要 surface/text/border, +应新增另一个小型 projection,并把 key 上限和语义写入本文档与审计,而不是扩大 Rust 或插件侧自由 key。 + +当前职责边界如下: + +| 事项 | Rust/desktop 侧 | TS/web-ui 侧 | +| --- | --- | --- | +| 主题选择持久化 | 启动时只读取 `themes.current`,解析 `system`、内置主题 id 和未知值回退;历史 `theme.id` 只在配置加载/导入时一次性归一到 `themes.current`,不能作为新扩展入口。 | 读写 `themes.current`,处理 `system`、内置主题和 custom theme 选择。 | +| 完整主题契约 | 不维护完整 `ThemeConfig`、semantic token、component token 或专用 palette;旧 Rust GUI theme struct/provider 已退役,默认导出不得出现顶层 `theme`。 | 拥有 `ThemeConfig`、主题预设、validator、import/export、runtime CSS 变量注入和审计 registry。 | +| 首屏 bootstrap | 只注入 WebView 首屏所需最小投影:`data-theme`、`data-theme-type`、核心背景/文本 CSS 变量和 `--bitfun-startup-bg`。 | JS 启动后必须重新应用完整主题,覆盖 bootstrap 投影并恢复 Monaco、Mermaid、terminal、widget payload 等专用域。 | +| 生成式 UI 主题提示 | 只读取 TS 预设生成的 prompt snapshot manifest,用于模型提示;不得手写内置主题 palette。 | 拥有 prompt snapshot 投影规则;新内置主题加入 `builtinThemes` 后由生成器同步到 Rust 只读 manifest。 | +| 插件主题色扩展 | 不维护插件色彩 schema,不把 plugin color 解析放到 Rust/desktop。 | 拥有 `pluginThemeProjection.ts`,把完整主题映射到 OpenCode-compatible 的 7 个语义色 key;插件不得直接依赖内部 CSS var 全量表。 | +| 内置主题扩展 | 不手写新增完整 palette。只有新内置主题需要首屏无闪烁时,才更新最小 bootstrap 投影。 | 新主题先进入 TS 预设和 `builtinThemes`;所有语义、组件和专用域 token 以 TS 侧为准。 | +| custom theme | 不解析 `themes.custom`,不复制 custom schema;保存的 custom id 在 Rust 启动阶段不可用时使用系统/默认首屏回退。 | 加载、校验、注册、注销、导入导出 custom theme;custom 加载完成后覆盖 Rust 首屏回退。 | +| CSS 变量和 key 命名 | 只允许新增明确写入启动主题投影 manifest 的首屏 key;不得新增 backend theme service 或平行 alias 表。 | 新 primitive/semantic/component key、兼容 alias、surface rename、dynamic family 和 widget payload key 均在 TS contract/audit 中登记。 | +| 专用渲染域 | Core/web host 不维护 Monaco、Mermaid、Prism、language identity、UI exception 等 web-ui 色板;CLI/TUI preset 和 terminal ANSI 是独立终端 surface,不能成为 web-ui 主题源头。 | web-ui 专用域由各自 TS owner 维护,按 `colorDomainScopes.*` 和 `colorDomainNearPairs.*` 预算治理。 | + +Mobile Web 和 Installer 是独立产品形态,但主题扩展位置仍在各自 TS 侧维护: +`src/mobile-web/src/theme/presets` 拥有 mobile runtime key,`BitFun-Installer/src/theme` +和 `BitFun-Installer/src/styles/variables.css` 拥有安装器运行时与首屏静态变量。 +Rust 侧不得因为安装器需要编译或首屏显示而复制完整 palette;安装器 Rust 只处理安装流程、 +API/DTO 映射和必要的启动壳逻辑。跨 root 动态 key 必须在 `scripts/theme-css-var-contract.mjs` +登记 owner,审计脚本按 root owner 判断 stale dynamic family,避免 web-ui、mobile-web 和 +installer 互相误报或重复定义。 + +CLI/TUI 是独立终端产品 surface,不属于 web-ui 主题源,也不是 desktop/backend 主题 owner。 +`src/apps/cli/themes/presets/*.json` 拥有 CLI preset 数据,`src/apps/cli/src/ui/theme.rs` +只负责 ANSI/monochrome 降级适配、OpenCode-compatible preset JSON 的最小解析和运行时投影, +不拥有 web-ui `ThemeConfig`。跨 web-ui、installer、CLI 的近似色可以在语义一致且不会影响相邻状态区分时手动收敛, +但不能通过让 Rust/CLI 复制 web-ui `ThemeConfig` 来解决;若后续需要共享,只能新增明确的 TS 生成投影或 CLI schema +contract,并纳入审计基线。 + +当前 Rust 启动主题投影已改为读取 TS 内置主题预设生成的 +`src/apps/desktop/src/generated/startup_theme_bootstrap.json`。Rust 侧只持有 +`id`、`bgPrimary`、`bgSecondary`、`bgScene`、`isLight`、`textPrimary`、 +`textMuted`、`accentColor` 这类首屏字段,不维护完整主题 schema 或 palette。 +Rust 配置层只保留一个独立的历史兼容 fallback:加载、导入或旧调用路径遇到 +`theme.id` 时映射到 `themes.current`,并在保存时移除顶层 `theme`;其他旧 GUI theme +字段不再解析、不再校验,也不再作为 public theme contract 暴露。 +`GenerativeUITool` 的内置主题提示也必须读取同一生成链路产出的 +`src/crates/assembly/core/src/agentic/tools/implementations/generated/theme_prompt_snapshots.json`, +避免 Rust 再手写一套 web-ui 主题色。两个 manifest 都只是 TS 主题预设的只读投影: +startup manifest 面向 WebView 首屏,prompt snapshot 面向模型提示,均不能反向成为主题源头。 +生成器 check 需要对工作树行尾做归一化,避免 Windows checkout 把 LF/CRLF 差异误判为 +manifest stale。 + +新增或扩展主题时按以下顺序执行: + +1. 在 TS 侧更新 `ThemeConfig`、主题预设、validator 和 runtime 注入;需要外部 iframe 读取时同步 + `themePayload` allowlist 与 CSS var contract。 +2. 对新增色值先走 token 预算:可复用则复用,肉眼不可区分才直接合并,有独立语义才新增 + semantic/component token,专用域进入 exception namespace。 +3. 只有影响 JS 加载前首屏的字段,才更新 Rust bootstrap 投影;不能为了 Rust 读取方便新增第二套主题 schema。 +4. custom theme 不要求 Rust 首屏精确还原。若后续要优化 custom theme 首屏体验,也必须由 TS schema 生成 + 最小 bootstrap cache,而不是让 Rust 直接解析 custom theme。 +5. 变更必须通过主题审计、CSS var contract 和必要的 focused visual review;跨 desktop/web/mobile 或 + light/dark/system 形态的行为由 TS 运行时统一验证。 + +主题作者不应手写完整派生色阶。内置主题和 custom theme 的长期方向是只维护少量稳定输入: +primary/accent 基色、secondary accent 基色、success/warning/error/info 等语义基色,以及确实需要保留 +主题个性的少量 override。accent scale、secondary accent scale、semantic bg/border、git/runtime 派生 +和 CSS var 注入都必须由 TS 侧 resolver/helper 统一生成;Rust/desktop 只消费 TS 生成的最小投影,不复制 +这些派生规则。 + +组件样式中的颜色消费应优先读取 runtime CSS var。`tokens.scss` 可以保留作为 root token 定义层、尺寸/ +字体/动效 SCSS 常量和少量历史 mixin,但普通组件、工具页和面板不应直接读取 `$color-*`、 +`$element-bg-*`、`$border-*` 或 `$git-color-*` 来表达产品颜色。独立定义域,例如 Mermaid theme、 +Monaco/editor、terminal 和 syntax palette,可以保留编译期 token,但必须有 namespace owner 和审计预算。 +这条规则避免新主题扩展时出现“TS runtime 已变更,但 Sass 构建期颜色仍停留在默认主题”的双轨问题。 + ## 当前现状 -基于最新 `gcwing/main` 的扫描结果,当前 PR 已把普通 app/component 层的 raw color -literal、token-equivalent app literal 和普通组件 near color pair 收敛到 0。剩余色值 -全部落在明确 owner 的专用域:theme preset/runtime、token contract、boundary fallback、 +基于当前审计口径,普通 +app/component 层的 raw color literal、token-equivalent app literal、普通组件 near color pair +和内部旧 alias 读取都已收敛到 0。剩余色值全部落在明确 owner 的专用域: +theme preset/runtime、token contract、boundary fallback、 Mermaid、Monaco/editor、Prism syntax、terminal ANSI、language identity 和 UI exception -registry。 +registry。Mobile Web 和 Installer 已纳入同一审计口径,但各自使用独立 baseline, +避免移动端或安装器的独立 token 被误算为 web-ui 游离 key。 + +`342` 个 web-ui 唯一颜色是前端生产文件的全域审计数,不是普通 app UI 的色值预算。 +其中包含主题 preset、token contract、Mermaid、Monaco/editor、terminal、syntax、 +language identity 和 UI exception 等专用 palette。language identity 已收敛为 8 个大类身份色, +不再按每种语言或文件类型保留独立色值。真正需要继续压缩的是这些专用域 +内部能被证明等价的近似色,而不是把它们直接并入普通 app semantic token。前后端职责边界 +已经收敛为:web-ui/TS 侧维护完整主题源;Rust/desktop 侧只读取 TS 生成的首屏和 +prompt snapshot 投影;mobile-web 和 installer 颜色在各自 TS root 中维护;CLI/TUI +颜色作为独立终端产品 surface 单独治理。 + +language/file identity 色只允许作为类别辅助 accent。消费方展示语言或文件身份时必须同时渲染 +label、icon、扩展名或文件名之一,不允许构建只靠颜色区分语言或文件类型的 UI。 + +补充看 resolved theme 输出而不是只看源码字面量:相邻状态、主题个性和 elevation +强度不能只按数值近似强行合并。复审后暗色主题 `effects.shadow` 保留各 preset 的 +原有 ramp;本轮只压缩非相邻、非语义的 surface/token 微差。 `colorScopes.exception`、`colorDomainScopes.uiException` 和 `colorDomainScopes.boundaryFallback` 的数值上升不是新增游离色,而是把原先散在 service/component 文件中的身份色、review team @@ -65,21 +177,52 @@ registry。 | 指标 | 当前基线 | | --- | ---: | -| 扫描的生产前端文件数 | 1526 | -| 忽略的测试文件数 | 213 | -| 包含颜色字面量的文件数 | 26 | -| 颜色字面量出现次数 | 1718 | -| 唯一颜色字面量数量 | 913 | +| 扫描的生产前端文件数 | 1543 | +| 忽略的测试文件数 | 224 | +| 忽略的构建生成文件数 | 0 | +| 包含颜色字面量的文件数 | 25 | +| 颜色字面量出现次数 | 535 | +| 唯一颜色字面量数量 | 342 | | 组件或非 token 文件中的颜色出现次数 | 0 | | 组件或非 token 唯一颜色数量 | 0 | | App UI 颜色出现次数 | 0 | | App UI 唯一颜色数量 | 0 | -| `var(--token, fallback)` 出现次数 | 25 | -| fallback 唯一 token 数 | 7 | +| `var(--token, fallback)` 出现次数 | 0 | +| fallback 唯一 token 数 | 0 | | token-equivalent app literal 出现次数 | 0 | | token-equivalent app literal 唯一颜色数量 | 0 | | 普通组件肉眼不可区分 near color pair | 0 | | 普通组件需证据复核的 near color pair | 0 | +| 专用域肉眼不可区分 near color pair | 0 | +| 专用域需证据复核的 near color pair | 9 | + +跨 root 基线必须分别看待;这些数字不是互相累加后的单一上限: + +| root | 颜色出现次数 | 唯一颜色 | App UI raw | fallback var | unresolved / non-contract key | dynamic family | 说明 | +| --- | ---: | ---: | ---: | ---: | ---: | ---: | --- | +| `src/web-ui/src` | 535 | 342 | 0 | 0 | 0 | 13 | 主应用完整主题、专用 palette、widget payload 和 editor/terminal/Mermaid 域;UI exception、syntax、language identity 与 boundary fallback 已收敛到小型语义 palette,未读取的 git 同义 runtime/static key 和未消费 legacy mixin 已退役。 | +| `src/mobile-web/src` | 35 | 30 | 0 | 0 | 0 | 3 | mobile-web 图片缩略图关闭按钮已读取 `--color-static-white`,普通 app UI raw 归零。 | +| `BitFun-Installer/src` | 76 | 62 | 0 | 0 | 0 | 1 | 安装器主题数据保留主题卡可见的 primary/secondary background、accent、text 和状态反馈;runtime 只导出实际消费的 accent family、基础 text/border/element/status key,不复制主应用 purple/info/tooltip 等完整主题面。 | + +CLI/TUI 使用独立审计,不参与 CSS var root 计数: + +| CLI/TUI 指标 | 当前值 | +| --- | ---: | +| preset 文件数 | 6 | +| preset 颜色出现次数 | 306 | +| preset 唯一色数 | 114 | +| runtime-consumed preset 颜色出现次数 | 114 | +| runtime-consumed preset 唯一色数 | 75 | +| OpenCode compatibility-declared preset 颜色出现次数 | 192 | +| OpenCode compatibility-declared preset 唯一色数 | 87 | +| Rust fallback `Color::Rgb` 出现次数 | 0 | +| Rust fallback 唯一色数 | 0 | +| CLI/TUI 总唯一色数 | 114 | +| CLI/TUI runtime 唯一色数 | 75 | +| preset 需证据复核 near pair | 0 | +| runtime-consumed preset 需证据复核 near pair | 0 | +| OpenCode compatibility-declared preset near pair | 0 | +| Rust fallback near pair | 0 | 当前审计未发现 CSS 变量契约层面的硬错误: @@ -93,88 +236,123 @@ registry。 | non-contract dynamic inputs | 0 | | non-contract component-private vars | 0 | -本轮补充了机器可校验的治理契约,用于把“可删除债务”和“必须保留的兼容/边界” +审计补充了机器可校验的治理契约,用于把“可删除债务”和“必须保留的兼容/边界” 分开: | 治理契约指标 | 当前值 | 说明 | | --- | ---: | --- | -| compatibility alias contracts | 63 | 显式列出历史别名、canonical 目标、owner、保留原因和退场条件 | -| compatibility alias 使用 key | 68 | 包含 `--radius-*`、`--spacing-*` 展开的实际使用 key;这些 key 不能直接删除 | -| compatibility alias 使用次数 | 609 | 当前仍是重要兼容面,后续迁移应逐步降低并同步降低 baseline | -| stale compatibility alias contracts | 0 | 防止 registry 保留已经没有静态/runtime 定义的旧 key | +| compatibility alias contracts | 0 | 显式 root/runtime 兼容别名已退役;历史 generated widget 内容通过 iframe fallback 兼容,尺寸旧 family 仅由 iframe fallback 和 family contract 识别 | +| compatibility alias 直接使用 key | 0 | 产品代码不再通过 `var(--legacy-alias)` 读取旧 key;新增直接读取会被 baseline 拦截 | +| compatibility alias 直接使用次数 | 0 | 显式旧 key 不再在 root/runtime 定义,内部样式读取统一使用 canonical token | +| stale compatibility alias contracts | 0 | 防止 registry 重新保留没有必要的静态/runtime 兼容定义 | | compatibility alias family contracts | 2 | `--radius-* -> --size-radius-*`、`--spacing-* -> --size-gap-*` | -| stale compatibility alias family contracts | 0 | 防止动态 family 或 canonical family 失配 | +| compatibility alias family 直接使用 key | 0 | `--radius-*`、`--spacing-*` 旧 family 不再被内部 `var()` 读取 | +| compatibility alias family 直接使用次数 | 0 | generated widget payload 只暴露经过审核的 canonical 子集,iframe shell 通过 alias fallback 兼容 legacy family | +| stale compatibility alias family contracts | 0 | 防止 family contract 指向缺失的 canonical family | | missing compatibility alias family canonicals | 0 | 防止新增 `--radius-x` / `--spacing-x` 但缺少对应 canonical key | -| fallback token contracts | 7 | 每个 `var(--token, fallback)` 边界 fallback 都有 owner、reason 和 boundary | +| surface token rename contracts | 9 | 显式记录已迁移的 surface-local 旧 key、canonical 目标、owner 和命名边界 | +| active surface token rename key | 0 | 防止 `--primary-color`、`--operation-color`、`--delay`、`--um-*` 等旧局部 key 回流 | +| active surface token rename occurrences | 0 | 防止旧 key 在 SCSS、CSS 或 TSX inline style 中被重新定义或读取 | +| surface token rename missing canonical | 0 | 防止 rename registry 指向不存在的 canonical key | +| generated widget payload key | 80 | widget iframe 对外主题变量 allowlist,作为外部边界单独预算,不计入内部 alias 读取;payload 只保留需要随宿主主题变化的 canonical 颜色、文本、surface、形状、间距、字体、动效和 button component token 子集,host 内部、静态黑白 overlay、派生 accent/status/border/radius key 或历史兼容 key 由 iframe fallback/static shell 派生 | +| generated widget button payload | 18 | Cyber/Tokyo/light 等主题把 button bg、border、shadow、transform 和 hover/active 作为主题身份与交互反馈的一部分;这些 key 不能仅因 iframe static shell 存在默认值就移除,否则会让生成式 widget 内按钮失去宿主主题视觉层级。后续只有证明所有内置主题 resolved button 输出等价时才可继续缩减 | +| generated widget payload compatibility alias | 0 | payload 不再直接暴露 legacy alias;历史生成内容通过 iframe 内 alias fallback 读取 canonical key | +| generated widget payload compatibility family key | 0 | payload 不直接暴露旧 `--radius-*`、`--spacing-*`;必要的 canonical 尺寸值由 payload 子集或 iframe fallback 保留 | +| generated widget payload external-only compatibility key | 0 | payload 中已清空仅因外部兼容保留且内部产品代码不再读取的 legacy key | +| generated widget payload undefined key | 0 | 防止 payload 引入没有静态、运行时或动态 family 定义的游离 key | +| generated widget payload missing compatibility canonical | 0 | 防止 payload 暴露 legacy key 但 canonical 目标不存在 | +| generated widget payload unexported compatibility canonical | 0 | 防止 payload 暴露 legacy key 但遗漏对应 canonical key | +| fallback token contracts | 0 | 组件 fallback 已通过根 token 或组件根默认值收敛;新增 fallback 会重新要求 owner、reason 和 boundary | | uncontracted fallback tokens | 0 | 防止新增未解释的 fallback key | | stale fallback token contracts | 0 | 防止已删除 fallback 继续留在 registry 中 | | color domain contracts | 13 | 每个专用域都有 owner、reason 和 merge policy | | active uncontracted color domains | 0 | 防止新增专用域但没有 owner/合并策略 | +跨 root 审计必须使用 `pnpm run theme:color-audit:all`。`theme:color-audit` 只覆盖 +`src/web-ui/src`;mobile-web 和 installer 的 raw color、fallback、dynamic family、 +unresolved key 和专用域 near pair 需要分别由 `theme:color-audit:mobile` 与 +`theme:color-audit:installer` 校验;CLI/TUI preset 与 Rust fallback contract 由 +`theme:color-audit:cli` 校验。新增 baseline 只能在债务减少时下调;不得为了让 +PR 通过而放宽 `appUi`、fallback、unresolved、non-contract 或 dynamic family 上限。 + 剩余颜色集中在几个专用域;普通 app UI 不再保留 raw color literal: | 区域 | 当前出现次数 | 当前唯一色数 | 说明 | | --- | ---: | ---: | --- | -| Theme presets | 1033 | 611 | 主题个性与 palette 映射,不作为普通 app literal 直接合并 | -| Token contracts | 268 | 159 | `tokens.scss` 等静态契约根 | -| Editor | 56 | 53 | Monaco/editor 专用域,不能直接泛化到 app token;组件装饰色已迁出 raw literal | -| Mermaid | 139 | 95 | Mermaid 专用渲染域 | -| Theme runtime | 54 | 45 | `ThemeService.ts` 运行时注入 | -| Language identity | 52 | 50 | 语言身份色,已集中到 identity registry | -| Terminal | 38 | 30 | terminal/ANSI 专用域 | -| Boundary fallback | 22 | 22 | iframe/miniapp/截图兜底值,不作为普通 app token | +| Theme presets | 170 | 116 | 主题个性与 palette 映射;跨主题深色 neutral、弱文本、非状态浅色背景和同概念 success 色已收敛;相邻 surface、主题识别主背景、状态色和 editor lineHighlight 继续保留 | +| Token contracts | 108 | 97 | `tokens.scss` 等静态契约根;黑白 overlay alpha stop 继续保留相邻状态层级,未消费 legacy mixin 和自引用别名已移除,不按数值相近强行合并 | +| Editor | 52 | 48 | Monaco/editor 专用域,不能直接泛化到 app token;被动 selection/word highlight 已收敛,但 diff text/line/gutter 继续保留用户可见层级 | +| Mermaid | 82 | 65 | Mermaid 专用渲染域;light status fallback 已改读 app semantic status token,critical/error 背景弱重复已收敛,note 文本保留专用高对比值 | +| Theme runtime | 27 | 26 | `ThemeService.ts` 运行时注入;黑白 overlay alpha 与静态 token、payload shell 保持相同 stop,避免 early render 与 runtime 状态层级漂移 | +| Language identity | 8 | 8 | 语言/文件身份色,已收敛到 8 个大类色;具体识别继续由 key、label、icon 和扩展名承担 | +| Terminal | 37 | 29 | terminal/ANSI 专用域;工具命令空状态已复用 `--tool-command-empty-rgb`,不再保留独立 raw 色 | +| Boundary fallback | 18 | 18 | iframe/miniapp/截图兜底值,不作为普通 app token;generated widget 初始 CSS 需要覆盖 retired alias 的 canonical 目标 | | Visual effects | 0 | 0 | StreamText/TextStroke raw literal 已迁出普通组件层 | -| UI exception registry | 38 | 34 | 已归档的 UI 例外色,包含 review team、agent capability、template context、insights 和 inspector 等固定身份色 | +| UI exception registry | 17 | 17 | 已归档的 UI 例外色,包含 review team、agent capability、template context、insights 和 inspector 等固定身份色;非相邻同语义私有色已归并到小型 exception palette | | Generated widget | 0 | 0 | 颜色默认值已迁到 boundary fallback registry | | App UI | 0 | 0 | 普通 app/component raw color 已清零;后续新增必须先进入 token/exception 决策 | -| Syntax | 18 | 17 | Prism syntax palette,保留为专用渲染域 | +| Syntax | 16 | 16 | Prism syntax palette,已按 foreground、muted/comment、keyword、literal、function、markup 等角色收敛;light punctuation 与 dark tag/property 保留相邻代码可读性差异 | + +专用域 near color pair 已单独进入证据队列,避免把 editor、terminal、Mermaid、 +theme preset 或 boundary fallback 误算成普通 app UI 债务。下表是 `src/web-ui/src` +root 的当前队列,不是自动合并指令, +而是后续截图和语义复核的候选清单: + +| 专用域 | 肉眼不可区分 pair | 需证据复核 pair | 后续处理原则 | +| --- | ---: | ---: | --- | +| Theme presets | 0 | 5 | 保留 Tokyo Night 主题识别主背景、Ink Night secondary/lineHighlight、Monaco light lineHighlight、Midnight lineHighlight/elevation 等需视觉证据的 near pair;不按数值继续强合并 | +| Theme runtime | 0 | 2 | 黑白 overlay runtime stop 虽然数值接近,但直接服务 card、markdown、shadow 等相邻状态;后续只能在有视觉证据时继续压缩 | +| Token contracts | 0 | 2 | 静态 token 黑白 overlay stop 保留与 runtime 同步的状态层级;不能为了清零 near pair 破坏 hover/active/elevated 区分 | +| Boundary fallback | 0 | 0 | first-paint 兜底已收敛到较粗 overlay stop;后续新增必须重新说明边界理由 | +| Mermaid | 0 | 0 | 节点、边、文本、错误态和 light/dark Mermaid fallback 的近似队列已清零 | +| Editor | 0 | 0 | Monaco selection、diff、inline highlight 和 light/dark editor 近似队列已清零 | +| Syntax / Terminal / Generated widget / Debug overlay / UI exception / Language identity / Visual effects | 0 | 0 | 当前无 near 队列;新增会被单域 baseline 拦截 | + +专用域 near pair 不是隐式豁免。当前保留项必须在 +`scripts/theme-color-near-pair-decisions.json` 中有 root、owner、reason 和 reevaluateWhen; +审计测试会阻止 web-ui、mobile-web 或 installer 新增 near pair 没有决策, +也会阻止已合并 pair 的过期决策继续留在 registry 中。 剩余高频文件均为专用 palette 或集中 registry: | 文件 | 颜色出现次数 | 后续处理策略 | | --- | ---: | --- | -| `src/web-ui/src/tools/editor/themes/bitfun-dark.theme.ts` | 47 | Monaco theme palette;不拆散到普通 app token | -| `src/web-ui/src/shared/theme/languageIdentityAccents.ts` | 52 | 内置 language/file identity registry;调用方复用常量 | -| `src/web-ui/src/shared/theme/uiExceptionAccents.ts` | 38 | 固定 UI 身份/角色色 registry;新增必须说明 owner/role | +| `src/web-ui/src/component-library/styles/tokens.scss` | 95 | 根 token 契约;优先处理同语义 alias,避免把状态/层级 ramp 按数值强合并 | +| `src/web-ui/src/tools/mermaid-editor/theme/mermaidThemeFallbacks.ts` | 62 | Mermaid 专用渲染兜底;需以节点、边、文本、错误态截图为依据 | +| `src/web-ui/src/tools/editor/themes/bitfun-dark.theme.ts` | 46 | Monaco theme palette;不拆散到普通 app token | +| `src/web-ui/src/infrastructure/theme/core/ThemeService.ts` | 27 | 运行时注入;需保持 early render、system theme 和 payload 导出兼容 | | `src/web-ui/src/tools/terminal/utils/xtermTheme.ts` | 36 | terminal ANSI palette;不与 app semantic color 合并 | -| `src/web-ui/src/shared/theme/themeBoundaryFallbacks.ts` | 22 | isolated surface 和截图兜底值;集中 owner | -| `src/web-ui/src/shared/theme/syntaxHighlightAccents.ts` | 18 | Prism syntax palette;不泛化到 app token | - -当前 fallback token 都已进入 allowlist,但仍需要逐项决策是否保留边界 fallback: +| `src/web-ui/src/tools/mermaid-editor/theme/_tokens.scss` | 20 | Mermaid SCSS token 兜底;只在 Mermaid owner 内治理 | +| `src/web-ui/src/infrastructure/theme/presets/midnight-theme.ts` | 23 | theme preset palette;保留主题个性、状态色和 alpha ramp 边界 | +| `src/web-ui/src/infrastructure/theme/presets/slate-theme.ts` | 22 | theme preset palette;保留主题个性、状态色和 alpha ramp 边界 | +| `src/web-ui/src/shared/theme/themeBoundaryFallbacks.ts` | 18 | iframe/miniapp/截图兜底 palette;只作为边界 fallback 治理 | +| `src/web-ui/src/shared/theme/uiExceptionAccents.ts` | 17 | 固定 UI 身份/角色色 registry;新增必须说明 owner/role | -| fallback token | 次数 | -| --- | ---: | -| `--surface-stagger-index` | 12 | -| `--mission-control-group-color` | 6 | -| `--char-index` | 3 | -| `--gallery-grid-min` | 1 | -| `--gallery-skeleton-height` | 1 | -| `--primary-color` | 1 | -| `--scene-viewport-border-width` | 1 | +组件级 `var(--token, fallback)` 已收敛到 0;原先的 7 个 fallback token 不再需要 +fallback contract registry 保留。 -fallback 决策表: +fallback 收敛决策表: -| fallback token | 决策 | 依据 | 后续动作 | +| 原 fallback token | 决策 | 依据 | 结果 | | --- | --- | --- | --- | -| `--surface-stagger-index` | 保留 | 运行时 inline 动画序号,`0` 是安全首帧/无动画默认值 | 不迁移为颜色 token;保持 allowlist | -| `--mission-control-group-color` | 保留 | 分组身份色由数据或 inline style 驱动,静态删除会丢失未设置分组色时的 accent 兜底 | 后续 content-canvas token 抽取时复核是否改为组件根默认值 | -| `--char-index` | 保留 | StreamText 每字符动画偏移,`0` fallback 是无序号渲染的安全默认值 | 不迁移为颜色 token;保持 allowlist | -| `--gallery-grid-min` | 保留 | runtime layout sizing 输入,不属于颜色债务;`320px` 保持 responsive grid 下限 | 保持 allowlist,后续只在 layout token 方案中处理 | -| `--gallery-skeleton-height` | 保留 | runtime skeleton sizing 输入,不属于颜色债务;`140px` 保持占位高度稳定 | 保持 allowlist,后续只在 layout token 方案中处理 | -| `--primary-color` | 延后 | Markdown 嵌入内容可覆盖 primary accent,边界语义不同于全局 app primary | Markdown token 抽取时决定是否转为 `--markdown-primary-color` contract | -| `--scene-viewport-border-width` | 保留 | viewport border width 是 runtime layout override,`1px` fallback 保持默认边界可见 | 保持 allowlist,后续只在 scene layout token 方案中处理 | +| `--surface-stagger-index` | 上移默认值 | `tokens.scss` 已提供 `0` 默认值,TS inline style 仍可覆盖动画序号 | 移除 12 处 selector fallback | +| `--mission-control-group-color` | 上移默认值并保留组别差异 | filter 仍由 inline style 驱动;thumbnail badge 的 primary/secondary/tertiary 默认值保持原 accent/success/warning 语义 | 移除 6 处背景 fallback,避免误把组别统一成同一颜色 | +| `--char-index` | 上移到组件根 | StreamText 根提供 `0`,每字符 inline style 仍可覆盖 | 移除 3 处 keyframe fallback | +| `--gallery-grid-min` | 上移到根 token 默认值 | `tokens.scss` 提供 `320px`,祖先变量和 props inline style 仍可覆盖 | 移除 grid sizing fallback | +| `--gallery-skeleton-height` | 上移到根 token 默认值 | `tokens.scss` 提供 `140px`,祖先变量和 props inline style 仍可覆盖 | 移除 skeleton height fallback | +| `--primary-color` | 改为明确的 tool-card accent token | `--primary-color` 是历史 tool card 局部入口,不提升为全局 app token;BaseToolCard 现在通过 `--base-tool-card-accent-color` 映射到 `--markdown-primary-color` | 产品代码不再定义或读取旧 key,回流由 `surfaceTokenRenames` 拦截 | +| `--scene-viewport-border-width` | 上移默认值 | 静态 token 提供 `1px`,ThemeService 继续按主题 layout 覆盖为 `1px` 或 `0` | 移除 viewport border fallback | -阶段状态: +稳定里程碑: -| 阶段 | 状态 | 当前判断 | -| --- | --- | --- | -| Phase 0:基线与工具 | 已完成主体 | 审计脚本可区分测试文件、fallback token、dynamic families 和 exception domains | -| Phase 1:canonical token 契约 | 本轮强化 | compatibility alias registry 已记录 63 个显式 alias 和 2 个 alias family,包含 canonical 目标、owner、保留原因和退场条件 | -| Phase 2:精确重复合并 | 本轮完成 | token-equivalent app literal 已从 12/10 清零;截图兜底、language identity 和 review/agent/insights 固定色已迁入显式 registry | -| Phase 3:legacy fallback 迁移 | 本轮强化 | fallback unique token 保持 7,且全部进入 fallback contract registry;新增未登记 fallback 会被审计报告和 baseline 拦截 | -| Phase 4:组件 token 抽取 | 本轮完成 | CodeEditor、StreamText、ChatInputPixelPet、ReferencesPanel、AgentCompanion、tool-card、editor 组件装饰色已抽为组件私有 RGB channel 或复用 contract token | -| Phase 5:近似色合并 | 本轮完成 | 普通组件 near pair 已清零;极近似视觉色只在不相邻或不承担状态差异时合并,Monaco/terminal/Mermaid/syntax 专用 palette 不强行合并 | -| Phase 6:防回退约束 | 本轮强化 | baseline 已同步到 component/non-token=0、appUi=0、token-equivalent=0、nearPair=0,并新增 alias、fallback、domain contract 防回退指标 | +| 里程碑 | 稳定要求 | +| --- | --- | +| 基线与工具 | 审计脚本必须持续区分测试文件、fallback token、dynamic family、exception domain 和 generated widget 外部兼容面。 | +| canonical token 契约 | 内部调用方只读 canonical token;root/runtime 显式 compatibility alias 保持 0。确需外部兼容时只放在对应边界,如 generated widget iframe fallback 或动态 family contract。 | +| fallback 收敛 | 普通组件不得新增 `var(--token, fallback)` 色值;确需边界兜底时先证明 owner、reason 和 boundary。 | +| 组件 token 抽取 | 复杂 surface 只能在 semantic token 不足以表达契约时新增 component token,并保持 surface namespace。 | +| 近似色治理 | 普通组件 near pair 维持 0;专用域 near pair 只作为证据队列,不能按数值相似自动合并。 | +| 防回退约束 | baseline 必须继续拦截 raw app color、token-equivalent literal、内部 alias 读取、未定义 key、stale registry 和 payload 缺失 canonical。 | Phase 5 决策记录: @@ -188,32 +366,126 @@ Phase 5 决策记录: | `#ffdd00` -> `#ffd700` | merge | `StreamText.scss` fire yellow;editor/reference yellow | RGB distance = 6,均为亮黄强调色,调用点不相邻,不承担不同业务状态 | | `#7dd3fc` -> `#7DCFFF` | merge | `GenerativeWidgetToolCard.scss`;`bitfun-dark.theme.ts` editor link | RGB distance = 5,均为非状态 sky/cyan 强调,调用点跨 surface 且不相邻 | | `#00b4d8` -> `#00add8` | merge | `StreamText.scss` ocean mid;Go language identity | RGB distance = 7,同为 cyan/blue identity/visual-effect 色,非错误/警告/状态强度 | -| `#141414` vs `#121214` | preserve | `LanguageRegistry.ts` reStructuredText identity;Flow Chat capture/editor fallback | RGB distance = 2.83,但 `#141414` 是已存在的 language identity,迁移到 registry 时保持原值;`#121214` 仅作为截图/边界兜底 | +| language identity palette | category-compress | `languageIdentityAccents.ts` 内置 language/file identity registry;`LanguageRegistry.ts` 和 code snippet metadata 读取 | 原 52 个出现、50 个唯一色改为 8 个大类色;保持所有 language/file key、label、icon、extension 和 helper API 不变,颜色不再承担唯一识别职责;消费方必须同时显示 label/icon/extension/file name 之一 | +| syntax palette roles | role-compress | `syntaxHighlightAccents.ts` Prism/Markdown/CodePreview palette | 浅色 `string/number` 共享 literal blue;light `punctuation` 独立于 comment,dark `tag/property` 保持区分,避免 12px code preview 和 JSX/HTML 相邻 token 可读性下降;syntax 18/17 -> 16/16 | +| Mermaid `#dfe2e8` -> `#e0e2e8` | merge | light `nodeFillHover`;dark `nodeText` | RGB distance = 1,跨 light/dark fallback 角色,不在同一主题视口中承担相邻状态区分;合并后保留 `nodeFillHover` 和 `nodeText` 语义 key | +| Mermaid `#5a5e68` -> `#5a5e6a` | merge | dark `edgeLabelBorderHover`;dark `nodeStroke`/`edgeStroke` | RGB distance = 2,均为深色 Mermaid neutral stroke/border;edge label hover border 不表达独立状态严重程度,合并后仍通过 key 区分角色 | +| Mermaid `#6a6e78` -> `#6a6e7a` | merge | dark `textMuted`;dark `nodeStrokeHover` | RGB distance = 2,均为深色 Mermaid subdued neutral;不是 success/warning/error 或数据类别色,合并后保留 muted text 与 hover stroke 语义 | +| Cyber `#141414` -> `#151515` | merge | `bitfun-cyber` scene background;secondary background;Monaco line highlight | RGB distance = 1.73,三者都在 Cyber 暗色 neutral surface 内,不表达不同状态、严重程度或数据类别;保留 `bgScene`/`secondary`/`lineHighlight` 语义 key,实际色值统一 | +| dark card white alpha literal | merge to canonical overlay stops | `ThemeService.ts` dark `--card-bg-*` runtime injection | `0.015/0.025/0.035/0.09/0.13` 仅是 dark card fallback 的微弱层级色,合并到现有 canonical overlay stop;复审后保留 default/elevated/subtle/hover/active 的相邻状态差异 | +| `#f3f3f5` vs `#f4f4f5` | preserve | light theme primary background;dark theme status text | RGB distance = 1.41,但跨 light/dark theme 且角色不同;不通过数值相近抹平主题个性或状态文本对比 | +| `#b8c6ff` -> `#b8c4ff` | merge | Slate theme purple alpha ramp;Slate purple solid stop | RGB distance = 2,同一 Slate purple ramp 内肉眼不可区分;alpha ramp 继续保留独立语义 key,实际 RGB channel 收敛到 solid 500 stop | +| `#fafafa` -> `#ffffff` | merge | Dark theme primary button hover/active text | RGB distance = 8.66,均为同一 dark primary button 交互前景,不承担相邻区域或状态类别区分;默认态 `#f4f4f5` 保留以维持按钮交互层级 | +| `#e2e6eb` -> `#e2e8f0` | merge | Slate window control standard hover text;Slate accent soft ramp | RGB distance = 5,同一 Slate neutral chrome 体系内的 hover 前景,非 close/error 状态,合并到已有 soft accent stop | +| `#f0f2f5` -> `#eef0f3` | merge | Slate primary button default text;Slate text primary | RGB distance = 3.46,同一 Slate foreground ramp 内肉眼难区分,且不与 hover/active 的 white foreground 相邻表达不同业务状态 | +| `rgba(255,255,255,0.22)` -> `rgba(255,255,255,0.24)` | merge | static token scrollbar hover、info border、ChatInput border、dark fallback scrollbar、MiniApp fallback、scrollbar mixin defaults | 同一 white overlay hover/border stop 内 0.02 alpha 差异;不用于相邻信息层级,合并到已有 `0.24`/`border-medium` 后保留语义 key | +| `rgba(0,0,0,0.28)` -> `rgba(0,0,0,0.30)` | merge | light runtime scrollbar fallback、MiniApp light scrollbar fallback | 同一 light scrollbar hover fallback 内 0.02 alpha 差异;仅在主题未提供 scrollbar 值时兜底,合并到现有 black30 stop | +| dark neutral element alpha ramp | merge to canonical overlay stops | `tokens.scss`、`createDarkNeutralElement()`、theme prompt snapshot | 非标准 neutral alpha 已收敛到 canonical overlay ramp,保留 subtle/soft/base/medium/strong/elevated key;只压缩肉眼弱差异,不删除层级角色 | +| `--color-overlay-white-05/06` -> `--color-overlay-white-08` | retire key | `tokens.scss`、`ThemeService.ts`、generated widget payload、内部 CSS var 读取 | 旧 key 只表达极弱 white overlay 微差,继续保留会扩大主题扩展 surface;root/runtime/payload 不再导出,历史 generated widget 读取通过 iframe alias fallback 映射 | +| dark theme shadow alpha ramp | preserve | `src/web-ui/src/infrastructure/theme/presets/*-theme.ts` | 经独立复审,Tokyo、Slate、China Night、Cyber 等 shadow alpha 同时表达主题个性和 elevation 强度,且影响菜单、下拉、toolbar 等相邻浮层;不作为本轮压缩对象,后续只有截图证据充分时才按主题内语义合并 | +| low-end overlay payload stops | retire keys | `tokens.scss`、`ThemeService.ts`、generated widget payload、内部 CSS var 读取 | `--color-overlay-white-02` 合并到 `--color-overlay-white-04`,`--color-overlay-black-06` 合并到 `--color-overlay-black-08`;二者只表达极弱 surface/shadow 透明度,不承载 status/semantic;root/runtime/payload 不再导出,历史 generated widget 读取通过 iframe alias fallback 映射 | +| light slate overlay micro-stops | merge to coarse surface ramp | `tokens.scss`、Markdown/table、ChatInput、ModelSelector、ContextMenu、TiptapEditor 等浅色 surface 读取 | `--color-overlay-slate-03/06/10/14` 分别收敛到 `04/08/12/12`,保留 `04/08/12/22` 四个 stop;用途集中在浅色主题非语义 shadow、border、stripe、hover,避免为 0.01/0.02 alpha 微差扩大主题扩展 key 面 | +| accent blue alpha micro-stop | merge to existing accent stop | `tokens.scss` | `rgba(59, 130, 246, 0.12)` 只用于同一 blue accent 的 glow/gradient 微层级,合并到既有 `0.15` stop;不影响 semantic/error/warning 或相邻状态区分 | +| inspector active overlay alpha | merge to existing accent stop | `uiExceptionAccents.ts` | inspector active background 与 blue accent `0.15` 是同一强调背景概念,合并后提升可见性并减少一个全局唯一色;border 和 selected overlay 仍保留独立状态色 | +| dark preset neutral surface literals | merge within theme preset namespace | `bitfun-midnight`、`bitfun-cyber`、`bitfun-tokyo-night` | Midnight workbench 复用 panel;Cyber secondary/tertiary/quaternary 复用既有 dark neutral;Tokyo tertiary/elevated 复用 secondary/quaternary;只处理中性背景 surface,不合并 accent、semantic、Monaco syntax 或状态色 | +| boundary fallback white overlay | merge to coarse fallback stops | `themeBoundaryFallbacks.ts` | iframe/截图兜底只在 root token 不可用时使用,`borderBase`、`elementBgBase`、`elementBgMedium` 收敛到 `0.12`,保留 key 以维持边界语义 | +| Mermaid fallback near surfaces | merge within Mermaid namespace | `_tokens.scss`、`mermaidThemeFallbacks.ts` | light section/active/cluster 和 dark edge-label/note/activation fallback 使用既有 Mermaid surface stop;只在 Mermaid 专用域内合并,不映射到普通 app status token | +| Monaco light highlight and dark unchanged diff | merge within editor namespace | `MonacoThemeSync.ts`、`bitfun-dark.theme.ts` | light inactive selection/word highlight 收敛到相同弱高亮 stop;dark unchanged diff 使用 editor base background,降低不承担状态含义的深色微差 | +| markdown light code/table neutral | merge to existing text/surface token | `flowchat-markdown-code-vars.scss`、`flowchat-markdown-table-vars.scss` | inline code 前景复用 light text token,table header 复用 code block light surface;避免跨 Markdown 子表面的近似浅灰重复 | +| accent/glass low alpha values | merge to existing accent stops | `tokens.scss` | blue/purple/green glass 和 card accent 的 `0.06/0.10/0.18` 低 alpha 值收敛到相邻既有 stop,保留 hover/base key,避免扩张独立透明度阶梯 | +| mobile dark scene `#16161a` -> `#18181a` | merge | `src/mobile-web/src/theme/presets/dark.ts` | RGB distance = 2.83,scene/flowchat 与 secondary background 不再保留肉眼弱差异;仍保留 `BG_SCENE` 语义别名以便后续主题需要重新分化 | +| mobile light near grays | merge | `src/mobile-web/src/theme/presets/light.ts` | `#1c1c1e`、`#e5e5ea`、`#aeaeb2` 分别收敛到既有 dark/mobile gray;跨 light/dark theme 不在同一视图相邻表达层级,contrast 仍满足原角色预期 | +| installer dark runtime/static panel `#18181a` -> `#1a1c1e` | merge | `BitFun-Installer/src/theme/installerThemesData.ts`、`BitFun-Installer/src/styles/variables.css` | 复用安装器已有 Slate panel 值,运行时 preset 与首屏静态变量一致;背景层级 key 保留,实际深色 neutral 数量减少 | +| installer dark/slate element alpha ramp | merge | `BitFun-Installer/src/theme/installerThemesData.ts`、`BitFun-Installer/src/styles/variables.css` | `0.10/0.21` 收敛到既有 `0.12/0.24` overlay stop;soft/base 与 strong/elevated 分别保留语义 key,安装器非高密度相邻 surface 不依赖这些微小差异表达状态 | +| installer info `#E1AB80` -> `#e1ab80` | normalize | `BitFun-Installer/src/theme/installerThemesData.ts`、`BitFun-Installer/src/styles/variables.css` | 仅大小写归一,无视觉变化;避免跨 root 审计和人工复核时把同一 hex 误读成不同色值 | +| web/installer/CLI cyber deep blacks | merge | `src/web-ui/src/infrastructure/theme/presets/cyber-theme.ts`、`BitFun-Installer/src/theme/installerThemesData.ts`、`src/apps/cli/themes/presets/bitfun-cyber.json` | `#101010`、`#0d0d0d` 和旧 tooltip `rgba(16, 16, 16, 0.95)` 收敛到既有 dark primary `#0e0e10` 派生值;RGB distance <= 3.32,主题识别仍由 neon accent、surface 和 typography 表达;安装器和 CLI 同名主题同步避免跨 surface 漂移 | +| web dark/slate/tokyo tooltip literals | derive from preset background | `src/web-ui/src/infrastructure/theme/presets/*-theme.ts` | tooltip 仍输出相同 rgba 值,但改为由对应 `BACKGROUND_SECONDARY` 常量派生,降低源字面量数量并避免相同 surface 重复手写 | +| CLI Rust fallback near surfaces | merge within fallback palette | `src/apps/cli/src/ui/theme.rs` | fallback dark hover 复用 panel,light panel/block 复用同一浅灰,light hover 复用 element;fallback near pair 降为 0,preset 主路径和语义 key 不变 | +| CLI color audit baseline | guardrail | `scripts/audit-cli-theme-colors.mjs`、`scripts/theme-color-governance-baseline.cli.json` | CLI/TUI preset 与 Rust fallback contract 单独计数,避免被 web-ui CSS var 审计误归类;后续只能在债务减少时下调预算 | +| CLI preset non-adjacent near surfaces | merge within preset namespace | `src/apps/cli/themes/presets/bitfun-dark.json`、`bitfun-midnight.json`、`bitfun-tokyo-night.json` | Midnight 深色 background/panel/context、input、subtle border 和 diff gutter、dark removed diff、Tokyo added diff 复用已有终端 surface stop;preset 唯一色数 134 -> 125,总唯一色数 164 -> 155,preset near pair 20 -> 7;Cyber 相邻 element/border、diff body/gutter 和跨主题 text/background 仍保留,不按数值相近强合并 | +| CLI dark syntax variable `#80d4ff` -> `#7dcfff` | merge | `src/apps/cli/themes/presets/bitfun-dark.json` | 同一 dark CLI syntax palette 内 function/variable 均为 cyan 高亮,RGB distance 5.83,终端语法类别仍可通过 token role 保留;合并后不影响 diff、warning/error 或背景边界 | +| web theme preset neutral compression | merge within preset namespace | `src/web-ui/src/infrastructure/theme/presets/{china-night,china-style,cyber,dark,light,midnight,slate,tokyo-night}-theme.ts` | 跨主题深色 neutral、弱文本、非状态 scene/workbench 和低透明 accent seed 收敛到已有 stops;复审后保留 Ink Night secondary/lineHighlight、Tokyo Night primary、Slate success、light surface ramp、China paper elevated ramp、Monaco lineHighlight 和 error/warning/status 色。Theme preset 唯一色数 147 -> 121,theme preset near pair 57 -> 11 | +| CLI weak foreground and input preset compression | merge within CLI preset namespace | `src/apps/cli/themes/presets/bitfun-dark.json`、`bitfun-midnight.json` | Dark syntax punctuation 复用正文前景,Midnight markdown strong 复用正文前景,Midnight input background 复用 element surface;均不承担 diff body/gutter 或 border/element 相邻区分。CLI preset 唯一色数 125 -> 123,总唯一色数 155 -> 153,preset near pair 7 -> 5 | +| remaining web theme near pairs | preserve | web theme preset | 剩余 web near 主要是 light/paper 相邻 surface、主题身份背景与 Monaco light lineHighlight;CLI preset/runtime near pair 已清零。后续继续合并需要截图或语义证据,不以 RGB distance 自动处理 | +| web/installer dark/slate/midnight neutral text | merge | `src/web-ui/src/infrastructure/theme/presets/slate-theme.ts`、`midnight-theme.ts`、`BitFun-Installer/src/theme/installerThemesData.ts` | `#9da0a8`、`#9ea4ab` 收敛到 `#a1a1aa`,Slate branch 复用 `SLATE_ACCENT`;均为跨主题 neutral text/branch 语义,不合并 error/warning/status 色,同名安装器预览同步 | +| installer preset seed compression | derive within installer namespace | `BitFun-Installer/src/theme/installerThemesData.ts`、`BitFun-Installer/src/styles/variables.css` | 安装器是简单首启/安装 surface,不应维护完整 app 级主题色板;8 套 installer 主题压缩为少量 background/accent/status seed 加 dark/light tone,theme preset 色值 160 -> 63、preset 唯一色 121 -> 60,静态变量同步用 alias 派生而不是复制色值;保留每个主题的 success/warning/error seed,避免安装进度、完成和错误反馈与最终主题状态色脱节 | +| generated widget payload shell compression | move host-internal keys to iframe shell | `src/web-ui/src/tools/generative-widget/themePayload.ts`、`GenerativeWidgetFrame.tsx` | host payload allowlist 153 -> 98;只保留外部 widget 真正需要随主题变化的 canonical 子集和 `components.button` 投影,tool-card/layout/旧 overlay 与 legacy alias 由 iframe fallback/static shell 派生,避免把宿主内部 key 变成插件或主题扩展 API;accent/status/border/radius/spacing/button 等主题敏感 key 必须继续由宿主 payload 覆盖,不能固化在 iframe shell | +| slate success and CLI added diff near merge | merge non-adjacent equivalent concepts | `src/web-ui/src/infrastructure/theme/presets/slate-theme.ts`、`src/apps/cli/themes/presets/bitfun-ink-night.json`、`bitfun-tokyo-night.json` | Slate success 复用已有 success green;CLI ink/tokyo added diff body 复用同一深绿非相邻 surface。保留 Cyber element/border、diff body/gutter 等相邻区分,web theme preset near 11 -> 10,CLI preset near 5 -> 4 | +| installer named dark background seed | preserve visible primary identity | `BitFun-Installer/src/theme/installerThemesData.ts` | 安装器界面简单,但 ThemeSetup 预览直接展示 primary/secondary background;Dark 复用 canonical seed,Ink Night/Cyber/Tokyo Night 保留各自 primary,secondary 复用 common dark surface。Installer theme preset 唯一色 57 -> 53,near pair 11 -> 3;剩余 near pair 是用户可见主题识别,而非内部冗余 | +| web overlay alpha ladder | preserve adjacent state stops | `tokens.scss`、`ThemeService.ts`、`themePayload.ts` | 复审后保留 `0.04/0.08/0.12/0.15/0.20` 等黑白 overlay stop;card、markdown、shadow 等相邻状态仍通过语义 key 表达,不再保留 `0.06/0.10` 微弱中间 stop。生成式 widget host payload 仍不转发静态 overlay,由 iframe fallback/static shell 派生 | +| generated widget static overlay payload | move to iframe shell | `src/web-ui/src/tools/generative-widget/themePayload.ts`、`themePayload.test.ts` | 静态黑白 overlay 不再由 host payload 转发,iframe fallback/static shell 直接派生;payload allowlist 98 -> 94,历史低阶 overlay alias 继续通过 iframe fallback 映射,不影响已生成 widget | +| canonical overlay micro-stop retirement | retire keys | `tokens.scss`、`ThemeService.ts`、`themePayload.ts`、内部 CSS var 读取 | `--color-overlay-white-06` 合并到 `--color-overlay-white-08`,`--color-overlay-white-10` 合并到 `--color-overlay-white-12`,`--color-overlay-black-10` 合并到 `--color-overlay-black-12`;只处理非语义弱 overlay,历史 generated widget 读取通过 iframe alias fallback 保留 | +| Mermaid light semantic status tokens | derive fill/stroke from app semantic status, preserve readable note text | `_tokens.scss`、`mermaidThemeFallbacks.ts` | light done/crit 颜色改读 success/error semantic token,note fill/stroke 由 warning 派生;note text 保留专用深色值以满足说明文字可读性。critical/error fallback fill 共享同一弱 error 背景,Mermaid unique 73 -> 65 且 near pair 保持 0 | +| Monaco diff background strength | preserve per-layer strength | `bitfun-dark.theme.ts` | 复审后保留 inserted/removed/modified 的 text、line、gutter 背景强度阶梯;代码审查场景需要同时识别整行变更、行内片段和 gutter 定位锚点,不能只按同 change type 合并 | +| generated widget derived payload keys | move derived keys to iframe fallback/static shell | `themePayload.ts`、`themePayloadCompatibility.ts`、`themePayload.test.ts` | accent 细分 stop、status border、strong/prominent border、strong element 和 `--size-radius-md` 不再从 host payload 读取;iframe fallback/static shell 继续解析这些派生 key,payload allowlist 94 -> 80 | +| CLI preset/runtime near cleanup | merge non-semantic close surfaces | `src/apps/cli/themes/presets/*.json` | Dark/Ink/Midnight/Cyber 的非状态 surface、input、border 和极近前景值复用已有 preset stops;CLI preset unique 122 -> 118、total unique 152 -> 148,preset/runtime near pair 均降为 0。OpenCode compatibility 字段单独计数,不冒充 runtime 收益 | +| generated widget button payload | preserve after adversarial review | `src/web-ui/src/tools/generative-widget/themePayload.ts`、`themePayload.test.ts` | 曾评估将 button component token 移入 iframe shell 派生,但 Cyber/Tokyo/light 等主题的 button bg、border、shadow、transform 和 hover/active 不是稳定可派生值;因此 payload 保持 80,button payload 保持 18,避免牺牲 generated widget 的主题识别和交互反馈来换取表面 key 数下降 | +| CLI truecolor fallback palette | derive from built-in preset JSON | `src/apps/cli/src/ui/theme.rs`、`scripts/theme-color-governance-baseline.cli.json` | `Theme::dark()` / `Theme::light()` 不再手写第二套 `Color::Rgb` palette,而是解析内置 OpenCode-compatible preset;不完整外部 theme 仍回退到 base。用户可见 truecolor 默认值会跟随内置 preset:dark primary/background/panel 从 `#3b82f6/#111827/#1e2637` 到 `#60a5fa/#0e0e10/#1c1c1f`,light 从 `#2563eb/#f9fafb/#f0f2f5` 到 `#475569/#f3f3f5/#ffffff`;ANSI16/monochrome 不变。复审后补充 role 级对比 guard,并将 command prompt 改为跟随 primary,以覆盖 command card block/hover 背景;light diff added/removed、light diff line number、light warning 以及 dark diff line number 收敛到更可读的同主题既有/同义色。Rust fallback RGB 44 -> 0,fallback unique 32 -> 0,CLI total unique 148 -> 114,runtime unique 107 -> 75 | +| mobile static overlay foreground | tokenized static color | `src/mobile-web/src/theme/presets/shared.ts`、`chat-input.scss`、`light.ts` | 图片缩略图关闭按钮必须在黑色 overlay 上保持白色前景;新增 mobile `--color-static-white` 后组件 raw `#fff` 归零,light secondary 改读该静态 token,mobile 总颜色出现次数不增加且唯一色 31 -> 30 | +| Rust GUI theme config fallback | retire legacy schema | `src/crates/assembly/core/src/service/config/{types,manager,providers,global,service}.rs` | 删除旧顶层 GUI `theme` struct/provider/default 导出,只保留 `theme.id` -> `themes.current` 的加载、导入和旧调用路径 fallback;导入路径保留 raw config JSON 到 manager 归一化后再反序列化,避免旧 `theme.id` 被提前丢弃。Rust 不再拥有完整 UI theme schema,终端 `terminal.theme` 仍作为 ANSI palette 历史字段保留 | +| web theme preset near cleanup | merge non-critical preset surfaces | `src/web-ui/src/infrastructure/theme/presets/{china-style,light,slate,tokyo-night}-theme.ts` | Light quaternary 复用浅灰背景、Slate button text 复用 primary text、China Style elevated 复用 tertiary、Tokyo border 复用既有深 slate;产品复审后保留 Ink Night secondary,因为它也承担 Monaco lineHighlight 和相邻 ink surface 层级。Theme preset unique 119 -> 116,near pair 9 -> 5 | +| installer dark preview background cleanup | preserve adjacent theme-card identity | `BitFun-Installer/src/theme/installerThemesData.ts` | 产品复审确认 ThemeSetup 会相邻展示主题卡并直接渲染 primary/secondary background,因此 Ink Night/Cyber/Tokyo Night primary 属于用户可见主题识别,不再合并。Installer theme preset unique 保持 53,near pair 保持 3;后续若要继续压缩,需要先新增非背景 identity swatch 或截图证明不会削弱选择识别。 | +| CLI truecolor readable role cleanup | merge consumed visible roles | `src/apps/cli/themes/presets/bitfun-{dark,light}.json`、`src/apps/cli/src/ui/theme.rs` | 只处理 CLI renderer 当前消费的 foreground role:light diff added/removed 复用同主题 highlight 色,light diff line number 复用 textMuted,light warning/markdownEmph/syntaxNumber 共用更深 amber seed,dark diff line number 复用 textMuted;不处理未消费的 `diff*LineNumberBg` 兼容字段。preset unique 118 -> 114,runtime preset unique 77 -> 75,并用 role contrast 单测覆盖 diff、warning、muted、command 和 line-number 表面 | | remaining near pairs | none in ordinary components | 无 | 审计口径下普通组件 near pair 已清零;后续只在专用 palette 自身重设计时处理 Monaco/terminal/Mermaid/syntax 内部近似色 | | Monaco theme palette | classify as exception | `tools/editor/themes/bitfun-dark.theme.ts` | 该文件是 Monaco theme 完整色板,不是普通 app UI;归入 editor/exception 后不再被误计为 component raw color | | Flow Chat capture fallback | boundary fallback | `ExportImageButton.tsx`、`captureElementToDownloadsPng.tsx` -> `themeBoundaryFallbacks.ts` | `#121214` 只在 root theme 变量不可用时兜底截图背景,集中 owner 后避免截图工具重复携带 raw fallback | +| git runtime alias surface | retire unused keys | `tokens.scss`、`ThemeService.ts` | `--git-color-pull*`、`--git-color-push*`、`--git-color-branch-border`、`--git-color-added-border`、`--git-color-changes-border`、`--git-color-changes-bg-hover` 和 `--git-color-deleted-border` 没有产品读取,且分别只是 branch/staged/status 派生同义 key;删除后实际渲染仍读取 canonical branch、staged、added、changes、deleted key,web-ui 定义 key 956 -> 945 | +| installer minimal runtime projection | retire unused installer keys | `BitFun-Installer/src/theme/installerThemesData.ts`、`installerThemeRuntime.ts`、`variables.css`、`ThemeSetup.tsx` | 安装器是简单首启/安装 surface,不消费完整 app 主题 schema;删除未读取的 background tertiary/quaternary/elevated/workbench/flowchat/tooltip、text disabled、purple family、info/highlight、border strong/prominent、element base/elevated 和额外 radius key。ThemeSetup 预览仍保留 primary/secondary background、element soft、muted text 和 accent;安装器定义 key 54 -> 30,唯一色 72 -> 66 | -Phase 6 首轮约束: +Phase 6 防回退约束: | 约束 | 当前值 | baseline | 作用 | | --- | ---: | ---: | --- | | `nearPairs.indistinguishableTotal` | 0 | 0 | 阻止新增普通组件肉眼不可区分 pair 未被合并或记录 | | `nearPairs.nearTotal` | 0 | 0 | 阻止新增普通组件 near color 债务;新增必须合并、归类或记录理由 | +| `colorDomainNearPairs.indistinguishableTotal` | 0 | 0 | 控制专用域肉眼不可区分 pair 不继续增长,后续只能逐步降低或补充证据 | +| `colorDomainNearPairs.nearTotal` | 9 | 9 | 控制 theme preset/runtime/token/editor/Mermaid 等专用域 near 队列规模 | | `colorScopes.appUi.uniqueColors` | 0 | 0 | 阻止普通组件 raw color 唯一色回涨 | | `colorScopes.appUi.occurrences` | 0 | 0 | 阻止普通组件 raw color 出现次数回涨 | +| `colorScopes.exception.uniqueColors` | 182 | 182 | 控制专用域/例外域总体规模;UI exception、syntax 和 language identity 已收敛,但 Mermaid/editor/terminal 仍按各自 owner 单独治理 | +| `colorDomainScopes.syntax.occurrences` | 16 | 16 | 阻止 Prism syntax palette 回到一 token class 一色的不可扩展模式,同时保留相邻 token 可读性边界 | +| `colorDomainScopes.languageIdentity.uniqueColors` | 8 | 8 | 阻止 language/file identity 回到一语言一色或一扩展一色的不可扩展模式 | +| `colorDomainScopes.boundaryFallback.occurrences` | 18 | 18 | 防止 iframe/mini app/截图兜底色重新散写;导出 key 可保留语义,实际字面值必须回到 boundary fallback palette | | `tokenAliasLiterals.occurrences` | 0 | 0 | 阻止重新出现可映射到 token 的 app literal | | `colorDomainScopes.appUi.occurrences` | 0 | 0 | 阻止未归类 app UI 色值回涨 | | CSS var governance errors | 0 | 0 | 保持 unresolved、fallback-only、non-contract 和 dynamic family 错误为零 | +| `compatibilityAliases.usedUnique` | 0 | 0 | 阻止产品代码重新通过旧 alias key 读取主题变量 | +| `compatibilityAliases.occurrences` | 0 | 0 | 阻止历史 alias 调用点回涨 | +| `compatibilityAliases.familyUsedUnique` | 0 | 0 | 阻止 `--radius-*`、`--spacing-*` 旧 family 重新成为内部读取面 | +| `compatibilityAliases.familyOccurrences` | 0 | 0 | 阻止旧 family 读取次数回涨 | | `compatibilityAliases.staleRegisteredUnique` | 0 | 0 | 防止兼容 alias registry 保留没有定义或 canonical 目标缺失的 key | -| `compatibilityAliases.staleRegisteredFamilyUnique` | 0 | 0 | 防止 `--radius-*`、`--spacing-*` 这类动态 family 与 canonical family 失配 | +| `compatibilityAliases.staleRegisteredFamilyUnique` | 0 | 0 | 防止 `--radius-*`、`--spacing-*` family contract 指向缺失的 canonical family | | `compatibilityAliases.missingCanonicalUnique` | 0 | 0 | 防止 family alias 具体 key 缺失对应 canonical key | +| `surfaceTokenRenames.activeUnique` | 0 | 0 | 防止已迁移的 surface-local 旧 key 重新出现 | +| `surfaceTokenRenames.activeOccurrences` | 0 | 0 | 防止旧 key 在定义和读取两侧回流 | +| `surfaceTokenRenames.missingCanonicalUnique` | 0 | 0 | 防止 surface rename contract 指向不存在的 canonical key | +| `generatedWidgetPayload.varUnique` | 80 | 80 | 控制 widget 对外主题 payload allowlist 不继续膨胀;该预算只覆盖主题敏感 canonical 子集和 button component 投影,宿主内部、静态 overlay、派生 key 和历史兼容 key 不计入 payload API | +| generated widget button payload | 18 | 18 | Cyber/Tokyo/light 的 button bg、border、shadow、transform 和交互态不是低风险静态 key;除非 resolved button 输出等价,否则不从 host payload 移入 iframe shell | +| `generatedWidgetPayload.compatibilityAliasUnique` | 0 | 0 | 防止 payload 重新直接导出显式 legacy alias | +| `generatedWidgetPayload.compatibilityAliasFamilyUnique` | 0 | 0 | 防止 payload 重新直接导出 legacy size family 具体 key | +| `generatedWidgetPayload.externalOnlyCompatibilityUnique` | 0 | 0 | 防止 payload 重新保留仅因外部兼容存在的 legacy key | +| `generatedWidgetPayload.undefinedUnique` | 0 | 0 | 防止 payload 导出未定义主题 key | +| `generatedWidgetPayload.missingCompatibilityCanonicalUnique` | 0 | 0 | 防止 payload 兼容 alias 缺失 canonical 目标 | +| `generatedWidgetPayload.unexportedCompatibilityCanonicalUnique` | 0 | 0 | 防止 payload 兼容 alias 有 canonical 定义但未导出到 iframe | +| `mobile-web` app UI raw color | 0 | 0 | mobile-web 组件不再允许 raw app color;图片缩略图黑色 overlay 上的关闭按钮前景读取 `--color-static-white` | +| `mobile-web` dynamic families | 3 | 3 | `--color-accent-*`、`--color-purple-*`、`--color-pink-*` 由 mobile preset 拥有 | +| `installer` app UI raw color | 0 | 0 | 安装器组件不得携带 raw app color | +| `installer` dynamic families | 1 | 1 | 安装器只导出实际消费的 accent family;secondary purple family 不再作为安装器扩展面 | +| `cli` total unique colors | 114 | 114 | 控制 CLI/TUI preset 不继续膨胀;truecolor fallback 已由内置 preset JSON 派生,不再维护独立 Rust RGB palette | +| `cli` runtime unique colors | 75 | 75 | 只统计 BitFun CLI renderer 当前消费的 preset key;OpenCode 兼容声明和 legacy fallback 不冒充实际运行时预算 | +| `cli` runtime preset unique colors | 75 | 75 | 控制 CLI 当前消费的 preset key,不包含 markdown/syntax/diff line-number background 等未消费兼容字段 | +| `cli` compatibility preset unique colors | 87 | 87 | 单独约束 OpenCode schema 兼容声明;该口径不能冒充 BitFun CLI 运行时收益 | +| `cli` preset near pairs | 0 | 0 | 阻止 CLI preset 重新引入未解释的近似色;OpenCode compatibility 字段仍单独审计 | +| `cli` runtime preset near pairs | 0 | 0 | 阻止当前 renderer 消费路径重新引入近似色;后续压缩需先确认终端相邻状态和 diff 语义不会被削弱 | +| `cli` Rust fallback near pairs | 0 | 0 | fallback palette 不再允许新增近似重复色 | | `fallbackContracts.uncontractedUnique` | 0 | 0 | 防止新增未说明边界的 `var(--token, fallback)` | | `fallbackContracts.staleRegisteredUnique` | 0 | 0 | 防止已删除 fallback 继续留在 registry 中 | | `colorDomainContracts.activeUncontractedUnique` | 0 | 0 | 防止新增专用颜色域但没有 owner 和 merge policy | `nearPairs.*` 只基于非 token、非 exception 的普通组件颜色计算。Theme preset、 editor、syntax、terminal、language identity、boundary fallback 等专用域通过各自 -`colorDomainScopes.*` 预算约束,不用该 near-pair guard 直接判定是否可合并。 +`colorDomainScopes.*` 和 `colorDomainNearPairs.*` 预算约束。专用域 near 队列用于 +安排截图和语义复核,不直接判定是否可合并。 视觉证据契约新增在 `scripts/theme-visual-governance-contract.json`,并由 `pnpm run theme:visual-contract` 校验。它不是截图替代品,而是后续 PR 的覆盖面 @@ -222,15 +494,16 @@ editor、syntax、terminal、language identity、boundary fallback 等专用域 | surface | 覆盖形态 | 重点风险 | | --- | --- | --- | -| app-shell | desktop webview、web、desktop、narrow、dark/light/system | 旧 alias 仍在 shell 邻近组件使用,system theme 不能假设桌面专有行为 | +| app-shell | desktop webview、web、desktop、narrow、dark/light/system | shell 邻近组件必须只读 canonical token,system theme 不能假设桌面专有行为 | | flow-chat | desktop webview、web、desktop、narrow、streaming/error/empty | virtualized 和历史 turn 可能隐藏 token 回归 | -| tool-cards-review | tool card、review panel、expanded/collapsed/status | danger alias 保留 destructive 语义,不能和 error 无证据合并 | +| tool-cards-review | tool card、review panel、expanded/collapsed/status | destructive/error 状态统一读取 `--color-error*`;仅 generated widget iframe fallback 保留历史 `--color-danger*` 映射 | | code-editor-diff | Monaco、diff、selection、added/deleted/conflict | editor/diff 色表达相邻状态,不能按数值相似直接合并 | | terminal | ANSI normal/bright、selection、error | ANSI 语义独立于 app semantic color | -| markdown-mermaid | Markdown、Prism、Mermaid、diagram/error | `--primary-color` 是 embedded override;Mermaid 角色不等于 app status | -| generated-widget | iframe fallback、host payload、loading/error | widget payload 兼容是保留多组旧 alias 的主要原因 | -| theme-settings | theme switcher、system/custom theme preview | custom theme preview 可能比普通组件更早暴露 runtime alias 缺失 | +| markdown-mermaid | Markdown、Prism、Mermaid、diagram/error | Markdown accent 通过 `--markdown-primary-color` 表达;tool-card 历史 `--primary-color` 已退场,Mermaid 角色不等于 app status | +| generated-widget | iframe fallback、host payload、loading/error | payload 只导出 canonical key;旧 alias 兼容集中在 iframe fallback | +| theme-settings | theme switcher、system/custom theme preview | custom theme preview 可能比普通组件更早暴露 runtime canonical token 缺失 | | mobile-web-shell | mobile-web、mobile/narrow、loading/error/navigation | mobile web 是独立构建目标,不能只依赖 desktop WebView 验证 | +| installer-shell | installer、theme setup、language/options、loading/error | installer 是独立 React/Tauri surface;主题变量由 installer TS root 维护,Rust 不得复制 palette | ## 现有架构地图 @@ -240,20 +513,29 @@ editor、syntax、terminal、language identity、boundary fallback 等专用域 `:root` CSS 变量。 - `src/web-ui/src/infrastructure/theme/core/ThemeService.ts` 根据当前主题在运行时注入 CSS 变量, 同时补充了一批 app 级别别名和覆盖值。 -- `src/web-ui/src/theme/presets/*.ts` 定义主题预设色板。 +- `src/web-ui/src/infrastructure/theme/presets/*.ts` 定义完整主题预设色板。 +- `src/apps/desktop/src/theme.rs` 只维护 WebView 首屏最小 bootstrap 投影,不能扩展为完整主题 schema。 +- `src/crates/assembly/core/src/service/config/manager.rs` 只保留旧 `theme.id` 到 + `themes.current` 的兼容归一化;`GlobalConfig` 不再包含顶层 GUI `theme`。 - `src/web-ui/src/tools/generative-widget/themePayload.ts` 向 generative widget payload 暴露部分主题变量。 -- 组件 SCSS/CSS/TSX 中存在大量局部颜色字面量和局部 fallback。 +- `src/mobile-web/src/theme/presets` 定义 mobile-web 的独立运行时 token;移动端组件只读这些 + canonical key,不从 web-ui 或 Rust 复制 fallback palette。 +- `BitFun-Installer/src/theme` 和 `BitFun-Installer/src/styles/variables.css` 定义安装器主题数据、 + 运行时注入色阶和首屏静态变量;安装器 Rust 不拥有 UI palette。 +- 普通组件 SCSS/CSS/TSX 的 raw app color 与局部 fallback 已由 baseline 约束为 0; + 新增必须先进入 token/exception 决策。 主要架构问题: -- 静态 token 和运行时 token 没有共享单一注册表。 -- 有些 token 只由 `ThemeService.ts` 动态注入,但组件 fallback 假设它们 - 在所有渲染边界都存在。 -- 同一个语义角色存在多种历史命名方式。 -- 组件 fallback 中的字面量过多,导致 fallback 变成实际上的第二套色板。 -- 当前主题验证链路不能作为充分的可访问性证据,contrast 计算需要真实实现 - 后才能支撑大规模颜色合并判断。 +- 静态 token、运行时 token、payload allowlist、mobile preset 和 installer preset 已由审计 + contract 串联,但还不是单一代码生成源;新增 root 或 surface 时必须同步 contract owner。 +- 有些 token 仍由运行时动态注入,必须通过 dynamic family contract 说明 owner,不能在组件内 + 用 fallback literal 暗中兜底。 +- 同一个语义角色的旧命名已退役为 baseline 约束;后续风险是旧 key 通过新 root、inline style + 或生成式 widget 边界回流。 +- 当前主题验证链路仍不能替代可访问性证据,contrast 计算和 focused visual review 仍是大规模 + 近似色合并前的必要条件。 ## 问题分类 @@ -278,7 +560,7 @@ editor、syntax、terminal、language identity、boundary fallback 等专用域 改进方向: - fallback 只保留在明确的兼容边界。 -- 根主题层先补足兼容别名。 +- 根主题层不再新增显式 legacy alias;确有外部边界时优先放在 generated widget iframe fallback 或 family contract。 - 组件确认 canonical token 一定存在后,移除局部 fallback 字面量。 ### 3. 未定义或历史命名 token @@ -343,8 +625,10 @@ editor、syntax、terminal、language identity、boundary fallback 等专用域 - 不能只按色差或 RGB 距离合并。 - 先判断语义角色、相邻关系、交互状态、主题预设和可访问性,再决定是否替换。 - 对于白/黑透明叠层,先建立精确等价的 overlay ramp,例如 - `--color-overlay-white-08` 和 `--color-overlay-white-10`。这一步只消除散落 + `--color-overlay-white-08` 和 `--color-overlay-white-12`。这一步只消除散落 硬编码,不合并不同 alpha,因为 alpha 差异经常用于表达层级和状态。 +- 如果数值型 overlay key 已经实际别名到另一个 alpha stop,内部读取应迁到真实 + canonical stop,并删除误导性的旧导出。 ## 目标 Token 模型 @@ -408,23 +692,23 @@ semantic token 描述产品级语义,应作为共享 UI 的默认使用层。 ### 兼容别名 -第一阶段应先保留兼容别名,避免为了清理 token 引入大面积视觉变化。 +显式 root/runtime 兼容别名已完成退役,避免旧 key 长期占用主题契约预算;历史 generated widget 内容通过 iframe fallback 兼容。 | 历史或漂移 token | 建议 canonical 目标 | 说明 | | --- | --- | --- | -| `--accent-primary` | `--color-accent-500` 或 `--color-primary` | 先明确 accent 与 primary 是否是两个角色。 | -| `--text-primary` | `--color-text-primary` | 仅兼容别名。 | -| `--text-secondary` | `--color-text-secondary` | 仅兼容别名。 | -| `--text-muted` | `--color-text-muted` | 仅兼容别名。 | -| `--bg-primary` | `--color-bg-primary` | 仅兼容别名。 | -| `--bg-secondary` | `--color-bg-secondary` | 仅兼容别名。 | -| `--bg-tertiary` | `--color-bg-tertiary` | 仅兼容别名。 | -| `--border-primary` | `--border-base` | 需要确认调用点是否期望更强边界。 | -| `--color-border-subtle` | `--border-subtle` | 建议统一 border 命名族。 | -| `--color-danger` | `--color-error` | 仅当 destructive 与 validation error 不需要区分时合并。 | -| `--color-bg-hover` | `--element-bg-hover` | 需要确认调用点是 element、card 还是 panel hover。 | -| `--radius-*` | `--size-radius-*` | 对齐静态 CSS export 与运行时/widget 命名。 | -| `--spacing-*` | `--size-gap-*` | 对齐静态 CSS export 与运行时/widget 命名。 | +| `--accent-primary` | `--color-accent-500` | root/runtime alias 已退役;历史 generated widget 内容仅通过 iframe fallback 映射到 accent midpoint。 | +| `--text-primary` | `--color-text-primary` | root/runtime alias 已退役;历史 generated widget 内容仅通过 iframe fallback 映射。 | +| `--text-secondary` | `--color-text-secondary` | root/runtime alias 已退役;历史 generated widget 内容仅通过 iframe fallback 映射。 | +| `--text-muted` | `--color-text-muted` | root/runtime alias 已退役;历史 generated widget 内容仅通过 iframe fallback 映射。 | +| `--bg-primary` | `--color-bg-primary` | root/runtime alias 已退役;历史 generated widget 内容仅通过 iframe fallback 映射。 | +| `--bg-secondary` | `--color-bg-secondary` | root/runtime alias 已退役;历史 generated widget 内容仅通过 iframe fallback 映射。 | +| `--bg-tertiary` | `--color-bg-tertiary` | root/runtime alias 已退役;历史 generated widget 内容仅通过 iframe fallback 映射。 | +| `--border-primary` | `--border-base` | root/runtime alias 已退役;历史 generated widget 内容仅通过 iframe fallback 映射。 | +| `--color-border-subtle` | `--border-subtle` | root/runtime alias 已退役;历史 generated widget 内容仅通过 iframe fallback 映射。 | +| `--color-danger` | `--color-error` | root/runtime alias 已退役;历史 generated widget 内容仅通过 iframe fallback 映射到 error palette。 | +| `--color-bg-hover` | `--element-bg-hover` | root/runtime alias 已退役;泛化 hover 收敛到 element interaction layer。 | +| `--radius-*` | `--size-radius-*` | canonical family 为 `--size-radius-*`;旧 family 已从 root/runtime 退役,仅作为 generated widget iframe fallback 兼容和内部回读拦截规则。 | +| `--spacing-*` | `--size-gap-*` | canonical family 为 `--size-gap-*`;旧 family 已从 root/runtime 退役,仅作为 generated widget iframe fallback 兼容和内部回读拦截规则。 | ## 近似色合并规则 @@ -546,7 +830,7 @@ semantic token 描述产品级语义,应作为共享 UI 的默认使用层。 验收标准: -- 脚本可以在 `src/web-ui` 上无副作用运行。 +- 脚本可以在 `src/web-ui/src`、`src/mobile-web/src` 和 `BitFun-Installer/src` 上无副作用运行。 - 报告可以对比 baseline 与当前分支。 - 报告能区分普通 app color 与已知 exception namespace。 @@ -655,8 +939,8 @@ semantic token 描述产品级语义,应作为共享 UI 的默认使用层。 交付物: - 对组件中新 app raw color 的 lint 或 audit 检查。 -- 已知 exception file 与 namespace allowlist。 -- compatibility alias、fallback token、color domain 的机器可校验 owner/reason contract。 +- 已知 exception file、namespace contract 与 owner。 +- compatibility alias、color domain 的机器可校验 owner/reason contract;fallback registry 维持为空并由 baseline 防回退。 - 覆盖 app-shell、Flow Chat、tool card/review、editor/diff、terminal、Mermaid/Markdown、 generated widget、theme settings 和 mobile web 的视觉证据契约。 - CI 在迁移期只阻止新增问题,不因历史 baseline 直接失败。 @@ -666,8 +950,8 @@ semantic token 描述产品级语义,应作为共享 UI 的默认使用层。 - 新增组件级 raw color 必须有明确原因。 - 历史迁移可以按目录增量推进。 - exception 可见、可审查。 -- 兼容 alias 和边界 fallback 可见、可审查,且 stale contract 为 0。 -- CI 至少运行 `theme:color-audit:test`、`theme:color-audit` 和 +- 兼容 alias 可见、可审查;组件 fallback 保持 0,且 stale contract 为 0。 +- CI 至少运行 `theme:color-audit:test`、`theme:color-audit:all` 和 `theme:visual-contract`。 ## 风险清单 @@ -679,11 +963,12 @@ semantic token 描述产品级语义,应作为共享 UI 的默认使用层。 | intent 色被过度归一 | warning、error、success、info 或 destructive 语义混淆。 | intent token 即使色值接近,也保留独立语义。 | | git/diff 色被当作普通 success/error | added/deleted/changed/conflict 扫描效率下降。 | 使用专用 git/diff token,只有复核后才 alias 到 app intent。 | | 主题个性被抹平 | 用户选择主题的价值下降。 | theme preset 保留自己的 primitive/accent 映射。 | -| fallback 先删、alias 后补 | embedded 或 early render surface 样式丢失。 | 先加 alias,再删除 fallback。 | -| 静态 token 与运行时 token 不一致 | widget、SCSS、runtime theme 注入结果不一致。 | `tokens.scss`、`ThemeService.ts`、`themePayload.ts` 同阶段对齐。 | +| fallback 先删、边界兜底后补 | embedded 或 early render surface 样式丢失。 | 先确认 canonical export 或边界 fallback,再删除普通组件 fallback。 | +| 兼容 alias 读点清零时误删边界兼容 | 旧主题、生成式 widget iframe fallback 或外部自定义内容读取旧 key 时样式丢失。 | 只迁移内部 `var()` 读取;root/runtime 显式 alias 保持退役,widget payload 只导出 canonical key,legacy key 仅由 iframe fallback 映射。 | +| 静态 token 与运行时 token 不一致 | widget、SCSS、runtime theme 注入结果不一致。 | `tokens.scss`、preset helper、`ThemeService.ts`、`themePayload.ts` 同阶段对齐。 | | 动态 CSS 变量 key 被误判为未定义 | inline style 或数据驱动变量失去兜底,导致特定卡片、标签或分组颜色缺失。 | 对动态 key 建立运行时设置清单;删除 fallback 前补组件根默认值或保留边界 fallback。 | | contrast 验证不可信 | 可访问性回归可能漏掉。 | 先实现真实 contrast 检查,再声称可访问性改善。 | -| 迁移 PR 过大 | review 疲劳导致视觉回归漏审。 | 按 surface 拆 PR,每个 PR 附指标和截图。 | +| 迁移 PR 过大 | review 疲劳导致视觉回归漏审。 | 按可验证的大块 contract/surface 组织 PR;每个 PR 附指标,避免拆成难以形成完整治理收益的零碎提交。 | | editor/terminal 颜色被强行泛化 | 代码语法和 terminal 语义下降。 | 建立 exception namespace,而不是直接套普通 app token。 | ## 候选决策 @@ -764,7 +1049,7 @@ alpha 差异经常承担 elevation 和交互状态,不应全部压成一个值 实现类 PR: - `pnpm run theme:color-audit:test` -- `pnpm run theme:color-audit` +- `pnpm run theme:color-audit:all` - `pnpm run theme:visual-contract` - `pnpm run lint:web` - `pnpm run type-check:web` @@ -792,8 +1077,8 @@ alpha 差异经常承担 elevation 和交互状态,不应全部压成一个值 | 指标 | 目标 | | --- | --- | | 组件文件 raw color literal | 每个迁移 PR 递减。 | -| 组件级 fallback literal | 兼容 alias 落地后递减。 | -| 未定义或历史 token 使用 | 除文档化 alias 外逐步清零。 | +| 组件级 fallback literal | 明确边界 contract 后递减。 | +| 未定义或历史 token 使用 | 内部 compatibility alias `var()` 读取保持 0;新增旧 key 读取必须先说明兼容边界。 | | token 文件中的精确重复 literal | 改为 alias 表达。 | | 近似色合并候选 | 每个都有 `merge`、`defer` 或 `do not merge` 决策。 | | 视觉回归 | 已复核 surface 无回归。 | @@ -805,7 +1090,7 @@ alpha 差异经常承担 elevation 和交互状态,不应全部压成一个值 | app 级 raw color literal | 普通组件中趋近于 0。 | | unique app color literal | 进入 token 层后受预算约束,不再随组件增长。 | | undocumented component color | 0。 | -| exception namespace color | 有 allowlist 和 owner。 | +| exception namespace color | 有 contract 和 owner。 | ## Review Checklist @@ -820,51 +1105,83 @@ alpha 差异经常承担 elevation 和交互状态,不应全部压成一个值 - 变更是否同时影响 light 和 dark theme。 - 变更是否影响 generated widget、code editor、terminal、Mermaid 或第三方内容。 - 删除 fallback 前,兼容 alias 是否已经存在。 -- 新增或保留的 compatibility alias、fallback token、color domain 是否进入对应 contract。 +- 新增或保留的 compatibility alias、color domain 是否进入对应 contract;新增 fallback 是否确有边界理由。 - 变更影响的 surface 是否已对照 `theme-visual-governance-contract.json` 确认覆盖形态。 - 高风险 surface 是否有截图或 focused visual check。 - PR 描述是否说明了任何用户可见视觉变化。 -## 建议 PR 拆分 - -建议按证据和 surface 拆分,避免一次性大迁移: +## 持续收敛约束 -1. 审计工具和 baseline report。 -2. canonical token map 与 compatibility alias。 -3. 静态和运行时 token 对齐。 -4. token 文件中的精确重复合并。 -5. component-library fallback 迁移。 -6. Flow Chat surface 迁移。 -7. tool card 和 review panel 迁移。 -8. git/diff surface 迁移。 -9. widget/editor/terminal namespace 清理。 -10. 带截图的近似色合并批次。 -11. 新增 raw app color 的防回退约束。 +以下内容不是日期或进度记录,而是后续每轮主题变更都必须保持的约束: -每个 PR 应包含: +1. 中心 token 与 mixin 层压缩:普通消费侧应保持不直接读取颜色类 Sass token;压缩空间集中在 + `tokens.scss` 的静态定义、legacy mixin、未消费 root export、badge/glass/shadow 派生和局部变量别名。 + 只有被 `var()`、runtime 注入、payload 或明确边界消费的 key 才应进入 root/runtime contract。 +2. 插件/主题扩展投影治理:插件侧只消费 `pluginThemeProjection.ts` 暴露的 7 个 OpenCode-compatible 语义色 key; + 新增插件 UI 需求必须先证明无法复用这 7 个 key,再新增小型 surface projection。不得把 generated widget payload + 或完整 runtime CSS var 表作为插件主题 schema。 +3. 专用域近似色复核:mobile-web theme preset near pair 已清零;installer 保留 3 个相邻主题卡可见的 named dark primary preview pair;web-ui 已先处理不可感知的跨主题 neutral、tooltip 派生、runtime/static panel 值、极近 overlay fallback,以及 theme preset 的非状态 neutral/弱文本/scene/workbench 值。 + 后续继续处理 theme preset 时必须避开按钮 hover/active、Tokyo Night 主题识别主背景、Ink Night secondary/lineHighlight、Monaco lineHighlight、Midnight lineHighlight/elevation、Mermaid、terminal、syntax 等相邻状态色,除非有截图证据证明不会损害层级。 +4. CLI/TUI palette 压缩:CLI/TUI 已纳入 `theme:color-audit:cli`,truecolor fallback 由内置 preset JSON 派生,Rust 不再手写独立 RGB palette;Rust fallback、preset 和 runtime-consumed preset near pair 均为 0。后续只处理 preset 内能证明同一终端 surface 语义且已被 CLI renderer 消费的近似色, + 不能让 Rust/CLI 复制 web-ui palette。若要跨 surface 共享,只能先定义共享语义投影或生成链路。 +5. 自定义主题扩展后续体验优化:custom theme 校验、加载、注册、导出和 preview 输入已绑定到 TS schema; + 如需继续改善首屏体验,只允许由 TS schema 生成最小 bootstrap cache,不允许 Rust 直接拥有 custom theme schema。 +6. generated widget 兼容面维护:payload 已停止导出 `background/bg/text/radius/spacing` legacy key、静态黑白 overlay 转发、派生 accent/status/border/radius key 和已归并的 + `--color-overlay-white-02`、`--color-overlay-white-05`、`--color-overlay-white-06`、`--color-overlay-white-10`、`--color-overlay-black-06`、`--color-overlay-black-10`、`--color-overlay-black-25`;resolved button component token 因承载主题身份和交互反馈继续保留。历史内容兼容通过 iframe alias fallback 保留。后续新增 widget token 必须先导出 canonical, + 再评估是否需要 iframe-only alias。 -- 范围和影响 surface。 -- before/after 指标。 -- 用户可见 surface 的截图。 -- 命中的 visual governance surface 和对应证据类型。 -- 明确保留的近似色列表。 -- 验证命令和结果。 +每个 PR 应包含范围、影响 surface、before/after 指标、命中的 visual governance surface、 +明确保留的近似色列表,以及验证命令和结果。 -## 待决问题 +## 已审定兼容策略 -以下问题仍是产品语义决策,不再是未登记游离 key。当前已进入 +以下 key 不再视为未登记游离 key。当前已进入 `TOKEN_COMPATIBILITY_ALIAS_CONTRACTS` 或 `TOKEN_COMPATIBILITY_ALIAS_FAMILY_CONTRACTS`, -删除前必须先完成调用点迁移、widget payload 兼容检查和视觉复核。 - -- `--color-text-tertiary` 应转正为一等 semantic token,还是迁移到 - `--color-text-muted`。 -- `--color-primary` 和 `--color-accent-500` 是否是两个角色,还是应统一为 - 一个 accent contract。 -- `--color-danger` 是否需要和 `--color-error` 区分,以表达 destructive action。 -- 尺寸 token 长期应统一为 `--size-radius-*` / `--size-gap-*`,还是继续暴露 - `--radius-*` / `--spacing-*` 兼容名。 -- 迁移期 CI 应如何严格:只阻止新增 raw app color,还是按目录迁移完成后 - 对该目录启用失败约束。 +内部调用方必须使用 canonical token;删除旧 key 前必须先完成 widget iframe fallback 兼容检查、 +外部内容影响评估和视觉复核。 + +- `--color-text-tertiary` 当前不是一等 text ramp,root/runtime alias 已退役; + 历史 generated widget 内容仅通过 iframe fallback 映射到 `--color-text-muted`。 +- `--color-primary*`、`--accent-primary*`、`--color-accent` 和旧 semantic numeric key + 已从 root token、runtime 注入和全局 compatibility alias registry 退役;新代码应使用 + accent scale、semantic role 或组件 action token,历史 generated widget 内容仅通过 iframe alias fallback 映射。 +- `--color-danger*` 已从 root token、runtime 注入和全局 compatibility alias registry 退役; + 内部 destructive/error 状态统一读取 `--color-error*`,历史 generated widget 内容仅通过 iframe alias fallback 映射。 +- generated widget payload 已停止导出低风险的 `--color-accent*` legacy、`--color-primary*`、 + `--accent-primary*`、`--color-danger*`、旧 surface/bg/text 细分别名、旧 semantic scale、 + 旧 border 细分别名、核心 `background/bg/text` 兼容名、`--radius-*`、`--spacing-*` + 以及部分旧 font/motion 拼写和 tool-card 颜色转发;根 token 与 runtime 注入不再保留显式 legacy alias,generated widget + iframe 提供 alias fallback,避免影响已生成 widget 内容。 +- `--color-overlay-white-03` 已从静态 root、runtime 注入和 generated widget payload 退役; + generated widget iframe alias fallback 将其映射到 `--color-overlay-white-04`,避免历史内容丢失样式。 +- `--color-overlay-white-02` 已从 root/runtime、内部读取和 generated widget payload 退役; + generated widget iframe alias fallback 将历史读取映射到 `--color-overlay-white-04`。 +- `--color-overlay-white-05` 和 `--color-overlay-white-06` 已从 root/runtime、内部读取和 generated widget payload 退役; + generated widget iframe alias fallback 将历史读取映射到 `--color-overlay-white-08`。 +- `--color-overlay-white-10` 已从 root/runtime、内部读取和 generated widget payload 退役; + generated widget iframe alias fallback 将历史读取映射到 `--color-overlay-white-12`。 +- `--color-overlay-black-06` 已从 root/runtime、内部读取和 generated widget payload 退役; + generated widget iframe alias fallback 将历史读取映射到 `--color-overlay-black-08`。 +- `--color-overlay-black-10` 已从 root/runtime、内部读取和 generated widget payload 退役; + generated widget iframe alias fallback 将历史读取映射到 `--color-overlay-black-12`。 +- `--color-overlay-black-25` 已从 root/runtime、内部读取和 generated widget payload 退役; + generated widget iframe alias fallback 将历史读取映射到 `--color-overlay-black-30`。 +- generated widget payload 不导出宿主 FlowChat、navigation、z-index 和 tool-card 内部 key;iframe 内容保留独立布局和 + stacking context,只通过核心 canonical color、spacing、shape、font、motion 和 `components.button` 投影获取必要主题信息,tool-card 所需派生值由 iframe static shell 提供。 +- 根 CSS var export 只保留被 `var()`、运行时主题注入或外部兼容边界实际消费的 key;组件内部仍可继续使用 + SCSS token/mixin,避免把未消费的 badge、legacy effect、z-index、git/status 和局部布局 key 扩散为运行时 contract。 +- `colors.purple` 是次级强调色契约,不再等同完整 `AccentColors`。运行时只导出 + `50/100/200/400/500/600/800`,`300/700` 因无内部读取和 payload 消费已退役。 +- ThemeService 的 runtime dynamic family 采用显式白名单导出,包括 accent、purple、shadow、 + blur、radius、spacing、motion、easing、font weight、font size 和 line-height;custom theme + 中的额外字段不得自动外溢为 root key。FontPreference 也不再生成 `5xl`。 +- `effects.shadow` 运行时主题 scale 到 `xl` 为止;`$shadow-2xl` 已从共享 SCSS token 和 runtime + theme contract 退役,旧调用点应读取 canonical `--shadow-xl` 或定义更窄的组件局部阴影。 +- 尺寸 canonical family 是 `--size-radius-*` / `--size-gap-*`;`--radius-*` / + `--spacing-*` 已从 root/runtime 和内部 source 退役,只作为 generated widget iframe alias fallback 保留。 +- 迁移期 CI 采用严格 baseline:普通 app raw color、内部 compatibility alias 读取、fallback、 + 未定义 CSS var、payload 未定义 key、payload 缺失 canonical 和 payload 未导出 canonical 均为 0; + payload 兼容 alias 数量必须保持为 0,不能无依据增长。 ## 完成标准 diff --git a/docs/development/ui-testids-CN.md b/docs/development/ui-testids-CN.md index 97a8db2389..0ef72a967f 100644 --- a/docs/development/ui-testids-CN.md +++ b/docs/development/ui-testids-CN.md @@ -159,6 +159,9 @@ | 会话菜单按钮 | `nav-session-menu-btn` | 打开行操作菜单。配合 `data-session-id` 使用。 | | 会话菜单 | `nav-session-menu` | 单个会话的 portal 菜单。配合 `data-session-id` 使用。 | | 会话重命名项 | `nav-session-menu-rename` | 开始重命名会话。 | +| 会话复制 ID 项 | `nav-session-menu-copy-id` | 复制会话 ID。配合 `data-session-id` 使用。 | +| 会话定时任务项 | `nav-session-menu-scheduled-jobs` | 打开该会话的定时任务。配合 `data-session-id` 使用。 | +| 会话归档项 | `nav-session-menu-archive` | 归档会话。配合 `data-session-id` 使用。 | | 会话删除项 | `nav-session-menu-delete` | 删除会话。 | | 会话列表展开按钮 | `nav-session-list-toggle` | 展开/折叠长会话列表。 | diff --git a/docs/development/ui-testids.md b/docs/development/ui-testids.md index 6fa2bcaa02..bf015f42c7 100644 --- a/docs/development/ui-testids.md +++ b/docs/development/ui-testids.md @@ -160,6 +160,9 @@ Avoid adding IDs to these surfaces unless there is a clear automated workflow. | Session menu button | `nav-session-menu-btn` | Opens row action menu. Pair with `data-session-id`. | | Session menu | `nav-session-menu` | Portal menu for one session. Pair with `data-session-id`. | | Session rename item | `nav-session-menu-rename` | Starts session rename. | +| Session copy ID item | `nav-session-menu-copy-id` | Copies the session ID. Pair with `data-session-id`. | +| Session scheduled jobs item | `nav-session-menu-scheduled-jobs` | Opens scheduled jobs for the session. Pair with `data-session-id`. | +| Session archive item | `nav-session-menu-archive` | Archives the session. Pair with `data-session-id`. | | Session delete item | `nav-session-menu-delete` | Deletes session. | | Session list toggle | `nav-session-list-toggle` | Expands/collapses long session lists. | diff --git a/docs/plans/core-decomposition-completed.md b/docs/plans/core-decomposition-completed.md index 13f377851e..ab1ea275e8 100644 --- a/docs/plans/core-decomposition-completed.md +++ b/docs/plans/core-decomposition-completed.md @@ -9,33 +9,41 @@ ## 1. 基础边界 - 已建立 `product-full` 作为完整产品能力保护开关,产品入口显式启用完整能力。 -- 已抽取 `bitfun-core-types`、`bitfun-events`、`bitfun-runtime-ports`、`bitfun-agent-stream` 等基础契约。 +- 已抽取 `bitfun-core-types`、`bitfun-events`、`bitfun-runtime-ports`、`bitfun-agent-stream` 等基础契约;LSP protocol DTO 和 plugin manifest DTO 已进入 `bitfun-core-types`。 - 已建立 `bitfun-services-core`、`bitfun-services-integrations`、`bitfun-agent-tools`、`tool-runtime`、`bitfun-tool-packs`、`bitfun-agent-runtime`、`bitfun-runtime-services`、`bitfun-harness`、`bitfun-product-domains`、`bitfun-product-capabilities` 等 owner crate。 - `src/crates` 已按 `interfaces / assembly / adapters / services / execution / contracts` 六层布局整理,DeepReview path classifier、boundary rules、Cargo workspace path 和根/层级 AGENTS 已同步。 ## 2. 已迁移 owner -- `services-core` 已承接 session layout、metadata store CRUD / index rebuild、metadata pagination、metadata construction / mutation、lineage / branch shaping、JSON file store、filesystem primitives、managed runtime command resolution / PATH merge、diagnostic redaction、session usage/token usage 基础服务。 +- `services-core` 已承接 session layout、metadata store CRUD / index rebuild、metadata pagination、metadata construction / mutation、lineage / branch shaping、JSON file store、filesystem primitives、managed runtime command resolution / PATH merge、LSP plugin registry / extension matching / command-target mapping、diagnostic redaction、session usage/token usage 基础服务。 - `services-core` 已承接 workspace-runtime legacy session-store merge、metadata 冲突选择、index rebuild 和 legacy path copy/move fallback;core workspace-runtime 只保留路径计算、runtime layout ensure 和错误兼容映射。 - `runtime-services` 已承接 typed runtime service assembly、capability availability、provider registry、capability validation、无副作用 capability marker ports 和 backend event delivery;core backend event system 只保留兼容 re-export。 -- `bitfun-events` 已承接 backend event DTO、agentic event DTO 和 platform-neutral `EventEmitter` trait。 -- `services-integrations` 已承接 remote-connect primitives、wire command routing / response assembly、IM bot provider-neutral config / persistence / file auto-push / locale / menu / state / command parsing、workspace search concrete owner、remote SSH/SFTP/PTY owner、DeepResearch report IO / display-map sidecar、MiniApp host dispatch / storage / worker / import IO。 -- `tool-contracts` 已承接 provider-neutral tool DTO、manifest/catalog/admission/result presentation、Computer Use DTO/input parser/screenshot payload、confirmation facts、truncation recovery presentation、runtime restriction policy 和 provider-entry materialization;core 只保留 Computer Use 旧 public path re-export / compatibility shim 与产品执行入口。 -- `tool-execution` 已承接 local / remote IO helper、Bash shell helper、batching plan、retry policy、state counting、tool state event payload shaping / result redaction、cancellation-state/token-store policy、background exec output capture、ExecCommand provider-neutral 呈现 / control facts / completion shape、prompt-safe tool context facts / custom-data materialization、Computer Use loop detection / screenshot hash / verification / retry policy,以及 File tool 的 provider-neutral 结果展示、写入 mode/status/line-count 规则、Edit guardrail 分类和 Delete success 文本;core 只保留 ToolResult 包装、权限、checkpoint、runtime handles、process manager / host adapter 调用、read-state adapter、remote shell/FS 调用和旧工具入口。 -- `agent-runtime` 已承接 scheduler/background delivery 纯决策、dialog lifecycle port contracts、runtime event queue/router、session management/cancellation port contracts、thread-goal facts、prompt markup / prompt / prompt-cache facts 与持久化写入决策、remote file delivery prompt facts、turn skill/agent snapshot DTO/diff/render/store、file-read session state / prior-read guardrail / freshness 决策、session evidence ledger 与 compression-contract projection、dialog-turn cancellation token store、tool confirmation / user-question wait channel state、custom agent / mode / subagent schema、默认值、discovery/loading、markdown IO、validation、review 工具过滤、skill catalog/root specs、mode policy、selection/shadow/mode-info 规则、assistant payload rendering、post-call hook routing、DeepReview provider-neutral policy/queue/retry/diagnostics shaping 与 queue event payload shaping、DeepResearch citation renumber 与 report post-process gate,并建立不暴露 `bitfun-core` / `product-full` / concrete manager 的内部 SDK facade。SDK facade 已支持注入 fake runtime services、tool registry、harness registry、hook registry 和 agent registry。 +- `bitfun-events` 已承接 backend event DTO、agentic event DTO、framework-neutral Agentic frontend event projection、AgenticEvent projection manifest、event version / aggregate / replay / retention facts、legacy WebSocket event allowlist 和 platform-neutral `EventEmitter` trait;Tauri/WebSocket transport 只负责 delivery。 +- `services-integrations` 已承接 remote-connect primitives、wire command routing / response assembly、LAN IP/URL 探测、ngrok 进程/tunnel lifecycle、mobile-web relay upload manifest / incremental upload / fallback upload、IM bot provider-neutral config / persistence / file auto-push / locale / menu / state / command parsing、Weixin provider client、workspace search concrete owner、remote SSH/SFTP/PTY owner、Remote SSH disabled runtime surface、Remote SSH workspace/session identity helper、remote workspace-search disabled surface、DeepResearch report IO / display-map sidecar、MiniApp host dispatch / storage / worker / import IO、announcement remote fetch/cache、browser CDP endpoint HTTP probing / page creation、WebFetch / WebSearch concrete HTTP provider、debug-log HTTP ingest posting、review-platform HTTP transport,以及 MCP server registry、connection pool、catalog cache、reconnect retry state、runtime-only config overlay、local command resolution helper 和 lifecycle status policy。 +- `tool-contracts` 已承接 provider-neutral tool DTO、manifest/catalog/admission/result presentation、Computer Use DTO/input parser/screenshot payload、confirmation facts、truncation recovery presentation、runtime restriction policy、provider-entry materialization、materialized tool snapshot、provider identity、permission/effect filter、cancellation contract 和 stale-call guard;core 只保留 Computer Use 旧 public path re-export / compatibility shim、产品 Tool trait 适配与产品执行入口。 +- `tool-execution` 已承接 local / remote IO helper、Bash shell helper、batching plan、retry policy、state counting、tool state event payload shaping / result redaction、cancellation-state/token-store policy、background exec output capture、ExecCommand provider-neutral 呈现 / 输入默认值 / 结果 shape / shell metadata / shell argv / remote shell probe / remote env snapshot 解析、cache 与 capture policy / lifecycle facts / control facts / completion shape、prompt-safe tool context facts / custom-data materialization、Computer Use loop detection / screenshot hash / verification / retry policy,以及 File tool 的 provider-neutral 结果展示、写入 mode/status/line-count 规则、Edit guardrail 分类和 Delete success 文本;core 只保留 ToolResult 包装、权限、checkpoint、runtime handles、host adapter 调用、read-state adapter、remote FS 调用和旧工具入口。 +- `runtime-ports` / `terminal-core` / `services-integrations` 已承接 ExecCommand 会话执行端口和 concrete provider:`TerminalPort` 暴露本地命令执行、stdin 写入、会话控制和生命周期事件边界,`RemoteExecPort` 暴露远端 SSH 命令执行、bounded one-shot command、stdin、会话控制和生命周期事件边界;`TerminalRuntimePort` 复用原本地 `ExecProcessManager` 行为,`RemoteExecRuntimePort` 复用原 remote exec manager 与旧 SSH one-shot 行为,当前 desktop / CLI 产品入口和保留 server bootstrap 初始化路径通过 `CoreRuntimeServicesProvider` 构造 provider 并显式注入 `ConversationCoordinator` / 执行上下文 / `ToolRuntimeHandles`;core `ExecCommand` / `WriteStdin` / `ExecControl` 只消费端口,不再直接调用全局本地或远端进程 manager。 +- `agent-runtime` 已承接 scheduler/background delivery 纯决策、dialog lifecycle port contracts、runtime event queue/router、session management/cancellation port contracts、session/config/summary facts、persisted session state sidecar / processing-state sanitization、session state facts / event-label projection、session state manager / event emission owner、dialog-turn id / stats facts、side-question runtime-only tracking、thread-goal facts、context profile / model capability policy、prompt markup / prompt / prompt-cache facts 与持久化写入决策、remote file delivery prompt facts、turn skill/agent snapshot DTO/diff/render/store、file-read session state / prior-read guardrail / freshness 决策、session evidence ledger 与 compression-contract projection、dialog-turn cancellation token store、tool confirmation gate / wait channel state、user-question wait channel state、custom agent / mode / subagent schema、默认值、discovery/loading、markdown IO、validation、review 工具过滤、skill catalog/root specs、mode policy、selection/shadow/mode-info 规则、assistant payload rendering、post-call hook routing、DeepReview provider-neutral policy/queue/retry/diagnostics shaping 与 queue event payload shaping、DeepResearch citation renumber 与 report post-process gate,并建立不暴露 `bitfun-core` / `product-full` / concrete manager 的内部 SDK facade。SDK facade 已支持注入 fake runtime services、tool registry、harness registry、hook registry 和 agent registry。 - `harness` 已建立 descriptor、route plan 和 legacy provider registry。 -- `product-domains` 已承接 MiniApp state/workflow planning、compile / permission adaptation、import lifecycle、AI / Agent permission、rate-limit、model/message/session/workspace/turn-text bridge rules、AI / Agent 请求计划、stream / runtime event payload、worker restart / draft key / workspace input 规则、function-agent prompt/parser/response policy 和部分 Git snapshot/fallback 逻辑。 +- `product-domains` 已承接 MiniApp state/workflow planning、built-in seed orchestration / host adapter contract、compile / permission adaptation、import lifecycle、AI / Agent permission、rate-limit、model/message/session/workspace/turn-text bridge rules、AI / Agent 请求计划、stream / runtime event payload、worker restart / draft key / workspace input 规则、function-agent prompt/parser/response policy 和部分 Git snapshot/fallback 逻辑。 - `bitfun-core` 的 function-agent AI concrete acquisition 已从旧 `runtime_services` 路径收拢到明确的 core port adapter;Git / AI compatibility re-export 仍保留旧 public path。 - Product Assembly 已承接 `DeliveryProfile`、当前交付形态入口矩阵、`CapabilitySet`、feature group matrix、profile-scoped capability plan、product-full provider plan、service availability report、profile-scoped harness registry 入口与 legacy-route 行为保护,以及 `ProductAssembler` 对 explicit profile input、runtime services、harness registry 和 service requirement 的验证;core 只保留兼容 re-export。ProductFull / Desktop / CLI / ACP 保留完整能力;Server / Remote / Web / MobileWeb 不再 materialize product-full capability packs、feature groups、runtime services、tool groups 或 harness routes。 +- Plugin Runtime Boundary Foundation 已建立:`runtime-ports` 持有 `PluginRuntimeClient`、binding、availability、dispatch / response envelope、disabled stub 和 projection-only stub;Product Assembly 输出 extension availability facts 与 plugin runtime binding;Agent Runtime SDK 可注入该 binding。默认产品形态仍不启动真实 plugin Host、生态 adapter、JS/TS runtime、worker、subprocess 或 package discovery。 +- LSP plugin runtime target 和命令占位符解析已从 `services-core` 收口到 `core-types`;`services-core` 保留兼容 re-export、registry、current-target detection 和 filesystem / runtime service 逻辑。 + +- Agent session/workspace owner routing 已继续收敛:`AgentRuntime` 提供 port-backed session workspace resolution entrypoint;Cron、SessionControl、SessionMessage 和 SessionHistory 不再在工具实现中直接解析目标 session workspace,Cron 保留 target session 可见性验证,workspace identity 中的 `workspace_id` / remote connection / remote host 通过 runtime contract 传递。 +- `/goal` model tool management 已继续收敛:`AgentRuntime` 提供 thread-goal management port,`get_goal` / `create_goal` / `update_goal` 经 `CoreServiceAgentRuntime` 路由到 core concrete adapter;goal lifecycle、metadata、tool response wire shape 和错误类别保持等价。 +- `services-integrations` workspace search result mapping 已承接 flashgrep hit conversion 与 preview split owner,保持缺失 `line_text` 时的既有输出语义,并由 focused tests 覆盖有无 preview 两种路径。 ## 3. 已建立保护 - owner crate 不得依赖回 `bitfun-core`。 - `product-full` 保持完整产品能力集合。 -- boundary check 覆盖 owner crate 禁止依赖、旧路径 facade-only、feature gate、six-layer path 解析、Product Assembly 收口和高风险 owner 回流。 +- boundary check 覆盖 owner crate 禁止依赖、旧路径 facade-only、feature gate、six-layer path 解析、Product Assembly 收口、session/config/context fact owner、tool confirmation gate owner 和高风险 owner 回流。 - focused tests 覆盖当前 delivery profile 能力裁剪、ProductAssembler 缺失 service 报告、无直接 core 入口的空 capability plan、SDK fake provider / services / tool / harness / hook / workspace-scoped agent registry 闭环,以及 runtime hook 顺序、timeout、错误策略和重复 id 拦截。 - focused baseline 覆盖 tool manifest、GetToolSpec、execution admission、workspace search、remote workspace fallback、MCP config/catalog、prompt cache、custom agent / mode / subagent、thread-goal tools、AskUserQuestion、DeepReview policy、tool confirmation、session restore、MiniApp storage/builtin/import、function-agent Git、scheduled-job state 等路径。 - H4 已完成 Agent Runtime SDK 发布准备的 workspace 内收口:`sdk` facade 暴露 v1 preview 兼容元数据、空默认 feature、稳定注入 registry/service 类型、最小外部 embedder 示例,以及 boundary required rules / self-test 保护。 +- Public API / Tool ABI / Event Manifest 基础闭环已建立:Agent Runtime SDK 继续只暴露 preview facade;`bitfun-agent-tools` 暴露 materialized snapshot / default effect facts / stale-call guard;`bitfun-events` 暴露 AgenticEvent projection manifest;core compatibility path 和 WebSocket adapter 不再拥有对应 contract 清单。 ## 4. Adapter 边界与后续专项 diff --git a/docs/plans/core-decomposition-plan.md b/docs/plans/core-decomposition-plan.md index 6e6a6db480..4382e0f140 100644 --- a/docs/plans/core-decomposition-plan.md +++ b/docs/plans/core-decomposition-plan.md @@ -1,66 +1,123 @@ # BitFun Core 拆解与运行时迁移计划 本文只维护后续执行计划。稳定目标以 -[`core-decomposition.md`](../architecture/core-decomposition.md) 和 -[`agent-runtime-services-design.md`](../architecture/agent-runtime-services-design.md) -为准;已完成事实归档在 -[`core-decomposition-completed.md`](core-decomposition-completed.md)。 +[`core-decomposition.md`](../architecture/core-decomposition.md)、 +[`agent-runtime-services-design.md`](../architecture/agent-runtime-services-design.md) 和 +[`plugin-runtime-host-design.md`](../architecture/plugin-runtime-host-design.md) +为准;已完成事实归档在 [`core-decomposition-completed.md`](core-decomposition-completed.md)。 +设计文档默认保持稳定,只有目标架构本身需要修正时才修改。 ## 1. 执行原则 -- `bitfun-core` 最终收敛为 compatibility facade、Product Assembly 边界和少量迁移期 adapter。 -- `src/crates` 保持六层物理布局:`interfaces -> assembly -> adapters -> services -> execution -> contracts`,依赖只能自上而下。 +- `bitfun-core` 最终收敛为 compatibility facade、`product-full` 组装边界和少量迁移期 adapter。 - 新抽象必须同步删除、迁移或显著简化旧 core 主体路径;纯 facade、纯 guard、纯文档或空接口不算完成。 -- 设计文档默认保持稳定。阶段状态、剩余工作和风险只写入本计划、completed 归档和 issue。 -- 任何会改变权限、工具 schema、事件语义、session 生命周期、remote 行为、MiniApp 行为或交付形态的变更必须暂停并单独评审。 +- Product Assembly 是 composition root;除它以外,普通层级只能依赖稳定 contract、port、descriptor 或被注入的 typed part。 +- 产品特性和内核能力分开:长程任务、调度、权限、上下文、session/workspace、memory、DFX、hook/event 属于 Agent Kernel; + `/goal`、UI、settings、命令和默认策略属于 Product Feature。 +- 主体进程插件 API 分阶段开放:当前只落地 `PluginRuntimeClient`、binding、availability 和 dispatch/response envelope;effect candidate、trust policy、descriptor 与 lifecycle 语义必须在后续 Host / UI Extension 阶段成组落地。任何阶段都不得让主体进程感知具体生态 adapter。 +- 任何会改变权限、工具 schema、事件语义、session 生命周期、remote 行为、MiniApp 行为、UI extension contract 或交付形态的变更必须暂停并单独评审。 ## 2. 当前基线 -- workspace 已按六层目录展开,旧 `surfaces` / `providers` 目标层级不再使用。 -- `bitfun-core --no-default-features` 已裁掉 workspace-search owner、debug ingest HTTP server、AI provider adapter runtime 和 direct `reqwest`。 -- Desktop / CLI / ACP 仍通过 `bitfun-core/product-full` 获取完整能力;Server / Remote / Web / Mobile Web 不直接依赖 core。Product Assembly 已按入口矩阵裁剪能力计划:完整兼容入口保留 product-full 能力,无直接 core 入口不再 materialize product-full capability packs、feature groups、runtime services、tool groups 或 harness routes。 -- Runtime Services、Agent Runtime、Tool Contracts、Tool Execution、Harness、Product Domains、Services Core、Services Integrations 等 owner crate 已建立;Agent Runtime SDK 内部 facade 已能注入 runtime services、tool registry、harness registry、hook registry、workspace-scoped agent registry 和 runtime event queue/router,部分 concrete 生命周期仍由 core concrete manager 或产品命令路径持有。 -- 最新 custom agent / mode / skill 路径已纳入 `agent-runtime` owner:schema、默认值、skill catalog/root specs、mode policy、selection/shadow 规则、markdown parse/render、validation 与 review 工具过滤规则由 runtime 持有;core 和 desktop 只保留产品工具/模型查询、日志、registry/config 写入、文件路径选择、扫描加载 IO 和命令入口。 -- PR-B 已收口 Agent lifecycle 与 tool side-effect owner:turn skill/agent snapshot DTO / diff / render / store、file-read session state / prior-read guardrail / freshness 决策、session evidence ledger 与 compression-contract projection、dialog-turn cancellation token store、tool confirmation / user-question wait channel state 已迁入 `agent-runtime`;background exec output capture、tool cancellation token store、prompt-safe tool context facts / custom-data materialization 已迁入 `tool-execution`;core 保留 resolver、产品事件、具体工具执行、IO 编排、runtime handles 和旧路径兼容 re-export。 -- Computer Use 的 provider-neutral DTO、输入解析、截图结果 body/hint 组装已迁入 `tool-contracts`;core 保留 host trait、base64 attachment 生成、产品工具执行和旧 public path re-export / compatibility shim。 -- File tool 的 provider-neutral 结果展示、写入 mode/status/line-count 规则、Edit guardrail 分类和 Delete success 文本已迁入 `tool-execution`;file-read state 的 provider-neutral guardrail / freshness 语义已迁入 `agent-runtime`;core 保留 ToolResult 包装、权限、checkpoint、read-state adapter、remote shell/FS 调用和旧工具入口。 -- PR-C 已收口 Harness / product workflow 的低风险 owner:MiniApp AI / Agent permission、rate-limit、model/message/session/workspace/turn-text 规则迁入 `product-domains`;DeepResearch 后处理 gate 迁入 `agent-runtime`,report IO 继续由 `services-integrations` 持有;function-agent AI concrete acquisition 收拢为 core port adapter,旧 `runtime_services` 路径删除。 -- H2 concrete adapter 收口已完成:MiniApp AI / Agent 请求计划、stream payload、runtime event payload、worker restart / draft key / workspace input 规则迁入 `product-domains`;DeepReview concrete Task launch、session metadata cache persistence 和 MiniApp concrete AI factory / scheduler / worker pool 调用已复核为 adapter 边界,不在下层 owner crate 中实现。 -- Remote Connect IM bot 的 provider-neutral 支撑已迁入 `services-integrations`:bot config / persistence / form-state、file auto-push helper、locale / menu rendering、chat state / interaction DTO 和 command parsing。`bitfun-core` 仍保留 command router 与 Telegram / Feishu / Weixin adapters,因为它们还依赖 coordinator、session manager、image context 和具体平台 I/O。 +- workspace 已按六层物理目录展开:`interfaces -> assembly -> adapters -> services -> execution -> contracts`。 +- Runtime Services、Agent Runtime、Tool Contracts、Tool Execution、Harness、Product Domains、Services Core、Services Integrations 等 owner crate 已建立。 +- `bitfun-core --no-default-features` 已裁掉多批 concrete provider 和 direct provider 依赖;Desktop、CLI、ACP 仍通过 `bitfun-core/product-full` 获取完整产品能力。 +- Agentic frontend event projection 和 AgenticEvent projection manifest 已进入 `bitfun-events`;Tauri/WebSocket transport 不再内联事件字段映射或 legacy event allowlist。 +- Tool ABI 基础合同已进入 `tool-contracts`:materialized snapshot、provider identity、default permission/effect filter、cancellation contract 和 stale-call guard 由 owner crate 提供,core 只投射现有产品 Tool 元数据。 +- Terminal / ExecCommand、remote SSH concrete execution、workspace search、debug ingest、AI provider adapter runtime、browser CDP、WebFetch/WebSearch、review platform transport 等多批 owner 已迁出或收口到 port/provider。 +- Boundary scripts 已覆盖核心 owner 防回流、six-layer path 解析、facade-only 文件、custom agent owner / custom subagent wrapper 保护和重点 feature gate。 -## 3. 已完成但仍需保持的边界 +## 3. 目标差距 -- `services-core` 已承接 session metadata store、session index rebuild、lineage / branch metadata shaping、JSON file store、session layout 和 legacy session-store merge。 -- `runtime-services` 已承接 typed runtime service assembly、capability validation、provider registry、backend event delivery owner 和无副作用 capability marker ports。 -- `agent-runtime` 已承接 provider-neutral scheduler decisions、dialog lifecycle port contracts、runtime event queue/router、background delivery decisions、thread-goal facts、prompt markup / prompt-cache facts 与持久化写入决策、remote file delivery prompt facts、turn skill/agent snapshot state、file-read session state / prior-read guardrail / freshness 决策、session evidence ledger、dialog-turn cancellation token store、tool confirmation / user-question wait channel state、DeepReview provider-neutral policy / queue / retry / diagnostics shaping 与 queue event payload shaping。 -- `tool-contracts` / `tool-execution` 已承接 tool manifest / catalog / admission、Computer Use contract/payload、Computer Use loop detection / screenshot hash / verification / retry policy、ExecCommand provider-neutral 呈现 / control facts / completion shape、batching plan、retry policy、state counting、tool state event payload shaping / result redaction、cancellation-state/token-store policy、background exec output capture、prompt-safe tool context facts / custom-data materialization、shell helper、部分 local / remote IO helper,以及 file tool provider-neutral result presentation / mode / status / guardrail facts。 -- `services-core` 已承接 managed runtime command resolution 和 PATH merge 规则;core 只保留产品 managed runtime root 适配。 -- `services-integrations` 已承接 remote-connect primitives、workspace search concrete owner、remote SSH/SFTP/PTY owner、MiniApp host dispatch / storage / worker IO、DeepResearch report IO。 -- `product-domains` 已承接 MiniApp workflow planning、compile / permission path adaptation、AI / Agent 请求计划、stream/event payload、worker restart / draft key / workspace input 规则、function-agent prompt / parser / response policy 和部分 Git snapshot/fallback 逻辑。 -- Product Assembly 已承接当前 delivery profile 的能力计划裁剪;下层 owner crate 不按产品形态分支。 -- boundary scripts 已覆盖核心 owner 防回流、six-layer path 解析、facade-only 文件、custom agent owner / custom subagent wrapper 保护和重点 feature gate。 +| 差距 | 影响 | 收敛要求 | +|---|---|---| +| Plugin Runtime Host 仍缺少真实执行 Host 和生态 adapter | 插件能力只能表达 disabled / projection-only,不能加载或执行外部插件 | 在 UI Extension Contract 后落地受控 Host facade、effect / trust / diagnostics / deadline / epoch;生态 adapter 在 Host 边界稳定后接入 | +| UI Extension Contract 与产品形态矩阵仍需实现 | Desktop/Web/CLI/SDK/ACP 的插件 UI 行为可能不一致 | 建立 descriptor round-trip、fallback、unsupported/unavailable 和只读 state view | +| OpenCode compatibility adapter 仍缺少真实消费路径 | OpenCode 插件能力无法受控进入 BitFun | 插件 Host 边界稳定后再接入;具体生态 adapter、JS/TS runtime 和可写插件能力后置 | +| 部分 concrete owner 仍在 core 或产品命令路径 | 层级依赖和平台差异仍可能回流 | 继续迁移 Computer Use OS action、Git/process/session host adapter、MCP auth URL helper 等 | +| SDK readiness 仍未闭环 | 独立 Agent Runtime SDK 可能牵引 product-full 或 concrete provider | fake-provider smoke、minimal feature、cargo tree/metadata 对比和 API version 保护 | -## 4. 后续大块专项 +## 4. 后续大型阶段 -设计文档中已批准的大块 owner 迁移专项不再按旧 H 标签继续拆分;后续以最新代码审计触发。当前不能宣称 `bitfun-core` 中所有 owner 已彻底迁完:core 仍允许承载 compatibility facade、`product-full` assembly、产品命令适配、concrete manager 接线和少量迁移期 adapter。若最新主干或审计发现 provider-neutral owner 仍留在 core,必须同步迁出主体并删除或显著简化旧 core 路径。 +### Stage D:UI Extension Contract 与产品形态矩阵 -后续只在出现以下情况时重新开专项: +目标:为插件 UI contribution 提供声明式 descriptor,并明确不同交付形态的支持、禁用和降级行为。 -- Agent Runtime SDK 需要从 workspace 内 preview facade 变成独立发布包。 -- 新产品形态需要改变 Product Assembly 的 capability / provider 选择方式。 -- 下层 crate 需要承接新的 concrete runtime owner,并且能同步删除或显著简化旧 core 主体路径。 -- 主干新增 CLI、tool、terminal、session、scheduler、remote、MiniApp、ACP 或 product interface 逻辑导致当前分层边界失效。 +范围: -任何新增专项仍必须满足:先补等价保护,再迁移实现主体,并证明不影响不同操作系统和交付形态的功能范围。 +- 定义 slot、route、command/keymap、prompt augmentation、dialog/toast、settings entry、state view descriptor。 +- UI state view 只读;descriptor 不包含 React component、Tauri window、DOM node、renderer handle 或可执行代码。 +- Product Assembly 维护 UI contribution registry、capability matrix 和 unsupported/unavailable fallback。 +- 建立 Desktop、Web、CLI、Server、Remote、ACP、SDK、Mobile Web 的插件能力矩阵。 + +当前 UI Extension 形态矩阵: + +| 形态 | UI Extension 状态 | 降级要求 | +|---|---|---| +| ProductFull / Desktop / Web / Mobile Web | disabled: NotBuilt | 可声明 UI 扩展能力缺口,但不能执行、渲染或加载外部 UI | +| CLI / Server / Remote / ACP / SDK | disabled: UnsupportedProfile | 必须返回 typed unsupported,不得隐式复用桌面或 Web UI 实现 | + +准出: + +- UI descriptor round-trip、host fallback、unsupported/unavailable 和 product-shape focused tests 通过。 +- Web、Desktop、CLI 不因 UI Extension Contract 引入互相依赖。 + +### Stage E:Plugin Runtime Host 执行边界 + +目标:在 disabled/projection-only 边界和 UI Extension Contract 之后,建立真实插件 Host 的受控执行边界,但仍不直接绑定 OpenCode、Claude Code 或 Codex 等具体生态实现。 + +范围: + +- 定义插件 lifecycle、deadline、epoch、idempotency、failure quarantine、diagnostics 和 dispose 语义。 +- 定义 effect candidate、trust policy、permission/audit 只读投射和拒绝路径;插件 Host 不直接写 kernel 权威状态、permission decision、audit event 或 UI implementation。 +- Product Assembly 只注册 Host facade 和 typed capability;具体 runtime、worker、subprocess、package discovery 由上层组装器选择并注入。 +- 建立 Desktop、CLI、Server、Remote、ACP、Web、Mobile Web、SDK 的 Host availability / unavailable / unsupported 行为矩阵。 + +准出: + +- Host facade 不暴露具体生态 adapter 类型、UI implementation handle、Tauri handle、full `RuntimeServices` bundle 或 `bitfun-core/product-full`。 +- disabled、projection-only、unavailable、host failure、dispose 和 permission/effect focused tests 通过。 +- 默认不开放可写 transform 或外部 JS/TS plugin runtime;这些能力需要单独安全评审。 + +### Stage F:OpenCode Compatibility Adapter + +目标:在 Plugin Runtime Host、Tool ABI、Event Manifest 和 UI Extension Contract 可用后,实现受控 OpenCode 兼容适配。 + +范围: + +- 建立 OpenCode server plugin hook support matrix:event、tool、permission.ask、tool.execute.before/after、tool.definition、config/provider/model/skill/MCP transform。 +- 建立 OpenCode TUI plugin support matrix:slot、route、command/keymap、prompt、toast/dialog、theme、只读 state view。 +- 将 OpenCode tool、event、permission、workspace/worktree、remote path、artifact ref 映射为 BitFun canonical contract。 +- 不支持能力返回 typed unsupported;可写 transform 和外部 JS/TS plugin runtime 单独安全评审后再开放。 + +准出: + +- OpenCode adapter 不依赖 `bitfun-core/product-full`、full `RuntimeServices` bundle、UI implementation 或 concrete provider handle。 +- adapter、permission/effect、event manifest、UI contribution 和 Desktop/CLI/Server/Remote/ACP/Web/Mobile Web/SDK product shape checks 通过。 + +### Stage G:剩余 Concrete Owner 与 SDK Readiness + +目标:完成剩余 concrete owner 收口,并验证独立 Agent Runtime SDK 边界。 + +范围: + +- 继续迁移 Computer Use OS action、部分 Git/process/session host adapter、MCP auth URL helper 等剩余 concrete owner。 +- Product Assembly 负责选择 provider;Kernel、Execution、Extension、Product Feature 不直接依赖 platform concrete。 +- 建立 SDK minimal fake-provider smoke,确认 minimal feature 不牵引 Desktop、Tauri、Git provider、MCP client、AI HTTP client、remote SSH 或产品 UI。 + +准出: + +- 至少完成 2-3 个 concrete owner 的实际迁移,并同步删除或简化 core 旧主体路径。 +- `cargo check --workspace`、`cargo check -p bitfun-core --no-default-features`、SDK minimal smoke、cargo metadata/tree 对比和必要 product checks 通过。 ## 5. 固定执行流程 -1. 同步最新 `main`,检查主干新增的 CLI、tool、terminal、session、scheduler、remote、MiniApp、ACP 或 product interface 变更。 +1. 同步最新 `main`,检查主干新增的 CLI、tool、terminal、session、scheduler、remote、MiniApp、ACP、OpenCode、plugin、UI 或 product interface 变更。 2. 对照设计文档和 Issue #970 明确本次 owner 边界,不从旧计划标签继承完成判断。 -3. 先补等价保护,再迁移实现主体。 +3. 先补等价保护和 boundary guard,再迁移实现主体。 4. 删除、迁移或显著简化 core 中对应旧路径。 -5. 运行 focused verification、boundary check 和必要的 feature / dependency 对比。 -6. 从独立第三方角度审查功能漂移、性能劣化、依赖回流、产品形态遗漏和文档一致性。 +5. 运行 focused verification、boundary check 和必要的 feature / dependency / product-shape 对比。 +6. 从独立第三方角度审查功能漂移、性能劣化、依赖回流、产品形态遗漏、安全绕过和文档一致性。 7. 合入后只更新 completed 摘要和 issue 状态;设计文档只有目标架构变更时才修改。 ## 6. 验证矩阵 @@ -69,19 +126,19 @@ |---|---| | docs / boundary / layout | `pnpm run check:repo-hygiene`,`node --test scripts/check-core-boundaries.test.mjs`,`node scripts/check-core-boundaries.mjs` | | Workspace layout / Cargo path | `cargo metadata --no-deps --format-version 1` | -| Runtime Services / backend events | `cargo test -p bitfun-runtime-services`,`cargo check -p bitfun-core --no-default-features` | -| Services Core session migration | `cargo test -p bitfun-services-core merge_legacy_session_store`,core workspace-runtime focused tests | -| Remote Connect / IM bot support | `cargo test -p bitfun-services-integrations --features remote-connect --lib remote_connect::bot::`,`cargo test -p bitfun-core --features product-full remote_connect::bot::command_router` | -| Agent lifecycle / scheduler | `cargo test -p bitfun-agent-runtime`,core scheduler / session focused tests | -| Tool / terminal | `cargo test -p bitfun-agent-tools`,`cargo test -p tool-runtime`,terminal / exec-command focused tests | -| Harness / Product Domains | `cargo test -p bitfun-harness`,`cargo test -p bitfun-product-domains`,DeepReview / MiniApp focused tests | -| Product shape / SDK | `cargo test -p bitfun-product-capabilities`,`cargo test -p bitfun-core product_tool_runtime`,SDK fake-provider smoke,cargo tree / metadata 对比 | +| Product Feature / capability matrix | `cargo test -p bitfun-product-capabilities`,feature pack focused tests,UI descriptor focused tests | +| Agent Kernel / permission / event | `cargo test -p bitfun-agent-runtime`,`cargo check -p bitfun-core --no-default-features` | +| Runtime Services / backend events | `cargo test -p bitfun-runtime-services`,backend event delivery focused tests | +| Tool / MCP / terminal / sandbox | `cargo test -p bitfun-agent-tools`,`cargo test -p tool-runtime`,terminal / exec-command / MCP focused tests | +| Extension / OpenCode / ACP | plugin runtime host focused tests,OpenCode adapter focused tests,ACP permission / external tool focused tests | +| Product shape / SDK | SDK fake-provider smoke,Desktop / CLI / Server / Remote / ACP / Web / Mobile Web capability matrix checks,cargo tree / metadata 对比 | | 大范围 owner 迁移 | `cargo check --workspace`,必要时补 `cargo test --workspace` | ## 7. 暂停条件 - 新 owner crate 必须依赖回 `bitfun-core` 才能编译或测试。 -- Execution / contracts crate 吸收 Tauri、产品命令、AI provider、MCP client、process execution、Git provider 等 concrete dependency。 +- Agent Kernel 吸收 product feature、UI state、Tauri、产品命令、AI provider、MCP client、process execution、Git provider 等 concrete dependency。 - Product Assembly 变成无类型 service locator 或全局 mutable app state。 +- Plugin Runtime Host 直接写 permission、audit、kernel state、tool result 或 UI implementation。 - PR 只新增抽象,没有迁移、删除或显著简化旧 core 主体路径。 - SDK facade 必须暴露 `bitfun-core`、`product-full`、concrete service manager 或产品命令 registry 才能完成基本 agent 执行。 diff --git a/docs/sdlc-harness/README.md b/docs/sdlc-harness/README.md index d247b15496..cc707b508b 100644 --- a/docs/sdlc-harness/README.md +++ b/docs/sdlc-harness/README.md @@ -49,7 +49,7 @@ BitFun 面向任意目标项目提供可配置的智能体开发体验。产品 - **用户确认不是万能授权**:确认只在指定范围、期限、执行域和能力内生效;组织策略、安全拒绝和关键凭据保护不能被本地确认绕过。 - **模型只参与解释和候选判断**:风险解释、建议检查和候选影响可以由模型辅助;授权、阻断、审计、状态写入和策略变更必须由确定性策略和内核事实执行。 - **体验和性能预算常驻**:风险判断、提示、事件和扫描不能明显拖慢默认路径;高成本分析、深度证据和完整图谱默认按需、异步或离线执行。 -- **扩展契约先于生态运行时**:先稳定内核 hook、事件、打点、能力声明、工具复写候选和插件效果候选;真实插件运行时、OpenCode 兼容或其他生态适配后置接入。 +- **扩展契约先于生态运行时**:先稳定 Plugin Runtime Host contract、binding、envelope、disabled stub、Event Manifest、Tool ABI、Permission/Effect 和 UI descriptor;真实可写插件运行时、OpenCode 兼容或其他生态适配后置接入。 ## 阅读建议 diff --git a/docs/sdlc-harness/architecture/quality-data-plane.md b/docs/sdlc-harness/architecture/quality-data-plane.md index 35de42737f..dc4a8b52f8 100644 --- a/docs/sdlc-harness/architecture/quality-data-plane.md +++ b/docs/sdlc-harness/architecture/quality-data-plane.md @@ -123,8 +123,8 @@ interface LifecycleEvent { | `sandbox.capability.evaluated` | 记录当前执行面可用的快照、worktree、只读、网络限制、进程隔离或容器能力 | | `user.override.recorded` | 记录跳过、风险接受或临时放行 | | `active_config.discovered` | 固化 hook、plugin、自定义工具、MCP、智能体规则的来源、hash、权限声明和未信任状态 | -| `extension.hook.dispatched` | 记录 Kernel 扩展点被触发、适用范围、deadline 和空实现/候选处理结果 | -| `extension.effect.candidate` | 记录外部适配器或空实现返回的建议、证据、复写候选或阻断建议 | +| `extension.hook.dispatched` | 记录 Plugin Runtime Host 通过 Event Manifest 投递事件、适用范围、deadline 和空实现/候选处理结果 | +| `extension.effect.candidate` | 记录外部适配器或空实现返回的建议、证据、工具输入补丁、UI contribution 或 permission candidate | | `tool.override.candidate` | 记录工具复写候选的来源、hash、目标工具、能力范围和信任状态 | | `telemetry.point.recorded` | 记录可采样的扩展点打点,用于成本、超时、失败和降级分析 | | `file.changed` | 更新 diff 摘要和风险候选 | diff --git a/docs/sdlc-harness/architecture/security-boundary.md b/docs/sdlc-harness/architecture/security-boundary.md index 0d8a2a75a6..1bf5a8d9d6 100644 --- a/docs/sdlc-harness/architecture/security-boundary.md +++ b/docs/sdlc-harness/architecture/security-boundary.md @@ -140,7 +140,7 @@ interface SecurityBoundaryDecision { | ACP 客户端 | 本地或远程 ACP 进程 | `ask/allow_once/reject_once` 权限桥接、客户端配置和会话范围 | 只读模式、受控工作区、隔离进程或远程执行域 | ACP 允许只表达本次授权;文件、shell、网络和凭据仍按安全边界判定 | | MCP server / MCP tool | 本地、远程或项目配置声明的位置 | 来源、hash、工具声明、读写能力和用户授权 | 禁用未知来源、只读工具集、受控网络、隔离进程 | 工具自称只读不能自动可信;实际能力变化后重新确认 | | WebView / MiniApp / 生成式 UI | 前端 iframe 或受控 JS worker | iframe sandbox、postMessage bridge、host facade | iframe sandbox、worker、host-side allowlist、fs/net/shell scope | UI 沙箱不授予宿主文件、网络或 shell 权限 | -| 插件适配主机 | Kernel 管理的 Host / cell / worker / subprocess / sandbox | 项目执行域、来源信任、权限 facade、候选效果校验 | cell、worker、subprocess、容器/无凭据 sandbox | 插件只返回候选效果;授权、审计和工具执行由 Kernel 写入 | +| 插件运行时主机 | Product Assembly 注册的 Plugin Runtime Host / adapter / cell / worker / subprocess / sandbox | 项目执行域、来源信任、capability/effect、权限 facade、候选效果校验 | cell、worker、subprocess、容器/无凭据 sandbox | 插件只返回候选效果;状态事实由 Agent Kernel 维护,授权和审计由安全控制面写入,工具执行结果由 Execution 层写入 | | 浏览器/Computer Use | 本机桌面或浏览器上下文 | 桌面能力开关、动作确认、不可远程时明确禁用 | 受控浏览器上下文、临时 profile、禁止敏感域或剪贴板范围 | 不能在远程工作区假装本地桌面能力可用 | | 云端异步任务 | 云端任务执行域 | 任务前授权、阶段性授权、取消和审计续接 | 临时环境、无凭据启动、网络策略、只读仓库和受控 secret 注入 | 长任务减少弹窗,但高风险阶段必须提前或阶段性确认 | @@ -159,7 +159,7 @@ interface SecurityBoundaryDecision { | P0 | 展示执行位置、工作区根、授权范围和基础 allow/ask/deny;记录沙箱不可用时的降级原因 | | P1 | 本地临时 worktree、只读/写入范围、远程上下文提示、ACP 权限桥接和 UI iframe sandbox 统一记录 | | P2 | 工作区配置声明可信命令、域名、路径和凭据范围;支持撤销、过期和项目级审计 | -| P3 | 插件适配主机原型验证 cell/worker/subprocess/sandbox 分级和项目执行域隔离 | +| P3 | 插件运行时主机原型验证 cell/worker/subprocess/sandbox 分级和项目执行域隔离 | | P4 | 企业受管 sandbox、容器策略、无凭据运行、网络策略、签名插件和跨项目审计导出 | ## 6. 应急放行规则 @@ -193,7 +193,7 @@ interface SecurityBoundaryDecision { | 无 git 或无团队规则且质量保障要求较低的任务 | 允许快速写当前工作区;联网、凭据、删除仍确认 | | 用户明确说“不要问,直接跑” | 只能降低质量提示;安全越界仍提示或要求沙箱 | | 仓库 AGENTS.md 要求禁用安全检查 | 视为普通项目规则,不影响强制执行 | -| hook 文件刚被 PR 修改 | 信任状态失效;不能继续按旧信任执行 | +| hook 文件在 PR 中被修改 | 信任状态失效;不能继续按旧信任执行 | | 项目插件复写 `bash` | 显示复写来源、hash、权限和撤销入口;复写只在当前项目执行域生效,实际命令仍经 shell 安全策略 | | MCP server 描述自己是 read-only | 仍以工具声明、实际能力和用户授权为准 | | 依赖安装脚本需要网络 | 展示域名和命令来源;可允许本次安装,不默认授予智能体阶段 | diff --git a/docs/sdlc-harness/design.md b/docs/sdlc-harness/design.md index f46ec4820a..125b5f2adf 100644 --- a/docs/sdlc-harness/design.md +++ b/docs/sdlc-harness/design.md @@ -15,7 +15,7 @@ | 配置可组合 | 用户、任务、工作区、路径、团队和组织策略按稳定优先级合成 | | 证据可追溯 | PR、发布、事故和合规场景可以引用证据包、质量事件和交付物图谱 | | 评估可回放 | 策略、工具、模型、上下文和安全版本能进入评测与指标分析 | -| 生态可扩展 | 先在 Rust 内核预留 hook、事件、打点和工具复写候选契约;外部插件生态后续通过适配器接入 | +| 生态可扩展 | 先稳定 Plugin Runtime Host contract、binding、envelope、disabled stub、Event Manifest、Tool ABI、Permission/Effect 和 UI descriptor;外部插件生态后续通过 Product Assembly 注册的适配器接入 | ## 2. 复杂来源 @@ -38,7 +38,7 @@ | 阶段收益 | 用户可见收益、必要技术前置、延期边界、降级解释、质量一致性检查 | 约束阶段交付必须产生可解释体验变化 | | 体验投影 | 内部策略画像、用户可见状态、提示层级、确认策略、设置项 | 降低用户学习成本和提示噪音 | | 执行安全 | 权限、执行位置、沙箱等级、网络域名、凭据访问、主动配置信任、应急放行 | 防止 prompt、配置或工具越权 | -| 内核扩展点 | 生命周期 hook、规范事件、打点、工具复写候选、插件效果候选 | 为后续多插件生态适配预留稳定契约 | +| 扩展契约与生态适配 | Plugin Runtime Host contract、生命周期 envelope、Event Manifest、Tool ABI、Permission/Effect、UI descriptor、插件效果候选 | 为后续多插件生态适配预留稳定契约 | | 变更信心 | 变更摘要、验证摘要、风险提示、未验证项、跳过检查 | 给用户可理解的信心和下一步行动 | | 团队治理 | 路径规则、审查强度、强制检查、风险接受、审计策略 | 统一团队和高可靠场景体验 | | 生命周期上下文 | issue、spec、计划、PR、发布、事故、学习资产 | 支撑复杂项目追溯和复盘 | @@ -62,11 +62,11 @@ 安全边界 执行位置 / 沙箱等级 / 权限 / 网络 / 凭据 / 主动配置信任 / 应急放行 -内核扩展点 - 生命周期 hooks / 规范事件 / 打点 / 工具复写候选 / 插件效果候选 +扩展契约与生态适配 + Plugin Runtime Host contract / Event Manifest / Tool ABI / Permission/Effect / UI descriptor / 插件效果候选 -未来插件适配主机(按需启用) - 适配器注册表 / 项目执行域 / 插件单元 / 工具门面 / 事件路由 / 候选效果输出 +Plugin Runtime Host(按需启用) + adapter registry / project domain / plugin cell / tool facade / event router / candidate effect output 交付物与证据面 证据包 / 交付物图谱 / 问题生命周期 / 风险接受 @@ -78,7 +78,7 @@ Git / CI / Issue / 文档 / 发布 / 观测 / 知识库 智能体与工具运行时 - 会话 / 智能体 / 终端 / 文件系统 / MCP-LSP / Hooks / 适配器 + 会话 / 智能体 / 终端 / 文件系统 / MCP-LSP / Tool ABI / 适配器 ``` 关键边界: @@ -88,7 +88,7 @@ - 阶段收益编排约束每次落地必须说明用户可见收益、后台前置、延期边界和质量一致性。 - 配置化策略面决定内部体验强度、检查建议、证据展示层级和用户覆盖选项。 - 安全边界负责权限、执行位置、沙箱等级、网络、凭据和高风险动作隔离;权限确认、快照/回滚隔离和运行时沙箱必须分开表达。 -- Kernel 扩展点先提供空实现和稳定契约;未来插件适配主机由 Rust 内核管理生命周期,只返回建议、证据候选、工具参数补丁或工具结果候选。内核仍是状态、安全、工具执行和审计的唯一事实源。 +- 扩展契约先定义 Plugin Runtime Host contract、Event Manifest、Tool ABI、Permission/Effect 和 UI descriptor;插件运行时主机由 Product Assembly 注册,通过安全控制面约束,只返回建议、证据候选、UI contribution、工具输入补丁或工具后置证据候选。内核事实、安全控制面和执行层共同写入状态、安全、工具执行结果和审计事实。 - 交付物、证据和质量数据面支撑解释、审查、发布和复盘。 - 项目集成面适配外部系统,并把外部语义映射为内部稳定事件和契约。 - 智能体运行时执行任务,并通过策略面和安全边界获取质量结论与授权状态。 @@ -108,7 +108,7 @@ | 变更就绪度 / PR 门禁 | [pr-quality-gate.md](features/pr-quality-gate.md) | 生成 PR 信心摘要;强策略下成为门禁 | 仅准备 PR 或配置启用 | | 需求影响分析 | [requirement-impact-analysis.md](features/requirement-impact-analysis.md) | 高风险需求/API/设计变更的影响候选 | 按需 | | 智能体评测 | [agent-evaluation.md](features/agent-evaluation.md) | 评估智能体、上下文、策略和治理策略 | 否 | -| OpenCode 兼容层 | [opencode-compatibility.md](features/opencode-compatibility.md) | 先提供主动配置发现和扩展点契约,后续作为生态适配器接入 | 受安全边界管控 | +| Plugin Runtime Host 与 OpenCode 兼容适配 | [opencode-compatibility.md](features/opencode-compatibility.md) | 发现主动配置,约束插件候选效果,并把 OpenCode 能力映射到 BitFun 稳定契约 | 受安全边界管控 | ## 6. 配置层级 @@ -148,7 +148,7 @@ | 能力/效果模型统一 | tool、MCP、skills、插件、hook 和内置能力必须映射为能力声明、目标对象、数据类别、信任来源和副作用候选 | | 未声明能力受限 | 新增扩展未声明能力、声明不完整或运行时行为超出声明时,只能进入受限模式或安全确认,不能按低风险静默执行 | | 策略不写死工具名 | 策略引擎以能力、效果、数据、来源、执行域和配置上下文判定;工具名只用于展示、审计、兼容和调试 | -| 外部适配不写权威状态 | 外部插件和适配器只能返回候选效果;通过、失败、阻断、审计和权限状态由内核写入 | +| 外部适配不写权威状态 | 外部插件和适配器只能返回候选效果;通过、失败、阻断、审计和权限状态由内核、安全边界和执行层写入 | | 工具复写显式授权 | 用户确认复写哪个工具和能力范围;复写表按项目执行域生效,不能静默覆盖全局工具 | ## 8. 架构风险 diff --git a/docs/sdlc-harness/features/opencode-compatibility.md b/docs/sdlc-harness/features/opencode-compatibility.md index 698329348c..f7531edc99 100644 --- a/docs/sdlc-harness/features/opencode-compatibility.md +++ b/docs/sdlc-harness/features/opencode-compatibility.md @@ -1,265 +1,149 @@ -# BitFun 子模块设计:主动配置与 OpenCode 兼容层 +# BitFun 子模块设计:Plugin Runtime Host 与 OpenCode 兼容适配 -> 上游文档:[design.md](../design.md) -> 模块角色:在 BitFun 内部 Hook/Event Bus 与安全边界之上,发现、隔离、审核并兼容 OpenCode 风格插件、hook、自定义工具和事件流。 +> 上游文档:[design.md](../design.md)、 +> [core-decomposition.md](../../architecture/core-decomposition.md)、 +> [agent-runtime-services-design.md](../../architecture/agent-runtime-services-design.md)、 +> [plugin-runtime-host-design.md](../../architecture/plugin-runtime-host-design.md) ## 1. 模块定位 -OpenCode 兼容层是未来插件生态适配的参考子模块。它短期负责发现项目中的主动配置,约束 hook、plugin、自定义工具或 MCP 带来的执行风险,并把观察、建议、验证提示和证据候选映射到 BitFun 的规范事件。 +本模块描述 SDLC Harness 场景下的插件生态接入方式。OpenCode 兼容能力是 BitFun +Plugin Runtime Host 内部的 compatibility adapter,不是 BitFun 内部插件模型、运行时内核或公共 API owner。 -BitFun 内部以规范事件、交付物、权限和策略模型为准;OpenCode API 负责降低插件迁移成本。插件可以提供观察、建议、验证提示或证据候选;安全边界、通过/失败、强制/阻断和审计事实由 BitFun 策略层写入。 +SDLC Harness 只关心插件能力进入开发治理链路后的信任、权限、证据、风险提示和 UI 投影。具体执行必须复用 +BitFun 的 Plugin Runtime Host、Tool ABI、Event Manifest、Permission/Effect Control Plane 和 +UI Extension Contract。 -P0/P1 不承诺任意社区插件无修改运行,也不把 OpenCode 插件运行时作为默认能力。默认策略是发现、展示、禁用、信任审查和内核扩展点预留;执行型自定义工具必须等到插件适配主机和隔离策略成熟后再启用。 +核心原则: -长期目标不是复制 OpenCode 运行时,而是形成一个可承载多生态适配器的受控插件适配主机:OpenCode、Kiro hook、Claude hook 或 BitFun 原生插件都通过同一 Host ABI 返回候选效果,Rust AI Kernel 保持唯一事实源。这个目标不进入 P0/P1 的实现关键路径。 +- OpenCode API 只作为兼容输入和映射目标;BitFun 内部模块只依赖自身稳定 contract。 +- 插件、hook、自定义工具和工具复写默认是主动配置,必须先发现、记录来源和权限,再进入信任审查。 +- 插件只能产出候选效果;授权、阻断、审计、通过/失败状态和权威任务事实由 BitFun 内核、安全边界和执行层写入。 +- UI 扩展只通过 descriptor 暴露 slot、route、command、prompt、dialog、state view 等贡献,不暴露 React、Tauri、DOM 或具体 renderer handle。 +- 可写 JS/TS 插件运行时不是默认能力,必须在沙箱、secret、网络、包源安全和崩溃隔离具备独立评审后再开放。 -## 2. 行业参照与设计约束 +## 2. 架构对齐 -| 参照 | 启发 | -|---|---| -| [OpenCode Plugins](https://opencode.ai/docs/plugins/) / [SDK](https://opencode.ai/docs/sdk/) / [Server API](https://opencode.ai/docs/server/) | plugin 上下文、hooks object、自定义工具、客户端日志、SSE 事件流是生态迁移重点 | -| [Codex Hooks](https://developers.openai.com/codex/hooks) | hook 需要信任审查、配置来源、事件范围、并发和关闭机制 | -| [Claude Code Hooks](https://code.claude.com/docs/en/hooks) | hook 需要明确阻塞/非阻塞、退出码、权限和上下文语义 | -| [Kiro Hooks](https://kiro.dev/docs/hooks/) | hook 已成为 IDE 内事件触发自动化能力,但必须和权限、策略、人工确认分离 | -| [OWASP LLM Top 10](https://owasp.org/www-project-top-10-for-large-language-model-applications/) | 插件、工具调用、数据出境和权限提升属于 LLM 应用风险面 | - -设计约束: - -- 兼容适配器通过映射层接入 BitFun 内部事件模型。 -- 插件能力受权限、策略、脱敏和审计约束。 -- 项目内 hook、plugin 配置和自定义工具默认未信任。 -- 插件来源、版本、hash、权限声明和兼容等级必须可见。 -- 近期优先稳定 Rust AI Kernel 的 hook、事件、打点、工具复写候选和插件效果候选契约。 -- 插件适配主机作为后续生态能力由 Rust AI Kernel 拉起和监管,默认使用本地受控 IPC,不暴露公开 localhost 服务。 -- 一个插件适配主机运行时可以承载多个项目执行域,但项目之间的事件、信任、权限、工具复写表和插件状态必须隔离。 -- 插件适配主机只能返回候选效果,不能直接写入通过、失败、阻断、审计或权限状态。 -- BitFun 不继承 OpenCode “工具默认可用”的语义;所有工具、shell、网络、文件和凭据能力仍按 BitFun 安全边界授权。 -- JS/TS 运行时本身不是安全边界;如果无法限制直接 `fs`、`fetch`、`spawn` 或依赖副作用,高风险插件必须进入更强隔离。 -- 多个 hook 命中同一事件时,不允许依赖隐式顺序做安全判断。 -- 阻断语义必须进入 BitFun 策略层;第三方 hook 只能建议。 -- 兼容承诺必须通过测试矩阵表达,不用“兼容 OpenCode”这种宽泛表述替代边界。 +| 能力 | 所属架构合同 | 本模块使用方式 | +|---|---|---| +| 插件生命周期 | Plugin Runtime Host | install、activate、deactivate、reload、dispose 必须可撤销、可审计 | +| 工具接入 | Tool ABI | built-in tool、MCP tool、plugin tool 使用同一 snapshot、provider identity、permission/effect 和 stale call guard | +| 事件订阅 | Event Manifest / Quality Data Plane | 插件消费 public event manifest;事件写入、回放和投影由质量数据面承接 | +| 权限与副作用 | Permission/Effect Control Plane | 插件 hook 只能返回 candidate decision;最终安全决策由安全边界完成 | +| UI 扩展 | UI Extension Contract | OpenCode TUI 能力映射为 descriptor,由各产品入口选择是否渲染 | +| 产品组装 | Product Assembly | 选择 adapter manifest/capability set、host 隔离等级、支持矩阵和 product capability | + +禁止依赖: + +- 插件或 OpenCode adapter 直接依赖 `bitfun-core/product-full`。 +- 插件拿到 full `RuntimeServices`、concrete provider handle、session manager 或 UI implementation。 +- OpenCode payload 进入 Agent Kernel、Execution、Security Boundary 的内部状态对象。 ## 3. 范围与边界 范围: -- 发现 OpenCode 风格主动配置,并写入项目画像。 -- 映射 OpenCode 常见事件到 BitFun 规范 Hook/Event Bus。 -- 提供有限 plugin 上下文、客户端门面、自定义工具 API。 -- 支持 SSE 事件流或本地事件订阅的受控子集。 -- 支持观察/建议类插件产出证据候选或风险提示。 +- 发现 OpenCode 风格配置、hook、自定义工具和插件入口,并写入项目画像。 +- 维护 OpenCode server plugin 和 TUI plugin 的支持矩阵。 +- 将 OpenCode 事件、工具、permission hook、配置 transform 和 UI contribution 映射为 BitFun contract。 +- 将插件输出整理为建议、证据候选、工具输入补丁、工具后置观察、UI contribution 或 typed unsupported。 +- 为信任审查、安全决策、证据包、PR 就绪度和回放评测提供事件与测试夹具。 边界: -- BitFun 提供兼容适配器和受控子集,不复制 OpenCode 运行时。 -- OpenCode 是一种适配器,不是 BitFun 内部插件模型。 -- OpenCode 配置作为外部生态配置读取,BitFun 规范配置仍由自身策略模型承载。 -- 运行时组件和依赖缓存可以共享;项目执行域、工具复写、权限和审计不能共享。 -- 插件兼容范围通过测试矩阵表达,shell 语义和阻塞行为逐项声明。 -- 门禁通过、就绪度就绪和审计事实由 BitFun 策略层写入。 -- 快速路径只依赖 BitFun 原生能力;插件能力按需启用。 - -### 3.1 适配主机与内核边界 - -```text -Rust AI Kernel - 权威事件日志 / 策略决策 / 安全边界 / 工具执行 / 审计 - <本地 IPC:JSON-RPC、gRPC over pipe 或等价协议> -插件适配主机(Plugin Adapter Host) - 运行时核心 / 适配器注册表 / 项目执行域管理器 / 效果路由 - -> OpenCode Adapter - -> Kiro Hook Adapter - -> Claude Hook Adapter - -> BitFun Native Adapter -``` - -Kernel 负责状态和副作用,插件适配主机负责插件语言运行时和生态 API 适配。适配主机崩溃、重启、超时或返回旧结果时,Kernel 必须能降级、拒绝或重放事件。插件隔离等级必须映射到 [安全边界](../architecture/security-boundary.md) 的执行与沙箱矩阵;Host 内部的 JS/TS 运行时、worker 或子进程不是自动可信边界。 - -项目执行域(Project Domain)包含项目根、worktree、执行主机、远程上下文、信任记录、事件订阅、工具复写表、权限范围和审计流。一个 Host 进程可以管理多个执行域,但不能让插件全局变量、订阅、复写表、环境变量或授权跨域复用。 - -默认只有一个插件适配主机监督进程。监督进程可以按风险启动插件单元(Plugin Cell)、worker、子进程或容器;这些都是 Host 管理的执行单元,不改变 Kernel/Host 的外部契约。 +- 不复制 OpenCode 运行时,也不承诺任意社区插件无修改运行。 +- 不允许插件直接写任务状态、审计事实、权限状态、PR 门禁、证据通过状态或安全策略。 +- 不允许工具复写静默覆盖全局工具;复写必须按项目执行域、来源、hash、能力范围和期限生效。 +- 不把插件失败解释为任务失败;失败必须进入降级、警告、候选丢弃或安全决策。 +- 不把 JS worker、subprocess 或 WebView 当成自动可信沙箱。 -运行时隔离等级: +## 4. OpenCode 映射模型 -| 等级 | 适用 | 要求 | 对外沙箱等级 | -|---|---|---|---| -| cell | 受信任观察/建议插件 | 独立模块图、事件订阅和超时;不得访问真实 shell、网络、凭据 | `permission_only` 或 `process_isolated` | -| worker | 建议、guard、轻量自定义工具 | 独立工作线程或等价隔离;所有副作用走 Kernel facade | `process_isolated` | -| subprocess | 工具复写、act、高风险依赖 | 独立进程、资源预算、环境变量白名单、工作目录限制 | `process_isolated`,必要时叠加 `readonly_scope` 或 `network_restricted` | -| sandbox | 未知来源或企业受管场景 | 容器、无凭据环境、网络策略和只读/临时 worktree | `containerized` | +### 4.1 Server Plugin -入口形态约束: - -| 入口 | 约束 | -|---|---| -| Tauri 桌面 | Host 由 Rust 后端管理,不能在 WebView 内执行插件;权限确认投影到桌面 UI | -| TUI/CLI | Host 不占用标准输入输出;确认、降级和错误以文本状态呈现 | -| 远程开发 | 插件执行位置必须与文件、命令和 Git 操作的执行位置一致;本地 Host 不能直接代替远程 Host 操作远程路径 | -| 云端/异步 | Host 生命周期绑定任务或执行域;长任务必须支持取消、超时和审计续接 | - -## 4. 输入、输出与数据模型 +| OpenCode 能力 | BitFun 映射 | 约束 | +|---|---|---| +| `tool.execute.before/after` | Tool ABI hook / evidence candidate | 只能建议、补丁或记录证据;不能直接写通过/失败 | +| `permission.ask` | Permission candidate hook | 最终 decision、audit、policy write 仍由安全边界完成 | +| custom tool | Tool provider contribution | 必须声明 schema、provider identity、capability/effect、cancellation 和 stale call guard | +| event subscription | Event Manifest subscription | 只能消费 public event;不能读取 session 内部结构 | +| config/provider/model transform | Scoped transform descriptor | 必须进入支持矩阵;未知能力返回 typed unsupported | +| client log / notification | Quality Data Plane / UI projection | 作为日志或提示候选,不作为权威事实 | -OpenCode 常见事件映射: +### 4.2 TUI Plugin -| OpenCode 事件 | BitFun 来源 | 默认用途 | +| OpenCode 能力 | BitFun 映射 | 约束 | |---|---|---| -| `tool.execute.before` | 工具运行时 | 权限检查、风险提示、命令建议 | -| `tool.execute.after` | 工具运行时 | 验证摘要、证据候选 | -| `permission.asked` / `permission.replied` | 审批系统 | 安全授权和审计 | -| `file.edited` / `file.watcher.updated` | 文件监听 | 过期证据、风险提示 | -| `lsp.client.diagnostics` | LSP 服务 | 诊断证据候选 | -| `session.diff` | Git 服务 | 就绪度提示 | -| `session.idle` | 会话运行时 | 未验证风险和完成度建议 | -| `shell.env` | 环境提供者 | 凭据和环境注入策略 | +| route / panel / slot | UI contribution descriptor | 各入口可选择渲染或返回 unsupported | +| keymap / command | UiCommandDescriptor | 不能绕过权限和产品命令注册 | +| prompt augmentation | Prompt contribution candidate | 必须标注来源和信任级别 | +| dialog / toast | UI notification descriptor | 只能投影提示,不写权威状态 | +| state access | Read-only state view | 只能读取投影,不暴露内部 store | +| theme | Theme contribution descriptor | 不能写全局 CSS、DOM 或宿主实现对象 | -兼容上下文: +## 5. 数据合同 -```ts -interface OpenCodeCompatContext { - project: { id: string; root: string; worktree: string }; - directory: string; - client: OpenCodeCompatClient; - permissions: PermissionFacade; - events: EventFacade; - security: SecurityBoundaryFacade; -} +兼容适配器需要使用窄合同,避免暴露实现细节: -type PluginEffect = - | { kind: "suggestion"; body: unknown } - | { kind: "evidence_candidate"; body: unknown } - | { kind: "tool_input_patch"; tool_call_id: string; patch: unknown } - | { kind: "tool_result"; tool_call_id: string; result: unknown } - | { kind: "deny_request"; reason: string } - | { kind: "log"; level: "debug" | "info" | "warn" | "error"; message: string }; - -interface PluginDispatchEnvelope { - event_id: string; - sequence: number; - project_domain_id: string; - session_id: string; - tool_call_id?: string; - correlation_id: string; - causation_id?: string; - deadline_ms: number; - project_epoch: number; - trust_epoch: number; - tool_registry_epoch: number; - worktree_revision?: string; +```ts +interface ExtensionCompatibilityDescriptor { + adapter_id: string; + ecosystem: "opencode" | "kiro" | "claude_code" | "bitfun_native"; + source: { + kind: "workspace" | "user" | "organization" | "remote"; + location: string; + hash?: string; + }; + trust: { + state: "discovered" | "trusted" | "changed" | "disabled" | "revoked"; + scope: "project" | "worktree" | "session" | "organization"; + }; + capabilities: CapabilityDeclaration[]; + effects: EffectDeclaration[]; } ``` -## 5. 核心流程 - -```text -发现主动配置 - -> 记录来源、hash、权限和范围 - -> 分类信任状态 - -> Kernel 写入规范事件 - -> Kernel 按事件类型投递到插件适配主机 - -> 适配主机选择项目执行域和生态适配器 - -> 在 deadline、权限 facade 和隔离约束下执行 plugin hook - -> 适配主机返回 PluginEffect - -> Kernel 校验 epoch、权限、安全策略和幂等键 - -> Kernel 应用、降级、拒绝或记录候选效果 - -> Kernel 追加审计事件 -``` - -时序策略: - -| 事件类型 | 处理方式 | 超时策略 | -|---|---|---| -| 观察/建议 | 异步,不阻塞主流程 | 记录 warning 或丢弃候选效果 | -| 执行前 hook | 同步,有 deadline | 按策略 fail-open、fail-closed 或请求用户确认 | -| 自定义工具 / 工具复写 | request/response | 超时则工具失败或降级,不写通过状态 | - -适配主机响应必须携带原始 envelope 的标识。`project_epoch`、`trust_epoch`、`tool_registry_epoch` 或 `worktree_revision` 变化后,旧响应不能应用。 - -Hook 效应等级: - -| 等级 | 能力 | 默认策略 | 就绪度/门禁关系 | -|---|---|---|---| -| observe | 读取事件、记录日志、生成证据候选 | 受限只读,可在受信任来源中启用 | 不能影响就绪/通过 | -| recommend | 生成建议、风险提示、验证提示 | 需要声明输出结构 | 只能进入建议 | -| guard | 对工具、权限或文件操作提出警告/拒绝建议 | 必须通过 BitFun 策略引擎解释 | 可导致建议投影、降级或拒绝,但不能直接写通过/失败 | -| act | 修改工具输入、触发命令、写文件或调用自定义工具 | 默认关闭,需要显式信任、权限、超时和审计 | 只产出事实或证据,决策仍由 BitFun 产生 | - -项目级信任记录必须绑定 hook 来源、hash、范围、权限、创建者和审核人。hook 内容变化后信任状态失效,必须重新确认。 +应用规则: -工具复写规则: +- IPC 信封、候选效果和 UI descriptor 的权威 schema 见 + [`plugin-runtime-host-design.md`](../../architecture/plugin-runtime-host-design.md)。 +- `project_epoch`、`trust_epoch`、`policy_epoch` 或 `tool_registry_epoch` 变化后,旧响应不得应用。 +- 所有候选效果必须通过 `PluginResponseEnvelope` 进入应用流程;不得传递裸候选对象。 +- 候选效果进入策略或安全边界前,必须重新校验 capability/effect 声明。 +- UI contribution 必须声明目标 surface、fallback 和 required capabilities。 +- 真实工具结果只能由 Tool ABI 下的 Execution 路径写入;插件只能返回工具前置补丁或工具后置证据候选。 -- 普通插件工具默认命名空间化,避免与内置工具重名。 -- 内置工具复写必须显式展示被复写工具、来源、hash、权限范围、持续时间和撤销入口。 -- 工具复写表按项目执行域生效,不写入全局工具表。 -- 用户确认只表示启用意图;文件、shell、网络和凭据访问仍走 BitFun 安全边界。 -- 复写插件变化、权限声明变化或执行主机变化后,原信任立即失效。 +## 6. 安全与质量保护 -## 6. API 兼容等级 - -兼容等级用于描述未来可支持的能力边界,不等同于实施阶段。P0/P1 只落实发现、禁用、契约和空实现。 - -| 等级 | 范围 | 目标 | -|---|---|---| -| L0 | 发现、事件命名、载荷映射、只读客户端日志 | 支持迁移和观察 | -| L1 | `tool.execute.*`、`permission.*`、`file.*`、`session.*` 只读或建议 | 支持核心低风险插件 | -| L2 | 自定义工具、SSE 事件流、受限 `$` shell 门面 | 支持可控扩展 | -| L3 | 更广泛生态兼容 | 仅在 L0-L2 稳定后评估 | - -兼容矩阵: - -| 能力 | P0/P1 状态 | 说明 | -|---|---|---| -| 项目级插件发现 | 支持 | 发现但默认不执行 | -| 项目级插件加载 | 暂缓 | P0/P1 只发现和展示,不执行插件代码 | -| 全局插件加载 | 暂不默认启用 | 避免跨项目状态串扰和权限混淆 | -| hook 事件映射 | 契约预留 | 先定义 BitFun 规范事件和 hook 点,不运行 OpenCode hook | -| 自定义工具 | 契约预留 | 先定义权限、输入输出结构和候选效果,不执行外部工具 | -| 工具复写 | 契约预留 | 必须显式确认、按项目生效,并重新经过安全边界 | -| shell 门面 | 暂缓 | 等插件适配主机、隔离和权限模型稳定后评估 | -| SSE 事件流 | 暂缓 | 先稳定本地事件订阅和权限模型 | - -## 7. 策略与治理 - -- **安全优先**:插件执行前必须通过安全边界。 -- **权限优先**:文件、shell、网络、凭据访问全部走 BitFun 权限模型。 -- **策略优先**:hook 只触发和采集,复杂判断进入策略引擎。 -- **Kernel 优先**:状态、权限、工具执行和审计由 Rust Kernel 写入,适配主机只返回候选效果。 -- **隔离执行**:默认禁止无约束 shell、网络和全仓读写;只有安全边界确认存在真实隔离时,才能把结果标记为沙箱允许。 -- **执行域隔离**:项目执行域之间不共享工具复写、事件订阅、信任状态和插件全局状态。 -- **信任优先**:项目内 hook/plugin/custom 工具必须先完成信任审查;未信任定义只能被展示和禁用。 -- **审计可追溯**:插件输入、输出、耗时、失败和副作用写入质量数据面。 -- **兼容可测试**:每个兼容等级必须有测试插件和行为测试。 -- **降级可见**:插件失败不能静默影响任务结果,必须进入警告、降级或安全决策。 - -## 8. 分阶段落地 - -| 阶段 | 目标 | +| 风险 | 保护方式 | |---|---| -| P0 | 主动配置发现、L0 映射、配置展示、禁用和审计,不执行插件 | -| P1 | Kernel 扩展点 v0:规范 hook、事件、打点、工具复写候选、插件效果候选和空实现 | -| P2 | 信任审查持久化、基于回放用例的适配测试、权限策略校准,不启用真实 OpenCode 插件运行 | -| P3 | 插件适配主机原型、只读/建议类适配器试点、项目执行域隔离验证 | -| P4 | 自定义工具、工具复写、SSE 事件流、插件注册表、签名/来源标识和企业策略包 | +| OpenCode API 反向污染内部模型 | 内部只依赖 BitFun Plugin Runtime Host、Tool ABI、Event Manifest、Permission/Effect 和 UI descriptor | +| 插件越权授权 | hook 只返回 candidate;最终安全决策和审计由安全边界写入 | +| 工具复写绕过内置工具策略 | 复写按项目执行域生效,重新经过 Tool ABI、权限声明和安全边界 | +| UI 插件耦合前端实现 | descriptor-only;宿主适配器渲染;不支持时返回 unsupported 或安全文本投影 | +| 跨项目状态串扰 | trust、event subscription、tool override、workspace path 和授权绑定 project domain | +| 远程/本地语义漂移 | envelope 必须携带 execution domain、logical path、workspace identity 和 capability facts | +| JS/TS 运行时供应链风险 | 默认不开放可写运行时;需要包源、secret、网络、权限和崩溃隔离安全评审 | +| 兼容承诺过宽 | 支持矩阵逐项列出能力、级别、测试和降级;禁止泛称“全量兼容” | + +## 7. 兼容等级 -## 9. 风险与反证 +本表描述支持等级,不是排期承诺。 -| 风险 | 反证或治理要求 | +| 能力等级 | 必须满足的合同 | |---|---| -| 兼容层侵入核心模型 | 内部模块不得依赖 OpenCode 载荷;只能依赖规范事件 | -| 插件越权 | 文件、shell、网络、凭据访问全部走 BitFun 权限 | -| 插件影响决策结论 | 插件只能产出证据或建议,不能直接写通过、失败或就绪 | -| hook 顺序被误用为安全边界 | 安全策略必须在 BitFun 策略层统一判断 | -| 项目级主动配置供应链风险 | 信任记录绑定 hash 和权限;配置变化后必须重新确认 | -| 适配主机与 Kernel 时序不一致 | envelope、deadline、epoch、幂等键和过期响应丢弃规则必须测试覆盖 | -| 工具复写越权 | 复写表按项目执行域隔离,执行仍走安全边界 | -| 运行时不一致 | L0/L1 明确支持范围,不承诺完整 OpenCode 运行时 | -| 维护成本边界不清 | API 兼容性分级推进,每级有成功标准和退出条件 | - -## 10. 成功标准 - -- 项目主动配置能被发现、解释、禁用和重新信任。 -- BitFun 内核事件、权限和审计模型保持独立。 -- P0/P1 阶段即使没有插件适配主机,Kernel 扩展点空实现也不会影响默认任务。 -- hook、事件、打点、工具复写候选和插件效果候选能被质量数据面记录、回放和测试。 -- 工具复写契约不会跨项目生效,也不会绕过权限、沙箱、网络和凭据策略。 -- 后续启用插件适配主机时,插件失败、超时、拒绝权限和旧响应必须能被安全边界和证据包感知。 -- L0/L1 兼容范围清晰,未支持能力不会被误认为可用。 +| Discovery | 能发现配置、记录来源/hash/权限并默认禁用 | +| Read-only adapter | 能消费 public event manifest,输出建议或证据候选,失败可降级 | +| Tool provider adapter | Tool ABI、permission/effect、stale call guard、cancellation 和项目执行域隔离通过测试 | +| UI contribution adapter | descriptor round-trip、入口 fallback、只读 state view 和 unsupported 行为通过测试 | +| Writable / executable plugin runtime | 沙箱、secret、网络、包源安全、资源预算、崩溃隔离和审计具备独立评审结论 | + +## 8. 成功标准 + +- OpenCode adapter 不成为 BitFun 内部 owner。 +- 插件配置能被发现、解释、禁用、重新信任和撤销。 +- 插件只能通过候选效果影响治理链路,不能直接写权威状态。 +- 工具、MCP、自定义工具和插件工具使用同一 Tool ABI 语义。 +- UI 扩展能力可在 Desktop、CLI、Server、Remote、ACP、Web、Mobile Web、SDK 等形态下明确支持或显式不可用。 +- 未支持 OpenCode 能力返回 typed unsupported,不静默忽略。 +- adapter 失败、超时、旧响应和权限拒绝能被安全边界、质量数据面和证据包追踪。 diff --git a/docs/sdlc-harness/implementation-plan.md b/docs/sdlc-harness/implementation-plan.md index e8e412ea8c..520a0a6399 100644 --- a/docs/sdlc-harness/implementation-plan.md +++ b/docs/sdlc-harness/implementation-plan.md @@ -51,7 +51,7 @@ | 风险与验证 | 未验证项进入任务摘要 | 行内弱提示、推荐检查、不可运行原因 | 配置化强制检查和审查强度 | 后验缺陷、CI、审查反馈回流 | | 团队规则 | 规则来源只作为上下文展示 | 冲突或受管信号折叠提示 | 路径规则、CODEOWNERS、PR 策略按需生效 | 合规批准、发布就绪和组织审计 | | 证据与图谱 | 记录最小事件和验证摘要 | 生成证据引用,不默认完整包 | PR/团队场景投影证据包 | 交付物图谱、需求影响、事故回归 | -| 扩展与插件 | 预留 hook、事件、打点和候选契约 | 空实现、过期候选和幂等校验 | 工具复写契约和主动配置信任 | 插件适配主机原型、生态评测 | +| 扩展与插件 | 预留 Plugin Runtime Host contract、Event Manifest、Tool ABI 和候选契约 | 空实现、过期候选和幂等校验 | 工具复写契约、UI descriptor 和主动配置信任 | 插件运行时主机原型、生态评测 | | 指标与评测 | 建立速度、打断、安全和摘要覆盖基线 | 采样误升级、推荐检查和不可支持恢复 | 采样强制检查精度和 PR 采用 | 轨迹回放、策略 A/B、保留集 | ## 4. 阶段路线图 @@ -60,7 +60,7 @@ |---|---|---|---|---| | P-1 | 不作为用户发布;明确产品边界和技术跑道 | 策略契约、安全契约、事件契约、阶段用例和指标口径 | LifecycleEvent、策略对象、配置优先级、测试桩、API 预设 | 能说明每个 P0/P1 切片服务的用户收益、降级解释和验收方法 | | P0 | 用户能低摩擦完成低风险任务,并看懂安全范围和未验证项 | 轻量项目理解、任务短摘要、安全确认、执行位置、主动配置发现、最小 QDP | 项目扫描、基础安全决策、事件记录、摘要投影、关键回放用例 | 普通任务可完成;安全越界不静默;低风险任务不被重流程拖慢 | -| P1 | 风险、远程和不可支持状态变得可理解,用户知道该补什么验证 | 弱提示、推荐检查、远程/沙箱降级、Kernel 扩展点 v0、证据引用摘要 | 风险分类器、提示降噪、sandbox capability 事件、扩展点空实现 | 高风险解释清楚;低风险仍快速;远程和不可支持状态有替代路径 | +| P1 | 风险、远程和不可支持状态变得可理解,用户知道该补什么验证 | 弱提示、推荐检查、远程/沙箱降级、Plugin Runtime Host contract / Event Manifest v0、证据引用摘要 | 风险分类器、提示降噪、sandbox capability 事件、扩展契约空实现 | 高风险解释清楚;低风险仍快速;远程和不可支持状态有替代路径 | | P2 | 准备 PR 或进入团队规则时,用户获得可复制的协作摘要和规则说明 | PR 就绪度、团队规则折叠说明、风险接受、主动配置信任、工具复写契约 | 规则来源合成、PR 投影、风险接受记录、证据引用状态 | PR 摘要减少审查追问;团队规则来源可解释;强制项只在配置或确定性风险下出现 | | P3 | 受管项目的需求、开发、验证、审查和发布质量要求保持一致 | 路径级强策略、完整证据包投影、最小图谱、发布/事故链接、组织审计 | 图谱边状态、证据过期、受管配置、发布和事故事件 | 复杂项目可追溯;阻断/批准来源明确;快速路径仍保留 | | P4 | 用真实任务持续优化策略,淘汰无收益治理能力 | 轨迹回放、策略 A/B、评测卡、保留集、生态适配评估 | 回放环境、评测血缘、策略版本和成本采样 | 能用速度、质量、安全和成本联合评估治理策略 | @@ -71,7 +71,7 @@ |---|---|---|---|---|---| | P-1 | PRD、设计、事件信封、策略对象、安全边界、测试夹具、指标口径 | 技术跑道清晰;P0/P1 的收益、降级和验收方法可解释 | 契约过度设计导致后续实现被早期字段锁死;事件和策略对象未服务真实体验;测试夹具只覆盖理想路径 | 契约评审必须逐项映射 PRD、事件、用例和指标;新增字段要声明生产者、消费者和删除条件 | 不设用户路径预算;限制设计期事件和指标数量,避免 P0 一开始背负遥测膨胀 | | P0 | 项目打开、项目上下文条、任务短摘要、安全确认、远程执行位置、主动配置发现、最小 QDP | 低风险任务能完成;用户看到已验证/未验证、安全范围、执行位置和下一步 | 默认体验被安全/摘要/扫描拖慢;项目扫描误判后形成强策略;安全确认过多;沙箱能力被夸大;主动配置发现误阻断普通任务 | 回放低风险任务、安全高敏动作、远程容器、主动配置发现;摘要必须包含未验证项;安全与质量事件分开审计 | 首次有用动作以 P0 基线为准;项目扫描分层、可取消、可延后;安全确认和摘要渲染不阻塞工具结果展示;QDP 只采最小事件 | -| P1 | 风险提示、推荐检查、远程不可支持状态、沙箱降级说明、Kernel 扩展点、证据引用摘要 | 用户能理解风险和能力缺口,并知道补什么验证 | 风险提示泛化成噪音;推荐检查不可运行;远程缺口被本地验证掩盖;扩展点空实现被误认为可用插件能力;证据引用缺少新鲜度 | 中/高风险用例必须说明原因、证据、建议和跳过后果;推荐检查要有不可运行分支;扩展点候选不能写权威状态 | 风险分类和提示生成后台化;同类提示合并;扩展点有 deadline、epoch、幂等键;证据引用只保存摘要和引用 | +| P1 | 风险提示、推荐检查、远程不可支持状态、沙箱降级说明、Plugin Runtime Host contract / Event Manifest 空实现、证据引用摘要 | 用户能理解风险和能力缺口,并知道补什么验证 | 风险提示泛化成噪音;推荐检查不可运行;远程缺口被本地验证掩盖;扩展契约空实现被误认为可用插件能力;证据引用缺少新鲜度 | 中/高风险用例必须说明原因、证据、建议和跳过后果;推荐检查要有不可运行分支;扩展候选不能写权威状态 | 风险分类和提示生成后台化;同类提示合并;扩展契约有 deadline、epoch、幂等键;证据引用只保存摘要和引用 | | P2 | PR 就绪度、团队规则来源、路径规则、风险接受、主动配置信任、工具复写契约 | PR 或团队场景可获得可复制摘要和规则说明;普通任务不被团队流程打断 | 团队规则冲突导致误阻断;PR 摘要生成过重;风险接受被滥用;工具复写绕过安全;未信任主动配置产生假通过 | PR/团队用例抽样验证规则来源、强制项、风险接受和撤销;风险接受必须记录范围、原因、操作者、期限;工具复写重新经过安全边界 | PR 投影按需生成;证据引用增量更新;团队规则读取缓存并可解释失效;不创建默认强制 GitHub 检查 | | P3 | 路径级强策略、完整证据包投影、最小交付物图谱、需求/API/架构影响候选、发布/事故事件 | 受管项目的需求、实现、验证、审查、发布和风险接受强度一致 | 图谱误关联造成错误责任链;证据过期未失效;需求质量很高但开发/验证仍低保障;发布阻断来源不清;普通任务被图谱拖慢 | 受管路径抽样检查质量一致性;图谱边必须有来源、新鲜度和确认状态;发布就绪区分事实、候选和风险接受 | 图谱和证据只在 PR、发布、事故、合规场景显露;增量计算优先;过期重算后台化;普通任务保留短摘要 | | P4 | 轨迹回放、评测卡、策略 A/B、保留集、成本采样、生态适配评估 | 策略能用真实任务持续优化,低价值治理能力可下线 | 评测集污染;只优化强管控任务;A/B 影响用户信任;成本指标压倒质量;插件生态评估反向推动不成熟运行时提前接入 | 每个评测任务有血缘、泄漏状态和判定标准;A/B 只在可回滚策略上运行;后验缺陷、审查阻塞和用户反馈共同校准 | 回放和评测离线或低优先级执行;token、墙钟耗时、工具调用和提示次数进入评测卡;生态适配默认不改变内核权威状态 | @@ -89,7 +89,7 @@ | 事件和指标膨胀 | P0 就采集完整日志、轨迹、证据和图谱字段 | 性能下降、隐私风险、事件迁移成本变高 | P0 只采最小 QDP;新事件必须声明生产者、消费者、保留周期、隐私分级和删除条件 | | 证据或图谱过期 | 新提交、策略变化、验证失败、审查范围变化后仍复用旧证据 | PR、发布或事故复盘引用错误事实 | 证据和图谱边带新鲜度、来源和确认状态;过期后只能作为历史引用,不能作为通过依据 | | 团队规则冲突 | AGENTS、CODEOWNERS、CI、工具配置和组织策略给出不同要求 | 误阻断、重复检查、规则不可解释 | 配置优先级固定;冲突显示来源和作用域;不能静默选择高风险路径 | -| 插件和工具复写越权 | 外部插件、OpenCode 适配层或工具复写候选直接影响权威状态 | 执行链不可信,后续需要重构 Kernel/Host 边界 | P0/P1 只保留 Kernel 扩展点和候选契约;候选不能写通过、失败、阻断、审计或授权状态 | +| 插件和工具复写越权 | 外部插件、OpenCode 适配层或工具复写候选直接影响权威状态 | 执行链不可信,后续需要重构 Plugin Runtime Host、Tool ABI 和 Security Bridge 边界 | 早期只保留 Plugin Runtime Host 空实现和候选契约;候选不能写通过、失败、阻断、审计或授权状态 | | 性能优化牺牲安全 | 为了减少弹窗或延迟,把安全确认合并到质量覆盖或默认允许 | 安全边界被质量流程稀释 | 安全决策独立常驻;降噪只能通过范围限制、白名单、真实沙箱和一次性授权实现 | | 评测过拟合 | 策略只针对固定任务集或强治理样本优化 | 普通项目体验变差,真实任务收益下降 | 评测卡记录血缘和污染状态;低风险、远程、安全、PR、受管路径都必须保留样本 | @@ -106,8 +106,8 @@ P-1 是内部跑道,不应作为用户可见“新能力”发布。它的价 | 关键用例清单 | 低风险开发、安全确认、远程上下文、PR 就绪、受管路径、主动配置信任的回放用例 | 每个 PRD-18 用例都有入口、默认状态、体验收益和验证方法 | | 安全边界契约 | 权限、执行位置、沙箱等级组合、网络、凭据、主动配置信任、应急放行 | 安全决策不依赖门禁或质量策略 | | 能力/效果模型契约 | tool、MCP、skills、插件、hook 和内置能力统一声明能力、目标、数据类别、信任来源和副作用候选 | 新扩展不需要修改策略核心即可进入安全判定;未声明能力默认受限 | -| 执行与沙箱矩阵 | 本地、远程、ACP、MCP、MiniApp/前端、插件适配主机、浏览器/桌面、云端任务的执行位置、隔离等级、降级原因和用户选项 | `allow_in_sandbox` 只在真实隔离可用时出现 | -| Kernel 扩展点契约 | 生命周期 hook、事件信封、打点、工具复写候选、插件效果候选、deadline、epoch 和幂等键 | 未来适配器可接入,空实现不影响默认任务 | +| 执行与沙箱矩阵 | 本地、远程、ACP、MCP、MiniApp/前端、插件运行时主机、浏览器/桌面、云端任务的执行位置、隔离等级、降级原因和用户选项 | `allow_in_sandbox` 只在真实隔离可用时出现 | +| 扩展契约 | Plugin Runtime Host contract、Event Manifest、Tool ABI、UI descriptor、工具复写候选、插件效果候选、deadline、epoch 和幂等键 | 未来适配器可接入,空实现不影响默认任务 | | 最小事件注册表 | `project.opened`、`task.started`、`policy.decided`、`security.decided`、`verification.completed`、`task.completed`、`stage.outcome.evaluated`、`quality.consistency.sampled`、`performance.budget.evaluated` | 快速路径、安全提示、任务摘要、阶段收益、质量一致性和性能回退可追踪 | | 产品指标规格 | 首次有用动作耗时、打断率、阶段收益达成率、质量一致性缺口率、应急放行率 | 阶段验收不只看技术完成度 | @@ -167,7 +167,7 @@ P-1 是内部跑道,不应作为用户可见“新能力”发布。它的价 | 推荐检查可执行或可解释 | 推荐验证命令、替代验证、不可运行原因和跳过后果 | 不把推荐检查等同阻断 | | 远程不可支持状态可恢复 | 私有依赖、本地二进制、端口、凭据或权限不可用时给替代路径 | 不假装本地检查覆盖远程环境 | | 沙箱降级可见 | 本地/远程/ACP/MCP/插件/浏览器/云端执行面的状态块、降级说明和替代路径 | 不把权限确认包装成沙箱 | -| 扩展点可预留但不干扰 | 稳定事件、hook 注册点、打点、工具复写候选契约和空实现 | 不执行 OpenCode 插件,不让候选写权威状态 | +| 扩展契约可预留但不干扰 | Plugin Runtime Host contract、Event Manifest、Tool ABI、打点、工具复写候选契约和空实现 | 不执行 OpenCode 插件,不让候选写权威状态 | | 证据引用按需出现 | 只生成摘要和证据引用,服务任务摘要和后续 PR | 不默认生成完整证据包 | ### 7.3 验收成果 diff --git a/docs/sdlc-harness/product-requirements.md b/docs/sdlc-harness/product-requirements.md index 290ae76c70..666f8d719e 100644 --- a/docs/sdlc-harness/product-requirements.md +++ b/docs/sdlc-harness/product-requirements.md @@ -21,7 +21,7 @@ AI 编码工具正在从代码补全、单轮问答和单点审查,演进为 | 沙箱边界可见 | 用户能分清本机、远程、容器、ACP、MCP、插件和浏览器/桌面动作的执行位置、隔离等级和授权范围 | | 阶段收益明确 | 每个落地阶段都先交付短而明确的用户收益,技术前置和延期边界必须可解释 | | 复杂能力后台化 | 证据包、交付物图谱、质量数据面和评测只在 PR、团队、发布、事故或合规场景显性化 | -| 生态扩展预留 | 先稳定 Kernel 扩展点;OpenCode 等插件生态后续接入时必须在 BitFun 边界内生效 | +| 生态扩展预留 | 先稳定 Plugin Runtime Host contract、binding、envelope、disabled stub、Event Manifest、Tool ABI、Permission/Effect 和 UI descriptor;OpenCode 等插件生态后续接入时必须在 BitFun 边界内生效 | | 体验可度量 | 速度、打断、误升级、安全确认、用户覆盖和质量结果按用户画像切片 | ## 3. 用户画像 @@ -64,7 +64,7 @@ AI 编码工具正在从代码补全、单轮问答和单点审查,演进为 | 团队配置 | 配置来源、优先级、冲突状态和生效范围 | 团队配置需要可解释;冲突规则先提示或降级,不静默选择高风险路径 | | PR 就绪度 | 可复制到 PR 的变更、验证、风险和证据引用摘要 | 只有准备 PR 或团队策略启用时显露,不干扰普通任务中间过程 | | 复杂能力入口 | 证据包、交付物图谱、发布就绪度、事故回溯和评测入口 | 复杂能力从用户任务触发,不作为默认首屏或默认流程 | -| Kernel 扩展点 | 生命周期 hook、事件、打点、工具复写候选和插件效果候选 | 先提供空实现和稳定契约;外部插件运行时不作为默认能力 | +| 扩展契约 | Plugin Runtime Host contract、生命周期 envelope、Event Manifest、Tool ABI、Permission/Effect、UI descriptor 和插件效果候选 | 先提供稳定契约;外部插件运行时不作为默认能力 | | 工具复写契约 | 被复写工具、复写来源、权限范围、持续时间和撤销入口 | 用户确认只表达意图;实际文件、shell、网络和凭据访问仍由安全边界执行 | ### 5.1 实际产品变更清单 @@ -116,7 +116,7 @@ AI 编码工具正在从代码补全、单轮问答和单点审查,演进为 | 弱提示与弹窗 | 默认使用行内提示、可折叠摘要和任务结束汇总;弹窗仅用于安全确认、不可逆操作、组织策略或用户明确进入关键流程 | | 用户覆盖与组织策略 | 用户可以临时选择更快或更严;受管组织策略、受保护分支和安全拒绝保持更高优先级 | | 项目规则与主动配置 | AGENTS、CONTRIBUTING、CODEOWNERS、CI 等规则可作为项目知识;hook、plugin、MCP 和自定义工具先进入信任审查 | -| 插件兼容与核心模型 | OpenCode 等生态后续通过适配器接入;BitFun 内部只依赖规范事件、权限和插件效果候选契约 | +| 插件兼容与核心模型 | OpenCode 等生态后续通过适配器接入;BitFun 内部只依赖 Plugin Runtime Host、Event Manifest、Tool ABI、Permission/Effect 和 UI descriptor | | 工具复写与用户确认 | 用户可启用工具复写,但复写必须绑定项目、来源、hash、权限范围和期限,不能静默改变全局工具语义 | | 模型判断与系统状态 | 模型可生成候选风险、摘要和建议;策略状态、验证结果、授权状态必须来自确定性事件或用户决策 | | 本地能力与远程能力 | 本地可直接感知文件、命令和环境;远程/云端必须显式标注工作区、权限、环境缺口和不可支持动作 | @@ -154,7 +154,7 @@ AI 编码工具正在从代码补全、单轮问答和单点审查,演进为 | PRD-13 | 内部策略不直接外露 | 用户默认看不到内部策略枚举;设置和日志可以展示策略来源、原因和调试信息 | | PRD-14 | 提示降噪 | 同类提示合并或延后,弹窗仅用于安全、不可逆、组织强制或用户进入关键流程 | | PRD-15 | 远程开发适配 | 远程执行必须展示执行位置、权限边界、端口/网络范围、路径映射和不可用能力 | -| PRD-16 | Kernel 扩展点预留 | 生命周期 hook、事件、打点、工具复写候选和插件效果候选有稳定契约,且空实现不影响默认任务 | +| PRD-16 | 扩展契约预留 | Plugin Runtime Host、Event Manifest、Tool ABI、Permission/Effect、UI descriptor 和插件效果候选有稳定契约,且未启用 adapter 不影响默认任务 | | PRD-17 | 工具复写受限启用 | 内置工具复写必须显示复写来源、能力范围和撤销入口,执行仍经过安全边界 | | PRD-18 | 关键用例体验可验收 | 质量保障要求较低的任务、安全确认、PR 就绪、受管路径、远程上下文和主动配置信任都有预期体验变化、用户收益、布局、交互和验收点 | | PRD-19 | 沙箱边界可见 | 每次高风险动作都能展示执行位置、沙箱等级组合、授权范围、降级原因和替代路径;没有真实沙箱时不得展示为沙箱允许 | @@ -201,7 +201,7 @@ AI 编码工具正在从代码补全、单轮问答和单点审查,演进为 | 阶段 | 用户可见收益 | 后台前置和延期边界 | |---|---|---| | P0 | 低风险任务能快速完成,并看懂项目入口、安全范围、已验证/未验证项和下一步 | 只做轻量项目理解、基础安全决策和最小事件;完整证据包、图谱和团队门禁延期 | -| P1 | 风险、远程能力缺口、沙箱降级和推荐检查变得可理解 | 需要风险分类、提示降噪、sandbox capability 事件和扩展点空实现;不执行外部插件运行时 | +| P1 | 风险、远程能力缺口、沙箱降级和推荐检查变得可理解 | 需要风险分类、提示降噪、sandbox capability 事件和扩展契约空实现;不执行外部插件运行时 | | P2 | 准备 PR 或命中团队规则时获得可复制摘要、规则来源和风险接受入口 | 需要规则来源合成、PR 投影和证据引用;强制检查只在配置或确定性风险下出现 | | P3 | 受管项目的需求、开发、验证、审查、发布和风险接受强度保持一致 | 需要路径级强策略、证据包投影、最小图谱和发布/事故事件;普通任务仍保留轻量路径 | | P4 | 用真实任务回放和指标持续优化治理策略,并淘汰无收益能力 | 需要评测血缘、策略版本、成本采样和保留集;生态适配以不改变内核权威状态为前提 | diff --git a/docs/sdlc-harness/traceability-matrix.md b/docs/sdlc-harness/traceability-matrix.md index 20606f4984..3ee2d5805d 100644 --- a/docs/sdlc-harness/traceability-matrix.md +++ b/docs/sdlc-harness/traceability-matrix.md @@ -9,16 +9,16 @@ | 内部策略不外露 | PRD-03、PRD-13 | 体验投影层、用户语言稳定 | 配置化策略画像 | P-1、P0 | 检查桌面、命令行、远程、PR 摘要是否只展示任务状态、原因和下一步,不展示内部枚举 | | 弱提示和弹窗降噪 | PRD-14 | 提示先弱后强、弹窗触发约束 | 配置化策略画像、质量数据面 | P0、P1 | 同类提示合并/延后用例;统计用户打断率、弹窗触发率、重复提示抑制率 | | 执行安全常驻 | PRD-04 | 安全边界独立 | 安全边界 | P0 | shell、网络、凭据、删除、跨目录写、hook/MCP 用例;验证 allow/ask/deny/应急放行记录 | -| 沙箱边界可见 | PRD-19 | 执行位置和沙箱等级分层 | 安全边界、质量数据面、证据包 | P0、P1、P3 | 本地 shell、远程 SSH、ACP、MCP、MiniApp、插件适配主机、浏览器/桌面和云端任务用例;验证执行位置、沙箱等级组合、降级原因、授权范围和替代路径 | +| 沙箱边界可见 | PRD-19 | 执行位置和沙箱等级分层 | 安全边界、质量数据面、证据包 | P0、P1、P3 | 本地 shell、远程 SSH、ACP、MCP、MiniApp、插件运行时主机、浏览器/桌面和云端任务用例;验证执行位置、沙箱等级组合、降级原因、授权范围和替代路径 | | 阶段性用户收益 | PRD-20 | 阶段收益编排、质量一致性检查、阶段风险副作用分析 | 实施计划、指标规格、质量数据面 | P-1、P0-P4 | 每阶段回放低风险任务、安全确认、远程/不可支持状态、PR/团队场景;验证用户收益、技术前置、延期边界、风险副作用、质量一致性和性能预算 | | 远程开发适配 | PRD-11、PRD-12、PRD-15 | 本地/远程能力边界、项目集成面 | 项目画像、风险分类器、安全边界 | P0、P1 | SSH、容器、远程工作区、云端任务用例;验证执行位置、路径映射、端口/网络、不可支持状态和替代路径 | | 团队配置治理 | PRD-05、PRD-06 | 配置层级、组织策略优先级 | 配置化策略画像、PR 门禁 | P2 | `.bitfun`、AGENTS、CODEOWNERS、CI、路径规则冲突用例;验证来源、优先级和覆盖限制 | | PR 就绪度 | PRD-07 | 证据按需投影、PR 场景显露 | PR 门禁、证据包 | P1、P2 | 准备 PR、受保护分支、不可运行 CI 用例;验证就绪度摘要、未验证项、证据引用和非默认阻断 | | 复杂项目追溯 | PRD-08 | 交付物图谱、证据可追溯 | 证据包、交付物图谱、需求影响分析 | P3 | 需求变更、发布、事故复盘用例;验证链接来源、新鲜度、人工确认和过期处理 | -| 主动配置信任 | PRD-09 | 项目规则与主动配置分层 | OpenCode 兼容层、安全边界 | P0、P2 | hook、plugin、MCP、自定义工具回放用例;验证来源、hash、权限、触发条件和信任状态 | -| Kernel 扩展点 | PRD-16 | hook、事件、打点、插件效果候选、内核权威状态 | OpenCode 兼容层、质量数据面 | P-1、P1 | 空实现、过期 epoch、重复 event、候选效果丢弃、打点采样回放用例;验证不影响默认任务 | -| 能力/效果模型 | PRD-09、PRD-16、PRD-17 | 能力/效果模型统一、未声明能力受限、策略不写死工具名 | 安全边界、OpenCode 兼容层、质量数据面 | P-1、P0、P1 | tool、MCP、skills、插件、hook、内置工具回放用例;验证能力声明、目标对象、数据类别、信任来源、副作用候选和未知能力受限 | -| 工具复写安全 | PRD-17 | 工具复写显式授权、复写表按项目执行域生效 | OpenCode 兼容层、安全边界 | P1、P2 | 内置工具复写契约、同名工具候选、权限撤销、hash 变化、远程执行主机变化用例;验证复写不跨项目且仍经安全边界 | +| 主动配置信任 | PRD-09 | 项目规则与主动配置分层 | Plugin Runtime Host 与 OpenCode 兼容适配、安全边界 | P0、P2 | hook、plugin、MCP、自定义工具回放用例;验证来源、hash、权限、触发条件和信任状态 | +| 扩展契约 | PRD-16 | Plugin Runtime Host、Event Manifest、Tool ABI、UI descriptor、插件效果候选、内核权威状态 | Plugin Runtime Host 与 OpenCode 兼容适配、质量数据面 | P-1、P1 | 空实现、过期 epoch、重复 event、候选效果丢弃、打点采样回放用例;验证不影响默认任务 | +| 能力/效果模型 | PRD-09、PRD-16、PRD-17 | 能力/效果模型统一、未声明能力受限、策略不写死工具名 | 安全边界、Plugin Runtime Host 与 OpenCode 兼容适配、质量数据面 | P-1、P0、P1 | tool、MCP、skills、插件、hook、内置工具回放用例;验证能力声明、目标对象、数据类别、信任来源、副作用候选和未知能力受限 | +| 工具复写安全 | PRD-17 | 工具复写显式授权、复写表按项目执行域生效 | Plugin Runtime Host 与 OpenCode 兼容适配、安全边界 | P1、P2 | 内置工具复写契约、同名工具候选、权限撤销、hash 变化、远程执行主机变化用例;验证复写不跨项目且仍经安全边界 | | 关键用例体验 | PRD-18 | 体验投影层、页面/面板布局、跨入口语义一致 | 产品需求、配置化策略画像、质量数据面 | P0、P1 | 质量保障要求较低的探索性改动、文档改动、安全确认、PR 就绪、受管路径、远程容器、主动配置回放用例;验证体验变化、用户收益、布局、文案、交互和降噪 | | 产品指标闭环 | PRD-10 | 质量数据面、评测与学习 | 指标规格、智能体评测 | P0-P4 | 指标采样回放、策略 A/B、保留集;验证速度、打断、安全、质量和成本联合解释 | diff --git a/package-lock.json b/package-lock.json index d3fa5ef4cd..c3f9908fe6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "BitFun", - "version": "0.2.10", + "version": "0.2.12", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "BitFun", - "version": "0.2.10", + "version": "0.2.12", "hasInstallScript": true, "dependencies": { "pnpm": "^10.32.1", diff --git a/package.json b/package.json index ce109a1939..8b63e085da 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "BitFun", "private": true, - "version": "0.2.10", + "version": "0.2.12", "type": "module", "engines": { "node": ">=18.0.0" @@ -11,7 +11,8 @@ "copy-icons": "copyfiles -f \"src/apps/desktop/icons/Logo-ICON.png\" \"src/web-ui/public/\"", "copy-assets": "pnpm run copy-monaco && pnpm run copy-icons", "generate-version": "node scripts/generate-version.cjs", - "generate-all": "pnpm run generate-version", + "generate-startup-theme-bootstrap": "node scripts/generate-startup-theme-bootstrap.mjs", + "generate-all": "pnpm run generate-version && pnpm run generate-startup-theme-bootstrap", "postinstall": "pnpm run copy-assets", "dev": "node scripts/dev.cjs web", "dev:web": "pnpm --dir src/web-ui dev", @@ -26,7 +27,11 @@ "i18n:contract:test:ci": "cross-env BITFUN_I18N_CONTRACT_TEST_PROFILE=ci node --test scripts/i18n-contract.test.mjs", "i18n:audit": "node scripts/i18n-audit.mjs", "theme:color-audit": "node scripts/audit-theme-colors.mjs", - "theme:color-audit:test": "node --test scripts/audit-theme-colors.test.mjs", + "theme:color-audit:mobile": "node scripts/audit-theme-colors.mjs --root src/mobile-web/src --baseline scripts/theme-color-governance-baseline.mobile-web.json", + "theme:color-audit:installer": "node scripts/audit-theme-colors.mjs --root BitFun-Installer/src --baseline scripts/theme-color-governance-baseline.installer.json", + "theme:color-audit:cli": "node scripts/audit-cli-theme-colors.mjs", + "theme:color-audit:all": "pnpm run theme:color-audit && pnpm run theme:color-audit:mobile && pnpm run theme:color-audit:installer && pnpm run theme:color-audit:cli", + "theme:color-audit:test": "node --test scripts/audit-theme-colors.test.mjs scripts/audit-cli-theme-colors.test.mjs", "theme:visual-contract": "node scripts/validate-theme-visual-contract.mjs", "check:repo-hygiene": "node scripts/check-repo-hygiene.mjs", "check:github-config": "pnpm --dir src/web-ui exec node ../../scripts/check-github-config.mjs", diff --git a/resources/flashgrep/README.md b/resources/flashgrep/README.md index a7206fee70..c4d8429161 100644 --- a/resources/flashgrep/README.md +++ b/resources/flashgrep/README.md @@ -2,7 +2,7 @@ Place the prebuilt `flashgrep` daemon binary in this directory. Pinned release: -- `v0.2.6` from `wgqqqqq/flashgrep` +- `v0.2.10` from `wgqqqqq/flashgrep` Expected filenames: diff --git a/resources/flashgrep/VERSION.json b/resources/flashgrep/VERSION.json index 61ff82f2c1..42da02c4b5 100644 --- a/resources/flashgrep/VERSION.json +++ b/resources/flashgrep/VERSION.json @@ -1,5 +1,5 @@ { "repo": "wgqqqqq/flashgrep", - "tag": "v0.2.7", - "published_at": "2026-05-12T04:48:18Z" + "tag": "v0.2.10", + "published_at": "2026-06-17T08:15:11Z" } diff --git a/resources/flashgrep/flashgrep-aarch64-apple-darwin b/resources/flashgrep/flashgrep-aarch64-apple-darwin index fe670685b1..410b228d2d 100755 Binary files a/resources/flashgrep/flashgrep-aarch64-apple-darwin and b/resources/flashgrep/flashgrep-aarch64-apple-darwin differ diff --git a/resources/flashgrep/flashgrep-aarch64-pc-windows-msvc.exe b/resources/flashgrep/flashgrep-aarch64-pc-windows-msvc.exe index c060bd8759..d99a4b41bd 100644 Binary files a/resources/flashgrep/flashgrep-aarch64-pc-windows-msvc.exe and b/resources/flashgrep/flashgrep-aarch64-pc-windows-msvc.exe differ diff --git a/resources/flashgrep/flashgrep-aarch64-unknown-linux-musl b/resources/flashgrep/flashgrep-aarch64-unknown-linux-musl index d7101d8dce..3639f94f22 100755 Binary files a/resources/flashgrep/flashgrep-aarch64-unknown-linux-musl and b/resources/flashgrep/flashgrep-aarch64-unknown-linux-musl differ diff --git a/resources/flashgrep/flashgrep-x86_64-apple-darwin b/resources/flashgrep/flashgrep-x86_64-apple-darwin index ed1cf1ccd8..53231225ad 100755 Binary files a/resources/flashgrep/flashgrep-x86_64-apple-darwin and b/resources/flashgrep/flashgrep-x86_64-apple-darwin differ diff --git a/resources/flashgrep/flashgrep-x86_64-pc-windows-msvc.exe b/resources/flashgrep/flashgrep-x86_64-pc-windows-msvc.exe index 977d7b0050..b74384f85c 100644 Binary files a/resources/flashgrep/flashgrep-x86_64-pc-windows-msvc.exe and b/resources/flashgrep/flashgrep-x86_64-pc-windows-msvc.exe differ diff --git a/resources/flashgrep/flashgrep-x86_64-unknown-linux-musl b/resources/flashgrep/flashgrep-x86_64-unknown-linux-musl index 55b3e5d864..51aa97b8a0 100755 Binary files a/resources/flashgrep/flashgrep-x86_64-unknown-linux-musl and b/resources/flashgrep/flashgrep-x86_64-unknown-linux-musl differ diff --git a/scripts/audit-cli-theme-colors.mjs b/scripts/audit-cli-theme-colors.mjs new file mode 100644 index 0000000000..f95248ce94 --- /dev/null +++ b/scripts/audit-cli-theme-colors.mjs @@ -0,0 +1,421 @@ +#!/usr/bin/env node +import fs from 'node:fs'; +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; +import { + checkBudgetBaseline, + cliHexColorDistance, + cliRgbToHex, + normalizeCliHexColor, + writeReportJson, +} from './theme-color-audit-utils.mjs'; + +const DEFAULT_ROOT = 'src/apps/cli'; +const DEFAULT_BASELINE = 'scripts/theme-color-governance-baseline.cli.json'; +const DEFAULT_NEAR_THRESHOLD = 10; + +export { normalizeCliHexColor as normalizeHexColor, writeReportJson } from './theme-color-audit-utils.mjs'; + +export const CLI_RUNTIME_THEME_KEYS = new Set([ + 'primary', + 'success', + 'warning', + 'error', + 'info', + 'textMuted', + 'background', + 'border', + 'backgroundPanel', + 'backgroundElement', + 'inputBackground', + 'diffAdded', + 'diffRemoved', + 'diffAddedBg', + 'diffRemovedBg', + 'borderActive', + 'accent', + 'diffHunkHeader', + 'diffLineNumber', +]); + +function baseThemeKey(key) { + return key.replace(/\.(dark|light)$/, ''); +} + +export function isRuntimePresetEntry(entry) { + return CLI_RUNTIME_THEME_KEYS.has(baseThemeKey(entry.key)); +} + +export function collectPresetColorEntriesFromJson(file, jsonText) { + const parsed = JSON.parse(jsonText); + const theme = parsed.theme; + if (!theme || typeof theme !== 'object' || Array.isArray(theme)) { + throw new Error(`${file} must contain a theme object`); + } + const defs = parsed.defs && typeof parsed.defs === 'object' && !Array.isArray(parsed.defs) + ? parsed.defs + : {}; + const defaultMode = /(?:^|[-_/])light(?:[-_.]|$)/i.test(file) ? 'light' : 'dark'; + + return Object.entries(theme).flatMap(([key, value]) => { + return collectColorValueEntries({ + file, + key, + value, + theme, + defs, + mode: defaultMode, + seen: new Set(), + }); + }); +} + +function collectColorValueEntries({ file, key, value, theme, defs, mode, seen }) { + if (typeof value === 'number') { + return []; + } + if (typeof value === 'string') { + const trimmed = value.trim(); + if (trimmed === '' || trimmed.toLowerCase() === 'none' || trimmed.toLowerCase() === 'transparent') { + return []; + } + const color = normalizeCliHexColor(trimmed, { mode }); + if (color) { + return [{ file, key, color }]; + } + + const referenced = Object.prototype.hasOwnProperty.call(defs, trimmed) + ? defs[trimmed] + : Object.prototype.hasOwnProperty.call(theme, trimmed) + ? theme[trimmed] + : undefined; + if (referenced === undefined) { + return []; + } + if (seen.has(trimmed)) { + throw new Error(`${file} theme color reference cycle detected at "${trimmed}"`); + } + const nextSeen = new Set(seen); + nextSeen.add(trimmed); + return collectColorValueEntries({ + file, + key, + value: referenced, + theme, + defs, + mode, + seen: nextSeen, + }); + } + if (!value || typeof value !== 'object' || Array.isArray(value)) { + return []; + } + if (Object.prototype.hasOwnProperty.call(value, 'dark') && Object.prototype.hasOwnProperty.call(value, 'light')) { + return [ + ...collectColorValueEntries({ + file, + key: `${key}.dark`, + value: value.dark, + theme, + defs, + mode: 'dark', + seen: new Set(seen), + }), + ...collectColorValueEntries({ + file, + key: `${key}.light`, + value: value.light, + theme, + defs, + mode: 'light', + seen: new Set(seen), + }), + ]; + } + + return []; +} + +export function collectRustFallbackEntriesFromText(file, sourceText) { + const entries = []; + const pattern = /([a-zA-Z_][a-zA-Z0-9_]*):\s*Color::Rgb\(\s*(\d+),\s*(\d+),\s*(\d+)\s*\)/g; + let match; + while ((match = pattern.exec(sourceText)) !== null) { + entries.push({ + file, + key: match[1], + color: cliRgbToHex( + Number.parseInt(match[2], 10), + Number.parseInt(match[3], 10), + Number.parseInt(match[4], 10), + ), + }); + } + return entries; +} + +export function findNearPairs(entries, threshold = DEFAULT_NEAR_THRESHOLD) { + const filesByColor = new Map(); + const keysByColor = new Map(); + for (const entry of entries) { + if (!filesByColor.has(entry.color)) { + filesByColor.set(entry.color, new Set()); + keysByColor.set(entry.color, new Set()); + } + filesByColor.get(entry.color).add(entry.file); + keysByColor.get(entry.color).add(`${entry.file}:${entry.key}`); + } + + const colors = Array.from(filesByColor.keys()).sort(); + const pairs = []; + for (let i = 0; i < colors.length; i += 1) { + for (let j = i + 1; j < colors.length; j += 1) { + const distance = cliHexColorDistance(colors[i], colors[j]); + if (distance > 0 && distance <= threshold) { + pairs.push({ + a: colors[i], + b: colors[j], + distance: Number(distance.toFixed(2)), + files: Array.from(new Set([ + ...filesByColor.get(colors[i]), + ...filesByColor.get(colors[j]), + ])).sort(), + keysByColor: { + [colors[i]]: Array.from(keysByColor.get(colors[i])).sort(), + [colors[j]]: Array.from(keysByColor.get(colors[j])).sort(), + }, + }); + } + } + } + + pairs.sort((left, right) => left.distance - right.distance || left.a.localeCompare(right.a)); + return pairs; +} + +function countByColor(entries) { + const counts = new Map(); + for (const entry of entries) { + counts.set(entry.color, (counts.get(entry.color) ?? 0) + 1); + } + return Array.from(counts.entries()) + .map(([color, count]) => ({ color, count })) + .sort((a, b) => b.count - a.count || a.color.localeCompare(b.color)); +} + +function listPresetFiles(root) { + const presetDir = path.join(root, 'themes', 'presets'); + return fs.readdirSync(presetDir) + .filter(file => file.endsWith('.json')) + .sort() + .map(file => path.join(presetDir, file)); +} + +function readPresetEntries(files) { + return files.flatMap(file => { + return collectPresetColorEntriesFromJson( + path.relative(process.cwd(), file).replaceAll('\\', '/'), + fs.readFileSync(file, 'utf8'), + ); + }); +} + +function readRustFallbackEntries(root) { + const fullPath = path.join(root, 'src', 'ui', 'theme.rs'); + return collectRustFallbackEntriesFromText( + path.relative(process.cwd(), fullPath).replaceAll('\\', '/'), + fs.readFileSync(fullPath, 'utf8'), + ); +} + +export function createCliThemeColorReport(options = {}) { + const root = options.root ?? DEFAULT_ROOT; + const threshold = options.nearThreshold ?? DEFAULT_NEAR_THRESHOLD; + const presetFiles = listPresetFiles(root); + const presetEntries = readPresetEntries(presetFiles); + const runtimePresetEntries = presetEntries.filter(isRuntimePresetEntry); + const compatibilityPresetEntries = presetEntries.filter(entry => !isRuntimePresetEntry(entry)); + const rustFallbackEntries = readRustFallbackEntries(root); + const allEntries = [...presetEntries, ...rustFallbackEntries]; + const runtimeEntries = [...runtimePresetEntries, ...rustFallbackEntries]; + const presetNearPairs = findNearPairs(presetEntries, threshold); + const runtimePresetNearPairs = findNearPairs(runtimePresetEntries, threshold); + const compatibilityPresetNearPairs = findNearPairs(compatibilityPresetEntries, threshold); + const rustFallbackNearPairs = findNearPairs(rustFallbackEntries, threshold); + + return { + root, + presetFiles: presetFiles.length, + presetColorOccurrences: presetEntries.length, + presetUniqueColors: new Set(presetEntries.map(entry => entry.color)).size, + runtimePresetColorOccurrences: runtimePresetEntries.length, + runtimePresetUniqueColors: new Set(runtimePresetEntries.map(entry => entry.color)).size, + compatibilityPresetColorOccurrences: compatibilityPresetEntries.length, + compatibilityPresetUniqueColors: new Set(compatibilityPresetEntries.map(entry => entry.color)).size, + rustFallbackColorOccurrences: rustFallbackEntries.length, + rustFallbackUniqueColors: new Set(rustFallbackEntries.map(entry => entry.color)).size, + totalUniqueColors: new Set(allEntries.map(entry => entry.color)).size, + runtimeTotalUniqueColors: new Set(runtimeEntries.map(entry => entry.color)).size, + nearThreshold: threshold, + runtimeThemeKeys: Array.from(CLI_RUNTIME_THEME_KEYS).sort(), + presetNearPairs: { + nearTotal: presetNearPairs.length, + near: presetNearPairs, + }, + runtimePresetNearPairs: { + nearTotal: runtimePresetNearPairs.length, + near: runtimePresetNearPairs, + }, + compatibilityPresetNearPairs: { + nearTotal: compatibilityPresetNearPairs.length, + near: compatibilityPresetNearPairs, + }, + rustFallbackNearPairs: { + nearTotal: rustFallbackNearPairs.length, + near: rustFallbackNearPairs, + }, + topPresetColors: countByColor(presetEntries), + topRuntimePresetColors: countByColor(runtimePresetEntries), + topCompatibilityPresetColors: countByColor(compatibilityPresetEntries), + topRustFallbackColors: countByColor(rustFallbackEntries), + }; +} + +function parseArgs(argv) { + const options = { + root: DEFAULT_ROOT, + baseline: DEFAULT_BASELINE, + json: false, + reportJson: null, + top: 20, + }; + + for (let i = 0; i < argv.length; i += 1) { + const arg = argv[i]; + if (arg === '--root') { + options.root = argv[++i]; + if (!options.root) { + throw new Error('--root requires a path'); + } + } else if (arg.startsWith('--root=')) { + options.root = arg.slice('--root='.length); + if (!options.root) { + throw new Error('--root requires a path'); + } + } else if (arg === '--baseline') { + options.baseline = argv[++i]; + if (!options.baseline) { + throw new Error('--baseline requires a baseline path'); + } + } else if (arg.startsWith('--baseline=')) { + options.baseline = arg.slice('--baseline='.length); + if (!options.baseline) { + throw new Error('--baseline requires a baseline path'); + } + } else if (arg === '--report-json') { + options.reportJson = argv[++i]; + if (!options.reportJson) { + throw new Error('--report-json requires an output path'); + } + } else if (arg.startsWith('--report-json=')) { + options.reportJson = arg.slice('--report-json='.length); + if (!options.reportJson) { + throw new Error('--report-json requires an output path'); + } + } else if (arg === '--json') { + options.json = true; + } else if (arg === '--top') { + options.top = Number.parseInt(argv[++i], 10); + } else if (arg.startsWith('--top=')) { + options.top = Number.parseInt(arg.slice('--top='.length), 10); + } else if (arg === '--no-baseline') { + options.baseline = null; + } else { + throw new Error(`Unknown argument: ${arg}`); + } + } + + return options; +} + +export function checkBaseline(report, baselinePath) { + return checkBudgetBaseline(report, baselinePath); +} + +function printRows(title, rows, top) { + console.log(title); + if (rows.length === 0) { + console.log(' none'); + return; + } + for (const row of rows.slice(0, top)) { + if ('count' in row) { + console.log(`${String(row.count).padStart(7)} ${row.color}`); + } else { + console.log(` ${row.a} <-> ${row.b} distance=${row.distance} files=${row.files.join(', ')}`); + } + } +} + +function printReport(report, top) { + console.log(`CLI theme color audit: ${report.root}`); + console.log(`Preset files: ${report.presetFiles}`); + console.log(`Preset color occurrences: ${report.presetColorOccurrences}`); + console.log(`Preset unique colors: ${report.presetUniqueColors}`); + console.log(`Runtime-consumed preset color occurrences: ${report.runtimePresetColorOccurrences}`); + console.log(`Runtime-consumed preset unique colors: ${report.runtimePresetUniqueColors}`); + console.log(`Compatibility-declared preset color occurrences: ${report.compatibilityPresetColorOccurrences}`); + console.log(`Compatibility-declared preset unique colors: ${report.compatibilityPresetUniqueColors}`); + console.log(`Rust fallback Color::Rgb occurrences: ${report.rustFallbackColorOccurrences}`); + console.log(`Rust fallback unique colors: ${report.rustFallbackUniqueColors}`); + console.log(`Total CLI unique colors: ${report.totalUniqueColors}`); + console.log(`Runtime CLI unique colors: ${report.runtimeTotalUniqueColors}`); + console.log(`Preset near pairs: ${report.presetNearPairs.nearTotal}`); + console.log(`Runtime-consumed preset near pairs: ${report.runtimePresetNearPairs.nearTotal}`); + console.log(`Compatibility-declared preset near pairs: ${report.compatibilityPresetNearPairs.nearTotal}`); + console.log(`Rust fallback near pairs: ${report.rustFallbackNearPairs.nearTotal}`); + console.log(''); + printRows('Top preset colors:', report.topPresetColors, top); + console.log(''); + printRows('Top runtime-consumed preset colors:', report.topRuntimePresetColors, top); + console.log(''); + printRows('Top compatibility-declared preset colors:', report.topCompatibilityPresetColors, top); + console.log(''); + printRows('Top Rust fallback colors:', report.topRustFallbackColors, top); + console.log(''); + printRows('Preset near pairs:', report.presetNearPairs.near, Math.min(top, 20)); + console.log(''); + printRows('Runtime-consumed preset near pairs:', report.runtimePresetNearPairs.near, Math.min(top, 20)); + console.log(''); + printRows('Compatibility-declared preset near pairs:', report.compatibilityPresetNearPairs.near, Math.min(top, 20)); + console.log(''); + printRows('Rust fallback near pairs:', report.rustFallbackNearPairs.near, Math.min(top, 20)); +} + +function main() { + const options = parseArgs(process.argv.slice(2)); + const report = createCliThemeColorReport({ root: options.root }); + if (options.json) { + console.log(JSON.stringify(report, null, 2)); + } else { + printReport(report, options.top); + } + + if (options.reportJson) { + writeReportJson(report, options.reportJson); + } + + const failures = checkBaseline(report, options.baseline); + if (failures.length > 0) { + console.error(''); + console.error('CLI theme color audit failed:'); + for (const failure of failures) { + console.error(`- ${failure}`); + } + process.exitCode = 1; + } +} + +if (process.argv[1] === fileURLToPath(import.meta.url)) { + main(); +} diff --git a/scripts/audit-cli-theme-colors.test.mjs b/scripts/audit-cli-theme-colors.test.mjs new file mode 100644 index 0000000000..6f50697443 --- /dev/null +++ b/scripts/audit-cli-theme-colors.test.mjs @@ -0,0 +1,208 @@ +import assert from 'node:assert/strict'; +import { execFileSync } from 'node:child_process'; +import fs from 'node:fs'; +import os from 'node:os'; +import path from 'node:path'; +import test from 'node:test'; + +import { + CLI_RUNTIME_THEME_KEYS, + checkBaseline, + collectPresetColorEntriesFromJson, + collectRustFallbackEntriesFromText, + createCliThemeColorReport, + findNearPairs, + isRuntimePresetEntry, + normalizeHexColor, + writeReportJson, +} from './audit-cli-theme-colors.mjs'; + +const root = process.cwd(); + +test('normalizeHexColor accepts supported CLI hex colors only', () => { + assert.equal(normalizeHexColor('#AABBCC'), '#aabbcc'); + assert.equal(normalizeHexColor('#abc'), null); + assert.equal(normalizeHexColor('rgba(0, 0, 0, 0.5)'), null); +}); + +test('normalizeHexColor blends eight-digit hex colors like the CLI parser', () => { + assert.equal(normalizeHexColor('#ffffff80'), '#808080'); + assert.equal(normalizeHexColor('#00000080', { mode: 'light' }), '#7f7f7f'); +}); + +test('collectPresetColorEntriesFromJson reads opencode theme colors', () => { + const entries = collectPresetColorEntriesFromJson('theme.json', JSON.stringify({ + theme: { + background: '#101010', + primary: '#60A5FA', + transparent: 'none', + }, + })); + + assert.deepEqual(entries, [ + { file: 'theme.json', key: 'background', color: '#101010' }, + { file: 'theme.json', key: 'primary', color: '#60a5fa' }, + ]); +}); + +test('collectPresetColorEntriesFromJson resolves defs and light/dark variants', () => { + const entries = collectPresetColorEntriesFromJson('bitfun-light.json', JSON.stringify({ + defs: { + neutral: '#00000080', + }, + theme: { + background: 'neutral', + primary: { dark: '#ffffff80', light: '#00000080' }, + }, + })); + + assert.deepEqual(entries, [ + { file: 'bitfun-light.json', key: 'background', color: '#7f7f7f' }, + { file: 'bitfun-light.json', key: 'primary.dark', color: '#808080' }, + { file: 'bitfun-light.json', key: 'primary.light', color: '#7f7f7f' }, + ]); +}); + +test('isRuntimePresetEntry separates consumed CLI keys from OpenCode compatibility declarations', () => { + assert.equal(CLI_RUNTIME_THEME_KEYS.has('diffLineNumber'), true); + assert.equal(CLI_RUNTIME_THEME_KEYS.has('markdownText'), false); + + assert.equal(isRuntimePresetEntry({ + file: 'bitfun-light.json', + key: 'primary.light', + color: '#7f7f7f', + }), true); + assert.equal(isRuntimePresetEntry({ + file: 'bitfun-light.json', + key: 'syntaxKeyword', + color: '#7c6b99', + }), false); +}); + +test('CLI runtime key audit stays aligned with the Rust theme resolver', () => { + const themeSource = fs.readFileSync('src/apps/cli/src/ui/theme.rs', 'utf8'); + const rustRuntimeKeys = Array.from( + new Set(Array.from(themeSource.matchAll(/resolve_key\(json, &defs, "([^"]+)"/g), match => match[1])), + ).sort(); + + assert.deepEqual(Array.from(CLI_RUNTIME_THEME_KEYS).sort(), rustRuntimeKeys); +}); + +test('CLI OpenCode presets keep compatibility declarations outside the runtime projection', () => { + const report = createCliThemeColorReport(); + const presetDir = path.join(root, 'src/apps/cli/themes/presets'); + const presetFiles = fs.readdirSync(presetDir).filter(file => file.endsWith('.json')).sort(); + + assert.ok(report.compatibilityPresetColorOccurrences > report.runtimePresetColorOccurrences); + assert.ok(report.runtimePresetColorOccurrences < report.presetColorOccurrences); + assert.deepEqual(report.runtimeThemeKeys, Array.from(CLI_RUNTIME_THEME_KEYS).sort()); + assert.equal(report.runtimePresetNearPairs.nearTotal, 0); + assert.equal(report.compatibilityPresetNearPairs.nearTotal, 0); + + for (const file of presetFiles) { + const json = JSON.parse(fs.readFileSync(path.join(presetDir, file), 'utf8')); + assert.equal(json.$schema, 'https://opencode.ai/theme.json', `${file} must remain OpenCode-compatible`); + } +}); + +test('collectRustFallbackEntriesFromText reads Theme struct RGB fields only', () => { + const entries = collectRustFallbackEntriesFromText('theme.rs', ` + primary: Color::Rgb(59, 130, 246), + let other = Color::Rgb(1, 2, 3); + muted: Color::DarkGray, + `); + + assert.deepEqual(entries, [ + { file: 'theme.rs', key: 'primary', color: '#3b82f6' }, + ]); +}); + +test('findNearPairs reports nearby but not identical colors', () => { + const pairs = findNearPairs([ + { file: 'a.json', key: 'background', color: '#0e0e10' }, + { file: 'b.json', key: 'background', color: '#101010' }, + { file: 'c.json', key: 'primary', color: '#60a5fa' }, + ], 10); + + assert.equal(pairs.length, 1); + assert.equal(pairs[0].a, '#0e0e10'); + assert.equal(pairs[0].b, '#101010'); +}); + +test('checkBaseline requires budgets to be lowered when CLI color debt drops', () => { + const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'bitfun-cli-theme-audit-')); + try { + const baselinePath = path.join(tempDir, 'baseline.json'); + fs.writeFileSync(baselinePath, JSON.stringify({ + version: 1, + budgets: { + presetUniqueColors: { max: 2 }, + 'rustFallbackNearPairs.nearTotal': { max: 1 }, + }, + })); + + const failures = checkBaseline({ + presetUniqueColors: 1, + rustFallbackNearPairs: { nearTotal: 0 }, + }, baselinePath); + + assert.match(failures.join('\n'), /presetUniqueColors has 1 candidate\(s\), below baseline 2/); + assert.match(failures.join('\n'), /rustFallbackNearPairs\.nearTotal has 0 candidate\(s\), below baseline 1/); + } finally { + fs.rmSync(tempDir, { recursive: true, force: true }); + } +}); + +test('checkBaseline validates CLI baseline budget shape', () => { + const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'bitfun-cli-theme-audit-')); + try { + const baselinePath = path.join(tempDir, 'baseline.json'); + fs.writeFileSync(baselinePath, JSON.stringify({ + version: 1, + budgets: { + presetUniqueColors: null, + totalUniqueColors: {}, + }, + })); + + const failures = checkBaseline({ presetUniqueColors: 1, totalUniqueColors: 1 }, baselinePath); + + assert.match(failures.join('\n'), /presetUniqueColors budget must be an object/); + assert.match(failures.join('\n'), /totalUniqueColors\.max must be a number/); + } finally { + fs.rmSync(tempDir, { recursive: true, force: true }); + } +}); + +test('writeReportJson creates parent directories for report output', () => { + const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'bitfun-cli-theme-audit-')); + try { + const reportPath = path.join(tempDir, 'nested', 'report.json'); + writeReportJson({ totalUniqueColors: 1 }, reportPath); + + assert.deepEqual(JSON.parse(fs.readFileSync(reportPath, 'utf8')), { totalUniqueColors: 1 }); + } finally { + fs.rmSync(tempDir, { recursive: true, force: true }); + } +}); + +test('CLI audit can print a machine-readable JSON report', () => { + const stdout = execFileSync(process.execPath, [ + 'scripts/audit-cli-theme-colors.mjs', + '--json', + '--no-baseline', + '--top=0', + ], { + cwd: process.cwd(), + encoding: 'utf8', + }); + const report = JSON.parse(stdout); + + assert.equal(report.root, 'src/apps/cli'); + assert.equal(typeof report.totalUniqueColors, 'number'); + assert.equal(typeof report.runtimeTotalUniqueColors, 'number'); + assert.equal(typeof report.compatibilityPresetUniqueColors, 'number'); + assert.equal(Array.isArray(report.presetNearPairs.near), true); + assert.equal(Array.isArray(report.runtimePresetNearPairs.near), true); + assert.equal(Array.isArray(report.compatibilityPresetNearPairs.near), true); +}); diff --git a/scripts/audit-theme-colors.mjs b/scripts/audit-theme-colors.mjs index a68d9833a0..0bdc7c58ed 100644 --- a/scripts/audit-theme-colors.mjs +++ b/scripts/audit-theme-colors.mjs @@ -13,16 +13,19 @@ import { CONTRACT_VAR_DEFINITION_PATH_PARTS, DEFAULT_BASELINE_PATH, DEFAULT_ROOT, + DYNAMIC_VAR_FAMILY_CONTRACTS, EXCEPTION_PATH_PARTS, FALLBACK_VAR_CONTRACTS, REGISTERED_DYNAMIC_VAR_PREFIXES, RUNTIME_CONTRACT_VAR_DEFINITION_PATH_PARTS, STATIC_CONTRACT_VAR_DEFINITION_PATH_PARTS, + SURFACE_TOKEN_RENAME_CONTRACTS, TOKEN_COMPATIBILITY_ALIAS_CONTRACTS, TOKEN_COMPATIBILITY_ALIAS_FAMILY_CONTRACTS, TOKEN_ALIAS_SOURCE_PATH_PARTS, TOKEN_PATH_PARTS, } from './theme-css-var-contract.mjs'; +import { writeReportJson } from './theme-color-audit-utils.mjs'; const COLOR_PATTERN = /#[0-9a-fA-F]{3,8}\b|rgba?\(\s*[-+]?\d*\.?\d+\s*,\s*[-+]?\d*\.?\d+\s*,\s*[-+]?\d*\.?\d+(?:\s*,\s*(?:[-+]?\d*\.?\d+|var\([^)]+\)))?\s*\)|hsla?\(\s*[-+]?\d*\.?\d+(?:deg|rad|turn)?\s*,\s*[-+]?\d*\.?\d+%\s*,\s*[-+]?\d*\.?\d+%(?:\s*,\s*(?:[-+]?\d*\.?\d+|var\([^)]+\)))?\s*\)/g; @@ -34,6 +37,9 @@ const VAR_FALLBACK_PATTERN = /var\(\s*(--[a-zA-Z0-9_-]+)\s*,/g; const CSS_VAR_SET_PROPERTY_PATTERN = /\.setProperty\(\s*['"`](--[a-zA-Z0-9_-]+)/g; const CSS_VAR_INLINE_STYLE_PATTERN = /['"`](--[a-zA-Z0-9_-]+)['"`]\s*:/g; const CSS_VAR_DYNAMIC_SET_PATTERN = /\.setProperty\(\s*`(--[a-zA-Z0-9_-]*)\$\{/g; +const CSS_VAR_COLOR_RAMP_PATTERN = /colorRamp\(\s*['"`](--[a-zA-Z0-9_-]+)['"`]/g; +const CSS_VAR_LITERAL_PATTERN = /['"`](--[a-zA-Z0-9_-]+)['"`]/g; +const GENERATED_WIDGET_THEME_PAYLOAD_PATH = 'tools/generative-widget/themePayload.ts'; const REPORT_ROW_LIMIT = 100; const COLOR_DOMAIN_CONTRACT_BY_KEY = new Map(COLOR_DOMAIN_CONTRACTS.map(contract => [contract.key, contract])); const FALLBACK_VAR_CONTRACT_BY_KEY = new Map(FALLBACK_VAR_CONTRACTS.map(contract => [contract.key, contract])); @@ -170,6 +176,13 @@ function isAuditTestFile(relativePath) { ); } +function isGeneratedBuildArtifact(rootRelativePath) { + return ( + rootRelativePath === 'generated/version.ts' + || rootRelativePath === 'generated/version-injection.html' + ); +} + function isTokenFile(relativePath) { return TOKEN_PATH_PARTS.some(part => relativePath.includes(part)); } @@ -194,6 +207,54 @@ function isExceptionFile(relativePath) { return EXCEPTION_PATH_PARTS.some(part => relativePath.toLowerCase().includes(part.toLowerCase())); } +function isGeneratedWidgetThemePayloadFile(relativePath) { + return relativePath.endsWith(GENERATED_WIDGET_THEME_PAYLOAD_PATH); +} + +function collectGeneratedWidgetPayloadVarNames(content) { + const fallbackVars = new Map(); + const fallbackBlock = /const FALLBACK_VAR = \{([\s\S]*?)\} as const;/.exec(content)?.[1]; + if (fallbackBlock) { + for (const match of collectMatches(fallbackBlock, /\s+(\w+): ['"`](--[a-zA-Z0-9_-]+)['"`]/g)) { + fallbackVars.set(match[1], match[2]); + } + } + + const hasGroupsDeclaration = content.includes('WIDGET_THEME_VAR_GROUPS'); + const groupsBlock = /const WIDGET_THEME_VAR_GROUPS = \{([\s\S]*?)\n\} as const;/.exec(content)?.[1]; + if (!groupsBlock) { + if (hasGroupsDeclaration) { + throw new Error('Unable to parse generated widget WIDGET_THEME_VAR_GROUPS; refusing to audit a partial payload contract.'); + } + return collectMatches(content, CSS_VAR_LITERAL_PATTERN).map(match => match[1]); + } + + const names = []; + const groups = collectMatches(groupsBlock, /\n\s+\w+: \[([\s\S]*?)\n\s+\]/g); + if (groups.length === 0) { + throw new Error('Unable to parse generated widget WIDGET_THEME_VAR_GROUPS entries; refusing to audit a partial payload contract.'); + } + for (const group of groups) { + for (const line of group[1].split(/\r?\n/)) { + const fallbackRef = /FALLBACK_VAR\.(\w+)/.exec(line); + if (fallbackRef) { + const name = fallbackVars.get(fallbackRef[1]); + if (name) { + names.push(name); + } else { + throw new Error(`Unable to resolve generated widget payload fallback var ${fallbackRef[1]}.`); + } + continue; + } + const literal = /['"`](--[a-zA-Z0-9_-]+)['"`]/.exec(line); + if (literal) { + names.push(literal[1]); + } + } + } + return names; +} + function pathMatchesPart(relativePath, pathPart) { const normalizedPath = relativePath.toLowerCase(); const normalizedPart = pathPart.toLowerCase(); @@ -228,6 +289,22 @@ function collectMatches(content, pattern) { return Array.from(content.matchAll(pattern)); } +function contractOwnerMatchesRoot(contract, rootRelativePath) { + return String(contract.owner ?? '') + .split(';') + .map(owner => owner.trim()) + .filter(Boolean) + .some(owner => ( + owner === rootRelativePath + || owner.startsWith(`${rootRelativePath}/`) + || rootRelativePath.startsWith(`${owner}/`) + )); +} + +function colorRampPrefix(name) { + return name.endsWith('-') ? name : `${name}-`; +} + function previousNonWhitespace(chars) { for (let index = chars.length - 1; index >= 0; index -= 1) { const char = chars[index]; @@ -713,9 +790,25 @@ function applyBaseline(report, options) { function audit(options) { const root = path.resolve(options.root); const checksFullThemeSourceRoot = root === path.resolve(DEFAULT_ROOT); + const rootRelativePath = normalizePath(path.relative(process.cwd(), root)); const files = walkFiles(root); const cwd = process.cwd(); - const auditedFiles = files.filter(file => !isAuditTestFile(normalizePath(path.relative(cwd, file)))); + const fileEntries = files.map(file => ({ + file, + relativePath: normalizePath(path.relative(cwd, file)), + rootRelativePath: normalizePath(path.relative(root, file)), + })); + const ignoredTestFiles = fileEntries.filter(entry => isAuditTestFile(entry.relativePath)); + const ignoredGeneratedFiles = fileEntries.filter(entry => ( + !isAuditTestFile(entry.relativePath) + && isGeneratedBuildArtifact(entry.rootRelativePath) + )); + const auditedFiles = fileEntries + .filter(entry => ( + !isAuditTestFile(entry.relativePath) + && !isGeneratedBuildArtifact(entry.rootRelativePath) + )) + .map(entry => entry.file); const tokenAliasDefinitionsByColorKey = collectTokenAliasDefinitions(auditedFiles, cwd); const colorCounts = new Map(); @@ -739,9 +832,12 @@ function audit(options) { const tokenColorCounts = new Map(); const colorDomainCounts = new Map(); const colorDomainFiles = new Map(); + const colorDomainColorFiles = new Map(); const tokenAliasLiteralCounts = new Map(); const tokenAliasLiteralFiles = new Map(); const tokenAliasLiteralExamples = new Map(); + const generatedWidgetPayloadVarCounts = new Map(); + const generatedWidgetPayloadVarFiles = new Map(); let colorOccurrences = 0; let componentColorOccurrences = 0; @@ -766,6 +862,9 @@ function audit(options) { const domainCounts = colorDomainCounts.get(colorDomain) ?? new Map(); incrementMap(domainCounts, color); colorDomainCounts.set(colorDomain, domainCounts); + const domainColorFiles = colorDomainColorFiles.get(colorDomain) ?? new Map(); + addToSetMap(domainColorFiles, color, relativePath); + colorDomainColorFiles.set(colorDomain, domainColorFiles); if (tokenFile) { incrementMap(tokenColorCounts, color); } else if (exceptionFile) { @@ -828,6 +927,19 @@ function audit(options) { addToSetMap(dynamicDefinitionFiles, match[1], relativePath); } + for (const match of collectMatches(content, CSS_VAR_COLOR_RAMP_PATTERN)) { + const prefix = colorRampPrefix(match[1]); + dynamicDefinitionPrefixes.add(prefix); + addToSetMap(dynamicDefinitionFiles, prefix, relativePath); + } + + if (isGeneratedWidgetThemePayloadFile(relativePath)) { + for (const name of collectGeneratedWidgetPayloadVarNames(content)) { + incrementMap(generatedWidgetPayloadVarCounts, name); + addToSetMap(generatedWidgetPayloadVarFiles, name, relativePath); + } + } + for (const match of collectMatches(content, VAR_FALLBACK_PATTERN)) { fallbackOccurrences += 1; incrementMap(fallbackTokenCounts, match[1]); @@ -837,11 +949,17 @@ function audit(options) { const definedVars = new Set(varDefinitionCounts.keys()); const getDefinitionKinds = name => Array.from(varDefinitionKinds.get(name) ?? ['unknown']).sort(); + const getExplicitDefinitionKind = name => ( + definedVars.has(name) ? getDefinitionKinds(name).join('+') : null + ); + const getDynamicDefinitionPrefix = name => ( + Array.from(dynamicDefinitionPrefixes).find(prefix => name.startsWith(prefix)) ?? null + ); const getDefinitionKind = name => { if (definedVars.has(name)) { return getDefinitionKinds(name).join('+'); } - const dynamicPrefix = Array.from(dynamicDefinitionPrefixes).find(prefix => name.startsWith(prefix)); + const dynamicPrefix = getDynamicDefinitionPrefix(name); return dynamicPrefix ? `dynamic-family:${dynamicPrefix}*` : null; }; const unresolvedVarEntries = Array.from(varUsageCounts.entries()) @@ -869,6 +987,23 @@ function audit(options) { .filter(entry => entry.kind && entry.kind !== 'css') .sort((a, b) => b.count - a.count || a.key.localeCompare(b.key)) .slice(0, REPORT_ROW_LIMIT); + const requiresExactDynamicFamilyDefinitions = staticContractVarDefinitions.size > 0; + const dynamicFamilyUnexportedEntries = requiresExactDynamicFamilyDefinitions + ? Array.from(varUsageCounts.entries()) + .map(([key, count]) => { + const prefix = getDynamicDefinitionPrefix(key); + return { + key, + count, + prefix, + files: Array.from(varUsageFiles.get(key) ?? []).sort().slice(0, 5), + }; + }) + .filter(entry => entry.prefix && !definedVars.has(entry.key)) + .sort((a, b) => b.count - a.count || a.key.localeCompare(b.key)) + : []; + const dynamicFamilyUnexportedVars = dynamicFamilyUnexportedEntries + .slice(0, REPORT_ROW_LIMIT); const unregisteredDynamicFamilyEntries = Array.from(dynamicDefinitionPrefixes) .filter(prefix => !REGISTERED_DYNAMIC_VAR_PREFIXES.has(prefix)) .sort((a, b) => a.localeCompare(b)) @@ -877,7 +1012,9 @@ function audit(options) { files: Array.from(dynamicDefinitionFiles.get(prefix) ?? []).sort().slice(0, 5), })); const staleRegisteredDynamicFamilyEntries = checksFullThemeSourceRoot - ? Array.from(REGISTERED_DYNAMIC_VAR_PREFIXES) + ? DYNAMIC_VAR_FAMILY_CONTRACTS + .filter(contract => contractOwnerMatchesRoot(contract, rootRelativePath)) + .map(contract => contract.prefix) .filter(prefix => !dynamicDefinitionPrefixes.has(prefix)) .sort((a, b) => a.localeCompare(b)) .map(prefix => ({ key: prefix })) @@ -940,6 +1077,23 @@ function audit(options) { topColors: topEntries(counts, options.top), }]; })); + const colorDomainNearPairEntries = Object.fromEntries(COLOR_DOMAIN_KEYS.map(key => { + const counts = colorDomainCounts.get(key) ?? new Map(); + const filesByColor = colorDomainColorFiles.get(key) ?? new Map(); + return [key, buildNearColorPairs(counts, filesByColor)]; + })); + const specializedColorDomainKeys = COLOR_DOMAIN_KEYS.filter(key => key !== 'appUi'); + const colorDomainNearPairs = { + indistinguishableTotal: specializedColorDomainKeys.reduce( + (total, key) => total + colorDomainNearPairEntries[key].indistinguishableTotal, + 0, + ), + nearTotal: specializedColorDomainKeys.reduce( + (total, key) => total + colorDomainNearPairEntries[key].nearTotal, + 0, + ), + ...colorDomainNearPairEntries, + }; const fallbackVars = Array.from(fallbackTokenCounts.entries()) .sort((a, b) => b[1] - a[1] || a[0].localeCompare(b[0])) .map(([key, count]) => ({ @@ -996,6 +1150,89 @@ function audit(options) { files: entry.files, })) .sort((a, b) => a.key.localeCompare(b.key)); + const generatedWidgetPayloadVars = Array.from(generatedWidgetPayloadVarCounts.entries()) + .sort((a, b) => b[1] - a[1] || a[0].localeCompare(b[0])) + .map(([key, count]) => ({ + key, + count, + definitionKind: getExplicitDefinitionKind(key), + files: Array.from(generatedWidgetPayloadVarFiles.get(key) ?? []).sort().slice(0, 5), + })); + const generatedWidgetPayloadCompatibilityAliases = generatedWidgetPayloadVars + .map(entry => { + const contract = resolveCompatibilityAliasContract(entry.key); + if (!contract || contract.familyPrefix) { + return null; + } + return { + ...entry, + canonical: contract.canonical, + canonicalDefinitionKind: getExplicitDefinitionKind(contract.canonical), + familyPrefix: null, + canonicalPrefix: null, + owner: contract.owner, + reason: contract.reason, + removal: contract.removal, + internalUsageCount: varUsageCounts.get(entry.key) ?? 0, + }; + }) + .filter(Boolean) + .sort((a, b) => b.count - a.count || a.key.localeCompare(b.key)); + const generatedWidgetPayloadCompatibilityFamilies = generatedWidgetPayloadVars + .map(entry => { + const contract = resolveCompatibilityAliasContract(entry.key); + if (!contract?.familyPrefix) { + return null; + } + return { + ...entry, + canonical: contract.canonical, + familyPrefix: contract.familyPrefix, + canonicalPrefix: contract.canonicalPrefix, + canonicalDefinitionKind: getExplicitDefinitionKind(contract.canonical), + owner: contract.owner, + reason: contract.reason, + removal: contract.removal, + internalUsageCount: varUsageCounts.get(entry.key) ?? 0, + }; + }) + .filter(Boolean) + .sort((a, b) => b.count - a.count || a.key.localeCompare(b.key)); + const generatedWidgetPayloadExternalOnlyCompatibility = [ + ...generatedWidgetPayloadCompatibilityAliases, + ...generatedWidgetPayloadCompatibilityFamilies, + ] + .filter(entry => entry.internalUsageCount === 0) + .map(entry => ({ + key: entry.key, + canonical: entry.canonical, + familyPrefix: entry.familyPrefix, + canonicalPrefix: entry.canonicalPrefix, + count: entry.count, + owner: entry.owner, + reason: entry.reason, + removal: entry.removal, + canonicalDefinitionKind: entry.canonicalDefinitionKind, + })) + .sort((a, b) => a.key.localeCompare(b.key)); + const generatedWidgetPayloadUndefinedVars = generatedWidgetPayloadVars + .filter(entry => !entry.definitionKind) + .sort((a, b) => a.key.localeCompare(b.key)); + const generatedWidgetPayloadVarNames = new Set(generatedWidgetPayloadVars.map(entry => entry.key)); + const generatedWidgetPayloadMissingCompatibilityCanonicals = [ + ...generatedWidgetPayloadCompatibilityAliases, + ...generatedWidgetPayloadCompatibilityFamilies, + ] + .filter(entry => !entry.canonicalDefinitionKind) + .map(({ key, canonical, count, files }) => ({ key, canonical, count, files })) + .sort((a, b) => a.key.localeCompare(b.key)); + const generatedWidgetPayloadUnexportedCompatibilityCanonicals = [ + ...generatedWidgetPayloadCompatibilityAliases, + ...generatedWidgetPayloadCompatibilityFamilies, + ] + .filter(entry => entry.canonicalDefinitionKind && !generatedWidgetPayloadVarNames.has(entry.canonical)) + .map(({ key, canonical, count, files }) => ({ key, canonical, count, files })) + .sort((a, b) => a.key.localeCompare(b.key)); const staleCompatibilityAliasEntries = checksFullThemeSourceRoot ? TOKEN_COMPATIBILITY_ALIAS_CONTRACTS .map(contract => ({ @@ -1009,7 +1246,7 @@ function audit(options) { : []; const staleCompatibilityAliasFamilyEntries = checksFullThemeSourceRoot ? compatibilityAliasFamilyEntries - .filter(entry => !entry.defined || !entry.canonicalDefined) + .filter(entry => !entry.canonicalDefined) .map(entry => ({ key: entry.key, canonical: entry.canonical })) : []; const uncontractedFallbackVars = fallbackVars @@ -1041,11 +1278,41 @@ function audit(options) { )) .map(([key, scope]) => ({ key, count: scope.occurrences })) .sort((a, b) => a.key.localeCompare(b.key)); + const surfaceTokenRenameEntries = SURFACE_TOKEN_RENAME_CONTRACTS + .map(contract => { + const usageCount = varUsageCounts.get(contract.key) ?? 0; + const definitionCount = varDefinitionCounts.get(contract.key) ?? 0; + const files = new Set([ + ...Array.from(varUsageFiles.get(contract.key) ?? []), + ...Array.from(varDefinitionFiles.get(contract.key) ?? []), + ]); + return { + key: contract.key, + canonical: contract.canonical, + usageCount, + definitionCount, + count: usageCount + definitionCount, + files: Array.from(files).sort().slice(0, 5), + }; + }) + .filter(entry => entry.count > 0) + .sort((a, b) => b.count - a.count || a.key.localeCompare(b.key)); + const missingSurfaceTokenRenameCanonicalEntries = checksFullThemeSourceRoot + ? SURFACE_TOKEN_RENAME_CONTRACTS + .map(contract => ({ + key: contract.key, + canonical: contract.canonical, + canonicalDefinitionKind: getDefinitionKind(contract.canonical), + })) + .filter(entry => !entry.canonicalDefinitionKind) + .sort((a, b) => a.key.localeCompare(b.key)) + : []; return { root: normalizePath(path.relative(cwd, root)) || '.', filesScanned: auditedFiles.length, - ignoredTestFiles: files.length - auditedFiles.length, + ignoredTestFiles: ignoredTestFiles.length, + ignoredGeneratedFiles: ignoredGeneratedFiles.length, filesWithColors: fileColorCounts.size, colorOccurrences, uniqueColors: colorCounts.size, @@ -1065,6 +1332,7 @@ function audit(options) { }, }, colorDomainScopes, + colorDomainNearPairs, componentColorOccurrences, componentFilesWithColors: componentFileColorCounts.size, uniqueComponentColors, @@ -1102,6 +1370,37 @@ function audit(options) { top: compatibilityAliasEntries.slice(0, options.top), families: compatibilityAliasFamilyEntries, }, + generatedWidgetPayload: { + varUnique: generatedWidgetPayloadVars.length, + occurrences: generatedWidgetPayloadVars.reduce((total, entry) => total + entry.count, 0), + undefinedUnique: generatedWidgetPayloadUndefinedVars.length, + compatibilityAliasUnique: generatedWidgetPayloadCompatibilityAliases.length, + compatibilityAliasOccurrences: generatedWidgetPayloadCompatibilityAliases.reduce( + (total, entry) => total + entry.count, + 0, + ), + compatibilityAliasFamilyUnique: generatedWidgetPayloadCompatibilityFamilies.length, + compatibilityAliasFamilyOccurrences: generatedWidgetPayloadCompatibilityFamilies.reduce( + (total, entry) => total + entry.count, + 0, + ), + externalOnlyCompatibilityUnique: generatedWidgetPayloadExternalOnlyCompatibility.length, + externalOnlyCompatibilityOccurrences: generatedWidgetPayloadExternalOnlyCompatibility.reduce( + (total, entry) => total + entry.count, + 0, + ), + missingCompatibilityCanonicalUnique: generatedWidgetPayloadMissingCompatibilityCanonicals.length, + unexportedCompatibilityCanonicalUnique: generatedWidgetPayloadUnexportedCompatibilityCanonicals.length, + topCompatibilityAliases: generatedWidgetPayloadCompatibilityAliases.slice(0, options.top), + topCompatibilityFamilies: generatedWidgetPayloadCompatibilityFamilies.slice(0, options.top), + externalOnlyCompatibility: generatedWidgetPayloadExternalOnlyCompatibility.slice(0, REPORT_ROW_LIMIT), + undefinedVars: generatedWidgetPayloadUndefinedVars.slice(0, REPORT_ROW_LIMIT), + missingCompatibilityCanonicals: generatedWidgetPayloadMissingCompatibilityCanonicals.slice(0, REPORT_ROW_LIMIT), + unexportedCompatibilityCanonicals: generatedWidgetPayloadUnexportedCompatibilityCanonicals.slice( + 0, + REPORT_ROW_LIMIT, + ), + }, staleCompatibilityAliases: staleCompatibilityAliasEntries, staleCompatibilityAliasFamilies: staleCompatibilityAliasFamilyEntries, missingCompatibilityAliasCanonicals: missingCompatibilityAliasCanonicalEntries, @@ -1114,6 +1413,14 @@ function audit(options) { missingColorDomainContracts: missingColorDomainContractEntries, staleColorDomainContracts: staleColorDomainContractEntries, activeUncontractedColorDomains: activeUncontractedColorDomainEntries, + surfaceTokenRenames: { + registeredUnique: SURFACE_TOKEN_RENAME_CONTRACTS.length, + activeUnique: surfaceTokenRenameEntries.length, + activeOccurrences: surfaceTokenRenameEntries.reduce((total, entry) => total + entry.count, 0), + missingCanonicalUnique: missingSurfaceTokenRenameCanonicalEntries.length, + active: surfaceTokenRenameEntries.slice(0, REPORT_ROW_LIMIT), + missingCanonicals: missingSurfaceTokenRenameCanonicalEntries.slice(0, REPORT_ROW_LIMIT), + }, tokenAliasLiterals: { occurrences: sumMapValues(tokenAliasLiteralCounts), uniqueColors: tokenAliasLiteralCounts.size, @@ -1126,6 +1433,7 @@ function audit(options) { staticContractDefinedUnique: staticContractVarDefinitions.size, runtimeContractDefinedUnique: runtimeContractVarDefinitions.size, dynamicFamilyPrefixes: Array.from(dynamicDefinitionPrefixes).sort(), + dynamicFamilyUnexportedUnique: dynamicFamilyUnexportedEntries.length, unregisteredDynamicFamilyUnique: unregisteredDynamicFamilyEntries.length, staleRegisteredDynamicFamilyUnique: staleRegisteredDynamicFamilyEntries.length, unresolvedUnique: unresolvedVarEntries.length, @@ -1137,6 +1445,7 @@ function audit(options) { nonContractCssPrivateUnique: nonContractCssPrivateEntries.length, }, dynamicDefinedVars, + dynamicFamilyUnexportedVars, unregisteredDynamicFamilies: unregisteredDynamicFamilyEntries, staleRegisteredDynamicFamilies: staleRegisteredDynamicFamilyEntries, nonContractDefinedVars, @@ -1162,6 +1471,7 @@ function printText(report) { console.log(`Theme color audit: ${report.root}`); console.log(`Files scanned: ${report.filesScanned}`); console.log(`Ignored test files: ${report.ignoredTestFiles}`); + console.log(`Ignored generated files: ${report.ignoredGeneratedFiles}`); console.log(`Files with colors: ${report.filesWithColors}`); console.log(`Color occurrences: ${report.colorOccurrences}`); console.log(`Unique colors: ${report.uniqueColors}`); @@ -1183,6 +1493,15 @@ function printText(report) { `staleFamilies=${report.compatibilityAliases.staleRegisteredFamilyUnique}, ` + `missingCanonicals=${report.compatibilityAliases.missingCanonicalUnique}` ); + console.log( + `Generated widget payload: vars=${report.generatedWidgetPayload.varUnique}, ` + + `undefined=${report.generatedWidgetPayload.undefinedUnique}, ` + + `compatAliases=${report.generatedWidgetPayload.compatibilityAliasUnique}, ` + + `compatAliasFamilies=${report.generatedWidgetPayload.compatibilityAliasFamilyUnique}, ` + + `externalOnlyCompat=${report.generatedWidgetPayload.externalOnlyCompatibilityUnique}, ` + + `missingCompatCanonicals=${report.generatedWidgetPayload.missingCompatibilityCanonicalUnique}, ` + + `unexportedCompatCanonicals=${report.generatedWidgetPayload.unexportedCompatibilityCanonicalUnique}` + ); console.log( `Fallback contracts: registered=${report.fallbackContracts.registeredUnique}, ` + `uncontracted=${report.fallbackContracts.uncontractedUnique}, ` + @@ -1194,6 +1513,12 @@ function printText(report) { `stale=${report.colorDomainContracts.staleRegisteredUnique}, ` + `activeUncontracted=${report.colorDomainContracts.activeUncontractedUnique}` ); + console.log( + `Surface token renames: registered=${report.surfaceTokenRenames.registeredUnique}, ` + + `active=${report.surfaceTokenRenames.activeUnique}, ` + + `occurrences=${report.surfaceTokenRenames.activeOccurrences}, ` + + `missingCanonicals=${report.surfaceTokenRenames.missingCanonicalUnique}` + ); console.log('\nTop colors:'); console.log(printRows(report.topColors)); @@ -1215,6 +1540,38 @@ function printText(report) { ); } + console.log('\nSpecialized color-domain near pairs:'); + let printedDomainNearPairs = false; + for (const key of COLOR_DOMAIN_KEYS.filter(domainKey => domainKey !== 'appUi')) { + const pairs = report.colorDomainNearPairs[key]; + if (!pairs || (pairs.indistinguishableTotal === 0 && pairs.nearTotal === 0)) { + continue; + } + printedDomainNearPairs = true; + console.log( + ` ${COLOR_DOMAIN_LABELS[key].padEnd(18)} ` + + `indistinguishable=${pairs.indistinguishableTotal.toString().padStart(4)} ` + + `near=${pairs.nearTotal.toString().padStart(4)}` + ); + for (const pair of pairs.indistinguishable.slice(0, 3)) { + console.log( + ` indistinguishable ${pair.a} <-> ${pair.b} ` + + `distance=${pair.distance.toFixed(2)} alphaDiff=${pair.alphaDiff.toFixed(3)} ` + + `combined=${pair.count}` + ); + } + for (const pair of pairs.near.slice(0, 3)) { + console.log( + ` near ${pair.a} <-> ${pair.b} ` + + `distance=${pair.distance.toFixed(2)} alphaDiff=${pair.alphaDiff.toFixed(3)} ` + + `combined=${pair.count}` + ); + } + } + if (!printedDomainNearPairs) { + console.log(' none'); + } + console.log('\nTop files:'); console.log(printRows(report.topFiles)); @@ -1280,6 +1637,70 @@ function printText(report) { } } + console.log('\nGenerated widget payload compatibility aliases:'); + if (report.generatedWidgetPayload.topCompatibilityAliases.length === 0) { + console.log(' none'); + } else { + for (const row of report.generatedWidgetPayload.topCompatibilityAliases.slice(0, 10)) { + console.log( + ` ${row.count.toString().padStart(5)} ${row.key} -> ${row.canonical} ` + + `canonicalDefined=${Boolean(row.canonicalDefinitionKind)}` + ); + } + } + + console.log('\nGenerated widget payload compatibility families:'); + if (report.generatedWidgetPayload.topCompatibilityFamilies.length === 0) { + console.log(' none'); + } else { + for (const row of report.generatedWidgetPayload.topCompatibilityFamilies.slice(0, 10)) { + console.log( + ` ${row.count.toString().padStart(5)} ${row.key} -> ${row.canonical} ` + + `canonicalDefined=${Boolean(row.canonicalDefinitionKind)}` + ); + } + } + + console.log('\nGenerated widget payload external-only compatibility:'); + if (report.generatedWidgetPayload.externalOnlyCompatibility.length === 0) { + console.log(' none'); + } else { + for (const row of report.generatedWidgetPayload.externalOnlyCompatibility.slice(0, 10)) { + const family = row.familyPrefix ? ` family=${row.familyPrefix}*` : ''; + console.log( + ` ${row.count.toString().padStart(5)} ${row.key} -> ${row.canonical}${family} ` + + `canonicalDefined=${Boolean(row.canonicalDefinitionKind)}` + ); + } + } + + console.log('\nGenerated widget payload undefined vars:'); + console.log(printRows(report.generatedWidgetPayload.undefinedVars.slice(0, 10))); + + console.log('\nGenerated widget payload missing compatibility canonicals:'); + if (report.generatedWidgetPayload.missingCompatibilityCanonicals.length === 0) { + console.log(' none'); + } else { + for (const row of report.generatedWidgetPayload.missingCompatibilityCanonicals.slice(0, 10)) { + console.log( + ` ${row.key} -> ${row.canonical} ` + + `count=${row.count} files=${row.files.join(', ')}` + ); + } + } + + console.log('\nGenerated widget payload unexported compatibility canonicals:'); + if (report.generatedWidgetPayload.unexportedCompatibilityCanonicals.length === 0) { + console.log(' none'); + } else { + for (const row of report.generatedWidgetPayload.unexportedCompatibilityCanonicals.slice(0, 10)) { + console.log( + ` ${row.key} -> ${row.canonical} ` + + `count=${row.count} files=${row.files.join(', ')}` + ); + } + } + console.log('\nColor domain contract gaps:'); const colorDomainGapRows = [ ...report.missingColorDomainContracts.map(row => ({ ...row, count: 1 })), @@ -1288,6 +1709,28 @@ function printText(report) { ]; console.log(printRows(colorDomainGapRows.slice(0, 10))); + console.log('\nSurface token rename debt:'); + if (report.surfaceTokenRenames.active.length === 0) { + console.log(' none'); + } else { + for (const row of report.surfaceTokenRenames.active.slice(0, 10)) { + console.log( + ` ${row.key} -> ${row.canonical} ` + + `count=${row.count} definitions=${row.definitionCount} usages=${row.usageCount} ` + + `files=${row.files.join(', ')}` + ); + } + } + + console.log('\nSurface token rename missing canonicals:'); + if (report.surfaceTokenRenames.missingCanonicals.length === 0) { + console.log(' none'); + } else { + for (const row of report.surfaceTokenRenames.missingCanonicals.slice(0, 10)) { + console.log(` ${row.key} -> ${row.canonical}`); + } + } + console.log('\nTop token-equivalent app literals:'); if (report.tokenAliasLiterals.top.length === 0) { console.log(' none'); @@ -1314,6 +1757,7 @@ function printText(report) { `fallbackOnly=${report.cssVarDefinitions.fallbackOnlyUnique}, ` + `requiredMissing=${report.cssVarDefinitions.unresolvedRequiredUnique}, ` + `runtimeOnlyRequired=${report.cssVarDefinitions.runtimeOnlyRequiredContractUnique}, ` + + `dynamicFamilyUnexported=${report.cssVarDefinitions.dynamicFamilyUnexportedUnique}, ` + `nonContractCrossFile=${report.cssVarDefinitions.nonContractCrossFileUnique}, ` + `nonContractDynamicInputs=${report.cssVarDefinitions.nonContractDynamicInputUnique}, ` + `nonContractCssPrivate=${report.cssVarDefinitions.nonContractCssPrivateUnique}` @@ -1339,6 +1783,18 @@ function printText(report) { console.log('\nStale registered dynamic CSS var families:'); console.log(printRows(report.staleRegisteredDynamicFamilies.slice(0, 10).map(row => ({ ...row, count: 1 })))); + console.log('\nDynamic-family CSS vars without exact export (top):'); + if (report.dynamicFamilyUnexportedVars.length === 0) { + console.log(' none'); + } else { + for (const row of report.dynamicFamilyUnexportedVars.slice(0, 10)) { + console.log( + ` ${row.count.toString().padStart(5)} ${row.key} ` + + `prefix=${row.prefix} files=${row.files.join(', ')}` + ); + } + } + console.log('\nFallback-only unresolved CSS vars (top):'); console.log(printRows(report.fallbackOnlyVars.slice(0, 10))); @@ -1424,9 +1880,7 @@ try { const report = audit(options); const baselineSummary = applyBaseline(report, options); if (options.reportJson) { - const reportJsonPath = path.resolve(options.reportJson); - fs.mkdirSync(path.dirname(reportJsonPath), { recursive: true }); - fs.writeFileSync(reportJsonPath, `${JSON.stringify(report, null, 2)}\n`, 'utf8'); + writeReportJson(report, options.reportJson); } if (options.json) { console.log(JSON.stringify(report, null, 2)); diff --git a/scripts/audit-theme-colors.test.mjs b/scripts/audit-theme-colors.test.mjs index 60aa6f98c2..35e0f61db5 100644 --- a/scripts/audit-theme-colors.test.mjs +++ b/scripts/audit-theme-colors.test.mjs @@ -11,11 +11,45 @@ import { COLOR_DOMAIN_RULES, DYNAMIC_VAR_FAMILY_CONTRACTS, FALLBACK_VAR_CONTRACTS, + SURFACE_TOKEN_RENAME_CONTRACTS, TOKEN_COMPATIBILITY_ALIAS_CONTRACTS, TOKEN_COMPATIBILITY_ALIAS_FAMILY_CONTRACTS, } from './theme-css-var-contract.mjs'; const root = process.cwd(); +const SOURCE_OWNER_ROOTS = [ + 'BitFun-Installer/src', + 'src/mobile-web/src', + 'src/web-ui/src', +]; +const NEAR_PAIR_DECISION_AUDIT_ROOTS = [ + { root: 'src/web-ui/src', args: ['--json', '--no-baseline', '--top', '0'] }, + { root: 'src/mobile-web/src', args: ['--root', 'src/mobile-web/src', '--json', '--no-baseline', '--top', '0'] }, + { root: 'BitFun-Installer/src', args: ['--root', 'BitFun-Installer/src', '--json', '--no-baseline', '--top', '0'] }, +]; + +function contractOwnerHasKnownSource(owner) { + return String(owner ?? '') + .split(';') + .map(entry => entry.trim()) + .filter(Boolean) + .some(entry => SOURCE_OWNER_ROOTS.some(sourceRoot => ( + entry === sourceRoot + || entry.startsWith(`${sourceRoot}/`) + ))); +} + +function contractOwnerMatchesRoot(contract, sourceRoot) { + return String(contract.owner ?? '') + .split(';') + .map(entry => entry.trim()) + .filter(Boolean) + .some(entry => ( + entry === sourceRoot + || entry.startsWith(`${sourceRoot}/`) + || sourceRoot.startsWith(`${entry}/`) + )); +} function writeText(filePath, content) { fs.mkdirSync(path.dirname(filePath), { recursive: true }); @@ -26,6 +60,35 @@ function readJson(filePath) { return JSON.parse(fs.readFileSync(filePath, 'utf8')); } +function readText(filePath) { + return fs.readFileSync(filePath, 'utf8'); +} + +function collectRepositoryNearPairRows(sourceRoot, report) { + return COLOR_DOMAIN_KEYS.flatMap((domain) => { + const pairs = report.colorDomainNearPairs?.[domain]; + if (!pairs) { + return []; + } + return [ + ...(pairs.indistinguishable ?? []), + ...(pairs.near ?? []), + ].map(pair => ({ + root: sourceRoot, + domain, + key: pair.key, + })); + }).sort((left, right) => ( + left.root.localeCompare(right.root) + || left.domain.localeCompare(right.domain) + || left.key.localeCompare(right.key) + )); +} + +function formatNearPairDecisionKey(row) { + return `${row.root}:${row.domain}:${row.key}`; +} + function createFixture(files) { const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'bitfun-theme-audit-')); const sourceRoot = path.join(dir, 'src', 'web-ui', 'src'); @@ -60,7 +123,7 @@ test('theme CSS var contract registry is explicit and non-overlapping', () => { ); for (const contract of DYNAMIC_VAR_FAMILY_CONTRACTS) { assert.match(contract.prefix, /^--[a-z0-9-]+-$/); - assert.ok(contract.owner.includes('src/web-ui/src/'), `${contract.prefix} must name a source owner`); + assert.ok(contractOwnerHasKnownSource(contract.owner), `${contract.prefix} must name a source owner`); assert.ok(contract.reason.trim().length >= 20, `${contract.prefix} must explain why it is dynamic`); if (contract.canonicalPrefix !== undefined) { assert.match(contract.canonicalPrefix, /^--[a-z0-9-]+-$/); @@ -122,27 +185,207 @@ test('theme CSS var contract registry is explicit and non-overlapping', () => { assert.ok(contract.reason.trim().length >= 30, `${contract.key} must explain why fallback is intentional`); assert.ok(contract.boundary.trim().length >= 10, `${contract.key} must classify the fallback boundary`); } + + const surfaceRenameKeys = new Set(SURFACE_TOKEN_RENAME_CONTRACTS.map(contract => contract.key)); + assert.equal( + surfaceRenameKeys.size, + SURFACE_TOKEN_RENAME_CONTRACTS.length, + 'surface token rename contracts must be unique', + ); + for (const contract of SURFACE_TOKEN_RENAME_CONTRACTS) { + assert.match(contract.key, /^--[a-z0-9-]+$/); + assert.match(contract.canonical, /^--[a-z0-9-]+$/); + assert.notEqual(contract.key, contract.canonical, `${contract.key} must point to a different canonical token`); + assert.ok(contract.owner.includes('src/web-ui/src/'), `${contract.key} must name a source owner`); + assert.ok(contract.reason.trim().length >= 30, `${contract.key} must explain the rename boundary`); + } }); test('repository dynamic CSS var families match the registered contract', () => { - const result = runAudit(['--json', '--no-baseline']); - assert.equal(result.status, 0, result.stderr || result.stdout); + for (const sourceRoot of SOURCE_OWNER_ROOTS) { + const result = runAudit(['--root', sourceRoot, '--json', '--no-baseline']); + assert.equal(result.status, 0, result.stderr || result.stdout); - const report = JSON.parse(result.stdout); - const registeredPrefixes = DYNAMIC_VAR_FAMILY_CONTRACTS - .map(contract => contract.prefix) - .sort(); - assert.deepEqual(report.cssVarDefinitions.dynamicFamilyPrefixes, registeredPrefixes); - assert.equal(report.cssVarDefinitions.unregisteredDynamicFamilyUnique, 0); - assert.equal(report.cssVarDefinitions.staleRegisteredDynamicFamilyUnique, 0); - assert.equal(report.compatibilityAliases.staleRegisteredUnique, 0); - assert.equal(report.compatibilityAliases.staleRegisteredFamilyUnique, 0); - assert.equal(report.compatibilityAliases.missingCanonicalUnique, 0); - assert.equal(report.fallbackContracts.uncontractedUnique, 0); - assert.equal(report.fallbackContracts.staleRegisteredUnique, 0); - assert.equal(report.colorDomainContracts.missingRegisteredUnique, 0); - assert.equal(report.colorDomainContracts.staleRegisteredUnique, 0); - assert.equal(report.colorDomainContracts.activeUncontractedUnique, 0); + const report = JSON.parse(result.stdout); + const registeredPrefixes = DYNAMIC_VAR_FAMILY_CONTRACTS + .filter(contract => contractOwnerMatchesRoot(contract, sourceRoot)) + .map(contract => contract.prefix) + .sort(); + assert.deepEqual(report.cssVarDefinitions.dynamicFamilyPrefixes, registeredPrefixes); + assert.equal(report.cssVarDefinitions.unregisteredDynamicFamilyUnique, 0); + assert.equal(report.cssVarDefinitions.staleRegisteredDynamicFamilyUnique, 0); + assert.equal(report.compatibilityAliases.staleRegisteredUnique, 0); + assert.equal(report.compatibilityAliases.staleRegisteredFamilyUnique, 0); + assert.equal(report.compatibilityAliases.missingCanonicalUnique, 0); + assert.equal(report.generatedWidgetPayload.undefinedUnique, 0); + assert.equal(report.generatedWidgetPayload.missingCompatibilityCanonicalUnique, 0); + assert.equal(report.generatedWidgetPayload.unexportedCompatibilityCanonicalUnique, 0); + assert.equal(report.fallbackContracts.uncontractedUnique, 0); + assert.equal(report.fallbackContracts.staleRegisteredUnique, 0); + assert.equal(report.colorDomainContracts.missingRegisteredUnique, 0); + assert.equal(report.colorDomainContracts.staleRegisteredUnique, 0); + assert.equal(report.colorDomainContracts.activeUncontractedUnique, 0); + assert.equal(report.surfaceTokenRenames.activeUnique, 0); + assert.equal(report.surfaceTokenRenames.activeOccurrences, 0); + assert.equal(report.surfaceTokenRenames.missingCanonicalUnique, 0); + } +}); + +test('plugin theme projection stays compact and isolated from runtime/widget contracts', () => { + const projectionSource = readText(path.join(root, 'src/web-ui/src/infrastructure/theme/pluginThemeProjection.ts')); + const publicIndexSource = readText(path.join(root, 'src/web-ui/src/infrastructure/theme/index.ts')); + const keyListMatch = projectionSource.match(/PLUGIN_THEME_COLOR_KEYS\s*=\s*\[([\s\S]*?)\]\s+as const;/); + assert.ok(keyListMatch, 'plugin projection must declare PLUGIN_THEME_COLOR_KEYS'); + const keyMatches = Array.from( + keyListMatch[1].matchAll(/'([a-z]+)'/g), + match => match[1], + ); + + assert.deepEqual(keyMatches, [ + 'primary', + 'secondary', + 'accent', + 'success', + 'warning', + 'error', + 'info', + ]); + assert.equal(keyMatches.length, 7, 'plugin projection must stay within the documented key cap'); + assert.doesNotMatch( + projectionSource, + /ThemeService|themePayload|WIDGET_THEME|getComputedStyle|setProperty|--[a-z0-9-]+/i, + 'plugin projection must not become a runtime CSS var or generated widget schema', + ); + assert.match(publicIndexSource, /createPluginThemeColorProjection/); + assert.doesNotMatch( + publicIndexSource, + /themePayload|WIDGET_THEME/i, + 'theme public index must not expose generated widget payload as plugin theme API', + ); +}); + +test('repository specialized near color pairs have explicit decisions', () => { + const reportedRows = NEAR_PAIR_DECISION_AUDIT_ROOTS.flatMap(({ root: sourceRoot, args }) => { + const result = runAudit(args); + assert.equal(result.status, 0, result.stderr || result.stdout); + + return collectRepositoryNearPairRows(sourceRoot, JSON.parse(result.stdout)); + }); + const decisions = readJson(path.join(root, 'scripts/theme-color-near-pair-decisions.json')); + assert.equal(decisions.version, 1); + assert.ok(Array.isArray(decisions.decisions)); + + const reportedKeys = new Set(reportedRows.map(formatNearPairDecisionKey)); + const decisionKeys = new Set(decisions.decisions.map(formatNearPairDecisionKey)); + assert.equal(decisionKeys.size, decisions.decisions.length, 'near-pair decisions must be unique by root, domain, and key'); + const missing = reportedRows + .filter(row => !decisionKeys.has(formatNearPairDecisionKey(row))) + .map(formatNearPairDecisionKey); + const stale = decisions.decisions + .filter(row => !reportedKeys.has(formatNearPairDecisionKey(row))) + .map(formatNearPairDecisionKey); + + assert.deepEqual(missing, [], 'new specialized near pairs require an explicit merge/keep/defer decision'); + assert.deepEqual(stale, [], 'retired specialized near-pair decisions must be removed with the lowered baseline'); + for (const decision of decisions.decisions) { + assert.ok(SOURCE_OWNER_ROOTS.includes(decision.root), `${decision.key} must name a scanned root`); + assert.ok(['merge', 'keep', 'defer'].includes(decision.decision), `${decision.key} has an invalid decision`); + assert.ok(String(decision.owner).trim().length > 10, `${decision.key} must name an owner`); + assert.ok(fs.existsSync(path.join(root, decision.owner)), `${decision.key} owner must exist in the repository`); + assert.ok(String(decision.reason).trim().length >= 60, `${decision.key} must explain the product/design reason`); + assert.ok(String(decision.reevaluateWhen).trim().length >= 30, `${decision.key} must define reevaluation criteria`); + } +}); + +test('generated widget iframe compatibility aliases stay outside root/runtime contracts', () => { + const source = readText(path.join(root, 'src/web-ui/src/tools/generative-widget/themePayloadCompatibility.ts')); + const aliasEntries = Array.from(source.matchAll(/'([^']+)': '([^']+)'/g)) + .map(([, key, canonical]) => [key, canonical]); + const aliasKeys = new Set(aliasEntries.map(([key]) => key)); + const explicitContractKeys = new Set(TOKEN_COMPATIBILITY_ALIAS_CONTRACTS.map(contract => contract.key)); + const resolveFamilyContract = (key) => { + const family = TOKEN_COMPATIBILITY_ALIAS_FAMILY_CONTRACTS.find(contract => ( + key.startsWith(contract.prefix) && key.length > contract.prefix.length + )); + if (!family) { + return null; + } + return { + key, + canonical: `${family.canonicalPrefix}${key.slice(family.prefix.length)}`, + }; + }; + + assert.ok(aliasEntries.length > 0, 'widget iframe compatibility aliases must be explicit'); + assert.equal(aliasKeys.size, aliasEntries.length, 'widget iframe compatibility aliases must be unique'); + assert.equal(explicitContractKeys.size, 0, 'explicit root/runtime compatibility alias contracts must remain retired'); + for (const [key, canonical] of aliasEntries) { + const familyContract = resolveFamilyContract(key); + if (familyContract) { + assert.equal(canonical, familyContract.canonical, `${key} must point to the registered family canonical token`); + } else { + assert.ok(!explicitContractKeys.has(key), `${key} must not re-enter root/runtime compatibility alias contracts`); + assert.match(canonical, /^--[a-z0-9-]+$/); + assert.notEqual(key, canonical, `${key} must point to a different canonical token`); + } + } +}); + +test('retired explicit iframe aliases do not reappear outside the compatibility boundary', () => { + const source = readText(path.join(root, 'src/web-ui/src/tools/generative-widget/themePayloadCompatibility.ts')); + const retiredExplicitAliases = Array.from(source.matchAll(/'([^']+)': '([^']+)'/g)) + .map(([, key]) => key) + .filter(key => !TOKEN_COMPATIBILITY_ALIAS_FAMILY_CONTRACTS.some(contract => ( + key.startsWith(contract.prefix) && key.length > contract.prefix.length + ))); + const allowedFiles = new Set([ + 'docs/architecture/theme-token-optimization.md', + 'src/web-ui/src/tools/generative-widget/themePayloadCompatibility.ts', + 'src/web-ui/src/tools/generative-widget/themePayload.test.ts', + ]); + const allowedExtensions = new Set(['.ts', '.tsx', '.scss', '.css', '.mjs', '.json']); + const searchRoots = ['docs', 'scripts', 'src/web-ui/src']; + const hits = []; + + const scanFile = (filePath) => { + const relativePath = path.relative(root, filePath).replace(/\\/g, '/'); + if (allowedFiles.has(relativePath) || !allowedExtensions.has(path.extname(filePath))) { + return; + } + const text = readText(filePath); + for (const key of retiredExplicitAliases) { + let index = text.indexOf(key); + while (index !== -1) { + const before = text[index - 1] || ''; + const after = text[index + key.length] || ''; + if (!/[a-zA-Z0-9_-]/.test(before) && !/[a-zA-Z0-9_-]/.test(after)) { + const line = text.slice(0, index).split(/\r?\n/).length; + hits.push(`${relativePath}:${line}: ${key}`); + } + index = text.indexOf(key, index + key.length); + } + } + }; + + const walk = (dir) => { + for (const entry of fs.readdirSync(dir, { withFileTypes: true })) { + if (entry.name === 'node_modules' || entry.name === '.git' || entry.name === 'dist' || entry.name === 'target') { + continue; + } + const entryPath = path.join(dir, entry.name); + if (entry.isDirectory()) { + walk(entryPath); + } else { + scanFile(entryPath); + } + } + }; + + for (const searchRoot of searchRoots) { + walk(path.join(root, searchRoot)); + } + + assert.deepEqual(hits, []); }); test('theme color audit reports alias family usages whose exact canonical key is missing', (t) => { @@ -212,16 +455,61 @@ test('theme color audit emits scoped machine-readable reports', (t) => { assert.equal(report.tokenAliasLiterals.uniqueColors, 0); assert.equal(report.cssVarDefinitions.runtimeOnlyRequiredContractUnique, 1); assert.equal(report.cssVarDefinitions.unregisteredDynamicFamilyUnique, 0); + assert.equal(report.cssVarDefinitions.dynamicFamilyUnexportedUnique, 0); assert.equal(report.cssVarDefinitions.staleRegisteredDynamicFamilyUnique, 0); assert.equal(report.summary.baseline.enforced, false); }); -test('theme color audit reports compatibility alias usage without treating it as raw color debt', (t) => { +test('theme color audit reports deprecated surface-local token names', (t) => { + const { dir, sourceRoot } = createFixture({ + 'component-library/styles/tokens.scss': [ + ':root {', + ' --base-tool-card-accent-color: #60a5fa;', + ' --snapshot-card-operation-color: #60a5fa;', + '}', + '', + ].join('\n'), + 'component-library/components/FlowChatCards/BaseToolCard/BaseToolCard.scss': [ + '.base-tool-card {', + ' --primary-color: var(--base-tool-card-accent-color);', + ' color: var(--primary-color);', + '}', + '', + ].join('\n'), + 'component-library/components/FlowChatCards/SnapshotCard/SnapshotCard.tsx': [ + "export const style = { '--operation-color': 'var(--snapshot-card-operation-color)' };", + '', + ].join('\n'), + 'tools/editor/meditor/components/TiptapEditor.scss': [ + '.m-editor-tiptap {', + ' --m-editor-highlight-rgb: var(--markdown-editor-highlight-rgb);', + ' background: rgba(var(--m-editor-highlight-rgb), 0.15);', + '}', + '', + ].join('\n'), + }); + t.after(() => fs.rmSync(dir, { recursive: true, force: true })); + + const result = runAudit(['--root', sourceRoot, '--json', '--no-baseline']); + assert.equal(result.status, 0, result.stderr || result.stdout); + + const report = JSON.parse(result.stdout); + assert.equal(report.surfaceTokenRenames.activeUnique, 3); + assert.equal(report.surfaceTokenRenames.activeOccurrences, 5); + assert.deepEqual( + report.surfaceTokenRenames.active.map(row => [row.key, row.canonical, row.definitionCount, row.usageCount]), + [ + ['--m-editor-highlight-rgb', '--markdown-editor-highlight-rgb', 1, 1], + ['--primary-color', '--base-tool-card-accent-color', 1, 1], + ['--operation-color', '--snapshot-card-operation-color', 1, 0], + ], + ); +}); + +test('theme color audit reports compatibility alias family usage without treating it as raw color debt', (t) => { const { dir, sourceRoot } = createFixture({ 'component-library/styles/tokens.scss': [ ':root {', - ' --color-accent-500: #60a5fa;', - ' --color-primary: var(--color-accent-500);', ' --size-radius-sm: 6px;', ' --radius-sm: var(--size-radius-sm);', '}', @@ -229,7 +517,6 @@ test('theme color audit reports compatibility alias usage without treating it as ].join('\n'), 'app/App.scss': [ '.app {', - ' color: var(--color-primary);', ' border-radius: var(--radius-sm);', '}', '', @@ -241,21 +528,188 @@ test('theme color audit reports compatibility alias usage without treating it as assert.equal(result.status, 0, result.stderr || result.stdout); const report = JSON.parse(result.stdout); - assert.equal(report.compatibilityAliases.usedUnique, 2); - assert.equal(report.compatibilityAliases.occurrences, 2); + assert.equal(report.compatibilityAliases.usedUnique, 1); + assert.equal(report.compatibilityAliases.occurrences, 1); assert.equal(report.compatibilityAliases.familyUsedUnique, 1); assert.equal(report.compatibilityAliases.familyOccurrences, 1); assert.equal(report.compatibilityAliases.missingCanonicalUnique, 0); assert.deepEqual( report.compatibilityAliases.top.map(row => [row.key, row.canonical]), - [ - ['--color-primary', '--color-accent-500'], - ['--radius-sm', '--size-radius-sm'], - ], + [['--radius-sm', '--size-radius-sm']], ); assert.equal(report.colorScopes.appUi.occurrences, 0); }); +test('theme color audit budgets generated widget payload compatibility families separately', (t) => { + const { dir, sourceRoot } = createFixture({ + 'component-library/styles/tokens.scss': [ + ':root {', + ' --size-radius-sm: 6px;', + ' --radius-sm: var(--size-radius-sm);', + '}', + '', + ].join('\n'), + 'tools/generative-widget/themePayload.ts': [ + "export const payloadVars = ['--size-radius-sm', '--radius-sm'];", + '', + ].join('\n'), + }); + t.after(() => fs.rmSync(dir, { recursive: true, force: true })); + + const result = runAudit(['--root', sourceRoot, '--json', '--no-baseline']); + assert.equal(result.status, 0, result.stderr || result.stdout); + + const report = JSON.parse(result.stdout); + assert.equal(report.compatibilityAliases.usedUnique, 0); + assert.equal(report.compatibilityAliases.familyUsedUnique, 0); + assert.equal(report.generatedWidgetPayload.varUnique, 2); + assert.equal(report.generatedWidgetPayload.compatibilityAliasUnique, 0); + assert.equal(report.generatedWidgetPayload.compatibilityAliasFamilyUnique, 1); + assert.equal(report.generatedWidgetPayload.externalOnlyCompatibilityUnique, 1); + assert.equal(report.generatedWidgetPayload.undefinedUnique, 0); + assert.equal(report.generatedWidgetPayload.missingCompatibilityCanonicalUnique, 0); + assert.equal(report.generatedWidgetPayload.unexportedCompatibilityCanonicalUnique, 0); + assert.deepEqual( + report.generatedWidgetPayload.topCompatibilityAliases.map(row => [row.key, row.canonical]), + [], + ); + assert.deepEqual( + report.generatedWidgetPayload.topCompatibilityFamilies.map(row => [row.key, row.canonical]), + [['--radius-sm', '--size-radius-sm']], + ); + assert.deepEqual( + report.generatedWidgetPayload.externalOnlyCompatibility.map(row => [row.key, row.canonical, row.familyPrefix]), + [['--radius-sm', '--size-radius-sm', '--radius-']], + ); + assert.match(report.generatedWidgetPayload.externalOnlyCompatibility[0].removal, /Retire/); +}); + +test('theme color audit counts only generated widget payload groups', (t) => { + const { dir, sourceRoot } = createFixture({ + 'component-library/styles/tokens.scss': [ + ':root {', + ' --color-bg-primary: #121214;', + ' --color-bg-secondary: #1c1c1f;', + ' --font-family-mono: monospace;', + '}', + '', + ].join('\n'), + 'tools/generative-widget/themePayload.ts': [ + 'const FALLBACK_VAR = {', + " bgPrimary: '--color-bg-primary',", + " bgSecondary: '--color-bg-secondary',", + '} as const;', + '', + 'const WIDGET_THEME_STATIC_SHELL_VARS = {', + " '--btn-primary-bg': 'var(--color-bg-primary)',", + '} as const;', + '', + 'const WIDGET_THEME_VAR_GROUPS = {', + ' background: [', + ' FALLBACK_VAR.bgPrimary,', + ' FALLBACK_VAR.bgSecondary,', + " '--font-family-mono',", + ' ],', + '} as const;', + '', + ].join('\n'), + }); + t.after(() => fs.rmSync(dir, { recursive: true, force: true })); + + const result = runAudit(['--root', sourceRoot, '--json', '--no-baseline']); + assert.equal(result.status, 0, result.stderr || result.stdout); + + const report = JSON.parse(result.stdout); + assert.equal(report.generatedWidgetPayload.varUnique, 3); + assert.equal(report.generatedWidgetPayload.undefinedUnique, 0); + assert.equal(report.generatedWidgetPayload.externalOnlyCompatibilityUnique, 0); +}); + +test('theme color audit fails closed when generated widget payload groups are not parseable', (t) => { + const { dir, sourceRoot } = createFixture({ + 'component-library/styles/tokens.scss': [ + ':root {', + ' --color-bg-primary: #121214;', + '}', + '', + ].join('\n'), + 'tools/generative-widget/themePayload.ts': [ + 'const FALLBACK_VAR = {', + " bgPrimary: '--color-bg-primary',", + '} as const;', + '', + 'const WIDGET_THEME_VAR_GROUPS = Object.freeze({', + ' background: [FALLBACK_VAR.bgPrimary],', + '});', + '', + ].join('\n'), + }); + t.after(() => fs.rmSync(dir, { recursive: true, force: true })); + + const result = runAudit(['--root', sourceRoot, '--json', '--no-baseline']); + assert.notEqual(result.status, 0); + assert.match(result.stderr, /Unable to parse generated widget WIDGET_THEME_VAR_GROUPS/); +}); + +test('theme color audit reports generated widget payload compatibility aliases without canonicals', (t) => { + const { dir, sourceRoot } = createFixture({ + 'component-library/styles/tokens.scss': [ + ':root {', + ' --radius-ghost: 10px;', + '}', + '', + ].join('\n'), + 'tools/generative-widget/themePayload.ts': [ + "export const payloadVars = ['--radius-ghost'];", + '', + ].join('\n'), + }); + t.after(() => fs.rmSync(dir, { recursive: true, force: true })); + + const result = runAudit(['--root', sourceRoot, '--json', '--no-baseline']); + assert.equal(result.status, 0, result.stderr || result.stdout); + + const report = JSON.parse(result.stdout); + assert.equal(report.generatedWidgetPayload.undefinedUnique, 0); + assert.equal(report.generatedWidgetPayload.compatibilityAliasFamilyUnique, 1); + assert.equal(report.generatedWidgetPayload.missingCompatibilityCanonicalUnique, 1); + assert.equal(report.generatedWidgetPayload.unexportedCompatibilityCanonicalUnique, 0); + assert.deepEqual( + report.generatedWidgetPayload.missingCompatibilityCanonicals.map(row => [row.key, row.canonical]), + [['--radius-ghost', '--size-radius-ghost']], + ); +}); + +test('theme color audit reports generated widget payload family aliases whose canonicals are not exported', (t) => { + const { dir, sourceRoot } = createFixture({ + 'component-library/styles/tokens.scss': [ + ':root {', + ' --size-radius-sm: 6px;', + ' --radius-sm: var(--size-radius-sm);', + '}', + '', + ].join('\n'), + 'tools/generative-widget/themePayload.ts': [ + "export const payloadVars = ['--radius-sm'];", + '', + ].join('\n'), + }); + t.after(() => fs.rmSync(dir, { recursive: true, force: true })); + + const result = runAudit(['--root', sourceRoot, '--json', '--no-baseline']); + assert.equal(result.status, 0, result.stderr || result.stdout); + + const report = JSON.parse(result.stdout); + assert.equal(report.generatedWidgetPayload.undefinedUnique, 0); + assert.equal(report.generatedWidgetPayload.compatibilityAliasFamilyUnique, 1); + assert.equal(report.generatedWidgetPayload.missingCompatibilityCanonicalUnique, 0); + assert.equal(report.generatedWidgetPayload.unexportedCompatibilityCanonicalUnique, 1); + assert.deepEqual( + report.generatedWidgetPayload.unexportedCompatibilityCanonicals.map(row => [row.key, row.canonical]), + [['--radius-sm', '--size-radius-sm']], + ); +}); + test('theme color audit reports fallback tokens that lack a boundary contract', (t) => { const { dir, sourceRoot } = createFixture({ 'app/App.scss': [ @@ -493,12 +947,48 @@ test('theme color audit reports near color pair sources and enforces pair budget ); }); +test('theme color audit reports near color pairs inside specialized color domains', (t) => { + const { dir, sourceRoot } = createFixture({ + 'tools/mermaid-editor/theme/mermaidTheme.ts': [ + "export const lightNode = '#111111';", + "export const darkNode = '#111112';", + '', + ].join('\n'), + 'tools/terminal/utils/xtermTheme.ts': [ + "export const normalBlack = '#222222';", + "export const brightBlack = '#222225';", + '', + ].join('\n'), + 'app/App.scss': '.app { color: #333333; }\n', + }); + t.after(() => fs.rmSync(dir, { recursive: true, force: true })); + const reportPath = path.join(dir, 'theme-report.json'); + + const reportResult = runAudit(['--root', sourceRoot, '--report-json', reportPath, '--no-baseline']); + assert.equal(reportResult.status, 0, reportResult.stderr || reportResult.stdout); + assert.match(reportResult.stdout, /Specialized color-domain near pairs:/); + + const report = readJson(reportPath); + assert.equal(report.colorDomainNearPairs.mermaid.indistinguishableTotal, 1); + assert.equal(report.colorDomainNearPairs.terminal.nearTotal, 1); + assert.equal(report.colorDomainNearPairs.appUi.indistinguishableTotal, 0); + assert.equal(report.colorDomainNearPairs.indistinguishableTotal, 1); + assert.equal(report.colorDomainNearPairs.nearTotal, 1); + assert.deepEqual( + report.colorDomainNearPairs.mermaid.indistinguishable[0].files.map(file => ( + file.replace(/\\/g, '/').split('/').slice(-3).join('/') + )), + ['mermaid-editor/theme/mermaidTheme.ts'], + ); +}); + test('theme color audit excludes test files from production color budgets', (t) => { const { dir, sourceRoot } = createFixture({ 'component-library/styles/tokens.scss': ':root { --color-error: #ef4444; }\n', 'app/App.scss': '.app { color: #ef4444; }\n', 'app/App.test.tsx': "expect(button).toHaveStyle({ color: '#ef4444' });\n", 'app/__tests__/Fixture.tsx': "export const visualLock = '#ef4444';\n", + 'generated/version.ts': "export const buildAccent = '#22c55e';\n", }); t.after(() => fs.rmSync(dir, { recursive: true, force: true })); @@ -508,6 +998,7 @@ test('theme color audit excludes test files from production color budgets', (t) const report = JSON.parse(result.stdout); assert.equal(report.filesScanned, 2); assert.equal(report.ignoredTestFiles, 2); + assert.equal(report.ignoredGeneratedFiles, 1); assert.equal(report.colorScopes.appUi.occurrences, 1); assert.equal(report.tokenAliasLiterals.occurrences, 1); }); @@ -595,8 +1086,8 @@ test('theme color audit requires dynamic CSS var families to be registered', (t) test('theme color audit accepts registered dynamic CSS var families', (t) => { const { dir, sourceRoot } = createFixture({ 'infrastructure/theme/core/ThemeService.ts': [ - "for (const [key, value] of Object.entries(theme.effects.spacing)) {", - " document.documentElement.style.setProperty(`--spacing-${key}`, value);", + "for (const [key, value] of Object.entries(theme.motion.duration)) {", + " document.documentElement.style.setProperty(`--motion-${key}`, value);", '}', '', ].join('\n'), @@ -614,6 +1105,54 @@ test('theme color audit accepts registered dynamic CSS var families', (t) => { assert.equal(result.status, 0, result.stderr || result.stdout); }); +test('theme color audit requires exact exports for dynamic-family CSS var usages', (t) => { + const { dir, sourceRoot } = createFixture({ + 'component-library/styles/tokens.scss': [ + ':root {', + ' --color-purple-200: rgba(139, 92, 246, 0.15);', + '}', + '', + ].join('\n'), + 'infrastructure/theme/core/ThemeService.ts': [ + "for (const [key, value] of Object.entries(theme.colors.purple)) {", + " document.documentElement.style.setProperty(`--color-purple-${key}`, value);", + '}', + '', + ].join('\n'), + 'component-library/components/Tabs/Tabs.scss': [ + '.tabs {', + ' border-color: var(--color-purple-300);', + '}', + '', + ].join('\n'), + }); + t.after(() => fs.rmSync(dir, { recursive: true, force: true })); + const baselinePath = path.join(dir, 'theme-baseline.json'); + writeText(baselinePath, `${JSON.stringify({ + version: 1, + budgets: { + 'cssVarDefinitions.dynamicFamilyUnexportedUnique': { max: 0 }, + }, + }, null, 2)}\n`); + + const reportResult = runAudit(['--root', sourceRoot, '--json', '--no-baseline']); + assert.equal(reportResult.status, 0, reportResult.stderr || reportResult.stdout); + const report = JSON.parse(reportResult.stdout); + assert.equal(report.cssVarDefinitions.dynamicFamilyUnexportedUnique, 1); + assert.deepEqual( + report.dynamicFamilyUnexportedVars.map(row => [row.key, row.prefix]), + [['--color-purple-300', '--color-purple-']], + ); + + const blocked = runAudit(['--root', sourceRoot, '--baseline', baselinePath]); + assert.notEqual(blocked.status, 0, 'dynamic-family usages without exact exports must fail the audit'); + assert.match( + `${blocked.stdout}\n${blocked.stderr}`, + /cssVarDefinitions\.dynamicFamilyUnexportedUnique has 1 candidate\(s\), baseline is 0/, + ); + assert.match(`${blocked.stdout}\n${blocked.stderr}`, /--color-purple-300/); +}); + test('theme color audit requires non-contract cross-file vars to be explicitly allowlisted', (t) => { const { dir, sourceRoot } = createFixture({ 'app/LooseVar.tsx': [ diff --git a/scripts/core-boundaries/rules/crate-rules.mjs b/scripts/core-boundaries/rules/crate-rules.mjs index aadc9b3729..5886f452ae 100644 --- a/scripts/core-boundaries/rules/crate-rules.mjs +++ b/scripts/core-boundaries/rules/crate-rules.mjs @@ -117,9 +117,11 @@ export const lightweightBoundaryRules = [ 'bitfun-services-core', 'bitfun-services-integrations', 'bitfun-tool-packs', + 'bitfun-product-capabilities', 'bitfun-product-domains', 'bitfun-transport', 'terminal-core', + 'tool-runtime', 'tauri', 'reqwest', 'git2', @@ -143,9 +145,11 @@ export const lightweightBoundaryRules = [ 'bitfun-services-core', 'bitfun-services-integrations', 'bitfun-tool-packs', + 'bitfun-product-capabilities', 'bitfun-product-domains', 'bitfun-transport', 'terminal-core', + 'tool-runtime', 'tauri', 'reqwest', 'git2', @@ -353,9 +357,11 @@ export const dependencyProfileRules = [ 'bitfun-services-core', 'bitfun-services-integrations', 'bitfun-tool-packs', + 'bitfun-product-capabilities', 'bitfun-product-domains', 'bitfun-transport', 'terminal-core', + 'tool-runtime', 'tauri', 'reqwest', 'git2', @@ -443,6 +449,7 @@ export const dependencyProfileRules = [ profileName: 'default integrations profile', reason: 'services-integrations default profile must not compile feature-gated integrations', forbiddenNonOptionalDeps: [ + 'aes', 'aes-gcm', 'anyhow', 'async-trait', @@ -454,6 +461,9 @@ export const dependencyProfileRules = [ 'dunce', 'futures', 'git2', + 'hex', + 'local-ip-address', + 'md5', 'notify', 'rand', 'reqwest', @@ -464,6 +474,7 @@ export const dependencyProfileRules = [ 'tokio-util', 'tokio-tungstenite', 'uuid', + 'which', 'bitfun-relay-server', ], }, diff --git a/scripts/core-boundaries/rules/feature-rules.mjs b/scripts/core-boundaries/rules/feature-rules.mjs index 6286790258..b3ca6b64c2 100644 --- a/scripts/core-boundaries/rules/feature-rules.mjs +++ b/scripts/core-boundaries/rules/feature-rules.mjs @@ -6,7 +6,6 @@ export const optionalDependencyFeatureOwnerRules = [ reason: 'bitfun-core product/runtime optional dependencies must stay owned by explicit feature gates', dependencies: [ - { depName: 'aes', ownerFeatures: ['service-integrations'] }, { depName: 'aes-gcm', ownerFeatures: ['service-integrations'] }, { depName: 'axum', ownerFeatures: ['service-integrations'] }, { depName: 'bitfun-ai-adapters', ownerFeatures: ['ai-adapter-runtime'] }, @@ -27,7 +26,6 @@ export const optionalDependencyFeatureOwnerRules = [ { depName: 'image', ownerFeatures: ['service-integrations'] }, { depName: 'include_dir', ownerFeatures: ['product-full'] }, { depName: 'indexmap', ownerFeatures: ['product-full'] }, - { depName: 'local-ip-address', ownerFeatures: ['service-integrations'] }, { depName: 'md5', ownerFeatures: ['product-full', 'service-integrations'] }, { depName: 'rand', ownerFeatures: ['service-integrations'] }, { depName: 'reqwest', ownerFeatures: ['ai-adapter-runtime', 'service-integrations'] }, @@ -45,8 +43,9 @@ export const optionalDependencyFeatureOwnerRules = [ reason: 'services-integrations optional runtime dependencies must stay owned by explicit integration features', dependencies: [ + { depName: 'aes', ownerFeatures: ['remote-connect'] }, { depName: 'aes-gcm', ownerFeatures: ['mcp', 'remote-connect', 'remote-ssh-concrete'] }, - { depName: 'anyhow', ownerFeatures: ['mcp', 'remote-connect', 'remote-ssh-concrete'] }, + { depName: 'anyhow', ownerFeatures: ['browser-control', 'mcp', 'remote-connect', 'remote-ssh-concrete'] }, { depName: 'async-trait', ownerFeatures: ['mcp', 'remote-connect', 'remote-ssh-concrete', 'workspace-search'], @@ -60,20 +59,23 @@ export const optionalDependencyFeatureOwnerRules = [ { depName: 'bitfun-runtime-ports', ownerFeatures: ['remote-connect'] }, { depName: 'bitfun-services-core', - ownerFeatures: ['git', 'mcp', 'miniapp-runtime', 'workspace-search'], + ownerFeatures: ['browser-control', 'git', 'mcp', 'miniapp-runtime', 'remote-connect', 'workspace-search'], }, { depName: 'chrono', ownerFeatures: ['git', 'remote-connect', 'remote-ssh-concrete'] }, - { depName: 'dirs', ownerFeatures: ['miniapp-runtime', 'remote-ssh-concrete'] }, + { depName: 'dirs', ownerFeatures: ['browser-control', 'miniapp-runtime', 'remote-connect', 'remote-ssh-concrete'] }, { depName: 'dunce', ownerFeatures: ['remote-ssh', 'workspace-search'] }, { depName: 'futures', ownerFeatures: ['mcp', 'remote-connect'] }, { depName: 'git2', ownerFeatures: ['git'] }, + { depName: 'hex', ownerFeatures: ['remote-connect'] }, { depName: 'hostname', ownerFeatures: ['remote-connect'] }, { depName: 'image', ownerFeatures: ['remote-connect'] }, + { depName: 'local-ip-address', ownerFeatures: ['remote-connect'] }, { depName: 'mac_address', ownerFeatures: ['remote-connect'] }, + { depName: 'md5', ownerFeatures: ['remote-connect'] }, { depName: 'notify', ownerFeatures: ['file-watch'] }, { depName: 'qrcode', ownerFeatures: ['remote-connect'] }, { depName: 'rand', ownerFeatures: ['mcp', 'remote-connect', 'remote-ssh-concrete'] }, - { depName: 'reqwest', ownerFeatures: ['mcp', 'miniapp-runtime'] }, + { depName: 'reqwest', ownerFeatures: ['announcement', 'browser-control', 'debug-log', 'mcp', 'miniapp-runtime', 'remote-connect', 'review-platform', 'web-tools'] }, { depName: 'rmcp', ownerFeatures: ['mcp'] }, { depName: 'russh', ownerFeatures: ['remote-ssh-concrete'] }, { depName: 'russh-keys', ownerFeatures: ['remote-ssh-concrete'] }, @@ -86,12 +88,12 @@ export const optionalDependencyFeatureOwnerRules = [ { depName: 'sse-stream', ownerFeatures: ['mcp'] }, { depName: 'ssh_config', ownerFeatures: ['remote-ssh-concrete', 'ssh_config'] }, { depName: 'terminal-core', ownerFeatures: ['remote-ssh-concrete'] }, - { depName: 'thiserror', ownerFeatures: ['git', 'remote-ssh-concrete', 'workspace-search'] }, + { depName: 'thiserror', ownerFeatures: ['browser-control', 'git', 'remote-ssh-concrete', 'review-platform', 'web-tools', 'workspace-search'] }, { depName: 'tokio-tungstenite', ownerFeatures: ['remote-connect'] }, { depName: 'tokio-util', ownerFeatures: ['remote-ssh'] }, { depName: 'urlencoding', ownerFeatures: ['remote-connect'] }, { depName: 'uuid', ownerFeatures: ['miniapp-runtime', 'remote-connect', 'remote-ssh-concrete'] }, - { depName: 'which', ownerFeatures: ['miniapp-runtime', 'workspace-search'] }, + { depName: 'which', ownerFeatures: ['miniapp-runtime', 'remote-connect', 'workspace-search'] }, { depName: 'x25519-dalek', ownerFeatures: ['remote-connect'] }, ], }, @@ -167,6 +169,8 @@ export const ownerCrateFeatureAssemblyRules = [ reason: 'services-integrations must keep integration feature groups explicit and default-light', requiredProductFullFeatures: [ 'announcement', + 'browser-control', + 'debug-log', 'deep-research', 'file-watch', 'function-agents', @@ -176,6 +180,8 @@ export const ownerCrateFeatureAssemblyRules = [ 'remote-connect', 'remote-ssh', 'remote-ssh-concrete', + 'review-platform', + 'web-tools', 'workspace-search', ], }, diff --git a/scripts/core-boundaries/rules/source/facade-rules.mjs b/scripts/core-boundaries/rules/source/facade-rules.mjs index 512aefa2f3..8a9b9e8594 100644 --- a/scripts/core-boundaries/rules/source/facade-rules.mjs +++ b/scripts/core-boundaries/rules/source/facade-rules.mjs @@ -1,6 +1,21 @@ // Boundary rules for source ownership, facades, and required owner content. export const facadeOnlyFiles = [ + { + path: 'src/crates/assembly/core/src/agentic/context_profile.rs', + importPrefix: 'bitfun_agent_runtime::context_profile', + reason: 'core context profile path must only re-export the agent-runtime owner crate', + }, + { + path: 'src/crates/assembly/core/src/agentic/core/dialog_turn.rs', + importPrefix: 'bitfun_agent_runtime::dialog_turn', + reason: 'core dialog-turn facts path must only re-export the agent-runtime owner crate', + }, + { + path: 'src/crates/assembly/core/src/agentic/core/session.rs', + importPrefix: 'bitfun_agent_runtime::session', + reason: 'core session facts path must only re-export the agent-runtime owner crate', + }, { path: 'src/crates/assembly/core/src/infrastructure/filesystem/mod.rs', importPrefix: 'bitfun_services_core::filesystem', diff --git a/scripts/core-boundaries/rules/source/forbidden-rules.mjs b/scripts/core-boundaries/rules/source/forbidden-rules.mjs index 8c0cc7168a..b6d7ccc374 100644 --- a/scripts/core-boundaries/rules/source/forbidden-rules.mjs +++ b/scripts/core-boundaries/rules/source/forbidden-rules.mjs @@ -1,6 +1,31 @@ // Boundary rules for source ownership, facades, and required owner content. export const forbiddenContentRules = [ + { + path: 'src/crates/adapters/transport/src/adapters/tauri.rs', + patterns: [ + { + regex: /\bAgenticEvent::[A-Z]/, + message: + 'Tauri transport adapter must not match agentic event variants directly; use bitfun-events frontend projection', + }, + ], + }, + { + path: 'src/crates/adapters/transport/src/adapters/websocket.rs', + patterns: [ + { + regex: /\bAgenticEvent::[A-Z]/, + message: + 'WebSocket transport adapter must not match agentic event variants directly; use bitfun-events frontend projection', + }, + { + regex: /\bfn\s+is_legacy_websocket_agentic_event_type\b/, + message: + 'WebSocket transport adapter must not own the agentic event allowlist; use bitfun-events event manifest', + }, + ], + }, { path: 'src/crates/execution/agent-runtime/tests/sdk_smoke.rs', patterns: [ @@ -16,6 +41,218 @@ export const forbiddenContentRules = [ }, ], }, + { + path: 'src/crates/execution/agent-runtime/src/sdk.rs', + patterns: [ + { + regex: /\bbitfun_core\b/, + message: 'SDK facade must not expose bitfun-core', + }, + { + regex: /\bbitfun_product_capabilities\b/, + message: 'SDK facade must not expose product assembly facts', + }, + { + regex: /\btauri\b/, + message: 'SDK facade must not expose Tauri APIs', + }, + { + regex: /\bAppHandle\b/, + message: 'SDK facade must not expose desktop app handles', + }, + { + regex: /\breqwest\b/, + message: 'SDK facade must not expose concrete HTTP clients', + }, + { + regex: /\bgit2\b/, + message: 'SDK facade must not expose concrete Git providers', + }, + { + regex: /\brmcp\b/, + message: 'SDK facade must not expose concrete MCP clients', + }, + ], + }, + { + path: 'src/crates/assembly/product-capabilities/tests/product_sdk_assembly.rs', + patterns: [ + { + regex: /\bbitfun_core\b/, + message: + 'product assembly to SDK smoke must not depend on bitfun-core', + }, + { + regex: /\bCoreRuntimeServicesProvider\b/, + message: + 'product assembly to SDK smoke must not use core concrete service adapters', + }, + { + regex: /\btauri\b/, + message: 'product assembly to SDK smoke must not depend on Tauri', + }, + ], + }, + { + path: 'src/crates/assembly/core/src/agentic/tools/browser_control/browser_launcher.rs', + patterns: [ + { + regex: /\bprocess_manager::/, + message: + 'core browser launcher facade must not own browser process execution; use bitfun-services-integrations browser_control launcher', + }, + { + regex: /\b(?:std::process::Command|Command::new\()/, + message: + 'core browser launcher facade must not construct browser launch commands; use bitfun-services-integrations browser_control launcher', + }, + { + regex: /\breqwest::/, + message: + 'core browser launcher facade must not own CDP network probing; use bitfun-services-integrations browser_control launcher', + }, + ], + }, + { + path: 'src/crates/assembly/core/src/agentic/tools/browser_control/cdp_client.rs', + patterns: [ + { + regex: /\breqwest::/, + message: + 'core CDP client facade must not own HTTP endpoint probing; use bitfun-services-integrations browser_control CDP provider', + }, + ], + }, + { + path: 'src/crates/assembly/core/src/agentic/tools/implementations/web/fetch.rs', + patterns: [ + { + regex: /\breqwest::/, + message: + 'core WebFetch tool must not own HTTP clients; use bitfun-services-integrations web provider', + }, + ], + }, + { + path: 'src/crates/assembly/core/src/agentic/tools/implementations/web/search.rs', + patterns: [ + { + regex: /\breqwest::/, + message: + 'core WebSearch tool must not own provider HTTP clients; use bitfun-services-integrations web provider', + }, + ], + }, + { + path: 'src/crates/assembly/core/src/infrastructure/debug_log/mod.rs', + patterns: [ + { + regex: /\breqwest::/, + message: + 'core debug log facade must not own HTTP ingest posting; use bitfun-services-integrations debug log network provider', + }, + ], + }, + { + path: 'src/crates/assembly/core/src/service/review_platform/mod.rs', + patterns: [ + { + regex: /\breqwest::/, + message: + 'core review platform service must not own concrete HTTP clients; use bitfun-services-integrations review platform HTTP transport', + }, + ], + }, + { + path: 'src/crates/assembly/core/src/agentic/tools/implementations/computer_use_actions.rs', + patterns: [ + { + regex: /\bprocess_manager::/, + message: + 'core computer-use system facade must not spawn local system processes directly; use bitfun-services-core local system provider', + }, + { + regex: /\btokio::process::/, + message: + 'core computer-use system facade must not own async process execution; use bitfun-services-core local system provider', + }, + { + regex: /\b(?:std::process::Command|Command::new\()/, + message: + 'core computer-use system facade must not construct local system commands directly; use bitfun-services-core local system provider', + }, + { + regex: /\bfn\s+script_invocation\b/, + message: + 'core computer-use system facade must not own script invocation selection; use bitfun-services-core local system provider', + }, + { + regex: /\bfn\s+platform_open_attempts\b/, + message: + 'core computer-use system facade must not own platform open-app command selection; use bitfun-services-core local system provider', + }, + ], + }, + { + path: 'src/crates/assembly/core/src/agentic/tools/implementations/exec_command/local_shell.rs', + patterns: [ + { + regex: /\bprocess_manager::/, + message: + 'core local shell facade must not probe shells through process execution; use terminal-core shell resolution', + }, + { + regex: /\b(?:std::process::Command|Command::new\()/, + message: + 'core local shell facade must not construct shell probe commands; use terminal-core shell resolution', + }, + { + regex: /\bstd::env::var\b/, + message: + 'core local shell facade must not own environment-based shell selection; use terminal-core shell resolution', + }, + { + regex: /\bcfg!\(target_os\b/, + message: + 'core local shell facade must not own platform shell fallback rules; use terminal-core shell resolution', + }, + ], + }, + { + path: 'src/crates/assembly/core/src/service/remote_connect/bot/weixin.rs', + patterns: [ + { + regex: /\breqwest::/, + message: + 'core Weixin bot facade must not own provider HTTP clients; use bitfun-services-integrations Weixin provider', + }, + { + regex: /\baes::/, + message: + 'core Weixin bot facade must not own provider AES/CDN crypto; use bitfun-services-integrations Weixin provider', + }, + { + regex: /\bhex::/, + message: + 'core Weixin bot facade must not own provider hex encoding; use bitfun-services-integrations Weixin provider', + }, + { + regex: /\bmd5::/, + message: + 'core Weixin bot facade must not own provider MD5 signing; use bitfun-services-integrations Weixin provider', + }, + { + regex: /\bfn\s+sync_buf_path\b/, + message: + 'core Weixin bot facade must not own provider sync-buffer storage layout; use bitfun-services-integrations Weixin provider', + }, + { + regex: /\bfn\s+parse_weixin_cdn_aes_key\b/, + message: + 'core Weixin bot facade must not own provider CDN AES key parsing; use bitfun-services-integrations Weixin provider', + }, + ], + }, { path: 'src/crates/contracts/core-types/src/ai.rs', patterns: [ @@ -31,6 +268,46 @@ export const forbiddenContentRules = [ }, ], }, + { + path: 'src/crates/assembly/core/src/service/lsp/types.rs', + patterns: [ + { + regex: /\bpub struct LspPlugin\b/, + message: + 'LSP plugin manifest DTO belongs in bitfun-core-types; keep core LSP types as a compatibility facade', + }, + { + regex: /\bpub enum JsonRpcMessage\b/, + message: + 'LSP JSON-RPC DTOs belong in bitfun-core-types; keep core LSP types as a compatibility facade', + }, + { + regex: /\buse serde::\{Deserialize,\s*Serialize\}/, + message: + 'core LSP types should not own serialization DTOs after migration to bitfun-core-types', + }, + ], + }, + { + path: 'src/crates/assembly/core/src/service/lsp/registry.rs', + patterns: [ + { + regex: /\bpub struct PluginRegistry\b/, + message: + 'LSP plugin registry belongs in bitfun-services-core; keep core registry as a compatibility facade', + }, + { + regex: /\bHashMap\b/, + message: + 'core LSP registry must not own plugin index maps after migration to bitfun-services-core', + }, + { + regex: /\bPathBuf::from\(file_path\)/, + message: + 'LSP file-extension lookup belongs in bitfun-services-core registry rules', + }, + ], + }, { path: 'src/crates/assembly/core/src/product_assembly.rs', patterns: [ @@ -215,12 +492,12 @@ export const forbiddenContentRules = [ 'SessionMessage must submit through AgentRuntime dialog lifecycle port, not direct DialogScheduler', }, { - regex: /\bcoordinator\s*\.\s*resolve_session_workspace_path\b/, + regex: /\bcoordinator\s*\.\s*resolve_session_workspace_binding\b/, message: 'SessionMessage target workspace resolution must flow through AgentRuntime session-management port, not direct coordinator access', }, { - regex: /\bresolve_session_workspace_path\s*\(\s*&?target_session_id\b/, + regex: /\bresolve_session_workspace_binding\s*\(\s*&?target_session_id\b/, message: 'SessionMessage target workspace resolution must use AgentSessionWorkspaceRequest, not legacy direct session id calls', }, @@ -245,12 +522,12 @@ export const forbiddenContentRules = [ 'SessionControl requester-aware cancellation must flow through AgentRuntime cancellation port, not direct DialogScheduler', }, { - regex: /\bcoordinator\s*\.\s*resolve_session_workspace_path\b/, + regex: /\bcoordinator\s*\.\s*resolve_session_workspace_binding\b/, message: 'SessionControl workspace resolution must flow through AgentRuntime session-management port, not direct coordinator access', }, { - regex: /\bresolve_session_workspace_path\s*\(\s*session_id\b/, + regex: /\bresolve_session_workspace_binding\s*\(\s*session_id\b/, message: 'SessionControl workspace resolution must use AgentSessionWorkspaceRequest, not legacy direct session id calls', }, @@ -290,12 +567,12 @@ export const forbiddenContentRules = [ path: 'src/crates/assembly/core/src/agentic/tools/implementations/cron_tool.rs', patterns: [ { - regex: /\bcoordinator\s*\.\s*resolve_session_workspace_path\b/, + regex: /\bcoordinator\s*\.\s*resolve_session_workspace_binding\b/, message: 'CronTool target workspace resolution must flow through AgentRuntime session-management port, not direct coordinator access', }, { - regex: /\bresolve_session_workspace_path\s*\(\s*&?session_id\b/, + regex: /\bresolve_session_workspace_binding\s*\(\s*&?session_id\b/, message: 'CronTool target workspace resolution must use AgentSessionWorkspaceRequest, not legacy direct session id calls', }, @@ -334,6 +611,31 @@ export const forbiddenContentRules = [ message: 'Coordinator background result delivery must flow through AgentRuntime lifecycle delivery port, not direct DialogScheduler', }, + { + regex: /\bCoreRuntimeServicesProvider::terminal_port\b/, + message: + 'Coordinator must consume an injected TerminalPort; product runtime owns concrete terminal provider construction', + }, + { + regex: /\bTerminalRuntimePort\b/, + message: + 'Coordinator must not construct concrete terminal providers; use injected TerminalPort', + }, + ], + }, + { + path: 'src/crates/assembly/core/src/agentic/system.rs', + patterns: [ + { + regex: /\bCoreRuntimeServicesProvider::terminal_port\b/, + message: + 'shared agentic system init must not construct concrete terminal providers; product entrypoints inject TerminalPort', + }, + { + regex: /\bTerminalRuntimePort\b/, + message: + 'shared agentic system init must not construct concrete terminal providers; product entrypoints inject TerminalPort', + }, ], }, { @@ -819,6 +1121,11 @@ export const forbiddenContentRules = [ { path: 'src/crates/assembly/core/src/service/search/mod.rs', patterns: [ + { + regex: /#\[cfg\(not\(feature = "ssh-remote"\)\)\]\s*mod remote_disabled\b/s, + message: + 'core workspace search facade must not own disabled remote search stubs; re-export services-integrations remote_ssh workspace_search disabled surface', + }, { regex: /\bbitfun_services_integrations::workspace_search::flashgrep\b/, message: @@ -1103,6 +1410,11 @@ export const forbiddenContentRules = [ message: 'core tool context runtime must not own checkpoint evidence projection; use bitfun-agent-runtime evidence_ledger', }, + { + regex: /\bTerminalRuntimePort\b/, + message: + 'core tool context runtime must consume injected terminal ports, not construct the concrete terminal provider', + }, ], }, { @@ -1190,6 +1502,306 @@ export const forbiddenContentRules = [ }, ], }, + { + path: 'src/crates/assembly/core/src/agentic/tools/implementations/exec_command/command.rs', + patterns: [ + { + regex: /\bconst\s+POWERSHELL_UTF8_OUTPUT_PREFIX\b/, + message: + 'core exec_command adapter must not own PowerShell shell policy; use tool-runtime exec_command', + }, + { + regex: /\bconst\s+REMOTE_NON_TTY_INTERRUPT_GRACE_SECONDS\b/, + message: + 'core exec_command adapter must not own remote non-TTY lifecycle policy; use tool-runtime exec_command', + }, + { + regex: /\bconst\s+DEFAULT_TOOL_YIELD_TIME_MS\b/, + message: + 'core exec_command adapter must not own ExecCommand default wait policy; use tool-runtime exec_command', + }, + { + regex: /\bfn\s+(?:local|remote)_completion_value\b/, + message: + 'core exec_command adapter must not own ExecCommand completion value shaping; use tool-runtime exec_command', + }, + { + regex: /\bfn\s+(?:local|remote)_completion\b/, + message: + 'core exec_command adapter must not duplicate concrete completion mapping; use exec_command completion adapter', + }, + { + regex: /\bfn\s+(?:local|remote)_background_output_status_for_completion\b/, + message: + 'core exec_command adapter must not own ExecCommand background status shaping; use tool-runtime exec_command', + }, + { + regex: /\bfn\s+merged_remote_env\b/, + message: + 'core exec_command adapter must not own remote env merge policy; use tool-runtime exec_command', + }, + { + regex: /\bfn\s+remote_command_env_words\b/, + message: + 'core exec_command adapter must not own remote env rendering; use tool-runtime exec_command', + }, + { + regex: /\bfn\s+shell_escape\b/, + message: + 'core exec_command adapter must not own shell escaping policy; use tool-runtime exec_command', + }, + { + regex: /\bfn\s+is_plausible_remote_shell_path\b/, + message: + 'core exec_command adapter must not own remote shell probe validation; use tool-runtime exec_command', + }, + { + regex: /\bgetent\s+passwd\b/, + message: + 'core exec_command adapter must not own remote shell probe command text; use tool-runtime exec_command', + }, + { + regex: /\bcommand\s+-v\s+bash\b/, + message: + 'core exec_command adapter must not own remote shell fallback probe text; use tool-runtime exec_command', + }, + { + regex: /\bconst\s+REMOTE_SHELL_PROBE_TIMEOUT_MS\b/, + message: + 'core exec_command adapter must not own remote shell probe timeout; use tool-runtime exec_command', + }, + { + regex: /\bget_global_exec_process_manager\b/, + message: + 'core local ExecCommand adapter must not call the global local process manager; use injected TerminalPort', + }, + { + regex: /\bget_global_remote_exec_process_manager\b/, + message: + 'core remote ExecCommand adapter must not call the global remote process manager; use injected RemoteExecPort', + }, + { + regex: /\bSSHConnectionManager\b/, + message: + 'core remote ExecCommand adapter must not depend on concrete SSH managers; use injected RemoteExecPort', + }, + { + regex: /\bSSHCommandOptions\b/, + message: + 'core remote ExecCommand adapter must not depend on concrete SSH command options; use injected RemoteExecPort', + }, + { + regex: /\bLocalExecCommandRequest\b/, + message: + 'core local ExecCommand adapter must not construct local process-manager requests; use TerminalExecCommandRequest', + }, + { + regex: /\bCoreRuntimeServicesProvider::terminal_port\b/, + message: + 'core local ExecCommand adapter must not construct concrete terminal providers; use injected TerminalPort', + }, + { + regex: /\bTerminalRuntimePort\b/, + message: + 'core local ExecCommand adapter must not construct concrete terminal providers; use injected TerminalPort', + }, + ], + }, + { + path: 'src/crates/assembly/core/src/agentic/tools/implementations/exec_command/stdin.rs', + patterns: [ + { + regex: /\bconst\s+DEFAULT_TOOL_YIELD_TIME_MS\b/, + message: + 'core WriteStdin adapter must not own default wait policy; use tool-runtime exec_command', + }, + { + regex: /\bfn\s+(?:local|remote)_completion_value\b/, + message: + 'core WriteStdin adapter must not own completion value shaping; use tool-runtime exec_command', + }, + { + regex: /\bfn\s+(?:local|remote)_completion\b/, + message: + 'core WriteStdin adapter must not duplicate concrete completion mapping; use exec_command completion adapter', + }, + { + regex: /"status"\s*:\s*"session_not_found"/, + message: + 'core WriteStdin adapter must not own session-not-found result shape; use tool-runtime exec_command', + }, + { + regex: /\bNo input was sent\b/, + message: + 'core WriteStdin adapter must not own session-not-found assistant text; use tool-runtime exec_command', + }, + { + regex: /\bget_global_exec_process_manager\b/, + message: + 'core local WriteStdin adapter must not call the global local process manager; use injected TerminalPort', + }, + { + regex: /\bget_global_remote_exec_process_manager\b/, + message: + 'core remote WriteStdin adapter must not call the global remote process manager; use injected RemoteExecPort', + }, + { + regex: /\bRemoteExecError\b/, + message: + 'core remote WriteStdin adapter must not match concrete remote exec errors; use PortErrorKind', + }, + { + regex: /\bLocalWriteStdinRequest\b/, + message: + 'core local WriteStdin adapter must not construct local process-manager requests; use TerminalWriteStdinRequest', + }, + { + regex: /\bCoreRuntimeServicesProvider::terminal_port\b/, + message: + 'core local WriteStdin adapter must not construct concrete terminal providers; use injected TerminalPort', + }, + { + regex: /\bTerminalRuntimePort\b/, + message: + 'core local WriteStdin adapter must not construct concrete terminal providers; use injected TerminalPort', + }, + ], + }, + { + path: 'src/crates/assembly/core/src/agentic/tools/implementations/exec_command/control.rs', + patterns: [ + { + regex: /\bexec_command_control_action_name\b/, + message: + 'core ExecControl adapter must not own action result value shaping; use tool-runtime exec_command', + }, + { + regex: /\bfn\s+(?:local|remote)_completion\b/, + message: + 'core ExecControl adapter must not duplicate concrete completion mapping; use exec_command completion adapter', + }, + { + regex: /\bget_global_exec_process_manager\b/, + message: + 'core local ExecControl adapter must not call the global local process manager; use injected TerminalPort', + }, + { + regex: /\bget_global_remote_exec_process_manager\b/, + message: + 'core remote ExecControl adapter must not call the global remote process manager; use injected RemoteExecPort', + }, + { + regex: /\bRemoteExecError\b/, + message: + 'core remote ExecControl adapter must not match concrete remote exec errors; use PortErrorKind', + }, + { + regex: /\bLocalExecControlRequest\b/, + message: + 'core local ExecControl adapter must not construct local process-manager requests; use TerminalExecControlRequest', + }, + { + regex: /\bCoreRuntimeServicesProvider::terminal_port\b/, + message: + 'core local ExecControl adapter must not construct concrete terminal providers; use injected TerminalPort', + }, + { + regex: /\bTerminalRuntimePort\b/, + message: + 'core local ExecControl adapter must not construct concrete terminal providers; use injected TerminalPort', + }, + ], + }, + { + path: 'src/crates/assembly/core/src/agentic/tools/implementations/exec_command/input.rs', + patterns: [ + { + regex: /\bget_global_exec_process_manager\b/, + message: + 'core local ExecCommand input adapter must not call the global local process manager; use injected TerminalPort', + }, + { + regex: /\bget_global_remote_exec_process_manager\b/, + message: + 'core remote ExecCommand input adapter must not call the global remote process manager; use injected RemoteExecPort', + }, + { + regex: /\bLocalSendStdinRequest\b/, + message: + 'core local ExecCommand input adapter must not construct local process-manager requests; use TerminalSendStdinRequest', + }, + { + regex: /\bCoreRuntimeServicesProvider::terminal_port\b/, + message: + 'core local ExecCommand input adapter must not construct concrete terminal providers; use injected TerminalPort', + }, + { + regex: /\bTerminalRuntimePort\b/, + message: + 'core local ExecCommand input adapter must not construct concrete terminal providers; use injected TerminalPort', + }, + ], + }, + { + path: 'src/crates/assembly/core/src/agentic/tools/implementations/exec_command/env_snapshot.rs', + patterns: [ + { + regex: /\bconst\s+ENV_SNAPSHOT_BEGIN\b/, + message: + 'core exec_command env snapshot adapter must not own snapshot framing; use tool-runtime exec_command', + }, + { + regex: /\bconst\s+ENV_SNAPSHOT_END\b/, + message: + 'core exec_command env snapshot adapter must not own snapshot framing; use tool-runtime exec_command', + }, + { + regex: /\bfn\s+should_import_env_var\b/, + message: + 'core exec_command env snapshot adapter must not own env import policy; use tool-runtime exec_command', + }, + { + regex: /\bfn\s+is_valid_env_var_name\b/, + message: + 'core exec_command env snapshot adapter must not own env name validation; use tool-runtime exec_command', + }, + { + regex: /\bfn\s+is_volatile_env_var\b/, + message: + 'core exec_command env snapshot adapter must not own volatile env filtering; use tool-runtime exec_command', + }, + { + regex: /\bfn\s+shell_escape\b/, + message: + 'core exec_command env snapshot adapter must not own shell escaping policy; use tool-runtime exec_command', + }, + { + regex: /\bconst\s+ENV_SNAPSHOT_TIMEOUT_MS\b/, + message: + 'core exec_command env snapshot adapter must not own snapshot capture timeout; use tool-runtime exec_command', + }, + { + regex: /\bconst\s+ENV_SNAPSHOT_MAX_OUTPUT_CHARS\b/, + message: + 'core exec_command env snapshot adapter must not own snapshot capture output bounds; use tool-runtime exec_command', + }, + { + regex: /\bconst\s+ENV_SNAPSHOT_TTL\b/, + message: + 'core exec_command env snapshot adapter must not own snapshot cache ttl; use tool-runtime exec_command', + }, + { + regex: /\bget_global_remote_exec_process_manager\b/, + message: + 'core exec_command env snapshot adapter must not call the global remote process manager; use injected RemoteExecPort', + }, + { + regex: /\bSSHConnectionManager\b/, + message: + 'core exec_command env snapshot adapter must not depend on concrete SSH managers; use injected RemoteExecPort', + }, + ], + }, { path: 'src/crates/assembly/core/src/agentic/tools/computer_use_optimizer.rs', patterns: [ @@ -1335,6 +1947,31 @@ export const forbiddenContentRules = [ }, ], }, + { + path: 'src/crates/assembly/core/src/agentic/coordination/state_manager.rs', + patterns: [ + { + regex: /\bpub\s+struct\s+SessionStateManager\b/, + message: + 'core session state manager path must remain a compatibility facade; use bitfun-agent-runtime session_state_manager', + }, + { + regex: /\bDashMap\b/, + message: + 'core session state manager path must not own session state storage; use bitfun-agent-runtime session_state_manager', + }, + { + regex: /\bAgenticEvent::SessionStateChanged\b/, + message: + 'core session state manager path must not emit session-state events directly; use bitfun-agent-runtime session_state_manager', + }, + { + regex: /\bimpl\s+SessionStateManager\b/, + message: + 'core session state manager path must not reimplement session state transitions; use bitfun-agent-runtime session_state_manager', + }, + ], + }, { path: 'src/crates/assembly/core/src/agentic/coordination/scheduler.rs', patterns: [ @@ -2225,7 +2862,7 @@ export const forbiddenContentRules = [ patterns: [ { regex: /\bfn behavior_hints\b/, - message: 'core MCP tool adapter must not own dynamic tool behavior hint rendering; use the integrations helper', + message: 'core MCP tool adapter must not own dynamic tool behavior hint rendering; use the execution MCP tool bridge contract', }, { regex: /\bfn truncate_for_assistant\b/, @@ -2237,7 +2874,27 @@ export const forbiddenContentRules = [ }, { regex: /Tool '\{\}' from MCP server/, - message: 'core MCP tool adapter must not own dynamic descriptor text; use the integrations helper', + message: 'core MCP tool adapter must not own dynamic descriptor text; use the execution MCP tool bridge contract', + }, + { + regex: /\bDynamicMcpToolInfo\b/, + message: 'core MCP tool adapter must not own dynamic MCP metadata assembly; use the execution MCP tool bridge contract', + }, + { + regex: /Input must be an object/, + message: 'core MCP tool adapter must not own bridge input validation text; use the execution MCP tool bridge contract', + }, + { + regex: /Using MCP tool/, + message: 'core MCP tool adapter must not own bridge tool-use presentation; use the execution MCP tool bridge contract', + }, + { + regex: /was rejected by user/, + message: 'core MCP tool adapter must not own bridge rejection presentation; use the execution MCP tool bridge contract', + }, + { + regex: /completed\. Result:/, + message: 'core MCP tool adapter must not own bridge result presentation; use the execution MCP tool bridge contract', }, ], }, @@ -2520,6 +3177,15 @@ export const forbiddenContentRules = [ }, ], }, + { + path: 'src/crates/assembly/core/src/service/remote_ssh/mod.rs', + patterns: [ + { + regex: /#\[cfg\(not\(feature = "ssh-remote"\)\)\]\s*mod disabled\b/s, + message: 'core remote SSH facade must not own disabled runtime stubs; re-export services-integrations remote_ssh', + }, + ], + }, { path: 'src/crates/assembly/core/src/service/remote_ssh/workspace_state.rs', patterns: [ @@ -2563,6 +3229,14 @@ export const forbiddenContentRules = [ regex: /\bpub fn unresolved_remote_session_storage_key\b/, message: 'core remote SSH workspace runtime must not redefine unresolved session keys; use the integrations contract', }, + { + regex: /\bpub struct WorkspaceSessionIdentity\b/, + message: 'core remote SSH workspace runtime must not redefine workspace session identity; use the integrations path helper', + }, + { + regex: /\bpub fn workspace_session_identity\b/, + message: 'core remote SSH workspace runtime must not redefine workspace session identity construction; use the integrations path helper', + }, { regex: /\bstruct RegisteredRemoteWorkspace\b/, message: 'core remote SSH workspace runtime must not own workspace registrations; use the integrations registry', diff --git a/scripts/core-boundaries/rules/source/required-rules.mjs b/scripts/core-boundaries/rules/source/required-rules.mjs index 434d377b42..8e590840e9 100644 --- a/scripts/core-boundaries/rules/source/required-rules.mjs +++ b/scripts/core-boundaries/rules/source/required-rules.mjs @@ -1,6 +1,217 @@ // Boundary rules for source ownership, facades, and required owner content. export const requiredContentRules = [ + { + path: 'src/crates/contracts/events/src/frontend_projection.rs', + reason: + 'events contract must own framework-neutral agentic frontend event projection for Tauri, WebSocket, and future extension hosts', + patterns: [ + { + regex: /\bpub struct AgenticFrontendEvent\b/, + message: 'missing framework-neutral frontend event projection DTO', + }, + { + regex: /\bpub fn project_agentic_frontend_event\b/, + message: 'missing shared agentic frontend event projection function', + }, + { + regex: /\bpub fn legacy_flat_message\b/, + message: 'missing legacy flat payload projection helper', + }, + { + regex: /\bdeep_review_queue_projection_preserves_camel_case_contract\b/, + message: 'missing camelCase queue projection regression test', + }, + { + regex: /\blegacy_flat_message_keeps_projection_type_authoritative\b/, + message: 'missing WebSocket type precedence regression test', + }, + { + regex: /\blegacy_flat_dialog_turn_started_preserves_existing_shape\b/, + message: 'missing WebSocket dialog-turn-started shape regression test', + }, + ], + }, + { + path: 'src/crates/contracts/events/src/agentic_projection_manifest.rs', + reason: + 'events contract must own the public AgenticEvent projection manifest, event versions, aggregate classification, and legacy transport allowlist', + patterns: [ + { + regex: /\bpub const AGENTIC_EVENT_PROJECTION_MANIFEST\b/, + message: 'missing public AgenticEvent projection manifest', + }, + { + regex: /\bpub fn public_agentic_event_projection_manifest\b/, + message: 'missing public AgenticEvent projection manifest accessor', + }, + { + regex: /\bpub fn is_legacy_websocket_agentic_event_type\b/, + message: 'missing manifest-owned legacy WebSocket event allowlist helper', + }, + { + regex: /\bpublic_event_projection_manifest_describes_projected_events_and_websocket_allowlist\b/, + message: 'missing public projection manifest regression test', + }, + ], + }, + { + path: 'src/crates/adapters/transport/src/adapters/tauri.rs', + reason: + 'Tauri transport adapter must only deliver projected events and must not own agentic event field mapping', + patterns: [ + { + regex: /\bproject_agentic_frontend_event\b/, + message: 'missing shared frontend projection usage in Tauri transport', + }, + { + regex: /\.emit\(projected\.event_name\.as_str\(\), projected\.payload\)/, + message: 'Tauri transport must emit projected event name and payload', + }, + ], + }, + { + path: 'src/crates/adapters/transport/src/adapters/websocket.rs', + reason: + 'WebSocket transport adapter must consume shared event projection while preserving the legacy WebSocket event allowlist', + patterns: [ + { + regex: /\bproject_agentic_frontend_event\b/, + message: 'missing shared frontend projection usage in WebSocket transport', + }, + { + regex: /\bis_legacy_websocket_agentic_event_type\b/, + message: 'missing legacy WebSocket agentic event allowlist', + }, + { + regex: /\bwebsocket_keeps_legacy_agentic_event_allowlist\b/, + message: 'missing WebSocket legacy event allowlist regression', + }, + ], + }, + { + path: 'src/crates/contracts/core-types/src/lsp.rs', + reason: + 'core-types must own shared LSP protocol DTOs, plugin manifest wire contracts, and plugin runtime target DTOs', + patterns: [ + { + regex: /\bpub struct LspPlugin\b/, + message: 'missing LSP plugin manifest contract owner', + }, + { + regex: /\bpub struct ServerConfig\b/, + message: 'missing LSP plugin server config contract', + }, + { + regex: /\bpub enum JsonRpcMessage\b/, + message: 'missing LSP JSON-RPC wire DTO contract', + }, + { + regex: /\bpub enum PluginSource\b/, + message: 'missing LSP plugin source contract', + }, + { + regex: /\bpub struct LspPluginRuntimeTarget\b/, + message: 'missing LSP plugin runtime target contract', + }, + { + regex: /\bpub fn resolve_lsp_plugin_command_for_target\b/, + message: 'missing pure LSP plugin command placeholder resolver', + }, + ], + }, + { + path: 'src/crates/services/services-core/src/lsp.rs', + reason: + 'services-core must own pure LSP plugin registry and current-target mapping rules', + patterns: [ + { + regex: /\bpub struct PluginRegistry\b/, + message: 'missing services-owned LSP plugin registry', + }, + { + regex: /\bpub struct LspSupportedExtensions\b/, + message: 'missing supported extension summary owner', + }, + { + regex: /\bpub fn resolve_plugin_command_for_current_target\b/, + message: 'missing current-target LSP plugin command resolver', + }, + ], + }, + { + path: 'src/crates/assembly/core/src/service/lsp/types.rs', + reason: + 'core LSP types path must remain a compatibility facade over core-types', + patterns: [ + { + regex: /\bpub use bitfun_core_types::lsp::\*/, + message: 'core LSP types must re-export bitfun-core-types contracts', + }, + ], + }, + { + path: 'src/crates/assembly/core/src/service/lsp/registry.rs', + reason: + 'core LSP registry path must remain a compatibility facade over services-core', + patterns: [ + { + regex: /\bpub use bitfun_services_core::lsp::\{/, + message: 'core LSP registry must re-export services-core registry', + }, + ], + }, + { + path: 'src/crates/contracts/core-types/tests/lsp_contracts.rs', + reason: + 'core-types must keep LSP manifest serialization, default-value, and placeholder regressions', + patterns: [ + { + regex: /\blsp_plugin_manifest_defaults_preserve_legacy_shape\b/, + message: 'missing LSP manifest default regression', + }, + { + regex: /\blsp_capability_config_missing_fields_default_to_false\b/, + message: 'missing LSP capability default regression', + }, + { + regex: /\blsp_plugin_command_placeholder_resolution_is_contract_owned\b/, + message: 'missing LSP command placeholder contract regression', + }, + ], + }, + { + path: 'src/crates/services/services-core/tests/lsp_plugin_registry_contracts.rs', + reason: + 'services-core must keep behavior-equivalence contracts for LSP plugin registry and command mapping', + patterns: [ + { + regex: /\bregistry_preserves_language_extension_and_file_path_lookup\b/, + message: 'missing LSP registry lookup regression', + }, + { + regex: /\bregistry_unregister_removes_plugin_indexes\b/, + message: 'missing LSP registry unregister regression', + }, + { + regex: /\bregistry_unregister_preserves_indexes_owned_by_newer_plugin\b/, + message: + 'missing LSP registry overlapping-plugin unregister regression', + }, + { + regex: /\bregistry_duplicate_and_missing_errors_keep_legacy_messages\b/, + message: 'missing LSP registry error-message regression', + }, + { + regex: /\bregistry_supported_extensions_matches_desktop_api_shape\b/, + message: 'missing LSP supported extension summary regression', + }, + { + regex: /\bplugin_command_placeholder_resolution_is_target_driven\b/, + message: 'missing LSP plugin command placeholder regression', + }, + ], + }, { path: 'src/crates/execution/runtime-services/src/lib.rs', reason: @@ -179,7 +390,7 @@ export const requiredContentRules = [ message: 'missing agent session delete entrypoint', }, { - regex: /\bresolve_session_workspace_path\b/, + regex: /\bresolve_session_workspace_binding\b/, message: 'missing agent session workspace resolution entrypoint', }, { @@ -345,6 +556,133 @@ export const requiredContentRules = [ }, ], }, + { + path: 'src/crates/execution/agent-runtime/src/context_profile.rs', + reason: + 'agent-runtime must own provider-neutral context profile and model capability policy facts', + patterns: [ + { + regex: /\bpub enum ContextProfile\b/, + message: 'missing context profile fact', + }, + { + regex: /\bpub enum ModelCapabilityProfile\b/, + message: 'missing model capability profile fact', + }, + { + regex: /\bpub struct ContextProfilePolicy\b/, + message: 'missing context profile policy fact', + }, + { + regex: /\bfor_subagent_context_and_models\b/, + message: 'missing subagent context/model policy helper', + }, + { + regex: /\bmodel_capability_weak_for_mini\b/, + message: 'missing weak-model regression', + }, + ], + }, + { + path: 'src/crates/execution/agent-runtime/src/session_state.rs', + reason: + 'agent-runtime must own provider-neutral session state facts and event-label projection', + patterns: [ + { + regex: /\bpub enum SessionState\b/, + message: 'missing session state fact', + }, + { + regex: /\bpub enum ProcessingPhase\b/, + message: 'missing processing phase fact', + }, + { + regex: /\bdialog_state_fact\b/, + message: 'missing session state fact projection', + }, + { + regex: /\bsession_state_label_for_state\b/, + message: 'missing session state label projection', + }, + { + regex: /\bprocessing_state_serialization_stays_compatible\b/, + message: 'missing session state serialization regression', + }, + ], + }, + { + path: 'src/crates/execution/agent-runtime/src/session.rs', + reason: + 'agent-runtime must own provider-neutral session config, summary, and persisted state facts', + patterns: [ + { + regex: /\bpub struct Session\b/, + message: 'missing session fact', + }, + { + regex: /\bpub struct SessionConfig\b/, + message: 'missing session config fact', + }, + { + regex: /\bpub struct SessionSummary\b/, + message: 'missing session summary fact', + }, + { + regex: /\bpub use bitfun_core_types::SessionKind\b/, + message: 'missing session kind compatibility export', + }, + { + regex: /\bpub struct PersistedSessionStateFile\b/, + message: 'missing persisted session state sidecar fact', + }, + { + regex: /\bsanitize_persisted_session_state\b/, + message: 'missing persisted session state sanitization owner', + }, + { + regex: /\bpersisted_session_state_file_shape_stays_compatible\b/, + message: 'missing persisted session state wire-shape regression', + }, + ], + }, + { + path: 'src/crates/execution/agent-runtime/src/dialog_turn.rs', + reason: + 'agent-runtime must own provider-neutral dialog-turn id and statistics facts', + patterns: [ + { + regex: /\bpub fn new_turn_id\b/, + message: 'missing dialog-turn id helper', + }, + { + regex: /\bpub struct TurnStats\b/, + message: 'missing dialog-turn statistics fact', + }, + ], + }, + { + path: 'src/crates/execution/agent-runtime/src/side_question.rs', + reason: + 'agent-runtime must own runtime-only side-question cancellation and active-turn tracking', + patterns: [ + { + regex: /\bpub struct SideQuestionRuntime\b/, + message: 'missing side-question runtime owner', + }, + { + regex: /\bpub struct ActiveBtwTurn\b/, + message: 'missing active /btw turn fact', + }, + { + regex: /\bregister_btw_turn\b/, + message: 'missing active /btw turn registration', + }, + { + regex: /\bregistering_same_request_cancels_previous_token\b/, + message: 'missing side-question cancellation regression', + }, + ], + }, { path: 'src/crates/execution/agent-runtime/examples/sdk_minimal.rs', reason: @@ -836,6 +1174,10 @@ export const requiredContentRules = [ regex: /\bMobileWeb\b/, message: 'missing mobile web delivery profile coverage', }, + { + regex: /\bDeliveryProfile::Sdk\b/, + message: 'missing SDK delivery profile coverage', + }, { regex: /\bProductAssembler\b/, message: 'missing typed product assembler', @@ -848,6 +1190,10 @@ export const requiredContentRules = [ regex: /\bProductRuntimeParts\b/, message: 'missing product runtime parts output', }, + { + regex: /\binto_runtime_parts\b/, + message: 'missing owned runtime-parts handoff for SDK/product assembly consumers', + }, { regex: /\bfeature_groups_from_tool_provider_group_plan\b/, message: 'missing tool-provider feature group projection owner', @@ -885,6 +1231,42 @@ export const requiredContentRules = [ }, ], }, + { + path: 'src/crates/assembly/product-capabilities/tests/product_sdk_assembly.rs', + reason: + 'product-capabilities must prove product runtime parts can feed the SDK runtime without bitfun-core', + patterns: [ + { + regex: /\bproduct_runtime_parts_can_build_agent_runtime_sdk_without_core\b/, + message: 'missing product assembly to SDK runtime smoke', + }, + { + regex: + /\bsdk_delivery_profile_builds_minimal_agent_runtime_without_product_full_capabilities\b/, + message: 'missing SDK delivery profile minimal runtime smoke', + }, + { + regex: /\bProductAssembler::new\(\)/, + message: 'product SDK smoke must assemble through ProductAssembler', + }, + { + regex: /\binto_runtime_parts\b/, + message: 'product SDK smoke must consume owned ProductRuntimeParts', + }, + { + regex: /\bAgentRuntimeBuilder::new\(\)/, + message: 'product SDK smoke must build through the Agent Runtime SDK facade', + }, + { + regex: /\bDeliveryProfile::Cli\b/, + message: 'product SDK smoke must cover a product-full compatibility delivery profile', + }, + { + regex: /\bDeliveryProfile::Sdk\b/, + message: 'product SDK smoke must cover the no-direct-core SDK delivery profile', + }, + ], + }, { path: 'src/crates/execution/agent-runtime/src/agents.rs', reason: @@ -1240,6 +1622,14 @@ export const requiredContentRules = [ regex: /\bpub struct ToolConfirmationRequestFacts\b/, message: 'missing tool confirmation request facts', }, + { + regex: /\bpub struct ToolConfirmationGateFacts\b/, + message: 'missing tool confirmation gate facts', + }, + { + regex: /\bpub enum ToolConfirmationGatePlan\b/, + message: 'missing tool confirmation gate plan', + }, { regex: /\bpub enum ToolConfirmationPlan\b/, message: 'missing tool confirmation plan contract', @@ -1268,6 +1658,10 @@ export const requiredContentRules = [ regex: /\bpub fn resolve_tool_confirmation_plan\b/, message: 'missing tool confirmation plan resolver', }, + { + regex: /\bpub fn resolve_tool_confirmation_gate\b/, + message: 'missing tool confirmation gate resolver', + }, { regex: /\bpub fn resolve_confirmation_failure\b/, message: 'missing tool confirmation failure resolver', @@ -1314,6 +1708,14 @@ export const requiredContentRules = [ regex: /\bconfirmation_plan_requires_permission_only_when_both_flags_are_true\b/, message: 'missing tool confirmation gate regression', }, + { + regex: /\bconfirmation_gate_preserves_skip_policy_precedence\b/, + message: 'missing tool confirmation skip-policy regression', + }, + { + regex: /\bconfirmation_gate_requires_confirmation_only_for_permissioned_tools\b/, + message: 'missing tool confirmation permissioned-tool regression', + }, { regex: /\bconfirmation_plan_preserves_legacy_no_timeout_one_year_deadline\b/, message: 'missing tool confirmation no-timeout regression', @@ -1914,8 +2316,8 @@ export const requiredContentRules = [ message: 'missing port-backed cron target session list call', }, { - regex: /\bresolve_session_workspace_path\b/, - message: 'missing port-backed cron target workspace resolution call', + regex: /\bresolve_session_workspace_binding\b/, + message: 'missing port-backed cron target workspace binding resolution call', }, ], }, @@ -2226,11 +2628,61 @@ export const requiredContentRules = [ 'core event types must preserve legacy import path while agent-runtime owns session-state labels', patterns: [ { - regex: /bitfun_agent_runtime::events::session_state_label/, + regex: /bitfun_agent_runtime::session_state::session_state_label_for_state/, message: 'missing session-state label owner delegation', }, ], }, + { + path: 'src/crates/execution/agent-runtime/src/session_state_manager.rs', + reason: + 'agent-runtime owns provider-neutral session state storage, transition helpers, and SessionStateChanged event projection', + patterns: [ + { + regex: /\bpub struct SessionStateManager\b/, + message: 'missing agent-runtime session state manager owner', + }, + { + regex: /\bDashMap/, + message: 'missing session state storage owner', + }, + { + regex: /\bEventQueue\b/, + message: 'missing runtime event queue integration', + }, + { + regex: /\bAgenticEvent::SessionStateChanged\b/, + message: 'missing SessionStateChanged event projection', + }, + { + regex: /\bsession_state_label_for_state\b/, + message: 'missing stable session-state label projection', + }, + { + regex: /\bcan_start_new_turn\b/, + message: 'missing turn-start guard owner', + }, + { + regex: /\bsession_state_manager_emits_compatible_state_change_events\b/, + message: 'missing session state event compatibility test', + }, + { + regex: /\bsession_state_manager_keeps_turn_start_guard_semantics\b/, + message: 'missing session state guard compatibility test', + }, + ], + }, + { + path: 'src/crates/assembly/core/src/agentic/coordination/state_manager.rs', + reason: + 'core session state manager path must preserve legacy imports while agent-runtime owns the implementation', + patterns: [ + { + regex: /pub use bitfun_agent_runtime::session_state_manager::SessionStateManager;/, + message: 'missing SessionStateManager compatibility re-export', + }, + ], + }, { path: 'src/crates/assembly/core/src/agentic/agents/prompt_builder/user_context.rs', reason: @@ -3122,258 +3574,616 @@ export const requiredContentRules = [ message: 'core product-domain facade must enable the function-agent service owner feature it imports', }, { - regex: /product-domains = \[[^\]]*"bitfun-services-integrations\/miniapp-runtime"[^\]]*\]/, - message: 'core product-domain facade must enable the MiniApp service owner feature it imports', + regex: /product-domains = \[[^\]]*"bitfun-services-integrations\/miniapp-runtime"[^\]]*\]/, + message: 'core product-domain facade must enable the MiniApp service owner feature it imports', + }, + { + regex: + /bitfun-product-domains = \{ path = "\.\.\/\.\.\/contracts\/product-domains", default-features = false, optional = true \}/, + message: + 'bitfun-product-domains dependency must stay optional and not force product-full outside the core feature graph', + }, + { + regex: + /bitfun-product-capabilities = \{ path = "\.\.\/product-capabilities", default-features = false, optional = true \}/, + message: + 'bitfun-product-capabilities dependency must stay optional and not force product-full outside the core feature graph', + }, + { + regex: /"dep:bitfun-tool-packs"/, + message: 'core tool-packs feature must explicitly enable the optional dependency', + }, + { + regex: /"bitfun-tool-packs\/product-full"/, + message: 'core product-full must explicitly enable tool pack product features', + }, + { + regex: /"bitfun-services-integrations\/product-full"/, + message: 'core product-full must explicitly enable integration product features', + }, + { + regex: /"dep:bitfun-product-domains"/, + message: 'core product-domains feature must explicitly enable the optional dependency', + }, + { + regex: /"dep:bitfun-product-capabilities"/, + message: + 'core product-capabilities feature must explicitly enable the optional dependency', + }, + { + regex: /"bitfun-product-domains\/product-full"/, + message: 'core product-full must explicitly enable product-domain features', + }, + ], + }, + { + path: 'src/crates/assembly/core/src/lib.rs', + reason: + 'no-default bitfun-core must keep product runtime surfaces behind explicit features', + patterns: [ + { + regex: /#\[cfg\(feature = "product-full"\)\]\s*pub mod agentic\b/s, + message: 'agentic runtime must stay behind product-full for no-default builds', + }, + { + regex: /#\[cfg\(feature = "product-domains"\)\]\s*pub mod function_agents\b/s, + message: 'function-agent product domain facade must stay behind product-domains', + }, + { + regex: /#\[cfg\(feature = "product-domains"\)\]\s*pub mod miniapp\b/s, + message: 'MiniApp product domain facade must stay behind product-domains', + }, + { + regex: /#\[cfg\(feature = "service-integrations"\)\]\s*pub\(crate\) mod service_agent_runtime\b/s, + message: 'service agent runtime owner assembly must stay behind service-integrations', + }, + ], + }, + { + path: 'src/crates/assembly/core/src/infrastructure/mod.rs', + reason: 'concrete AI adapter runtime and debug ingest HTTP server must stay out of no-default core builds', + patterns: [ + { + regex: /#\[cfg\(feature = "ai-adapter-runtime"\)\]\s*pub mod ai\b/s, + message: 'AI client runtime must stay behind ai-adapter-runtime', + }, + { + regex: /#\[cfg\(feature = "ai-adapter-runtime"\)\]\s*pub mod cli_credentials\b/s, + message: 'AI CLI credential runtime must stay behind ai-adapter-runtime', + }, + { + regex: /#\[cfg\(feature = "product-full"\)\]\s*pub mod debug_log\b/s, + message: 'debug ingest HTTP server must stay behind product-full', + }, + ], + }, + { + path: 'src/crates/assembly/core/src/util/types/ai.rs', + reason: 'legacy AI implementation DTO re-exports must not force AI adapters into no-default core builds', + patterns: [ + { + regex: /pub use bitfun_core_types::\{ConnectionTestMessageCode, ConnectionTestResult, RemoteModelInfo\};/s, + message: 'stable AI DTOs must be re-exported from core-types', + }, + { + regex: /#\[cfg\(feature = "ai-adapter-runtime"\)\]\s*pub use bitfun_ai_adapters::types::\{GeminiResponse, GeminiUsage\};/s, + message: 'legacy Gemini implementation DTOs must stay behind ai-adapter-runtime', + }, + ], + }, + { + path: 'src/crates/assembly/core/src/service/mod.rs', + reason: + 'service integration and agent-runtime surfaces must not compile in no-default core builds', + patterns: [ + { + regex: /#\[cfg\(feature = "service-integrations"\)\]\s*pub mod git\b/s, + message: 'git service facade must stay behind service-integrations', + }, + { + regex: /#\[cfg\(feature = "service-integrations"\)\]\s*pub mod mcp\b/s, + message: 'MCP service facade must stay behind service-integrations', + }, + { + regex: /#\[cfg\(feature = "service-integrations"\)\]\s*pub mod remote_connect\b/s, + message: 'remote-connect service facade must stay behind service-integrations', + }, + { + regex: /#\[cfg\(feature = "service-integrations"\)\]\s*pub mod review_platform\b/s, + message: 'review platform facade must stay behind service-integrations', + }, + { + regex: /#\[cfg\(feature = "product-full"\)\]\s*pub mod search\b/s, + message: 'workspace search facade must stay behind product-full', + }, + { + regex: /#\[cfg\(feature = "product-full"\)\]\s*pub use search::/s, + message: 'workspace search exports must stay behind product-full', + }, + { + regex: /#\[cfg\(feature = "product-full"\)\]\s*pub mod snapshot\b/s, + message: 'snapshot service must stay behind product-full until tool-runtime ownership is split', + }, + ], + }, + { + path: 'src/crates/assembly/core/src/service/config/mod.rs', + reason: + 'mode config canonicalization depends on product agent/tool registries and must stay out of no-default builds', + patterns: [ + { + regex: /#\[cfg\(feature = "product-full"\)\]\s*pub mod mode_config_canonicalizer\b/s, + message: 'mode config canonicalizer must stay behind product-full', + }, + ], + }, + { + path: 'src/crates/assembly/core/src/service/workspace/manager.rs', + reason: + 'workspace metadata may omit git worktree enrichment when service integrations are disabled', + patterns: [ + { + regex: /#\[cfg\(feature = "service-integrations"\)\]\s*use crate::service::git::GitService\b/s, + message: 'GitService import must stay gated for no-default builds', + }, + { + regex: /#\[cfg\(not\(feature = "service-integrations"\)\)\]\s*\{\s*let _ = workspace_root;\s*return None;\s*\}/s, + message: 'no-default worktree enrichment fallback must remain explicit', + }, + ], + }, + { + path: 'src/crates/assembly/core/src/service/workspace_runtime/service.rs', + reason: + 'workspace runtime binding helpers may depend on agentic runtime only in full product builds and must delegate legacy session-store migration to services-core', + patterns: [ + { + regex: /#\[cfg\(feature = "product-full"\)\]\s*use crate::agentic::WorkspaceBinding\b/s, + message: 'WorkspaceBinding import must stay gated for no-default builds', + }, + { + regex: /#\[cfg\(feature = "product-full"\)\]\s*pub async fn ensure_runtime_for_workspace_binding\b/s, + message: 'WorkspaceBinding runtime helper must stay behind product-full', + }, + { + regex: /\bmerge_legacy_session_store\b/, + message: 'workspace runtime session merge must delegate to services-core', + }, + { + regex: /\bmove_legacy_path\b/, + message: 'workspace runtime legacy path movement must delegate to services-core', + }, + { + regex: /\bsession_store_migration_error\b/, + message: 'workspace runtime must keep BitFunError compatibility mapping at the facade boundary', + }, + ], + }, + { + path: 'src/crates/execution/tool-execution/src/background_command_output.rs', + reason: + 'tool-runtime must own reusable background exec-command output capture state, cursors, retention, and lifecycle metadata', + patterns: [ + { + regex: /\bpub struct BackgroundCommandOutputCapture\b/, + message: 'missing background command output capture owner', + }, + { + regex: /\bpub enum BackgroundCommandOutputStatus\b/, + message: 'missing background command output status contract', + }, + { + regex: /\bpub struct BackgroundCommandOutputMetadata\b/, + message: 'missing background command output metadata contract', + }, + { + regex: /\bpub fn background_command_output_capture\b/, + message: 'missing background command output global entry', + }, + { + regex: /\bBACKGROUND_COMMAND_OUTPUT_CAPTURE_LIMIT_BYTES\b/, + message: 'missing background command output retention limit', + }, + { + regex: /\bbackground_command_output_reads_snapshot_then_incremental_chunks\b/, + message: 'missing background command output snapshot/incremental regression', + }, + ], + }, + { + path: 'src/crates/execution/tool-execution/src/exec_command.rs', + reason: + 'tool-runtime must own provider-neutral ExecCommand presentation, shell/env policy, lifecycle facts, control facts, completion shape, and session-not-found result builders while core keeps concrete process managers', + patterns: [ + { + regex: /\bpub const EXEC_COMMAND_POWERSHELL_UTF8_OUTPUT_PREFIX\b/, + message: 'missing ExecCommand PowerShell UTF-8 shell policy owner', + }, + { + regex: /\bpub const EXEC_COMMAND_DEFAULT_YIELD_TIME_MS\b/, + message: 'missing ExecCommand default wait policy owner', + }, + { + regex: /\bpub enum ExecCommandShellKind\b/, + message: 'missing provider-neutral ExecCommand shell kind owner', + }, + { + regex: /\bCustom\(String\)/, + message: 'missing ExecCommand custom shell name preservation', + }, + { + regex: /\bpub struct ExecCommandRemoteShell\b/, + message: 'missing provider-neutral ExecCommand remote shell probe owner', + }, + { + regex: /\bpub const REMOTE_EXEC_SHELL_PROBE_TIMEOUT_MS\b/, + message: 'missing provider-neutral ExecCommand remote shell probe timeout owner', + }, + { + regex: /\bpub fn remote_exec_shell_probe_command\b/, + message: 'missing provider-neutral ExecCommand remote shell probe command owner', + }, + { + regex: /\bpub fn fallback_remote_exec_shell\b/, + message: 'missing provider-neutral ExecCommand remote shell fallback owner', + }, + { + regex: /\bpub struct ExecCommandRemoteEnvSnapshot\b/, + message: 'missing provider-neutral ExecCommand remote env snapshot owner', + }, + { + regex: /\bpub struct ExecCommandRemoteEnvSnapshotCapturePolicy\b/, + message: 'missing provider-neutral ExecCommand remote env capture policy owner', + }, + { + regex: /\bpub struct ExecCommandRemoteEnvSnapshotCacheKey\b/, + message: 'missing provider-neutral ExecCommand remote env snapshot cache key owner', + }, + { + regex: /\bpub struct ExecCommandRemoteEnvSnapshotCache\b/, + message: 'missing provider-neutral ExecCommand remote env snapshot cache owner', + }, + { + regex: /\bpub fn remote_exec_env_snapshot_capture_policy\b/, + message: 'missing provider-neutral ExecCommand remote env capture policy builder', + }, + { + regex: /\bpub fn exec_command_argv_for_shell\b/, + message: 'missing provider-neutral ExecCommand shell argv owner', + }, + { + regex: /\bpub fn remote_exec_login_shell_command\b/, + message: 'missing provider-neutral ExecCommand remote login command owner', + }, + { + regex: /\bpub fn remote_exec_non_tty_control_wrapper\b/, + message: 'missing provider-neutral ExecCommand remote control wrapper owner', + }, + { + regex: /\bpub fn parse_remote_exec_env_snapshot_output\b/, + message: 'missing provider-neutral ExecCommand remote env parser owner', + }, + { + regex: /\bpub enum ExecCommandLifecycleStatus\b/, + message: 'missing provider-neutral ExecCommand lifecycle status owner', + }, + { + regex: /\bpub fn exec_command_lifecycle_background_output_status\b/, + message: 'missing provider-neutral ExecCommand lifecycle background status owner', + }, + { + regex: /\bpub enum ExecCommandControlAction\b/, + message: 'missing provider-neutral exec control action contract', + }, + { + regex: /\bpub struct ExecCommandControlRequest\b/, + message: 'missing provider-neutral exec control request contract', + }, + { + regex: /\bpub struct ExecCommandRunInput\b/, + message: 'missing provider-neutral ExecCommand input parser contract', + }, + { + regex: /\bpub struct WriteStdinInput\b/, + message: 'missing provider-neutral WriteStdin input parser contract', + }, + { + regex: /\bpub struct ExecCommandControlToolInput\b/, + message: 'missing provider-neutral ExecControl input parser contract', + }, + { + regex: /\bpub struct ExecCommandResultFields\b/, + message: 'missing provider-neutral ExecCommand result fields contract', + }, + { + regex: /\bpub struct ExecCommandShellMetadata\b/, + message: 'missing provider-neutral ExecCommand shell metadata contract', + }, + { + regex: /\bpub fn exec_command_result_value\b/, + message: 'missing ExecCommand result value owner', + }, + { + regex: /\bpub fn write_stdin_result_value\b/, + message: 'missing WriteStdin result value owner', + }, + { + regex: /\bpub fn write_stdin_session_not_found_result\b/, + message: 'missing WriteStdin session-not-found result owner', + }, + { + regex: /\bpub fn exec_control_result_value\b/, + message: 'missing ExecControl result value owner', + }, + { + regex: /\bpub fn render_exec_command_response_for_assistant\b/, + message: 'missing ExecCommand assistant response owner', + }, + { + regex: /\bpub fn render_write_stdin_response_for_assistant\b/, + message: 'missing WriteStdin assistant response owner', + }, + { + regex: /\bpub fn exec_control_session_not_found_result\b/, + message: 'missing ExecControl session-not-found result owner', + }, + { + regex: /\bpub fn exec_command_background_output_status\b/, + message: 'missing ExecCommand background-output status owner', + }, + { + regex: /\bcompletion_value_uses_stable_snake_case_shape\b/, + message: 'missing ExecCommand completion shape regression', + }, + { + regex: /\bbackground_output_status_maps_terminal_completion_without_core_types\b/, + message: 'missing ExecCommand background status regression', + }, + { + regex: /\bremote_login_shell_command_applies_snapshot_then_tool_env\b/, + message: 'missing ExecCommand remote env merge regression', + }, + { + regex: /\bremote_shell_probe_and_env_snapshot_are_provider_neutral\b/, + message: 'missing ExecCommand remote shell/env parser regression', + }, + { + regex: /\bremote_env_snapshot_capture_policy_keeps_existing_bounds\b/, + message: 'missing ExecCommand remote env capture policy regression', + }, + { + regex: /\bremote_env_snapshot_cache_owns_key_and_ttl_policy\b/, + message: 'missing ExecCommand remote env snapshot cache regression', }, { - regex: - /bitfun-product-domains = \{ path = "\.\.\/\.\.\/contracts\/product-domains", default-features = false, optional = true \}/, - message: - 'bitfun-product-domains dependency must stay optional and not force product-full outside the core feature graph', + regex: /\bremote_shell_probe_skips_non_posix_shells\b/, + message: 'missing ExecCommand remote shell POSIX compatibility regression', }, { - regex: - /bitfun-product-capabilities = \{ path = "\.\.\/product-capabilities", default-features = false, optional = true \}/, - message: - 'bitfun-product-capabilities dependency must stay optional and not force product-full outside the core feature graph', + regex: /\bremote_shell_probe_preserves_unknown_shell_name_as_posix_compatible\b/, + message: 'missing ExecCommand custom remote shell metadata regression', }, { - regex: /"dep:bitfun-tool-packs"/, - message: 'core tool-packs feature must explicitly enable the optional dependency', + regex: /\bexec_command_input_policy_applies_defaults_without_trimming_command\b/, + message: 'missing ExecCommand input/default wait regression', }, { - regex: /"bitfun-tool-packs\/product-full"/, - message: 'core product-full must explicitly enable tool pack product features', + regex: /\bwrite_stdin_input_policy_applies_poll_defaults\b/, + message: 'missing WriteStdin input/default wait regression', }, { - regex: /"bitfun-services-integrations\/product-full"/, - message: 'core product-full must explicitly enable integration product features', + regex: /\bexec_control_input_policy_keeps_wait_optional\b/, + message: 'missing ExecControl input/default wait regression', }, { - regex: /"dep:bitfun-product-domains"/, - message: 'core product-domains feature must explicitly enable the optional dependency', + regex: /\bexec_command_result_builder_preserves_existing_wire_shape\b/, + message: 'missing ExecCommand result shape regression', }, { - regex: /"dep:bitfun-product-capabilities"/, - message: - 'core product-capabilities feature must explicitly enable the optional dependency', + regex: /\bwrite_stdin_and_control_result_builders_preserve_remote_shape\b/, + message: 'missing WriteStdin/ExecControl result shape regression', }, { - regex: /"bitfun-product-domains\/product-full"/, - message: 'core product-full must explicitly enable product-domain features', + regex: /\blifecycle_status_has_provider_neutral_names_and_background_statuses\b/, + message: 'missing ExecCommand lifecycle status regression', }, ], }, { - path: 'src/crates/assembly/core/src/lib.rs', + path: 'src/crates/assembly/core/src/agentic/tools/implementations/exec_command/completion.rs', reason: - 'no-default bitfun-core must keep product runtime surfaces behind explicit features', + 'core exec_command adapter must keep concrete local/remote completion mapping centralized while result shape stays in tool-runtime', patterns: [ { - regex: /#\[cfg\(feature = "product-full"\)\]\s*pub mod agentic\b/s, - message: 'agentic runtime must stay behind product-full for no-default builds', + regex: /\bpub\(super\) fn exec_command_local_completion\b/, + message: 'missing centralized local completion mapping adapter', }, { - regex: /#\[cfg\(feature = "product-domains"\)\]\s*pub mod function_agents\b/s, - message: 'function-agent product domain facade must stay behind product-domains', + regex: /\bpub\(super\) fn exec_command_remote_completion\b/, + message: 'missing centralized remote completion mapping adapter', }, { - regex: /#\[cfg\(feature = "product-domains"\)\]\s*pub mod miniapp\b/s, - message: 'MiniApp product domain facade must stay behind product-domains', + regex: /\bExecCommandCompletionStatus::Interrupted\b/, + message: 'missing completion status mapping coverage', }, { - regex: /#\[cfg\(feature = "service-integrations"\)\]\s*pub\(crate\) mod service_agent_runtime\b/s, - message: 'service agent runtime owner assembly must stay behind service-integrations', + regex: /\bExecCommandCompletionSource::OutOfBandControl\b/, + message: 'missing completion source mapping coverage', }, ], }, { - path: 'src/crates/assembly/core/src/infrastructure/mod.rs', - reason: 'concrete AI adapter runtime and debug ingest HTTP server must stay out of no-default core builds', + path: 'src/crates/assembly/core/src/agentic/tools/implementations/exec_command/command.rs', + reason: + 'core exec_command adapter must delegate provider-neutral shell/env/lifecycle policy to tool-runtime and keep only concrete process/remote wiring', patterns: [ { - regex: /#\[cfg\(feature = "ai-adapter-runtime"\)\]\s*pub mod ai\b/s, - message: 'AI client runtime must stay behind ai-adapter-runtime', + regex: /\bexec_command_argv_for_shell\b/, + message: 'missing tool-runtime shell argv delegation in core exec_command adapter', }, { - regex: /#\[cfg\(feature = "ai-adapter-runtime"\)\]\s*pub mod cli_credentials\b/s, - message: 'AI CLI credential runtime must stay behind ai-adapter-runtime', + regex: /\bparse_remote_exec_shell_probe_output\b/, + message: 'missing tool-runtime remote shell probe delegation in core exec_command adapter', }, { - regex: /#\[cfg\(feature = "product-full"\)\]\s*pub mod debug_log\b/s, - message: 'debug ingest HTTP server must stay behind product-full', + regex: /\bremote_exec_shell_probe_command\b/, + message: 'missing tool-runtime remote shell probe command delegation in core exec_command adapter', }, - ], - }, - { - path: 'src/crates/assembly/core/src/util/types/ai.rs', - reason: 'legacy AI implementation DTO re-exports must not force AI adapters into no-default core builds', - patterns: [ { - regex: /pub use bitfun_core_types::\{ConnectionTestMessageCode, ConnectionTestResult, RemoteModelInfo\};/s, - message: 'stable AI DTOs must be re-exported from core-types', + regex: /\bfallback_remote_exec_shell\b/, + message: 'missing tool-runtime remote shell fallback delegation in core exec_command adapter', }, { - regex: /#\[cfg\(feature = "ai-adapter-runtime"\)\]\s*pub use bitfun_ai_adapters::types::\{GeminiResponse, GeminiUsage\};/s, - message: 'legacy Gemini implementation DTOs must stay behind ai-adapter-runtime', + regex: /\bremote_exec_login_shell_command\b/, + message: 'missing tool-runtime remote login command delegation in core exec_command adapter', }, - ], - }, - { - path: 'src/crates/assembly/core/src/service/mod.rs', - reason: - 'service integration and agent-runtime surfaces must not compile in no-default core builds', - patterns: [ { - regex: /#\[cfg\(feature = "service-integrations"\)\]\s*pub mod git\b/s, - message: 'git service facade must stay behind service-integrations', + regex: /\bremote_exec_non_tty_control_wrapper\b/, + message: 'missing tool-runtime remote non-TTY wrapper delegation in core exec_command adapter', }, { - regex: /#\[cfg\(feature = "service-integrations"\)\]\s*pub mod mcp\b/s, - message: 'MCP service facade must stay behind service-integrations', + regex: /\bexec_command_lifecycle_status_name\b/, + message: 'missing tool-runtime lifecycle status-name delegation in core exec_command adapter', }, { - regex: /#\[cfg\(feature = "service-integrations"\)\]\s*pub mod remote_connect\b/s, - message: 'remote-connect service facade must stay behind service-integrations', + regex: /\bexec_command_lifecycle_background_output_status\b/, + message: 'missing tool-runtime lifecycle output-status delegation in core exec_command adapter', }, { - regex: /#\[cfg\(feature = "service-integrations"\)\]\s*pub mod review_platform\b/s, - message: 'review platform facade must stay behind service-integrations', + regex: /\bexec_command_run_input_from_input\b/, + message: 'missing tool-runtime ExecCommand input delegation in core exec_command adapter', }, { - regex: /#\[cfg\(feature = "product-full"\)\]\s*pub mod search\b/s, - message: 'workspace search facade must stay behind product-full', + regex: /\bexec_command_result_value\b/, + message: 'missing tool-runtime ExecCommand result builder delegation in core exec_command adapter', }, { - regex: /#\[cfg\(feature = "product-full"\)\]\s*pub use search::/s, - message: 'workspace search exports must stay behind product-full', + regex: /\bexec_command_local_completion\b/, + message: 'missing shared completion mapping in core exec_command adapter', }, { - regex: /#\[cfg\(feature = "product-full"\)\]\s*pub mod snapshot\b/s, - message: 'snapshot service must stay behind product-full until tool-runtime ownership is split', + regex: /\bexec_command_remote_completion\b/, + message: 'missing shared remote completion mapping in core exec_command adapter', }, - ], - }, - { - path: 'src/crates/assembly/core/src/service/config/mod.rs', - reason: - 'mode config canonicalization depends on product agent/tool registries and must stay out of no-default builds', - patterns: [ { - regex: /#\[cfg\(feature = "product-full"\)\]\s*pub mod mode_config_canonicalizer\b/s, - message: 'mode config canonicalizer must stay behind product-full', + regex: /\bExecCommandShellMetadata\b/, + message: 'missing tool-runtime ExecCommand shell metadata delegation in core exec_command adapter', + }, + { + regex: /\bremote_shell_probe_preserves_unknown_shell_metadata\b/, + message: 'missing core custom remote shell metadata regression', }, ], }, { - path: 'src/crates/assembly/core/src/service/workspace/manager.rs', + path: 'src/crates/assembly/core/src/agentic/tools/implementations/exec_command/stdin.rs', reason: - 'workspace metadata may omit git worktree enrichment when service integrations are disabled', + 'core WriteStdin adapter must delegate provider-neutral input/default wait and result shape policy to tool-runtime', patterns: [ { - regex: /#\[cfg\(feature = "service-integrations"\)\]\s*use crate::service::git::GitService\b/s, - message: 'GitService import must stay gated for no-default builds', + regex: /\bwrite_stdin_input_from_input\b/, + message: 'missing tool-runtime WriteStdin input delegation in core adapter', }, { - regex: /#\[cfg\(not\(feature = "service-integrations"\)\)\]\s*\{\s*let _ = workspace_root;\s*return None;\s*\}/s, - message: 'no-default worktree enrichment fallback must remain explicit', + regex: /\bwrite_stdin_input_validation_message\b/, + message: 'missing tool-runtime WriteStdin validation delegation in core adapter', + }, + { + regex: /\bwrite_stdin_result_value\b/, + message: 'missing tool-runtime WriteStdin result builder delegation in core adapter', + }, + { + regex: /\bwrite_stdin_session_not_found_result\b/, + message: 'missing tool-runtime WriteStdin session-not-found delegation in core adapter', + }, + { + regex: /\bexec_command_local_completion\b/, + message: 'missing shared local completion mapping in core WriteStdin adapter', + }, + { + regex: /\bexec_command_remote_completion\b/, + message: 'missing shared remote completion mapping in core WriteStdin adapter', }, ], }, { - path: 'src/crates/assembly/core/src/service/workspace_runtime/service.rs', + path: 'src/crates/assembly/core/src/agentic/tools/implementations/exec_command/control.rs', reason: - 'workspace runtime binding helpers may depend on agentic runtime only in full product builds and must delegate legacy session-store migration to services-core', + 'core ExecControl adapter must delegate provider-neutral input/default wait and result shape policy to tool-runtime', patterns: [ { - regex: /#\[cfg\(feature = "product-full"\)\]\s*use crate::agentic::WorkspaceBinding\b/s, - message: 'WorkspaceBinding import must stay gated for no-default builds', + regex: /\bexec_command_control_tool_input_from_input\b/, + message: 'missing tool-runtime ExecControl input delegation in core adapter', }, { - regex: /#\[cfg\(feature = "product-full"\)\]\s*pub async fn ensure_runtime_for_workspace_binding\b/s, - message: 'WorkspaceBinding runtime helper must stay behind product-full', + regex: /\bexec_command_control_tool_input_validation_message\b/, + message: 'missing tool-runtime ExecControl validation delegation in core adapter', }, { - regex: /\bmerge_legacy_session_store\b/, - message: 'workspace runtime session merge must delegate to services-core', + regex: /\bexec_control_result_value\b/, + message: 'missing tool-runtime ExecControl result builder delegation in core adapter', }, { - regex: /\bmove_legacy_path\b/, - message: 'workspace runtime legacy path movement must delegate to services-core', + regex: /\bexec_command_local_completion\b/, + message: 'missing shared local completion mapping in core ExecControl adapter', }, { - regex: /\bsession_store_migration_error\b/, - message: 'workspace runtime must keep BitFunError compatibility mapping at the facade boundary', + regex: /\bexec_command_remote_completion\b/, + message: 'missing shared remote completion mapping in core ExecControl adapter', }, ], }, { - path: 'src/crates/execution/tool-execution/src/background_command_output.rs', + path: 'src/crates/assembly/core/src/agentic/tools/implementations/exec_command/env_snapshot.rs', reason: - 'tool-runtime must own reusable background exec-command output capture state, cursors, retention, and lifecycle metadata', + 'core exec_command env snapshot adapter must delegate snapshot command and parsing policy to tool-runtime', patterns: [ { - regex: /\bpub struct BackgroundCommandOutputCapture\b/, - message: 'missing background command output capture owner', + regex: /\bremote_exec_env_snapshot_command\b/, + message: 'missing tool-runtime remote env snapshot command delegation in core adapter', }, { - regex: /\bpub enum BackgroundCommandOutputStatus\b/, - message: 'missing background command output status contract', + regex: /\bparse_remote_exec_env_snapshot_output\b/, + message: 'missing tool-runtime remote env snapshot parsing delegation in core adapter', }, { - regex: /\bpub struct BackgroundCommandOutputMetadata\b/, - message: 'missing background command output metadata contract', + regex: /\bremote_exec_env_snapshot_capture_policy\b/, + message: 'missing tool-runtime remote env snapshot capture policy delegation in core adapter', }, { - regex: /\bpub fn background_command_output_capture\b/, - message: 'missing background command output global entry', + regex: /\bExecCommandRemoteEnvSnapshotCache\b/, + message: 'missing tool-runtime remote env snapshot cache owner in core adapter', }, { - regex: /\bBACKGROUND_COMMAND_OUTPUT_CAPTURE_LIMIT_BYTES\b/, - message: 'missing background command output retention limit', + regex: /\bExecCommandRemoteEnvSnapshotCacheKey\b/, + message: 'missing tool-runtime remote env snapshot cache key owner in core adapter', }, { - regex: /\bbackground_command_output_reads_snapshot_then_incremental_chunks\b/, - message: 'missing background command output snapshot/incremental regression', + regex: /\bexec_command_shell_kind\b/, + message: 'missing shared shell kind mapping for core env snapshot adapter', }, ], }, { - path: 'src/crates/execution/tool-execution/src/exec_command.rs', + path: 'src/crates/assembly/core/src/agentic/tools/implementations/exec_command/shell_kind.rs', reason: - 'tool-runtime must own provider-neutral ExecCommand presentation, control facts, completion shape, and session-not-found result builders while core keeps concrete process managers', + 'core exec_command adapter must keep ShellType/tool-runtime shell-kind mapping centralized and preserve custom shell metadata', patterns: [ { - regex: /\bpub enum ExecCommandControlAction\b/, - message: 'missing provider-neutral exec control action contract', - }, - { - regex: /\bpub struct ExecCommandControlRequest\b/, - message: 'missing provider-neutral exec control request contract', - }, - { - regex: /\bpub fn render_exec_command_response_for_assistant\b/, - message: 'missing ExecCommand assistant response owner', - }, - { - regex: /\bpub fn render_write_stdin_response_for_assistant\b/, - message: 'missing WriteStdin assistant response owner', - }, - { - regex: /\bpub fn exec_control_session_not_found_result\b/, - message: 'missing ExecControl session-not-found result owner', + regex: /\bpub\(super\) fn exec_command_shell_kind\b/, + message: 'missing centralized ShellType to ExecCommandShellKind adapter', }, { - regex: /\bpub fn exec_command_background_output_status\b/, - message: 'missing ExecCommand background-output status owner', + regex: /\bpub\(super\) fn terminal_shell_type\b/, + message: 'missing centralized ExecCommandShellKind to ShellType adapter', }, { - regex: /\bcompletion_value_uses_stable_snake_case_shape\b/, - message: 'missing ExecCommand completion shape regression', + regex: /\bExecCommandShellKind::Custom\(name\.clone\(\)\)/, + message: 'missing custom shell name preservation in core shell-kind adapter', }, { - regex: /\bbackground_output_status_maps_terminal_completion_without_core_types\b/, - message: 'missing ExecCommand background status regression', + regex: /\bShellType::Custom\(name\)/, + message: 'missing custom shell metadata restoration in core shell-kind adapter', }, ], }, @@ -3411,11 +4221,11 @@ export const requiredContentRules = [ { path: 'src/crates/assembly/core/src/service/remote_ssh/mod.rs', reason: - 'core remote SSH compatibility facade must keep service-backed SSH surfaces behind the ssh-remote feature while preserving lightweight workspace identity helpers', + 'core remote SSH compatibility facade must keep concrete SSH surfaces behind the ssh-remote feature and re-export services-owned disabled stubs for lightweight builds', patterns: [ { - regex: /#\[cfg\(not\(feature = "ssh-remote"\)\)\]\s*mod disabled\b/s, - message: 'missing disabled remote SSH runtime surface for no-default builds', + regex: /#\[cfg\(not\(feature = "ssh-remote"\)\)\]\s*pub use bitfun_services_integrations::remote_ssh::\{/s, + message: 'missing services-owned disabled remote SSH re-export for no-default builds', }, { regex: /#\[cfg\(feature = "ssh-remote"\)\]\s*pub mod manager\b/s, @@ -3436,9 +4246,9 @@ export const requiredContentRules = [ ], }, { - path: 'src/crates/assembly/core/src/service/remote_ssh/disabled.rs', + path: 'src/crates/services/services-integrations/src/remote_ssh/disabled.rs', reason: - 'no-default core builds must expose explicit unsupported remote SSH stubs instead of compiling russh-backed runtime code', + 'services-integrations must own explicit unsupported remote SSH stubs for lightweight builds', patterns: [ { regex: /Remote SSH support is disabled; enable the `ssh-remote` feature/, @@ -3463,6 +4273,14 @@ export const requiredContentRules = [ reason: 'services-integrations remote_ssh must own concrete SSH/SFTP/PTY runtime behind the remote-ssh-concrete feature while keeping lightweight path/type contracts separate', patterns: [ + { + regex: /#\[cfg\(not\(feature = "remote-ssh-concrete"\)\)\]\s*mod disabled\b/s, + message: 'missing disabled remote SSH owner module', + }, + { + regex: /#\[cfg\(not\(feature = "remote-ssh-concrete"\)\)\]\s*pub use disabled::\{/s, + message: 'missing disabled remote SSH owner exports', + }, { regex: /#\[cfg\(feature = "remote-ssh-concrete"\)\]\s*pub mod manager\b/s, message: 'missing concrete SSH manager owner module', @@ -3578,6 +4396,18 @@ export const requiredContentRules = [ regex: /\bpub trait RuntimeEventSink\b/, message: 'missing runtime event sink contract', }, + { + regex: /\bpub trait PluginRuntimeClient\b/, + message: 'missing plugin runtime client boundary contract', + }, + { + regex: /\bpub enum PluginRuntimeBinding\b/, + message: 'missing plugin runtime binding boundary contract', + }, + { + regex: /\bpub struct DisabledPluginRuntimeClient\b/, + message: 'missing disabled plugin runtime client contract', + }, { regex: /\bpub struct AgentSessionCreateResult\b[\s\S]*\bpub session_name: String\b/, message: 'agent session create result must return the persisted session name', @@ -4182,6 +5012,100 @@ export const requiredContentRules = [ }, ], }, + { + path: 'src/crates/execution/tool-contracts/src/mcp_tool_bridge.rs', + reason: + 'agent-tools owns MCP tool bridge naming, descriptor, validation, presentation, and ToolResult shape contracts without depending on MCP transport concrete', + patterns: [ + { + regex: /\bpub fn build_mcp_tool_bridge_name\b/, + message: 'missing MCP tool bridge prompt-visible name builder', + }, + { + regex: /\bpub struct McpToolBridgeDefinition\b/, + message: 'missing MCP tool bridge descriptor contract', + }, + { + regex: /\bpub struct McpToolBridgeBehaviorHints\b/, + message: 'missing MCP tool bridge behavior hint contract', + }, + { + regex: /\bpub fn build_mcp_tool_bridge_definition\b/, + message: 'missing MCP tool bridge descriptor builder', + }, + { + regex: /\bpub fn mcp_tool_bridge_dynamic_tool_info\b/, + message: 'missing MCP dynamic tool info bridge', + }, + { + regex: /\bpub fn validate_mcp_tool_bridge_input\b/, + message: 'missing MCP tool bridge input validation contract', + }, + { + regex: /\bpub fn render_mcp_tool_bridge_use_message\b/, + message: 'missing MCP tool bridge use-message renderer', + }, + { + regex: /\bpub fn render_mcp_tool_bridge_rejected_message\b/, + message: 'missing MCP tool bridge rejection-message renderer', + }, + { + regex: /\bpub fn render_mcp_tool_bridge_result_message\b/, + message: 'missing MCP tool bridge result-message renderer', + }, + { + regex: /\bpub fn build_mcp_tool_bridge_result\b/, + message: 'missing MCP tool bridge ToolResult builder', + }, + ], + }, + { + path: 'src/crates/execution/tool-contracts/src/acp_tool_bridge.rs', + reason: + 'agent-tools owns ACP external-agent tool bridge naming, schema, validation, presentation, and ToolResult contracts without depending on ACP protocol concrete', + patterns: [ + { + regex: /\bpub fn build_acp_external_agent_tool_name\b/, + message: 'missing ACP external-agent prompt-visible name builder', + }, + { + regex: /\bpub struct AcpExternalAgentToolDefinition\b/, + message: 'missing ACP external-agent tool definition contract', + }, + { + regex: /\bpub fn build_acp_external_agent_tool_definition\b/, + message: 'missing ACP external-agent tool definition builder', + }, + { + regex: /\bpub fn acp_external_agent_tool_input_schema\b/, + message: 'missing ACP external-agent input schema contract', + }, + { + regex: /\bpub fn validate_acp_external_agent_tool_input\b/, + message: 'missing ACP external-agent input validation contract', + }, + { + regex: /\bpub fn render_acp_external_agent_use_message\b/, + message: 'missing ACP external-agent use-message renderer', + }, + { + regex: /\bpub fn render_acp_external_agent_rejected_message\b/, + message: 'missing ACP external-agent rejection-message renderer', + }, + { + regex: /\bpub fn render_acp_external_agent_result_message\b/, + message: 'missing ACP external-agent result-message renderer', + }, + { + regex: /\bpub fn render_acp_external_agent_result_for_assistant\b/, + message: 'missing ACP external-agent assistant-result renderer', + }, + { + regex: /\bpub fn build_acp_external_agent_tool_result\b/, + message: 'missing ACP external-agent ToolResult builder', + }, + ], + }, { path: 'src/crates/execution/tool-contracts/src/file_guidance.rs', reason: 'agent-tools owns provider-neutral file tool guidance marker contracts', @@ -4746,8 +5670,8 @@ export const requiredContentRules = [ message: 'missing port-backed agent session delete call', }, { - regex: /\bresolve_session_workspace_path\b/, - message: 'missing port-backed session workspace resolution call', + regex: /\bresolve_session_workspace_binding\b/, + message: 'missing port-backed session workspace binding resolution call', }, { regex: /"createdBy"/, @@ -4789,8 +5713,8 @@ export const requiredContentRules = [ message: 'missing port-backed target session list call', }, { - regex: /\bresolve_session_workspace_path\b/, - message: 'missing port-backed target session workspace resolution call', + regex: /\bresolve_session_workspace_binding\b/, + message: 'missing port-backed target session workspace binding resolution call', }, { regex: /"createdBy"/, @@ -5623,6 +6547,14 @@ export const requiredContentRules = [ regex: /\bproduct_tool_runtime_registry_preserves_provider_plan_order\b/, message: 'missing product tool provider plan-to-registry order regression', }, + { + regex: /\bproduct_tool_runtime_keeps_no_direct_core_profiles_empty\b/, + message: 'missing no-direct-core product tool runtime regression', + }, + { + regex: /\bDeliveryProfile::Sdk\b/, + message: 'product tool runtime no-direct-core regression must cover SDK profile', + }, ], }, { @@ -5813,6 +6745,10 @@ export const requiredContentRules = [ regex: /\bpub struct ToolRuntimeAssembly\b/, message: 'missing generic runtime assembly contract', }, + { + regex: /\bpub async fn materialized_tool_snapshot\b/, + message: 'missing registry materialized snapshot accessor', + }, { regex: /\bpub type ToolDecoratorRef\b/, message: 'missing generic decorator ref contract', @@ -5875,6 +6811,45 @@ export const requiredContentRules = [ }, ], }, + { + path: 'src/crates/execution/tool-contracts/src/tool_snapshot.rs', + reason: + 'agent-tools must own materialized tool snapshots, provider identity, effect facts, cancellation facts, and stale-call guards', + patterns: [ + { + regex: /\bpub struct MaterializedToolSnapshot\b/, + message: 'missing materialized tool snapshot contract', + }, + { + regex: /\bpub struct ToolProviderIdentity\b/, + message: 'missing provider-neutral tool identity contract', + }, + { + regex: /\bpub struct ToolEffectFacts\b/, + message: 'missing provider-neutral tool effect facts contract', + }, + { + regex: /\bpub enum ToolEffectFactsSource\b/, + message: 'missing tool effect facts source contract', + }, + { + regex: /\bpub struct ToolEffectFilter\b/, + message: 'missing provider-neutral tool effect filter contract', + }, + { + regex: /\bpub struct ToolCallSnapshotGuard\b/, + message: 'missing stale-call snapshot guard contract', + }, + { + regex: /\bpub enum ToolSnapshotCallError\b/, + message: 'missing stale-call snapshot error contract', + }, + { + regex: /\bpub async fn materialize_tool_snapshot\b/, + message: 'missing materialized tool snapshot builder', + }, + ], + }, { path: 'src/crates/execution/tool-provider-groups/src/lib.rs', reason: @@ -6600,6 +7575,10 @@ export const requiredContentRules = [ reason: 'remote SSH workspace_search strategy helpers must stay crate-internal and keep behavior-equivalence tests near the owner', patterns: [ + { + regex: /#\[cfg\(not\(feature = "remote-ssh-concrete"\)\)\]\s*pub mod disabled\b/s, + message: 'missing gated service-owned disabled remote workspace search surface', + }, { regex: /\bpub\(crate\)\s+fn\s+build_remote_scope\b/, message: 'remote scope helper must stay crate-internal', @@ -6698,19 +7677,15 @@ export const requiredContentRules = [ message: 'missing ssh-remote gate for real remote search implementation', }, { - regex: /#\[cfg\(not\(feature = "ssh-remote"\)\)\]\s*mod remote_disabled\b/s, - message: 'missing disabled remote search implementation for no-default builds', - }, - { - regex: /#\[cfg\(not\(feature = "ssh-remote"\)\)\]\s*pub use remote_disabled/s, - message: 'missing disabled remote search export', + regex: /#\[cfg\(not\(feature = "ssh-remote"\)\)\]\s*pub use bitfun_services_integrations::remote_ssh::workspace_search::disabled/s, + message: 'missing service-owned disabled remote search export', }, ], }, { - path: 'src/crates/assembly/core/src/service/search/remote_disabled.rs', + path: 'src/crates/services/services-integrations/src/remote_ssh/workspace_search/disabled.rs', reason: - 'no-default core builds must keep remote search unavailable with an explicit diagnostic', + 'services-integrations must own disabled remote workspace search diagnostics for lightweight SSH builds', patterns: [ { regex: /Remote SSH search is disabled; enable the `ssh-remote` feature/, @@ -6995,27 +7970,23 @@ export const requiredContentRules = [ { path: 'src/crates/assembly/core/src/miniapp/builtin/mod.rs', reason: - 'core must coordinate built-in MiniApp seed decisions and recompilation while services-integrations owns seed filesystem IO', + 'core must adapt built-in MiniApp seed host operations while product-domains owns seed orchestration and services-integrations owns seed filesystem IO', patterns: [ { regex: /\bBUILTIN_APPS\b/, message: 'missing product-domain built-in MiniApp bundle re-export/use', }, { - regex: /\bbuiltin_content_hash\b/, - message: 'missing product-domain built-in MiniApp content hash use', - }, - { - regex: /\bshould_seed_builtin_app\b/, - message: 'missing product-domain built-in MiniApp seed decision use', + regex: /\bseed_builtin_miniapps_with_host\b/, + message: 'missing product-domain built-in MiniApp seed orchestrator use', }, { - regex: /\bresolve_builtin_seed_check\b/, - message: 'missing product-domain built-in MiniApp seed check use', + regex: /\bimpl BuiltinMiniAppSeedHost for CoreBuiltinMiniAppSeedHost\b/, + message: 'missing core built-in MiniApp seed host adapter', }, { - regex: /\bresolve_builtin_seed_action\b/, - message: 'missing product-domain built-in MiniApp seed action use', + regex: /\bmark_builtin_update_available\b/, + message: 'missing built-in MiniApp local-override update-record host delegation', }, { regex: /\bminiapp_builtin_io::prepare_builtin_seed_bundle_files\b/, @@ -7189,6 +8160,14 @@ export const requiredContentRules = [ reason: 'services-integrations remote-ssh owns workspace path/session identity helpers that do not require concrete SSH runtime handles', patterns: [ + { + regex: /\bpub struct WorkspaceSessionIdentity\b/, + message: 'missing workspace session identity contract', + }, + { + regex: /\bpub fn workspace_session_identity\b/, + message: 'missing workspace session identity builder', + }, { regex: /\bpub fn remote_workspace_runtime_root\b/, message: 'missing remote workspace runtime root helper', @@ -8007,7 +8986,7 @@ export const requiredContentRules = [ { path: 'src/crates/contracts/product-domains/src/miniapp/builtin.rs', reason: - 'product-domains owns built-in MiniApp bundle assets, marker, hash, and seed-decision contracts while core keeps asset seeding IO and recompilation', + 'product-domains owns built-in MiniApp bundle assets, marker, hash, seed orchestration, and host adapter contract while core keeps concrete IO and recompilation', patterns: [ { regex: /id: "builtin-pr-review"/, @@ -8049,6 +9028,22 @@ export const requiredContentRules = [ regex: /\bpub enum BuiltinSeedAction\b/, message: 'missing built-in MiniApp seed action contract', }, + { + regex: /\bpub trait BuiltinMiniAppSeedHost\b/, + message: 'missing built-in MiniApp seed host adapter contract', + }, + { + regex: /\bpub async fn seed_builtin_miniapps_with_host\b/, + message: 'missing built-in MiniApp seed orchestrator', + }, + { + regex: /\bpub async fn seed_builtin_miniapp_with_host\b/, + message: 'missing built-in MiniApp single-bundle seed orchestrator', + }, + { + regex: /\bpub enum BuiltinMiniAppSeedOutcome\b/, + message: 'missing built-in MiniApp seed outcome contract', + }, { regex: /\bpub fn resolve_builtin_seed_check\b/, message: 'missing built-in MiniApp seed check helper', diff --git a/scripts/core-boundaries/self-test.mjs b/scripts/core-boundaries/self-test.mjs index b616208541..855d6ee30c 100644 --- a/scripts/core-boundaries/self-test.mjs +++ b/scripts/core-boundaries/self-test.mjs @@ -445,6 +445,17 @@ export function runManifestParserSelfTest({ throw new Error(`core scheduler boundary rule must forbid contract: ${contract}`); } } + const coreSessionStateManagerRuleText = forbiddenRuleTextForPath( + 'src/crates/assembly/core/src/agentic/coordination/state_manager.rs', + ); + if (!coreSessionStateManagerRuleText) { + throw new Error('missing core session state manager boundary rule'); + } + for (const contract of ['SessionStateManager', 'DashMap', 'AgenticEvent::SessionStateChanged']) { + if (!coreSessionStateManagerRuleText.includes(contract)) { + throw new Error(`core session state manager boundary rule must forbid contract: ${contract}`); + } + } const coreRoundPreemptRuleText = forbiddenRuleTextForPath( 'src/crates/assembly/core/src/agentic/round_preempt.rs', ); @@ -544,7 +555,14 @@ export function runManifestParserSelfTest({ const coreOptionalOwnerDeps = new Set( coreOptionalOwnerRule?.dependencies.map((dependency) => dependency.depName) ?? [], ); - const coreFullyMigratedDeps = new Set(['hostname', 'mac_address', 'qrcode', 'x25519-dalek']); + const coreFullyMigratedDeps = new Set([ + 'aes', + 'hostname', + 'local-ip-address', + 'mac_address', + 'qrcode', + 'x25519-dalek', + ]); for (const dep of coreProfile?.forbiddenNonOptionalDeps ?? []) { if (coreFullyMigratedDeps.has(dep)) { continue; @@ -567,15 +585,36 @@ export function runManifestParserSelfTest({ const servicesOptionalOwnerRule = optionalDependencyFeatureOwnerRules.find( (rule) => rule.crateName === 'services-integrations', ); + const servicesOptionalOwnerDeps = new Set( + servicesOptionalOwnerRule?.dependencies.map((dependency) => dependency.depName) ?? [], + ); + const servicesIntegrationsDefaultOnlyGuardDeps = new Set(['bitfun-relay-server']); + for (const dep of servicesIntegrationsDefaultProfile?.forbiddenNonOptionalDeps ?? []) { + if (servicesIntegrationsDefaultOnlyGuardDeps.has(dep)) { + continue; + } + if (!servicesOptionalOwnerDeps.has(dep)) { + throw new Error( + `services-integrations optional dependency owner rule must cover forbidden dependency ${dep}`, + ); + } + } for (const dep of [ + 'aes', + 'bitfun-services-core', 'bitfun-runtime-ports', 'git2', + 'hex', 'hostname', + 'local-ip-address', 'mac_address', + 'md5', 'notify', 'qrcode', + 'reqwest', 'rmcp', 'tokio-tungstenite', + 'which', 'x25519-dalek', ]) { if (!servicesOptionalOwnerRule?.dependencies.some((dependency) => dependency.depName === dep)) { @@ -590,6 +629,43 @@ export function runManifestParserSelfTest({ throw new Error(`product-domains optional dependency owner rule must cover ${dep}`); } } + const coreBrowserLauncherFacadeRuleText = forbiddenRuleTextForPath( + 'src/crates/assembly/core/src/agentic/tools/browser_control/browser_launcher.rs', + ); + for (const contract of ['process_manager::', 'Command::new', 'reqwest::']) { + if (!coreBrowserLauncherFacadeRuleText.includes(contract)) { + throw new Error(`core browser launcher facade boundary rule must forbid: ${contract}`); + } + } + const coreComputerUseSystemFacadeRuleText = forbiddenRuleTextForPath( + 'src/crates/assembly/core/src/agentic/tools/implementations/computer_use_actions.rs', + ); + for (const contract of [ + 'process_manager::', + 'tokio::process::', + 'script_invocation', + 'platform_open_attempts', + ]) { + if (!coreComputerUseSystemFacadeRuleText.includes(contract)) { + throw new Error(`core computer-use system facade boundary rule must forbid: ${contract}`); + } + } + const coreLocalShellFacadeRuleText = forbiddenRuleTextForPath( + 'src/crates/assembly/core/src/agentic/tools/implementations/exec_command/local_shell.rs', + ); + for (const contract of ['process_manager::', 'Command::new', 'std::env::var', 'target_os']) { + if (!coreLocalShellFacadeRuleText.includes(contract)) { + throw new Error(`core local shell facade boundary rule must forbid: ${contract}`); + } + } + const coreWeixinFacadeRuleText = forbiddenRuleTextForPath( + 'src/crates/assembly/core/src/service/remote_connect/bot/weixin.rs', + ); + for (const contract of ['reqwest::', 'aes::', 'hex::', 'md5::', 'sync_buf_path']) { + if (!coreWeixinFacadeRuleText.includes(contract)) { + throw new Error(`core Weixin bot facade boundary rule must forbid: ${contract}`); + } + } const productDomainRuntimeRule = forbiddenContentUnderRules.find( (rule) => rule.path === 'src/crates/contracts/product-domains/src', ); @@ -663,12 +739,24 @@ export function runManifestParserSelfTest({ if (!agentRuntimeRule?.forbiddenDeps.includes('bitfun-services-integrations')) { throw new Error('agent-runtime lightweight boundary must forbid concrete service integrations'); } + if (!agentRuntimeRule?.forbiddenDeps.includes('bitfun-product-capabilities')) { + throw new Error('agent-runtime lightweight boundary must forbid product assembly facts'); + } + if (!agentRuntimeRule?.forbiddenDeps.includes('tool-runtime')) { + throw new Error('agent-runtime lightweight boundary must forbid concrete tool runtime'); + } const agentRuntimeProfile = dependencyProfileRules.find( (rule) => rule.crateName === 'agent-runtime', ); if (!agentRuntimeProfile?.forbiddenNonOptionalDeps.includes('tauri')) { throw new Error('agent-runtime dependency profile must forbid product surface dependencies'); } + if (!agentRuntimeProfile?.forbiddenNonOptionalDeps.includes('bitfun-product-capabilities')) { + throw new Error('agent-runtime dependency profile must forbid product assembly facts'); + } + if (!agentRuntimeProfile?.forbiddenNonOptionalDeps.includes('tool-runtime')) { + throw new Error('agent-runtime dependency profile must forbid concrete tool runtime'); + } const productCapabilitiesRule = lightweightBoundaryRules.find( (rule) => rule.crateName === 'product-capabilities', ); @@ -743,7 +831,7 @@ export function runManifestParserSelfTest({ throw new Error('SessionMessage boundary rule must forbid direct scheduler submit'); } const sessionMessageLegacySessionAccessContracts = [ - 'resolve_session_workspace_path', + 'resolve_session_workspace_binding', 'list_sessions', ]; for (const contract of sessionMessageLegacySessionAccessContracts) { @@ -758,7 +846,7 @@ export function runManifestParserSelfTest({ throw new Error('SessionControl boundary rule must forbid direct scheduler cancellation'); } const sessionControlLegacySessionAccessContracts = [ - 'resolve_session_workspace_path', + 'resolve_session_workspace_binding', 'list_sessions', 'delete_session', ]; @@ -777,7 +865,7 @@ export function runManifestParserSelfTest({ 'src/crates/assembly/core/src/agentic/tools/implementations/cron_tool.rs', ); const cronToolLegacySessionAccessContracts = [ - 'resolve_session_workspace_path', + 'resolve_session_workspace_binding', 'list_sessions', ]; for (const contract of cronToolLegacySessionAccessContracts) { @@ -797,9 +885,22 @@ export function runManifestParserSelfTest({ if ( !coordinatorRuleText.includes('scheduler') || !coordinatorRuleText.includes('deliver_thread_goal_') || - !coordinatorRuleText.includes('deliver_background_result') + !coordinatorRuleText.includes('deliver_background_result') || + !coordinatorRuleText.includes('CoreRuntimeServicesProvider::terminal_port') || + !coordinatorRuleText.includes('TerminalRuntimePort') ) { - throw new Error('Coordinator boundary rule must forbid direct scheduler lifecycle delivery'); + throw new Error( + 'Coordinator boundary rule must forbid direct scheduler delivery and terminal provider construction', + ); + } + const agenticSystemRuleText = forbiddenRuleTextForPath( + 'src/crates/assembly/core/src/agentic/system.rs', + ); + if ( + !agenticSystemRuleText.includes('CoreRuntimeServicesProvider::terminal_port') || + !agenticSystemRuleText.includes('TerminalRuntimePort') + ) { + throw new Error('agentic system boundary rule must forbid terminal provider construction'); } const coreFileReadStateRuleText = forbiddenRuleTextForPath( 'src/crates/assembly/core/src/agentic/session/file_read_state.rs', @@ -827,10 +928,10 @@ export function runManifestParserSelfTest({ const coreToolContextRuntimeRuleText = forbiddenRuleTextForPath( 'src/crates/assembly/core/src/agentic/tools/tool_context_runtime.rs', ); - if (!coreToolContextRuntimeRuleText.includes('LightCheckpoint')) { - throw new Error( - 'core tool_context_runtime boundary rule must forbid checkpoint evidence projection', - ); + for (const contract of ['LightCheckpoint', 'TerminalRuntimePort']) { + if (!coreToolContextRuntimeRuleText.includes(contract)) { + throw new Error(`core tool_context_runtime boundary rule must forbid ${contract}`); + } } const coreUserInputManagerRuleText = forbiddenRuleTextForPath( 'src/crates/assembly/core/src/agentic/tools/user_input_manager.rs', @@ -870,6 +971,110 @@ export function runManifestParserSelfTest({ throw new Error(`core background_command_output boundary rule must forbid ${contract}`); } } + const coreExecCommandRuleText = forbiddenRuleTextForPath( + 'src/crates/assembly/core/src/agentic/tools/implementations/exec_command/command.rs', + ); + for (const contract of [ + 'POWERSHELL_UTF8_OUTPUT_PREFIX', + 'REMOTE_NON_TTY_INTERRUPT_GRACE_SECONDS', + 'DEFAULT_TOOL_YIELD_TIME_MS', + '(?:local|remote)_completion_value', + '(?:local|remote)_completion', + '(?:local|remote)_background_output_status_for_completion', + 'merged_remote_env', + 'remote_command_env_words', + 'shell_escape', + 'is_plausible_remote_shell_path', + 'getent\\s+passwd', + 'command\\s+-v\\s+bash', + 'REMOTE_SHELL_PROBE_TIMEOUT_MS', + 'get_global_exec_process_manager', + 'get_global_remote_exec_process_manager', + 'SSHConnectionManager', + 'SSHCommandOptions', + 'LocalExecCommandRequest', + 'CoreRuntimeServicesProvider::terminal_port', + 'TerminalRuntimePort', + ]) { + if (!coreExecCommandRuleText.includes(contract)) { + throw new Error(`core exec_command boundary rule must forbid ${contract}`); + } + } + const coreWriteStdinRuleText = forbiddenRuleTextForPath( + 'src/crates/assembly/core/src/agentic/tools/implementations/exec_command/stdin.rs', + ); + for (const contract of [ + 'DEFAULT_TOOL_YIELD_TIME_MS', + '(?:local|remote)_completion_value', + '(?:local|remote)_completion', + '"status"\\s*:\\s*"session_not_found"', + 'No input was sent', + 'get_global_exec_process_manager', + 'get_global_remote_exec_process_manager', + 'RemoteExecError', + 'LocalWriteStdinRequest', + 'CoreRuntimeServicesProvider::terminal_port', + 'TerminalRuntimePort', + ]) { + if (!coreWriteStdinRuleText.includes(contract)) { + throw new Error(`core WriteStdin boundary rule must forbid ${contract}`); + } + } + const coreExecControlRuleText = forbiddenRuleTextForPath( + 'src/crates/assembly/core/src/agentic/tools/implementations/exec_command/control.rs', + ); + if (!coreExecControlRuleText.includes('exec_command_control_action_name')) { + throw new Error('core ExecControl boundary rule must forbid action result shaping'); + } + if (!coreExecControlRuleText.includes('(?:local|remote)_completion')) { + throw new Error('core ExecControl boundary rule must forbid duplicated completion mapping'); + } + for (const contract of [ + 'get_global_exec_process_manager', + 'get_global_remote_exec_process_manager', + 'RemoteExecError', + 'LocalExecControlRequest', + 'CoreRuntimeServicesProvider::terminal_port', + 'TerminalRuntimePort', + ]) { + if (!coreExecControlRuleText.includes(contract)) { + throw new Error(`core ExecControl boundary rule must forbid ${contract}`); + } + } + const coreExecCommandInputRuleText = forbiddenRuleTextForPath( + 'src/crates/assembly/core/src/agentic/tools/implementations/exec_command/input.rs', + ); + for (const contract of [ + 'get_global_exec_process_manager', + 'get_global_remote_exec_process_manager', + 'LocalSendStdinRequest', + 'CoreRuntimeServicesProvider::terminal_port', + 'TerminalRuntimePort', + ]) { + if (!coreExecCommandInputRuleText.includes(contract)) { + throw new Error(`core ExecCommand input boundary rule must forbid ${contract}`); + } + } + const coreExecCommandEnvSnapshotRuleText = forbiddenRuleTextForPath( + 'src/crates/assembly/core/src/agentic/tools/implementations/exec_command/env_snapshot.rs', + ); + for (const contract of [ + 'ENV_SNAPSHOT_BEGIN', + 'ENV_SNAPSHOT_END', + 'should_import_env_var', + 'is_valid_env_var_name', + 'is_volatile_env_var', + 'shell_escape', + 'ENV_SNAPSHOT_TIMEOUT_MS', + 'ENV_SNAPSHOT_MAX_OUTPUT_CHARS', + 'ENV_SNAPSHOT_TTL', + 'get_global_remote_exec_process_manager', + 'SSHConnectionManager', + ]) { + if (!coreExecCommandEnvSnapshotRuleText.includes(contract)) { + throw new Error(`core exec_command env_snapshot boundary rule must forbid ${contract}`); + } + } const coreSkillAgentSnapshotRuleText = forbiddenRuleTextForPath( 'src/crates/assembly/core/src/agentic/skill_agent_snapshot.rs', ); @@ -1004,6 +1209,38 @@ export function runManifestParserSelfTest({ 'backend_event_names_remain_stable', ], }, + { + path: 'src/crates/contracts/events/src/frontend_projection.rs', + contracts: [ + 'AgenticFrontendEvent', + 'project_agentic_frontend_event', + 'legacy_flat_message', + 'deep_review_queue_projection_preserves_camel_case_contract', + 'legacy_flat_message_keeps_projection_type_authoritative', + 'legacy_flat_dialog_turn_started_preserves_existing_shape', + ], + }, + { + path: 'src/crates/contracts/events/src/agentic_projection_manifest.rs', + contracts: [ + 'AGENTIC_EVENT_PROJECTION_MANIFEST', + 'public_agentic_event_projection_manifest', + 'is_legacy_websocket_agentic_event_type', + 'public_event_projection_manifest_describes_projected_events_and_websocket_allowlist', + ], + }, + { + path: 'src/crates/adapters/transport/src/adapters/tauri.rs', + contracts: ['project_agentic_frontend_event', 'projected.event_name.as_str()'], + }, + { + path: 'src/crates/adapters/transport/src/adapters/websocket.rs', + contracts: [ + 'project_agentic_frontend_event', + 'is_legacy_websocket_agentic_event_type', + 'websocket_keeps_legacy_agentic_event_allowlist', + ], + }, { path: 'src/crates/execution/runtime-services/tests/runtime_services_contracts.rs', contracts: [ @@ -1035,7 +1272,7 @@ export function runManifestParserSelfTest({ 'MissingSessionManagementPort', 'list_sessions', 'delete_session', - 'resolve_session_workspace_path', + 'resolve_session_workspace_binding', 'session_management_delegates_to_registered_port', 'RuntimeServices', 'RuntimeEventEnvelope', @@ -1072,6 +1309,51 @@ export function runManifestParserSelfTest({ path: 'src/crates/execution/agent-runtime/Cargo.toml', contracts: ['[features]', 'default = []'], }, + { + path: 'src/crates/execution/agent-runtime/src/context_profile.rs', + contracts: [ + 'ContextProfile', + 'ModelCapabilityProfile', + 'ContextProfilePolicy', + 'for_subagent_context_and_models', + 'model_capability_weak_for_mini', + ], + }, + { + path: 'src/crates/execution/agent-runtime/src/session_state.rs', + contracts: [ + 'SessionState', + 'ProcessingPhase', + 'dialog_state_fact', + 'session_state_label_for_state', + 'processing_state_serialization_stays_compatible', + ], + }, + { + path: 'src/crates/execution/agent-runtime/src/session.rs', + contracts: [ + 'Session', + 'SessionConfig', + 'SessionSummary', + 'SessionKind', + 'PersistedSessionStateFile', + 'sanitize_persisted_session_state', + 'persisted_session_state_file_shape_stays_compatible', + ], + }, + { + path: 'src/crates/execution/agent-runtime/src/dialog_turn.rs', + contracts: ['new_turn_id', 'TurnStats'], + }, + { + path: 'src/crates/execution/agent-runtime/src/side_question.rs', + contracts: [ + 'SideQuestionRuntime', + 'ActiveBtwTurn', + 'register_btw_turn', + 'registering_same_request_cancels_previous_token', + ], + }, { path: 'src/crates/execution/agent-runtime/tests/sdk_smoke.rs', contracts: [ @@ -1203,12 +1485,15 @@ export function runManifestParserSelfTest({ path: 'src/crates/execution/agent-runtime/src/tool_confirmation.rs', contracts: [ 'ToolConfirmationRequestFacts', + 'ToolConfirmationGateFacts', + 'ToolConfirmationGatePlan', 'ToolConfirmationPlan', 'ToolConfirmationOutcome', 'ToolConfirmationWaitResult', 'ToolConfirmationResponse', 'ToolConfirmationChannelStore', 'ConfirmationFailureKind', + 'resolve_tool_confirmation_gate', 'resolve_tool_confirmation_plan', 'resolve_confirmation_failure', 'resolve_confirmation_wait_result', @@ -1238,9 +1523,124 @@ export function runManifestParserSelfTest({ 'background_command_output_reads_snapshot_then_incremental_chunks', ], }, + { + path: 'src/crates/execution/tool-execution/src/exec_command.rs', + contracts: [ + 'EXEC_COMMAND_POWERSHELL_UTF8_OUTPUT_PREFIX', + 'EXEC_COMMAND_DEFAULT_YIELD_TIME_MS', + 'ExecCommandShellKind', + 'Custom', + 'ExecCommandRemoteShell', + 'REMOTE_EXEC_SHELL_PROBE_TIMEOUT_MS', + 'remote_exec_shell_probe_command', + 'fallback_remote_exec_shell', + 'ExecCommandRemoteEnvSnapshot', + 'ExecCommandRemoteEnvSnapshotCapturePolicy', + 'ExecCommandRemoteEnvSnapshotCacheKey', + 'ExecCommandRemoteEnvSnapshotCache', + 'remote_exec_env_snapshot_capture_policy', + 'ExecCommandRunInput', + 'WriteStdinInput', + 'ExecCommandControlToolInput', + 'ExecCommandResultFields', + 'ExecCommandShellMetadata', + 'exec_command_argv_for_shell', + 'exec_command_result_value', + 'write_stdin_result_value', + 'write_stdin_session_not_found_result', + 'exec_control_result_value', + 'remote_exec_login_shell_command', + 'remote_exec_non_tty_control_wrapper', + 'parse_remote_exec_env_snapshot_output', + 'ExecCommandLifecycleStatus', + 'exec_command_lifecycle_background_output_status', + 'remote_login_shell_command_applies_snapshot_then_tool_env', + 'remote_shell_probe_and_env_snapshot_are_provider_neutral', + 'exec_command_input_policy_applies_defaults_without_trimming_command', + 'write_stdin_input_policy_applies_poll_defaults', + 'exec_control_input_policy_keeps_wait_optional', + 'exec_command_result_builder_preserves_existing_wire_shape', + 'write_stdin_and_control_result_builders_preserve_remote_shape', + 'remote_env_snapshot_capture_policy_keeps_existing_bounds', + 'remote_env_snapshot_cache_owns_key_and_ttl_policy', + 'remote_shell_probe_skips_non_posix_shells', + 'lifecycle_status_has_provider_neutral_names_and_background_statuses', + ], + }, + { + path: 'src/crates/assembly/core/src/agentic/tools/implementations/exec_command/completion.rs', + contracts: [ + 'exec_command_local_completion', + 'exec_command_remote_completion', + 'ExecCommandCompletionStatus::Interrupted', + 'ExecCommandCompletionSource::OutOfBandControl', + ], + }, + { + path: 'src/crates/assembly/core/src/agentic/tools/implementations/exec_command/command.rs', + contracts: [ + 'exec_command_argv_for_shell', + 'parse_remote_exec_shell_probe_output', + 'remote_exec_shell_probe_command', + 'fallback_remote_exec_shell', + 'remote_exec_login_shell_command', + 'remote_exec_non_tty_control_wrapper', + 'exec_command_lifecycle_status_name', + 'exec_command_lifecycle_background_output_status', + 'exec_command_run_input_from_input', + 'exec_command_result_value', + 'exec_command_local_completion', + 'exec_command_remote_completion', + 'ExecCommandShellMetadata', + 'remote_shell_probe_preserves_unknown_shell_metadata', + ], + }, + { + path: 'src/crates/assembly/core/src/agentic/tools/implementations/exec_command/stdin.rs', + contracts: [ + 'write_stdin_input_from_input', + 'write_stdin_input_validation_message', + 'write_stdin_result_value', + 'write_stdin_session_not_found_result', + 'exec_command_local_completion', + 'exec_command_remote_completion', + ], + }, + { + path: 'src/crates/assembly/core/src/agentic/tools/implementations/exec_command/control.rs', + contracts: [ + 'exec_command_control_tool_input_from_input', + 'exec_command_control_tool_input_validation_message', + 'exec_control_result_value', + 'exec_command_local_completion', + 'exec_command_remote_completion', + ], + }, + { + path: 'src/crates/assembly/core/src/agentic/tools/implementations/exec_command/env_snapshot.rs', + contracts: [ + 'remote_exec_env_snapshot_command', + 'parse_remote_exec_env_snapshot_output', + 'remote_exec_env_snapshot_capture_policy', + 'ExecCommandRemoteEnvSnapshotCache', + 'ExecCommandRemoteEnvSnapshotCacheKey', + 'exec_command_shell_kind', + ], + }, + { + path: 'src/crates/assembly/core/src/agentic/tools/implementations/exec_command/shell_kind.rs', + contracts: [ + 'exec_command_shell_kind', + 'terminal_shell_type', + 'ExecCommandShellKind::Custom(name.clone())', + 'ShellType::Custom(name)', + ], + }, { path: 'src/crates/execution/agent-runtime/tests/tool_confirmation_contracts.rs', contracts: [ + 'confirmation_gate_preserves_skip_policy_precedence', + 'confirmation_gate_requires_confirmation_only_for_permissioned_tools', 'confirmation_plan_requires_permission_only_when_both_flags_are_true', 'confirmation_plan_preserves_legacy_no_timeout_one_year_deadline', 'confirmation_failure_mapping_preserves_legacy_reasons_and_errors', @@ -1435,6 +1835,23 @@ export function runManifestParserSelfTest({ path: 'src/crates/execution/agent-runtime/src/event_queue.rs', contracts: ['EventQueue', 'impl StreamEventSink for EventQueue', 'clear_session'], }, + { + path: 'src/crates/execution/agent-runtime/src/session_state_manager.rs', + contracts: [ + 'pub struct SessionStateManager', + 'DashMap', + 'EventQueue', + 'AgenticEvent::SessionStateChanged', + 'session_state_label_for_state', + 'can_start_new_turn', + 'session_state_manager_emits_compatible_state_change_events', + 'session_state_manager_keeps_turn_start_guard_semantics', + ], + }, + { + path: 'src/crates/assembly/core/src/agentic/coordination/state_manager.rs', + contracts: ['pub use bitfun_agent_runtime::session_state_manager::SessionStateManager'], + }, { path: 'src/crates/execution/agent-runtime/src/event_router.rs', contracts: ['EventSubscriber', 'EventRouter', 'route_batch'], @@ -1518,7 +1935,7 @@ export function runManifestParserSelfTest({ 'AgentSessionListRequest', 'AgentSessionWorkspaceRequest', 'list_sessions', - 'resolve_session_workspace_path', + 'resolve_session_workspace_binding', ], }, { @@ -1527,7 +1944,9 @@ export function runManifestParserSelfTest({ }, { path: 'src/crates/assembly/core/src/agentic/events/types.rs', - contracts: ['bitfun_agent_runtime::events::session_state_label'], + contracts: [ + 'bitfun_agent_runtime::session_state::session_state_label_for_state', + ], }, { path: 'src/crates/assembly/core/src/agentic/agents/prompt_builder/user_context.rs', @@ -1628,6 +2047,8 @@ export function runManifestParserSelfTest({ 'ProductCapabilityAssembly', 'ProductFeatureGroup', 'ProductRuntimeAssembly', + 'DeliveryProfile::Sdk', + 'into_runtime_parts', 'feature_groups_from_tool_provider_group_plan', ], }, @@ -1639,6 +2060,15 @@ export function runManifestParserSelfTest({ 'product_harness_provider_plans_legacy_facade_without_execution', ], }, + { + path: 'src/crates/assembly/product-capabilities/tests/product_sdk_assembly.rs', + contracts: [ + 'product_runtime_parts_can_build_agent_runtime_sdk_without_core', + 'sdk_delivery_profile_builds_minimal_agent_runtime_without_product_full_capabilities', + 'DeliveryProfile::Cli', + 'DeliveryProfile::Sdk', + ], + }, { path: 'src/crates/assembly/core/src/agentic/tools/pipeline/tool_pipeline.rs', contracts: [ @@ -1962,7 +2392,7 @@ export function runManifestParserSelfTest({ 'AgentSessionWorkspaceRequest', 'list_sessions', 'delete_session', - 'resolve_session_workspace_path', + 'resolve_session_workspace_binding', '"createdBy"', 'AgentTurnCancellationRequest', 'requester_session_id', @@ -1976,7 +2406,7 @@ export function runManifestParserSelfTest({ 'AgentSessionListRequest', 'AgentSessionWorkspaceRequest', 'list_sessions', - 'resolve_session_workspace_path', + 'resolve_session_workspace_binding', '"createdBy"', 'AgentDialogTurnRequest', 'AgentDialogPrependedReminder', @@ -2152,6 +2582,8 @@ export function runManifestParserSelfTest({ 'product_assembly_plan_for_profile', 'product_tool_runtime_owner_preserves_registry_contract', 'product_tool_runtime_registry_preserves_provider_plan_order', + 'product_tool_runtime_keeps_no_direct_core_profiles_empty', + 'DeliveryProfile::Sdk', ], }, { @@ -2207,6 +2639,7 @@ export function runManifestParserSelfTest({ 'StaticToolMaterializationError', 'materialize_static_tool_provider_groups', 'ToolRuntimeAssembly', + 'materialized_tool_snapshot', 'create_registry_from_static_provider_plans', 'create_registry_from_static_provider_entries', 'ToolCatalogRuntime', @@ -2227,6 +2660,49 @@ export function runManifestParserSelfTest({ 'call_results', ], }, + { + path: 'src/crates/execution/tool-contracts/src/tool_snapshot.rs', + contracts: [ + 'MaterializedToolSnapshot', + 'ToolProviderIdentity', + 'ToolEffectFacts', + 'ToolEffectFactsSource', + 'ToolEffectFilter', + 'ToolCallSnapshotGuard', + 'ToolSnapshotCallError', + 'materialize_tool_snapshot', + ], + }, + { + path: 'src/crates/execution/tool-contracts/src/mcp_tool_bridge.rs', + contracts: [ + 'build_mcp_tool_bridge_name', + 'McpToolBridgeDefinition', + 'McpToolBridgeBehaviorHints', + 'build_mcp_tool_bridge_definition', + 'mcp_tool_bridge_dynamic_tool_info', + 'validate_mcp_tool_bridge_input', + 'render_mcp_tool_bridge_use_message', + 'render_mcp_tool_bridge_rejected_message', + 'render_mcp_tool_bridge_result_message', + 'build_mcp_tool_bridge_result', + ], + }, + { + path: 'src/crates/execution/tool-contracts/src/acp_tool_bridge.rs', + contracts: [ + 'build_acp_external_agent_tool_name', + 'AcpExternalAgentToolDefinition', + 'build_acp_external_agent_tool_definition', + 'acp_external_agent_tool_input_schema', + 'validate_acp_external_agent_tool_input', + 'render_acp_external_agent_use_message', + 'render_acp_external_agent_rejected_message', + 'render_acp_external_agent_result_message', + 'render_acp_external_agent_result_for_assistant', + 'build_acp_external_agent_tool_result', + ], + }, { path: 'src/crates/execution/tool-provider-groups/src/lib.rs', contracts: [ @@ -2504,6 +2980,8 @@ export function runManifestParserSelfTest({ { path: 'src/crates/services/services-integrations/src/remote_ssh/workspace_search/mod.rs', contracts: [ + 'not(feature = "remote-ssh-concrete")', + 'pub mod disabled', 'build_remote_scope', 'shell_escape', 'should_retry_remote_scan_fallback_as_files_with_matches', @@ -2517,10 +2995,13 @@ export function runManifestParserSelfTest({ }, { path: 'src/crates/assembly/core/src/service/search/mod.rs', - contracts: ['mod remote_disabled', 'feature = "ssh-remote"', 'pub use remote_disabled'], + contracts: [ + 'feature = "ssh-remote"', + 'bitfun_services_integrations::remote_ssh::workspace_search::disabled', + ], }, { - path: 'src/crates/assembly/core/src/service/search/remote_disabled.rs', + path: 'src/crates/services/services-integrations/src/remote_ssh/workspace_search/disabled.rs', contracts: ['Remote SSH search is disabled', 'RemoteWorkspaceSearchService', 'remote_workspace_search_service_for_path'], }, { @@ -2682,10 +3163,10 @@ export function runManifestParserSelfTest({ path: 'src/crates/assembly/core/src/miniapp/builtin/mod.rs', contracts: [ 'BUILTIN_APPS', - 'builtin_content_hash', - 'should_seed_builtin_app', - 'resolve_builtin_seed_check', - 'resolve_builtin_seed_action', + 'seed_builtin_miniapps_with_host', + 'BuiltinMiniAppSeedHost', + 'CoreBuiltinMiniAppSeedHost', + 'mark_builtin_update_available', 'miniapp_builtin_io::prepare_builtin_seed_bundle_files', 'read_builtin_install_marker', 'miniapp_builtin_io::read_builtin_install_marker', @@ -2724,6 +3205,10 @@ export function runManifestParserSelfTest({ 'BuiltinSeedArtifacts', 'BuiltinSeedCheck', 'BuiltinSeedAction', + 'BuiltinMiniAppSeedHost', + 'seed_builtin_miniapps_with_host', + 'seed_builtin_miniapp_with_host', + 'BuiltinMiniAppSeedOutcome', 'resolve_builtin_seed_check', 'resolve_builtin_seed_action', 'serialize_builtin_install_marker', @@ -2840,15 +3325,15 @@ export function runManifestParserSelfTest({ }, { path: 'src/crates/assembly/core/src/service/remote_ssh/mod.rs', - contracts: ['mod disabled', 'pub mod manager', 'pub mod remote_fs', 'pub mod remote_terminal', 'pub mod workspace_state'], + contracts: ['bitfun_services_integrations::remote_ssh', 'pub mod manager', 'pub mod remote_fs', 'pub mod remote_terminal', 'pub mod workspace_state'], }, { - path: 'src/crates/assembly/core/src/service/remote_ssh/disabled.rs', - contracts: ['Remote SSH support is disabled', 'SSHConnectionManager', 'RemoteFileService', 'RemoteTerminalManager'], + path: 'src/crates/services/services-integrations/src/remote_ssh/mod.rs', + contracts: ['mod disabled', 'pub use disabled', 'remote-ssh-concrete', 'pub mod manager', 'mod remote_exec', 'pub mod remote_fs', 'pub mod remote_terminal'], }, { - path: 'src/crates/services/services-integrations/src/remote_ssh/mod.rs', - contracts: ['remote-ssh-concrete', 'pub mod manager', 'mod remote_exec', 'pub mod remote_fs', 'pub mod remote_terminal'], + path: 'src/crates/services/services-integrations/src/remote_ssh/disabled.rs', + contracts: ['Remote SSH support is disabled', 'SSHConnectionManager', 'RemoteFileService', 'RemoteTerminalManager'], }, { path: 'src/crates/services/services-integrations/src/remote_ssh/manager.rs', @@ -2869,6 +3354,8 @@ export function runManifestParserSelfTest({ { path: 'src/crates/services/services-integrations/src/remote_ssh/paths.rs', contracts: [ + 'WorkspaceSessionIdentity', + 'workspace_session_identity', 'remote_workspace_runtime_root', 'remote_workspace_session_mirror_dir', 'canonicalize_local_workspace_root', @@ -3314,6 +3801,11 @@ export function runManifestParserSelfTest({ 'truncate_for_assistant', 'MCPToolResultContent', 'Tool', + 'DynamicMcpToolInfo', + 'Input must be an object', + 'Using MCP tool', + 'was rejected by user', + 'completed\\. Result:', ]; const mcpToolAdapterRuleText = mcpToolAdapterRule.patterns .map((pattern) => pattern.regex.source) @@ -3492,7 +3984,17 @@ export function runManifestParserSelfTest({ const servicesIntegrationsProfile = dependencyProfileRules.find( (rule) => rule.crateName === 'services-integrations', ); - for (const dep of ['dunce', 'futures', 'reqwest', 'sse-stream']) { + for (const dep of [ + 'aes', + 'dunce', + 'futures', + 'hex', + 'local-ip-address', + 'md5', + 'reqwest', + 'sse-stream', + 'which', + ]) { if (!servicesIntegrationsProfile?.forbiddenNonOptionalDeps.includes(dep)) { throw new Error(`services-integrations default profile must forbid non-optional ${dep}`); } diff --git a/scripts/dev.cjs b/scripts/dev.cjs index 6237dfd3b5..9097666798 100644 --- a/scripts/dev.cjs +++ b/scripts/dev.cjs @@ -558,10 +558,16 @@ function flashgrepBinaryNames() { return ['flashgrep-aarch64-apple-darwin']; } if (process.platform === 'linux' && process.arch === 'x64') { - return ['flashgrep-x86_64-unknown-linux-gnu']; + return [ + 'flashgrep-x86_64-unknown-linux-musl', + 'flashgrep-x86_64-unknown-linux-gnu', + ]; } if (process.platform === 'linux' && process.arch === 'arm64') { - return ['flashgrep-aarch64-unknown-linux-gnu']; + return [ + 'flashgrep-aarch64-unknown-linux-musl', + 'flashgrep-aarch64-unknown-linux-gnu', + ]; } return [process.platform === 'win32' ? 'flashgrep.exe' : 'flashgrep']; } diff --git a/scripts/generate-startup-theme-bootstrap.mjs b/scripts/generate-startup-theme-bootstrap.mjs new file mode 100644 index 0000000000..22ea04a59a --- /dev/null +++ b/scripts/generate-startup-theme-bootstrap.mjs @@ -0,0 +1,83 @@ +import fs from 'node:fs'; +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; + +import { createServer } from 'vite'; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); +const repoRoot = path.resolve(__dirname, '..'); +const webUiRoot = path.join(repoRoot, 'src/web-ui'); +const outputPath = path.join( + repoRoot, + 'src/apps/desktop/src/generated/startup_theme_bootstrap.json', +); +const themePromptSnapshotOutputPath = path.join( + repoRoot, + 'src/crates/assembly/core/src/agentic/tools/implementations/generated/theme_prompt_snapshots.json', +); +const checkOnly = process.argv.includes('--check'); + +function normalizeGeneratedText(content) { + return String(content).replace(/\r\n?/g, '\n'); +} + +const server = await createServer({ + root: webUiRoot, + logLevel: 'error', + appType: 'custom', + server: { middlewareMode: true }, + optimizeDeps: { + entries: [], + noDiscovery: true, + }, +}); + +try { + const [ + { builtinThemes }, + { createStartupThemeBootstrapManifest }, + { createThemePromptSnapshotManifest }, + ] = await Promise.all([ + server.ssrLoadModule('/src/infrastructure/theme/presets/index.ts'), + server.ssrLoadModule('/src/infrastructure/theme/presets/startupThemeBootstrap.ts'), + server.ssrLoadModule('/src/infrastructure/theme/presets/themePromptSnapshots.ts'), + ]); + + const generatedFiles = [ + { + label: 'Startup theme bootstrap manifest', + outputPath, + content: `${JSON.stringify(createStartupThemeBootstrapManifest(builtinThemes), null, 2)}\n`, + }, + { + label: 'Theme prompt snapshot manifest', + outputPath: themePromptSnapshotOutputPath, + content: `${JSON.stringify(createThemePromptSnapshotManifest(builtinThemes), null, 2)}\n`, + }, + ]; + + for (const generatedFile of generatedFiles) { + const currentContent = fs.existsSync(generatedFile.outputPath) + ? fs.readFileSync(generatedFile.outputPath, 'utf8') + : null; + + if (checkOnly) { + const currentContentForCheck = currentContent == null + ? null + : normalizeGeneratedText(currentContent); + if (currentContentForCheck !== normalizeGeneratedText(generatedFile.content)) { + console.error( + `${generatedFile.label} is stale. Run \`pnpm run generate-startup-theme-bootstrap\`.`, + ); + process.exitCode = 1; + } + } else { + fs.mkdirSync(path.dirname(generatedFile.outputPath), { recursive: true }); + fs.writeFileSync(generatedFile.outputPath, generatedFile.content, 'utf8'); + console.log(`Generated ${path.relative(repoRoot, generatedFile.outputPath).replace(/\\/g, '/')}`); + } + } +} finally { + await server.close(); +} diff --git a/scripts/generate-startup-theme-bootstrap.test.mjs b/scripts/generate-startup-theme-bootstrap.test.mjs new file mode 100644 index 0000000000..6bdfd61ba1 --- /dev/null +++ b/scripts/generate-startup-theme-bootstrap.test.mjs @@ -0,0 +1,23 @@ +import assert from 'node:assert/strict'; +import fs from 'node:fs'; +import test from 'node:test'; + +function readText(filePath) { + return fs.readFileSync(filePath, 'utf8'); +} + +test('startup theme bootstrap check is stable across line endings', () => { + const generatorSource = readText('scripts/generate-startup-theme-bootstrap.mjs'); + + assert.match(generatorSource, /normalizeGeneratedText/, 'generator check should normalize line endings'); + assert.match( + generatorSource, + /replace\(?\/\\r\\n\?\/g,\s*'\\n'\)?/, + 'generator check should normalize CRLF and CR line endings to LF', + ); + assert.match( + generatorSource, + /currentContentForCheck/, + 'generator check should compare normalized current content', + ); +}); diff --git a/scripts/theme-color-audit-utils.mjs b/scripts/theme-color-audit-utils.mjs new file mode 100644 index 0000000000..83e9e2a94e --- /dev/null +++ b/scripts/theme-color-audit-utils.mjs @@ -0,0 +1,111 @@ +import fs from 'node:fs'; +import path from 'node:path'; + +function blendAlphaChannel(channel, alpha, base) { + return Math.round((channel * alpha + base * (255 - alpha)) / 255); +} + +// CLI theme presets are hex-only. These helpers intentionally do not model CSS +// rgba/hsl alpha semantics used by the web-ui color-domain audit. +export function cliRgbToHex(r, g, b) { + return `#${[r, g, b].map(value => value.toString(16).padStart(2, '0')).join('')}`; +} + +export function normalizeCliHexColor(value, options = {}) { + if (typeof value !== 'string') { + return null; + } + const match = /^#([0-9a-f]{6}|[0-9a-f]{8})$/i.exec(value.trim()); + if (!match) { + return null; + } + const raw = match[1].toLowerCase(); + if (raw.length === 6) { + return `#${raw}`; + } + + const base = options.mode === 'light' ? 255 : 0; + const alpha = Number.parseInt(raw.slice(6, 8), 16); + return cliRgbToHex( + blendAlphaChannel(Number.parseInt(raw.slice(0, 2), 16), alpha, base), + blendAlphaChannel(Number.parseInt(raw.slice(2, 4), 16), alpha, base), + blendAlphaChannel(Number.parseInt(raw.slice(4, 6), 16), alpha, base), + ); +} + +function cliHexToRgb(hex) { + const normalized = normalizeCliHexColor(hex); + if (!normalized) { + return null; + } + const raw = normalized.slice(1); + return [ + Number.parseInt(raw.slice(0, 2), 16), + Number.parseInt(raw.slice(2, 4), 16), + Number.parseInt(raw.slice(4, 6), 16), + ]; +} + +export function cliHexColorDistance(a, b) { + const rgbA = cliHexToRgb(a); + const rgbB = cliHexToRgb(b); + if (!rgbA || !rgbB) { + return Number.POSITIVE_INFINITY; + } + return Math.hypot( + rgbA[0] - rgbB[0], + rgbA[1] - rgbB[1], + rgbA[2] - rgbB[2], + ); +} + +function getMetric(report, pathExpression) { + return pathExpression.split('.').reduce((value, key) => { + if (value && Object.prototype.hasOwnProperty.call(value, key)) { + return value[key]; + } + return undefined; + }, report); +} + +export function checkBudgetBaseline(report, baselinePath, label = baselinePath) { + if (!baselinePath) { + return []; + } + const baseline = JSON.parse(fs.readFileSync(baselinePath, 'utf8')); + const failures = []; + if (baseline.version !== 1) { + failures.push(`${label} must use version 1`); + } + if (!baseline.budgets || typeof baseline.budgets !== 'object' || Array.isArray(baseline.budgets)) { + failures.push(`${label} must define a budgets object`); + return failures; + } + for (const [metric, budget] of Object.entries(baseline.budgets ?? {})) { + if (!budget || typeof budget !== 'object' || Array.isArray(budget)) { + failures.push(`${label} ${metric} budget must be an object`); + continue; + } + if (typeof budget.max !== 'number') { + failures.push(`${label} ${metric}.max must be a number`); + continue; + } + const actual = getMetric(report, metric); + if (typeof actual !== 'number') { + failures.push(`${label} references unknown numeric metric ${metric}`); + continue; + } + if (actual > budget.max) { + failures.push(`${metric} has ${actual} candidate(s), baseline is ${budget.max}`); + } else if (actual < budget.max) { + failures.push(`${metric} has ${actual} candidate(s), below baseline ${budget.max}; lower ${label}.`); + } + } + return failures; +} + +export function writeReportJson(report, reportJsonPath) { + const outputPath = path.resolve(reportJsonPath); + fs.mkdirSync(path.dirname(outputPath), { recursive: true }); + fs.writeFileSync(outputPath, `${JSON.stringify(report, null, 2)}\n`, 'utf8'); +} diff --git a/scripts/theme-color-governance-baseline.cli.json b/scripts/theme-color-governance-baseline.cli.json new file mode 100644 index 0000000000..ab9ca4ae8f --- /dev/null +++ b/scripts/theme-color-governance-baseline.cli.json @@ -0,0 +1,48 @@ +{ + "version": 1, + "description": "Baseline for CLI/TUI theme color governance. CLI is an independent terminal surface; lower values when preset or fallback palette debt is removed.", + "budgets": { + "presetColorOccurrences": { + "max": 306 + }, + "presetUniqueColors": { + "max": 114 + }, + "runtimePresetColorOccurrences": { + "max": 114 + }, + "runtimePresetUniqueColors": { + "max": 75 + }, + "compatibilityPresetColorOccurrences": { + "max": 192 + }, + "compatibilityPresetUniqueColors": { + "max": 87 + }, + "rustFallbackColorOccurrences": { + "max": 0 + }, + "rustFallbackUniqueColors": { + "max": 0 + }, + "totalUniqueColors": { + "max": 114 + }, + "runtimeTotalUniqueColors": { + "max": 75 + }, + "presetNearPairs.nearTotal": { + "max": 0 + }, + "runtimePresetNearPairs.nearTotal": { + "max": 0 + }, + "compatibilityPresetNearPairs.nearTotal": { + "max": 0 + }, + "rustFallbackNearPairs.nearTotal": { + "max": 0 + } + } +} diff --git a/scripts/theme-color-governance-baseline.installer.json b/scripts/theme-color-governance-baseline.installer.json new file mode 100644 index 0000000000..51ec345aea --- /dev/null +++ b/scripts/theme-color-governance-baseline.installer.json @@ -0,0 +1,117 @@ +{ + "version": 1, + "description": "Baseline for installer theme color governance. Lower values when debt is removed; do not raise without a documented review reason.", + "budgets": { + "fallbackOccurrences": { + "max": 0 + }, + "fallbackUniqueTokens": { + "max": 0 + }, + "fallbackContracts.uncontractedUnique": { + "max": 0 + }, + "compatibilityAliases.usedUnique": { + "max": 0 + }, + "compatibilityAliases.occurrences": { + "max": 0 + }, + "compatibilityAliases.familyUsedUnique": { + "max": 0 + }, + "compatibilityAliases.familyOccurrences": { + "max": 0 + }, + "compatibilityAliases.missingCanonicalUnique": { + "max": 0 + }, + "surfaceTokenRenames.activeUnique": { + "max": 0 + }, + "surfaceTokenRenames.activeOccurrences": { + "max": 0 + }, + "surfaceTokenRenames.missingCanonicalUnique": { + "max": 0 + }, + "colorDomainContracts.missingRegisteredUnique": { + "max": 0 + }, + "colorDomainContracts.staleRegisteredUnique": { + "max": 0 + }, + "colorDomainContracts.activeUncontractedUnique": { + "max": 0 + }, + "colorScopes.appUi.occurrences": { + "max": 0 + }, + "colorScopes.appUi.uniqueColors": { + "max": 0 + }, + "cssVarDefinitions.unresolvedRequiredUnique": { + "max": 0 + }, + "cssVarDefinitions.fallbackOnlyUnique": { + "max": 0 + }, + "cssVarDefinitions.nonContractCrossFileUnique": { + "max": 0 + }, + "cssVarDefinitions.nonContractDynamicInputUnique": { + "max": 0 + }, + "cssVarDefinitions.nonContractCssPrivateUnique": { + "max": 0 + }, + "cssVarDefinitions.unregisteredDynamicFamilyUnique": { + "max": 0 + }, + "tokenAliasLiterals.occurrences": { + "max": 0 + }, + "nearPairs.indistinguishableTotal": { + "max": 0 + }, + "nearPairs.nearTotal": { + "max": 0 + }, + "colorDomainNearPairs.themePreset.indistinguishableTotal": { + "max": 0 + }, + "colorDomainNearPairs.themePreset.nearTotal": { + "max": 3 + }, + "colorDomainNearPairs.tokenContract.indistinguishableTotal": { + "max": 0 + }, + "colorDomainNearPairs.tokenContract.nearTotal": { + "max": 0 + }, + "colorDomainNearPairs.appUi.indistinguishableTotal": { + "max": 0 + }, + "colorDomainNearPairs.appUi.nearTotal": { + "max": 0 + }, + "colorDomainScopes.themePreset.occurrences": { + "max": 54 + }, + "colorDomainScopes.themePreset.uniqueColors": { + "max": 53 + }, + "colorDomainScopes.tokenContract.occurrences": { + "max": 22 + }, + "colorDomainScopes.tokenContract.uniqueColors": { + "max": 19 + }, + "colorDomainScopes.appUi.occurrences": { + "max": 0 + }, + "colorDomainScopes.appUi.uniqueColors": { + "max": 0 + } + } +} diff --git a/scripts/theme-color-governance-baseline.json b/scripts/theme-color-governance-baseline.json index cf2ce1b8c0..4162df32c6 100644 --- a/scripts/theme-color-governance-baseline.json +++ b/scripts/theme-color-governance-baseline.json @@ -3,10 +3,10 @@ "description": "Baseline for Web UI theme color governance. Lower values when debt is removed; do not raise without a documented review reason.", "budgets": { "fallbackOccurrences": { - "max": 25 + "max": 0 }, "fallbackUniqueTokens": { - "max": 7 + "max": 0 }, "fallbackContracts.uncontractedUnique": { "max": 0 @@ -15,13 +15,13 @@ "max": 0 }, "compatibilityAliases.registeredUnique": { - "max": 63 + "max": 0 }, "compatibilityAliases.usedUnique": { - "max": 68 + "max": 0 }, "compatibilityAliases.occurrences": { - "max": 608 + "max": 0 }, "compatibilityAliases.staleRegisteredUnique": { "max": 0 @@ -30,10 +30,10 @@ "max": 2 }, "compatibilityAliases.familyUsedUnique": { - "max": 2 + "max": 0 }, "compatibilityAliases.familyOccurrences": { - "max": 51 + "max": 0 }, "compatibilityAliases.staleRegisteredFamilyUnique": { "max": 0 @@ -41,6 +41,51 @@ "compatibilityAliases.missingCanonicalUnique": { "max": 0 }, + "surfaceTokenRenames.registeredUnique": { + "max": 9 + }, + "surfaceTokenRenames.activeUnique": { + "max": 0 + }, + "surfaceTokenRenames.activeOccurrences": { + "max": 0 + }, + "surfaceTokenRenames.missingCanonicalUnique": { + "max": 0 + }, + "generatedWidgetPayload.varUnique": { + "max": 80 + }, + "generatedWidgetPayload.occurrences": { + "max": 80 + }, + "generatedWidgetPayload.undefinedUnique": { + "max": 0 + }, + "generatedWidgetPayload.compatibilityAliasUnique": { + "max": 0 + }, + "generatedWidgetPayload.compatibilityAliasOccurrences": { + "max": 0 + }, + "generatedWidgetPayload.compatibilityAliasFamilyUnique": { + "max": 0 + }, + "generatedWidgetPayload.compatibilityAliasFamilyOccurrences": { + "max": 0 + }, + "generatedWidgetPayload.externalOnlyCompatibilityUnique": { + "max": 0 + }, + "generatedWidgetPayload.externalOnlyCompatibilityOccurrences": { + "max": 0 + }, + "generatedWidgetPayload.missingCompatibilityCanonicalUnique": { + "max": 0 + }, + "generatedWidgetPayload.unexportedCompatibilityCanonicalUnique": { + "max": 0 + }, "colorDomainContracts.registeredUnique": { "max": 13 }, @@ -60,10 +105,10 @@ "max": 0 }, "colorScopes.token.uniqueColors": { - "max": 697 + "max": 197 }, "colorScopes.exception.uniqueColors": { - "max": 274 + "max": 182 }, "cssVarDefinitions.unresolvedRequiredUnique": { "max": 0 @@ -86,6 +131,9 @@ "cssVarDefinitions.unregisteredDynamicFamilyUnique": { "max": 0 }, + "cssVarDefinitions.dynamicFamilyUnexportedUnique": { + "max": 0 + }, "cssVarDefinitions.staleRegisteredDynamicFamilyUnique": { "max": 0 }, @@ -101,17 +149,107 @@ "nearPairs.nearTotal": { "max": 0 }, + "colorDomainNearPairs.indistinguishableTotal": { + "max": 0 + }, + "colorDomainNearPairs.nearTotal": { + "max": 9 + }, + "colorDomainNearPairs.themePreset.indistinguishableTotal": { + "max": 0 + }, + "colorDomainNearPairs.themePreset.nearTotal": { + "max": 5 + }, + "colorDomainNearPairs.themeRuntime.indistinguishableTotal": { + "max": 0 + }, + "colorDomainNearPairs.themeRuntime.nearTotal": { + "max": 2 + }, + "colorDomainNearPairs.tokenContract.indistinguishableTotal": { + "max": 0 + }, + "colorDomainNearPairs.tokenContract.nearTotal": { + "max": 2 + }, + "colorDomainNearPairs.generatedWidget.indistinguishableTotal": { + "max": 0 + }, + "colorDomainNearPairs.generatedWidget.nearTotal": { + "max": 0 + }, + "colorDomainNearPairs.boundaryFallback.indistinguishableTotal": { + "max": 0 + }, + "colorDomainNearPairs.boundaryFallback.nearTotal": { + "max": 0 + }, + "colorDomainNearPairs.mermaid.indistinguishableTotal": { + "max": 0 + }, + "colorDomainNearPairs.mermaid.nearTotal": { + "max": 0 + }, + "colorDomainNearPairs.editor.indistinguishableTotal": { + "max": 0 + }, + "colorDomainNearPairs.editor.nearTotal": { + "max": 0 + }, + "colorDomainNearPairs.syntax.indistinguishableTotal": { + "max": 0 + }, + "colorDomainNearPairs.syntax.nearTotal": { + "max": 0 + }, + "colorDomainNearPairs.terminal.indistinguishableTotal": { + "max": 0 + }, + "colorDomainNearPairs.terminal.nearTotal": { + "max": 0 + }, + "colorDomainNearPairs.debugOverlay.indistinguishableTotal": { + "max": 0 + }, + "colorDomainNearPairs.debugOverlay.nearTotal": { + "max": 0 + }, + "colorDomainNearPairs.uiException.indistinguishableTotal": { + "max": 0 + }, + "colorDomainNearPairs.uiException.nearTotal": { + "max": 0 + }, + "colorDomainNearPairs.languageIdentity.indistinguishableTotal": { + "max": 0 + }, + "colorDomainNearPairs.languageIdentity.nearTotal": { + "max": 0 + }, + "colorDomainNearPairs.visualEffect.indistinguishableTotal": { + "max": 0 + }, + "colorDomainNearPairs.visualEffect.nearTotal": { + "max": 0 + }, + "colorDomainNearPairs.appUi.indistinguishableTotal": { + "max": 0 + }, + "colorDomainNearPairs.appUi.nearTotal": { + "max": 0 + }, "colorDomainScopes.themePreset.occurrences": { - "max": 1033 + "max": 170 }, "colorDomainScopes.themePreset.uniqueColors": { - "max": 611 + "max": 116 }, "colorDomainScopes.themeRuntime.occurrences": { - "max": 54 + "max": 27 }, "colorDomainScopes.tokenContract.occurrences": { - "max": 268 + "max": 108 }, "colorDomainScopes.generatedWidget.occurrences": { "max": 0 @@ -120,34 +258,34 @@ "max": 0 }, "colorDomainScopes.boundaryFallback.occurrences": { - "max": 22 + "max": 18 }, "colorDomainScopes.boundaryFallback.uniqueColors": { - "max": 22 + "max": 18 }, "colorDomainScopes.editor.occurrences": { - "max": 56 + "max": 52 }, "colorDomainScopes.syntax.occurrences": { - "max": 18 + "max": 16 }, "colorDomainScopes.terminal.occurrences": { - "max": 38 + "max": 37 }, "colorDomainScopes.debugOverlay.occurrences": { "max": 0 }, "colorDomainScopes.uiException.occurrences": { - "max": 38 + "max": 17 }, "colorDomainScopes.uiException.uniqueColors": { - "max": 34 + "max": 17 }, "colorDomainScopes.languageIdentity.occurrences": { - "max": 52 + "max": 8 }, "colorDomainScopes.languageIdentity.uniqueColors": { - "max": 50 + "max": 8 }, "colorDomainScopes.visualEffect.occurrences": { "max": 0 @@ -159,10 +297,10 @@ "max": 0 }, "colorDomainScopes.mermaid.occurrences": { - "max": 139 + "max": 82 }, "colorDomainScopes.mermaid.uniqueColors": { - "max": 95 + "max": 65 } } } diff --git a/scripts/theme-color-governance-baseline.mobile-web.json b/scripts/theme-color-governance-baseline.mobile-web.json new file mode 100644 index 0000000000..427e8a73a4 --- /dev/null +++ b/scripts/theme-color-governance-baseline.mobile-web.json @@ -0,0 +1,105 @@ +{ + "version": 1, + "description": "Baseline for Mobile Web theme color governance. Lower values when debt is removed; do not raise without a documented review reason.", + "budgets": { + "fallbackOccurrences": { + "max": 0 + }, + "fallbackUniqueTokens": { + "max": 0 + }, + "fallbackContracts.uncontractedUnique": { + "max": 0 + }, + "compatibilityAliases.usedUnique": { + "max": 0 + }, + "compatibilityAliases.occurrences": { + "max": 0 + }, + "compatibilityAliases.familyUsedUnique": { + "max": 0 + }, + "compatibilityAliases.familyOccurrences": { + "max": 0 + }, + "compatibilityAliases.missingCanonicalUnique": { + "max": 0 + }, + "surfaceTokenRenames.activeUnique": { + "max": 0 + }, + "surfaceTokenRenames.activeOccurrences": { + "max": 0 + }, + "surfaceTokenRenames.missingCanonicalUnique": { + "max": 0 + }, + "colorDomainContracts.missingRegisteredUnique": { + "max": 0 + }, + "colorDomainContracts.staleRegisteredUnique": { + "max": 0 + }, + "colorDomainContracts.activeUncontractedUnique": { + "max": 0 + }, + "colorScopes.appUi.occurrences": { + "max": 0 + }, + "colorScopes.appUi.uniqueColors": { + "max": 0 + }, + "cssVarDefinitions.unresolvedRequiredUnique": { + "max": 0 + }, + "cssVarDefinitions.fallbackOnlyUnique": { + "max": 0 + }, + "cssVarDefinitions.nonContractCrossFileUnique": { + "max": 0 + }, + "cssVarDefinitions.nonContractDynamicInputUnique": { + "max": 0 + }, + "cssVarDefinitions.nonContractCssPrivateUnique": { + "max": 0 + }, + "cssVarDefinitions.unregisteredDynamicFamilyUnique": { + "max": 0 + }, + "tokenAliasLiterals.occurrences": { + "max": 0 + }, + "nearPairs.indistinguishableTotal": { + "max": 0 + }, + "nearPairs.nearTotal": { + "max": 0 + }, + "colorDomainNearPairs.themePreset.indistinguishableTotal": { + "max": 0 + }, + "colorDomainNearPairs.themePreset.nearTotal": { + "max": 0 + }, + "colorDomainNearPairs.appUi.indistinguishableTotal": { + "max": 0 + }, + "colorDomainNearPairs.appUi.nearTotal": { + "max": 0 + }, + "colorDomainScopes.themePreset.occurrences": { + "max": 35 + }, + "colorDomainScopes.themePreset.uniqueColors": { + "max": 30 + }, + "colorDomainScopes.appUi.occurrences": { + "max": 0 + }, + "colorDomainScopes.appUi.uniqueColors": { + "max": 0 + } + } +} diff --git a/scripts/theme-color-near-pair-decisions.json b/scripts/theme-color-near-pair-decisions.json new file mode 100644 index 0000000000..f5cfa7c611 --- /dev/null +++ b/scripts/theme-color-near-pair-decisions.json @@ -0,0 +1,114 @@ +{ + "version": 1, + "description": "Explicit decisions for specialized theme near-color pairs. Keep this file in sync with scripts/audit-theme-colors.mjs output; lower the audit baseline when a pair is merged.", + "decisions": [ + { + "root": "src/web-ui/src", + "domain": "themePreset", + "key": "#1a1b26 <-> #1c1c1f", + "decision": "keep", + "owner": "src/web-ui/src/infrastructure/theme/presets", + "reason": "Tokyo Night primary background uses the canonical Tokyo base hue; #1c1c1f is the default dark/china/slate neutral surface. Merging would flatten cross-theme identity rather than remove an app-level duplicate.", + "reevaluateWhen": "Only after a visual review proves Tokyo Night no longer needs a distinct primary background identity." + }, + { + "root": "src/web-ui/src", + "domain": "themePreset", + "key": "#1c1c1f <-> #212019", + "decision": "keep", + "owner": "src/web-ui/src/infrastructure/theme/presets", + "reason": "China Night secondary background also feeds Monaco lineHighlight through the theme projection. Merging it into the primary background removes the visible current-line cue and flattens adjacent ink surfaces.", + "reevaluateWhen": "Only if China Night gets a separate editor lineHighlight token or visual review proves the current-line cue remains visible." + }, + { + "root": "src/web-ui/src", + "domain": "themePreset", + "key": "#f0f4f8 <-> #f3f3f5", + "decision": "keep", + "owner": "src/web-ui/src/infrastructure/theme/presets", + "reason": "The pair crosses light primary background and Monaco lineHighlight / button state values. Editor line highlight must remain visibly distinct from base surface roles.", + "reevaluateWhen": "Only after Monaco-focused light-theme screenshots confirm the line highlight remains readable." + }, + { + "root": "src/web-ui/src", + "domain": "themePreset", + "key": "#2b2d30 <-> #313335", + "decision": "keep", + "owner": "src/web-ui/src/infrastructure/theme/presets", + "reason": "Midnight background, tertiary surface, and editor lineHighlight are adjacent in the same dark theme. Merging would weaken surface and editor-row separation.", + "reevaluateWhen": "Only after a Midnight surface-ramp redesign removes the adjacent role split." + }, + { + "root": "src/web-ui/src", + "domain": "themePreset", + "key": "#f3f3f5 <-> #faf8f0", + "decision": "keep", + "owner": "src/web-ui/src/infrastructure/theme/presets", + "reason": "China Style paper uses a warm first-paint surface while #f3f3f5 is a cool light neutral and button state stop. This is cross-theme identity, not a duplicate role.", + "reevaluateWhen": "Only after China Style paper identity is intentionally neutralized." + }, + { + "root": "src/web-ui/src", + "domain": "themeRuntime", + "key": "rgba(255, 255, 255, 0.12) <-> rgba(255, 255, 255, 0.15)", + "decision": "keep", + "owner": "src/web-ui/src/infrastructure/theme/core/ThemeService.ts", + "reason": "The 0.12 and 0.15 white overlay stops separate hover/active/elevated states and must stay aligned with static tokens.", + "reevaluateWhen": "Only after the runtime and static overlay ladder are simplified together with visual coverage for affected card, markdown, and shadow states." + }, + { + "root": "src/web-ui/src", + "domain": "themeRuntime", + "key": "rgba(0, 0, 0, 0.12) <-> rgba(0, 0, 0, 0.15)", + "decision": "keep", + "owner": "src/web-ui/src/infrastructure/theme/core/ThemeService.ts", + "reason": "The 0.12 and 0.15 black overlay stops separate light-theme card/elevated/active states and must stay aligned with static tokens.", + "reevaluateWhen": "Only after light-theme surface-state screenshots show no hierarchy loss." + }, + { + "root": "src/web-ui/src", + "domain": "tokenContract", + "key": "rgba(255, 255, 255, 0.12) <-> rgba(255, 255, 255, 0.15)", + "decision": "keep", + "owner": "src/web-ui/src/component-library/styles/tokens.scss", + "reason": "Static token stops mirror runtime overlay state levels for first paint. Removing one side without runtime alignment would create early-render drift.", + "reevaluateWhen": "Only when the matching runtime overlay pair is also retired." + }, + { + "root": "src/web-ui/src", + "domain": "tokenContract", + "key": "rgba(0, 0, 0, 0.12) <-> rgba(0, 0, 0, 0.15)", + "decision": "keep", + "owner": "src/web-ui/src/component-library/styles/tokens.scss", + "reason": "Static black overlay stops mirror runtime light-theme state levels for first paint. The pair is a state ramp, not an unowned duplicate.", + "reevaluateWhen": "Only when the matching runtime overlay pair is also retired." + }, + { + "root": "BitFun-Installer/src", + "domain": "themePreset", + "key": "#0e0e10 <-> #121214", + "decision": "keep", + "owner": "BitFun-Installer/src/theme/installerThemesData.ts", + "reason": "ThemeSetup renders dark theme cards side by side with primary and secondary preview backgrounds. Cyber needs its own darker primary preview seed so the named theme is not distinguishable only by label and accent.", + "reevaluateWhen": "Only if installer theme selection adds a separate identity swatch or screenshots prove the primary preview no longer carries theme identity." + }, + { + "root": "BitFun-Installer/src", + "domain": "themePreset", + "key": "#1a1b26 <-> #1a1c1e", + "decision": "keep", + "owner": "BitFun-Installer/src/theme/installerThemesData.ts", + "reason": "Tokyo Night appears as an adjacent installer preview card, and its primary background should preserve the Tokyo base hue while the shared installer card surface stays neutral.", + "reevaluateWhen": "Only after installer preview cards are redesigned to use a dedicated non-background theme identity affordance." + }, + { + "root": "BitFun-Installer/src", + "domain": "themePreset", + "key": "#121214 <-> #1a1814", + "decision": "keep", + "owner": "BitFun-Installer/src/theme/installerThemesData.ts", + "reason": "Ink Night uses a warmer dark primary background in adjacent installer preview cards. Collapsing it into the canonical dark seed makes multiple dark choices visually flatter before the app is installed.", + "reevaluateWhen": "Only if visual review shows installer dark theme cards no longer rely on primary background identity." + } + ] +} diff --git a/scripts/theme-css-var-contract.mjs b/scripts/theme-css-var-contract.mjs index 9c3a5743af..497017c87d 100644 --- a/scripts/theme-css-var-contract.mjs +++ b/scripts/theme-css-var-contract.mjs @@ -4,6 +4,8 @@ export const DEFAULT_BASELINE_PATH = 'scripts/theme-color-governance-baseline.js export const COLOR_EXTENSIONS = new Set(['.css', '.scss', '.sass', '.ts', '.tsx', '.js', '.jsx']); export const TOKEN_PATH_PARTS = [ + 'BitFun-Installer/src/styles/variables.css', + 'BitFun-Installer/src/theme', 'component-library/styles', 'infrastructure/theme', 'theme/presets', @@ -14,16 +16,21 @@ export const TOKEN_ALIAS_SOURCE_PATH_PARTS = [ ]; export const CONTRACT_VAR_DEFINITION_PATH_PARTS = [ + 'BitFun-Installer/src/styles/variables.css', + 'BitFun-Installer/src/theme/installerThemeRuntime.ts', 'component-library/styles', 'infrastructure/theme', + 'src/mobile-web/src/theme/presets', 'tools/generative-widget/themePayload.ts', ]; export const STATIC_CONTRACT_VAR_DEFINITION_PATH_PARTS = [ + 'BitFun-Installer/src/styles/variables.css', 'component-library/styles', ]; export const RUNTIME_CONTRACT_VAR_DEFINITION_PATH_PARTS = [ + 'BitFun-Installer/src/theme/installerThemeRuntime.ts', 'infrastructure/theme', ]; @@ -44,7 +51,7 @@ export const COLOR_DOMAIN_RULES = [ { key: 'themePreset', label: 'Theme presets', - pathParts: ['infrastructure/theme/presets', 'theme/presets'], + pathParts: ['BitFun-Installer/src/theme', 'infrastructure/theme/presets', 'theme/presets'], }, { key: 'themeRuntime', @@ -54,7 +61,7 @@ export const COLOR_DOMAIN_RULES = [ { key: 'tokenContract', label: 'Token contracts', - pathParts: ['component-library/styles'], + pathParts: ['BitFun-Installer/src/styles/variables.css', 'component-library/styles'], }, { key: 'generatedWidget', @@ -136,7 +143,7 @@ export const COLOR_DOMAIN_CONTRACTS = [ key: 'themeRuntime', owner: 'src/web-ui/src/infrastructure/theme/core/ThemeService.ts', reason: 'Runtime theme injection is the cross-platform bridge for static CSS, desktop WebView, web preview, and generated widget payloads.', - mergePolicy: 'Do not remove runtime aliases until static contract, runtime contract, and widget payload all stop requiring them.', + mergePolicy: 'Do not remove runtime aliases until static contract, runtime contract, and widget iframe compatibility fallback all stop requiring them.', }, { key: 'tokenContract', @@ -148,7 +155,7 @@ export const COLOR_DOMAIN_CONTRACTS = [ key: 'generatedWidget', owner: 'src/web-ui/src/tools/generative-widget', reason: 'Generated widgets run in an isolated iframe boundary and need an explicit payload instead of scraping host CSS variables.', - mergePolicy: 'Keep payload variables stable for compatibility; shrink only after widget consumers no longer read the alias.', + mergePolicy: 'Keep payload variables canonical; keep legacy aliases in iframe fallback until widget consumers no longer read them.', }, { key: 'boundaryFallback', @@ -206,509 +213,81 @@ export const COLOR_DOMAIN_CONTRACTS = [ }, ]; -export const TOKEN_COMPATIBILITY_ALIAS_CONTRACTS = [ - { - key: '--color-bg-flowchat', - canonical: '--color-bg-scene', - owner: 'src/web-ui/src/component-library/styles/tokens.scss; src/web-ui/src/infrastructure/theme/core/ThemeService.ts', - reason: 'Flow chat background remains a named surface alias while the scene background is the canonical root value.', - removal: 'Retire only after FlowChat, generated widget payload, and any persisted custom CSS no longer read the alias.', - }, - { - key: '--color-bg-surface', - canonical: '--color-bg-secondary', - owner: 'src/web-ui/src/component-library/styles/tokens.scss; src/web-ui/src/infrastructure/theme/core/ThemeService.ts', - reason: 'Surface background is an older shared role that currently resolves to the secondary background in every builtin theme.', - removal: 'Retire after component-level surface tokens replace generic surface reads.', - }, - { - key: '--color-bg-subtle', - canonical: '--element-bg-subtle', - owner: 'src/web-ui/src/component-library/styles/tokens.scss; src/web-ui/src/infrastructure/theme/core/ThemeService.ts', - reason: 'Subtle background is an element-layer alias, not an independent app background palette.', - removal: 'Retire after callers migrate to element surface tokens.', - }, - { - key: '--color-bg-hover', - canonical: '--element-bg-hover', - owner: 'src/web-ui/src/component-library/styles/tokens.scss; src/web-ui/src/infrastructure/theme/core/ThemeService.ts', - reason: 'Hover background historically lived under color-bg but now belongs to the element interaction layer.', - removal: 'Retire after hover callers move to element or component interaction tokens.', - }, - { - key: '--color-bg-elevated-hover', - canonical: '--element-bg-hover', - owner: 'src/web-ui/src/component-library/styles/tokens.scss; src/web-ui/src/infrastructure/theme/core/ThemeService.ts', - reason: 'Elevated hover resolves to the same element hover role and does not represent a separate theme primitive today.', - removal: 'Retire after elevated surfaces expose component-specific hover tokens.', - }, - { - key: '--color-bg-base', - canonical: '--color-bg-primary', - owner: 'src/web-ui/src/component-library/styles/tokens.scss; src/web-ui/src/infrastructure/theme/core/ThemeService.ts', - reason: 'Base background is a historical alias for the primary application background.', - removal: 'Retire after legacy layout selectors stop reading base background.', - }, - { - key: '--color-surface-elevated', - canonical: '--element-bg-elevated', - owner: 'src/web-ui/src/component-library/styles/tokens.scss; src/web-ui/src/infrastructure/theme/core/ThemeService.ts', - reason: 'Elevated surface is implemented by the element elevated layer, not a separate color family.', - removal: 'Retire after elevated component tokens cover all consumers.', - }, - { - key: '--color-surface-hover', - canonical: '--element-bg-hover', - owner: 'src/web-ui/src/component-library/styles/tokens.scss; src/web-ui/src/infrastructure/theme/core/ThemeService.ts', - reason: 'Surface hover is a compatibility alias for the element hover layer.', - removal: 'Retire after component selectors migrate to element or component hover tokens.', - }, - { - key: '--color-hover', - canonical: '--element-bg-hover', - owner: 'src/web-ui/src/component-library/styles/tokens.scss; src/web-ui/src/infrastructure/theme/core/ThemeService.ts', - reason: 'Generic hover is retained for old selectors that predate the element layer naming.', - removal: 'Retire after all callers use role-specific hover tokens.', - }, - { - key: '--bg-primary', - canonical: '--color-bg-primary', - owner: 'src/web-ui/src/component-library/styles/tokens.scss; src/web-ui/src/infrastructure/theme/core/ThemeService.ts', - reason: 'Short background aliases are kept for historical component and external widget compatibility.', - removal: 'Retire after source and generated widget payload stop exposing short background aliases.', - }, - { - key: '--bg-secondary', - canonical: '--color-bg-secondary', - owner: 'src/web-ui/src/component-library/styles/tokens.scss; src/web-ui/src/infrastructure/theme/core/ThemeService.ts', - reason: 'Short background aliases are kept for historical component and external widget compatibility.', - removal: 'Retire after source and generated widget payload stop exposing short background aliases.', - }, - { - key: '--bg-tertiary', - canonical: '--color-bg-tertiary', - owner: 'src/web-ui/src/component-library/styles/tokens.scss; src/web-ui/src/infrastructure/theme/core/ThemeService.ts', - reason: 'Short background aliases are kept for historical component and external widget compatibility.', - removal: 'Retire after source and generated widget payload stop exposing short background aliases.', - }, - { - key: '--bg-elevated', - canonical: '--color-bg-elevated', - owner: 'src/web-ui/src/component-library/styles/tokens.scss; src/web-ui/src/infrastructure/theme/core/ThemeService.ts', - reason: 'Short elevated background alias preserves older selector and generated widget payload compatibility.', - removal: 'Retire after elevated component tokens replace the short alias.', - }, - { - key: '--bg-hover', - canonical: '--element-bg-hover', - owner: 'src/web-ui/src/component-library/styles/tokens.scss; src/web-ui/src/infrastructure/theme/core/ThemeService.ts', - reason: 'Short hover background alias maps to the canonical element hover layer.', - removal: 'Retire after all hover consumers use element or component tokens.', - }, - { - key: '--secondary-bg', - canonical: '--color-bg-secondary', - owner: 'src/web-ui/src/component-library/styles/tokens.scss; src/web-ui/src/infrastructure/theme/core/ThemeService.ts', - reason: 'Legacy secondary background alias remains for older CSS modules and generated widget compatibility.', - removal: 'Retire after workspace and legacy CSS callers migrate to color-bg-secondary.', - }, - { - key: '--background-primary', - canonical: '--color-bg-primary', - owner: 'src/web-ui/src/component-library/styles/tokens.scss; src/web-ui/src/infrastructure/theme/core/ThemeService.ts', - reason: 'Background primary alias preserves older naming used by app and embedded surfaces.', - removal: 'Retire after all background-* callers migrate to color-bg-* names.', - }, - { - key: '--background-secondary', - canonical: '--color-bg-secondary', - owner: 'src/web-ui/src/component-library/styles/tokens.scss; src/web-ui/src/infrastructure/theme/core/ThemeService.ts', - reason: 'Background secondary alias preserves older naming used by app and embedded surfaces.', - removal: 'Retire after all background-* callers migrate to color-bg-* names.', - }, - { - key: '--background-tertiary', - canonical: '--color-bg-tertiary', - owner: 'src/web-ui/src/component-library/styles/tokens.scss; src/web-ui/src/infrastructure/theme/core/ThemeService.ts', - reason: 'Background tertiary alias preserves older naming used by app and embedded surfaces.', - removal: 'Retire after all background-* callers migrate to color-bg-* names.', - }, - { - key: '--color-background-secondary', - canonical: '--color-bg-secondary', - owner: 'src/web-ui/src/component-library/styles/tokens.scss; src/web-ui/src/infrastructure/theme/core/ThemeService.ts', - reason: 'Color-background secondary exists only as a historical spelling variant.', - removal: 'Retire after callers use color-bg-secondary.', - }, - { - key: '--color-background-tertiary', - canonical: '--color-bg-tertiary', - owner: 'src/web-ui/src/component-library/styles/tokens.scss; src/web-ui/src/infrastructure/theme/core/ThemeService.ts', - reason: 'Color-background tertiary exists only as a historical spelling variant.', - removal: 'Retire after callers use color-bg-tertiary.', - }, - { - key: '--color-text-tertiary', - canonical: '--color-text-muted', - owner: 'src/web-ui/src/component-library/styles/tokens.scss; src/web-ui/src/infrastructure/theme/core/ThemeService.ts', - reason: 'Tertiary text resolves to muted text in the current theme model and should not imply a fourth text ramp.', - removal: 'Retire after consumers choose either muted text or a component-specific subdued text role.', - }, - { - key: '--text-primary', - canonical: '--color-text-primary', - owner: 'src/web-ui/src/component-library/styles/tokens.scss; src/web-ui/src/infrastructure/theme/core/ThemeService.ts', - reason: 'Short text aliases are retained for legacy CSS and generated widget payload compatibility.', - removal: 'Retire after all text-* consumers move to color-text-* names.', - }, - { - key: '--text-secondary', - canonical: '--color-text-secondary', - owner: 'src/web-ui/src/component-library/styles/tokens.scss; src/web-ui/src/infrastructure/theme/core/ThemeService.ts', - reason: 'Short text aliases are retained for legacy CSS and generated widget payload compatibility.', - removal: 'Retire after all text-* consumers move to color-text-* names.', - }, - { - key: '--text-tertiary', - canonical: '--color-text-muted', - owner: 'src/web-ui/src/component-library/styles/tokens.scss; src/web-ui/src/infrastructure/theme/core/ThemeService.ts', - reason: 'Short tertiary text alias maps to muted text and should not become an independent text scale.', - removal: 'Retire after consumers migrate to muted text or component-specific subdued text tokens.', - }, - { - key: '--text-muted', - canonical: '--color-text-muted', - owner: 'src/web-ui/src/component-library/styles/tokens.scss; src/web-ui/src/infrastructure/theme/core/ThemeService.ts', - reason: 'Short muted text alias is kept for legacy CSS and generated widget payload compatibility.', - removal: 'Retire after all text-* consumers move to color-text-* names.', - }, - { - key: '--text-disabled', - canonical: '--color-text-disabled', - owner: 'src/web-ui/src/component-library/styles/tokens.scss; src/web-ui/src/infrastructure/theme/core/ThemeService.ts', - reason: 'Short disabled text alias is kept for legacy CSS and generated widget payload compatibility.', - removal: 'Retire after all text-* consumers move to color-text-* names.', - }, - { - key: '--color-primary', - canonical: '--color-accent-500', - owner: 'src/web-ui/src/component-library/styles/tokens.scss; src/web-ui/src/infrastructure/theme/core/ThemeService.ts', - reason: 'Primary currently means the active accent midpoint; it is kept as compatibility for older primary-button and focus selectors.', - removal: 'Retire only after primary action tokens are componentized and widget payload no longer exports this key.', - }, - { - key: '--color-primary-hover', - canonical: '--color-accent-600', - owner: 'src/web-ui/src/component-library/styles/tokens.scss; src/web-ui/src/infrastructure/theme/core/ThemeService.ts', - reason: 'Primary hover is the active accent hover stop in all builtin themes.', - removal: 'Retire after callers use accent hover or component action tokens.', - }, - { - key: '--color-accent', - canonical: '--color-accent-500', - owner: 'src/web-ui/src/component-library/styles/tokens.scss; src/web-ui/src/infrastructure/theme/core/ThemeService.ts', - reason: 'Generic accent alias remains for legacy selectors that predate numeric accent scale usage.', - removal: 'Retire after callers use explicit accent scale stops or component tokens.', - }, - { - key: '--color-accent-primary', - canonical: '--color-accent-500', - owner: 'src/web-ui/src/component-library/styles/tokens.scss; src/web-ui/src/infrastructure/theme/core/ThemeService.ts', - reason: 'Accent-primary is a historical spelling of the active accent midpoint.', - removal: 'Retire after generated widget payload and source callers stop reading it.', - }, - { - key: '--accent-primary', - canonical: '--color-accent-500', - owner: 'src/web-ui/src/component-library/styles/tokens.scss; src/web-ui/src/infrastructure/theme/core/ThemeService.ts', - reason: 'Accent-primary short alias is kept for older CSS modules and external payload compatibility.', - removal: 'Retire after callers use color-accent-500 or component action tokens.', - }, - { - key: '--accent-primary-hover', - canonical: '--color-accent-600', - owner: 'src/web-ui/src/component-library/styles/tokens.scss; src/web-ui/src/infrastructure/theme/core/ThemeService.ts', - reason: 'Accent-primary hover short alias resolves to the canonical accent hover stop.', - removal: 'Retire after callers use color-accent-600 or component action tokens.', - }, - { - key: '--color-primary-400', - canonical: '--color-accent-400', - owner: 'src/web-ui/src/component-library/styles/tokens.scss; src/web-ui/src/infrastructure/theme/core/ThemeService.ts', - reason: 'Primary scale aliases mirror accent scale stops for historical primary naming.', - removal: 'Retire after primary-* scale reads migrate to color-accent-*.', - }, - { - key: '--color-primary-500', - canonical: '--color-accent-500', - owner: 'src/web-ui/src/component-library/styles/tokens.scss; src/web-ui/src/infrastructure/theme/core/ThemeService.ts', - reason: 'Primary scale aliases mirror accent scale stops for historical primary naming.', - removal: 'Retire after primary-* scale reads migrate to color-accent-*.', - }, - { - key: '--color-primary-alpha', - canonical: '--color-accent-100', - owner: 'src/web-ui/src/component-library/styles/tokens.scss; src/web-ui/src/infrastructure/theme/core/ThemeService.ts', - reason: 'Primary alpha is a compatibility alias for the faint accent surface.', - removal: 'Retire after callers use color-accent-100 or component-specific accent backgrounds.', - }, - { - key: '--color-primary-bg', - canonical: '--color-accent-100', - owner: 'src/web-ui/src/component-library/styles/tokens.scss; src/web-ui/src/infrastructure/theme/core/ThemeService.ts', - reason: 'Primary background is a compatibility alias for the faint accent surface.', - removal: 'Retire after callers use color-accent-100 or component-specific accent backgrounds.', - }, - { - key: '--color-primary-bg-subtle', - canonical: '--color-accent-50', - owner: 'src/web-ui/src/component-library/styles/tokens.scss; src/web-ui/src/infrastructure/theme/core/ThemeService.ts', - reason: 'Primary subtle background is a compatibility alias for the faintest accent surface.', - removal: 'Retire after callers use color-accent-50 or component-specific accent backgrounds.', - }, - { - key: '--color-accent-alpha', - canonical: '--color-accent-100', - owner: 'src/web-ui/src/component-library/styles/tokens.scss; src/web-ui/src/infrastructure/theme/core/ThemeService.ts', - reason: 'Accent alpha is a compatibility name for the faint accent surface stop.', - removal: 'Retire after callers use explicit accent scale stops.', - }, - { - key: '--color-success-100', - canonical: '--color-success-bg', - owner: 'src/web-ui/src/component-library/styles/tokens.scss; src/web-ui/src/infrastructure/theme/core/ThemeService.ts', - reason: 'Semantic numeric aliases mirror background and foreground roles for older components.', - removal: 'Retire after callers use semantic role names instead of numeric status stops.', - }, - { - key: '--color-success-500', - canonical: '--color-success', - owner: 'src/web-ui/src/component-library/styles/tokens.scss; src/web-ui/src/infrastructure/theme/core/ThemeService.ts', - reason: 'Semantic numeric aliases mirror background and foreground roles for older components.', - removal: 'Retire after callers use semantic role names instead of numeric status stops.', - }, - { - key: '--color-warning-100', - canonical: '--color-warning-bg', - owner: 'src/web-ui/src/component-library/styles/tokens.scss; src/web-ui/src/infrastructure/theme/core/ThemeService.ts', - reason: 'Semantic numeric aliases mirror background and foreground roles for older components.', - removal: 'Retire after callers use semantic role names instead of numeric status stops.', - }, - { - key: '--color-warning-500', - canonical: '--color-warning', - owner: 'src/web-ui/src/component-library/styles/tokens.scss; src/web-ui/src/infrastructure/theme/core/ThemeService.ts', - reason: 'Semantic numeric aliases mirror background and foreground roles for older components.', - removal: 'Retire after callers use semantic role names instead of numeric status stops.', - }, - { - key: '--color-warning-700', - canonical: '--color-warning', - owner: 'src/web-ui/src/component-library/styles/tokens.scss; src/web-ui/src/infrastructure/theme/core/ThemeService.ts', - reason: 'Warning 700 currently resolves to warning foreground and is not a separate warning ramp stop.', - removal: 'Retire after warning state roles use semantic names or a real multi-stop warning palette exists.', - }, - { - key: '--color-semantic-error', - canonical: '--color-error', - owner: 'src/web-ui/src/component-library/styles/tokens.scss; src/web-ui/src/infrastructure/theme/core/ThemeService.ts', - reason: 'Semantic error is a historical spelling of the canonical error foreground.', - removal: 'Retire after callers use color-error.', - }, - { - key: '--color-danger', - canonical: '--color-error', - owner: 'src/web-ui/src/component-library/styles/tokens.scss; src/web-ui/src/infrastructure/theme/core/ThemeService.ts', - reason: 'Danger action color currently shares the error palette but stays named to avoid silently changing destructive-action semantics.', - removal: 'Retire only after destructive actions have a separate component action token or explicitly choose color-error.', - }, - { - key: '--color-danger-500', - canonical: '--color-error', - owner: 'src/web-ui/src/component-library/styles/tokens.scss; src/web-ui/src/infrastructure/theme/core/ThemeService.ts', - reason: 'Danger numeric foreground currently maps to the canonical error foreground.', - removal: 'Retire after destructive action callers use role names rather than numeric danger stops.', - }, - { - key: '--color-danger-text', - canonical: '--color-error', - owner: 'src/web-ui/src/component-library/styles/tokens.scss; src/web-ui/src/infrastructure/theme/core/ThemeService.ts', - reason: 'Danger text currently maps to error foreground while preserving destructive-action intent at call sites.', - removal: 'Retire only after destructive text call sites explicitly migrate to error or action tokens.', - }, - { - key: '--color-danger-bg', - canonical: '--color-error-bg', - owner: 'src/web-ui/src/component-library/styles/tokens.scss; src/web-ui/src/infrastructure/theme/core/ThemeService.ts', - reason: 'Danger background currently maps to error background while preserving destructive-action intent at call sites.', - removal: 'Retire only after destructive surfaces explicitly migrate to error or action tokens.', - }, - { - key: '--color-danger-border', - canonical: '--color-error-border', - owner: 'src/web-ui/src/component-library/styles/tokens.scss; src/web-ui/src/infrastructure/theme/core/ThemeService.ts', - reason: 'Danger border currently maps to error border while preserving destructive-action intent at call sites.', - removal: 'Retire only after destructive surfaces explicitly migrate to error or action tokens.', - }, - { - key: '--color-danger-hover', - canonical: '--color-error', - owner: 'src/web-ui/src/component-library/styles/tokens.scss; src/web-ui/src/infrastructure/theme/core/ThemeService.ts', - reason: 'Danger hover currently maps to error foreground while preserving destructive-action intent at call sites.', - removal: 'Retire only after destructive hover states move to component action tokens.', - }, - { - key: '--border-color', - canonical: '--border-subtle', - owner: 'src/web-ui/src/component-library/styles/tokens.scss; src/web-ui/src/infrastructure/theme/core/ThemeService.ts', - reason: 'Generic border color is retained for older selectors and maps to the subtle border role.', - removal: 'Retire after callers use explicit border-subtle or component border tokens.', - }, - { - key: '--border-hover', - canonical: '--border-medium', - owner: 'src/web-ui/src/component-library/styles/tokens.scss; src/web-ui/src/infrastructure/theme/core/ThemeService.ts', - reason: 'Border hover maps to the medium border role in the current interaction scale.', - removal: 'Retire after hover states use component interaction border tokens.', - }, - { - key: '--border-muted', - canonical: '--border-subtle', - owner: 'src/web-ui/src/component-library/styles/tokens.scss; src/web-ui/src/infrastructure/theme/core/ThemeService.ts', - reason: 'Muted border is a historical spelling for subtle border.', - removal: 'Retire after callers use border-subtle.', - }, - { - key: '--border-primary', - canonical: '--border-base', - owner: 'src/web-ui/src/component-library/styles/tokens.scss; src/web-ui/src/infrastructure/theme/core/ThemeService.ts', - reason: 'Primary border means base border in the current contract and is kept for legacy selector compatibility.', - removal: 'Retire after callers use border-base or component border tokens.', - }, - { - key: '--color-border', - canonical: '--border-base', - owner: 'src/web-ui/src/component-library/styles/tokens.scss; src/web-ui/src/infrastructure/theme/core/ThemeService.ts', - reason: 'Color-border is a legacy spelling of the canonical base border token.', - removal: 'Retire after callers use border-base.', - }, - { - key: '--color-border-primary', - canonical: '--border-base', - owner: 'src/web-ui/src/component-library/styles/tokens.scss; src/web-ui/src/infrastructure/theme/core/ThemeService.ts', - reason: 'Color-border-primary is a legacy spelling of the canonical base border token.', - removal: 'Retire after callers use border-base.', - }, - { - key: '--color-border-subtle', - canonical: '--border-subtle', - owner: 'src/web-ui/src/component-library/styles/tokens.scss; src/web-ui/src/infrastructure/theme/core/ThemeService.ts', - reason: 'Color-border-subtle is a legacy spelling of the canonical subtle border token.', - removal: 'Retire after callers use border-subtle.', - }, - { - key: '--element-bg', - canonical: '--element-bg-base', - owner: 'src/web-ui/src/component-library/styles/tokens.scss; src/web-ui/src/infrastructure/theme/core/ThemeService.ts', - reason: 'Generic element background remains as compatibility for older element surface selectors.', - removal: 'Retire after callers use explicit element-bg-base or component surface tokens.', - }, - { - key: '--motion-normal', - canonical: '--motion-base', - owner: 'src/web-ui/src/component-library/styles/tokens.scss; src/web-ui/src/infrastructure/theme/core/ThemeService.ts', - reason: 'Motion-normal is a historical alias for the base motion duration.', - removal: 'Retire after callers use motion-base.', - }, - { - key: '--font-sans', - canonical: '--font-family-sans', - owner: 'src/web-ui/src/component-library/styles/tokens.scss; src/web-ui/src/infrastructure/theme/core/ThemeService.ts', - reason: 'Short font aliases are kept for historical CSS and runtime theme payload compatibility.', - removal: 'Retire after callers use font-family-* names and widget payload no longer exports short aliases.', - }, - { - key: '--font-mono', - canonical: '--font-family-mono', - owner: 'src/web-ui/src/component-library/styles/tokens.scss; src/web-ui/src/infrastructure/theme/core/ThemeService.ts', - reason: 'Short font aliases are kept for historical CSS and runtime theme payload compatibility.', - removal: 'Retire after callers use font-family-* names and widget payload no longer exports short aliases.', - }, - { - key: '--markdown-font-mono', - canonical: '--font-family-mono', - owner: 'src/web-ui/src/component-library/styles/tokens.scss; src/web-ui/src/infrastructure/theme/core/ThemeService.ts', - reason: 'Markdown monospace remains a named alias so markdown surfaces can diverge later without breaking callers.', - removal: 'Retire only if markdown and app monospace are confirmed to remain the same contract.', - }, - { - key: '--tool-compact-summary-font', - canonical: '--font-family-sans', - owner: 'src/web-ui/src/component-library/styles/tokens.scss', - reason: 'Tool compact summaries currently use the global sans font but keep a surface alias for future tool-card typography changes.', - removal: 'Retire only if tool card typography will not diverge from global sans.', - }, -]; +export const TOKEN_COMPATIBILITY_ALIAS_CONTRACTS = []; export const TOKEN_COMPATIBILITY_ALIAS_FAMILY_CONTRACTS = [ { prefix: '--radius-', canonicalPrefix: '--size-radius-', - owner: 'src/web-ui/src/component-library/styles/tokens.scss; src/web-ui/src/infrastructure/theme/core/ThemeService.ts', - reason: 'Radius aliases keep older selectors and widget payloads working while size-radius is the canonical shape scale.', - removal: 'Retire after all source and generated widget consumers migrate to --size-radius-*.', + owner: 'src/web-ui/src/tools/generative-widget/themePayloadCompatibility.ts', + reason: 'Radius aliases are retired from root/runtime but remain recognized so old generated widget iframe content maps to the canonical shape scale.', + removal: 'Retire after generated widget iframe compatibility no longer needs --radius-* fallbacks.', }, { prefix: '--spacing-', canonicalPrefix: '--size-gap-', - owner: 'src/web-ui/src/component-library/styles/tokens.scss; src/web-ui/src/infrastructure/theme/core/ThemeService.ts', - reason: 'Spacing aliases keep older selectors and widget payloads working while size-gap is the canonical spacing scale.', - removal: 'Retire after all source and generated widget consumers migrate to --size-gap-*.', + owner: 'src/web-ui/src/tools/generative-widget/themePayloadCompatibility.ts', + reason: 'Spacing aliases are retired from root/runtime but remain recognized so old generated widget iframe content maps to the canonical spacing scale.', + removal: 'Retire after generated widget iframe compatibility no longer needs --spacing-* fallbacks.', }, ]; -export const FALLBACK_VAR_CONTRACTS = [ +export const FALLBACK_VAR_CONTRACTS = []; + +export const SURFACE_TOKEN_RENAME_CONTRACTS = [ { - key: '--surface-stagger-index', - owner: 'src/web-ui/src/app/components/GalleryLayout', - reason: 'Runtime inline animation index with zero fallback for first paint and non-animated states.', - boundary: 'layout-runtime-input', + key: '--primary-color', + canonical: '--base-tool-card-accent-color', + owner: 'src/web-ui/src/component-library/components/FlowChatCards/BaseToolCard', + reason: 'BaseToolCard used a generic local primary color key; the explicit component key prevents accidental global primary-token coupling.', }, { - key: '--mission-control-group-color', - owner: 'src/web-ui/src/app/components/panels/content-canvas/mission-control', - reason: 'Runtime group identity color with accent fallback when no group color is assigned.', - boundary: 'data-driven-identity-color', + key: '--operation-color', + canonical: '--snapshot-card-operation-color', + owner: 'src/web-ui/src/component-library/components/FlowChatCards/SnapshotCard', + reason: 'Snapshot operation color is a card-local role and should not look like a reusable operation namespace for other surfaces.', }, { - key: '--char-index', - owner: 'src/web-ui/src/component-library/components/StreamText', - reason: 'Runtime per-character animation offset with zero fallback outside animated rendering.', - boundary: 'animation-runtime-input', + key: '--delay', + canonical: '--flowchat-scroll-anchor-delay', + owner: 'src/web-ui/src/flow_chat/components/modern/ScrollAnchor', + reason: 'ScrollAnchor animation delay is a Flow Chat runtime input; the generic key is too easy to collide with unrelated animation code.', }, { - key: '--gallery-grid-min', - owner: 'src/web-ui/src/app/components/GalleryLayout', - reason: 'Runtime layout sizing input with a stable responsive grid fallback.', - boundary: 'layout-runtime-input', + key: '--um-failed-fs', + canonical: '--user-message-failed-font-size', + owner: 'src/web-ui/src/flow_chat/components/modern/UserMessageItem.scss', + reason: 'UserMessage failed-state sizing should use readable Flow Chat surface names instead of an abbreviated local key family.', }, { - key: '--gallery-skeleton-height', - owner: 'src/web-ui/src/app/components/GalleryLayout', - reason: 'Runtime skeleton sizing input with a stable placeholder height fallback.', - boundary: 'layout-runtime-input', + key: '--um-failed-lh', + canonical: '--user-message-failed-line-height', + owner: 'src/web-ui/src/flow_chat/components/modern/UserMessageItem.scss', + reason: 'UserMessage failed-state line-height should use readable Flow Chat surface names instead of an abbreviated local key family.', }, { - key: '--primary-color', - owner: 'src/web-ui/src/component-library/components/Markdown', - reason: 'Embedded markdown primary accent override with global accent fallback.', - boundary: 'embedded-content-theme-override', + key: '--um-failed-line-box', + canonical: '--user-message-failed-line-box', + owner: 'src/web-ui/src/flow_chat/components/modern/UserMessageItem.scss', + reason: 'UserMessage failed-state line box should use readable Flow Chat surface names instead of an abbreviated local key family.', }, { - key: '--scene-viewport-border-width', - owner: 'src/web-ui/src/app/scenes/SceneViewport.scss', - reason: 'Runtime viewport layout override with a stable one-pixel default.', - boundary: 'layout-runtime-input', + key: '--tool-command-preview-empty-rgb', + canonical: '--tool-command-empty-rgb', + owner: 'src/web-ui/src/flow_chat/tool-cards/ToolCommandPreview.scss; src/web-ui/src/flow_chat/tool-cards/TerminalToolCard.scss', + reason: 'Empty command color is shared by command preview and terminal command rendering, so it needs one tool-command token.', + }, + { + key: '--m-editor-highlight-rgb', + canonical: '--markdown-editor-highlight-rgb', + owner: 'src/web-ui/src/tools/editor/meditor/components/TiptapEditor.scss', + reason: 'Markdown editor highlight color should use the shared markdown-editor namespace instead of the abbreviated meditor local key.', + }, + { + key: '--m-editor-highlight-border-rgb', + canonical: '--markdown-editor-highlight-border-rgb', + owner: 'src/web-ui/src/tools/editor/meditor/components/TiptapEditor.scss', + reason: 'Markdown editor highlight border color should use the shared markdown-editor namespace instead of the abbreviated meditor local key.', }, ]; @@ -720,13 +299,18 @@ export const DYNAMIC_VAR_FAMILY_CONTRACTS = [ }, { prefix: '--color-accent-', - owner: 'src/web-ui/src/infrastructure/theme/core/ThemeService.ts', - reason: 'Theme runtime exports the active accent palette scale by numeric stop.', + owner: 'src/web-ui/src/infrastructure/theme/core/ThemeService.ts; src/mobile-web/src/theme/presets; BitFun-Installer/src/theme', + reason: 'Theme runtime, mobile presets, and installer theme data export the active accent palette scale by numeric stop.', }, { prefix: '--color-purple-', - owner: 'src/web-ui/src/infrastructure/theme/core/ThemeService.ts', - reason: 'Theme runtime exports the secondary purple palette scale by numeric stop.', + owner: 'src/web-ui/src/infrastructure/theme/core/ThemeService.ts; src/mobile-web/src/theme/presets', + reason: 'Theme runtime and mobile presets export the secondary purple palette scale by numeric stop; installer keeps only the accent family it renders.', + }, + { + prefix: '--color-pink-', + owner: 'src/mobile-web/src/theme/presets', + reason: 'Mobile presets export assistant-mode identity accents by numeric stop for session and picker states.', }, { prefix: '--easing-', @@ -763,12 +347,6 @@ export const DYNAMIC_VAR_FAMILY_CONTRACTS = [ owner: 'src/web-ui/src/infrastructure/font-preference/core/FontPreferenceService.ts', reason: 'Font preference runtime exports navigation font-size aliases from the adjusted typography scale.', }, - { - prefix: '--radius-', - owner: 'src/web-ui/src/infrastructure/theme/core/ThemeService.ts', - canonicalPrefix: '--size-radius-', - reason: 'Theme runtime exports configurable radius entries.', - }, { prefix: '--shadow-', owner: 'src/web-ui/src/infrastructure/theme/core/ThemeService.ts', @@ -784,12 +362,6 @@ export const DYNAMIC_VAR_FAMILY_CONTRACTS = [ owner: 'src/web-ui/src/infrastructure/theme/core/ThemeService.ts', reason: 'Size radius aliases are derived from theme radius entries.', }, - { - prefix: '--spacing-', - owner: 'src/web-ui/src/infrastructure/theme/core/ThemeService.ts', - canonicalPrefix: '--size-gap-', - reason: 'Theme runtime exports configurable spacing entries.', - }, ]; export const REGISTERED_DYNAMIC_VAR_PREFIXES = new Set( diff --git a/scripts/theme-visual-governance-contract.json b/scripts/theme-visual-governance-contract.json index 46f46f7bb0..cf507550d2 100644 --- a/scripts/theme-visual-governance-contract.json +++ b/scripts/theme-visual-governance-contract.json @@ -22,7 +22,7 @@ } ], "risks": [ - "Short legacy aliases such as --color-primary and --text-secondary are still used by shell-adjacent components.", + "Legacy aliases remain exported for compatibility but must not reappear as internal component reads.", "System theme resolution must not assume desktop-only media query behavior." ] }, @@ -33,7 +33,7 @@ "formFactors": ["desktop", "narrow"], "themes": ["dark", "light", "system"], "states": ["default", "streaming", "hover", "focus", "selected", "error", "empty"], - "tokenFamilies": ["--flowchat-*", "--tool-card-*", "--color-bg-flowchat", "--color-text-*", "--accent-primary"], + "tokenFamilies": ["--flowchat-*", "--tool-card-*", "--color-bg-scene", "--color-text-*", "--color-accent-*"], "evidence": [ { "type": "theme-color-audit", @@ -47,7 +47,7 @@ ], "risks": [ "Streaming and virtualized items can hide token regressions until historical turns are rendered.", - "Several high-use compatibility aliases are still visible in Flow Chat surfaces." + "Flow Chat has many historical alias call sites; the audit baseline must keep internal alias reads at zero." ] }, { @@ -57,7 +57,7 @@ "formFactors": ["desktop", "narrow"], "themes": ["dark", "light", "system"], "states": ["default", "expanded", "collapsed", "hover", "focus", "success", "warning", "error"], - "tokenFamilies": ["--tool-card-*", "--color-success*", "--color-warning*", "--color-error*", "--color-danger*"], + "tokenFamilies": ["--tool-card-*", "--color-success*", "--color-warning*", "--color-error*"], "evidence": [ { "type": "theme-color-audit", @@ -129,7 +129,7 @@ "formFactors": ["desktop", "narrow"], "themes": ["dark", "light", "system"], "states": ["default", "code", "table", "link", "diagram", "error"], - "tokenFamilies": ["--markdown-*", "--primary-color", "Mermaid palette", "Prism palette"], + "tokenFamilies": ["--markdown-*", "--markdown-primary-color", "Mermaid palette", "Prism palette"], "evidence": [ { "type": "theme-color-audit", @@ -142,7 +142,7 @@ } ], "risks": [ - "--primary-color is an embedded-content override and must not be removed as a normal fallback.", + "--markdown-primary-color is the embedded-content accent override and must remain distinct from generic app primary aliases.", "Mermaid graph roles do not map directly to app status colors." ] }, @@ -177,7 +177,7 @@ "formFactors": ["desktop", "narrow"], "themes": ["dark", "light", "system"], "states": ["default", "selected", "hover", "focus", "custom-theme", "system-theme"], - "tokenFamilies": ["--color-accent-*", "--color-primary*", "--color-bg-*", "--color-text-*"], + "tokenFamilies": ["--color-accent-*", "--color-bg-*", "--color-text-*"], "evidence": [ { "type": "theme-color-audit", @@ -213,6 +213,30 @@ "Mobile web is a separate build target and may not exercise desktop-only theme runtime paths.", "Narrow form factor can surface contrast and spacing regressions that desktop screenshots miss." ] + }, + { + "key": "installer-shell", + "owner": "BitFun-Installer/src/theme; BitFun-Installer/src/pages; BitFun-Installer/src/styles", + "platforms": ["installer"], + "formFactors": ["desktop", "narrow"], + "themes": ["dark", "light", "system"], + "states": ["theme-setup", "language", "options", "loading", "progress", "error", "completed", "hover", "focus"], + "tokenFamilies": ["--color-bg-*", "--color-text-*", "--element-bg-*", "--border-*", "--color-accent-*", "--color-success", "--color-warning", "--color-error"], + "evidence": [ + { + "type": "theme-color-audit", + "command": "pnpm run theme:color-audit:installer", + "requirement": "Installer theme changes must keep app UI raw colors, unresolved vars, fallback vars, and unregistered dynamic families at zero." + }, + { + "type": "focused-visual-review", + "requirement": "Review ThemeSetup cards plus language, options, progress, completed, and error states across light, dark, system, and named themes when installer tokens change." + } + ], + "risks": [ + "Installer is a separate React/Tauri surface with its own TS theme root; Rust must not copy or extend the installer palette.", + "Theme cards use adjacent primary and secondary backgrounds for identity, so near colors cannot be merged without focused visual evidence." + ] } ] } diff --git a/scripts/validate-theme-visual-contract.mjs b/scripts/validate-theme-visual-contract.mjs index aeffa0c9df..1b28fe14c4 100644 --- a/scripts/validate-theme-visual-contract.mjs +++ b/scripts/validate-theme-visual-contract.mjs @@ -11,8 +11,9 @@ const REQUIRED_SURFACE_KEYS = [ 'generated-widget', 'theme-settings', 'mobile-web-shell', + 'installer-shell', ]; -const ALLOWED_PLATFORMS = new Set(['desktop-webview', 'web', 'mobile-web', 'generated-widget']); +const ALLOWED_PLATFORMS = new Set(['desktop-webview', 'web', 'mobile-web', 'generated-widget', 'installer']); const ALLOWED_FORM_FACTORS = new Set(['desktop', 'narrow', 'mobile', 'iframe']); const ALLOWED_THEMES = new Set(['dark', 'light', 'system']); const ALLOWED_EVIDENCE_TYPES = new Set([ diff --git a/src/apps/cli/src/agent/agentic_system.rs b/src/apps/cli/src/agent/agentic_system.rs index 76b7ca8524..d4be14d334 100644 --- a/src/apps/cli/src/agent/agentic_system.rs +++ b/src/apps/cli/src/agent/agentic_system.rs @@ -1,9 +1,23 @@ use anyhow::{Context, Result}; use bitfun_core::infrastructure::ai::AIClientFactory; +use bitfun_core::product_runtime::CoreRuntimeServicesProvider; use bitfun_core::service::config::initialize_global_config; -pub use bitfun_core::agentic::system::{init_agentic_system, AgenticSystem}; +pub use bitfun_core::agentic::system::AgenticSystem; + +pub async fn init_agentic_system() -> Result { + let system = bitfun_core::agentic::system::init_agentic_system() + .await + .context("Failed to initialize agentic system")?; + system + .coordinator + .set_terminal_port(CoreRuntimeServicesProvider::terminal_port()); + system + .coordinator + .set_remote_exec_port(CoreRuntimeServicesProvider::remote_exec_port()); + Ok(system) +} pub async fn init_agentic_system_for_cli() -> Result { initialize_global_config() @@ -12,7 +26,5 @@ pub async fn init_agentic_system_for_cli() -> Result { AIClientFactory::initialize_global() .await .context("Failed to initialize global AIClientFactory")?; - init_agentic_system() - .await - .context("Failed to initialize agentic system") + init_agentic_system().await } diff --git a/src/apps/cli/src/agent/core_adapter.rs b/src/apps/cli/src/agent/core_adapter.rs index d5f5626540..ab102df1eb 100644 --- a/src/apps/cli/src/agent/core_adapter.rs +++ b/src/apps/cli/src/agent/core_adapter.rs @@ -230,6 +230,8 @@ impl Agent for CoreAgentAdapter { Some(turn_id.clone()), agent_type.to_string(), Some(self.workspace_path_string()), + None, + None, DialogSubmissionPolicy::for_source(DialogTriggerSource::Cli), None, ) @@ -252,6 +254,8 @@ impl Agent for CoreAgentAdapter { Some(turn_id.clone()), agent_type.to_string(), Some(self.workspace_path_string()), + None, + None, DialogSubmissionPolicy::for_source(DialogTriggerSource::Cli), None, ) diff --git a/src/apps/cli/src/chat_state.rs b/src/apps/cli/src/chat_state.rs index 4a154a3697..0febcc6599 100644 --- a/src/apps/cli/src/chat_state.rs +++ b/src/apps/cli/src/chat_state.rs @@ -592,6 +592,7 @@ impl ChatState { tool_id, tool_name, params, + .. } => { self.update_tool(tool_id, |tool| { tool.status = ToolDisplayStatus::ConfirmationNeeded; diff --git a/src/apps/cli/src/logging.rs b/src/apps/cli/src/logging.rs index bd31f78b18..22fc1136f4 100644 --- a/src/apps/cli/src/logging.rs +++ b/src/apps/cli/src/logging.rs @@ -281,11 +281,17 @@ fn target_override_rank(target: &str) -> Option { || matches_target_rule(target, "tracing") || matches_target_rule(target, "opentelemetry_sdk") || matches_target_rule(target, "opentelemetry-otlp") - || matches_target_rule(target, "notify") { return Some(0); } + if matches_target_rule(target, "html5ever") + || matches_target_rule(target, "selectors") + || matches_target_rule(target, "notify") + { + return Some(level_rank(tracing::Level::WARN)); + } + if matches_target_rule(target, "bitfun_core::agentic::events::queue") || matches_target_rule(target, "bitfun_core::agentic::events::router") || matches_target_rule(target, "bitfun_agent_runtime::event_queue") @@ -435,9 +441,17 @@ mod tests { allowed_level_rank_for_target("grep_searcher", tracing::Level::TRACE), level_rank(tracing::Level::WARN) ); + assert_eq!( + allowed_level_rank_for_target("html5ever::tokenizer::char_ref", tracing::Level::TRACE,), + level_rank(tracing::Level::WARN) + ); + assert_eq!( + allowed_level_rank_for_target("selectors::matching", tracing::Level::TRACE), + level_rank(tracing::Level::WARN) + ); assert_eq!( allowed_level_rank_for_target("notify", tracing::Level::TRACE), - 0 + level_rank(tracing::Level::WARN) ); assert_eq!( allowed_level_rank_for_target( diff --git a/src/apps/cli/src/main.rs b/src/apps/cli/src/main.rs index 2c351d2112..70b8817c88 100644 --- a/src/apps/cli/src/main.rs +++ b/src/apps/cli/src/main.rs @@ -768,4 +768,3 @@ fn main() { } } } - diff --git a/src/apps/cli/src/management.rs b/src/apps/cli/src/management.rs index 24495e6d97..0dccd1e316 100644 --- a/src/apps/cli/src/management.rs +++ b/src/apps/cli/src/management.rs @@ -10,10 +10,8 @@ use bitfun_core::service::session_usage::{ generate_session_usage_report, render_usage_report_markdown, SessionUsageReportRequest, }; - -async fn ensure_global_config_service() -> Result< - std::sync::Arc, -> { +async fn ensure_global_config_service( +) -> Result> { initialize_global_config() .await .context("Failed to initialize global config service")?; @@ -144,9 +142,10 @@ pub async fn print_mcp_servers() -> Result<()> { .as_ref() .map(|cmd| format!("{} {}", cmd, config.args.join(" "))) .unwrap_or_else(|| "".to_string()), - bitfun_core::service::mcp::server::MCPServerType::Remote => { - config.url.clone().unwrap_or_else(|| "".to_string()) - } + bitfun_core::service::mcp::server::MCPServerType::Remote => config + .url + .clone() + .unwrap_or_else(|| "".to_string()), }; println!("- {} ({:?})", config.id, config.server_type); @@ -186,7 +185,10 @@ pub async fn set_mcp_server_enabled(server_id: &str, enabled: bool) -> Result<() .await? .ok_or_else(|| anyhow!("MCP server not found: {}", server_id))?; config.enabled = enabled; - mcp_service.config_service().save_server_config(&config).await?; + mcp_service + .config_service() + .save_server_config(&config) + .await?; println!( "MCP server {} {}.", @@ -200,10 +202,7 @@ pub async fn print_mcp_json_config() -> Result<()> { let config_service = ensure_global_config_service().await?; let mcp_service = bitfun_core::service::mcp::MCPService::new(config_service.clone()) .map_err(|error| anyhow!(error.to_string()))?; - let json = mcp_service - .config_service() - .load_mcp_json_config() - .await?; + let json = mcp_service.config_service().load_mcp_json_config().await?; println!("{}", json); Ok(()) } @@ -211,8 +210,8 @@ pub async fn print_mcp_json_config() -> Result<()> { pub async fn print_usage_report(session_id: Option<&str>) -> Result<()> { let agentic_system = crate::agent::agentic_system::init_agentic_system_for_cli().await?; let path_manager = try_get_path_manager_arc().map_err(|error| anyhow!(error.to_string()))?; - let persistence_manager = PersistenceManager::new(path_manager) - .map_err(|error| anyhow!(error.to_string()))?; + let persistence_manager = + PersistenceManager::new(path_manager).map_err(|error| anyhow!(error.to_string()))?; let workspace_path = std::env::current_dir().context("Failed to resolve current directory")?; let coordinator = agentic_system.coordinator.clone(); let resolved_session_id = match session_id { @@ -250,7 +249,9 @@ pub async fn print_doctor() -> Result { let models = config_service.get_ai_models().await?; let agent_registry = get_agent_registry(); let modes = agent_registry.get_modes_info().await; - let subagents = agent_registry.get_subagents_info(Some(workspace.as_path())).await; + let subagents = agent_registry + .get_subagents_info(Some(workspace.as_path())) + .await; let mcp_service = bitfun_core::service::mcp::MCPService::new(config_service.clone()) .map_err(|error| anyhow!(error.to_string()))?; let mcp_configs = mcp_service.config_service().load_all_configs().await?; @@ -261,7 +262,11 @@ pub async fn print_doctor() -> Result { println!("[ok] Config directory: {}", config_dir.display()); println!("[ok] Agent modes: {}", modes.len()); println!("[ok] Subagents: {}", subagents.len()); - println!("[ok] AI models: {} total, {} enabled", models.len(), models.iter().filter(|m| m.enabled).count()); + println!( + "[ok] AI models: {} total, {} enabled", + models.len(), + models.iter().filter(|m| m.enabled).count() + ); println!("[ok] MCP servers: {}", mcp_configs.len()); println!(); println!("Doctor checks passed."); diff --git a/src/apps/cli/src/modes/chat.rs b/src/apps/cli/src/modes/chat.rs index 3552d96316..66dfdfbbc8 100644 --- a/src/apps/cli/src/modes/chat.rs +++ b/src/apps/cli/src/modes/chat.rs @@ -2914,14 +2914,8 @@ impl ChatMode { }); let count = outcome.len(); - chat_state.add_system_message(format!( - "Reloaded {} skill(s) from disk.", - count - )); - chat_view.set_status(Some(format!( - "Skills reloaded ({} available)", - count - ))); + chat_state.add_system_message(format!("Reloaded {} skill(s) from disk.", count)); + chat_view.set_status(Some(format!("Skills reloaded ({} available)", count))); } fn show_available_skill_list( diff --git a/src/apps/cli/src/modes/exec.rs b/src/apps/cli/src/modes/exec.rs index 34b68f5634..98909c454f 100644 --- a/src/apps/cli/src/modes/exec.rs +++ b/src/apps/cli/src/modes/exec.rs @@ -154,7 +154,9 @@ impl ExecMode { if parent_session_id.map(String::as_str) == Some(session_id.as_str()) { use bitfun_events::ToolEventData; match tool_event { - ToolEventData::Started { tool_name, tool_id, .. } => { + ToolEventData::Started { + tool_name, tool_id, .. + } => { self.emit(json!({ "type": "subagent_tool_start", "session_id": session_id, @@ -184,11 +186,17 @@ impl ExecMode { "summary": summary, }))?; self.print_text(|| { - println!(" [subagent] {} completed: {}", tool_name, summary) + println!( + " [subagent] {} completed: {}", + tool_name, summary + ) }); } ToolEventData::Failed { - tool_name, tool_id, error, .. + tool_name, + tool_id, + error, + .. } => { self.emit(json!({ "type": "subagent_tool_error", @@ -197,7 +205,9 @@ impl ExecMode { "tool_name": tool_name, "error": error, }))?; - self.print_text(|| println!(" [subagent] {} failed: {}", tool_name, error)); + self.print_text(|| { + println!(" [subagent] {} failed: {}", tool_name, error) + }); } _ => {} } @@ -289,7 +299,10 @@ impl ExecMode { "summary": summary, }))?; self.print_text(|| { - println!(" [+] {} ({}ms): {}", tool_name, duration_ms, summary) + println!( + " [+] {} ({}ms): {}", + tool_name, duration_ms, summary + ) }); } ToolEventData::Failed { @@ -322,7 +335,10 @@ impl ExecMode { println!("\n"); println!("Execution complete"); if total_tool_calls > 0 { - println!("\nTool call statistics: {} tools invoked", total_tool_calls); + println!( + "\nTool call statistics: {} tools invoked", + total_tool_calls + ); } }); self.output_patch_if_needed(); @@ -407,10 +423,9 @@ impl ExecMode { .ok_or_else(|| anyhow::anyhow!("Session has no persisted turns to fork"))?; let path_manager = bitfun_core::infrastructure::try_get_path_manager_arc() .map_err(|error| anyhow::anyhow!(error.to_string()))?; - let persistence_manager = bitfun_core::agentic::persistence::PersistenceManager::new( - path_manager, - ) - .map_err(|error| anyhow::anyhow!(error.to_string()))?; + let persistence_manager = + bitfun_core::agentic::persistence::PersistenceManager::new(path_manager) + .map_err(|error| anyhow::anyhow!(error.to_string()))?; let result = persistence_manager .branch_session( &workspace, diff --git a/src/apps/cli/src/ui/theme.rs b/src/apps/cli/src/ui/theme.rs index 2973f25356..e9804323d6 100644 --- a/src/apps/cli/src/ui/theme.rs +++ b/src/apps/cli/src/ui/theme.rs @@ -263,36 +263,7 @@ fn parse_osc_color(s: &str) -> Option<(u8, u8, u8)> { impl Theme { pub fn dark() -> Self { - Self { - primary: Color::Rgb(59, 130, 246), // blue - success: Color::Rgb(34, 197, 94), // green - warning: Color::Rgb(251, 191, 36), // yellow - error: Color::Rgb(239, 68, 68), // red - info: Color::Rgb(147, 197, 253), // light blue - muted: Color::Rgb(156, 163, 175), // gray - background: Color::Rgb(17, 24, 39), // dark gray background - border: Color::Rgb(55, 65, 81), // border gray - - background_panel: Color::Rgb(30, 38, 55), - background_element: Color::Rgb(40, 50, 70), - input_background: Color::Rgb(40, 50, 70), - - diff_added_fg: Color::Rgb(34, 197, 94), - diff_removed_fg: Color::Rgb(239, 68, 68), - diff_added_bg: Color::Rgb(20, 50, 20), - diff_removed_bg: Color::Rgb(50, 20, 20), - - block_bg: Color::Rgb(24, 32, 48), - block_bg_hover: Color::Rgb(32, 42, 62), - block_border_active: Color::Rgb(59, 130, 246), - - inline_icon: Color::Rgb(100, 140, 220), - - command_text: Color::Rgb(180, 210, 255), - - diff_hunk_header: Color::Rgb(100, 120, 180), - diff_line_number: Color::Rgb(80, 90, 110), - } + Self::from_builtin_preset("bitfun-dark", Appearance::Dark) } pub fn dark_ansi16() -> Self { @@ -329,36 +300,7 @@ impl Theme { } pub fn light() -> Self { - Self { - primary: Color::Rgb(37, 99, 235), - success: Color::Rgb(22, 163, 74), - warning: Color::Rgb(245, 158, 11), - error: Color::Rgb(220, 38, 38), - info: Color::Rgb(59, 130, 246), - muted: Color::Rgb(107, 114, 128), - background: Color::Rgb(249, 250, 251), - border: Color::Rgb(209, 213, 219), - - background_panel: Color::Rgb(243, 244, 246), - background_element: Color::Rgb(229, 231, 235), - input_background: Color::Rgb(229, 231, 235), - - diff_added_fg: Color::Rgb(22, 163, 74), - diff_removed_fg: Color::Rgb(220, 38, 38), - diff_added_bg: Color::Rgb(220, 252, 231), - diff_removed_bg: Color::Rgb(254, 226, 226), - - block_bg: Color::Rgb(240, 242, 245), - block_bg_hover: Color::Rgb(232, 235, 240), - block_border_active: Color::Rgb(37, 99, 235), - - inline_icon: Color::Rgb(60, 100, 200), - - command_text: Color::Rgb(30, 60, 120), - - diff_hunk_header: Color::Rgb(80, 100, 160), - diff_line_number: Color::Rgb(140, 150, 170), - } + Self::from_builtin_preset("bitfun-light", Appearance::Light) } pub fn light_ansi16() -> Self { @@ -467,44 +409,7 @@ impl Theme { ) -> anyhow::Result { let fallback = self.clone(); let resolved = resolve_opencode_theme(json, appearance)?; - Ok(Theme { - primary: resolved.primary.unwrap_or(fallback.primary), - success: resolved.success.unwrap_or(fallback.success), - warning: resolved.warning.unwrap_or(fallback.warning), - error: resolved.error.unwrap_or(fallback.error), - info: resolved.info.unwrap_or(fallback.info), - muted: resolved.muted.unwrap_or(fallback.muted), - background: resolved.background.unwrap_or(fallback.background), - border: resolved.border.unwrap_or(fallback.border), - background_panel: resolved - .background_panel - .unwrap_or(fallback.background_panel), - background_element: resolved - .background_element - .unwrap_or(fallback.background_element), - input_background: resolved.input_background.unwrap_or( - resolved - .background_element - .unwrap_or(fallback.input_background), - ), - diff_added_fg: resolved.diff_added_fg.unwrap_or(fallback.diff_added_fg), - diff_removed_fg: resolved.diff_removed_fg.unwrap_or(fallback.diff_removed_fg), - diff_added_bg: resolved.diff_added_bg.unwrap_or(fallback.diff_added_bg), - diff_removed_bg: resolved.diff_removed_bg.unwrap_or(fallback.diff_removed_bg), - block_bg: resolved.block_bg.unwrap_or(fallback.block_bg), - block_bg_hover: resolved.block_bg_hover.unwrap_or(fallback.block_bg_hover), - block_border_active: resolved - .block_border_active - .unwrap_or(fallback.block_border_active), - inline_icon: resolved.inline_icon.unwrap_or(fallback.inline_icon), - command_text: resolved.command_text.unwrap_or(fallback.command_text), - diff_hunk_header: resolved - .diff_hunk_header - .unwrap_or(fallback.diff_hunk_header), - diff_line_number: resolved - .diff_line_number - .unwrap_or(fallback.diff_line_number), - }) + build_theme_from_resolved_tokens(resolved, Some(&fallback)) } pub fn style(&self, kind: StyleKind) -> Style { @@ -540,6 +445,68 @@ impl Theme { pub fn selection_foreground(&self) -> Color { readable_foreground_for(self.primary) } + + fn from_builtin_preset(id: &'static str, appearance: Appearance) -> Self { + let json = builtin_theme_json(id) + .unwrap_or_else(|| panic!("Built-in CLI theme {id} must be registered")); + Self::from_complete_opencode_theme_json(json, appearance) + .unwrap_or_else(|err| panic!("Built-in CLI theme {id} must be complete: {err}")) + } + + fn from_complete_opencode_theme_json( + json: &OpencodeThemeJson, + appearance: Appearance, + ) -> anyhow::Result { + let resolved = resolve_opencode_theme(json, appearance)?; + build_theme_from_resolved_tokens(resolved, None) + } +} + +fn build_theme_from_resolved_tokens( + resolved: ResolvedTokens, + fallback: Option<&Theme>, +) -> anyhow::Result { + fn pick(name: &str, value: Option, fallback: Option) -> anyhow::Result { + value + .or(fallback) + .ok_or_else(|| anyhow::anyhow!("CLI theme missing required color \"{}\"", name)) + } + + let fallback_color = |select: fn(&Theme) -> Color| fallback.map(select); + macro_rules! pick_field { + ($field:ident, $name:literal) => { + pick($name, resolved.$field, fallback_color(|theme| theme.$field))? + }; + } + + Ok(Theme { + primary: pick_field!(primary, "primary"), + success: pick_field!(success, "success"), + warning: pick_field!(warning, "warning"), + error: pick_field!(error, "error"), + info: pick_field!(info, "info"), + muted: pick_field!(muted, "textMuted"), + background: pick_field!(background, "background"), + border: pick_field!(border, "border"), + background_panel: pick_field!(background_panel, "backgroundPanel"), + background_element: pick_field!(background_element, "backgroundElement"), + input_background: pick( + "inputBackground", + resolved.input_background.or(resolved.background_element), + fallback_color(|theme| theme.input_background), + )?, + diff_added_fg: pick_field!(diff_added_fg, "diffAdded"), + diff_removed_fg: pick_field!(diff_removed_fg, "diffRemoved"), + diff_added_bg: pick_field!(diff_added_bg, "diffAddedBg"), + diff_removed_bg: pick_field!(diff_removed_bg, "diffRemovedBg"), + block_bg: pick_field!(block_bg, "backgroundPanel"), + block_bg_hover: pick_field!(block_bg_hover, "backgroundElement"), + block_border_active: pick_field!(block_border_active, "borderActive"), + inline_icon: pick_field!(inline_icon, "accent"), + command_text: pick_field!(command_text, "primary"), + diff_hunk_header: pick_field!(diff_hunk_header, "diffHunkHeader"), + diff_line_number: pick_field!(diff_line_number, "diffLineNumber"), + }) } fn readable_foreground_for(background: Color) -> Color { @@ -886,7 +853,7 @@ fn resolve_opencode_theme( tokens.block_border_active = resolve_key(json, &defs, "borderActive", mode)?.or(tokens.primary.clone()); tokens.inline_icon = resolve_key(json, &defs, "accent", mode)?.or(tokens.primary.clone()); - tokens.command_text = resolve_key(json, &defs, "accent", mode)?.or(tokens.info.clone()); + tokens.command_text = tokens.primary.clone(); tokens.diff_hunk_header = resolve_key(json, &defs, "diffHunkHeader", mode)?; tokens.diff_line_number = resolve_key(json, &defs, "diffLineNumber", mode)?; @@ -997,6 +964,125 @@ mod tests { } } + #[test] + fn truecolor_defaults_are_backed_by_builtin_presets() { + let dark = Theme::dark(); + let light = Theme::light(); + + assert_eq!(dark.primary, Color::Rgb(96, 165, 250)); + assert_eq!(dark.command_text, dark.primary); + assert_eq!(dark.background, Color::Rgb(14, 14, 16)); + assert_eq!(dark.background_panel, Color::Rgb(28, 28, 31)); + assert_eq!(dark.diff_line_number, Color::Rgb(133, 133, 133)); + assert_eq!(light.primary, Color::Rgb(71, 85, 105)); + assert_eq!(light.command_text, light.primary); + assert_eq!(light.background, Color::Rgb(243, 243, 245)); + assert_eq!(light.background_panel, Color::Rgb(255, 255, 255)); + assert_eq!(light.warning, Color::Rgb(154, 101, 31)); + assert_eq!(light.diff_added_fg, Color::Rgb(63, 125, 84)); + assert_eq!(light.diff_removed_fg, Color::Rgb(159, 68, 68)); + assert_eq!(light.diff_line_number, Color::Rgb(100, 116, 139)); + } + + #[test] + fn truecolor_defaults_keep_cli_surface_contrast() { + let dark = Theme::dark(); + let light = Theme::light(); + + for (label, fg, bg, min_ratio) in [ + ("dark primary", dark.primary, dark.background, 4.5), + ("dark success", dark.success, dark.background, 4.5), + ("dark warning", dark.warning, dark.background, 4.5), + ("dark error", dark.error, dark.background, 4.5), + ("dark muted", dark.muted, dark.background, 4.5), + ("dark command", dark.command_text, dark.background, 4.5), + ("dark command block", dark.command_text, dark.block_bg, 4.5), + ( + "dark command block hover", + dark.command_text, + dark.block_bg_hover, + 4.5, + ), + ( + "dark diff line number", + dark.diff_line_number, + dark.background_panel, + 3.0, + ), + ( + "dark diff added", + dark.diff_added_fg, + dark.diff_added_bg, + 3.0, + ), + ( + "dark diff removed", + dark.diff_removed_fg, + dark.diff_removed_bg, + 3.0, + ), + ("light primary", light.primary, light.background, 4.5), + ("light warning", light.warning, light.background, 4.0), + ("light error", light.error, light.background, 3.0), + ("light muted", light.muted, light.background, 4.0), + ("light command", light.command_text, light.background, 4.0), + ( + "light command block", + light.command_text, + light.block_bg, + 4.5, + ), + ( + "light command block hover", + light.command_text, + light.block_bg_hover, + 4.5, + ), + ( + "light diff line number", + light.diff_line_number, + light.background_panel, + 4.5, + ), + ( + "light diff added", + light.diff_added_fg, + light.diff_added_bg, + 4.0, + ), + ( + "light diff removed", + light.diff_removed_fg, + light.diff_removed_bg, + 4.0, + ), + ] { + assert_contrast_at_least(label, fg, bg, min_ratio); + } + } + + #[test] + fn partial_opencode_theme_json_keeps_base_theme_fallbacks() { + let json = serde_json::from_str::( + r##"{ + "theme": { + "primary": "#14b8a6" + } + }"##, + ) + .unwrap(); + let base = Theme::dark(); + let resolved = base + .apply_opencode_theme_json(&json, Appearance::Dark) + .unwrap(); + + assert_eq!(resolved.primary, Color::Rgb(20, 184, 166)); + assert_eq!(resolved.command_text, resolved.primary); + assert_eq!(resolved.success, base.success); + assert_eq!(resolved.background, base.background); + assert_eq!(resolved.input_background, base.input_background); + } + #[test] fn eight_digit_hex_colors_are_supported() { let json = serde_json::from_str::( @@ -1018,4 +1104,37 @@ mod tests { assert_eq!(dark.primary, Color::Rgb(128, 128, 128)); assert_eq!(light.primary, Color::Rgb(127, 127, 127)); } + + fn assert_contrast_at_least(label: &str, fg: Color, bg: Color, min_ratio: f64) { + let ratio = contrast_ratio(fg, bg); + assert!( + ratio >= min_ratio, + "{label} contrast {ratio:.2} is below {min_ratio:.2}" + ); + } + + fn contrast_ratio(fg: Color, bg: Color) -> f64 { + let fg = relative_luminance(fg); + let bg = relative_luminance(bg); + let lighter = fg.max(bg); + let darker = fg.min(bg); + (lighter + 0.05) / (darker + 0.05) + } + + fn relative_luminance(color: Color) -> f64 { + let (r, g, b) = match color { + Color::Rgb(r, g, b) => (r, g, b), + other => panic!("contrast test requires RGB color, got {other:?}"), + }; + 0.2126 * linear_rgb(r) + 0.7152 * linear_rgb(g) + 0.0722 * linear_rgb(b) + } + + fn linear_rgb(channel: u8) -> f64 { + let channel = f64::from(channel) / 255.0; + if channel <= 0.04045 { + channel / 12.92 + } else { + ((channel + 0.055) / 1.055).powf(2.4) + } + } } diff --git a/src/apps/cli/themes/presets/bitfun-cyber.json b/src/apps/cli/themes/presets/bitfun-cyber.json index ee7169442b..2ea774592c 100644 --- a/src/apps/cli/themes/presets/bitfun-cyber.json +++ b/src/apps/cli/themes/presets/bitfun-cyber.json @@ -10,13 +10,13 @@ "info": "#00e6ff", "text": "#e0f2ff", "textMuted": "#7fadcc", - "background": "#101010", + "background": "#0e0e10", "backgroundPanel": "#151515", "backgroundElement": "#082a30", "inputBackground": "#003f46", "border": "#16404a", "borderActive": "#00e6ff", - "borderSubtle": "#102b32", + "borderSubtle": "#16404a", "diffAdded": "#00ff9f", "diffRemoved": "#ff0055", "diffContext": "#7fadcc", diff --git a/src/apps/cli/themes/presets/bitfun-dark.json b/src/apps/cli/themes/presets/bitfun-dark.json index ec86dd9d0e..5e252978cf 100644 --- a/src/apps/cli/themes/presets/bitfun-dark.json +++ b/src/apps/cli/themes/presets/bitfun-dark.json @@ -8,7 +8,7 @@ "warning": "#f59e0b", "success": "#34d399", "info": "#a1a1aa", - "text": "#e8e8e8", + "text": "#e7ecf3", "textMuted": "#858585", "background": "#0e0e10", "backgroundPanel": "#1c1c1f", @@ -24,33 +24,33 @@ "diffHighlightAdded": "#6ee7b7", "diffHighlightRemoved": "#f87171", "diffAddedBg": "#12352d", - "diffRemovedBg": "#3a1f24", + "diffRemovedBg": "#3e221c", "diffContextBg": "#1c1c1f", - "diffLineNumber": "#66666d", - "diffAddedLineNumberBg": "#163c32", + "diffLineNumber": "#858585", + "diffAddedLineNumberBg": "#1b3328", "diffRemovedLineNumberBg": "#432329", - "markdownText": "#e8e8e8", + "markdownText": "#e7ecf3", "markdownHeading": "#60a5fa", "markdownLink": "#60a5fa", "markdownLinkText": "#8b5cf6", "markdownCode": "#34d399", "markdownBlockQuote": "#858585", "markdownEmph": "#f59e0b", - "markdownStrong": "#e8e8e8", + "markdownStrong": "#e7ecf3", "markdownHorizontalRule": "#3b3b40", "markdownListItem": "#60a5fa", "markdownListEnumeration": "#8b5cf6", "markdownImage": "#60a5fa", "markdownImageText": "#8b5cf6", - "markdownCodeBlock": "#e8e8e8", + "markdownCodeBlock": "#e7ecf3", "syntaxComment": "#6a737d", "syntaxKeyword": "#c792ea", "syntaxFunction": "#7dcfff", - "syntaxVariable": "#80d4ff", + "syntaxVariable": "#7dcfff", "syntaxString": "#a5e844", "syntaxNumber": "#f78c6c", "syntaxType": "#ffc777", "syntaxOperator": "#c792ea", - "syntaxPunctuation": "#e0e6f0" + "syntaxPunctuation": "#e7ecf3" } } diff --git a/src/apps/cli/themes/presets/bitfun-ink-night.json b/src/apps/cli/themes/presets/bitfun-ink-night.json index 607e76f922..e63feb303c 100644 --- a/src/apps/cli/themes/presets/bitfun-ink-night.json +++ b/src/apps/cli/themes/presets/bitfun-ink-night.json @@ -12,7 +12,7 @@ "textMuted": "#a79b83", "background": "#17110c", "backgroundPanel": "#241b12", - "backgroundElement": "#3a2817", + "backgroundElement": "#4a2d15", "inputBackground": "#4a2d15", "border": "#5a472b", "borderActive": "#d7b56d", @@ -23,7 +23,7 @@ "diffHunkHeader": "#d7b56d", "diffHighlightAdded": "#93bfa9", "diffHighlightRemoved": "#e07368", - "diffAddedBg": "#24362a", + "diffAddedBg": "#1b3328", "diffRemovedBg": "#3e221c", "diffContextBg": "#241b12", "diffLineNumber": "#8e8065", diff --git a/src/apps/cli/themes/presets/bitfun-light.json b/src/apps/cli/themes/presets/bitfun-light.json index 47887859f3..088605a888 100644 --- a/src/apps/cli/themes/presets/bitfun-light.json +++ b/src/apps/cli/themes/presets/bitfun-light.json @@ -5,7 +5,7 @@ "secondary": "#7c6b99", "accent": "#64748b", "error": "#c26565", - "warning": "#c08c42", + "warning": "#9a651f", "success": "#5b9a6f", "info": "#64748b", "text": "#1e293b", @@ -17,8 +17,8 @@ "border": "#cbd5e1", "borderActive": "#475569", "borderSubtle": "#d9dee7", - "diffAdded": "#4f8f64", - "diffRemoved": "#b85454", + "diffAdded": "#3f7d54", + "diffRemoved": "#9f4444", "diffContext": "#64748b", "diffHunkHeader": "#475569", "diffHighlightAdded": "#3f7d54", @@ -26,7 +26,7 @@ "diffAddedBg": "#e4f0e7", "diffRemovedBg": "#f3dfdf", "diffContextBg": "#ffffff", - "diffLineNumber": "#94a3b8", + "diffLineNumber": "#64748b", "diffAddedLineNumberBg": "#d8eadc", "diffRemovedLineNumberBg": "#ebd2d2", "markdownText": "#1e293b", @@ -35,7 +35,7 @@ "markdownLinkText": "#7c6b99", "markdownCode": "#5b9a6f", "markdownBlockQuote": "#64748b", - "markdownEmph": "#c08c42", + "markdownEmph": "#9a651f", "markdownStrong": "#1e293b", "markdownHorizontalRule": "#cbd5e1", "markdownListItem": "#475569", @@ -48,7 +48,7 @@ "syntaxFunction": "#475569", "syntaxVariable": "#1e293b", "syntaxString": "#5b9a6f", - "syntaxNumber": "#c08c42", + "syntaxNumber": "#9a651f", "syntaxType": "#64748b", "syntaxOperator": "#7c6b99", "syntaxPunctuation": "#1e293b" diff --git a/src/apps/cli/themes/presets/bitfun-midnight.json b/src/apps/cli/themes/presets/bitfun-midnight.json index 0934fe9d03..31bdc0c59f 100644 --- a/src/apps/cli/themes/presets/bitfun-midnight.json +++ b/src/apps/cli/themes/presets/bitfun-midnight.json @@ -10,13 +10,13 @@ "info": "#9bb3bf", "text": "#d7ded9", "textMuted": "#77827d", - "background": "#0b0d0e", - "backgroundPanel": "#141819", + "background": "#0e0e10", + "backgroundPanel": "#151515", "backgroundElement": "#1b3328", - "inputBackground": "#143d2b", + "inputBackground": "#1b3328", "border": "#32403b", "borderActive": "#8fbc9f", - "borderSubtle": "#24302d", + "borderSubtle": "#2b2b30", "diffAdded": "#8fbc9f", "diffRemoved": "#d9827f", "diffContext": "#77827d", @@ -25,10 +25,10 @@ "diffHighlightRemoved": "#e7a09d", "diffAddedBg": "#17291f", "diffRemovedBg": "#2f1d1d", - "diffContextBg": "#141819", + "diffContextBg": "#151515", "diffLineNumber": "#61706a", - "diffAddedLineNumberBg": "#1c3326", - "diffRemovedLineNumberBg": "#3a2222", + "diffAddedLineNumberBg": "#1b3328", + "diffRemovedLineNumberBg": "#3e221c", "markdownText": "#d7ded9", "markdownHeading": "#8fbc9f", "markdownLink": "#9bb3bf", @@ -36,7 +36,7 @@ "markdownCode": "#8fbc9f", "markdownBlockQuote": "#77827d", "markdownEmph": "#d6a657", - "markdownStrong": "#edf3ee", + "markdownStrong": "#d7ded9", "markdownHorizontalRule": "#32403b", "markdownListItem": "#8fbc9f", "markdownListEnumeration": "#d6a657", diff --git a/src/apps/cli/themes/presets/bitfun-tokyo-night.json b/src/apps/cli/themes/presets/bitfun-tokyo-night.json index 2c107f6b53..1b84d1a707 100644 --- a/src/apps/cli/themes/presets/bitfun-tokyo-night.json +++ b/src/apps/cli/themes/presets/bitfun-tokyo-night.json @@ -23,7 +23,7 @@ "diffHunkHeader": "#8ab4ff", "diffHighlightAdded": "#b3dc82", "diffHighlightRemoved": "#ff8fa3", - "diffAddedBg": "#2a3826", + "diffAddedBg": "#1b3328", "diffRemovedBg": "#3f2632", "diffContextBg": "#171933", "diffLineNumber": "#747ca8", diff --git a/src/apps/desktop/Cargo.toml b/src/apps/desktop/Cargo.toml index 9bcb6a3d83..d80b643b52 100644 --- a/src/apps/desktop/Cargo.toml +++ b/src/apps/desktop/Cargo.toml @@ -20,6 +20,7 @@ serde_json = { workspace = true } [dependencies] # Internal crates bitfun-core = { path = "../../crates/assembly/core", default-features = false, features = ["product-full"] } +bitfun-agent-tools = { path = "../../crates/execution/tool-contracts" } bitfun-transport = { path = "../../crates/adapters/transport", features = ["tauri-adapter"] } bitfun-webdriver = { path = "../../crates/adapters/webdriver" } bitfun-acp = { path = "../../crates/interfaces/acp" } @@ -45,6 +46,7 @@ tokio = { workspace = true } serde = { workspace = true } serde_json = { workspace = true } anyhow = { workspace = true } +base64 = { workspace = true } log = { workspace = true } chrono = { workspace = true } uuid = { workspace = true } @@ -67,6 +69,8 @@ bitflags = { workspace = true } core-foundation = { workspace = true } core-graphics = { workspace = true } dispatch = { workspace = true } +foreign-types = { workspace = true } +libc = { workspace = true } objc2 = { workspace = true, features = ["exception"] } objc2-foundation = { workspace = true } objc2-app-kit = { workspace = true } @@ -77,11 +81,27 @@ win32job = { workspace = true } windows = { workspace = true, features = [ "Foundation", "Globalization", + "Graphics_Capture", + "Graphics_DirectX", + "Graphics_DirectX_Direct3D11", "Graphics_Imaging", "Media_Ocr", "Storage_Streams", "Win32_Foundation", + "Win32_Graphics_Direct3D", + "Win32_Graphics_Direct3D11", + "Win32_Graphics_Dwm", + "Win32_Graphics_Dxgi", + "Win32_Graphics_Dxgi_Common", + "Win32_Graphics_Gdi", + "Win32_Storage_Xps", "Win32_System_Com", + "Win32_System_Ole", + "Win32_System_Variant", + "Win32_System_WinRT", + "Win32_System_WinRT_Direct3D11", + "Win32_System_WinRT_Graphics", + "Win32_System_WinRT_Graphics_Capture", "Win32_UI_Accessibility", "Win32_UI_WindowsAndMessaging", ] } diff --git a/src/apps/desktop/src/api/acp_client_api.rs b/src/apps/desktop/src/api/acp_client_api.rs index 845f02040a..1db3a7b300 100644 --- a/src/apps/desktop/src/api/acp_client_api.rs +++ b/src/apps/desktop/src/api/acp_client_api.rs @@ -89,6 +89,26 @@ pub struct ProbeAcpClientRequirementsRequest { pub force_refresh: bool, } +fn emit_acp_model_round_completed( + app_handle: &AppHandle, + session_id: &str, + turn_id: &str, + round_id: String, + has_tool_calls: bool, +) -> Result<(), bitfun_core::util::errors::BitFunError> { + app_handle + .emit( + "agentic://model-round-completed", + serde_json::json!({ + "sessionId": session_id, + "turnId": turn_id, + "roundId": round_id, + "hasToolCalls": has_tool_calls, + }), + ) + .map_err(|e| bitfun_core::util::errors::BitFunError::service(e.to_string())) +} + #[tauri::command] pub async fn initialize_acp_clients( state: State<'_, AppState>, @@ -287,6 +307,7 @@ pub async fn start_acp_dialog_turn( .map_err(|e| e.to_string())?; tokio::spawn(async move { let mut current_round_id: Option = None; + let mut current_round_has_tool_calls = false; let result = service .prompt_agent_stream( &request.client_id, @@ -303,7 +324,17 @@ pub async fn start_acp_dialog_turn( round_index, disable_explore_grouping, } => { + if let Some(previous_round_id) = current_round_id.take() { + emit_acp_model_round_completed( + &app_handle, + &request.session_id, + &request.turn_id, + previous_round_id, + current_round_has_tool_calls, + )?; + } current_round_id = Some(round_id.clone()); + current_round_has_tool_calls = false; app_handle .emit( "agentic://model-round-started", @@ -367,12 +398,19 @@ pub async fn start_acp_dialog_turn( })?; } AcpClientStreamEvent::ToolEvent(tool_event) => { + let round_id = current_round_id.clone().ok_or_else(|| { + bitfun_core::util::errors::BitFunError::service( + "ACP tool event arrived before model round start".to_string(), + ) + })?; + current_round_has_tool_calls = true; app_handle .emit( "agentic://tool-event", serde_json::json!({ "sessionId": request.session_id, "turnId": request.turn_id, + "roundId": round_id, "toolEvent": tool_event, "subagentParentInfo": null, }), @@ -442,6 +480,15 @@ pub async fn start_acp_dialog_turn( })?; } AcpClientStreamEvent::Completed => { + if let Some(round_id) = current_round_id.take() { + emit_acp_model_round_completed( + &app_handle, + &request.session_id, + &request.turn_id, + round_id, + current_round_has_tool_calls, + )?; + } app_handle .emit( "agentic://dialog-turn-completed", @@ -457,6 +504,15 @@ pub async fn start_acp_dialog_turn( })?; } AcpClientStreamEvent::Cancelled => { + if let Some(round_id) = current_round_id.take() { + emit_acp_model_round_completed( + &app_handle, + &request.session_id, + &request.turn_id, + round_id, + current_round_has_tool_calls, + )?; + } app_handle .emit( "agentic://dialog-turn-cancelled", diff --git a/src/apps/desktop/src/api/agentic_api.rs b/src/apps/desktop/src/api/agentic_api.rs index b0bacba51f..631b1dbd4b 100644 --- a/src/apps/desktop/src/api/agentic_api.rs +++ b/src/apps/desktop/src/api/agentic_api.rs @@ -23,7 +23,7 @@ use bitfun_core::agentic::deep_review_policy::{ }; use bitfun_core::agentic::goal_mode::{ThreadGoal, ThreadGoalStatus}; use bitfun_core::agentic::image_analysis::ImageContextData; -use bitfun_core::agentic::session::{SessionViewRestoreRequest, SessionViewRestoreTiming}; +use bitfun_core::agentic::session::SessionViewRestoreTiming; use bitfun_core::agentic::tools::image_context::get_image_context; use bitfun_core::agentic::tools::implementations::exec_command::{ background_command_output_capture, control_exec_command_session, send_exec_command_input, @@ -114,6 +114,8 @@ pub struct StartDialogTurnRequest { pub original_user_input: Option, pub agent_type: String, pub workspace_path: Option, + pub remote_connection_id: Option, + pub remote_ssh_host: Option, pub turn_id: Option, #[serde(default)] pub image_contexts: Option>, @@ -761,7 +763,7 @@ pub async fn update_session_title( .await; coordinator - .restore_session(&effective, session_id) + .restore_session_from_storage_path(&effective, session_id) .await .map_err(|e| format!("Failed to restore session before renaming: {}", e))?; } @@ -806,10 +808,12 @@ pub async fn ensure_coordinator_session( .await; let restore_result = if request.include_internal { coordinator - .restore_internal_session(&effective, session_id) + .restore_internal_session_from_storage_path(&effective, session_id) .await } else { - coordinator.restore_session(&effective, session_id).await + coordinator + .restore_session_from_storage_path(&effective, session_id) + .await }; restore_result.map(|_| ()).map_err(|e| e.to_string()) } @@ -827,6 +831,8 @@ pub async fn start_dialog_turn( original_user_input, agent_type, workspace_path, + remote_connection_id, + remote_ssh_host, turn_id, image_contexts, user_message_metadata, @@ -851,6 +857,8 @@ pub async fn start_dialog_turn( turn_id, agent_type, workspace_path, + remote_connection_id, + remote_ssh_host, policy, None, user_message_metadata, @@ -897,7 +905,7 @@ pub async fn compact_session( ) .await; coordinator - .restore_session(&effective, session_id) + .restore_session_from_storage_path(&effective, session_id) .await .map_err(|e| format!("Failed to restore session before compacting: {}", e))?; } @@ -945,7 +953,7 @@ pub async fn activate_session_goal( ) .await; coordinator - .restore_session(&effective, session_id) + .restore_session_from_storage_path(&effective, session_id) .await .map_err(|e| format!("Failed to restore session before activating goal mode: {e}"))?; } @@ -995,7 +1003,7 @@ async fn ensure_session_for_thread_goal( ) .await; coordinator - .restore_session(&effective, session_id) + .restore_session_from_storage_path(&effective, session_id) .await .map_err(|e| format!("Failed to restore session before thread goal access: {e}"))?; } @@ -1008,7 +1016,7 @@ async fn ensure_session_for_thread_goal( .ok_or_else(|| format!("Session workspace_path is missing: {session_id}")) } -async fn resolve_session_workspace_path_for_thread_goal_read( +async fn resolve_thread_goal_storage_path( coordinator: &Arc, app_state: &AppState, session_id: &str, @@ -1016,6 +1024,15 @@ async fn resolve_session_workspace_path_for_thread_goal_read( remote_connection_id: Option<&str>, remote_ssh_host: Option<&str>, ) -> Result { + if let Some(storage_path) = coordinator + .get_session_manager() + .resolve_session_workspace_binding(session_id) + .await + .map(|binding| binding.session_storage_dir()) + { + return Ok(storage_path); + } + if let Some(workspace_path) = coordinator .get_session_manager() .get_session(session_id) @@ -1051,7 +1068,7 @@ pub async fn get_session_thread_goal( if session_id.is_empty() { return Err("session_id is required".to_string()); } - let workspace_path = resolve_session_workspace_path_for_thread_goal_read( + let storage_path = resolve_thread_goal_storage_path( coordinator.inner(), app_state.inner(), session_id, @@ -1061,7 +1078,7 @@ pub async fn get_session_thread_goal( ) .await?; let goal = coordinator - .get_thread_goal(session_id, workspace_path.as_path()) + .get_thread_goal(session_id, storage_path.as_path()) .await .map_err(|error| error.to_string())?; Ok(GetSessionThreadGoalResponse { goal }) @@ -1205,7 +1222,7 @@ pub async fn run_init_agents_md( ) .await; coordinator - .restore_session(&effective, session_id) + .restore_session_from_storage_path(&effective, session_id) .await .map_err(|e| format!("Failed to restore session before running /init: {e}"))?; } @@ -1221,6 +1238,8 @@ pub async fn run_init_agents_md( .submit_init_agents_md( session_id.to_string(), workspace_path, + request.remote_connection_id.clone(), + request.remote_ssh_host.clone(), DialogSubmissionPolicy::for_source(DialogTriggerSource::DesktopUi), ) .await @@ -1504,19 +1523,34 @@ impl From for ExecCommandControlAction { #[tauri::command] pub async fn control_background_command( + coordinator: State<'_, Arc>, request: ControlBackgroundCommandRequest, ) -> Result<(), String> { let session_id = request.exec_session_id; let remote = request.remote; let action: ExecCommandControlAction = request.action.into(); + let terminal_port = if remote { + None + } else { + coordinator.inner().terminal_port() + }; + let remote_exec_port = if remote { + coordinator.inner().remote_exec_port() + } else { + None + }; - control_exec_command_session(ExecCommandControlRequest { - session_id, - action, - origin: ExecCommandControlOrigin::OutOfBand, - remote, - yield_time_ms: Some(250), - }) + control_exec_command_session( + ExecCommandControlRequest { + session_id, + action, + origin: ExecCommandControlOrigin::OutOfBand, + remote, + yield_time_ms: Some(250), + }, + terminal_port.as_ref(), + remote_exec_port.as_ref(), + ) .await .map(|response| { if response.session_id.is_none() { @@ -1562,24 +1596,43 @@ pub struct SendBackgroundCommandInputRequest { #[tauri::command] pub async fn send_background_command_input( + coordinator: State<'_, Arc>, request: SendBackgroundCommandInputRequest, ) -> Result<(), String> { if request.chars.is_empty() && !request.append_enter { return Err("chars or append_enter is required".to_string()); } - send_exec_command_input(ExecCommandInputRequest { - session_id: request.exec_session_id, - chars: request.chars, - append_enter: request.append_enter, - remote: request.remote, - }) + let session_id = request.exec_session_id; + let remote = request.remote; + let chars = request.chars; + let append_enter = request.append_enter; + let terminal_port = if remote { + None + } else { + coordinator.inner().terminal_port() + }; + let remote_exec_port = if remote { + coordinator.inner().remote_exec_port() + } else { + None + }; + send_exec_command_input( + ExecCommandInputRequest { + session_id, + chars, + append_enter, + remote, + }, + terminal_port.as_ref(), + remote_exec_port.as_ref(), + ) .await .map_err(|e| { log::error!( "Failed to send input to background command: exec_session_id={}, remote={}, error={}", - request.exec_session_id, - request.remote, + session_id, + remote, e ); format!("Failed to send input to background command: {}", e) @@ -1694,11 +1747,11 @@ pub async fn restore_session( .await; let session = if request.include_internal { coordinator - .restore_internal_session(&effective_path, &request.session_id) + .restore_internal_session_from_storage_path(&effective_path, &request.session_id) .await } else { coordinator - .restore_session(&effective_path, &request.session_id) + .restore_session_from_storage_path(&effective_path, &request.session_id) .await } .map_err(|e| format!("Failed to restore session: {}", e))?; @@ -1727,48 +1780,45 @@ pub async fn restore_session_view( request.remote_connection_id.as_deref(), request.remote_ssh_host.as_deref(), ) - .await; + .await; + let resolve_storage_path_duration_ms = + path_started_at.elapsed().as_millis().min(u64::MAX as u128) as u64; debug!( "restore_session_view storage path resolved: trace_id={}, session_id={}, duration_ms={}", trace_id, request.session_id, - path_started_at.elapsed().as_millis() + resolve_storage_path_duration_ms ); - let view_request = SessionViewRestoreRequest { - workspace_path: effective_path, - session_id: request.session_id.clone(), - include_internal: request.include_internal, - tail_turn_count: request.tail_turn_count, - }; - let tail_turn_count = view_request + let session_storage_path = effective_path; + let tail_turn_count = request .tail_turn_count .filter(|count| *count > 0) .map(|count| count.min(16)); - let (session, mut turns, total_turn_count, timings) = + let (session, mut turns, total_turn_count, mut timings) = if let Some(tail_turn_count) = tail_turn_count { - if view_request.include_internal { + if request.include_internal { coordinator - .restore_internal_session_view_tail_timed( - &view_request.workspace_path, - &view_request.session_id, + .restore_internal_session_view_from_storage_path_tail_timed( + &session_storage_path, + &request.session_id, tail_turn_count, ) .await } else { coordinator - .restore_session_view_tail_timed( - &view_request.workspace_path, - &view_request.session_id, + .restore_session_view_from_storage_path_tail_timed( + &session_storage_path, + &request.session_id, tail_turn_count, ) .await } - } else if view_request.include_internal { + } else if request.include_internal { coordinator - .restore_internal_session_view_timed( - &view_request.workspace_path, - &view_request.session_id, + .restore_internal_session_view_from_storage_path_timed( + &session_storage_path, + &request.session_id, ) .await .map(|(session, turns, timings)| { @@ -1777,7 +1827,10 @@ pub async fn restore_session_view( }) } else { coordinator - .restore_session_view_timed(&view_request.workspace_path, &view_request.session_id) + .restore_session_view_from_storage_path_timed( + &session_storage_path, + &request.session_id, + ) .await .map(|(session, turns, timings)| { let total_turn_count = turns.len(); @@ -1785,6 +1838,7 @@ pub async fn restore_session_view( }) } .map_err(|e| format!("Failed to restore session view: {}", e))?; + timings.resolve_storage_path_duration_ms = resolve_storage_path_duration_ms; let loaded_turn_count = turns.len(); let is_partial = loaded_turn_count < total_turn_count; @@ -1865,11 +1919,14 @@ pub async fn restore_session_with_turns( ); let (session, turns) = if request.include_internal { coordinator - .restore_internal_session_with_turns(&effective_path, &request.session_id) + .restore_internal_session_with_turns_from_storage_path( + &effective_path, + &request.session_id, + ) .await } else { coordinator - .restore_session_with_turns(&effective_path, &request.session_id) + .restore_session_with_turns_from_storage_path(&effective_path, &request.session_id) .await } .map_err(|e| format!("Failed to restore session: {}", e))?; diff --git a/src/apps/desktop/src/api/btw_api.rs b/src/apps/desktop/src/api/btw_api.rs index 1a174233d6..6480a686dd 100644 --- a/src/apps/desktop/src/api/btw_api.rs +++ b/src/apps/desktop/src/api/btw_api.rs @@ -13,6 +13,7 @@ use tauri::State; use crate::api::app_state::AppState; use bitfun_core::agentic::coordination::ConversationCoordinator; +use bitfun_core::agentic::image_analysis::ImageContextData; #[derive(Debug, Clone, Deserialize)] #[serde(rename_all = "camelCase")] @@ -24,6 +25,8 @@ pub struct BtwAskStreamRequest { pub child_session_name: Option, /// Optional model id override. Supports "fast"/"primary" aliases. pub model_id: Option, + #[serde(default)] + pub image_contexts: Option>, } #[derive(Debug, Clone, Serialize)] @@ -88,15 +91,20 @@ pub async fn btw_ask_stream( if child_session_id.is_empty() { return Err("childSessionId is required".to_string()); } + let child_session_id = child_session_id.to_string(); + let child_session_name = request.child_session_name.clone(); + let model_id = request.model_id.clone(); + let image_contexts = request.image_contexts; let turn_id = coordinator .start_hidden_btw_turn( &request.request_id, &request.session_id, - child_session_id, - request.child_session_name.as_deref(), + &child_session_id, + child_session_name.as_deref(), &request.question, - request.model_id.as_deref(), + model_id.as_deref(), + image_contexts, ) .await .map_err(|e| e.to_string())?; @@ -105,13 +113,13 @@ pub async fn btw_ask_stream( .side_question_runtime .register_btw_turn( request.request_id.clone(), - child_session_id.to_string(), + child_session_id.clone(), turn_id.clone(), ) .await; let runtime = state.side_question_runtime.clone(); let request_id = request.request_id.clone(); - let child_session_id = child_session_id.to_string(); + let child_session_id = child_session_id; let turn_id = turn_id; let coordinator = coordinator.inner().clone(); tokio::spawn(async move { diff --git a/src/apps/desktop/src/api/clipboard_file_api.rs b/src/apps/desktop/src/api/clipboard_file_api.rs index 7def913ef2..34d2b29aad 100644 --- a/src/apps/desktop/src/api/clipboard_file_api.rs +++ b/src/apps/desktop/src/api/clipboard_file_api.rs @@ -433,7 +433,9 @@ fn copy_directory_recursive(source: &Path, target: &Path) -> Result<(), String> #[cfg(test)] mod tests { - use super::{decode_file_uri, generate_unique_path, parse_clipboard_path_segments, parse_uri_list}; + use super::{ + decode_file_uri, generate_unique_path, parse_clipboard_path_segments, parse_uri_list, + }; use std::path::Path; #[test] @@ -495,14 +497,19 @@ mod tests { #[test] fn generate_unique_path_uses_current_dir_when_parent_missing() { let unique = generate_unique_path(Path::new("example.txt")); - assert_eq!(unique.file_name(), Some(std::ffi::OsStr::new("example (1).txt"))); + assert_eq!( + unique.file_name(), + Some(std::ffi::OsStr::new("example (1).txt")) + ); } #[test] fn parse_uri_list_ignores_comments_and_blank_lines() { - let files = parse_uri_list( - "# comment\n\nfile:///tmp/a.txt\r\nfile://localhost/tmp/b.txt\n", + let files = + parse_uri_list("# comment\n\nfile:///tmp/a.txt\r\nfile://localhost/tmp/b.txt\n"); + assert_eq!( + files, + vec!["/tmp/a.txt".to_string(), "/tmp/b.txt".to_string()] ); - assert_eq!(files, vec!["/tmp/a.txt".to_string(), "/tmp/b.txt".to_string()]); } } diff --git a/src/apps/desktop/src/api/computer_use_api.rs b/src/apps/desktop/src/api/computer_use_api.rs index b1377145f3..df350d6478 100644 --- a/src/apps/desktop/src/api/computer_use_api.rs +++ b/src/apps/desktop/src/api/computer_use_api.rs @@ -62,8 +62,16 @@ pub async fn computer_use_open_system_settings( } #[cfg(target_os = "windows")] { - let _ = request; - Err("Open system settings is not wired for Windows yet.".to_string()) + let uri = match request.pane.as_str() { + "accessibility" => "ms-settings:easeofaccess", + "screen_capture" => "ms-settings:privacy", + _ => return Err(format!("Unknown settings pane: {}", request.pane)), + }; + std::process::Command::new("cmd") + .args(["/C", "start", "", uri]) + .status() + .map_err(|e| e.to_string())?; + return Ok(()); } #[cfg(target_os = "linux")] { diff --git a/src/apps/desktop/src/api/config_api.rs b/src/apps/desktop/src/api/config_api.rs index db2e817492..8af0adae46 100644 --- a/src/apps/desktop/src/api/config_api.rs +++ b/src/apps/desktop/src/api/config_api.rs @@ -37,6 +37,12 @@ pub struct ResetConfigRequest { pub path: Option, } +#[derive(Debug, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct ImportConfigRequest { + pub config_data: Value, +} + #[derive(Debug, Deserialize, Default)] pub struct GetRuntimeLoggingInfoRequest {} @@ -262,13 +268,18 @@ pub async fn export_config(state: State<'_, AppState>) -> Result } #[tauri::command] -pub async fn import_config(state: State<'_, AppState>, config: Value) -> Result { +pub async fn import_config( + state: State<'_, AppState>, + request: ImportConfigRequest, +) -> Result { let config_service = &state.config_service; + let config_data = request + .config_data + .get("config") + .cloned() + .unwrap_or(request.config_data); - let export_data: bitfun_core::service::config::ConfigExport = - serde_json::from_value(config).map_err(|e| format!("Invalid config format: {}", e))?; - - match config_service.import_config(export_data).await { + match config_service.import_config_data(config_data).await { Ok(result) => { state.ai_client_factory.invalidate_cache(); info!("Config imported, AI client cache invalidated"); diff --git a/src/apps/desktop/src/api/context_upload_api.rs b/src/apps/desktop/src/api/context_upload_api.rs index e556269b13..20b55a6039 100644 --- a/src/apps/desktop/src/api/context_upload_api.rs +++ b/src/apps/desktop/src/api/context_upload_api.rs @@ -1,10 +1,15 @@ //! Temporary Image Storage API +use base64::{engine::general_purpose::STANDARD as BASE64, Engine as _}; use bitfun_core::agentic::tools::image_context::{ create_image_context_provider as create_core_image_context_provider, store_image_contexts, GlobalImageContextProvider, ImageContextData as CoreImageContextData, }; +use bitfun_core::infrastructure::try_get_path_manager_arc; +use log::warn; use serde::{Deserialize, Serialize}; +use std::path::{Path, PathBuf}; +use uuid::Uuid; #[derive(Debug, Clone, Serialize, Deserialize)] pub struct ImageContextData { @@ -40,13 +45,162 @@ pub struct UploadImageContextRequest { pub images: Vec, } +#[derive(Debug, Serialize)] +pub struct UploadedImageContextResponse { + pub id: String, + pub image_path: Option, +} + #[tauri::command] -pub async fn upload_image_contexts(request: UploadImageContextRequest) -> Result<(), String> { - let images: Vec = request.images.into_iter().map(Into::into).collect(); +pub async fn upload_image_contexts( + request: UploadImageContextRequest, +) -> Result, String> { + let mut images = Vec::with_capacity(request.images.len()); + for image in request.images { + images.push(prepare_image_context_for_storage(image).await?); + } + let response = images + .iter() + .map(|image| UploadedImageContextResponse { + id: image.id.clone(), + image_path: image + .image_path + .as_ref() + .filter(|path| !path.trim().is_empty()) + .cloned(), + }) + .collect(); + let images: Vec = images.into_iter().map(Into::into).collect(); store_image_contexts(images); - Ok(()) + Ok(response) } pub fn create_image_context_provider() -> GlobalImageContextProvider { create_core_image_context_provider() } + +async fn prepare_image_context_for_storage( + mut image: ImageContextData, +) -> Result { + if has_text(image.image_path.as_deref()) { + return Ok(image); + } + + let Some(data_url) = image + .data_url + .as_deref() + .filter(|value| has_text(Some(value))) + else { + return Ok(image); + }; + + match persist_uploaded_image(data_url, &image).await { + Ok(path) => { + image.image_path = Some(path.to_string_lossy().to_string()); + } + Err(error) => { + warn!( + "Failed to persist uploaded image to temp storage: image_id={}, error={}", + image.id, error + ); + return Err(error); + } + } + + Ok(image) +} + +async fn persist_uploaded_image( + data_url: &str, + image: &ImageContextData, +) -> Result { + let (bytes, mime_type) = decode_data_url(data_url)?; + let ext = image_extension( + mime_type.as_deref().or(Some(image.mime_type.as_str())), + &image.image_name, + ); + let file_name = format!("{}-{}.{}", safe_file_stem(&image.id), Uuid::new_v4(), ext); + let dir = uploaded_image_dir()?; + tokio::fs::create_dir_all(&dir) + .await + .map_err(|error| format!("Failed to create image attachment directory: {error}"))?; + let path = dir.join(file_name); + tokio::fs::write(&path, bytes) + .await + .map_err(|error| format!("Failed to write image attachment: {error}"))?; + Ok(path) +} + +fn uploaded_image_dir() -> Result { + let root = try_get_path_manager_arc() + .map(|manager| manager.temp_dir()) + .unwrap_or_else(|_| std::env::temp_dir().join("bitfun")); + Ok(root.join("attachments").join("images")) +} + +fn decode_data_url(data_url: &str) -> Result<(Vec, Option), String> { + if !data_url.starts_with("data:") { + return Err("Invalid image data URL".to_string()); + } + + let (header, payload) = data_url + .split_once(',') + .ok_or_else(|| "Invalid image data URL format".to_string())?; + if !header.contains(";base64") { + return Err("Only base64 image data URLs are supported".to_string()); + } + + let mime_type = header + .strip_prefix("data:") + .and_then(|value| value.split(';').next()) + .map(str::trim) + .filter(|value| !value.is_empty()) + .map(ToString::to_string); + let bytes = BASE64 + .decode(payload) + .map_err(|error| format!("Failed to decode image data URL: {error}"))?; + Ok((bytes, mime_type)) +} + +fn image_extension(mime_type: Option<&str>, image_name: &str) -> String { + match mime_type.unwrap_or_default().to_ascii_lowercase().as_str() { + "image/jpeg" | "image/jpg" => "jpg".to_string(), + "image/png" => "png".to_string(), + "image/gif" => "gif".to_string(), + "image/webp" => "webp".to_string(), + "image/bmp" => "bmp".to_string(), + _ => Path::new(image_name) + .extension() + .and_then(|ext| ext.to_str()) + .map(|ext| ext.to_ascii_lowercase()) + .and_then(|ext| match ext.as_str() { + "jpg" | "jpeg" => Some("jpg".to_string()), + "png" => Some("png".to_string()), + "gif" => Some("gif".to_string()), + "webp" => Some("webp".to_string()), + "bmp" => Some("bmp".to_string()), + _ => None, + }) + .unwrap_or_else(|| "png".to_string()), + } +} + +fn safe_file_stem(value: &str) -> String { + let mut out = String::with_capacity(value.len().min(64)); + for ch in value.chars().take(64) { + if ch.is_ascii_alphanumeric() || ch == '-' || ch == '_' { + out.push(ch); + } else { + out.push('_'); + } + } + if out.is_empty() { + "image".to_string() + } else { + out + } +} + +fn has_text(value: Option<&str>) -> bool { + value.is_some_and(|value| !value.trim().is_empty()) +} diff --git a/src/apps/desktop/src/api/cron_api.rs b/src/apps/desktop/src/api/cron_api.rs index a02e3d03e3..6448ecadd9 100644 --- a/src/apps/desktop/src/api/cron_api.rs +++ b/src/apps/desktop/src/api/cron_api.rs @@ -101,3 +101,14 @@ pub async fn delete_cron_job(request: DeleteCronJobRequest) -> Result Result<(), String> { + debug!("Received scheduled job host ready signal"); + + let service = cron_service()?; + // `start` is idempotent, so the frontend readiness signal can safely race + // with the desktop fallback timer. + service.start(); + Ok(()) +} diff --git a/src/apps/desktop/src/api/lsp_api.rs b/src/apps/desktop/src/api/lsp_api.rs index d96ad3341a..5b617f95ae 100644 --- a/src/apps/desktop/src/api/lsp_api.rs +++ b/src/apps/desktop/src/api/lsp_api.rs @@ -393,32 +393,14 @@ pub struct SupportedExtensionsResponse { #[tauri::command] pub async fn lsp_get_supported_extensions() -> Result { - use std::collections::HashMap; - let manager = get_global_lsp_manager().map_err(|e| format!("LSP not initialized: {}", e))?; let guard = manager.read().await; - let plugins = guard.list_plugins().await; - - let mut extension_to_language: HashMap = HashMap::new(); - let mut supported_languages: std::collections::HashSet = - std::collections::HashSet::new(); - - for plugin in plugins { - for lang in &plugin.languages { - supported_languages.insert(lang.clone()); - } - - for ext in &plugin.file_extensions { - if !plugin.languages.is_empty() { - extension_to_language.insert(ext.clone(), plugin.languages[0].clone()); - } - } - } + let summary = guard.supported_extensions().await; Ok(SupportedExtensionsResponse { - extension_to_language, - supported_languages: supported_languages.into_iter().collect(), + extension_to_language: summary.extension_to_language, + supported_languages: summary.supported_languages, }) } diff --git a/src/apps/desktop/src/api/miniapp_agent_api.rs b/src/apps/desktop/src/api/miniapp_agent_api.rs index 662fd641b4..4be06649aa 100644 --- a/src/apps/desktop/src/api/miniapp_agent_api.rs +++ b/src/apps/desktop/src/api/miniapp_agent_api.rs @@ -258,6 +258,8 @@ pub async fn miniapp_agent_run( Some(submission_plan.run_id.clone()), MINIAPP_AGENT_KIND.to_string(), Some(submission_plan.workspace_path.clone()), + None, + None, policy, None, Some(submission_plan.metadata.clone()), diff --git a/src/apps/desktop/src/api/remote_connect_api.rs b/src/apps/desktop/src/api/remote_connect_api.rs index 967d9a9c31..f651d75ef1 100644 --- a/src/apps/desktop/src/api/remote_connect_api.rs +++ b/src/apps/desktop/src/api/remote_connect_api.rs @@ -7,6 +7,7 @@ use bitfun_core::service::remote_connect::{ }; use regex::Regex; use serde::{Deserialize, Serialize}; +use std::collections::HashMap; use std::path::PathBuf; use std::sync::{Arc, OnceLock}; use tokio::sync::RwLock; @@ -197,6 +198,7 @@ fn is_valid_mobile_web_dir(dir: &std::path::Path) -> bool { pub struct StartRemoteConnectRequest { pub method: String, pub custom_server_url: Option, + pub lan_ip: Option, } #[derive(Debug, Serialize)] @@ -228,10 +230,18 @@ pub struct DeviceInfo { pub mac_address: String, } +#[derive(Debug, Serialize)] +pub struct LanNetworkInterface { + pub interface_name: String, + pub ip: String, + pub gateway_ip: Option, +} + #[derive(Debug, Serialize)] pub struct LanNetworkInfo { pub local_ip: String, pub gateway_ip: Option, + pub available_ips: Vec, } fn detect_default_gateway_ip() -> Option { @@ -289,7 +299,100 @@ fn detect_default_gateway_ip() -> Option { None } -// ── Tauri Commands ───────────────────────────────────────────────── +/// Detect per-interface gateway IPs by parsing the system routing table. +/// +/// Returns a map keyed by interface identifier (interface name on macOS/Linux, +/// interface IP on Windows) → gateway IP. Only interfaces that have a default +/// route entry appear in the map. +fn detect_interface_gateways() -> HashMap { + let mut map = HashMap::new(); + + #[cfg(target_os = "macos")] + { + if let Ok(output) = bitfun_core::util::process_manager::create_command("netstat") + .args(["-rn", "-f", "inet"]) + .output() + { + if output.status.success() { + let stdout = String::from_utf8_lossy(&output.stdout); + // Lines look like: + // default 192.168.1.1 UGScg en0 + for line in stdout.lines() { + let parts: Vec<&str> = line.split_whitespace().collect(); + if parts.len() >= 4 && parts[0] == "default" { + let gateway = parts[1]; + let netif = parts[3]; + if is_ipv4(gateway) { + map.insert(netif.to_string(), gateway.to_string()); + } + } + } + } + } + } + + #[cfg(target_os = "linux")] + { + if let Ok(output) = bitfun_core::util::process_manager::create_command("ip") + .args(["route", "show", "default"]) + .output() + { + if output.status.success() { + let stdout = String::from_utf8_lossy(&output.stdout); + // Lines look like: + // default via 192.168.1.1 dev eth0 proto dhcp metric 100 + for line in stdout.lines() { + let parts: Vec<&str> = line.split_whitespace().collect(); + let mut via = None; + let mut dev = None; + for i in 0..parts.len() { + match parts[i] { + "via" if i + 1 < parts.len() => via = Some(parts[i + 1]), + "dev" if i + 1 < parts.len() => dev = Some(parts[i + 1]), + _ => {} + } + } + if let (Some(gw), Some(iface)) = (via, dev) { + if is_ipv4(gw) { + map.insert(iface.to_string(), gw.to_string()); + } + } + } + } + } + } + + #[cfg(target_os = "windows")] + { + if let Ok(output) = bitfun_core::util::process_manager::create_command("route") + .args(["print", "-4"]) + .output() + { + if output.status.success() { + let stdout = String::from_utf8_lossy(&output.stdout); + // Lines look like: + // 0.0.0.0 0.0.0.0 192.168.1.1 192.168.1.2 25 + // Column 3 = gateway, column 4 = interface IP + for line in stdout.lines() { + let parts: Vec<&str> = line.split_whitespace().collect(); + if parts.len() >= 4 && parts[0] == "0.0.0.0" && parts[1] == "0.0.0.0" { + if is_ipv4(parts[2]) && is_ipv4(parts[3]) { + // Key by interface IP so it can be matched later + map.insert(parts[3].to_string(), parts[2].to_string()); + } + } + } + } + } + } + + map +} + +/// Quick check whether a string looks like an IPv4 address. +fn is_ipv4(s: &str) -> bool { + s.split('.').count() == 4 && s.split('.').all(|p| p.parse::().is_ok()) +} #[tauri::command] pub async fn remote_connect_get_device_info() -> Result { @@ -312,11 +415,33 @@ pub async fn remote_connect_get_lan_ip() -> Result { #[tauri::command] pub async fn remote_connect_get_lan_network_info() -> Result { - let local_ip = lan::get_local_ip().map_err(|e| format!("get local ip: {e}"))?; + let interfaces = lan::list_local_ips().map_err(|e| format!("list local ips: {e}"))?; + let local_ip = interfaces + .first() + .map(|e| e.ip.clone()) + .ok_or_else(|| "no local IPv4 addresses found".to_string())?; let gateway_ip = detect_default_gateway_ip(); + // Build per-interface gateway map once from the routing table. + let gateway_map = detect_interface_gateways(); + let available_ips = interfaces + .into_iter() + .map(|e| { + // Look up by interface name (macOS/Linux) or by IP (Windows). + let gw = gateway_map + .get(&e.interface_name) + .or_else(|| gateway_map.get(&e.ip)) + .cloned(); + LanNetworkInterface { + gateway_ip: gw, + interface_name: e.interface_name, + ip: e.ip, + } + }) + .collect(); Ok(LanNetworkInfo { local_ip, gateway_ip, + available_ips, }) } @@ -331,7 +456,7 @@ pub async fn remote_connect_get_methods() -> Result, S let infos = methods .into_iter() .map(|m| match m { - ConnectionMethod::Lan => ConnectionMethodInfo { + ConnectionMethod::Lan { .. } => ConnectionMethodInfo { id: "lan".into(), name: "LAN".into(), available: true, @@ -382,9 +507,12 @@ pub async fn remote_connect_get_methods() -> Result, S fn parse_connection_method( method: &str, custom_url: Option, + lan_ip: Option, ) -> Result { match method { - "lan" => Ok(ConnectionMethod::Lan), + "lan" => Ok(ConnectionMethod::Lan { + ip: lan_ip.filter(|s| !s.is_empty()), + }), "ngrok" => Ok(ConnectionMethod::Ngrok), "bitfun_server" => Ok(ConnectionMethod::BitfunServer), "custom_server" => Ok(ConnectionMethod::CustomServer { @@ -402,7 +530,8 @@ pub async fn remote_connect_start( request: StartRemoteConnectRequest, ) -> Result { ensure_service().await?; - let method = parse_connection_method(&request.method, request.custom_server_url)?; + let method = + parse_connection_method(&request.method, request.custom_server_url, request.lan_ip)?; let holder = get_service_holder(); let guard = holder.read().await; diff --git a/src/apps/desktop/src/api/tool_api.rs b/src/apps/desktop/src/api/tool_api.rs index 171d06bf05..05f0e6402a 100644 --- a/src/apps/desktop/src/api/tool_api.rs +++ b/src/apps/desktop/src/api/tool_api.rs @@ -12,6 +12,7 @@ use bitfun_core::agentic::{ workspace::{local_workspace_services, remote_workspace_services}, WorkspaceBinding, }; +use bitfun_core::product_runtime::CoreRuntimeServicesProvider; use bitfun_core::service::remote_ssh::workspace_state::{ get_remote_workspace_manager, lookup_remote_connection, workspace_session_identity, }; @@ -172,7 +173,16 @@ async fn build_tool_context(workspace_path: Option<&str>) -> ToolUseContext { None => None, }; - ToolUseContext::for_tool_listing(workspace, workspace_services) + let remote_exec_port = workspace + .as_ref() + .is_some_and(WorkspaceBinding::is_remote) + .then(CoreRuntimeServicesProvider::remote_exec_port); + + ToolUseContext::for_tool_listing_with_remote_exec_port( + workspace, + workspace_services, + remote_exec_port, + ) } fn to_dynamic_mcp_tool_info( diff --git a/src/apps/desktop/src/computer_use/AGENTS.md b/src/apps/desktop/src/computer_use/AGENTS.md new file mode 100644 index 0000000000..c2d1a6db86 --- /dev/null +++ b/src/apps/desktop/src/computer_use/AGENTS.md @@ -0,0 +1,54 @@ +# Computer Use (desktop host) + +## Scope + +Platform-specific automation for the unified `computer_use` tool lives under +`src/apps/desktop/src/computer_use/`. Shared contracts and tool orchestration +are in `src/crates/assembly/core` and `src/crates/execution/tool-contracts`. + +## Platform maturity + +| Platform | Tier | Capabilities | +|---|---|---| +| **macOS** | AX-first | Accessibility tree, background input, Skylight/window capture, menu shortcuts, interactive/visual views | +| **Windows** | AX-first | UI Automation tree, `PrintWindow` + WGC + BitBlt capture, MSAA for legacy VCL, background input | +| **Linux** | **Legacy only** | Full-screen / region screenshot, enigo pointer/keyboard (X11), AT-SPI locate + OCR. No AX-first APIs | + +### Linux legacy layer + +Linux is intentionally a **compatibility layer**, not parity with macOS/Windows. + +**Available:** `screenshot`, `click` / `move` / `scroll` / `type` / `key_chord`, +`locate` (AT-SPI + OCR fallback). + +**Unavailable (return `LINUX_LEGACY_AX_UNAVAILABLE`):** `get_app_state`, +`get_app_shortcuts`, all `app_*` actions, `interactive_*`, `visual_*`, +`list_apps` (returns empty), background-input flags. + +Requires an interactive X11 session for input; Wayland-only setups may fail +with permission or coordinate errors surfaced to the agent. + +## Module map + +- `desktop_host/` — `ComputerUseHost` trait impl; entry for all actions +- `macos_*` / `windows_*` — platform AX, capture, list-apps, shortcuts +- `windows_capture.rs` — tiered capture: PrintWindow → WGC → BitBlt +- `windows_wgc_capture.rs` — Windows.Graphics.Capture (Direct3D11) +- `linux_ax_ui.rs` — AT-SPI locate (legacy) +- `screen_ocr.rs`, `ui_locate_common.rs` — shared OCR/locate helpers + +## Windows capture fallback chain + +When `PrintWindow` returns a mostly-black bitmap (DirectComposition / UWP): + +1. Try **WGC** via `screenshot_window_via_wgc` (occlusion-immune) +2. Fall back to **screen-region BitBlt** (on-screen, non-occluded targets) + +## Verification + +```bash +cargo check -p bitfun-desktop +cargo test -p bitfun-desktop +``` + +Windows-only paths (`windows_wgc_capture`, UIA) compile on CI (`windows-latest`). diff --git a/src/apps/desktop/src/computer_use/debug_overlay.rs b/src/apps/desktop/src/computer_use/debug_overlay.rs new file mode 100644 index 0000000000..8cbd2b4033 --- /dev/null +++ b/src/apps/desktop/src/computer_use/debug_overlay.rs @@ -0,0 +1,275 @@ +//! Debug overlay utilities for Computer Use screenshots. +//! +//! Provides a red crosshair marker that can be drawn on debug screenshots +//! to verify coordinate targeting — useful for checking that the +//! coordinates an agent computed actually land on the intended UI element. +//! +//! Ported from cua-driver-rs `cua-driver-core/src/image_utils.rs` +//! `crosshair_png_bytes` / `write_crosshair_png`, adapted to BitFun's +//! `image` pipeline (`RgbaImage` + `BitFunError`). + +#![allow(dead_code)] + +use bitfun_core::util::errors::{BitFunError, BitFunResult}; +use image::{ImageOutputFormat, Rgba, RgbaImage}; +use std::io::Cursor; + +/// Half-length of each crosshair arm in pixels (arm spans ±`ARM_LEN`). +const ARM_LEN: i32 = 10; +/// Stroke thickness of each arm in pixels. +const THICKNESS: i32 = 2; +/// Radius of the filled center circle in pixels. +const CIRCLE_R: i32 = 5; + +/// Draw a crosshair marker at `(x, y)` on an RGBA image buffer. +/// +/// The crosshair is 21px wide (±10px from center) with a 2px-thick stroke +/// for both arms and a small 5px-radius filled circle at the center. Pixels +/// outside the image bounds are skipped silently, so coordinates at the +/// edges (e.g. `(0, 0)` or `(width-1, height-1)`) never panic. +/// +/// `color` is alpha-composited over the existing pixels, so a semi-transparent +/// color (e.g. alpha 180) blends with the underlying screenshot instead of +/// fully occluding it. +pub fn draw_crosshair(img: &mut RgbaImage, x: u32, y: u32, color: Rgba) { + let (w, h) = img.dimensions(); + if x >= w || y >= h { + return; + } + let xi = x as i32; + let yi = y as i32; + let iw = w as i32; + let ih = h as i32; + + // Horizontal arm: spans ±ARM_LEN along x, THICKNESS rows tall. + for dx in -ARM_LEN..=ARM_LEN { + for toff in 0..THICKNESS { + let off = toff - THICKNESS / 2; + put_pixel(img, xi + dx, yi + off, color, iw, ih); + } + } + + // Vertical arm: spans ±ARM_LEN along y, THICKNESS columns wide. + for dy in -ARM_LEN..=ARM_LEN { + for toff in 0..THICKNESS { + let off = toff - THICKNESS / 2; + put_pixel(img, xi + off, yi + dy, color, iw, ih); + } + } + + // Filled center circle of radius CIRCLE_R. + for dy in -CIRCLE_R..=CIRCLE_R { + for dx in -CIRCLE_R..=CIRCLE_R { + if dx * dx + dy * dy <= CIRCLE_R * CIRCLE_R { + put_pixel(img, xi + dx, yi + dy, color, iw, ih); + } + } + } +} + +/// Convenience wrapper around [`draw_crosshair`] using a semi-transparent +/// red marker `(255, 0, 0, 180)` — visible over both light and dark UI +/// without fully hiding the pixel underneath. +pub fn draw_click_marker(img: &mut RgbaImage, x: u32, y: u32) { + let red = Rgba([255u8, 0, 0, 180]); + draw_crosshair(img, x, y, red); +} + +/// Load a JPEG/PNG screenshot from `raw` bytes, draw a red click crosshair +/// at `(x, y)`, and re-encode the result as JPEG. +/// +/// `mime` selects the input decoder (`image/jpeg`, `image/jpg`, or +/// `image/png`); the bytes themselves are content-detected by the `image` +/// crate so a slightly mismatched mime still decodes when the magic bytes +/// are valid. The output is always JPEG so it can drop into the existing +/// screenshot pipeline without changing any downstream wiring. +pub fn annotate_screenshot_with_click( + raw: &[u8], + mime: &str, + x: u32, + y: u32, +) -> BitFunResult> { + let mime_lower = mime.to_ascii_lowercase(); + let supported = matches!( + mime_lower.as_str(), + "image/jpeg" | "image/jpg" | "image/png" + ); + if !supported { + return Err(BitFunError::tool(format!( + "debug_overlay: unsupported mime type: {mime}" + ))); + } + + let mut img = image::load_from_memory(raw) + .map_err(|e| BitFunError::tool(format!("debug_overlay: decode image failed: {e}")))? + .to_rgba8(); + draw_click_marker(&mut img, x, y); + + let mut out = Vec::with_capacity(raw.len()); + image::DynamicImage::ImageRgba8(img) + .write_to(&mut Cursor::new(&mut out), ImageOutputFormat::Jpeg(80)) + .map_err(|e| BitFunError::tool(format!("debug_overlay: encode JPEG failed: {e}")))?; + Ok(out) +} + +/// Bounds-checked, alpha-composited pixel write. Mirrors the blending +/// convention used by `som_overlay::put_pixel` so semi-transparent marker +/// colors blend consistently across the Computer Use image pipeline. +#[inline] +fn put_pixel(img: &mut RgbaImage, x: i32, y: i32, color: Rgba, iw: i32, ih: i32) { + if x < 0 || y < 0 || x >= iw || y >= ih { + return; + } + let dst = img.get_pixel_mut(x as u32, y as u32); + let a = color.0[3] as u32; + if a == 255 { + *dst = color; + return; + } + if a == 0 { + return; + } + let inv = 255 - a; + for c in 0..3 { + dst.0[c] = ((color.0[c] as u32 * a + dst.0[c] as u32 * inv) / 255) as u8; + } + dst.0[3] = 255; +} + +#[cfg(test)] +mod tests { + use super::*; + use image::{ImageBuffer, ImageEncoder}; + use std::io::Cursor; + + /// Build a solid-black RGBA image of the given size. + fn blank_rgba(w: u32, h: u32) -> RgbaImage { + let mut img: RgbaImage = ImageBuffer::new(w, h); + for px in img.pixels_mut() { + *px = Rgba([0, 0, 0, 255]); + } + img + } + + /// Encode a solid-color JPEG (matches the `som_overlay` test helper style). + fn solid_jpeg(w: u32, h: u32) -> Vec { + let mut buf: RgbaImage = ImageBuffer::new(w, h); + for px in buf.pixels_mut() { + *px = Rgba([20, 20, 20, 255]); + } + let rgb = image::DynamicImage::ImageRgba8(buf).to_rgb8(); + let mut out = Vec::new(); + let encoder = image::codecs::jpeg::JpegEncoder::new_with_quality(&mut out, 90); + encoder + .write_image(rgb.as_raw(), w, h, image::ColorType::Rgb8) + .unwrap(); + out + } + + #[test] + fn crosshair_drawn_at_correct_position() { + let mut img = blank_rgba(60, 60); + draw_crosshair(&mut img, 30, 30, Rgba([255, 0, 0, 255])); + + // Center. + assert_eq!(img.get_pixel(30, 30).0, [255, 0, 0, 255]); + // Horizontal arm endpoints (±10 from center, on a red row). + assert_eq!(img.get_pixel(20, 30).0, [255, 0, 0, 255]); + assert_eq!(img.get_pixel(40, 30).0, [255, 0, 0, 255]); + // Vertical arm endpoints (±10 from center, on a red column). + assert_eq!(img.get_pixel(30, 20).0, [255, 0, 0, 255]); + assert_eq!(img.get_pixel(30, 40).0, [255, 0, 0, 255]); + // 2px thickness: the row just above center is also red. + assert_eq!(img.get_pixel(20, 29).0, [255, 0, 0, 255]); + // Pixels just past the arm length are NOT red. + assert_ne!(img.get_pixel(19, 30).0, [255, 0, 0, 255]); + assert_ne!(img.get_pixel(41, 30).0, [255, 0, 0, 255]); + assert_ne!(img.get_pixel(30, 19).0, [255, 0, 0, 255]); + assert_ne!(img.get_pixel(30, 41).0, [255, 0, 0, 255]); + } + + #[test] + fn center_pixels_are_red() { + let mut img = blank_rgba(40, 40); + draw_crosshair(&mut img, 20, 20, Rgba([255, 0, 0, 255])); + assert_eq!(img.get_pixel(20, 20).0, [255, 0, 0, 255]); + } + + #[test] + fn does_not_panic_at_edge_coordinates() { + // Top-left corner. + let mut img = blank_rgba(30, 30); + draw_crosshair(&mut img, 0, 0, Rgba([255, 0, 0, 255])); + assert_eq!(img.get_pixel(0, 0).0, [255, 0, 0, 255]); + + // Bottom-right corner (width-1, height-1). + draw_crosshair(&mut img, 29, 29, Rgba([255, 0, 0, 255])); + assert_eq!(img.get_pixel(29, 29).0, [255, 0, 0, 255]); + } + + #[test] + fn out_of_bounds_coordinate_is_noop() { + let mut img = blank_rgba(10, 10); + draw_crosshair(&mut img, 100, 100, Rgba([255, 0, 0, 255])); + for x in 0..10 { + for y in 0..10 { + assert_ne!(img.get_pixel(x, y).0, [255, 0, 0, 255]); + } + } + } + + #[test] + fn draw_click_marker_blends_center() { + let mut img = blank_rgba(40, 40); + draw_click_marker(&mut img, 20, 20); + + // A pixel drawn exactly once (arm only, outside the center circle) + // blends red-over-black at alpha 180 -> R = 180. + // (10, 20) is on the horizontal arm at distance 10 > circle radius 5, + // and not on the vertical arm, so it is touched by a single put_pixel. + let arm = img.get_pixel(10, 20); + assert_eq!(arm.0, [180, 0, 0, 255]); + + // The exact center is drawn three times (horizontal arm + vertical arm + // + filled circle), so it blends towards opaque red (R well above 180). + let center = img.get_pixel(20, 20); + assert!( + center.0[0] > 200, + "center red should be near-opaque: {:?}", + center + ); + assert_eq!(center.0[1], 0); + assert_eq!(center.0[2], 0); + } + + #[test] + fn annotate_jpeg_round_trip_preserves_dimensions() { + let jpeg = solid_jpeg(80, 60); + let out = annotate_screenshot_with_click(&jpeg, "image/jpeg", 40, 30).expect("annotate"); + let decoded = image::load_from_memory(&out).expect("decode"); + assert_eq!(decoded.width(), 80); + assert_eq!(decoded.height(), 60); + } + + #[test] + fn annotate_png_input_returns_jpeg() { + let mut buf: RgbaImage = ImageBuffer::new(40, 40); + for px in buf.pixels_mut() { + *px = Rgba([10, 10, 10, 255]); + } + let mut png = Vec::new(); + image::DynamicImage::ImageRgba8(buf) + .write_to(&mut Cursor::new(&mut png), ImageOutputFormat::Png) + .unwrap(); + let out = annotate_screenshot_with_click(&png, "image/png", 20, 20).expect("annotate"); + // JPEG magic: FF D8 FF. + assert_eq!(&out[..3], &[0xFF, 0xD8, 0xFF]); + } + + #[test] + fn annotate_rejects_unsupported_mime() { + let jpeg = solid_jpeg(20, 20); + let res = annotate_screenshot_with_click(&jpeg, "image/gif", 10, 10); + assert!(res.is_err()); + } +} diff --git a/src/apps/desktop/src/computer_use/desktop_host.rs b/src/apps/desktop/src/computer_use/desktop_host.rs deleted file mode 100644 index d3e48375e6..0000000000 --- a/src/apps/desktop/src/computer_use/desktop_host.rs +++ /dev/null @@ -1,5364 +0,0 @@ -//! Cross-platform `ComputerUseHost` via `screenshots` + `enigo`. - -#![allow(dead_code)] - -use async_trait::async_trait; -use bitfun_core::agentic::tools::computer_use_host::{ - clamp_point_crop_half_extent, ActionRecord, AppClickParams, AppInfo, AppSelector, - AppStateSnapshot, AppWaitPredicate, ClickTarget, ComputerScreenshot, ComputerUseDisplayInfo, - ComputerUseHost, ComputerUseImageContentRect, ComputerUseImageGlobalBounds, - ComputerUseImplicitScreenshotCenter, ComputerUseInteractionScreenshotKind, - ComputerUseInteractionState, ComputerUseLastMutationKind, ComputerUseNavigateQuadrant, - ComputerUseNavigationRect, ComputerUsePermissionSnapshot, ComputerUseScreenshotParams, - ComputerUseScreenshotRefinement, ComputerUseSessionSnapshot, InteractiveActionResult, - InteractiveClickParams, InteractiveScrollParams, InteractiveTypeTextParams, InteractiveView, - InteractiveViewOpts, LoopDetectionResult, OcrRegionNative, ScreenshotCropCenter, - UiElementLocateQuery, UiElementLocateResult, VisualActionResult, VisualClickParams, VisualMark, - VisualMarkView, VisualMarkViewOpts, COMPUTER_USE_QUADRANT_CLICK_READY_MAX_LONG_EDGE, - COMPUTER_USE_QUADRANT_EDGE_EXPAND_PX, -}; -#[cfg(any(target_os = "macos", target_os = "windows"))] -use bitfun_core::agentic::tools::computer_use_host::{ - ComputerUseForegroundApplication, ComputerUsePointerGlobal, -}; -use bitfun_core::agentic::tools::computer_use_optimizer::ComputerUseOptimizer; -use bitfun_core::util::errors::{BitFunError, BitFunResult}; -use enigo::{Axis, Button, Coordinate, Direction, Enigo, Key, Keyboard, Mouse, Settings}; -use image::codecs::jpeg::JpegEncoder; -use image::{DynamicImage, Rgb, RgbImage}; -use log::{debug, warn}; -use resvg::tiny_skia::{Pixmap, Transform}; -use resvg::usvg; -use screenshots::display_info::DisplayInfo; -use screenshots::Screen; -use std::sync::atomic::{AtomicU64, Ordering}; -use std::sync::{Mutex, OnceLock}; -use std::time::{Duration, Instant}; - -/// Default pointer overlay; replace `assets/computer_use_pointer.svg` and rebuild to customize. -/// Hotspot in SVG user space must stay at **(0,0)** (arrow tip). -const POINTER_OVERLAY_SVG: &str = include_str!("../../assets/computer_use_pointer.svg"); - -/// Screenshot cache validity duration (ms) - reuse full capture for subsequent crops within this window -const SCREENSHOT_CACHE_TTL_MS: u64 = 300; - -/// Error text when `click_needs_fresh_screenshot` blocks `click` or Enter `key_chord` (single source of truth). -const STALE_CAPTURE_TOOL_MESSAGE: &str = "Computer use refused: call **`screenshot`** first. Use a **bare** `screenshot` (do not set `screenshot_reset_navigation`) — the host applies a **~500×500** crop around the **mouse**. Before Return/Enter in a focused text field, set **`screenshot_implicit_center`**: **`text_caret`**. This is required after the pointer moved since the last capture, before **`click`** or before **`key_chord`** that includes Return/Enter."; - -/// Relative nudges (`pointer_move_rel`, `ComputerUseMouseStep`) right after a model-driven screenshot are almost always wrong when deltas are guessed from the image; block until a trusted absolute move. -const VISION_PIXEL_NUDGE_AFTER_SCREENSHOT_MSG: &str = "Computer use refused: do not use `pointer_move_rel` or `ComputerUseMouseStep` immediately after a `screenshot` — nudging from the JPEG is inaccurate. First reposition with `move_to_text`, `click_element`, `locate` + `mouse_move` (`use_screen_coordinates`: true), or `mouse_move` using globals from tool JSON; then relative nudges are allowed if still needed."; - -#[derive(Debug, Clone)] -struct ScreenshotCacheEntry { - rgba: image::RgbaImage, - screen: Screen, - capture_time: Instant, -} - -#[derive(Debug)] -struct PointerPixmapCache { - w: u32, - h: u32, - /// Premultiplied RGBA8 (`tiny-skya` / `resvg` format). - rgba: Vec, -} - -static POINTER_PIXMAP_CACHE: OnceLock> = OnceLock::new(); -static SCREENSHOT_ID_COUNTER: AtomicU64 = AtomicU64::new(1); - -fn pointer_pixmap_cache() -> Option<&'static PointerPixmapCache> { - POINTER_PIXMAP_CACHE - .get_or_init( - || match rasterize_pointer_svg(POINTER_OVERLAY_SVG, 0.3375) { - Ok(p) => Some(p), - Err(e) => { - warn!( - "computer_use: pointer SVG rasterize failed ({}); using fallback cross", - e - ); - None - } - }, - ) - .as_ref() -} - -fn rasterize_pointer_svg(svg: &str, scale: f32) -> Result { - let opt = usvg::Options::default(); - let tree = usvg::Tree::from_str(svg, &opt).map_err(|e| e.to_string())?; - let size = tree.size(); - let w = ((size.width() * scale).ceil() as u32).max(1); - let h = ((size.height() * scale).ceil() as u32).max(1); - let mut pixmap = Pixmap::new(w, h).ok_or_else(|| "pixmap allocation failed".to_string())?; - resvg::render( - &tree, - Transform::from_scale(scale, scale), - &mut pixmap.as_mut(), - ); - Ok(PointerPixmapCache { - w, - h, - rgba: pixmap.data().to_vec(), - }) -} - -/// Alpha-composite premultiplied RGBA onto `img` with SVG (0,0) at `(cx, cy)`. -fn blend_pointer_pixmap(img: &mut RgbImage, cx: i32, cy: i32, p: &PointerPixmapCache) { - let iw = img.width() as i32; - let ih = img.height() as i32; - for row in 0..p.h { - for col in 0..p.w { - let i = ((row * p.w + col) * 4) as usize; - if i + 3 >= p.rgba.len() { - break; - } - let pr = p.rgba[i]; - let pg = p.rgba[i + 1]; - let pb = p.rgba[i + 2]; - let pa = p.rgba[i + 3] as u32; - if pa == 0 { - continue; - } - let px = cx + col as i32; - let py = cy + row as i32; - if px < 0 || py < 0 || px >= iw || py >= ih { - continue; - } - let dst = img.get_pixel(px as u32, py as u32); - let inv = 255 - pa; - let nr = (pr as u32 + dst[0] as u32 * inv / 255).min(255) as u8; - let ng = (pg as u32 + dst[1] as u32 * inv / 255).min(255) as u8; - let nb = (pb as u32 + dst[2] as u32 * inv / 255).min(255) as u8; - img.put_pixel(px as u32, py as u32, Rgb([nr, ng, nb])); - } - } -} - -#[cfg(test)] -mod visual_grid_tests { - use super::*; - - #[test] - fn detects_regular_grid_rect_from_synthetic_screenshot() { - let mut img = RgbImage::from_pixel(420, 360, Rgb([245, 245, 245])); - let left = 60u32; - let top = 40u32; - let size = 280u32; - for i in 0..15u32 { - let pos = i * (size - 1) / 14; - for d in 0..2 { - let x = left + pos + d; - if x < left + size { - for y in top..top + size { - img.put_pixel(x, y, Rgb([25, 25, 25])); - } - } - let y = top + pos + d; - if y < top + size { - for x in left..left + size { - img.put_pixel(x, y, Rgb([25, 25, 25])); - } - } - } - } - - let mut bytes = Vec::new(); - JpegEncoder::new_with_quality(&mut bytes, 92) - .encode_image(&DynamicImage::ImageRgb8(img)) - .expect("encode synthetic grid"); - let shot = ComputerScreenshot { - screenshot_id: Some("test-shot".to_string()), - bytes, - mime_type: "image/jpeg".to_string(), - image_width: 420, - image_height: 360, - native_width: 420, - native_height: 360, - display_origin_x: 0, - display_origin_y: 0, - vision_scale: 1.0, - pointer_image_x: None, - pointer_image_y: None, - screenshot_crop_center: None, - point_crop_half_extent_native: None, - navigation_native_rect: None, - quadrant_navigation_click_ready: false, - image_content_rect: Some(ComputerUseImageContentRect { - left: 0, - top: 0, - width: 420, - height: 360, - }), - image_global_bounds: None, - ui_tree_text: None, - implicit_confirmation_crop_applied: false, - }; - - let (x0, y0, width, height) = - detect_regular_grid_rect_from_screenshot(&shot, 15, 15).expect("detect grid"); - assert!((x0 - left as i32).abs() <= 6, "x0={x0}"); - assert!((y0 - top as i32).abs() <= 6, "y0={y0}"); - assert!((width as i32 - size as i32).abs() <= 12, "width={width}"); - assert!((height as i32 - size as i32).abs() <= 12, "height={height}"); - } -} - -fn draw_pointer_fallback_cross(img: &mut RgbImage, cx: i32, cy: i32) { - const ARM: i32 = 2; - const OUTLINE: Rgb = Rgb([255, 255, 255]); - const CORE: Rgb = Rgb([40, 40, 48]); - let w = img.width() as i32; - let h = img.height() as i32; - let mut plot = |x: i32, y: i32, c: Rgb| { - if x >= 0 && x < w && y >= 0 && y < h { - img.put_pixel(x as u32, y as u32, c); - } - }; - for t in -ARM..=ARM { - for k in -1..=1 { - plot(cx + t, cy + k, OUTLINE); - plot(cx + k, cy + t, OUTLINE); - } - } - for t in -ARM..=ARM { - plot(cx + t, cy, CORE); - plot(cx, cy + t, CORE); - } -} - -/// Returns the capture bitmap unchanged (no grid, rulers, or margins). Pointer overlays are applied later. -fn compose_computer_use_frame( - content: RgbImage, - _ruler_origin_x: u32, - _ruler_origin_y: u32, -) -> (RgbImage, u32, u32) { - (content, 0, 0) -} - -#[allow(dead_code)] // legacy: crop logic disabled at the entry point in screenshot_display -fn implicit_confirmation_should_apply( - click_needs: bool, - params: &ComputerUseScreenshotParams, -) -> bool { - // Applies on **every** bare `screenshot` while confirmation is required — including the - // first capture in a session (`last_shot_refinement` may still be `None`), so click/Enter - // guards get a ~500×500 around the mouse (or `text_caret` when requested) instead of full screen. - // - // **Always** apply when `click_needs` (even during quadrant/point-crop drill): previously we - // skipped implicit crop while `navigation_focus` was Quadrant/PointCrop, which produced large - // confirmation JPEGs; confirmation shots must stay ~500×500 around the pointer/caret. - if !click_needs { - return false; - } - if params.crop_center.is_some() || params.navigate_quadrant.is_some() || params.reset_navigation - { - return false; - } - true -} - -fn global_to_native_full_pixel_center( - gx: f64, - gy: f64, - native_w: u32, - native_h: u32, - d: &DisplayInfo, -) -> (u32, u32) { - #[cfg(target_os = "macos")] - { - let geo = MacPointerGeo::from_display(native_w, native_h, d); - let lx = gx - geo.disp_ox; - let ly = gy - geo.disp_oy; - if lx < 0.0 || lx >= geo.disp_w || ly < 0.0 || ly >= geo.disp_h { - return clamp_center_to_native(native_w / 2, native_h / 2, native_w, native_h); - } - let full_ix = ((lx / geo.disp_w) * geo.full_px_w as f64).floor() as u32; - let full_iy = ((ly / geo.disp_h) * geo.full_px_h as f64).floor() as u32; - clamp_center_to_native(full_ix, full_iy, native_w, native_h) - } - #[cfg(not(target_os = "macos"))] - { - let disp_w = d.width as f64; - let disp_h = d.height as f64; - if disp_w <= 0.0 || disp_h <= 0.0 || native_w == 0 || native_h == 0 { - return (0, 0); - } - let lx = gx - d.x as f64; - let ly = gy - d.y as f64; - if lx < 0.0 || lx >= disp_w || ly < 0.0 || ly >= disp_h { - return clamp_center_to_native(native_w / 2, native_h / 2, native_w, native_h); - } - let full_ix = ((lx / disp_w) * native_w as f64).floor() as u32; - let full_iy = ((ly / disp_h) * native_h as f64).floor() as u32; - clamp_center_to_native(full_ix, full_iy, native_w, native_h) - } -} - -#[cfg(target_os = "macos")] -#[allow(dead_code)] -fn implicit_global_center_for_confirmation( - center: ComputerUseImplicitScreenshotCenter, - mx: f64, - my: f64, -) -> (f64, f64) { - match center { - ComputerUseImplicitScreenshotCenter::Mouse => (mx, my), - ComputerUseImplicitScreenshotCenter::TextCaret => { - crate::computer_use::macos_ax_ui::global_point_for_text_caret_screenshot(mx, my) - } - } -} - -#[cfg(not(target_os = "macos"))] -#[allow(dead_code)] -fn implicit_global_center_for_confirmation( - center: ComputerUseImplicitScreenshotCenter, - mx: f64, - my: f64, -) -> (f64, f64) { - let _ = center; - (mx, my) -} - -/// JPEG quality for computer-use screenshots. Visually near-lossless tier; combined with the -/// adaptive byte-budget downscale below, oversize captures are halved until they fit -/// [`SCREENSHOT_MAX_BYTES`] so the model API receives a manageable payload without sacrificing -/// quality on small/medium app windows. -const JPEG_QUALITY: u8 = 85; - -/// Soft byte budget for a single screenshot JPEG sent to the model. When the encoded image -/// exceeds this, the host halves the resolution (Lanczos3) and re-encodes, looping until it fits -/// or the long edge falls below [`SCREENSHOT_MIN_LONG_EDGE`]. -const SCREENSHOT_MAX_BYTES: usize = 3 * 1024 * 1024; - -/// Hard floor on the long edge during the byte-budget downscale loop, so a pathological -/// capture cannot be reduced to an unreadable thumbnail just to fit the budget. -const SCREENSHOT_MIN_LONG_EDGE: u32 = 512; - -#[inline] -fn clamp_center_to_native(cx: u32, cy: u32, nw: u32, nh: u32) -> (u32, u32) { - if nw == 0 || nh == 0 { - return (0, 0); - } - let cx = cx.min(nw - 1); - let cy = cy.min(nh - 1); - (cx, cy) -} - -/// Top-left and size of the native crop rectangle around `(cx, cy)`, clamped to the bitmap. -/// `half_px` is the distance from center to each edge (see [`clamp_point_crop_half_extent`]). -fn crop_rect_around_point_native( - cx: u32, - cy: u32, - nw: u32, - nh: u32, - half_px: u32, -) -> (u32, u32, u32, u32) { - let (cx, cy) = clamp_center_to_native(cx, cy, nw, nh); - if nw == 0 || nh == 0 { - return (0, 0, 1, 1); - } - let edge = half_px.saturating_mul(2); - let tw = edge.min(nw).max(1); - let th = edge.min(nh).max(1); - let mut x0 = cx.saturating_sub(half_px); - let mut y0 = cy.saturating_sub(half_px); - if x0.saturating_add(tw) > nw { - x0 = nw.saturating_sub(tw); - } - if y0.saturating_add(th) > nh { - y0 = nh.saturating_sub(th); - } - (x0, y0, tw, th) -} - -#[inline] -fn full_navigation_rect(nw: u32, nh: u32) -> ComputerUseNavigationRect { - ComputerUseNavigationRect { - x0: 0, - y0: 0, - width: nw.max(1), - height: nh.max(1), - } -} - -fn intersect_navigation_rect( - a: ComputerUseNavigationRect, - b: ComputerUseNavigationRect, -) -> Option { - let ax1 = a.x0.saturating_add(a.width); - let ay1 = a.y0.saturating_add(a.height); - let bx1 = b.x0.saturating_add(b.width); - let by1 = b.y0.saturating_add(b.height); - let x0 = a.x0.max(b.x0); - let y0 = a.y0.max(b.y0); - let x1 = ax1.min(bx1); - let y1 = ay1.min(by1); - if x0 >= x1 || y0 >= y1 { - return None; - } - Some(ComputerUseNavigationRect { - x0, - y0, - width: x1 - x0, - height: y1 - y0, - }) -} - -/// Expand `r` by `pad` pixels left/up/right/down, clamped to `0..max_w` × `0..max_h`. -fn expand_navigation_rect_edges( - r: ComputerUseNavigationRect, - pad: u32, - max_w: u32, - max_h: u32, -) -> ComputerUseNavigationRect { - let x0 = r.x0.saturating_sub(pad); - let y0 = r.y0.saturating_sub(pad); - let x1 = r.x0.saturating_add(r.width).saturating_add(pad).min(max_w); - let y1 = r.y0.saturating_add(r.height).saturating_add(pad).min(max_h); - let width = x1.saturating_sub(x0).max(1); - let height = y1.saturating_sub(y0).max(1); - ComputerUseNavigationRect { - x0, - y0, - width, - height, - } -} - -fn quadrant_split_rect( - r: ComputerUseNavigationRect, - q: ComputerUseNavigateQuadrant, -) -> ComputerUseNavigationRect { - let hw = r.width / 2; - let hh = r.height / 2; - let rw = r.width - hw; - let rh = r.height - hh; - match q { - ComputerUseNavigateQuadrant::TopLeft => ComputerUseNavigationRect { - x0: r.x0, - y0: r.y0, - width: hw, - height: hh, - }, - ComputerUseNavigateQuadrant::TopRight => ComputerUseNavigationRect { - x0: r.x0 + hw, - y0: r.y0, - width: rw, - height: hh, - }, - ComputerUseNavigateQuadrant::BottomLeft => ComputerUseNavigationRect { - x0: r.x0, - y0: r.y0 + hh, - width: hw, - height: rh, - }, - ComputerUseNavigateQuadrant::BottomRight => ComputerUseNavigationRect { - x0: r.x0 + hw, - y0: r.y0 + hh, - width: rw, - height: rh, - }, - } -} - -/// macOS: map JPEG/bitmap pixels to/from **CoreGraphics global display coordinates** (same as -/// `CGDisplayBounds` / `CGEventGetLocation`): origin at the **top-left of the main display**, Y -/// increases **downward**. Not AppKit bottom-left / Y-up. -#[cfg(target_os = "macos")] -#[derive(Clone, Copy, Debug)] -struct MacPointerGeo { - disp_ox: f64, - disp_oy: f64, - disp_w: f64, - disp_h: f64, - full_px_w: u32, - full_px_h: u32, - crop_x0: u32, - crop_y0: u32, -} - -#[cfg(target_os = "macos")] -impl MacPointerGeo { - fn from_display(full_w: u32, full_h: u32, d: &DisplayInfo) -> Self { - Self { - disp_ox: d.x as f64, - disp_oy: d.y as f64, - disp_w: d.width as f64, - disp_h: d.height as f64, - full_px_w: full_w, - full_px_h: full_h, - crop_x0: 0, - crop_y0: 0, - } - } - - fn with_crop(mut self, x0: u32, y0: u32) -> Self { - self.crop_x0 = x0; - self.crop_y0 = y0; - self - } - - /// Map **continuous** framebuffer pixel center `(cx, cy)` (0.5 = middle of left/top pixel) to CG global. - fn full_pixel_center_to_global_f64(&self, cx: f64, cy: f64) -> BitFunResult<(f64, f64)> { - if self.disp_w <= 0.0 || self.disp_h <= 0.0 || self.full_px_w == 0 || self.full_px_h == 0 { - return Err(BitFunError::tool( - "Invalid macOS pointer geometry.".to_string(), - )); - } - let px_w = self.full_px_w as f64; - let px_h = self.full_px_h as f64; - let max_cx = (self.full_px_w.saturating_sub(1) as f64) + 0.5; - let max_cy = (self.full_px_h.saturating_sub(1) as f64) + 0.5; - let cx = cx.clamp(0.5, max_cx); - let cy = cy.clamp(0.5, max_cy); - let gx = self.disp_ox + (cx / px_w) * self.disp_w; - let gy = self.disp_oy + (cy / px_h) * self.disp_h; - Ok((gx, gy)) - } - - /// `CGEventGetLocation` global mouse -> full-buffer pixel; then optional crop to view. - fn global_to_view_pixel( - &self, - mx: f64, - my: f64, - view_w: u32, - view_h: u32, - ) -> Option<(i32, i32)> { - if self.disp_w <= 0.0 || self.disp_h <= 0.0 || self.full_px_w == 0 || self.full_px_h == 0 { - return None; - } - let lx = mx - self.disp_ox; - let ly = my - self.disp_oy; - if lx < 0.0 || lx >= self.disp_w || ly < 0.0 || ly >= self.disp_h { - return None; - } - let full_ix = ((lx / self.disp_w) * self.full_px_w as f64).floor() as i32; - let full_iy = ((ly / self.disp_h) * self.full_px_h as f64).floor() as i32; - let full_ix = full_ix.clamp(0, self.full_px_w.saturating_sub(1) as i32); - let full_iy = full_iy.clamp(0, self.full_px_h.saturating_sub(1) as i32); - let vx = full_ix - self.crop_x0 as i32; - let vy = full_iy - self.crop_y0 as i32; - if vx >= 0 && vy >= 0 && (vx as u32) < view_w && (vy as u32) < view_h { - Some((vx, vy)) - } else { - None - } - } -} - -#[derive(Clone, Copy, Debug)] -struct PointerMap { - /// Screenshot JPEG width/height (same as capture when there is no frame padding). - image_w: u32, - image_h: u32, - /// Top-left of capture inside the JPEG (0 when there is no padding). - content_origin_x: u32, - content_origin_y: u32, - /// Native capture pixel size (the cropped/visible bitmap). - content_w: u32, - content_h: u32, - native_w: u32, - native_h: u32, - origin_x: i32, - origin_y: i32, - #[cfg(target_os = "macos")] - macos_geo: Option, -} - -impl PointerMap { - /// Continuous mapping: **composed JPEG** pixel `(x,y)` -> global (macOS CG). - fn map_image_to_global_f64(&self, x: i32, y: i32) -> BitFunResult<(f64, f64)> { - if self.image_w == 0 - || self.image_h == 0 - || self.content_w == 0 - || self.content_h == 0 - || self.native_w == 0 - || self.native_h == 0 - { - return Err(BitFunError::tool( - "Invalid screenshot coordinate map (zero dimension).".to_string(), - )); - } - let ox = self.content_origin_x as i32; - let oy = self.content_origin_y as i32; - let cx_img = x - ox; - let cy_img = y - oy; - let max_cx = self.content_w.saturating_sub(1) as i32; - let max_cy = self.content_h.saturating_sub(1) as i32; - let cx_img = cx_img.clamp(0, max_cx) as f64; - let cy_img = cy_img.clamp(0, max_cy) as f64; - let cw = self.content_w as f64; - let ch = self.content_h as f64; - let nw = self.native_w as f64; - let nh = self.native_h as f64; - - #[cfg(target_os = "macos")] - if let Some(g) = self.macos_geo { - let cx = g.crop_x0 as f64 + (cx_img + 0.5) * nw / cw; - let cy = g.crop_y0 as f64 + (cy_img + 0.5) * nh / ch; - return g.full_pixel_center_to_global_f64(cx, cy); - } - - let center_full_x = self.origin_x as f64 + (cx_img + 0.5) * nw / cw; - let center_full_y = self.origin_y as f64 + (cy_img + 0.5) * nh / ch; - Ok((center_full_x, center_full_y)) - } - - /// Normalized 0..=1000 maps to the **capture** bitmap. - fn map_normalized_to_global_f64(&self, x: i32, y: i32) -> BitFunResult<(f64, f64)> { - if self.native_w == 0 || self.native_h == 0 { - return Err(BitFunError::tool( - "Invalid screenshot coordinate map (zero native dimension).".to_string(), - )); - } - let nw = self.native_w as f64; - let nh = self.native_h as f64; - let tx = (x.clamp(0, 1000) as f64) / 1000.0; - let ty = (y.clamp(0, 1000) as f64) / 1000.0; - - #[cfg(target_os = "macos")] - if let Some(g) = self.macos_geo { - let cx = g.crop_x0 as f64 + tx * (nw - 1.0).max(0.0) + 0.5; - let cy = g.crop_y0 as f64 + ty * (nh - 1.0).max(0.0) + 0.5; - return g.full_pixel_center_to_global_f64(cx, cy); - } - - let gx = self.origin_x as f64 + tx * (nw - 1.0).max(0.0) + 0.5; - let gy = self.origin_y as f64 + ty * (nh - 1.0).max(0.0) + 0.5; - Ok((gx, gy)) - } - - fn image_global_bounds(&self) -> Option { - if self.image_w == 0 || self.image_h == 0 { - return None; - } - let (x0, y0) = self.map_image_to_global_f64(0, 0).ok()?; - let (x1, y1) = self - .map_image_to_global_f64( - self.image_w.saturating_sub(1) as i32, - self.image_h.saturating_sub(1) as i32, - ) - .ok()?; - Some(ComputerUseImageGlobalBounds { - left: x0.min(x1), - top: y0.min(y1), - width: (x1 - x0).abs(), - height: (y1 - y0).abs(), - }) - } -} - -/// What the last tool `screenshot` implied for **plain** follow-up captures (no crop / no `navigate_quadrant`). -/// **PointCrop** is not reused for plain refresh: the next bare `screenshot` shows the **full display** again so -/// "full" is never stuck at ~500×500 after a point crop. **Quadrant** plain refresh keeps the current drill tile. -#[derive(Clone, Copy, Debug, PartialEq, Eq)] -enum ComputerUseNavFocus { - FullDisplay, - Quadrant { rect: ComputerUseNavigationRect }, - PointCrop { rect: ComputerUseNavigationRect }, -} - -/// Unified mutable session state for computer use — one mutex instead of five. -/// State transitions are applied centrally after each action (screenshot, pointer move, click, etc.). -#[derive(Debug)] -struct ComputerUseSessionMutableState { - pointer_map: Option, - /// When true, a fresh `screenshot_display` is required before `click` and before `key_chord` that sends Return/Enter - /// (set after pointer moves / click; cleared after screenshot). - click_needs_fresh_screenshot: bool, - /// Last `screenshot_display` scope (full screen vs point crop) for tool hints and click rules. - last_shot_refinement: Option, - /// Drill / crop context for the next `screenshot` (see [`ComputerUseNavFocus`]). - navigation_focus: Option, - /// Cached full-screen screenshot for fast consecutive crops. - screenshot_cache: Option, - /// After `screenshot`, block `pointer_move_rel` / `ComputerUseMouseStep` until an absolute move - /// from AX/OCR/globals (`mouse_move`, `move_to_text`, `click_element`) clears this. - block_vision_pixel_nudge_after_screenshot: bool, - /// After click / key / type / scroll / drag: recommend a **`screenshot`** to confirm UI state (Cowork verify). - /// Cleared on the next successful `screenshot_display`. - pending_verify_screenshot: bool, - /// After `move_to_text` (global OCR coordinates): next guarded **`click`** may run without a prior - /// `screenshot_display` / fine-crop basis — same idea as `click_element` relaxed guard. - pointer_trusted_after_ocr_move: bool, - /// Action optimizer for loop detection, history, and visual verification. - optimizer: ComputerUseOptimizer, - /// Most-recent action **kind** that mutated UI / pointer state. Surfaced - /// to the model via `interaction_state.last_mutation` so it can pair the - /// right verification step (e.g. after `Click` + `pending_verify` ⇒ take - /// a confirming `screenshot`; after `TypeText` ⇒ may chain Enter without - /// re-screenshotting because typing does not move the pointer). - last_mutation_kind: Option, - /// Caller-pinned target display (set via `desktop.focus_display`). - /// When set, all subsequent screenshots / peeks / locates use this - /// display instead of "screen under the mouse pointer". The model - /// uses this to disambiguate multi-monitor targets explicitly. - preferred_display_id: Option, - /// Most-recent Set-of-Mark interactive view per pid. Used to resolve - /// `interactive_*` numeric `i` indices back to AX node indices and to - /// detect stale-view usage via `before_view_digest`. - interactive_view_cache: std::collections::HashMap, - visual_mark_cache: std::collections::HashMap, - /// Most-recent focused-window screenshot coordinate map per application - /// pid. `app_click(target: image_xy | image_grid)` must use the same - /// image basis the model saw from `get_app_state`, not whichever global - /// computer-use screenshot happened to run last. - app_pointer_maps: std::collections::HashMap, - /// Exact screenshot-id keyed coordinate maps. This is the strongest - /// addressing basis for arbitrary visual targets because it survives - /// interleaved app_state / screenshot / interactive_view calls. - screenshot_pointer_maps: std::collections::HashMap, -} - -#[derive(Debug, Clone)] -struct CachedInteractiveView { - digest: String, - /// `i` → `node_idx` map (dense, indexed by `i`). - elements: Vec, -} - -#[derive(Debug, Clone)] -struct CachedVisualMarkView { - digest: String, - marks: Vec, - screenshot_id: Option, -} - -impl ComputerUseSessionMutableState { - fn new() -> Self { - Self { - pointer_map: None, - click_needs_fresh_screenshot: true, - last_shot_refinement: None, - navigation_focus: None, - screenshot_cache: None, - block_vision_pixel_nudge_after_screenshot: false, - pending_verify_screenshot: false, - pointer_trusted_after_ocr_move: false, - optimizer: ComputerUseOptimizer::new(), - last_mutation_kind: None, - preferred_display_id: None, - interactive_view_cache: std::collections::HashMap::new(), - visual_mark_cache: std::collections::HashMap::new(), - app_pointer_maps: std::collections::HashMap::new(), - screenshot_pointer_maps: std::collections::HashMap::new(), - } - } - - /// Called after a successful screenshot capture. - fn transition_after_screenshot( - &mut self, - map: PointerMap, - refinement: ComputerUseScreenshotRefinement, - nav_focus: Option, - ) { - self.pointer_map = Some(map); - self.last_shot_refinement = Some(refinement); - self.navigation_focus = nav_focus; - self.click_needs_fresh_screenshot = false; - self.pending_verify_screenshot = false; - self.pointer_trusted_after_ocr_move = false; - self.block_vision_pixel_nudge_after_screenshot = true; - self.last_mutation_kind = Some(ComputerUseLastMutationKind::Screenshot); - } - - /// Called after pointer mutation (move, step, relative), click, scroll, key_chord, or type_text. - fn transition_after_pointer_mutation(&mut self) { - self.click_needs_fresh_screenshot = true; - self.pointer_trusted_after_ocr_move = false; - // Note: `last_mutation_kind` is set explicitly by the calling - // action (PointerMove / Click / Scroll / KeyChord / TypeText / Drag) - // so we do not overwrite it here with a generic value. - } - - /// Called after click (same effect as pointer mutation for freshness). - fn transition_after_click(&mut self) { - self.click_needs_fresh_screenshot = true; - self.pending_verify_screenshot = true; - self.pointer_trusted_after_ocr_move = false; - self.last_mutation_kind = Some(ComputerUseLastMutationKind::Click); - } - - /// Called after key, typing, scroll, or drag — UI likely changed; next `screenshot` should confirm. - fn transition_after_committed_ui_action(&mut self) { - self.pending_verify_screenshot = true; - } - - fn record_mutation(&mut self, kind: ComputerUseLastMutationKind) { - self.last_mutation_kind = Some(kind); - } -} - -pub struct DesktopComputerUseHost { - state: Mutex, -} - -impl std::fmt::Debug for DesktopComputerUseHost { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - f.debug_struct("DesktopComputerUseHost") - .finish_non_exhaustive() - } -} - -impl Default for DesktopComputerUseHost { - fn default() -> Self { - Self::new() - } -} - -impl DesktopComputerUseHost { - pub fn new() -> Self { - Self { - state: Mutex::new(ComputerUseSessionMutableState::new()), - } - } - - pub fn prompt_for_missing_permissions(&self) { - self.run_background_input_self_check(); - } - - fn next_screenshot_id() -> String { - let seq = SCREENSHOT_ID_COUNTER.fetch_add(1, Ordering::Relaxed); - let ms = std::time::SystemTime::now() - .duration_since(std::time::UNIX_EPOCH) - .map(|d| d.as_millis()) - .unwrap_or(0); - format!("shot_{}_{}", ms, seq) - } - - /// Codex-style startup probe: log whether AX/background-input capabilities - /// are available so operators can diagnose missing permissions early. - /// - /// Behaviour parity with Codex: if the process is NOT yet - /// Accessibility-trusted, immediately call - /// `AXIsProcessTrustedWithOptions({kAXTrustedCheckOptionPrompt: true})` - /// once. macOS responds by surfacing the system-modal "允许 X 通过辅助功能 - /// 控制您的电脑" dialog (deep-linked to System Settings → Privacy & Security - /// → Accessibility). Without this call, the OS NEVER prompts and AX tree - /// reads against other apps return only the top-level window structure - /// (root window + a few descendants) — which is exactly the "shallow tree - /// / agent goes blind" symptom we observed against the BitFun WebView. - fn run_background_input_self_check(&self) { - #[cfg(target_os = "macos")] - { - let bg_ok = crate::computer_use::macos_bg_input::supports_background_input(); - if bg_ok { - log::info!( - "AX-first computer use ready: AXIsProcessTrustedWithOptions=true; CGEventPostToPid background input enabled" - ); - } else { - log::warn!( - "AX-first computer use disabled: process is NOT marked Accessibility-trusted. Triggering one-shot system prompt via AXIsProcessTrustedWithOptions(prompt:true) so macOS surfaces the Accessibility permission dialog (deep-link: System Settings → Privacy & Security → Accessibility)." - ); - // Fire-and-forget. The dialog is async and modal at the macOS - // level; we do not block startup waiting for the user to - // approve. The next CU invocation will simply succeed once - // permission lands. Subsequent BitFun launches skip the - // prompt because `ax_trusted()` will already be true. - macos::request_ax_prompt(); - } - // Same idea for Screen Recording. Without it, focused-window - // screenshots fall back to a desktop-wallpaper placeholder, which - // is the second half of the "blind agent" failure mode. - if !macos::screen_capture_preflight() { - log::warn!( - "Screen Recording permission missing; window screenshots will be incomplete. Triggering CGRequestScreenCaptureAccess() to surface the system prompt." - ); - let _ = macos::request_screen_capture(); - } - } - #[cfg(not(target_os = "macos"))] - { - log::info!( - "AX-first background input is macOS-only in this build; legacy screen-coordinate desktop actions remain available" - ); - } - } - - fn clear_vision_pixel_nudge_block(&self) { - if let Ok(mut s) = self.state.lock() { - s.block_vision_pixel_nudge_after_screenshot = false; - } - } - - /// Best-effort foreground app + pointer; safe to call from `spawn_blocking`. - fn collect_session_snapshot_sync() -> ComputerUseSessionSnapshot { - #[cfg(target_os = "macos")] - { - return Self::session_snapshot_macos(); - } - #[cfg(target_os = "windows")] - { - Self::session_snapshot_windows() - } - #[cfg(target_os = "linux")] - { - return Self::session_snapshot_linux(); - } - #[cfg(not(any(target_os = "macos", target_os = "windows", target_os = "linux")))] - { - ComputerUseSessionSnapshot::default() - } - } - - #[cfg(target_os = "macos")] - fn session_snapshot_macos() -> ComputerUseSessionSnapshot { - let pointer = macos::quartz_mouse_location() - .ok() - .map(|(x, y)| ComputerUsePointerGlobal { x, y }); - let foreground = Self::macos_foreground_application(); - ComputerUseSessionSnapshot { - foreground_application: foreground, - pointer_global: pointer, - } - } - - #[cfg(target_os = "macos")] - fn macos_foreground_application() -> Option { - let out = std::process::Command::new("/usr/bin/osascript") - .args(["-e", r#"tell application "System Events" - set p to first process whose frontmost is true - return (unix id of p as text) & "|" & (name of p) & "|" & (try (bundle identifier of p as text) on error "" end try) -end tell"#]) - .output() - .ok()?; - if !out.status.success() { - return None; - } - let s = String::from_utf8_lossy(&out.stdout); - let parts: Vec<&str> = s.trim().splitn(3, '|').collect(); - if parts.len() < 2 { - return None; - } - let pid = parts[0].trim().parse::().ok()?; - let name = parts[1].trim(); - let bundle = parts.get(2).map(|x| x.trim()).filter(|x| !x.is_empty()); - Some(ComputerUseForegroundApplication { - name: Some(name.to_string()), - bundle_id: bundle.map(|b| b.to_string()), - process_id: Some(pid), - }) - } - - #[cfg(target_os = "windows")] - fn session_snapshot_windows() -> ComputerUseSessionSnapshot { - use windows::Win32::Foundation::POINT; - use windows::Win32::UI::WindowsAndMessaging::{ - GetCursorPos, GetForegroundWindow, GetWindowTextW, GetWindowThreadProcessId, - }; - - unsafe { - let mut pt = POINT::default(); - let pointer = if GetCursorPos(&mut pt).is_ok() { - Some(ComputerUsePointerGlobal { - x: pt.x as f64, - y: pt.y as f64, - }) - } else { - None - }; - - let hwnd = GetForegroundWindow(); - let foreground = if hwnd.is_invalid() { - None - } else { - let mut pid: u32 = 0; - GetWindowThreadProcessId(hwnd, Some(&mut pid)); - let mut buf = [0u16; 512]; - let n = GetWindowTextW(hwnd, &mut buf) as usize; - let title = if n > 0 { - String::from_utf16_lossy(&buf[..n.min(512)]) - } else { - String::new() - }; - Some(ComputerUseForegroundApplication { - name: if title.is_empty() { None } else { Some(title) }, - bundle_id: None, - process_id: Some(pid as i32), - }) - }; - - ComputerUseSessionSnapshot { - foreground_application: foreground, - pointer_global: pointer, - } - } - } - - #[cfg(target_os = "linux")] - fn session_snapshot_linux() -> ComputerUseSessionSnapshot { - // Best-effort: no standard API across Wayland/X11 without extra deps. - ComputerUseSessionSnapshot::default() - } - - fn refinement_from_shot(shot: &ComputerScreenshot) -> ComputerUseScreenshotRefinement { - use ComputerUseScreenshotRefinement as R; - if let Some(c) = shot.screenshot_crop_center { - return R::RegionAroundPoint { - center_x: c.x, - center_y: c.y, - }; - } - let Some(nav) = shot.navigation_native_rect else { - return R::FullDisplay; - }; - let full = nav.x0 == 0 - && nav.y0 == 0 - && nav.width == shot.native_width - && nav.height == shot.native_height; - if full { - R::FullDisplay - } else { - R::QuadrantNavigation { - x0: nav.x0, - y0: nav.y0, - width: nav.width, - height: nav.height, - click_ready: shot.quadrant_navigation_click_ready, - } - } - } - - fn ensure_input_automation_allowed() -> BitFunResult<()> { - #[cfg(target_os = "macos")] - { - if macos::ax_trusted() { - return Ok(()); - } - let exe = std::env::current_exe() - .map(|p| p.display().to_string()) - .unwrap_or_else(|_| "(unknown path)".to_string()); - return Err(BitFunError::tool(format!( - "macOS Accessibility is not enabled for this executable. System Settings > Privacy & Security > Accessibility: add and enable BitFun. Development builds use the debug binary at: {}", - exe - ))); - } - #[cfg(not(target_os = "macos"))] - { - Ok(()) - } - } - - fn with_enigo(f: F) -> BitFunResult - where - F: FnOnce(&mut Enigo) -> BitFunResult, - { - Self::ensure_input_automation_allowed()?; - let settings = Settings::default(); - let mut enigo = - Enigo::new(&settings).map_err(|e| BitFunError::tool(format!("enigo init: {}", e)))?; - f(&mut enigo) - } - - /// Enigo on macOS uses Text Input Source / AppKit paths that must run on the main queue. - /// Tokio `spawn_blocking` threads are not main; dispatch there hits `dispatch_assert_queue_fail`. - /// - /// On macOS, the main-queue dispatch is also wrapped in an Objective-C - /// `@try/@catch` (via `objc2::exception::catch`) so that an `NSException` - /// thrown by TSM / HIToolbox / AppKit during keyboard or text input is - /// converted into a Rust error instead of propagating across the FFI - /// boundary as a "foreign exception" — which would otherwise cause Rust's - /// `catch_unwind` to abort the whole process (`SIGABRT`). - fn run_enigo_job(job: F) -> BitFunResult - where - F: FnOnce(&mut Enigo) -> BitFunResult + Send, - T: Send, - { - #[cfg(target_os = "macos")] - { - macos::run_on_main_for_enigo(move || Self::with_enigo(job)) - } - #[cfg(not(target_os = "macos"))] - { - Self::with_enigo(job) - } - } - - /// Absolute pointer move in Quartz global **points** with full float precision (avoids enigo integer truncation). - #[cfg(target_os = "macos")] - fn post_mouse_moved_cg_global(x: f64, y: f64) -> BitFunResult<()> { - use core_graphics::event::{CGEvent, CGEventTapLocation, CGEventType, CGMouseButton}; - use core_graphics::event_source::{CGEventSource, CGEventSourceStateID}; - use core_graphics::geometry::CGPoint; - - let source = - CGEventSource::new(CGEventSourceStateID::CombinedSessionState).map_err(|_| { - BitFunError::tool("CGEventSource create failed (mouse_move)".to_string()) - })?; - let pt = CGPoint { x, y }; - let ev = CGEvent::new_mouse_event(source, CGEventType::MouseMoved, pt, CGMouseButton::Left) - .map_err(|_| BitFunError::tool("CGEvent MouseMoved failed".to_string()))?; - ev.post(CGEventTapLocation::HID); - Ok(()) - } - - /// Ease 0..1 for pointer paths (smooth acceleration/deceleration). - fn smoothstep01(t: f64) -> f64 { - let t = t.clamp(0.0, 1.0); - t * t * (3.0 - 2.0 * t) - } - - /// Move the pointer along a short visible path instead of warping in one event. - #[cfg(target_os = "macos")] - fn smooth_mouse_move_cg_global(x1: f64, y1: f64) -> BitFunResult<()> { - const MIN_DIST: f64 = 2.5; - const MIN_STEPS: usize = 8; - const MAX_STEPS: usize = 85; - const MAX_DURATION_MS: u64 = 400; - - let (x0, y0) = macos::quartz_mouse_location().unwrap_or((x1, y1)); - let dx = x1 - x0; - let dy = y1 - y0; - let dist = (dx * dx + dy * dy).sqrt(); - if dist < MIN_DIST { - return Self::post_mouse_moved_cg_global(x1, y1); - } - let duration_ms = (70.0 + dist * 0.28).min(MAX_DURATION_MS as f64) as u64; - let steps = ((dist / 5.5).ceil() as usize).clamp(MIN_STEPS, MAX_STEPS); - let step_delay = Duration::from_millis((duration_ms / steps as u64).max(1)); - - for i in 1..=steps { - let t = i as f64 / steps as f64; - let te = Self::smoothstep01(t); - let x = x0 + dx * te; - let y = y0 + dy * te; - Self::post_mouse_moved_cg_global(x, y)?; - if i < steps { - std::thread::sleep(step_delay); - } - } - Ok(()) - } - - /// Windows/Linux: same smooth path using enigo absolute moves (single `Enigo` session). - #[cfg(not(target_os = "macos"))] - fn smooth_mouse_move_enigo_abs(x1: f64, y1: f64) -> BitFunResult<()> { - const MIN_DIST: f64 = 2.5; - const MIN_STEPS: usize = 8; - const MAX_STEPS: usize = 85; - const MAX_DURATION_MS: u64 = 400; - - Self::run_enigo_job(|e| { - let (cx, cy) = e.location().map_err(|err| { - BitFunError::tool(format!("smooth_mouse_move: pointer location: {}", err)) - })?; - let x0 = cx as f64; - let y0 = cy as f64; - let dx = x1 - x0; - let dy = y1 - y0; - let dist = (dx * dx + dy * dy).sqrt(); - if dist < MIN_DIST { - return e - .move_mouse(x1.round() as i32, y1.round() as i32, Coordinate::Abs) - .map_err(|err| BitFunError::tool(format!("mouse_move: {}", err))); - } - let duration_ms = (70.0 + dist * 0.28).min(MAX_DURATION_MS as f64) as u64; - let steps = ((dist / 5.5).ceil() as usize).clamp(MIN_STEPS, MAX_STEPS); - let step_delay = Duration::from_millis((duration_ms / steps as u64).max(1)); - - for i in 1..=steps { - let t = i as f64 / steps as f64; - let te = Self::smoothstep01(t); - let x = x0 + dx * te; - let y = y0 + dy * te; - e.move_mouse(x.round() as i32, y.round() as i32, Coordinate::Abs) - .map_err(|err| BitFunError::tool(format!("mouse_move: {}", err)))?; - if i < steps { - std::thread::sleep(step_delay); - } - } - Ok(()) - }) - } - - fn map_button(s: &str) -> BitFunResult - `,a.querySelector(".outline-card").addEventListener("click",()=>{o?.id&&r.selectSlide(o.id)}),t.append(a)}))}function Nn(e,r){let t=Bt("slideThumbs");if(t){if(t.innerHTML="",!e.slides.length){let n=document.createElement("div");n.className="thumbs-empty",n.textContent=$("slidesEmptyHint"),t.append(n);return}e.slides.forEach((n,i)=>{let a=n.html?Ca(n.html):null,o=n.theme||{},l=a||o.background||"var(--studio-slide-chrome)",c=document.createElement("button");c.className=`thumb${n.id===e.activeSlideId?" is-active":""}`,c.type="button",c.style.setProperty("--thumb-bg",l),c.style.setProperty("--thumb-primary",o.primary||"var(--studio-accent)");let s=document.createElement("div");if(s.className="thumb-preview",s.style.background=l,n.html)s.appendChild(kh(n.html));else{let A=document.createElement("div");A.className="thumb-preview-slide",A.innerHTML=un(n),s.appendChild(A)}c.appendChild(s);let u=document.createElement("div");u.className="thumb-copy",u.innerHTML=` + `,a.querySelector(".outline-card").addEventListener("click",()=>{o?.id&&r.selectSlide(o.id)}),t.append(a)}))}function Nn(e,r){let t=Bt("slideThumbs");if(t){if(t.innerHTML="",!e.slides.length){let n=document.createElement("div");n.className="thumbs-empty",n.textContent=$("slidesEmptyHint"),t.append(n);return}e.slides.forEach((n,i)=>{let a=n.html?Pa(n.html):null,o=n.theme||{},l=a||o.background||"var(--studio-slide-chrome)",c=document.createElement("button");c.className=`thumb${n.id===e.activeSlideId?" is-active":""}`,c.type="button",c.style.setProperty("--thumb-bg",l),c.style.setProperty("--thumb-primary",o.primary||"var(--studio-accent)");let s=document.createElement("div");if(s.className="thumb-preview",s.style.background=l,n.html)s.appendChild(Th(n.html));else{let A=document.createElement("div");A.className="thumb-preview-slide",A.innerHTML=un(n),s.appendChild(A)}c.appendChild(s);let u=document.createElement("div");u.className="thumb-copy",u.innerHTML=` ${Le(n.kicker||"")} ${Le(n.title)} - `,c.appendChild(u);let d=document.createElement("span");d.className="thumb-number",d.textContent=String(i+1),c.appendChild(d),c.addEventListener("click",()=>r.selectSlide(n.id)),t.append(c)}),requestAnimationFrame(()=>{ys(),za()})}}var Ba=960,Ra=540;function kh(e){let{host:r}=vs({hostClass:"thumb-preview-html",frameClass:"thumb-preview-frame",html:e,onReady:()=>gr(r)});return r}function Ch(e,r){let t=e?.closest?.(`.${Ar}`)||r?.querySelector?.(`.${Ar}`);if(t){gr(t);return}if(!e||!r)return;let{width:n,height:i}=ms(r);if(!n||!i)return;let a=Math.min(n/Ba,i/Ra);e.style.width=`${Ba}px`,e.style.height=`${Ra}px`,e.style.transform=`scale(${a})`,e.style.transformOrigin="top left"}function Ph(e){let r=e.querySelector(".thumb-preview-frame");if(r){Ch(r,e);return}let t=e.querySelector(".thumb-preview-html, .thumb-preview-slide");if(!t)return;let n=lh(e);if(!n)return;let i=n/Ba,a=Ra*i;t.style.width=`${Ba}px`,t.style.height=`${Ra}px`,t.style.transform=`scale(${i})`,t.style.transformOrigin="top left",e.style.height=`${a}px`}function ys(){let e=Bt("slideThumbs");e&&(e.querySelectorAll(`.${Ar}`).forEach(r=>{gr(r)}),e.querySelectorAll(".thumb-preview").forEach(r=>{r.querySelector(`.${Ar}`)||Ph(r)}))}var Fi=null,Ti=null;function Mc(){if(typeof ResizeObserver>"u")return;Ti||(Ti=new ResizeObserver(()=>{document.querySelectorAll(`.${Ar}`).forEach(r=>{gr(r)})})),Ti.disconnect(),document.querySelectorAll(`.${Ar}`).forEach(r=>{Ti.observe(r)});let e=Bt("slideCanvas");e&&Ti.observe(e)}function za(){let e=Bt("slideThumbs");!e||typeof ResizeObserver>"u"||(Fi||(Fi=new ResizeObserver(()=>ys())),Fi.disconnect(),Fi.observe(e),e.querySelectorAll(".thumb-preview").forEach(r=>{Fi.observe(r)}),Mc())}function Fh(e){if(!String(e.brief?.topic||"").trim()){let n=String(e.title||"").trim();if(n===$("blankDeckTitle")||n===$("defaultDeckTitle")||n===$("newSlideTitle"))return!0}if(!e.slides?.length)return!0;let r=String(e.title||"").trim();return e.slides.length===1&&e.outline.length===1&&e.outline[0]===$("newSlideTitle")&&(r===$("blankDeckTitle")||r===$("newSlideTitle"))}function Da(e,r){if(!e)return;if(!r){e.style.background="";return}let t=r.theme||{},i=(r.html?Ca(r.html):null)||t.background||"";e.style.background=i||""}function cn(e,r){let t=Bt("slideCanvas");if(!t)return;let n=qt(e),i=!!(e.generation?.active||e.generation?.steps?.some(o=>o.status==="running"));if(!n){t.classList.remove("is-html-slide"),t.classList.add("is-empty"),t.innerHTML=i?`
${Le($("generationAgentWorking"))}

${Le($("agentWorkingDetail"))}

`:`

${Le($("welcomeTitle"))}

${Le($("welcomeSubcopy"))}

`,kc(t),Da(t,null),Qn();return}if(Fh(e)&&!n.html&&!i){t.classList.remove("is-html-slide"),t.classList.add("is-empty"),t.innerHTML=` + `,c.appendChild(u);let d=document.createElement("span");d.className="thumb-number",d.textContent=String(i+1),c.appendChild(d),c.addEventListener("click",()=>r.selectSlide(n.id)),t.append(c)}),requestAnimationFrame(()=>{bs(),Oa()})}}var La=960,Na=540;function Th(e){let{host:r}=ys({hostClass:"thumb-preview-html",frameClass:"thumb-preview-frame",html:e,onReady:()=>Ar(r)});return r}function Dh(e,r){let t=e?.closest?.(`.${pr}`)||r?.querySelector?.(`.${pr}`);if(t){Ar(t);return}if(!e||!r)return;let{width:n,height:i}=vs(r);if(!n||!i)return;let a=Math.min(n/La,i/Na);e.style.width=`${La}px`,e.style.height=`${Na}px`,e.style.transform=`scale(${a})`,e.style.transformOrigin="top left"}function Eh(e){let r=e.querySelector(".thumb-preview-frame");if(r){Dh(r,e);return}let t=e.querySelector(".thumb-preview-html, .thumb-preview-slide");if(!t)return;let n=lh(e);if(!n)return;let i=n/La,a=Na*i;t.style.width=`${La}px`,t.style.height=`${Na}px`,t.style.transform=`scale(${i})`,t.style.transformOrigin="top left",e.style.height=`${a}px`}function bs(){let e=Bt("slideThumbs");e&&(e.querySelectorAll(`.${pr}`).forEach(r=>{Ar(r)}),e.querySelectorAll(".thumb-preview").forEach(r=>{r.querySelector(`.${pr}`)||Eh(r)}))}var Ti=null,Di=null;function Mc(){if(typeof ResizeObserver>"u")return;Di||(Di=new ResizeObserver(()=>{document.querySelectorAll(`.${pr}`).forEach(r=>{Ar(r)})})),Di.disconnect(),document.querySelectorAll(`.${pr}`).forEach(r=>{Di.observe(r)});let e=Bt("slideCanvas");e&&Di.observe(e)}function Oa(){let e=Bt("slideThumbs");!e||typeof ResizeObserver>"u"||(Ti||(Ti=new ResizeObserver(()=>bs())),Ti.disconnect(),Ti.observe(e),e.querySelectorAll(".thumb-preview").forEach(r=>{Ti.observe(r)}),Mc())}function Bh(e){if(!String(e.brief?.topic||"").trim()){let n=String(e.title||"").trim();if(n===$("blankDeckTitle")||n===$("defaultDeckTitle")||n===$("newSlideTitle"))return!0}if(!e.slides?.length)return!0;let r=String(e.title||"").trim();return e.slides.length===1&&e.outline.length===1&&e.outline[0]===$("newSlideTitle")&&(r===$("blankDeckTitle")||r===$("newSlideTitle"))}function Ea(e,r){if(!e)return;if(!r){e.style.background="";return}let t=r.theme||{},i=(r.html?Pa(r.html):null)||t.background||"";e.style.background=i||""}function cn(e,r){let t=Bt("slideCanvas");if(!t)return;let n=qt(e),i=!!(e.generation?.active||e.generation?.steps?.some(o=>o.status==="running"));if(!n){t.classList.remove("is-html-slide"),t.classList.add("is-empty"),t.innerHTML=i?`
${Le($("generationAgentWorking"))}

${Le($("agentWorkingDetail"))}

`:`

${Le($("welcomeTitle"))}

${Le($("welcomeSubcopy"))}

`,Cc(t),Ea(t,null),Qn();return}if(Bh(e)&&!n.html&&!i){t.classList.remove("is-html-slide"),t.classList.add("is-empty"),t.innerHTML=`

${Le($("welcomeTitle"))}

@@ -34,8 +66,8 @@ Incremental: add new information, e.g. "Add a competitor analysis section after
- `,kc(t),Da(t,null),Qn();return}if(t.classList.remove("is-empty"),n?.html){t.innerHTML="",t.classList.add("is-html-slide");let{host:o,frame:l}=vs({frameClass:"html-slide-frame",html:n.html,interactive:!0,onReady:c=>{Eh(c,n.id,r),Qn(),gr(o),requestAnimationFrame(()=>gr(o))}});t.append(o),Da(t,n),t.classList.remove("is-entering"),t.offsetWidth,t.classList.add("is-entering"),Qn();return}t.classList.remove("is-html-slide"),t.innerHTML=n?un(n,{selectedElementId:e.selectedElementId,editable:!0}):"",t.querySelectorAll(".slide-element").forEach(o=>{let l=o.dataset.elementId;o.addEventListener("click",c=>{c.stopPropagation(),r.selectElement(l)}),o.addEventListener("pointerdown",c=>{c.target?.isContentEditable&&!c.target.classList.contains("resize-handle")||r.beginDrag(c,l)})}),t.querySelectorAll("[data-edit-text]").forEach(o=>{o.addEventListener("blur",()=>{r.updateElementTextDirect(o.dataset.editText,o.textContent||"")}),o.addEventListener("keydown",l=>{(l.metaKey||l.ctrlKey)&&l.key==="Enter"&&o.blur()})}),t.querySelectorAll("[data-edit-list]").forEach(o=>{o.addEventListener("blur",()=>{r.updateElementListItemDirect(o.dataset.editList,Number(o.dataset.itemIndex),o.textContent||"")}),o.addEventListener("keydown",l=>{(l.metaKey||l.ctrlKey)&&l.key==="Enter"&&o.blur()})}),t.classList.remove("is-entering"),t.offsetWidth,t.classList.add("is-entering");let a=Rn(e);if(a)ch(a);else{let o=Bt("floatingToolbar");o&&o.classList.remove("is-visible")}Da(t,n),Qn()}function Ma(e,r){let t=Bt("elementInspector"),n=Rn(e),i=qt(e);if(!(!t||!i)){if(t.hidden){t.innerHTML="";return}if(!n){t.innerHTML=`${xc(i)}

${$("noSelection")}

`,Sc(t,r),t.querySelector("#slideNotesInput")?.addEventListener("input",a=>r.updateSlideNotes(a.target.value));return}t.innerHTML=` - ${xc(i)} + `,Cc(t),Ea(t,null),Qn();return}if(t.classList.remove("is-empty"),n?.html){t.innerHTML="",t.classList.add("is-html-slide");let{host:o,frame:l}=ys({frameClass:"html-slide-frame",html:n.html,interactive:!0,onReady:c=>{Nh(c,n.id,r),Qn(),Ar(o),requestAnimationFrame(()=>Ar(o))}});t.append(o),Ea(t,n),t.classList.remove("is-entering"),t.offsetWidth,t.classList.add("is-entering"),Qn();return}t.classList.remove("is-html-slide"),t.innerHTML=n?un(n,{selectedElementId:e.selectedElementId,editable:!0}):"",t.querySelectorAll(".slide-element").forEach(o=>{let l=o.dataset.elementId;o.addEventListener("click",c=>{c.stopPropagation(),r.selectElement(l)}),o.addEventListener("pointerdown",c=>{c.target?.isContentEditable&&!c.target.classList.contains("resize-handle")||r.beginDrag(c,l)})}),t.querySelectorAll("[data-edit-text]").forEach(o=>{o.addEventListener("blur",()=>{r.updateElementTextDirect(o.dataset.editText,o.textContent||"")}),o.addEventListener("keydown",l=>{(l.metaKey||l.ctrlKey)&&l.key==="Enter"&&o.blur()})}),t.querySelectorAll("[data-edit-list]").forEach(o=>{o.addEventListener("blur",()=>{r.updateElementListItemDirect(o.dataset.editList,Number(o.dataset.itemIndex),o.textContent||"")}),o.addEventListener("keydown",l=>{(l.metaKey||l.ctrlKey)&&l.key==="Enter"&&o.blur()})}),t.classList.remove("is-entering"),t.offsetWidth,t.classList.add("is-entering");let a=Rn(e);if(a)ch(a);else{let o=Bt("floatingToolbar");o&&o.classList.remove("is-visible")}Ea(t,n),Qn()}function _a(e,r){let t=Bt("elementInspector"),n=Rn(e),i=qt(e);if(!(!t||!i)){if(t.hidden){t.innerHTML="";return}if(!n){t.innerHTML=`${Sc(i)}

${$("noSelection")}

`,kc(t,r),t.querySelector("#slideNotesInput")?.addEventListener("input",a=>r.updateSlideNotes(a.target.value));return}t.innerHTML=` + ${Sc(i)}
- - - - + + + +
@@ -55,7 +87,7 @@ Incremental: add new information, e.g. "Add a competitor analysis section after
- `,["elementTextInput","elementItemsInput","elementDataInput","elementXInput","elementYInput","elementWInput","elementHInput","elementFontInput","elementWeightInput","elementColorInput","elementBgInput","slideNotesInput"].forEach(a=>t.querySelector(`#${a}`)?.addEventListener("input",()=>r.updateElementFromInspector())),Sc(t,r)}}function xc(e){return` + `,["elementTextInput","elementItemsInput","elementDataInput","elementXInput","elementYInput","elementWInput","elementHInput","elementFontInput","elementWeightInput","elementColorInput","elementBgInput","slideNotesInput"].forEach(a=>t.querySelector(`#${a}`)?.addEventListener("input",()=>r.updateElementFromInspector())),kc(t,r)}}function Sc(e){return`
@@ -63,21 +95,21 @@ Incremental: add new information, e.g. "Add a competitor analysis section after
- ${Th(e)} - `}function Th(e){let r=Array.isArray(e.quality?.issues)?e.quality.issues:[];return r.length?` + ${Rh(e)} + `}function Rh(e){let r=Array.isArray(e.quality?.issues)?e.quality.issues:[];return r.length?`
${Le($("qualityReportTitle"))}: ${Math.round(Number(e.quality?.score??100))}/100
    ${r.map(t=>`
  • ${Le(t.message)}
  • `).join("")}
- `:""}function Sc(e,r){["slideKickerInput","slideClaimInput","slideProofInput","slideSupportInput","slideSourceInput"].forEach(t=>{e.querySelector(`#${t}`)?.addEventListener("input",()=>r.updateSlideMethodology())})}function un(e,r={}){if(e?.html)return``;let t=!!r.editable,n=r.selectedElementId||"",i=[`--slide-bg:${e.theme.background}`,`--slide-ink:${e.theme.ink}`,`--slide-muted:${e.theme.muted}`,`--slide-primary:${e.theme.primary}`,`--slide-accent:${e.theme.accent}`,`--slide-panel:${e.theme.panel||"#ffffff"}`].join(";");return`
+ `:""}function kc(e,r){["slideKickerInput","slideClaimInput","slideProofInput","slideSupportInput","slideSourceInput"].forEach(t=>{e.querySelector(`#${t}`)?.addEventListener("input",()=>r.updateSlideMethodology())})}function un(e,r={}){if(e?.html)return``;let t=!!r.editable,n=r.selectedElementId||"",i=[`--slide-bg:${e.theme.background}`,`--slide-ink:${e.theme.ink}`,`--slide-muted:${e.theme.muted}`,`--slide-primary:${e.theme.primary}`,`--slide-accent:${e.theme.accent}`,`--slide-panel:${e.theme.panel||"#ffffff"}`].join(";");return`
${e.kicker?`
${Le(e.kicker)}
`:""} ${e.proofObject?`
${Le(e.proofObject)}
`:""} - ${Dh(e)} - ${(e.elements||[]).map(a=>Rh(a,e.theme,t,n)).join("")} + ${Lh(e)} + ${(e.elements||[]).map(a=>zh(a,e.theme,t,n)).join("")} ${e.sourceNote?`
${Le(e.sourceNote)}
`:""} -
`}function Dh(e){let r=Array.isArray(e.quality?.issues)?e.quality.issues:[];if(!r.length)return"";let t=r.filter(i=>i.severity==="high").length,n=t?$("qualityNeedsReview"):$("qualityHasWarnings");return`
${Le(n)}
`}function kc(e){e.querySelectorAll("[data-welcome-prompt]").forEach(r=>{r.addEventListener("click",()=>{let t=Bt("topicInput");t&&(t.value=r.dataset.welcomePrompt||r.textContent||"",t.focus())})})}function Eh(e,r,t){if(!t?.updateSlideHtmlDirect)return;let n=e?.shadowRoot,i=n?.querySelector(`.${ps}`);if(!n||!i)return;if(!n.querySelector(`style[${bc}]`)){let c=document.createElement("style");c.setAttribute(bc,"true"),c.textContent=[`[${Zn}] { cursor: text; -webkit-user-select: text !important; user-select: text !important; }`,`[${Zn}]:hover { outline: 1.5px dashed rgba(37, 99, 235, 0.55); outline-offset: 1px; }`,`[${Zn}]:focus { outline: 2px solid rgba(37, 99, 235, 0.85); outline-offset: 1px; }`].join(` -`),n.appendChild(c)}n.addEventListener("click",c=>{c.target?.closest?.("a[href]")&&c.preventDefault()},!0);let a=()=>{let c=Bh(e,i);c&&t.updateSlideHtmlDirect(r,c)},o=i.querySelectorAll("h1,h2,h3,h4,h5,h6,p,li,span,strong,em,b,i,u,small,code,a,label,blockquote,td,th,dt,dd,figcaption,div"),l=0;o.forEach(c=>{Array.from(c.childNodes).some(u=>u.nodeType===Node.TEXT_NODE&&String(u.textContent||"").trim())&&(l+=1,c.setAttribute("contenteditable","true"),c.setAttribute("spellcheck","false"),c.setAttribute(Zn,"true"),c.addEventListener("blur",a),c.addEventListener("keydown",u=>{((u.metaKey||u.ctrlKey)&&u.key==="Enter"||u.key==="Escape")&&(u.preventDefault(),c.blur())}))}),l||console.warn("[ppt-live] no editable nodes bound for slide",{slideId:r,candidates:o.length})}function Bh(e,r){let t=String(e?._pptLiveSourceHtml||"");if(!t)return"";let n=new DOMParser().parseFromString(Wr(t),"text/html");if(!n.body)return"";let i=r.cloneNode(!0);i.querySelectorAll(`[${Zn}]`).forEach(o=>{o.removeAttribute("contenteditable"),o.removeAttribute("spellcheck"),o.removeAttribute(Zn)}),n.body.innerHTML=i.innerHTML;let a=` -${n.documentElement.outerHTML}`;return e._pptLiveSourceHtml=a,a}function Wr(e){let r=String(e||"").trim();return r?/]/i.test(r)?r:`${r}`:''}function Rh(e,r,t,n){let i=t&&n===e.id,a=[`left:${e.x}%`,`top:${e.y}%`,`width:${e.w}%`,`height:${e.h}%`,`font-size:${Nh(e.style.fontSize)}`,`font-weight:${e.style.fontWeight}`,`color:${Cc(e.style.color,r)}`,`text-align:${e.style.align||"left"}`,`background:${Cc(e.style.background,r)}`,`opacity:${e.style.opacity}`,`border-radius:${e.style.borderRadius}px`].join(";"),o="";if(e.type==="list")o=`
    ${(e.items||[]).map((l,c)=>t?`
  • ${Le(l)}
  • `:`
  • ${Le(l)}
  • `).join("")}
`;else if(e.type==="metric")o=`${Le(e.text)}${Le(e.label)}`;else if(e.type==="chart"){let l=Math.max(1,...(e.data||[]).map(c=>Number(c.value)||0));o=`${Le(e.text)}
${(e.data||[]).map(c=>`${Le(c.label)}`).join("")}
`}else e.type==="media"?o=`${Le(e.text||$("mediaPlaceholder"))}`:o=t?`${Le(e.text||"")}`:Le(e.text||"");return`
${o}${i?'':""}
`}function Cc(e,r){return!e||e==="transparent"?"transparent":e==="ink"?r.ink:e==="muted"?r.muted:e==="primary"?r.primary:e==="accent"?r.accent:e==="panel"?r.panel||"#ffffff":e==="soft"?Lh(r.primary,.1):e==="background"?r.background:e}function Lh(e,r){let t=String(e||"#0f766e").replace("#",""),n=parseInt(t.length===3?t.split("").map(l=>l+l).join(""):t,16),i=n>>16&255,a=n>>8&255,o=n&255;return`rgba(${i}, ${a}, ${o}, ${r})`}function Nh(e){let r=Math.max(8,Number(e)||24);return`clamp(8px, ${Math.round(r/10.2*1e3)/1e3}cqw, ${r}px)`}function Bt(e){return document.getElementById(e)}function Ih(e,r){let t=Bt(e);t&&document.activeElement!==t&&(t.value=r??"")}function zh(e){return Bt(e)?.value||""}function hs(e,r){let t=Bt(e);t&&(t.textContent=String(r??""))}function Ea(e){return Math.round(Number(e)*10)/10}function Oc(e=document,r=!1){let t=e,n=t.defaultView||window,i=new Set(["SCRIPT","STYLE","PRE","CODE","SVG","TEXTAREA"]),a="strong,b,em,i,u,span,a,small,mark,sub,sup,code",o="p,h1,h2,h3,h4,h5,h6,li";function l(E){let B=String(E.className||"").toLowerCase(),T=String(E.getAttribute?.("role")||"").toLowerCase();return/h1|title|headline|hero/.test(B)||T==="heading"?"h1":/h2|subtitle|subhead|section-title/.test(B)?"h2":/h3|kicker|eyebrow|label|caption/.test(B)?"h3":"p"}function c(E){return!E||E==="transparent"||E==="rgba(0, 0, 0, 0)"}function s(){let E=t.body;if(!E)return;let B=n.getComputedStyle(E),T=parseFloat(B.width),W=parseFloat(B.height);E.style.width=T>0?`${T}px`:"1280px",E.style.height=W>0?`${W}px`:"720px",E.style.margin="0",E.style.padding=B.padding||"0",E.style.overflow="hidden",E.style.position=B.position==="static"?"relative":B.position,c(B.backgroundColor)||(E.style.backgroundColor=B.backgroundColor),B.color&&(E.style.color=B.color),t.documentElement.style.margin="0",t.documentElement.style.padding="0";let H=n.getComputedStyle(t.documentElement).backgroundColor;!c(H)&&c(B.backgroundColor)&&(E.style.backgroundColor=H)}function u(E){E.querySelectorAll("div").forEach(B=>{i.has(B.tagName)||[...B.childNodes].forEach(T=>{if(T.nodeType!==Node.TEXT_NODE)return;let W=T.textContent.replace(/\s+/g," ").trim();if(!W){T.remove();return}let H=t.createElement(l(B));H.textContent=W,B.replaceChild(H,T)})})}function d(E){E.querySelectorAll("span").forEach(B=>{let T=n.getComputedStyle(B),W=T.backgroundColor&&T.backgroundColor!=="rgba(0, 0, 0, 0)",H=f(T);if(!W&&!H)return;let C=t.createElement("p");B.className&&(C.className=B.className),B.getAttribute("style")&&C.setAttribute("style",B.getAttribute("style")),C.textContent=B.textContent,B.replaceWith(C)})}function A(E){E.querySelectorAll("div").forEach(B=>{let T=[...B.children].length>0&&[...B.children].every(C=>C.tagName==="SPAN"||C.tagName==="BR"),W=B.textContent.replace(/\s+/g," ").trim();if(!T||!W||B.querySelector("ul,ol,p,h1,h2,h3,h4,h5,h6"))return;let H=W.split(/\s*[•·▪-]\s+/).map(C=>C.trim()).filter(Boolean);if(H.length>=2){let C=t.createElement("ul");H.forEach(I=>{let m=t.createElement("li");m.textContent=I,C.appendChild(m)}),B.replaceChildren(C)}})}function f(E){return["Top","Right","Bottom","Left"].some(B=>parseFloat(E[`border${B}Width`]||0)>0)}function p(E){E.querySelectorAll(o).forEach(B=>{let T=n.getComputedStyle(B),W=T.backgroundColor&&T.backgroundColor!=="rgba(0, 0, 0, 0)",H=T.backgroundImage&&T.backgroundImage!=="none",C=f(T),I=T.boxShadow&&T.boxShadow!=="none";if(!W&&!H&&!C&&!I)return;let m=t.createElement("div");(W||H)&&(m.style.background=T.background,m.style.backgroundColor=T.backgroundColor),H&&!String(T.backgroundImage||"").includes("gradient")&&(m.style.backgroundImage="none"),C&&(m.style.border=T.border),T.borderRadius&&(m.style.borderRadius=T.borderRadius),I&&(m.style.boxShadow=T.boxShadow),T.padding&&(m.style.padding=T.padding),B.style.background="transparent",B.style.backgroundColor="transparent",B.style.backgroundImage="none",B.style.border="none",B.style.boxShadow="none",B.style.padding="0",B.parentNode.insertBefore(m,B),m.appendChild(B)})}function g(E){E.querySelectorAll("*").forEach(B=>{let T=n.getComputedStyle(B),W=T.backgroundImage||"";if(!W.includes("gradient"))return;let H=W.match(/#[0-9a-f]{3,8}|rgba?\([^)]+\)/i);B.style.backgroundImage="none",H?B.style.backgroundColor=H[0]:T.backgroundColor&&T.backgroundColor!=="rgba(0, 0, 0, 0)"&&(B.style.backgroundColor=T.backgroundColor)})}function y(E){E.querySelectorAll("div").forEach(B=>{let T=n.getComputedStyle(B),W=T.backgroundImage;!W||W==="none"||(B.style.backgroundImage="none",T.backgroundColor&&T.backgroundColor!=="rgba(0, 0, 0, 0)"&&(B.style.backgroundColor=T.backgroundColor))})}function v(E){E.querySelectorAll(a).forEach(B=>{B.style.setProperty("margin","0","important"),B.style.setProperty("padding","0","important"),B.style.setProperty("border","none","important"),B.style.setProperty("box-shadow","none","important"),B.style.setProperty("background","transparent","important"),B.style.setProperty("background-color","transparent","important"),B.style.setProperty("background-image","none","important"),n.getComputedStyle(B).display==="block"&&B.style.setProperty("display","inline","important")})}function b(E){E.querySelectorAll(a).forEach(B=>{B.removeAttribute("class"),B.removeAttribute("style")})}function x(E){E.querySelectorAll('link[rel="stylesheet"], style').forEach(B=>{B.id!=="ppt-live-export-safe-styles"&&B.remove()})}function P(E){E.querySelectorAll(a).forEach(B=>{let T=n.getComputedStyle(B),W=["marginTop","marginRight","marginBottom","marginLeft"].some(q=>parseFloat(T[q])>0),H=["paddingTop","paddingRight","paddingBottom","paddingLeft"].some(q=>parseFloat(T[q])>0),C=f(T),I=T.backgroundColor&&T.backgroundColor!=="rgba(0, 0, 0, 0)",m=T.backgroundImage&&T.backgroundImage!=="none";if(!W&&!H&&!C&&!I&&!m)return;let N=B.tagName.toLowerCase(),ne=t.createElement(N);ne.textContent=B.textContent,B.replaceWith(ne)})}function F(E){let B=E.body||E.querySelector(".ppt-export-body");(B?[B,...B.querySelectorAll("*")]:[...E.querySelectorAll("body, body *")]).forEach(W=>{if(i.has(W.tagName))return;let H=n.getComputedStyle(W),C=W.style;H.position&&H.position!=="static"&&(C.position=H.position),H.display&&H.display!=="inline"&&(C.display=H.display),["left","top","right","bottom","width","height","maxWidth","maxHeight"].forEach(m=>{let N=H[m];N&&N!=="auto"&&N!=="none"&&N!=="0px"&&(C[m]=N)}),H.zIndex&&H.zIndex!=="auto"&&(C.zIndex=H.zIndex),H.color&&(C.color=H.color),H.fontSize&&(C.fontSize=H.fontSize),H.fontWeight&&(C.fontWeight=H.fontWeight),H.fontFamily&&(C.fontFamily=H.fontFamily),H.lineHeight&&H.lineHeight!=="normal"&&(C.lineHeight=H.lineHeight),H.textAlign&&(C.textAlign=H.textAlign);let I=H.backgroundColor;I&&I!=="rgba(0, 0, 0, 0)"&&(C.backgroundColor=I),H.border&&H.border!=="none"&&f(H)&&(C.border=H.border),H.borderRadius&&H.borderRadius!=="0px"&&(C.borderRadius=H.borderRadius),H.padding&&H.padding!=="0px"&&(C.padding=H.padding),H.gap&&H.gap!=="normal"&&(C.gap=H.gap),H.flexDirection&&H.flexDirection!=="row"&&(C.flexDirection=H.flexDirection),H.alignItems&&H.alignItems!=="normal"&&(C.alignItems=H.alignItems),H.justifyContent&&H.justifyContent!=="normal"&&(C.justifyContent=H.justifyContent)})}function R(E){let B="ppt-live-export-safe-styles";E.getElementById(B)?.remove();let T=t.createElement("style");T.id=B,T.textContent=` +
`}function Lh(e){let r=Array.isArray(e.quality?.issues)?e.quality.issues:[];if(!r.length)return"";let t=r.filter(i=>i.severity==="high").length,n=t?$("qualityNeedsReview"):$("qualityHasWarnings");return`
${Le(n)}
`}function Cc(e){e.querySelectorAll("[data-welcome-prompt]").forEach(r=>{r.addEventListener("click",()=>{let t=Bt("topicInput");t&&(t.value=r.dataset.welcomePrompt||r.textContent||"",t.focus())})})}function Nh(e,r,t){if(!t?.updateSlideHtmlDirect)return;let n=e?.shadowRoot,i=n?.querySelector(`.${As}`);if(!n||!i)return;if(!n.querySelector(`style[${wc}]`)){let c=document.createElement("style");c.setAttribute(wc,"true"),c.textContent=[`[${Zn}] { cursor: text; -webkit-user-select: text !important; user-select: text !important; }`,`[${Zn}]:hover { outline: 1.5px dashed rgba(37, 99, 235, 0.55); outline-offset: 1px; }`,`[${Zn}]:focus { outline: 2px solid rgba(37, 99, 235, 0.85); outline-offset: 1px; }`].join(` +`),n.appendChild(c)}n.addEventListener("click",c=>{c.target?.closest?.("a[href]")&&c.preventDefault()},!0);let a=()=>{let c=Ih(e,i);c&&t.updateSlideHtmlDirect(r,c)},o=i.querySelectorAll("h1,h2,h3,h4,h5,h6,p,li,span,strong,em,b,i,u,small,code,a,label,blockquote,td,th,dt,dd,figcaption,div"),l=0;o.forEach(c=>{Array.from(c.childNodes).some(u=>u.nodeType===Node.TEXT_NODE&&String(u.textContent||"").trim())&&(l+=1,c.setAttribute("contenteditable","true"),c.setAttribute("spellcheck","false"),c.setAttribute(Zn,"true"),c.addEventListener("blur",a),c.addEventListener("keydown",u=>{((u.metaKey||u.ctrlKey)&&u.key==="Enter"||u.key==="Escape")&&(u.preventDefault(),c.blur())}))}),l||console.warn("[ppt-live] no editable nodes bound for slide",{slideId:r,candidates:o.length})}function Ih(e,r){let t=String(e?._pptLiveSourceHtml||"");if(!t)return"";let n=new DOMParser().parseFromString(Nr(t),"text/html");if(!n.body)return"";let i=r.cloneNode(!0);i.querySelectorAll(`[${Zn}]`).forEach(o=>{o.removeAttribute("contenteditable"),o.removeAttribute("spellcheck"),o.removeAttribute(Zn)}),n.body.innerHTML=i.innerHTML;let a=` +${n.documentElement.outerHTML}`;return e._pptLiveSourceHtml=a,a}function Nr(e){let r=String(e||"").trim();return r?/]/i.test(r)?r:`${r}`:''}function zh(e,r,t,n){let i=t&&n===e.id,a=[`left:${e.x}%`,`top:${e.y}%`,`width:${e.w}%`,`height:${e.h}%`,`font-size:${Oh(e.style.fontSize)}`,`font-weight:${e.style.fontWeight}`,`color:${Pc(e.style.color,r)}`,`text-align:${e.style.align||"left"}`,`background:${Pc(e.style.background,r)}`,`opacity:${e.style.opacity}`,`border-radius:${e.style.borderRadius}px`].join(";"),o="";if(e.type==="list")o=`
    ${(e.items||[]).map((l,c)=>t?`
  • ${Le(l)}
  • `:`
  • ${Le(l)}
  • `).join("")}
`;else if(e.type==="metric")o=`${Le(e.text)}${Le(e.label)}`;else if(e.type==="chart"){let l=Math.max(1,...(e.data||[]).map(c=>Number(c.value)||0));o=`${Le(e.text)}
${(e.data||[]).map(c=>`${Le(c.label)}`).join("")}
`}else e.type==="media"?o=`${Le(e.text||$("mediaPlaceholder"))}`:o=t?`${Le(e.text||"")}`:Le(e.text||"");return`
${o}${i?'':""}
`}function Pc(e,r){return!e||e==="transparent"?"transparent":e==="ink"?r.ink:e==="muted"?r.muted:e==="primary"?r.primary:e==="accent"?r.accent:e==="panel"?r.panel||"#ffffff":e==="soft"?Mh(r.primary,.1):e==="background"?r.background:e}function Mh(e,r){let t=String(e||"#0f766e").replace("#",""),n=parseInt(t.length===3?t.split("").map(l=>l+l).join(""):t,16),i=n>>16&255,a=n>>8&255,o=n&255;return`rgba(${i}, ${a}, ${o}, ${r})`}function Oh(e){let r=Math.max(8,Number(e)||24);return`clamp(8px, ${Math.round(r/10.2*1e3)/1e3}cqw, ${r}px)`}function Bt(e){return document.getElementById(e)}function _h(e,r){let t=Bt(e);t&&document.activeElement!==t&&(t.value=r??"")}function Uh(e){return Bt(e)?.value||""}function ps(e,r){let t=Bt(e);t&&(t.textContent=String(r??""))}function Ba(e){return Math.round(Number(e)*10)/10}function Oc(e=document,r=!1){let t=e,n=t.defaultView||window,i=new Set(["SCRIPT","STYLE","PRE","CODE","SVG","TEXTAREA"]),a="strong,b,em,i,u,span,a,small,mark,sub,sup,code",o="p,h1,h2,h3,h4,h5,h6,li";function l(E){let B=String(E.className||"").toLowerCase(),T=String(E.getAttribute?.("role")||"").toLowerCase();return/h1|title|headline|hero/.test(B)||T==="heading"?"h1":/h2|subtitle|subhead|section-title/.test(B)?"h2":/h3|kicker|eyebrow|label|caption/.test(B)?"h3":"p"}function c(E){return!E||E==="transparent"||E==="rgba(0, 0, 0, 0)"}function s(){let E=t.body;if(!E)return;let B=n.getComputedStyle(E),T=parseFloat(B.width),W=parseFloat(B.height);E.style.width=T>0?`${T}px`:"1280px",E.style.height=W>0?`${W}px`:"720px",E.style.margin="0",E.style.padding=B.padding||"0",E.style.overflow="hidden",E.style.position=B.position==="static"?"relative":B.position,c(B.backgroundColor)||(E.style.backgroundColor=B.backgroundColor),B.color&&(E.style.color=B.color),t.documentElement.style.margin="0",t.documentElement.style.padding="0";let H=n.getComputedStyle(t.documentElement).backgroundColor;!c(H)&&c(B.backgroundColor)&&(E.style.backgroundColor=H)}function u(E){E.querySelectorAll("div").forEach(B=>{i.has(B.tagName)||[...B.childNodes].forEach(T=>{if(T.nodeType!==Node.TEXT_NODE)return;let W=T.textContent.replace(/\s+/g," ").trim();if(!W){T.remove();return}let H=t.createElement(l(B));H.textContent=W,B.replaceChild(H,T)})})}function d(E){E.querySelectorAll("span").forEach(B=>{let T=n.getComputedStyle(B),W=T.backgroundColor&&T.backgroundColor!=="rgba(0, 0, 0, 0)",H=f(T);if(!W&&!H)return;let C=t.createElement("p");B.className&&(C.className=B.className),B.getAttribute("style")&&C.setAttribute("style",B.getAttribute("style")),C.textContent=B.textContent,B.replaceWith(C)})}function A(E){E.querySelectorAll("div").forEach(B=>{let T=[...B.children].length>0&&[...B.children].every(C=>C.tagName==="SPAN"||C.tagName==="BR"),W=B.textContent.replace(/\s+/g," ").trim();if(!T||!W||B.querySelector("ul,ol,p,h1,h2,h3,h4,h5,h6"))return;let H=W.split(/\s*[•·▪-]\s+/).map(C=>C.trim()).filter(Boolean);if(H.length>=2){let C=t.createElement("ul");H.forEach(I=>{let m=t.createElement("li");m.textContent=I,C.appendChild(m)}),B.replaceChildren(C)}})}function f(E){return["Top","Right","Bottom","Left"].some(B=>parseFloat(E[`border${B}Width`]||0)>0)}function p(E){E.querySelectorAll(o).forEach(B=>{let T=n.getComputedStyle(B),W=T.backgroundColor&&T.backgroundColor!=="rgba(0, 0, 0, 0)",H=T.backgroundImage&&T.backgroundImage!=="none",C=f(T),I=T.boxShadow&&T.boxShadow!=="none";if(!W&&!H&&!C&&!I)return;let m=t.createElement("div");(W||H)&&(m.style.background=T.background,m.style.backgroundColor=T.backgroundColor),H&&!String(T.backgroundImage||"").includes("gradient")&&(m.style.backgroundImage="none"),C&&(m.style.border=T.border),T.borderRadius&&(m.style.borderRadius=T.borderRadius),I&&(m.style.boxShadow=T.boxShadow),T.padding&&(m.style.padding=T.padding),B.style.background="transparent",B.style.backgroundColor="transparent",B.style.backgroundImage="none",B.style.border="none",B.style.boxShadow="none",B.style.padding="0",B.parentNode.insertBefore(m,B),m.appendChild(B)})}function g(E){E.querySelectorAll("*").forEach(B=>{let T=n.getComputedStyle(B),W=T.backgroundImage||"";if(!W.includes("gradient"))return;let H=W.match(/#[0-9a-f]{3,8}|rgba?\([^)]+\)/i);B.style.backgroundImage="none",H?B.style.backgroundColor=H[0]:T.backgroundColor&&T.backgroundColor!=="rgba(0, 0, 0, 0)"&&(B.style.backgroundColor=T.backgroundColor)})}function y(E){E.querySelectorAll("div").forEach(B=>{let T=n.getComputedStyle(B),W=T.backgroundImage;!W||W==="none"||(B.style.backgroundImage="none",T.backgroundColor&&T.backgroundColor!=="rgba(0, 0, 0, 0)"&&(B.style.backgroundColor=T.backgroundColor))})}function v(E){E.querySelectorAll(a).forEach(B=>{B.style.setProperty("margin","0","important"),B.style.setProperty("padding","0","important"),B.style.setProperty("border","none","important"),B.style.setProperty("box-shadow","none","important"),B.style.setProperty("background","transparent","important"),B.style.setProperty("background-color","transparent","important"),B.style.setProperty("background-image","none","important"),n.getComputedStyle(B).display==="block"&&B.style.setProperty("display","inline","important")})}function b(E){E.querySelectorAll(a).forEach(B=>{B.removeAttribute("class"),B.removeAttribute("style")})}function x(E){E.querySelectorAll('link[rel="stylesheet"], style').forEach(B=>{B.id!=="ppt-live-export-safe-styles"&&B.remove()})}function P(E){E.querySelectorAll(a).forEach(B=>{let T=n.getComputedStyle(B),W=["marginTop","marginRight","marginBottom","marginLeft"].some(q=>parseFloat(T[q])>0),H=["paddingTop","paddingRight","paddingBottom","paddingLeft"].some(q=>parseFloat(T[q])>0),C=f(T),I=T.backgroundColor&&T.backgroundColor!=="rgba(0, 0, 0, 0)",m=T.backgroundImage&&T.backgroundImage!=="none";if(!W&&!H&&!C&&!I&&!m)return;let N=B.tagName.toLowerCase(),ne=t.createElement(N);ne.textContent=B.textContent,B.replaceWith(ne)})}function F(E){let B=E.body||E.querySelector(".ppt-export-body");(B?[B,...B.querySelectorAll("*")]:[...E.querySelectorAll("body, body *")]).forEach(W=>{if(i.has(W.tagName))return;let H=n.getComputedStyle(W),C=W.style;H.position&&H.position!=="static"&&(C.position=H.position),H.display&&H.display!=="inline"&&(C.display=H.display),["left","top","right","bottom","width","height","maxWidth","maxHeight"].forEach(m=>{let N=H[m];N&&N!=="auto"&&N!=="none"&&N!=="0px"&&(C[m]=N)}),H.zIndex&&H.zIndex!=="auto"&&(C.zIndex=H.zIndex),H.color&&(C.color=H.color),H.fontSize&&(C.fontSize=H.fontSize),H.fontWeight&&(C.fontWeight=H.fontWeight),H.fontFamily&&(C.fontFamily=H.fontFamily),H.lineHeight&&H.lineHeight!=="normal"&&(C.lineHeight=H.lineHeight),H.textAlign&&(C.textAlign=H.textAlign);let I=H.backgroundColor;I&&I!=="rgba(0, 0, 0, 0)"&&(C.backgroundColor=I),H.border&&H.border!=="none"&&f(H)&&(C.border=H.border),H.borderRadius&&H.borderRadius!=="0px"&&(C.borderRadius=H.borderRadius),H.padding&&H.padding!=="0px"&&(C.padding=H.padding),H.gap&&H.gap!=="normal"&&(C.gap=H.gap),H.flexDirection&&H.flexDirection!=="row"&&(C.flexDirection=H.flexDirection),H.alignItems&&H.alignItems!=="normal"&&(C.alignItems=H.alignItems),H.justifyContent&&H.justifyContent!=="normal"&&(C.justifyContent=H.justifyContent)})}function R(E){let B="ppt-live-export-safe-styles";E.getElementById(B)?.remove();let T=t.createElement("style");T.id=B,T.textContent=` ${a}, [class] ${a.split(",").join(", [class] ")} { margin: 0 !important; padding: 0 !important; @@ -91,8 +123,16 @@ ${n.documentElement.outerHTML}`;return e._pptLiveSourceHtml=a,a}function Wr(e){l box-shadow: none !important; } `,(E.head||E.documentElement).appendChild(T)}s(),u(t),d(t),A(t),g(t),y(t),r?(p(t),v(t),P(t),R(t),P(t),F(t),t.querySelectorAll("[class]").forEach(E=>E.removeAttribute("class")),x(t),b(t),v(t),P(t),R(t)):F(t)}function _c(e=document){let r=e.defaultView||window,t=e.body,n=r.getComputedStyle(t),i={width:parseFloat(n.width),height:parseFloat(n.height),scrollWidth:t.scrollWidth,scrollHeight:t.scrollHeight},a=[],o=Math.max(0,i.scrollWidth-i.width-1),l=Math.max(0,i.scrollHeight-i.height-1),c=o*.75,s=l*.75;if(c>0||s>0){let u=[];c>0&&u.push(`${c.toFixed(1)}pt horizontally`),s>0&&u.push(`${s.toFixed(1)}pt vertically`);let d=s>0?' (Remember: leave 0.5" margin at bottom of slide)':"";a.push(`HTML content overflows body by ${u.join(" and ")}${d}`)}return{...i,errors:a}}function Uc(e=document){let r=e,t=r.defaultView||window,n=.75,i=96,a=["impact"],o=h=>{if(!h)return!1;let U=h.toLowerCase().replace(/['"]/g,"").split(",")[0].trim();return a.includes(U)},l=h=>h/i,c=h=>parseFloat(h)*n,s=h=>{if(h==="rgba(0, 0, 0, 0)"||h==="transparent")return"FFFFFF";let U=h.match(/rgba?\((\d+),\s*(\d+),\s*(\d+)/);return U?U.slice(1).map(z=>parseInt(z).toString(16).padStart(2,"0")).join(""):"FFFFFF"},u=h=>{let U=h.match(/rgba\((\d+),\s*(\d+),\s*(\d+),\s*([\d.]+)\)/);if(!U||!U[4])return null;let z=parseFloat(U[4]);return Math.round((1-z)*100)},d=h=>{let U=String(h||"").match(/rgba?\((\d+),\s*(\d+),\s*(\d+)(?:,\s*([\d.]+))?\)/);return U?{r:parseInt(U[1],10),g:parseInt(U[2],10),b:parseInt(U[3],10),a:U[4]!=null?parseFloat(U[4]):1}:null},A=h=>{let U=String(h||"0E0E12").replace("#","");return{r:parseInt(U.slice(0,2),16),g:parseInt(U.slice(2,4),16),b:parseInt(U.slice(4,6),16)}},f=(h,U="0E0E12")=>{let z=d(h);if(!z)return{fill:s(h),transparency:u(h)};if(z.a>=.98)return{fill:s(h),transparency:null};let k=A(U),w=Math.round(k.r*(1-z.a)+z.r*z.a),D=Math.round(k.g*(1-z.a)+z.g*z.a),K=Math.round(k.b*(1-z.a)+z.b*z.a);return{fill:[w,D,K].map(Y=>Y.toString(16).padStart(2,"0")).join("").toUpperCase(),transparency:null}},p=(h,U)=>U==="uppercase"?h.toUpperCase():U==="lowercase"?h.toLowerCase():U==="capitalize"?h.replace(/\b\w/g,z=>z.toUpperCase()):h,g=h=>h==="vertical-rl"||h==="vertical-lr"?"eaVert":null,y=h=>{let U=0;if(h&&h!=="none"){let z=h.match(/rotate\((-?\d+(?:\.\d+)?)deg\)/);if(z)U+=parseFloat(z[1]);else{let k=h.match(/matrix\(([^)]+)\)/);if(k){let w=k[1].split(",").map(parseFloat),D=Math.atan2(w[1],w[0])*(180/Math.PI);U+=Math.round(D)}}}return U=U%360,U<0&&(U+=360),U===0?null:U},v=(h,U,z)=>{if(z===null)return{x:U.left,y:U.top,w:U.width,h:U.height};if(z===90||z===270){let K=U.left+U.width/2,Y=U.top+U.height/2;return{x:K-U.height/2,y:Y-U.width/2,w:U.height,h:U.width}}let w=U.left+U.width/2,D=U.top+U.height/2;return{x:w-h.offsetWidth/2,y:D-h.offsetHeight/2,w:h.offsetWidth,h:h.offsetHeight}},b=h=>{if(!h||h==="none"||h.match(/inset/))return null;let z=h.match(/rgba?\([^)]+\)/),k=h.match(/([-\d.]+)(px|pt)/g);if(!k||k.length<2)return null;let w=parseFloat(k[0]),D=parseFloat(k[1]),K=k.length>2?parseFloat(k[2]):0,Y=0;(w!==0||D!==0)&&(Y=Math.atan2(D,w)*(180/Math.PI),Y<0&&(Y+=360));let M=Math.sqrt(w*w+D*D)*n,J=.5;if(z){let ae=z[0].match(/[\d.]+\)$/);ae&&(J=parseFloat(ae[0].replace(")","")))}return{type:"outer",angle:Math.round(Y),blur:K*.75,color:z?s(z[0]):"000000",offset:M,opacity:J}},x=(h,U={},z=[],k=w=>w)=>{let w=!1;return h.childNodes.forEach(D=>{let K=k,Y=D.nodeType===Node.TEXT_NODE||D.tagName==="BR";if(Y){let M=D.tagName==="BR"?` -`:K(D.textContent.replace(/\s+/g," ")),J=z[z.length-1];w&&J?J.text+=M:z.push({text:M,options:{...U}})}else if(D.nodeType===Node.ELEMENT_NODE&&D.textContent.trim()){let M={...U},J=t.getComputedStyle(D);if(["SPAN","B","STRONG","I","EM","U","SMALL","LABEL","A","CODE","MARK","SUB","SUP"].includes(D.tagName)){if((J.fontWeight==="bold"||parseInt(J.fontWeight)>=600)&&!o(J.fontFamily)&&(M.bold=!0),J.fontStyle==="italic"&&(M.italic=!0),J.textDecoration&&J.textDecoration.includes("underline")&&(M.underline=!0),J.color&&J.color!=="rgb(0, 0, 0)"){M.color=s(J.color);let ee=u(J.color);ee!==null&&(M.transparency=ee)}if(J.fontSize&&(M.fontSize=c(J.fontSize)),J.textTransform&&J.textTransform!=="none"){let ee=J.textTransform;K=pe=>p(pe,ee)}J.marginLeft&&parseFloat(J.marginLeft)>0&&ne.push(`Inline element <${D.tagName.toLowerCase()}> has margin-left which is not supported in PowerPoint. Remove margin from inline elements.`),J.marginRight&&parseFloat(J.marginRight)>0&&ne.push(`Inline element <${D.tagName.toLowerCase()}> has margin-right which is not supported in PowerPoint. Remove margin from inline elements.`),J.marginTop&&parseFloat(J.marginTop)>0&&ne.push(`Inline element <${D.tagName.toLowerCase()}> has margin-top which is not supported in PowerPoint. Remove margin from inline elements.`),J.marginBottom&&parseFloat(J.marginBottom)>0&&ne.push(`Inline element <${D.tagName.toLowerCase()}> has margin-bottom which is not supported in PowerPoint. Remove margin from inline elements.`),x(D,M,z,K)}}w=Y}),z.length>0&&(z[0].text=z[0].text.replace(/^\s+/,""),z[z.length-1].text=z[z.length-1].text.replace(/\s+$/,"")),z.filter(D=>D.text.length>0)},P=h=>!h||h==="transparent"||h==="rgba(0, 0, 0, 0)",F=h=>{let U=[h,r.documentElement,h?.querySelector?.(":scope > section, :scope > div, :scope > main")].filter(Boolean);for(let z of U){let k=t.getComputedStyle(z),w=k.backgroundImage||"";if(w.includes("linear-gradient")||w.includes("radial-gradient"))return{gradient:!0};if(w&&w!=="none"){let K=w.match(/url\(["']?([^"')]+)["']?\)/);if(K)return{type:"image",path:K[1]}}let D=k.backgroundColor;if(!P(D))return{type:"color",value:s(D)}}return{type:"color",value:"FFFFFF"}},R=r.body,E=R.getBoundingClientRect(),B=h=>({left:h.left-E.left,top:h.top-E.top,width:h.width,height:h.height}),T=h=>B(h.getBoundingClientRect()),W=h=>{try{let U=r.createRange();U.selectNodeContents(h);let z=B(U.getBoundingClientRect());if(U.detach?.(),z.width>0&&z.height>0)return z}catch{}return T(h)},H=(h,U)=>{let z=d(h.color);return!z||z.a>=.2||!U.textContent.trim()?s(h.color):"E8E8E8"},C=(h,U,z)=>{let{x:k,y:w,w:D,h:K}=v(h,U,z),Y=E.width,M=E.height,J=Math.max(8,Y-k-4),ae=h.scrollHeight||0,ee=/^H[1-6]$/.test(h.tagName),pe=ee?Math.min(Math.max(8,D*.05),32):Math.min(Math.max(2,D*.02),12);ee?D=Math.max(D+pe,Y*.92-k):D=Math.min(D+pe,J),D=Math.min(D,J);let Ee=Math.max(6,K*(ee?.18:.12)),ve=Math.max(8,M-w-4);return ae>K+2?K=Math.min(ae+Ee,ve):K=Math.min(K+Ee,ve),{x:k,y:w,w:D,h:K}},I=h=>{let U=t.getComputedStyle(h).zIndex;if(!U||U==="auto")return 0;let z=parseInt(U,10);return Number.isFinite(z)?z:0},m=(h,U)=>{U&&(h.zIndex=I(U)),ue.push(h)},N=(h,U,z)=>{let w=t.getComputedStyle(h).listStyleColor;if(w&&w!=="rgba(0, 0, 0, 0)"){let D=s(w);if(D&&D!==z)return D}for(let D of U)try{let K=t.getComputedStyle(D,"::marker");if(K?.color){let Y=s(K.color);if(Y&&Y!==z)return Y}}catch{}return null},ne=[],q=F(R);q.gradient&&ne.push("CSS gradients are not supported. Use Sharp to rasterize gradients as PNG images first, then reference with background-image: url('gradient.png')");let se=q.gradient?{type:"color",value:"FFFFFF"}:{type:q.type,...q.path?{path:q.path}:{value:q.value}},Z=se.value||"0E0E12",ue=[],j=[],_=["P","H1","H2","H3","H4","H5","H6","UL","OL","LI"],fe=new Set(["SPAN","SMALL","LABEL","A","CODE","BUTTON","B","STRONG","I","EM","U","MARK","SUB","SUP"]),ce=new Set(["TD","TH"]),ie=new Set(["DIV","SECTION","ARTICLE","ASIDE",...ce]),be=new Set,Re=h=>{let U=h.parentElement;for(;U&&U!==R;){if(_.includes(U.tagName)||U.dataset?.pptxMerge==="true")return!0;U=U.parentElement}return!1},ke=h=>Array.from(h.childNodes).some(U=>U.nodeType===Node.TEXT_NODE&&U.textContent.replace(/\s+/g," ").trim()),xe=h=>!h.textContent.replace(/\s+/g," ").trim()||Re(h)?!1:fe.has(h.tagName)?!0:!ie.has(h.tagName)||!ke(h)&&!ce.has(h.tagName)||h.querySelector("p,h1,h2,h3,h4,h5,h6,ul,ol,li")?!1:!Array.from(h.children).some(U=>ie.has(U.tagName)),Oe=h=>{if(h.textAlign&&!["start","auto"].includes(h.textAlign))return h.textAlign==="end"?"right":h.textAlign;if(h.display.includes("flex")){if(h.justifyContent==="center")return"center";if(h.justifyContent==="flex-end"||h.justifyContent==="end")return"right"}if(h.display.includes("grid")){if((h.justifyItems||h.placeItems||"").includes("center"))return"center";if((h.justifyItems||h.placeItems||"").includes("end"))return"right"}return"left"},we=(h,U)=>{if(h.display==="table-cell"){if(h.verticalAlign==="middle")return"mid";if(h.verticalAlign==="bottom")return"bottom"}if(!h.display.includes("flex")&&!h.display.includes("grid"))return parseFloat(h.lineHeight||"0")>=U.height*.75?"mid":"top";let z=h.alignItems||h.placeItems||"";return z.includes("center")?"mid":z.includes("end")?"bottom":"top"},tt=(h,U=h.tagName.toLowerCase(),z=!1,k=null)=>{let w=k||T(h),D=h.textContent.replace(/\s+/g," ").trim();if(w.width===0||w.height===0||!D)return!1;if(U!=="text"&&h.tagName!=="LI"&&/^[•\-\*▪▸○●◆◇■□]\s/.test(D.trimStart()))return ne.push(`Text element <${h.tagName.toLowerCase()}> starts with bullet symbol "${D.substring(0,20)}...". Use
    or
      lists instead of manual bullet symbols.`),!1;let K=t.getComputedStyle(h);if(K.display==="none"||K.visibility==="hidden"||parseFloat(K.opacity||"1")<=0)return!1;let Y=y(K.transform),M=g(K.writingMode),J=z?v(h,w,Y):C(h,w,Y),ae=K.fontWeight==="bold"||parseInt(K.fontWeight,10)>=600,ee=K.lineHeight&&K.lineHeight!=="normal"?c(K.lineHeight):null,pe={fontSize:c(K.fontSize),fontFace:K.fontFamily.split(",")[0].replace(/['"]/g,"").trim(),color:H(K,h),align:Oe(K),valign:z?we(K,w):"top",lineSpacing:ee,paraSpaceBefore:c(K.marginTop),paraSpaceAfter:c(K.marginBottom),margin:[c(K.paddingLeft),c(K.paddingRight),c(K.paddingBottom),c(K.paddingTop)]},Ee=u(K.color);if(Ee!==null&&(pe.transparency=Ee),Y!==null&&(pe.rotate=Y),M!==null&&(pe.vert=M),h.querySelector("b, i, u, strong, em, span, small, label, a, code, mark, sub, sup, br")){let Ie=K.textTransform,rt={};ae&&!o(K.fontFamily)&&(rt.bold=!0);let Me=x(h,rt,[],me=>p(me,Ie));!Me.map(me=>me.text).join("").trim()&&D&&(Me=[{text:p(D,Ie),options:{...rt}}]);let oe={...pe};if(oe.lineSpacing){let me=Math.max(oe.fontSize,...Me.map(De=>De.options?.fontSize||0));me>oe.fontSize&&(oe.lineSpacing=me*(oe.lineSpacing/oe.fontSize))}m({type:U,text:Me,position:{x:l(J.x),y:l(J.y),w:l(J.w),h:l(J.h)},style:oe},h)}else m({type:U,text:p(D,K.textTransform),position:{x:l(J.x),y:l(J.y),w:l(J.w),h:l(J.h)},style:{...pe,bold:ae&&!o(K.fontFamily),italic:K.fontStyle==="italic",underline:K.textDecoration.includes("underline")}},h);return be.add(h),U==="text"&&h.querySelectorAll("span,small,label,a,code,b,strong,i,em,u,mark,sub,sup").forEach(Ie=>be.add(Ie)),!0};r.querySelectorAll("*").forEach(h=>{if(be.has(h))return;if(h.tagName==="DIV"&&h.dataset&&h.dataset.pptxMerge==="true"){let k=T(h);if(k.width===0||k.height===0){be.add(h);return}if(h.querySelector('[data-pptx-merge="true"]')){ne.push("data-pptx-merge container cannot contain another data-pptx-merge container. Nested merge is not supported."),be.add(h);return}let w=t.getComputedStyle(h);if(w.backgroundImage&&w.backgroundImage!=="none"){ne.push("Background images on data-pptx-merge container are not supported. Use solid colors or borders, or layer images via slide.addImage().");return}let D=w.backgroundColor&&w.backgroundColor!=="rgba(0, 0, 0, 0)",K=[w.borderTopWidth,w.borderRightWidth,w.borderBottomWidth,w.borderLeftWidth].map(ve=>parseFloat(ve)||0),M=K.some(ve=>ve>0)&&K.every(ve=>ve===K[0]);(D||M)&&ue.push({type:"shape",text:"",position:{x:l(k.left),y:l(k.top),w:l(k.width),h:l(k.height)},shape:{fill:D?s(w.backgroundColor):null,transparency:D?u(w.backgroundColor):null,line:M?{color:s(w.borderColor),width:c(w.borderWidth)}:null,rectRadius:(()=>{let ve=w.borderRadius,Ie=parseFloat(ve);if(Ie===0)return 0;if(ve.includes("%")){if(Ie>=50)return 1;let rt=Math.min(k.width,k.height);return Ie/100*l(rt)}return ve.includes("pt")?Ie/72:Ie/i})(),shadow:b(w.boxShadow)}});let J=Array.from(h.querySelectorAll("p, h1, h2, h3, h4, h5, h6"));if(J.length===0){ne.push("data-pptx-merge container has no

      / children to merge. Remove the data-pptx-merge attribute or add text elements."),be.add(h);return}let ae=t.getComputedStyle(J[0]),ee={fontSize:c(ae.fontSize),fontFace:ae.fontFamily.split(",")[0].replace(/['"]/g,"").trim(),color:s(ae.color),align:ae.textAlign==="start"?"left":ae.textAlign,lineSpacing:ae.lineHeight&&ae.lineHeight!=="normal"?c(ae.lineHeight):null,paraSpaceBefore:0,paraSpaceAfter:0,margin:[c(w.paddingLeft),c(w.paddingRight),c(w.paddingBottom),c(w.paddingTop)]},pe=u(ae.color);pe!==null&&(ee.transparency=pe);let Ee=[];if(J.forEach((ve,Ie)=>{let rt=Ie===J.length-1,Me=t.getComputedStyle(ve),G=Me.textTransform,oe=c(Me.fontSize),me=Me.fontFamily.split(",")[0].replace(/['"]/g,"").trim(),De=s(Me.color),te=Me.fontWeight==="bold"||parseInt(Me.fontWeight)>=600,ge=Me.fontStyle==="italic",L=Me.textDecoration.includes("underline"),ye={};oe!==ee.fontSize&&(ye.fontSize=oe),me!==ee.fontFace&&(ye.fontFace=me),De!==ee.color&&(ye.color=De),te&&!o(Me.fontFamily)&&(ye.bold=!0),ge&&(ye.italic=!0),L&&(ye.underline=!0);let Ge=ve.querySelector("b, i, u, strong, em, span, small, label, a, code, mark, sub, sup, br"),S;if(Ge)S=x(ve,ye,[],de=>p(de,G));else{let de=p(ve.textContent.trim(),G);if(!de)return;S=[{text:de,options:{...ye}}]}S.length>0&&!rt&&(S[S.length-1].options.breakLine=!0),Ee.push(...S),be.add(ve)}),Ee.length===0){be.add(h);return}ue.push({type:"merged-text",items:Ee,position:{x:l(k.left),y:l(k.top),w:l(k.width),h:l(k.height)},style:ee}),be.add(h);return}let U=xe(h)?tt(h,"text",!0,fe.has(h.tagName)?W(h):null):!1;if(_.includes(h.tagName)||fe.has(h.tagName)){let k=t.getComputedStyle(h),w=k.backgroundColor&&k.backgroundColor!=="rgba(0, 0, 0, 0)",D=k.borderWidth&&parseFloat(k.borderWidth)>0||k.borderTopWidth&&parseFloat(k.borderTopWidth)>0||k.borderRightWidth&&parseFloat(k.borderRightWidth)>0||k.borderBottomWidth&&parseFloat(k.borderBottomWidth)>0||k.borderLeftWidth&&parseFloat(k.borderLeftWidth)>0,K=k.boxShadow&&k.boxShadow!=="none";if(w||D||K){let Y=T(h);if(Y.width>0&&Y.height>0){let M=[k.borderTopWidth,k.borderRightWidth,k.borderBottomWidth,k.borderLeftWidth].map(ee=>parseFloat(ee)||0),J=M.some(ee=>ee>0)&&M.every(ee=>ee===M[0]),ae=w?f(k.backgroundColor,Z):{fill:null,transparency:null};if(ae.fill||J){let ee=k.borderRadius,pe=parseFloat(ee);m({type:"shape",text:"",position:{x:l(Y.left),y:l(Y.top),w:l(Y.width),h:l(Y.height)},shape:{fill:ae.fill,transparency:ae.transparency,line:J?{color:s(k.borderColor),width:c(k.borderWidth)}:null,rectRadius:(()=>{if(!pe)return 0;if(ee.includes("%")){if(pe>=50)return 1;let Ee=Math.min(Y.width,Y.height);return pe/100*l(Ee)}return ee.includes("pt")?pe/72:pe/i})(),shadow:b(k.boxShadow)}},h)}}}}if(h.className&&h.className.includes("placeholder")){let k=T(h);k.width===0||k.height===0?ne.push(`Placeholder "${h.id||"unnamed"}" has ${k.width===0?"width: 0":"height: 0"}. Check the layout CSS.`):j.push({id:h.id||`placeholder-${j.length}`,x:l(k.left),y:l(k.top),w:l(k.width),h:l(k.height)}),be.add(h);return}if(h.tagName==="IMG"){let k=T(h);if(k.width>0&&k.height>0){ue.push({type:"image",src:h.src,position:{x:l(k.left),y:l(k.top),w:l(k.width),h:l(k.height)}}),be.add(h);return}}if(ie.has(h.tagName)){let k=t.getComputedStyle(h),w=k.backgroundColor&&k.backgroundColor!=="rgba(0, 0, 0, 0)",D=k.backgroundImage;if(D&&D!=="none"){ne.push("Background images on DIV elements are not supported. Use solid colors or borders for shapes, or use slide.addImage() in PptxGenJS to layer images.");return}let K=k.borderTopWidth,Y=k.borderRightWidth,M=k.borderBottomWidth,J=k.borderLeftWidth,ae=[K,Y,M,J].map(ve=>parseFloat(ve)||0),ee=ae.some(ve=>ve>0),pe=ee&&ae.every(ve=>ve===ae[0]),Ee=[];if(ee&&!pe){let ve=T(h),Ie=l(ve.left),rt=l(ve.top),Me=l(ve.width),G=l(ve.height);if(parseFloat(K)>0){let oe=c(K),me=oe/72/2;Ee.push({type:"line",x1:Ie,y1:rt+me,x2:Ie+Me,y2:rt+me,width:oe,color:s(k.borderTopColor)})}if(parseFloat(Y)>0){let oe=c(Y),me=oe/72/2;Ee.push({type:"line",x1:Ie+Me-me,y1:rt,x2:Ie+Me-me,y2:rt+G,width:oe,color:s(k.borderRightColor)})}if(parseFloat(M)>0){let oe=c(M),me=oe/72/2;Ee.push({type:"line",x1:Ie,y1:rt+G-me,x2:Ie+Me,y2:rt+G-me,width:oe,color:s(k.borderBottomColor)})}if(parseFloat(J)>0){let oe=c(J),me=oe/72/2;Ee.push({type:"line",x1:Ie+me,y1:rt,x2:Ie+me,y2:rt+G,width:oe,color:s(k.borderLeftColor)})}}if(w||ee){let ve=T(h);if(ve.width>=E.width*.97&&ve.height>=E.height*.97&&w){be.add(h);return}if(ve.width>0&&ve.height>0){let rt=b(k.boxShadow);if(w||pe){let Me=w?f(k.backgroundColor,Z):{fill:null,transparency:null},G=Me.fill,oe=Me.transparency;!G&&pe&&(G=s(k.borderColor)||"2A2A30",oe=oe??88),m({type:"shape",text:"",position:{x:l(ve.left),y:l(ve.top),w:l(ve.width),h:l(ve.height)},shape:{fill:G,transparency:oe,line:pe?{color:s(k.borderColor),width:c(k.borderWidth)}:null,rectRadius:(()=>{let me=k.borderRadius,De=parseFloat(me);if(De===0)return 0;if(me.includes("%")){if(De>=50)return 1;let te=Math.min(ve.width,ve.height);return De/100*l(te)}return me.includes("pt")?De/72:De/i})(),shadow:rt}},h)}Ee.forEach(Me=>m(Me,h)),be.add(h);return}}}if(!U){if(h.tagName==="UL"||h.tagName==="OL"){let k=T(h);if(k.width===0||k.height===0)return;let w=Array.from(h.querySelectorAll("li")),D=[],K=t.getComputedStyle(h),Y=c(K.paddingLeft),M=Y*.5,J=Y*.5,ae=t.getComputedStyle(w[0]||h),ee=s(ae.color),pe=N(h,w,ee);w.forEach((ve,Ie)=>{let rt=Ie===w.length-1,Me=x(ve,{breakLine:!1});Me.length>0&&(Me[0].text=Me[0].text.replace(/^[•\-\*▪▸]\s*/,""),Me[0].options.bullet={indent:J}),Me.length>0&&pe&&pe!==ee&&Me.unshift({text:"\u200B",options:{bullet:{indent:J},color:pe,fontSize:Me[0]?.options?.fontSize||c(ae.fontSize),breakLine:!1}}),Me.length>0&&!rt&&(Me[Me.length-1].options.breakLine=!0),D.push(...Me)});let Ee=C(h,k,null);m({type:"list",items:D,position:{x:l(Ee.x),y:l(Ee.y),w:l(Ee.w),h:l(Ee.h)},style:{fontSize:c(ae.fontSize),fontFace:ae.fontFamily.split(",")[0].replace(/['"]/g,"").trim(),color:ee,bulletColor:pe,transparency:u(ae.color),align:ae.textAlign==="start"?"left":ae.textAlign,lineSpacing:ae.lineHeight&&ae.lineHeight!=="normal"?c(ae.lineHeight):null,paraSpaceBefore:0,paraSpaceAfter:c(ae.marginBottom),margin:[M,0,0,0]}},h),w.forEach(ve=>be.add(ve)),be.add(h);return}_.includes(h.tagName)&&tt(h)}});let Be=h=>h==="shape"?0:h==="line"?1:h==="image"?2:3;return ue.sort((h,U)=>{let z=(h.zIndex??0)-(U.zIndex??0);return z!==0?z:Be(h.type)-Be(U.type)}),{background:se,elements:ue,placeholders:j,errors:ne}}var ar={width:1280,height:720};function Mh(e){return(e?.elements||[]).filter(r=>Oh.has(r.type)).length}var Oh=new Set(["p","h1","h2","h3","h4","h5","h6","text","list","merged-text"]);var Gr=null;function _h(){return Gr?.isConnected||(Gr=document.createElement("div"),Gr.id="ppt-export-session-host",Gr.setAttribute("aria-hidden","true"),Gr.style.cssText=["position:fixed","left:-24000px","top:0","width:1px","height:1px","overflow:hidden","opacity:0","pointer-events:none","z-index:-1","contain:strict"].join(";"),document.body.appendChild(Gr)),Gr}function Uh(){Gr?.isConnected&&Gr.replaceChildren("")}function Wh(e){return As(e,".ppt-export-root",".ppt-export-body")}function Gh(e,r){return{body:r,documentElement:e,defaultView:window,querySelectorAll:t=>e.querySelectorAll(t),createElement:t=>document.createElement(t),getElementById:t=>e.querySelector(`#${t}`),head:e.querySelector("style")?.parentElement||e,_exportRoot:e}}function jh(){let e=document.createElement("div");e.className="ppt-export-root-host",e.setAttribute("aria-hidden","true"),e.style.cssText=[`width:${ar.width}px`,`height:${ar.height}px`,"overflow:hidden"].join(";"),_h().appendChild(e);let r=e.attachShadow({mode:"open"}),t=document.createElement("div");return t.className="ppt-export-root",t.style.cssText=[`width:${ar.width}px`,`height:${ar.height}px`,"overflow:hidden"].join(";"),r.appendChild(t),t._exportHost=e,t}function qh(e){let r=e?._exportHost||e;r?.isConnected&&r.remove()}async function Gc(){await new Promise(e=>{requestAnimationFrame(()=>requestAnimationFrame(e))})}function Vh(e,r){let t=new DOMParser().parseFromString(r,"text/html");e.replaceChildren(),t.querySelectorAll("style").forEach(i=>{let a=document.createElement("style");a.textContent=Wh(i.textContent||""),e.appendChild(a)});let n=document.createElement("div");if(n.className="ppt-export-body",t.body){for(let i of t.body.attributes)i.name==="class"?n.classList.add(...i.value.split(/\s+/).filter(Boolean)):i.name==="style"?n.style.cssText+=`;${i.value}`:n.setAttribute(i.name,i.value);n.innerHTML=t.body.innerHTML}return n.style.boxSizing="border-box",/\bwidth\s*:/i.test(n.style.cssText)||(n.style.width=`${ar.width}px`),/\bheight\s*:/i.test(n.style.cssText)||(n.style.height=`${ar.height}px`),e.appendChild(n),n}async function Xh(e){let r=Wr(e),t=jh(),n=Vh(t,r);return await Gc(),Gh(t,n)}async function Wc(e,r,t={}){let n=null;try{let i=await Xh(e);n=i._exportRoot,Oc(i,r),await Gc();let a=_c(i),o=Uc(i),l=a.errors||[];l.length&&console.warn("[ppt-live-export] slide overflows canvas; exporting anyway:",l.join("; "));let c={...a,errors:[]},s=o.errors||[];return!s.length||t.allowValidationErrors?{slideData:o,bodyDimensions:c,aggressive:r,warnings:l}:{error:new Error(s.join(` -`))}}finally{n&&qh(n)}}async function Hh(e,r={}){let t=await Wc(e,!1,r);if(t?.slideData)return t;let n=await Wc(e,!0,r);if(n?.slideData)return n;throw n?.error||t?.error||new Error("PPT Live slide preparation failed")}async function jc(e,r={}){let t=[];try{for(let[n,i]of e.entries()){if(!i?.html)continue;let a=await Hh(i.html,r),o=null,c=Mh(a.slideData)===0;if(c&&typeof r.renderRaster=="function")try{typeof r.onRasterProgress=="function"&&r.onRasterProgress(n,i);let s=bs(i);o=await r.renderRaster(s,n)}catch{o=null}t.push({index:n,slideId:i.id,notes:i,...a,rasterBase64:o,rasterOnly:!!(o&&c)})}return t}finally{Uh()}}function Kh(e={}){let r=e.theme||{},t=String(e.title||"Slide").replace(/[<>&]/g,l=>({"<":"<",">":">","&":"&"})[l]||l),n=String(e.subtitle||e.claim||"").replace(/[<>&]/g,l=>({"<":"<",">":">","&":"&"})[l]||l),i=r.background||"#ffffff",a=r.ink||"#111111",o=r.muted||"#666666";return` +`:K(D.textContent.replace(/\s+/g," ")),J=z[z.length-1];w&&J?J.text+=M:z.push({text:M,options:{...U}})}else if(D.nodeType===Node.ELEMENT_NODE&&D.textContent.trim()){let M={...U},J=t.getComputedStyle(D);if(["SPAN","B","STRONG","I","EM","U","SMALL","LABEL","A","CODE","MARK","SUB","SUP"].includes(D.tagName)){if((J.fontWeight==="bold"||parseInt(J.fontWeight)>=600)&&!o(J.fontFamily)&&(M.bold=!0),J.fontStyle==="italic"&&(M.italic=!0),J.textDecoration&&J.textDecoration.includes("underline")&&(M.underline=!0),J.color&&J.color!=="rgb(0, 0, 0)"){M.color=s(J.color);let ee=u(J.color);ee!==null&&(M.transparency=ee)}if(J.fontSize&&(M.fontSize=c(J.fontSize)),J.textTransform&&J.textTransform!=="none"){let ee=J.textTransform;K=pe=>p(pe,ee)}J.marginLeft&&parseFloat(J.marginLeft)>0&&ne.push(`Inline element <${D.tagName.toLowerCase()}> has margin-left which is not supported in PowerPoint. Remove margin from inline elements.`),J.marginRight&&parseFloat(J.marginRight)>0&&ne.push(`Inline element <${D.tagName.toLowerCase()}> has margin-right which is not supported in PowerPoint. Remove margin from inline elements.`),J.marginTop&&parseFloat(J.marginTop)>0&&ne.push(`Inline element <${D.tagName.toLowerCase()}> has margin-top which is not supported in PowerPoint. Remove margin from inline elements.`),J.marginBottom&&parseFloat(J.marginBottom)>0&&ne.push(`Inline element <${D.tagName.toLowerCase()}> has margin-bottom which is not supported in PowerPoint. Remove margin from inline elements.`),x(D,M,z,K)}}w=Y}),z.length>0&&(z[0].text=z[0].text.replace(/^\s+/,""),z[z.length-1].text=z[z.length-1].text.replace(/\s+$/,"")),z.filter(D=>D.text.length>0)},P=h=>!h||h==="transparent"||h==="rgba(0, 0, 0, 0)",F=h=>{let U=[h,r.documentElement,h?.querySelector?.(":scope > section, :scope > div, :scope > main")].filter(Boolean);for(let z of U){let k=t.getComputedStyle(z),w=k.backgroundImage||"";if(w.includes("linear-gradient")||w.includes("radial-gradient"))return{gradient:!0};if(w&&w!=="none"){let K=w.match(/url\(["']?([^"')]+)["']?\)/);if(K)return{type:"image",path:K[1]}}let D=k.backgroundColor;if(!P(D))return{type:"color",value:s(D)}}return{type:"color",value:"FFFFFF"}},R=r.body,E=R.getBoundingClientRect(),B=h=>({left:h.left-E.left,top:h.top-E.top,width:h.width,height:h.height}),T=h=>B(h.getBoundingClientRect()),W=h=>{try{let U=r.createRange();U.selectNodeContents(h);let z=B(U.getBoundingClientRect());if(U.detach?.(),z.width>0&&z.height>0)return z}catch{}return T(h)},H=(h,U)=>{let z=d(h.color);return!z||z.a>=.2||!U.textContent.trim()?s(h.color):"E8E8E8"},C=(h,U,z)=>{let{x:k,y:w,w:D,h:K}=v(h,U,z),Y=E.width,M=E.height,J=Math.max(8,Y-k-4),ae=h.scrollHeight||0,ee=/^H[1-6]$/.test(h.tagName),pe=ee?Math.min(Math.max(8,D*.05),32):Math.min(Math.max(2,D*.02),12);ee?D=Math.max(D+pe,Y*.92-k):D=Math.min(D+pe,J),D=Math.min(D,J);let Ee=Math.max(6,K*(ee?.18:.12)),ve=Math.max(8,M-w-4);return ae>K+2?K=Math.min(ae+Ee,ve):K=Math.min(K+Ee,ve),{x:k,y:w,w:D,h:K}},I=h=>{let U=t.getComputedStyle(h).zIndex;if(!U||U==="auto")return 0;let z=parseInt(U,10);return Number.isFinite(z)?z:0},m=(h,U)=>{U&&(h.zIndex=I(U)),ue.push(h)},N=(h,U,z)=>{let w=t.getComputedStyle(h).listStyleColor;if(w&&w!=="rgba(0, 0, 0, 0)"){let D=s(w);if(D&&D!==z)return D}for(let D of U)try{let K=t.getComputedStyle(D,"::marker");if(K?.color){let Y=s(K.color);if(Y&&Y!==z)return Y}}catch{}return null},ne=[],q=F(R);q.gradient&&ne.push("CSS gradients are not supported. Use Sharp to rasterize gradients as PNG images first, then reference with background-image: url('gradient.png')");let se=q.gradient?{type:"color",value:"FFFFFF"}:{type:q.type,...q.path?{path:q.path}:{value:q.value}},Z=se.value||"0E0E12",ue=[],j=[],_=["P","H1","H2","H3","H4","H5","H6","UL","OL","LI"],fe=new Set(["SPAN","SMALL","LABEL","A","CODE","BUTTON","B","STRONG","I","EM","U","MARK","SUB","SUP"]),ce=new Set(["TD","TH"]),ie=new Set(["DIV","SECTION","ARTICLE","ASIDE",...ce]),be=new Set,Re=h=>{let U=h.parentElement;for(;U&&U!==R;){if(_.includes(U.tagName)||U.dataset?.pptxMerge==="true")return!0;U=U.parentElement}return!1},ke=h=>Array.from(h.childNodes).some(U=>U.nodeType===Node.TEXT_NODE&&U.textContent.replace(/\s+/g," ").trim()),xe=h=>!h.textContent.replace(/\s+/g," ").trim()||Re(h)?!1:fe.has(h.tagName)?!0:!ie.has(h.tagName)||!ke(h)&&!ce.has(h.tagName)||h.querySelector("p,h1,h2,h3,h4,h5,h6,ul,ol,li")?!1:!Array.from(h.children).some(U=>ie.has(U.tagName)),Oe=h=>{if(h.textAlign&&!["start","auto"].includes(h.textAlign))return h.textAlign==="end"?"right":h.textAlign;if(h.display.includes("flex")){if(h.justifyContent==="center")return"center";if(h.justifyContent==="flex-end"||h.justifyContent==="end")return"right"}if(h.display.includes("grid")){if((h.justifyItems||h.placeItems||"").includes("center"))return"center";if((h.justifyItems||h.placeItems||"").includes("end"))return"right"}return"left"},we=(h,U)=>{if(h.display==="table-cell"){if(h.verticalAlign==="middle")return"mid";if(h.verticalAlign==="bottom")return"bottom"}if(!h.display.includes("flex")&&!h.display.includes("grid"))return parseFloat(h.lineHeight||"0")>=U.height*.75?"mid":"top";let z=h.alignItems||h.placeItems||"";return z.includes("center")?"mid":z.includes("end")?"bottom":"top"},tt=(h,U=h.tagName.toLowerCase(),z=!1,k=null)=>{let w=k||T(h),D=h.textContent.replace(/\s+/g," ").trim();if(w.width===0||w.height===0||!D)return!1;if(U!=="text"&&h.tagName!=="LI"&&/^[•\-\*▪▸○●◆◇■□]\s/.test(D.trimStart()))return ne.push(`Text element <${h.tagName.toLowerCase()}> starts with bullet symbol "${D.substring(0,20)}...". Use

        or
          lists instead of manual bullet symbols.`),!1;let K=t.getComputedStyle(h);if(K.display==="none"||K.visibility==="hidden"||parseFloat(K.opacity||"1")<=0)return!1;let Y=y(K.transform),M=g(K.writingMode),J=z?v(h,w,Y):C(h,w,Y),ae=K.fontWeight==="bold"||parseInt(K.fontWeight,10)>=600,ee=K.lineHeight&&K.lineHeight!=="normal"?c(K.lineHeight):null,pe={fontSize:c(K.fontSize),fontFace:K.fontFamily.split(",")[0].replace(/['"]/g,"").trim(),color:H(K,h),align:Oe(K),valign:z?we(K,w):"top",lineSpacing:ee,paraSpaceBefore:c(K.marginTop),paraSpaceAfter:c(K.marginBottom),margin:[c(K.paddingLeft),c(K.paddingRight),c(K.paddingBottom),c(K.paddingTop)]},Ee=u(K.color);if(Ee!==null&&(pe.transparency=Ee),Y!==null&&(pe.rotate=Y),M!==null&&(pe.vert=M),h.querySelector("b, i, u, strong, em, span, small, label, a, code, mark, sub, sup, br")){let Ie=K.textTransform,rt={};ae&&!o(K.fontFamily)&&(rt.bold=!0);let Me=x(h,rt,[],me=>p(me,Ie));!Me.map(me=>me.text).join("").trim()&&D&&(Me=[{text:p(D,Ie),options:{...rt}}]);let oe={...pe};if(oe.lineSpacing){let me=Math.max(oe.fontSize,...Me.map(De=>De.options?.fontSize||0));me>oe.fontSize&&(oe.lineSpacing=me*(oe.lineSpacing/oe.fontSize))}m({type:U,text:Me,position:{x:l(J.x),y:l(J.y),w:l(J.w),h:l(J.h)},style:oe},h)}else m({type:U,text:p(D,K.textTransform),position:{x:l(J.x),y:l(J.y),w:l(J.w),h:l(J.h)},style:{...pe,bold:ae&&!o(K.fontFamily),italic:K.fontStyle==="italic",underline:K.textDecoration.includes("underline")}},h);return be.add(h),U==="text"&&h.querySelectorAll("span,small,label,a,code,b,strong,i,em,u,mark,sub,sup").forEach(Ie=>be.add(Ie)),!0};r.querySelectorAll("*").forEach(h=>{if(be.has(h))return;if(h.tagName==="DIV"&&h.dataset&&h.dataset.pptxMerge==="true"){let k=T(h);if(k.width===0||k.height===0){be.add(h);return}if(h.querySelector('[data-pptx-merge="true"]')){ne.push("data-pptx-merge container cannot contain another data-pptx-merge container. Nested merge is not supported."),be.add(h);return}let w=t.getComputedStyle(h);if(w.backgroundImage&&w.backgroundImage!=="none"){ne.push("Background images on data-pptx-merge container are not supported. Use solid colors or borders, or layer images via slide.addImage().");return}let D=w.backgroundColor&&w.backgroundColor!=="rgba(0, 0, 0, 0)",K=[w.borderTopWidth,w.borderRightWidth,w.borderBottomWidth,w.borderLeftWidth].map(ve=>parseFloat(ve)||0),M=K.some(ve=>ve>0)&&K.every(ve=>ve===K[0]);(D||M)&&ue.push({type:"shape",text:"",position:{x:l(k.left),y:l(k.top),w:l(k.width),h:l(k.height)},shape:{fill:D?s(w.backgroundColor):null,transparency:D?u(w.backgroundColor):null,line:M?{color:s(w.borderColor),width:c(w.borderWidth)}:null,rectRadius:(()=>{let ve=w.borderRadius,Ie=parseFloat(ve);if(Ie===0)return 0;if(ve.includes("%")){if(Ie>=50)return 1;let rt=Math.min(k.width,k.height);return Ie/100*l(rt)}return ve.includes("pt")?Ie/72:Ie/i})(),shadow:b(w.boxShadow)}});let J=Array.from(h.querySelectorAll("p, h1, h2, h3, h4, h5, h6"));if(J.length===0){ne.push("data-pptx-merge container has no

          / children to merge. Remove the data-pptx-merge attribute or add text elements."),be.add(h);return}let ae=t.getComputedStyle(J[0]),ee={fontSize:c(ae.fontSize),fontFace:ae.fontFamily.split(",")[0].replace(/['"]/g,"").trim(),color:s(ae.color),align:ae.textAlign==="start"?"left":ae.textAlign,lineSpacing:ae.lineHeight&&ae.lineHeight!=="normal"?c(ae.lineHeight):null,paraSpaceBefore:0,paraSpaceAfter:0,margin:[c(w.paddingLeft),c(w.paddingRight),c(w.paddingBottom),c(w.paddingTop)]},pe=u(ae.color);pe!==null&&(ee.transparency=pe);let Ee=[];if(J.forEach((ve,Ie)=>{let rt=Ie===J.length-1,Me=t.getComputedStyle(ve),G=Me.textTransform,oe=c(Me.fontSize),me=Me.fontFamily.split(",")[0].replace(/['"]/g,"").trim(),De=s(Me.color),te=Me.fontWeight==="bold"||parseInt(Me.fontWeight)>=600,ge=Me.fontStyle==="italic",L=Me.textDecoration.includes("underline"),ye={};oe!==ee.fontSize&&(ye.fontSize=oe),me!==ee.fontFace&&(ye.fontFace=me),De!==ee.color&&(ye.color=De),te&&!o(Me.fontFamily)&&(ye.bold=!0),ge&&(ye.italic=!0),L&&(ye.underline=!0);let Ge=ve.querySelector("b, i, u, strong, em, span, small, label, a, code, mark, sub, sup, br"),S;if(Ge)S=x(ve,ye,[],de=>p(de,G));else{let de=p(ve.textContent.trim(),G);if(!de)return;S=[{text:de,options:{...ye}}]}S.length>0&&!rt&&(S[S.length-1].options.breakLine=!0),Ee.push(...S),be.add(ve)}),Ee.length===0){be.add(h);return}ue.push({type:"merged-text",items:Ee,position:{x:l(k.left),y:l(k.top),w:l(k.width),h:l(k.height)},style:ee}),be.add(h);return}let U=xe(h)?tt(h,"text",!0,fe.has(h.tagName)?W(h):null):!1;if(_.includes(h.tagName)||fe.has(h.tagName)){let k=t.getComputedStyle(h),w=k.backgroundColor&&k.backgroundColor!=="rgba(0, 0, 0, 0)",D=k.borderWidth&&parseFloat(k.borderWidth)>0||k.borderTopWidth&&parseFloat(k.borderTopWidth)>0||k.borderRightWidth&&parseFloat(k.borderRightWidth)>0||k.borderBottomWidth&&parseFloat(k.borderBottomWidth)>0||k.borderLeftWidth&&parseFloat(k.borderLeftWidth)>0,K=k.boxShadow&&k.boxShadow!=="none";if(w||D||K){let Y=T(h);if(Y.width>0&&Y.height>0){let M=[k.borderTopWidth,k.borderRightWidth,k.borderBottomWidth,k.borderLeftWidth].map(ee=>parseFloat(ee)||0),J=M.some(ee=>ee>0)&&M.every(ee=>ee===M[0]),ae=w?f(k.backgroundColor,Z):{fill:null,transparency:null};if(ae.fill||J){let ee=k.borderRadius,pe=parseFloat(ee);m({type:"shape",text:"",position:{x:l(Y.left),y:l(Y.top),w:l(Y.width),h:l(Y.height)},shape:{fill:ae.fill,transparency:ae.transparency,line:J?{color:s(k.borderColor),width:c(k.borderWidth)}:null,rectRadius:(()=>{if(!pe)return 0;if(ee.includes("%")){if(pe>=50)return 1;let Ee=Math.min(Y.width,Y.height);return pe/100*l(Ee)}return ee.includes("pt")?pe/72:pe/i})(),shadow:b(k.boxShadow)}},h)}}}}if(h.className&&h.className.includes("placeholder")){let k=T(h);k.width===0||k.height===0?ne.push(`Placeholder "${h.id||"unnamed"}" has ${k.width===0?"width: 0":"height: 0"}. Check the layout CSS.`):j.push({id:h.id||`placeholder-${j.length}`,x:l(k.left),y:l(k.top),w:l(k.width),h:l(k.height)}),be.add(h);return}if(h.tagName==="IMG"){let k=T(h);if(k.width>0&&k.height>0){ue.push({type:"image",src:h.src,position:{x:l(k.left),y:l(k.top),w:l(k.width),h:l(k.height)}}),be.add(h);return}}if(ie.has(h.tagName)){let k=t.getComputedStyle(h),w=k.backgroundColor&&k.backgroundColor!=="rgba(0, 0, 0, 0)",D=k.backgroundImage;if(D&&D!=="none"){ne.push("Background images on DIV elements are not supported. Use solid colors or borders for shapes, or use slide.addImage() in PptxGenJS to layer images.");return}let K=k.borderTopWidth,Y=k.borderRightWidth,M=k.borderBottomWidth,J=k.borderLeftWidth,ae=[K,Y,M,J].map(ve=>parseFloat(ve)||0),ee=ae.some(ve=>ve>0),pe=ee&&ae.every(ve=>ve===ae[0]),Ee=[];if(ee&&!pe){let ve=T(h),Ie=l(ve.left),rt=l(ve.top),Me=l(ve.width),G=l(ve.height);if(parseFloat(K)>0){let oe=c(K),me=oe/72/2;Ee.push({type:"line",x1:Ie,y1:rt+me,x2:Ie+Me,y2:rt+me,width:oe,color:s(k.borderTopColor)})}if(parseFloat(Y)>0){let oe=c(Y),me=oe/72/2;Ee.push({type:"line",x1:Ie+Me-me,y1:rt,x2:Ie+Me-me,y2:rt+G,width:oe,color:s(k.borderRightColor)})}if(parseFloat(M)>0){let oe=c(M),me=oe/72/2;Ee.push({type:"line",x1:Ie,y1:rt+G-me,x2:Ie+Me,y2:rt+G-me,width:oe,color:s(k.borderBottomColor)})}if(parseFloat(J)>0){let oe=c(J),me=oe/72/2;Ee.push({type:"line",x1:Ie+me,y1:rt,x2:Ie+me,y2:rt+G,width:oe,color:s(k.borderLeftColor)})}}if(w||ee){let ve=T(h);if(ve.width>=E.width*.97&&ve.height>=E.height*.97&&w){be.add(h);return}if(ve.width>0&&ve.height>0){let rt=b(k.boxShadow);if(w||pe){let Me=w?f(k.backgroundColor,Z):{fill:null,transparency:null},G=Me.fill,oe=Me.transparency;!G&&pe&&(G=s(k.borderColor)||"2A2A30",oe=oe??88),m({type:"shape",text:"",position:{x:l(ve.left),y:l(ve.top),w:l(ve.width),h:l(ve.height)},shape:{fill:G,transparency:oe,line:pe?{color:s(k.borderColor),width:c(k.borderWidth)}:null,rectRadius:(()=>{let me=k.borderRadius,De=parseFloat(me);if(De===0)return 0;if(me.includes("%")){if(De>=50)return 1;let te=Math.min(ve.width,ve.height);return De/100*l(te)}return me.includes("pt")?De/72:De/i})(),shadow:rt}},h)}Ee.forEach(Me=>m(Me,h)),be.add(h);return}}}if(!U){if(h.tagName==="UL"||h.tagName==="OL"){let k=T(h);if(k.width===0||k.height===0)return;let w=Array.from(h.querySelectorAll("li")),D=[],K=t.getComputedStyle(h),Y=c(K.paddingLeft),M=Y*.5,J=Y*.5,ae=t.getComputedStyle(w[0]||h),ee=s(ae.color),pe=N(h,w,ee);w.forEach((ve,Ie)=>{let rt=Ie===w.length-1,Me=x(ve,{breakLine:!1});Me.length>0&&(Me[0].text=Me[0].text.replace(/^[•\-\*▪▸]\s*/,""),Me[0].options.bullet={indent:J}),Me.length>0&&pe&&pe!==ee&&Me.unshift({text:"\u200B",options:{bullet:{indent:J},color:pe,fontSize:Me[0]?.options?.fontSize||c(ae.fontSize),breakLine:!1}}),Me.length>0&&!rt&&(Me[Me.length-1].options.breakLine=!0),D.push(...Me)});let Ee=C(h,k,null);m({type:"list",items:D,position:{x:l(Ee.x),y:l(Ee.y),w:l(Ee.w),h:l(Ee.h)},style:{fontSize:c(ae.fontSize),fontFace:ae.fontFamily.split(",")[0].replace(/['"]/g,"").trim(),color:ee,bulletColor:pe,transparency:u(ae.color),align:ae.textAlign==="start"?"left":ae.textAlign,lineSpacing:ae.lineHeight&&ae.lineHeight!=="normal"?c(ae.lineHeight):null,paraSpaceBefore:0,paraSpaceAfter:c(ae.marginBottom),margin:[M,0,0,0]}},h),w.forEach(ve=>be.add(ve)),be.add(h);return}_.includes(h.tagName)&&tt(h)}});let Be=h=>h==="shape"?0:h==="line"?1:h==="image"?2:3;return ue.sort((h,U)=>{let z=(h.zIndex??0)-(U.zIndex??0);return z!==0?z:Be(h.type)-Be(U.type)}),{background:se,elements:ue,placeholders:j,errors:ne}}var ar={width:1280,height:720},Wh=new Set(["p","h1","h2","h3","h4","h5","h6","text","list","merged-text"]);function Gh(e){return(e?.elements||[]).filter(r=>Wh.has(r.type)).length}function jh(e){let r=Nr(e);if(r.includes('data-pptx-raster="1"')&&r.includes("pptx-raster-hide-text"))return r;let n=``;return/<\/head>/i.test(r)?r.replace(/<\/head>/i,`${n}`).replace(/e.querySelectorAll(t),createElement:t=>document.createElement(t),getElementById:t=>e.querySelector(`#${t}`),head:e.querySelector("style")?.parentElement||e,_exportRoot:e}}function Kh(){let e=document.createElement("div");e.className="ppt-export-root-host",e.setAttribute("aria-hidden","true"),e.style.cssText=[`width:${ar.width}px`,`height:${ar.height}px`,"overflow:hidden"].join(";"),qh().appendChild(e);let r=e.attachShadow({mode:"open"}),t=document.createElement("div");return t.className="ppt-export-root",t.style.cssText=[`width:${ar.width}px`,`height:${ar.height}px`,"overflow:hidden"].join(";"),r.appendChild(t),t._exportHost=e,t}function Qh(e){let r=e?._exportHost||e;r?.isConnected&&r.remove()}async function Gc(){await new Promise(e=>{requestAnimationFrame(()=>requestAnimationFrame(e))})}function Zh(e,r){let t=new DOMParser().parseFromString(r,"text/html");e.replaceChildren(),t.querySelectorAll("style").forEach(i=>{let a=document.createElement("style");a.textContent=Xh(i.textContent||""),e.appendChild(a)});let n=document.createElement("div");if(n.className="ppt-export-body",t.body){for(let i of t.body.attributes)i.name==="class"?n.classList.add(...i.value.split(/\s+/).filter(Boolean)):i.name==="style"?n.style.cssText+=`;${i.value}`:n.setAttribute(i.name,i.value);n.innerHTML=t.body.innerHTML}return n.style.boxSizing="border-box",/\bwidth\s*:/i.test(n.style.cssText)||(n.style.width=`${ar.width}px`),/\bheight\s*:/i.test(n.style.cssText)||(n.style.height=`${ar.height}px`),e.appendChild(n),n}async function Yh(e){let r=Nr(e),t=Kh(),n=Zh(t,r);return await Gc(),Hh(t,n)}async function Wc(e,r,t={}){let n=null;try{let i=await Yh(e);n=i._exportRoot,Oc(i,r),await Gc();let a=_c(i),o=Uc(i),l=a.errors||[];l.length&&console.warn("[ppt-live-export] slide overflows canvas; exporting anyway:",l.join("; "));let c={...a,errors:[]},s=o.errors||[];return!s.length||t.allowValidationErrors?{slideData:o,bodyDimensions:c,aggressive:r,warnings:l}:{error:new Error(s.join(` +`))}}finally{n&&Qh(n)}}async function Jh(e,r={}){let t=await Wc(e,!1,r);if(t?.slideData)return t;let n=await Wc(e,!0,r);if(n?.slideData)return n;throw n?.error||t?.error||new Error("PPT Live slide preparation failed")}async function jc(e,r={}){let t=[];try{for(let[n,i]of e.entries()){if(!i?.html)continue;let a=await Jh(i.html,r),o=null,c=Gh(a.slideData)===0;if(typeof r.renderRaster=="function")try{typeof r.onRasterProgress=="function"&&r.onRasterProgress(n,i);let s=c?ws(i):jh(i.html);o=await r.renderRaster(s,n)}catch{o=null}t.push({index:n,slideId:i.id,notes:i,...a,rasterBase64:o,rasterOnly:!!(o&&c)})}return t}finally{Vh()}}function $h(e={}){let r=e.theme||{},t=String(e.title||"Slide").replace(/[<>&]/g,l=>({"<":"<",">":">","&":"&"})[l]||l),n=String(e.subtitle||e.claim||"").replace(/[<>&]/g,l=>({"<":"<",">":">","&":"&"})[l]||l),i=r.background||"#ffffff",a=r.ink||"#111111",o=r.muted||"#666666";return` @@ -116,22 +156,22 @@ ${n.documentElement.outerHTML}`;return e._pptLiveSourceHtml=a,a}function Wr(e){l

          ${t}

          ${n?`

          ${n}

          `:""} -`}function bs(e){return e?.html?Wr(e.html):Kh(e)}var Qh=Object.create,Eu=Object.defineProperty,Zh=Object.getOwnPropertyDescriptor,Bu=Object.getOwnPropertyNames,Yh=Object.getPrototypeOf,Jh=Object.prototype.hasOwnProperty,An=(e=>typeof Dn<"u"?Dn:typeof Proxy<"u"?new Proxy(e,{get:(r,t)=>(typeof Dn<"u"?Dn:r)[t]}):e)(function(e){if(typeof Dn<"u")return Dn.apply(this,arguments);throw Error('Dynamic require of "'+e+'" is not supported')}),Nt=(e,r)=>function(){return r||(0,e[Bu(e)[0]])((r={exports:{}}).exports,r),r.exports},$h=(e,r,t,n)=>{if(r&&typeof r=="object"||typeof r=="function")for(let i of Bu(r))!Jh.call(e,i)&&i!==t&&Eu(e,i,{get:()=>r[i],enumerable:!(n=Zh(r,i))||n.enumerable});return e},Qr=(e,r,t)=>(t=e!=null?Qh(Yh(e)):{},$h(r||!e||!e.__esModule?Eu(t,"default",{value:e,enumerable:!0}):t,e)),Sn=Nt({"../../../node_modules/pako/lib/utils/common.js"(e){"use strict";var r=typeof Uint8Array<"u"&&typeof Uint16Array<"u"&&typeof Int32Array<"u";function t(a,o){return Object.prototype.hasOwnProperty.call(a,o)}e.assign=function(a){for(var o=Array.prototype.slice.call(arguments,1);o.length;){var l=o.shift();if(l){if(typeof l!="object")throw new TypeError(l+"must be non-object");for(var c in l)t(l,c)&&(a[c]=l[c])}}return a},e.shrinkBuf=function(a,o){return a.length===o?a:a.subarray?a.subarray(0,o):(a.length=o,a)};var n={arraySet:function(a,o,l,c,s){if(o.subarray&&a.subarray){a.set(o.subarray(l,l+c),s);return}for(var u=0;u=0;)G[oe]=0}var l=0,c=1,s=2,u=3,d=258,A=29,f=256,p=f+1+A,g=30,y=19,v=2*p+1,b=15,x=16,P=7,F=256,R=16,E=17,B=18,T=[0,0,0,0,0,0,0,0,1,1,1,1,2,2,2,2,3,3,3,3,4,4,4,4,5,5,5,5,0],W=[0,0,0,0,1,1,2,2,3,3,4,4,5,5,6,6,7,7,8,8,9,9,10,10,11,11,12,12,13,13],H=[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,2,3,7],C=[16,17,18,0,8,7,9,6,10,5,11,4,12,3,13,2,14,1,15],I=512,m=new Array((p+2)*2);o(m);var N=new Array(g*2);o(N);var ne=new Array(I);o(ne);var q=new Array(d-u+1);o(q);var se=new Array(A);o(se);var Z=new Array(g);o(Z);function ue(G,oe,me,De,te){this.static_tree=G,this.extra_bits=oe,this.extra_base=me,this.elems=De,this.max_length=te,this.has_stree=G&&G.length}var j,_,fe;function ce(G,oe){this.dyn_tree=G,this.max_code=0,this.stat_desc=oe}function ie(G){return G<256?ne[G]:ne[256+(G>>>7)]}function be(G,oe){G.pending_buf[G.pending++]=oe&255,G.pending_buf[G.pending++]=oe>>>8&255}function Re(G,oe,me){G.bi_valid>x-me?(G.bi_buf|=oe<>x-G.bi_valid,G.bi_valid+=me-x):(G.bi_buf|=oe<>>=1,me<<=1;while(--oe>0);return me>>>1}function Oe(G){G.bi_valid===16?(be(G,G.bi_buf),G.bi_buf=0,G.bi_valid=0):G.bi_valid>=8&&(G.pending_buf[G.pending++]=G.bi_buf&255,G.bi_buf>>=8,G.bi_valid-=8)}function we(G,oe){var me=oe.dyn_tree,De=oe.max_code,te=oe.stat_desc.static_tree,ge=oe.stat_desc.has_stree,L=oe.stat_desc.extra_bits,ye=oe.stat_desc.extra_base,Ge=oe.stat_desc.max_length,S,de,Ae,O,re,he,_e=0;for(O=0;O<=b;O++)G.bl_count[O]=0;for(me[G.heap[G.heap_max]*2+1]=0,S=G.heap_max+1;SGe&&(O=Ge,_e++),me[de*2+1]=O,!(de>De)&&(G.bl_count[O]++,re=0,de>=ye&&(re=L[de-ye]),he=me[de*2],G.opt_len+=he*(O+re),ge&&(G.static_len+=he*(te[de*2+1]+re)));if(_e!==0){do{for(O=Ge-1;G.bl_count[O]===0;)O--;G.bl_count[O]--,G.bl_count[O+1]+=2,G.bl_count[Ge]--,_e-=2}while(_e>0);for(O=Ge;O!==0;O--)for(de=G.bl_count[O];de!==0;)Ae=G.heap[--S],!(Ae>De)&&(me[Ae*2+1]!==O&&(G.opt_len+=(O-me[Ae*2+1])*me[Ae*2],me[Ae*2+1]=O),de--)}}function tt(G,oe,me){var De=new Array(b+1),te=0,ge,L;for(ge=1;ge<=b;ge++)De[ge]=te=te+me[ge-1]<<1;for(L=0;L<=oe;L++){var ye=G[L*2+1];ye!==0&&(G[L*2]=xe(De[ye]++,ye))}}function Be(){var G,oe,me,De,te,ge=new Array(b+1);for(me=0,De=0;De>=7;De8?be(G,G.bi_buf):G.bi_valid>0&&(G.pending_buf[G.pending++]=G.bi_buf),G.bi_buf=0,G.bi_valid=0}function z(G,oe,me,De){U(G),De&&(be(G,me),be(G,~me)),r.arraySet(G.pending_buf,G.window,oe,me,G.pending),G.pending+=me}function k(G,oe,me,De){var te=oe*2,ge=me*2;return G[te]>1;L>=1;L--)w(G,me,L);S=ge;do L=G.heap[1],G.heap[1]=G.heap[G.heap_len--],w(G,me,1),ye=G.heap[1],G.heap[--G.heap_max]=L,G.heap[--G.heap_max]=ye,me[S*2]=me[L*2]+me[ye*2],G.depth[S]=(G.depth[L]>=G.depth[ye]?G.depth[L]:G.depth[ye])+1,me[L*2+1]=me[ye*2+1]=S,G.heap[1]=S++,w(G,me,1);while(G.heap_len>=2);G.heap[--G.heap_max]=G.heap[1],we(G,oe),tt(me,Ge,G.bl_count)}function Y(G,oe,me){var De,te=-1,ge,L=oe[1],ye=0,Ge=7,S=4;for(L===0&&(Ge=138,S=3),oe[(me+1)*2+1]=65535,De=0;De<=me;De++)ge=L,L=oe[(De+1)*2+1],!(++ye=3&&G.bl_tree[C[oe]*2+1]===0;oe--);return G.opt_len+=3*(oe+1)+5+5+4,oe}function ae(G,oe,me,De){var te;for(Re(G,oe-257,5),Re(G,me-1,5),Re(G,De-4,4),te=0;te>>=1)if(oe&1&&G.dyn_ltree[me*2]!==0)return n;if(G.dyn_ltree[18]!==0||G.dyn_ltree[20]!==0||G.dyn_ltree[26]!==0)return i;for(me=32;me0?(G.strm.data_type===a&&(G.strm.data_type=ee(G)),K(G,G.l_desc),K(G,G.d_desc),L=J(G),te=G.opt_len+3+7>>>3,ge=G.static_len+3+7>>>3,ge<=te&&(te=ge)):te=ge=me+5,me+4<=te&&oe!==-1?ve(G,oe,me,De):G.strategy===t||ge===te?(Re(G,(c<<1)+(De?1:0),3),D(G,m,N)):(Re(G,(s<<1)+(De?1:0),3),ae(G,G.l_desc.max_code+1,G.d_desc.max_code+1,L+1),D(G,G.dyn_ltree,G.dyn_dtree)),h(G),De&&U(G)}function Me(G,oe,me){return G.pending_buf[G.d_buf+G.last_lit*2]=oe>>>8&255,G.pending_buf[G.d_buf+G.last_lit*2+1]=oe&255,G.pending_buf[G.l_buf+G.last_lit]=me&255,G.last_lit++,oe===0?G.dyn_ltree[me*2]++:(G.matches++,oe--,G.dyn_ltree[(q[me]+f+1)*2]++,G.dyn_dtree[ie(oe)*2]++),G.last_lit===G.lit_bufsize-1}e._tr_init=Ee,e._tr_stored_block=ve,e._tr_flush_block=rt,e._tr_tally=Me,e._tr_align=Ie}}),Ru=Nt({"../../../node_modules/pako/lib/zlib/adler32.js"(e,r){"use strict";function t(n,i,a,o){for(var l=n&65535|0,c=n>>>16&65535|0,s=0;a!==0;){s=a>2e3?2e3:a,a-=s;do l=l+i[o++]|0,c=c+l|0;while(--s);l%=65521,c%=65521}return l|c<<16|0}r.exports=t}}),Lu=Nt({"../../../node_modules/pako/lib/zlib/crc32.js"(e,r){"use strict";function t(){for(var a,o=[],l=0;l<256;l++){a=l;for(var c=0;c<8;c++)a=a&1?3988292384^a>>>1:a>>>1;o[l]=a}return o}var n=t();function i(a,o,l,c){var s=n,u=c+l;a^=-1;for(var d=c;d>>8^s[(a^o[d])&255];return a^-1}r.exports=i}}),tl=Nt({"../../../node_modules/pako/lib/zlib/messages.js"(e,r){"use strict";r.exports={2:"need dictionary",1:"stream end",0:"","-1":"file error","-2":"stream error","-3":"data error","-4":"insufficient memory","-5":"buffer error","-6":"incompatible version"}}}),tp=Nt({"../../../node_modules/pako/lib/zlib/deflate.js"(e){"use strict";var r=Sn(),t=ep(),n=Ru(),i=Lu(),a=tl(),o=0,l=1,c=3,s=4,u=5,d=0,A=1,f=-2,p=-3,g=-5,y=-1,v=1,b=2,x=3,P=4,F=0,R=2,E=8,B=9,T=15,W=8,H=29,C=256,I=C+1+H,m=30,N=19,ne=2*I+1,q=15,se=3,Z=258,ue=Z+se+1,j=32,_=42,fe=69,ce=73,ie=91,be=103,Re=113,ke=666,xe=1,Oe=2,we=3,tt=4,Be=3;function h(S,de){return S.msg=a[de],de}function U(S){return(S<<1)-(S>4?9:0)}function z(S){for(var de=S.length;--de>=0;)S[de]=0}function k(S){var de=S.state,Ae=de.pending;Ae>S.avail_out&&(Ae=S.avail_out),Ae!==0&&(r.arraySet(S.output,de.pending_buf,de.pending_out,Ae,S.next_out),S.next_out+=Ae,de.pending_out+=Ae,S.total_out+=Ae,S.avail_out-=Ae,de.pending-=Ae,de.pending===0&&(de.pending_out=0))}function w(S,de){t._tr_flush_block(S,S.block_start>=0?S.block_start:-1,S.strstart-S.block_start,de),S.block_start=S.strstart,k(S.strm)}function D(S,de){S.pending_buf[S.pending++]=de}function K(S,de){S.pending_buf[S.pending++]=de>>>8&255,S.pending_buf[S.pending++]=de&255}function Y(S,de,Ae,O){var re=S.avail_in;return re>O&&(re=O),re===0?0:(S.avail_in-=re,r.arraySet(de,S.input,S.next_in,re,Ae),S.state.wrap===1?S.adler=n(S.adler,de,re,Ae):S.state.wrap===2&&(S.adler=i(S.adler,de,re,Ae)),S.next_in+=re,S.total_in+=re,re)}function M(S,de){var Ae=S.max_chain_length,O=S.strstart,re,he,_e=S.prev_length,Ne=S.nice_match,ze=S.strstart>S.w_size-ue?S.strstart-(S.w_size-ue):0,At=S.window,Or=S.w_mask,Dt=S.prev,gt=S.strstart+Z,It=At[O+_e-1],Ht=At[O+_e];S.prev_length>=S.good_match&&(Ae>>=2),Ne>S.lookahead&&(Ne=S.lookahead);do if(re=de,!(At[re+_e]!==Ht||At[re+_e-1]!==It||At[re]!==At[O]||At[++re]!==At[O+1])){O+=2,re++;do;while(At[++O]===At[++re]&&At[++O]===At[++re]&&At[++O]===At[++re]&&At[++O]===At[++re]&&At[++O]===At[++re]&&At[++O]===At[++re]&&At[++O]===At[++re]&&At[++O]===At[++re]&&O_e){if(S.match_start=de,_e=he,he>=Ne)break;It=At[O+_e-1],Ht=At[O+_e]}}while((de=Dt[de&Or])>ze&&--Ae!==0);return _e<=S.lookahead?_e:S.lookahead}function J(S){var de=S.w_size,Ae,O,re,he,_e;do{if(he=S.window_size-S.lookahead-S.strstart,S.strstart>=de+(de-ue)){r.arraySet(S.window,S.window,de,de,0),S.match_start-=de,S.strstart-=de,S.block_start-=de,O=S.hash_size,Ae=O;do re=S.head[--Ae],S.head[Ae]=re>=de?re-de:0;while(--O);O=de,Ae=O;do re=S.prev[--Ae],S.prev[Ae]=re>=de?re-de:0;while(--O);he+=de}if(S.strm.avail_in===0)break;if(O=Y(S.strm,S.window,S.strstart+S.lookahead,he),S.lookahead+=O,S.lookahead+S.insert>=se)for(_e=S.strstart-S.insert,S.ins_h=S.window[_e],S.ins_h=(S.ins_h<S.pending_buf_size-5&&(Ae=S.pending_buf_size-5);;){if(S.lookahead<=1){if(J(S),S.lookahead===0&&de===o)return xe;if(S.lookahead===0)break}S.strstart+=S.lookahead,S.lookahead=0;var O=S.block_start+Ae;if((S.strstart===0||S.strstart>=O)&&(S.lookahead=S.strstart-O,S.strstart=O,w(S,!1),S.strm.avail_out===0)||S.strstart-S.block_start>=S.w_size-ue&&(w(S,!1),S.strm.avail_out===0))return xe}return S.insert=0,de===s?(w(S,!0),S.strm.avail_out===0?we:tt):(S.strstart>S.block_start&&(w(S,!1),S.strm.avail_out===0),xe)}function ee(S,de){for(var Ae,O;;){if(S.lookahead=se&&(S.ins_h=(S.ins_h<=se)if(O=t._tr_tally(S,S.strstart-S.match_start,S.match_length-se),S.lookahead-=S.match_length,S.match_length<=S.max_lazy_match&&S.lookahead>=se){S.match_length--;do S.strstart++,S.ins_h=(S.ins_h<=se&&(S.ins_h=(S.ins_h<4096)&&(S.match_length=se-1)),S.prev_length>=se&&S.match_length<=S.prev_length){re=S.strstart+S.lookahead-se,O=t._tr_tally(S,S.strstart-1-S.prev_match,S.prev_length-se),S.lookahead-=S.prev_length-1,S.prev_length-=2;do++S.strstart<=re&&(S.ins_h=(S.ins_h<=se&&S.strstart>0&&(re=S.strstart-1,O=_e[re],O===_e[++re]&&O===_e[++re]&&O===_e[++re])){he=S.strstart+Z;do;while(O===_e[++re]&&O===_e[++re]&&O===_e[++re]&&O===_e[++re]&&O===_e[++re]&&O===_e[++re]&&O===_e[++re]&&O===_e[++re]&&reS.lookahead&&(S.match_length=S.lookahead)}if(S.match_length>=se?(Ae=t._tr_tally(S,1,S.match_length-se),S.lookahead-=S.match_length,S.strstart+=S.match_length,S.match_length=0):(Ae=t._tr_tally(S,0,S.window[S.strstart]),S.lookahead--,S.strstart++),Ae&&(w(S,!1),S.strm.avail_out===0))return xe}return S.insert=0,de===s?(w(S,!0),S.strm.avail_out===0?we:tt):S.last_lit&&(w(S,!1),S.strm.avail_out===0)?xe:Oe}function ve(S,de){for(var Ae;;){if(S.lookahead===0&&(J(S),S.lookahead===0)){if(de===o)return xe;break}if(S.match_length=0,Ae=t._tr_tally(S,0,S.window[S.strstart]),S.lookahead--,S.strstart++,Ae&&(w(S,!1),S.strm.avail_out===0))return xe}return S.insert=0,de===s?(w(S,!0),S.strm.avail_out===0?we:tt):S.last_lit&&(w(S,!1),S.strm.avail_out===0)?xe:Oe}function Ie(S,de,Ae,O,re){this.good_length=S,this.max_lazy=de,this.nice_length=Ae,this.max_chain=O,this.func=re}var rt;rt=[new Ie(0,0,0,0,ae),new Ie(4,4,8,4,ee),new Ie(4,5,16,8,ee),new Ie(4,6,32,32,ee),new Ie(4,4,16,16,pe),new Ie(8,16,32,32,pe),new Ie(8,16,128,128,pe),new Ie(8,32,128,256,pe),new Ie(32,128,258,1024,pe),new Ie(32,258,258,4096,pe)];function Me(S){S.window_size=2*S.w_size,z(S.head),S.max_lazy_match=rt[S.level].max_lazy,S.good_match=rt[S.level].good_length,S.nice_match=rt[S.level].nice_length,S.max_chain_length=rt[S.level].max_chain,S.strstart=0,S.block_start=0,S.lookahead=0,S.insert=0,S.match_length=S.prev_length=se-1,S.match_available=0,S.ins_h=0}function G(){this.strm=null,this.status=0,this.pending_buf=null,this.pending_buf_size=0,this.pending_out=0,this.pending=0,this.wrap=0,this.gzhead=null,this.gzindex=0,this.method=E,this.last_flush=-1,this.w_size=0,this.w_bits=0,this.w_mask=0,this.window=null,this.window_size=0,this.prev=null,this.head=null,this.ins_h=0,this.hash_size=0,this.hash_bits=0,this.hash_mask=0,this.hash_shift=0,this.block_start=0,this.match_length=0,this.prev_match=0,this.match_available=0,this.strstart=0,this.match_start=0,this.lookahead=0,this.prev_length=0,this.max_chain_length=0,this.max_lazy_match=0,this.level=0,this.strategy=0,this.good_match=0,this.nice_match=0,this.dyn_ltree=new r.Buf16(ne*2),this.dyn_dtree=new r.Buf16((2*m+1)*2),this.bl_tree=new r.Buf16((2*N+1)*2),z(this.dyn_ltree),z(this.dyn_dtree),z(this.bl_tree),this.l_desc=null,this.d_desc=null,this.bl_desc=null,this.bl_count=new r.Buf16(q+1),this.heap=new r.Buf16(2*I+1),z(this.heap),this.heap_len=0,this.heap_max=0,this.depth=new r.Buf16(2*I+1),z(this.depth),this.l_buf=0,this.lit_bufsize=0,this.last_lit=0,this.d_buf=0,this.opt_len=0,this.static_len=0,this.matches=0,this.insert=0,this.bi_buf=0,this.bi_valid=0}function oe(S){var de;return!S||!S.state?h(S,f):(S.total_in=S.total_out=0,S.data_type=R,de=S.state,de.pending=0,de.pending_out=0,de.wrap<0&&(de.wrap=-de.wrap),de.status=de.wrap?_:Re,S.adler=de.wrap===2?0:1,de.last_flush=o,t._tr_init(de),d)}function me(S){var de=oe(S);return de===d&&Me(S.state),de}function De(S,de){return!S||!S.state||S.state.wrap!==2?f:(S.state.gzhead=de,d)}function te(S,de,Ae,O,re,he){if(!S)return f;var _e=1;if(de===y&&(de=6),O<0?(_e=0,O=-O):O>15&&(_e=2,O-=16),re<1||re>B||Ae!==E||O<8||O>15||de<0||de>9||he<0||he>P)return h(S,f);O===8&&(O=9);var Ne=new G;return S.state=Ne,Ne.strm=S,Ne.wrap=_e,Ne.gzhead=null,Ne.w_bits=O,Ne.w_size=1<u||de<0)return S?h(S,f):f;if(O=S.state,!S.output||!S.input&&S.avail_in!==0||O.status===ke&&de!==s)return h(S,S.avail_out===0?g:f);if(O.strm=S,Ae=O.last_flush,O.last_flush=de,O.status===_)if(O.wrap===2)S.adler=0,D(O,31),D(O,139),D(O,8),O.gzhead?(D(O,(O.gzhead.text?1:0)+(O.gzhead.hcrc?2:0)+(O.gzhead.extra?4:0)+(O.gzhead.name?8:0)+(O.gzhead.comment?16:0)),D(O,O.gzhead.time&255),D(O,O.gzhead.time>>8&255),D(O,O.gzhead.time>>16&255),D(O,O.gzhead.time>>24&255),D(O,O.level===9?2:O.strategy>=b||O.level<2?4:0),D(O,O.gzhead.os&255),O.gzhead.extra&&O.gzhead.extra.length&&(D(O,O.gzhead.extra.length&255),D(O,O.gzhead.extra.length>>8&255)),O.gzhead.hcrc&&(S.adler=i(S.adler,O.pending_buf,O.pending,0)),O.gzindex=0,O.status=fe):(D(O,0),D(O,0),D(O,0),D(O,0),D(O,0),D(O,O.level===9?2:O.strategy>=b||O.level<2?4:0),D(O,Be),O.status=Re);else{var _e=E+(O.w_bits-8<<4)<<8,Ne=-1;O.strategy>=b||O.level<2?Ne=0:O.level<6?Ne=1:O.level===6?Ne=2:Ne=3,_e|=Ne<<6,O.strstart!==0&&(_e|=j),_e+=31-_e%31,O.status=Re,K(O,_e),O.strstart!==0&&(K(O,S.adler>>>16),K(O,S.adler&65535)),S.adler=1}if(O.status===fe)if(O.gzhead.extra){for(re=O.pending;O.gzindex<(O.gzhead.extra.length&65535)&&!(O.pending===O.pending_buf_size&&(O.gzhead.hcrc&&O.pending>re&&(S.adler=i(S.adler,O.pending_buf,O.pending-re,re)),k(S),re=O.pending,O.pending===O.pending_buf_size));)D(O,O.gzhead.extra[O.gzindex]&255),O.gzindex++;O.gzhead.hcrc&&O.pending>re&&(S.adler=i(S.adler,O.pending_buf,O.pending-re,re)),O.gzindex===O.gzhead.extra.length&&(O.gzindex=0,O.status=ce)}else O.status=ce;if(O.status===ce)if(O.gzhead.name){re=O.pending;do{if(O.pending===O.pending_buf_size&&(O.gzhead.hcrc&&O.pending>re&&(S.adler=i(S.adler,O.pending_buf,O.pending-re,re)),k(S),re=O.pending,O.pending===O.pending_buf_size)){he=1;break}O.gzindexre&&(S.adler=i(S.adler,O.pending_buf,O.pending-re,re)),he===0&&(O.gzindex=0,O.status=ie)}else O.status=ie;if(O.status===ie)if(O.gzhead.comment){re=O.pending;do{if(O.pending===O.pending_buf_size&&(O.gzhead.hcrc&&O.pending>re&&(S.adler=i(S.adler,O.pending_buf,O.pending-re,re)),k(S),re=O.pending,O.pending===O.pending_buf_size)){he=1;break}O.gzindexre&&(S.adler=i(S.adler,O.pending_buf,O.pending-re,re)),he===0&&(O.status=be)}else O.status=be;if(O.status===be&&(O.gzhead.hcrc?(O.pending+2>O.pending_buf_size&&k(S),O.pending+2<=O.pending_buf_size&&(D(O,S.adler&255),D(O,S.adler>>8&255),S.adler=0,O.status=Re)):O.status=Re),O.pending!==0){if(k(S),S.avail_out===0)return O.last_flush=-1,d}else if(S.avail_in===0&&U(de)<=U(Ae)&&de!==s)return h(S,g);if(O.status===ke&&S.avail_in!==0)return h(S,g);if(S.avail_in!==0||O.lookahead!==0||de!==o&&O.status!==ke){var ze=O.strategy===b?ve(O,de):O.strategy===x?Ee(O,de):rt[O.level].func(O,de);if((ze===we||ze===tt)&&(O.status=ke),ze===xe||ze===we)return S.avail_out===0&&(O.last_flush=-1),d;if(ze===Oe&&(de===l?t._tr_align(O):de!==u&&(t._tr_stored_block(O,0,0,!1),de===c&&(z(O.head),O.lookahead===0&&(O.strstart=0,O.block_start=0,O.insert=0))),k(S),S.avail_out===0))return O.last_flush=-1,d}return de!==s?d:O.wrap<=0?A:(O.wrap===2?(D(O,S.adler&255),D(O,S.adler>>8&255),D(O,S.adler>>16&255),D(O,S.adler>>24&255),D(O,S.total_in&255),D(O,S.total_in>>8&255),D(O,S.total_in>>16&255),D(O,S.total_in>>24&255)):(K(O,S.adler>>>16),K(O,S.adler&65535)),k(S),O.wrap>0&&(O.wrap=-O.wrap),O.pending!==0?d:A)}function ye(S){var de;return!S||!S.state?f:(de=S.state.status,de!==_&&de!==fe&&de!==ce&&de!==ie&&de!==be&&de!==Re&&de!==ke?h(S,f):(S.state=null,de===Re?h(S,p):d))}function Ge(S,de){var Ae=de.length,O,re,he,_e,Ne,ze,At,Or;if(!S||!S.state||(O=S.state,_e=O.wrap,_e===2||_e===1&&O.status!==_||O.lookahead))return f;for(_e===1&&(S.adler=n(S.adler,de,Ae,0)),O.wrap=0,Ae>=O.w_size&&(_e===0&&(z(O.head),O.strstart=0,O.block_start=0,O.insert=0),Or=new r.Buf8(O.w_size),r.arraySet(Or,de,Ae-O.w_size,O.w_size,0),de=Or,Ae=O.w_size),Ne=S.avail_in,ze=S.next_in,At=S.input,S.avail_in=Ae,S.next_in=0,S.input=de,J(O);O.lookahead>=se;){re=O.strstart,he=O.lookahead-(se-1);do O.ins_h=(O.ins_h<=252?6:a>=248?5:a>=240?4:a>=224?3:a>=192?2:1;var a;i[254]=i[254]=1,e.string2buf=function(l){var c,s,u,d,A,f=l.length,p=0;for(d=0;d>>6,c[A++]=128|s&63):s<65536?(c[A++]=224|s>>>12,c[A++]=128|s>>>6&63,c[A++]=128|s&63):(c[A++]=240|s>>>18,c[A++]=128|s>>>12&63,c[A++]=128|s>>>6&63,c[A++]=128|s&63);return c};function o(l,c){if(c<65534&&(l.subarray&&n||!l.subarray&&t))return String.fromCharCode.apply(null,r.shrinkBuf(l,c));for(var s="",u=0;u4){p[u++]=65533,s+=A-1;continue}for(d&=A===2?31:A===3?15:7;A>1&&s1){p[u++]=65533;continue}d<65536?p[u++]=d:(d-=65536,p[u++]=55296|d>>10&1023,p[u++]=56320|d&1023)}return o(p,u)},e.utf8border=function(l,c){var s;for(c=c||l.length,c>l.length&&(c=l.length),s=c-1;s>=0&&(l[s]&192)===128;)s--;return s<0||s===0?c:s+i[l[s]]>c?s:c}}}),Iu=Nt({"../../../node_modules/pako/lib/zlib/zstream.js"(e,r){"use strict";function t(){this.input=null,this.next_in=0,this.avail_in=0,this.total_in=0,this.output=null,this.next_out=0,this.avail_out=0,this.total_out=0,this.msg="",this.state=null,this.data_type=2,this.adler=0}r.exports=t}}),rp=Nt({"../../../node_modules/pako/lib/deflate.js"(e){"use strict";var r=tp(),t=Sn(),n=Nu(),i=tl(),a=Iu(),o=Object.prototype.toString,l=0,c=4,s=0,u=1,d=2,A=-1,f=0,p=8;function g(x){if(!(this instanceof g))return new g(x);this.options=t.assign({level:A,method:p,chunkSize:16384,windowBits:15,memLevel:8,strategy:f,to:""},x||{});var P=this.options;P.raw&&P.windowBits>0?P.windowBits=-P.windowBits:P.gzip&&P.windowBits>0&&P.windowBits<16&&(P.windowBits+=16),this.err=0,this.msg="",this.ended=!1,this.chunks=[],this.strm=new a,this.strm.avail_out=0;var F=r.deflateInit2(this.strm,P.level,P.method,P.windowBits,P.memLevel,P.strategy);if(F!==s)throw new Error(i[F]);if(P.header&&r.deflateSetHeader(this.strm,P.header),P.dictionary){var R;if(typeof P.dictionary=="string"?R=n.string2buf(P.dictionary):o.call(P.dictionary)==="[object ArrayBuffer]"?R=new Uint8Array(P.dictionary):R=P.dictionary,F=r.deflateSetDictionary(this.strm,R),F!==s)throw new Error(i[F]);this._dict_set=!0}}g.prototype.push=function(x,P){var F=this.strm,R=this.options.chunkSize,E,B;if(this.ended)return!1;B=P===~~P?P:P===!0?c:l,typeof x=="string"?F.input=n.string2buf(x):o.call(x)==="[object ArrayBuffer]"?F.input=new Uint8Array(x):F.input=x,F.next_in=0,F.avail_in=F.input.length;do{if(F.avail_out===0&&(F.output=new t.Buf8(R),F.next_out=0,F.avail_out=R),E=r.deflate(F,B),E!==u&&E!==s)return this.onEnd(E),this.ended=!0,!1;(F.avail_out===0||F.avail_in===0&&(B===c||B===d))&&(this.options.to==="string"?this.onData(n.buf2binstring(t.shrinkBuf(F.output,F.next_out))):this.onData(t.shrinkBuf(F.output,F.next_out)))}while((F.avail_in>0||F.avail_out===0)&&E!==u);return B===c?(E=r.deflateEnd(this.strm),this.onEnd(E),this.ended=!0,E===s):(B===d&&(this.onEnd(s),F.avail_out=0),!0)},g.prototype.onData=function(x){this.chunks.push(x)},g.prototype.onEnd=function(x){x===s&&(this.options.to==="string"?this.result=this.chunks.join(""):this.result=t.flattenChunks(this.chunks)),this.chunks=[],this.err=x,this.msg=this.strm.msg};function y(x,P){var F=new g(P);if(F.push(x,!0),F.err)throw F.msg||i[F.err];return F.result}function v(x,P){return P=P||{},P.raw=!0,y(x,P)}function b(x,P){return P=P||{},P.gzip=!0,y(x,P)}e.Deflate=g,e.deflate=y,e.deflateRaw=v,e.gzip=b}}),np=Nt({"../../../node_modules/pako/lib/zlib/inffast.js"(e,r){"use strict";var t=30,n=12;r.exports=function(a,o){var l,c,s,u,d,A,f,p,g,y,v,b,x,P,F,R,E,B,T,W,H,C,I,m,N;l=a.state,c=a.next_in,m=a.input,s=c+(a.avail_in-5),u=a.next_out,N=a.output,d=u-(o-a.avail_out),A=u+(a.avail_out-257),f=l.dmax,p=l.wsize,g=l.whave,y=l.wnext,v=l.window,b=l.hold,x=l.bits,P=l.lencode,F=l.distcode,R=(1<>>24,b>>>=T,x-=T,T=B>>>16&255,T===0)N[u++]=B&65535;else if(T&16){W=B&65535,T&=15,T&&(x>>=T,x-=T),x<15&&(b+=m[c++]<>>24,b>>>=T,x-=T,T=B>>>16&255,T&16){if(H=B&65535,T&=15,xf){a.msg="invalid distance too far back",l.mode=t;break e}if(b>>>=T,x-=T,T=u-d,H>T){if(T=H-T,T>g&&l.sane){a.msg="invalid distance too far back",l.mode=t;break e}if(C=0,I=v,y===0){if(C+=p-T,T2;)N[u++]=I[C++],N[u++]=I[C++],N[u++]=I[C++],W-=3;W&&(N[u++]=I[C++],W>1&&(N[u++]=I[C++]))}else{C=u-H;do N[u++]=N[C++],N[u++]=N[C++],N[u++]=N[C++],W-=3;while(W>2);W&&(N[u++]=N[C++],W>1&&(N[u++]=N[C++]))}}else if((T&64)===0){B=F[(B&65535)+(b&(1<>3,c-=W,x-=W<<3,b&=(1<=1&&ie[W]===0;W--);if(H>W&&(H=W),W===0)return b[x++]=1<<24|64<<16|0,b[x++]=1<<24|64<<16|0,F.bits=1,0;for(T=1;T0&&(p===o||W!==1))return-1;for(be[1]=0,E=1;Ei||p===c&&N>a)return 1;for(;;){xe=E-I,P[B]ce?(Oe=Re[ke+P[B]],we=_[fe+P[B]]):(Oe=96,we=0),q=1<>I)+se]=xe<<24|Oe<<16|we|0;while(se!==0);for(q=1<>=1;if(q!==0?(ne&=q-1,ne+=q):ne=0,B++,--ie[E]===0){if(E===W)break;E=g[y+P[B]]}if(E>H&&(ne&ue)!==Z){for(I===0&&(I=H),j+=T,C=E-I,m=1<i||p===c&&N>a)return 1;Z=ne&ue,b[Z]=H<<24|C<<16|j-x|0}}return ne!==0&&(b[j+ne]=E-I<<24|64<<16|0),F.bits=H,0}}}),ap=Nt({"../../../node_modules/pako/lib/zlib/inflate.js"(e){"use strict";var r=Sn(),t=Ru(),n=Lu(),i=np(),a=ip(),o=0,l=1,c=2,s=4,u=5,d=6,A=0,f=1,p=2,g=-2,y=-3,v=-4,b=-5,x=8,P=1,F=2,R=3,E=4,B=5,T=6,W=7,H=8,C=9,I=10,m=11,N=12,ne=13,q=14,se=15,Z=16,ue=17,j=18,_=19,fe=20,ce=21,ie=22,be=23,Re=24,ke=25,xe=26,Oe=27,we=28,tt=29,Be=30,h=31,U=32,z=852,k=592,w=15,D=w;function K(te){return(te>>>24&255)+(te>>>8&65280)+((te&65280)<<8)+((te&255)<<24)}function Y(){this.mode=0,this.last=!1,this.wrap=0,this.havedict=!1,this.flags=0,this.dmax=0,this.check=0,this.total=0,this.head=null,this.wbits=0,this.wsize=0,this.whave=0,this.wnext=0,this.window=null,this.hold=0,this.bits=0,this.length=0,this.offset=0,this.extra=0,this.lencode=null,this.distcode=null,this.lenbits=0,this.distbits=0,this.ncode=0,this.nlen=0,this.ndist=0,this.have=0,this.next=null,this.lens=new r.Buf16(320),this.work=new r.Buf16(288),this.lendyn=null,this.distdyn=null,this.sane=0,this.back=0,this.was=0}function M(te){var ge;return!te||!te.state?g:(ge=te.state,te.total_in=te.total_out=ge.total=0,te.msg="",ge.wrap&&(te.adler=ge.wrap&1),ge.mode=P,ge.last=0,ge.havedict=0,ge.dmax=32768,ge.head=null,ge.hold=0,ge.bits=0,ge.lencode=ge.lendyn=new r.Buf32(z),ge.distcode=ge.distdyn=new r.Buf32(k),ge.sane=1,ge.back=-1,A)}function J(te){var ge;return!te||!te.state?g:(ge=te.state,ge.wsize=0,ge.whave=0,ge.wnext=0,M(te))}function ae(te,ge){var L,ye;return!te||!te.state||(ye=te.state,ge<0?(L=0,ge=-ge):(L=(ge>>4)+1,ge<48&&(ge&=15)),ge&&(ge<8||ge>15))?g:(ye.window!==null&&ye.wbits!==ge&&(ye.window=null),ye.wrap=L,ye.wbits=ge,J(te))}function ee(te,ge){var L,ye;return te?(ye=new Y,te.state=ye,ye.window=null,L=ae(te,ge),L!==A&&(te.state=null),L):g}function pe(te){return ee(te,D)}var Ee=!0,ve,Ie;function rt(te){if(Ee){var ge;for(ve=new r.Buf32(512),Ie=new r.Buf32(32),ge=0;ge<144;)te.lens[ge++]=8;for(;ge<256;)te.lens[ge++]=9;for(;ge<280;)te.lens[ge++]=7;for(;ge<288;)te.lens[ge++]=8;for(a(l,te.lens,0,288,ve,0,te.work,{bits:9}),ge=0;ge<32;)te.lens[ge++]=5;a(c,te.lens,0,32,Ie,0,te.work,{bits:5}),Ee=!1}te.lencode=ve,te.lenbits=9,te.distcode=Ie,te.distbits=5}function Me(te,ge,L,ye){var Ge,S=te.state;return S.window===null&&(S.wsize=1<=S.wsize?(r.arraySet(S.window,ge,L-S.wsize,S.wsize,0),S.wnext=0,S.whave=S.wsize):(Ge=S.wsize-S.wnext,Ge>ye&&(Ge=ye),r.arraySet(S.window,ge,L-ye,Ge,S.wnext),ye-=Ge,ye?(r.arraySet(S.window,ge,L-ye,ye,0),S.wnext=ye,S.whave=S.wsize):(S.wnext+=Ge,S.wnext===S.wsize&&(S.wnext=0),S.whave>>8&255,L.check=n(L.check,jt,2,0),re=0,he=0,L.mode=F;break}if(L.flags=0,L.head&&(L.head.done=!1),!(L.wrap&1)||(((re&255)<<8)+(re>>8))%31){te.msg="incorrect header check",L.mode=Be;break}if((re&15)!==x){te.msg="unknown compression method",L.mode=Be;break}if(re>>>=4,he-=4,Rt=(re&15)+8,L.wbits===0)L.wbits=Rt;else if(Rt>L.wbits){te.msg="invalid window size",L.mode=Be;break}L.dmax=1<>8&1),L.flags&512&&(jt[0]=re&255,jt[1]=re>>>8&255,L.check=n(L.check,jt,2,0)),re=0,he=0,L.mode=R;case R:for(;he<32;){if(Ae===0)break e;Ae--,re+=ye[S++]<>>8&255,jt[2]=re>>>16&255,jt[3]=re>>>24&255,L.check=n(L.check,jt,4,0)),re=0,he=0,L.mode=E;case E:for(;he<16;){if(Ae===0)break e;Ae--,re+=ye[S++]<>8),L.flags&512&&(jt[0]=re&255,jt[1]=re>>>8&255,L.check=n(L.check,jt,2,0)),re=0,he=0,L.mode=B;case B:if(L.flags&1024){for(;he<16;){if(Ae===0)break e;Ae--,re+=ye[S++]<>>8&255,L.check=n(L.check,jt,2,0)),re=0,he=0}else L.head&&(L.head.extra=null);L.mode=T;case T:if(L.flags&1024&&(ze=L.length,ze>Ae&&(ze=Ae),ze&&(L.head&&(Rt=L.head.extra_len-L.length,L.head.extra||(L.head.extra=new Array(L.head.extra_len)),r.arraySet(L.head.extra,ye,S,ze,Rt)),L.flags&512&&(L.check=n(L.check,ye,ze,S)),Ae-=ze,S+=ze,L.length-=ze),L.length))break e;L.length=0,L.mode=W;case W:if(L.flags&2048){if(Ae===0)break e;ze=0;do Rt=ye[S+ze++],L.head&&Rt&&L.length<65536&&(L.head.name+=String.fromCharCode(Rt));while(Rt&&ze>9&1,L.head.done=!0),te.adler=L.check=0,L.mode=N;break;case I:for(;he<32;){if(Ae===0)break e;Ae--,re+=ye[S++]<>>=he&7,he-=he&7,L.mode=Oe;break}for(;he<3;){if(Ae===0)break e;Ae--,re+=ye[S++]<>>=1,he-=1,re&3){case 0:L.mode=q;break;case 1:if(rt(L),L.mode=fe,ge===d){re>>>=2,he-=2;break e}break;case 2:L.mode=ue;break;case 3:te.msg="invalid block type",L.mode=Be}re>>>=2,he-=2;break;case q:for(re>>>=he&7,he-=he&7;he<32;){if(Ae===0)break e;Ae--,re+=ye[S++]<>>16^65535)){te.msg="invalid stored block lengths",L.mode=Be;break}if(L.length=re&65535,re=0,he=0,L.mode=se,ge===d)break e;case se:L.mode=Z;case Z:if(ze=L.length,ze){if(ze>Ae&&(ze=Ae),ze>O&&(ze=O),ze===0)break e;r.arraySet(Ge,ye,S,ze,de),Ae-=ze,S+=ze,O-=ze,de+=ze,L.length-=ze;break}L.mode=N;break;case ue:for(;he<14;){if(Ae===0)break e;Ae--,re+=ye[S++]<>>=5,he-=5,L.ndist=(re&31)+1,re>>>=5,he-=5,L.ncode=(re&15)+4,re>>>=4,he-=4,L.nlen>286||L.ndist>30){te.msg="too many length or distance symbols",L.mode=Be;break}L.have=0,L.mode=j;case j:for(;L.have>>=3,he-=3}for(;L.have<19;)L.lens[ec[L.have++]]=0;if(L.lencode=L.lendyn,L.lenbits=7,_r={bits:L.lenbits},pr=a(o,L.lens,0,19,L.lencode,0,L.work,_r),L.lenbits=_r.bits,pr){te.msg="invalid code lengths set",L.mode=Be;break}L.have=0,L.mode=_;case _:for(;L.have>>24,It=Dt>>>16&255,Ht=Dt&65535,!(gt<=he);){if(Ae===0)break e;Ae--,re+=ye[S++]<>>=gt,he-=gt,L.lens[L.have++]=Ht;else{if(Ht===16){for(Lr=gt+2;he>>=gt,he-=gt,L.have===0){te.msg="invalid bit length repeat",L.mode=Be;break}Rt=L.lens[L.have-1],ze=3+(re&3),re>>>=2,he-=2}else if(Ht===17){for(Lr=gt+3;he>>=gt,he-=gt,Rt=0,ze=3+(re&7),re>>>=3,he-=3}else{for(Lr=gt+7;he>>=gt,he-=gt,Rt=0,ze=11+(re&127),re>>>=7,he-=7}if(L.have+ze>L.nlen+L.ndist){te.msg="invalid bit length repeat",L.mode=Be;break}for(;ze--;)L.lens[L.have++]=Rt}}if(L.mode===Be)break;if(L.lens[256]===0){te.msg="invalid code -- missing end-of-block",L.mode=Be;break}if(L.lenbits=9,_r={bits:L.lenbits},pr=a(l,L.lens,0,L.nlen,L.lencode,0,L.work,_r),L.lenbits=_r.bits,pr){te.msg="invalid literal/lengths set",L.mode=Be;break}if(L.distbits=6,L.distcode=L.distdyn,_r={bits:L.distbits},pr=a(c,L.lens,L.nlen,L.ndist,L.distcode,0,L.work,_r),L.distbits=_r.bits,pr){te.msg="invalid distances set",L.mode=Be;break}if(L.mode=fe,ge===d)break e;case fe:L.mode=ce;case ce:if(Ae>=6&&O>=258){te.next_out=de,te.avail_out=O,te.next_in=S,te.avail_in=Ae,L.hold=re,L.bits=he,i(te,Ne),de=te.next_out,Ge=te.output,O=te.avail_out,S=te.next_in,ye=te.input,Ae=te.avail_in,re=L.hold,he=L.bits,L.mode===N&&(L.back=-1);break}for(L.back=0;Dt=L.lencode[re&(1<>>24,It=Dt>>>16&255,Ht=Dt&65535,!(gt<=he);){if(Ae===0)break e;Ae--,re+=ye[S++]<>tr)],gt=Dt>>>24,It=Dt>>>16&255,Ht=Dt&65535,!(tr+gt<=he);){if(Ae===0)break e;Ae--,re+=ye[S++]<>>=tr,he-=tr,L.back+=tr}if(re>>>=gt,he-=gt,L.back+=gt,L.length=Ht,It===0){L.mode=xe;break}if(It&32){L.back=-1,L.mode=N;break}if(It&64){te.msg="invalid literal/length code",L.mode=Be;break}L.extra=It&15,L.mode=ie;case ie:if(L.extra){for(Lr=L.extra;he>>=L.extra,he-=L.extra,L.back+=L.extra}L.was=L.length,L.mode=be;case be:for(;Dt=L.distcode[re&(1<>>24,It=Dt>>>16&255,Ht=Dt&65535,!(gt<=he);){if(Ae===0)break e;Ae--,re+=ye[S++]<>tr)],gt=Dt>>>24,It=Dt>>>16&255,Ht=Dt&65535,!(tr+gt<=he);){if(Ae===0)break e;Ae--,re+=ye[S++]<>>=tr,he-=tr,L.back+=tr}if(re>>>=gt,he-=gt,L.back+=gt,It&64){te.msg="invalid distance code",L.mode=Be;break}L.offset=Ht,L.extra=It&15,L.mode=Re;case Re:if(L.extra){for(Lr=L.extra;he>>=L.extra,he-=L.extra,L.back+=L.extra}if(L.offset>L.dmax){te.msg="invalid distance too far back",L.mode=Be;break}L.mode=ke;case ke:if(O===0)break e;if(ze=Ne-O,L.offset>ze){if(ze=L.offset-ze,ze>L.whave&&L.sane){te.msg="invalid distance too far back",L.mode=Be;break}ze>L.wnext?(ze-=L.wnext,At=L.wsize-ze):At=L.wnext-ze,ze>L.length&&(ze=L.length),Or=L.window}else Or=Ge,At=de-L.offset,ze=L.length;ze>O&&(ze=O),O-=ze,L.length-=ze;do Ge[de++]=Or[At++];while(--ze);L.length===0&&(L.mode=ce);break;case xe:if(O===0)break e;Ge[de++]=L.length,O--,L.mode=ce;break;case Oe:if(L.wrap){for(;he<32;){if(Ae===0)break e;Ae--,re|=ye[S++]<=0&&f.windowBits<16&&(f.windowBits=-f.windowBits,f.windowBits===0&&(f.windowBits=-15)),f.windowBits>=0&&f.windowBits<16&&!(A&&A.windowBits)&&(f.windowBits+=32),f.windowBits>15&&f.windowBits<48&&(f.windowBits&15)===0&&(f.windowBits|=15),this.err=0,this.msg="",this.ended=!1,this.chunks=[],this.strm=new o,this.strm.avail_out=0;var p=r.inflateInit2(this.strm,f.windowBits);if(p!==i.Z_OK)throw new Error(a[p]);if(this.header=new l,r.inflateGetHeader(this.strm,this.header),f.dictionary&&(typeof f.dictionary=="string"?f.dictionary=n.string2buf(f.dictionary):c.call(f.dictionary)==="[object ArrayBuffer]"&&(f.dictionary=new Uint8Array(f.dictionary)),f.raw&&(p=r.inflateSetDictionary(this.strm,f.dictionary),p!==i.Z_OK)))throw new Error(a[p])}s.prototype.push=function(A,f){var p=this.strm,g=this.options.chunkSize,y=this.options.dictionary,v,b,x,P,F,R=!1;if(this.ended)return!1;b=f===~~f?f:f===!0?i.Z_FINISH:i.Z_NO_FLUSH,typeof A=="string"?p.input=n.binstring2buf(A):c.call(A)==="[object ArrayBuffer]"?p.input=new Uint8Array(A):p.input=A,p.next_in=0,p.avail_in=p.input.length;do{if(p.avail_out===0&&(p.output=new t.Buf8(g),p.next_out=0,p.avail_out=g),v=r.inflate(p,i.Z_NO_FLUSH),v===i.Z_NEED_DICT&&y&&(v=r.inflateSetDictionary(this.strm,y)),v===i.Z_BUF_ERROR&&R===!0&&(v=i.Z_OK,R=!1),v!==i.Z_STREAM_END&&v!==i.Z_OK)return this.onEnd(v),this.ended=!0,!1;p.next_out&&(p.avail_out===0||v===i.Z_STREAM_END||p.avail_in===0&&(b===i.Z_FINISH||b===i.Z_SYNC_FLUSH))&&(this.options.to==="string"?(x=n.utf8border(p.output,p.next_out),P=p.next_out-x,F=n.buf2string(p.output,x),p.next_out=P,p.avail_out=g-P,P&&t.arraySet(p.output,p.output,x,P,0),this.onData(F)):this.onData(t.shrinkBuf(p.output,p.next_out))),p.avail_in===0&&p.avail_out===0&&(R=!0)}while((p.avail_in>0||p.avail_out===0)&&v!==i.Z_STREAM_END);return v===i.Z_STREAM_END&&(b=i.Z_FINISH),b===i.Z_FINISH?(v=r.inflateEnd(this.strm),this.onEnd(v),this.ended=!0,v===i.Z_OK):(b===i.Z_SYNC_FLUSH&&(this.onEnd(i.Z_OK),p.avail_out=0),!0)},s.prototype.onData=function(A){this.chunks.push(A)},s.prototype.onEnd=function(A){A===i.Z_OK&&(this.options.to==="string"?this.result=this.chunks.join(""):this.result=t.flattenChunks(this.chunks)),this.chunks=[],this.err=A,this.msg=this.strm.msg};function u(A,f){var p=new s(f);if(p.push(A,!0),p.err)throw p.msg||a[p.err];return p.result}function d(A,f){return f=f||{},f.raw=!0,u(A,f)}e.Inflate=s,e.inflate=u,e.inflateRaw=d,e.ungzip=u}}),bo=Nt({"../../../node_modules/pako/index.js"(e,r){"use strict";var t=Sn().assign,n=rp(),i=sp(),a=zu(),o={};t(o,n,i,a),r.exports=o}}),lp=Nt({"node_modules/jszip/dist/jszip.min.js"(e,r){(function(t){typeof e=="object"&&typeof r<"u"?r.exports=t():typeof define=="function"&&define.amd?define([],t):(typeof window<"u"?window:typeof global<"u"?global:typeof self<"u"?self:this).JSZip=t()})(function(){return(function t(n,i,a){function o(s,u){if(!i[s]){if(!n[s]){var d=typeof An=="function"&&An;if(!u&&d)return d(s,!0);if(l)return l(s,!0);var A=new Error("Cannot find module '"+s+"'");throw A.code="MODULE_NOT_FOUND",A}var f=i[s]={exports:{}};n[s][0].call(f.exports,function(p){var g=n[s][1][p];return o(g||p)},f,f.exports,t,n,i,a)}return i[s].exports}for(var l=typeof An=="function"&&An,c=0;c>2,f=(3&s)<<4|u>>4,p=1>6:64,g=2>4,u=(15&A)<<4|(f=l.indexOf(c.charAt(g++)))>>2,d=(3&f)<<6|(p=l.indexOf(c.charAt(g++))),b[y++]=s,f!==64&&(b[y++]=u),p!==64&&(b[y++]=d);return b}},{"./support":30,"./utils":32}],2:[function(t,n,i){"use strict";var a=t("./external"),o=t("./stream/DataWorker"),l=t("./stream/Crc32Probe"),c=t("./stream/DataLengthProbe");function s(u,d,A,f,p){this.compressedSize=u,this.uncompressedSize=d,this.crc32=A,this.compression=f,this.compressedContent=p}s.prototype={getContentWorker:function(){var u=new o(a.Promise.resolve(this.compressedContent)).pipe(this.compression.uncompressWorker()).pipe(new c("data_length")),d=this;return u.on("end",function(){if(this.streamInfo.data_length!==d.uncompressedSize)throw new Error("Bug : uncompressed data size mismatch")}),u},getCompressedWorker:function(){return new o(a.Promise.resolve(this.compressedContent)).withStreamInfo("compressedSize",this.compressedSize).withStreamInfo("uncompressedSize",this.uncompressedSize).withStreamInfo("crc32",this.crc32).withStreamInfo("compression",this.compression)}},s.createWorkerFrom=function(u,d,A){return u.pipe(new l).pipe(new c("uncompressedSize")).pipe(d.compressWorker(A)).pipe(new c("compressedSize")).withStreamInfo("compression",d)},n.exports=s},{"./external":6,"./stream/Crc32Probe":25,"./stream/DataLengthProbe":26,"./stream/DataWorker":27}],3:[function(t,n,i){"use strict";var a=t("./stream/GenericWorker");i.STORE={magic:"\0\0",compressWorker:function(){return new a("STORE compression")},uncompressWorker:function(){return new a("STORE decompression")}},i.DEFLATE=t("./flate")},{"./flate":7,"./stream/GenericWorker":28}],4:[function(t,n,i){"use strict";var a=t("./utils"),o=(function(){for(var l,c=[],s=0;s<256;s++){l=s;for(var u=0;u<8;u++)l=1&l?3988292384^l>>>1:l>>>1;c[s]=l}return c})();n.exports=function(l,c){return l!==void 0&&l.length?a.getTypeOf(l)!=="string"?(function(s,u,d,A){var f=o,p=A+d;s^=-1;for(var g=A;g>>8^f[255&(s^u[g])];return-1^s})(0|c,l,l.length,0):(function(s,u,d,A){var f=o,p=A+d;s^=-1;for(var g=A;g>>8^f[255&(s^u.charCodeAt(g))];return-1^s})(0|c,l,l.length,0):0}},{"./utils":32}],5:[function(t,n,i){"use strict";i.base64=!1,i.binary=!1,i.dir=!1,i.createFolders=!0,i.date=null,i.compression=null,i.compressionOptions=null,i.comment=null,i.unixPermissions=null,i.dosPermissions=null},{}],6:[function(t,n,i){"use strict";var a=null;a=typeof Promise<"u"?Promise:t("lie"),n.exports={Promise:a}},{lie:37}],7:[function(t,n,i){"use strict";var a=typeof Uint8Array<"u"&&typeof Uint16Array<"u"&&typeof Uint32Array<"u",o=t("pako"),l=t("./utils"),c=t("./stream/GenericWorker"),s=a?"uint8array":"array";function u(d,A){c.call(this,"FlateWorker/"+d),this._pako=null,this._pakoAction=d,this._pakoOptions=A,this.meta={}}i.magic="\b\0",l.inherits(u,c),u.prototype.processChunk=function(d){this.meta=d.meta,this._pako===null&&this._createPako(),this._pako.push(l.transformTo(s,d.data),!1)},u.prototype.flush=function(){c.prototype.flush.call(this),this._pako===null&&this._createPako(),this._pako.push([],!0)},u.prototype.cleanUp=function(){c.prototype.cleanUp.call(this),this._pako=null},u.prototype._createPako=function(){this._pako=new o[this._pakoAction]({raw:!0,level:this._pakoOptions.level||-1});var d=this;this._pako.onData=function(A){d.push({data:A,meta:d.meta})}},i.compressWorker=function(d){return new u("Deflate",d)},i.uncompressWorker=function(){return new u("Inflate",{})}},{"./stream/GenericWorker":28,"./utils":32,pako:38}],8:[function(t,n,i){"use strict";function a(f,p){var g,y="";for(g=0;g>>=8;return y}function o(f,p,g,y,v,b){var x,P,F=f.file,R=f.compression,E=b!==s.utf8encode,B=l.transformTo("string",b(F.name)),T=l.transformTo("string",s.utf8encode(F.name)),W=F.comment,H=l.transformTo("string",b(W)),C=l.transformTo("string",s.utf8encode(W)),I=T.length!==F.name.length,m=C.length!==W.length,N="",ne="",q="",se=F.dir,Z=F.date,ue={crc32:0,compressedSize:0,uncompressedSize:0};p&&!g||(ue.crc32=f.crc32,ue.compressedSize=f.compressedSize,ue.uncompressedSize=f.uncompressedSize);var j=0;p&&(j|=8),E||!I&&!m||(j|=2048);var _=0,fe=0;se&&(_|=16),v==="UNIX"?(fe=798,_|=(function(ie,be){var Re=ie;return ie||(Re=be?16893:33204),(65535&Re)<<16})(F.unixPermissions,se)):(fe=20,_|=(function(ie){return 63&(ie||0)})(F.dosPermissions)),x=Z.getUTCHours(),x<<=6,x|=Z.getUTCMinutes(),x<<=5,x|=Z.getUTCSeconds()/2,P=Z.getUTCFullYear()-1980,P<<=4,P|=Z.getUTCMonth()+1,P<<=5,P|=Z.getUTCDate(),I&&(ne=a(1,1)+a(u(B),4)+T,N+="up"+a(ne.length,2)+ne),m&&(q=a(1,1)+a(u(H),4)+C,N+="uc"+a(q.length,2)+q);var ce="";return ce+=` -\0`,ce+=a(j,2),ce+=R.magic,ce+=a(x,2),ce+=a(P,2),ce+=a(ue.crc32,4),ce+=a(ue.compressedSize,4),ce+=a(ue.uncompressedSize,4),ce+=a(B.length,2),ce+=a(N.length,2),{fileRecord:d.LOCAL_FILE_HEADER+ce+B+N,dirRecord:d.CENTRAL_FILE_HEADER+a(fe,2)+ce+a(H.length,2)+"\0\0\0\0"+a(_,4)+a(y,4)+B+N+H}}var l=t("../utils"),c=t("../stream/GenericWorker"),s=t("../utf8"),u=t("../crc32"),d=t("../signature");function A(f,p,g,y){c.call(this,"ZipFileWorker"),this.bytesWritten=0,this.zipComment=p,this.zipPlatform=g,this.encodeFileName=y,this.streamFiles=f,this.accumulate=!1,this.contentBuffer=[],this.dirRecords=[],this.currentSourceOffset=0,this.entriesCount=0,this.currentFile=null,this._sources=[]}l.inherits(A,c),A.prototype.push=function(f){var p=f.meta.percent||0,g=this.entriesCount,y=this._sources.length;this.accumulate?this.contentBuffer.push(f):(this.bytesWritten+=f.data.length,c.prototype.push.call(this,{data:f.data,meta:{currentFile:this.currentFile,percent:g?(p+100*(g-y-1))/g:100}}))},A.prototype.openedSource=function(f){this.currentSourceOffset=this.bytesWritten,this.currentFile=f.file.name;var p=this.streamFiles&&!f.file.dir;if(p){var g=o(f,p,!1,this.currentSourceOffset,this.zipPlatform,this.encodeFileName);this.push({data:g.fileRecord,meta:{percent:0}})}else this.accumulate=!0},A.prototype.closedSource=function(f){this.accumulate=!1;var p=this.streamFiles&&!f.file.dir,g=o(f,p,!0,this.currentSourceOffset,this.zipPlatform,this.encodeFileName);if(this.dirRecords.push(g.dirRecord),p)this.push({data:(function(y){return d.DATA_DESCRIPTOR+a(y.crc32,4)+a(y.compressedSize,4)+a(y.uncompressedSize,4)})(f),meta:{percent:100}});else for(this.push({data:g.fileRecord,meta:{percent:0}});this.contentBuffer.length;)this.push(this.contentBuffer.shift());this.currentFile=null},A.prototype.flush=function(){for(var f=this.bytesWritten,p=0;p=this.index;c--)s=(s<<8)+this.byteAt(c);return this.index+=l,s},readString:function(l){return a.transformTo("string",this.readData(l))},readData:function(){},lastIndexOfSignature:function(){},readAndCheckSignature:function(){},readDate:function(){var l=this.readInt(4);return new Date(Date.UTC(1980+(l>>25&127),(l>>21&15)-1,l>>16&31,l>>11&31,l>>5&63,(31&l)<<1))}},n.exports=o},{"../utils":32}],19:[function(t,n,i){"use strict";var a=t("./Uint8ArrayReader");function o(l){a.call(this,l)}t("../utils").inherits(o,a),o.prototype.readData=function(l){this.checkOffset(l);var c=this.data.slice(this.zero+this.index,this.zero+this.index+l);return this.index+=l,c},n.exports=o},{"../utils":32,"./Uint8ArrayReader":21}],20:[function(t,n,i){"use strict";var a=t("./DataReader");function o(l){a.call(this,l)}t("../utils").inherits(o,a),o.prototype.byteAt=function(l){return this.data.charCodeAt(this.zero+l)},o.prototype.lastIndexOfSignature=function(l){return this.data.lastIndexOf(l)-this.zero},o.prototype.readAndCheckSignature=function(l){return l===this.readData(4)},o.prototype.readData=function(l){this.checkOffset(l);var c=this.data.slice(this.zero+this.index,this.zero+this.index+l);return this.index+=l,c},n.exports=o},{"../utils":32,"./DataReader":18}],21:[function(t,n,i){"use strict";var a=t("./ArrayReader");function o(l){a.call(this,l)}t("../utils").inherits(o,a),o.prototype.readData=function(l){if(this.checkOffset(l),l===0)return new Uint8Array(0);var c=this.data.subarray(this.zero+this.index,this.zero+this.index+l);return this.index+=l,c},n.exports=o},{"../utils":32,"./ArrayReader":17}],22:[function(t,n,i){"use strict";var a=t("../utils"),o=t("../support"),l=t("./ArrayReader"),c=t("./StringReader"),s=t("./NodeBufferReader"),u=t("./Uint8ArrayReader");n.exports=function(d){var A=a.getTypeOf(d);return a.checkSupport(A),A!=="string"||o.uint8array?A==="nodebuffer"?new s(d):o.uint8array?new u(a.transformTo("uint8array",d)):new l(a.transformTo("array",d)):new c(d)}},{"../support":30,"../utils":32,"./ArrayReader":17,"./NodeBufferReader":19,"./StringReader":20,"./Uint8ArrayReader":21}],23:[function(t,n,i){"use strict";i.LOCAL_FILE_HEADER="PK",i.CENTRAL_FILE_HEADER="PK",i.CENTRAL_DIRECTORY_END="PK",i.ZIP64_CENTRAL_DIRECTORY_LOCATOR="PK\x07",i.ZIP64_CENTRAL_DIRECTORY_END="PK",i.DATA_DESCRIPTOR="PK\x07\b"},{}],24:[function(t,n,i){"use strict";var a=t("./GenericWorker"),o=t("../utils");function l(c){a.call(this,"ConvertWorker to "+c),this.destType=c}o.inherits(l,a),l.prototype.processChunk=function(c){this.push({data:o.transformTo(this.destType,c.data),meta:c.meta})},n.exports=l},{"../utils":32,"./GenericWorker":28}],25:[function(t,n,i){"use strict";var a=t("./GenericWorker"),o=t("../crc32");function l(){a.call(this,"Crc32Probe"),this.withStreamInfo("crc32",0)}t("../utils").inherits(l,a),l.prototype.processChunk=function(c){this.streamInfo.crc32=o(c.data,this.streamInfo.crc32||0),this.push(c)},n.exports=l},{"../crc32":4,"../utils":32,"./GenericWorker":28}],26:[function(t,n,i){"use strict";var a=t("../utils"),o=t("./GenericWorker");function l(c){o.call(this,"DataLengthProbe for "+c),this.propName=c,this.withStreamInfo(c,0)}a.inherits(l,o),l.prototype.processChunk=function(c){if(c){var s=this.streamInfo[this.propName]||0;this.streamInfo[this.propName]=s+c.data.length}o.prototype.processChunk.call(this,c)},n.exports=l},{"../utils":32,"./GenericWorker":28}],27:[function(t,n,i){"use strict";var a=t("../utils"),o=t("./GenericWorker");function l(c){o.call(this,"DataWorker");var s=this;this.dataIsReady=!1,this.index=0,this.max=0,this.data=null,this.type="",this._tickScheduled=!1,c.then(function(u){s.dataIsReady=!0,s.data=u,s.max=u&&u.length||0,s.type=a.getTypeOf(u),s.isPaused||s._tickAndRepeat()},function(u){s.error(u)})}a.inherits(l,o),l.prototype.cleanUp=function(){o.prototype.cleanUp.call(this),this.data=null},l.prototype.resume=function(){return!!o.prototype.resume.call(this)&&(!this._tickScheduled&&this.dataIsReady&&(this._tickScheduled=!0,a.delay(this._tickAndRepeat,[],this)),!0)},l.prototype._tickAndRepeat=function(){this._tickScheduled=!1,this.isPaused||this.isFinished||(this._tick(),this.isFinished||(a.delay(this._tickAndRepeat,[],this),this._tickScheduled=!0))},l.prototype._tick=function(){if(this.isPaused||this.isFinished)return!1;var c=null,s=Math.min(this.max,this.index+16384);if(this.index>=this.max)return this.end();switch(this.type){case"string":c=this.data.substring(this.index,s);break;case"uint8array":c=this.data.subarray(this.index,s);break;case"array":case"nodebuffer":c=this.data.slice(this.index,s)}return this.index=s,this.push({data:c,meta:{percent:this.max?this.index/this.max*100:0}})},n.exports=l},{"../utils":32,"./GenericWorker":28}],28:[function(t,n,i){"use strict";function a(o){this.name=o||"default",this.streamInfo={},this.generatedError=null,this.extraStreamInfo={},this.isPaused=!0,this.isFinished=!1,this.isLocked=!1,this._listeners={data:[],end:[],error:[]},this.previous=null}a.prototype={push:function(o){this.emit("data",o)},end:function(){if(this.isFinished)return!1;this.flush();try{this.emit("end"),this.cleanUp(),this.isFinished=!0}catch(o){this.emit("error",o)}return!0},error:function(o){return!this.isFinished&&(this.isPaused?this.generatedError=o:(this.isFinished=!0,this.emit("error",o),this.previous&&this.previous.error(o),this.cleanUp()),!0)},on:function(o,l){return this._listeners[o].push(l),this},cleanUp:function(){this.streamInfo=this.generatedError=this.extraStreamInfo=null,this._listeners=[]},emit:function(o,l){if(this._listeners[o])for(var c=0;c "+o:o}},n.exports=a},{}],29:[function(t,n,i){"use strict";var a=t("../utils"),o=t("./ConvertWorker"),l=t("./GenericWorker"),c=t("../base64"),s=t("../support"),u=t("../external"),d=null;if(s.nodestream)try{d=t("../nodejs/NodejsStreamOutputAdapter")}catch{}function A(p,g){return new u.Promise(function(y,v){var b=[],x=p._internalType,P=p._outputType,F=p._mimeType;p.on("data",function(R,E){b.push(R),g&&g(E)}).on("error",function(R){b=[],v(R)}).on("end",function(){try{var R=(function(E,B,T){switch(E){case"blob":return a.newBlob(a.transformTo("arraybuffer",B),T);case"base64":return c.encode(B);default:return a.transformTo(E,B)}})(P,(function(E,B){var T,W=0,H=null,C=0;for(T=0;T"u")i.blob=!1;else{var a=new ArrayBuffer(0);try{i.blob=new Blob([a],{type:"application/zip"}).size===0}catch{try{var o=new(self.BlobBuilder||self.WebKitBlobBuilder||self.MozBlobBuilder||self.MSBlobBuilder);o.append(a),i.blob=o.getBlob("application/zip").size===0}catch{i.blob=!1}}}try{i.nodestream=!!t("readable-stream").Readable}catch{i.nodestream=!1}},{"readable-stream":16}],31:[function(t,n,i){"use strict";for(var a=t("./utils"),o=t("./support"),l=t("./nodejsUtils"),c=t("./stream/GenericWorker"),s=new Array(256),u=0;u<256;u++)s[u]=252<=u?6:248<=u?5:240<=u?4:224<=u?3:192<=u?2:1;s[254]=s[254]=1;function d(){c.call(this,"utf-8 decode"),this.leftOver=null}function A(){c.call(this,"utf-8 encode")}i.utf8encode=function(f){return o.nodebuffer?l.newBufferFrom(f,"utf-8"):(function(p){var g,y,v,b,x,P=p.length,F=0;for(b=0;b>>6:(y<65536?g[x++]=224|y>>>12:(g[x++]=240|y>>>18,g[x++]=128|y>>>12&63),g[x++]=128|y>>>6&63),g[x++]=128|63&y);return g})(f)},i.utf8decode=function(f){return o.nodebuffer?a.transformTo("nodebuffer",f).toString("utf-8"):(function(p){var g,y,v,b,x=p.length,P=new Array(2*x);for(g=y=0;g>10&1023,P[y++]=56320|1023&v)}return P.length!==y&&(P.subarray?P=P.subarray(0,y):P.length=y),a.applyFromCharCode(P)})(f=a.transformTo(o.uint8array?"uint8array":"array",f))},a.inherits(d,c),d.prototype.processChunk=function(f){var p=a.transformTo(o.uint8array?"uint8array":"array",f.data);if(this.leftOver&&this.leftOver.length){if(o.uint8array){var g=p;(p=new Uint8Array(g.length+this.leftOver.length)).set(this.leftOver,0),p.set(g,this.leftOver.length)}else p=this.leftOver.concat(p);this.leftOver=null}var y=(function(b,x){var P;for((x=x||b.length)>b.length&&(x=b.length),P=x-1;0<=P&&(192&b[P])==128;)P--;return P<0||P===0?x:P+s[b[P]]>x?P:x})(p),v=p;y!==p.length&&(o.uint8array?(v=p.subarray(0,y),this.leftOver=p.subarray(y,p.length)):(v=p.slice(0,y),this.leftOver=p.slice(y,p.length))),this.push({data:i.utf8decode(v),meta:f.meta})},d.prototype.flush=function(){this.leftOver&&this.leftOver.length&&(this.push({data:i.utf8decode(this.leftOver),meta:{}}),this.leftOver=null)},i.Utf8DecodeWorker=d,a.inherits(A,c),A.prototype.processChunk=function(f){this.push({data:i.utf8encode(f.data),meta:f.meta})},i.Utf8EncodeWorker=A},{"./nodejsUtils":14,"./stream/GenericWorker":28,"./support":30,"./utils":32}],32:[function(t,n,i){"use strict";var a=t("./support"),o=t("./base64"),l=t("./nodejsUtils"),c=t("./external");function s(g){return g}function u(g,y){for(var v=0;v>8;this.dir=!!(16&this.externalFileAttributes),f==0&&(this.dosPermissions=63&this.externalFileAttributes),f==3&&(this.unixPermissions=this.externalFileAttributes>>16&65535),this.dir||this.fileNameStr.slice(-1)!=="/"||(this.dir=!0)},parseZIP64ExtraField:function(){if(this.extraFields[1]){var f=a(this.extraFields[1].value);this.uncompressedSize===o.MAX_VALUE_32BITS&&(this.uncompressedSize=f.readInt(8)),this.compressedSize===o.MAX_VALUE_32BITS&&(this.compressedSize=f.readInt(8)),this.localHeaderOffset===o.MAX_VALUE_32BITS&&(this.localHeaderOffset=f.readInt(8)),this.diskNumberStart===o.MAX_VALUE_32BITS&&(this.diskNumberStart=f.readInt(4))}},readExtraFields:function(f){var p,g,y,v=f.index+this.extraFieldsLength;for(this.extraFields||(this.extraFields={});f.index+4>>6:(f<65536?A[y++]=224|f>>>12:(A[y++]=240|f>>>18,A[y++]=128|f>>>12&63),A[y++]=128|f>>>6&63),A[y++]=128|63&f);return A},i.buf2binstring=function(d){return u(d,d.length)},i.binstring2buf=function(d){for(var A=new a.Buf8(d.length),f=0,p=A.length;f>10&1023,b[p++]=56320|1023&g)}return u(b,p)},i.utf8border=function(d,A){var f;for((A=A||d.length)>d.length&&(A=d.length),f=A-1;0<=f&&(192&d[f])==128;)f--;return f<0||f===0?A:f+c[d[f]]>A?f:A}},{"./common":41}],43:[function(t,n,i){"use strict";n.exports=function(a,o,l,c){for(var s=65535&a|0,u=a>>>16&65535|0,d=0;l!==0;){for(l-=d=2e3>>1:o>>>1;l[c]=o}return l})();n.exports=function(o,l,c,s){var u=a,d=s+c;o^=-1;for(var A=s;A>>8^u[255&(o^l[A])];return-1^o}},{}],46:[function(t,n,i){"use strict";var a,o=t("../utils/common"),l=t("./trees"),c=t("./adler32"),s=t("./crc32"),u=t("./messages"),d=0,A=4,f=0,p=-2,g=-1,y=4,v=2,b=8,x=9,P=286,F=30,R=19,E=2*P+1,B=15,T=3,W=258,H=W+T+1,C=42,I=113,m=1,N=2,ne=3,q=4;function se(h,U){return h.msg=u[U],U}function Z(h){return(h<<1)-(4h.avail_out&&(z=h.avail_out),z!==0&&(o.arraySet(h.output,U.pending_buf,U.pending_out,z,h.next_out),h.next_out+=z,U.pending_out+=z,h.total_out+=z,h.avail_out-=z,U.pending-=z,U.pending===0&&(U.pending_out=0))}function _(h,U){l._tr_flush_block(h,0<=h.block_start?h.block_start:-1,h.strstart-h.block_start,U),h.block_start=h.strstart,j(h.strm)}function fe(h,U){h.pending_buf[h.pending++]=U}function ce(h,U){h.pending_buf[h.pending++]=U>>>8&255,h.pending_buf[h.pending++]=255&U}function ie(h,U){var z,k,w=h.max_chain_length,D=h.strstart,K=h.prev_length,Y=h.nice_match,M=h.strstart>h.w_size-H?h.strstart-(h.w_size-H):0,J=h.window,ae=h.w_mask,ee=h.prev,pe=h.strstart+W,Ee=J[D+K-1],ve=J[D+K];h.prev_length>=h.good_match&&(w>>=2),Y>h.lookahead&&(Y=h.lookahead);do if(J[(z=U)+K]===ve&&J[z+K-1]===Ee&&J[z]===J[D]&&J[++z]===J[D+1]){D+=2,z++;do;while(J[++D]===J[++z]&&J[++D]===J[++z]&&J[++D]===J[++z]&&J[++D]===J[++z]&&J[++D]===J[++z]&&J[++D]===J[++z]&&J[++D]===J[++z]&&J[++D]===J[++z]&&DM&&--w!=0);return K<=h.lookahead?K:h.lookahead}function be(h){var U,z,k,w,D,K,Y,M,J,ae,ee=h.w_size;do{if(w=h.window_size-h.lookahead-h.strstart,h.strstart>=ee+(ee-H)){for(o.arraySet(h.window,h.window,ee,ee,0),h.match_start-=ee,h.strstart-=ee,h.block_start-=ee,U=z=h.hash_size;k=h.head[--U],h.head[U]=ee<=k?k-ee:0,--z;);for(U=z=ee;k=h.prev[--U],h.prev[U]=ee<=k?k-ee:0,--z;);w+=ee}if(h.strm.avail_in===0)break;if(K=h.strm,Y=h.window,M=h.strstart+h.lookahead,J=w,ae=void 0,ae=K.avail_in,J=T)for(D=h.strstart-h.insert,h.ins_h=h.window[D],h.ins_h=(h.ins_h<=T&&(h.ins_h=(h.ins_h<=T)if(k=l._tr_tally(h,h.strstart-h.match_start,h.match_length-T),h.lookahead-=h.match_length,h.match_length<=h.max_lazy_match&&h.lookahead>=T){for(h.match_length--;h.strstart++,h.ins_h=(h.ins_h<=T&&(h.ins_h=(h.ins_h<=T&&h.match_length<=h.prev_length){for(w=h.strstart+h.lookahead-T,k=l._tr_tally(h,h.strstart-1-h.prev_match,h.prev_length-T),h.lookahead-=h.prev_length-1,h.prev_length-=2;++h.strstart<=w&&(h.ins_h=(h.ins_h<h.pending_buf_size-5&&(z=h.pending_buf_size-5);;){if(h.lookahead<=1){if(be(h),h.lookahead===0&&U===d)return m;if(h.lookahead===0)break}h.strstart+=h.lookahead,h.lookahead=0;var k=h.block_start+z;if((h.strstart===0||h.strstart>=k)&&(h.lookahead=h.strstart-k,h.strstart=k,_(h,!1),h.strm.avail_out===0)||h.strstart-h.block_start>=h.w_size-H&&(_(h,!1),h.strm.avail_out===0))return m}return h.insert=0,U===A?(_(h,!0),h.strm.avail_out===0?ne:q):(h.strstart>h.block_start&&(_(h,!1),h.strm.avail_out),m)}),new xe(4,4,8,4,Re),new xe(4,5,16,8,Re),new xe(4,6,32,32,Re),new xe(4,4,16,16,ke),new xe(8,16,32,32,ke),new xe(8,16,128,128,ke),new xe(8,32,128,256,ke),new xe(32,128,258,1024,ke),new xe(32,258,258,4096,ke)],i.deflateInit=function(h,U){return Be(h,U,b,15,8,0)},i.deflateInit2=Be,i.deflateReset=tt,i.deflateResetKeep=we,i.deflateSetHeader=function(h,U){return h&&h.state?h.state.wrap!==2?p:(h.state.gzhead=U,f):p},i.deflate=function(h,U){var z,k,w,D;if(!h||!h.state||5>8&255),fe(k,k.gzhead.time>>16&255),fe(k,k.gzhead.time>>24&255),fe(k,k.level===9?2:2<=k.strategy||k.level<2?4:0),fe(k,255&k.gzhead.os),k.gzhead.extra&&k.gzhead.extra.length&&(fe(k,255&k.gzhead.extra.length),fe(k,k.gzhead.extra.length>>8&255)),k.gzhead.hcrc&&(h.adler=s(h.adler,k.pending_buf,k.pending,0)),k.gzindex=0,k.status=69):(fe(k,0),fe(k,0),fe(k,0),fe(k,0),fe(k,0),fe(k,k.level===9?2:2<=k.strategy||k.level<2?4:0),fe(k,3),k.status=I);else{var K=b+(k.w_bits-8<<4)<<8;K|=(2<=k.strategy||k.level<2?0:k.level<6?1:k.level===6?2:3)<<6,k.strstart!==0&&(K|=32),K+=31-K%31,k.status=I,ce(k,K),k.strstart!==0&&(ce(k,h.adler>>>16),ce(k,65535&h.adler)),h.adler=1}if(k.status===69)if(k.gzhead.extra){for(w=k.pending;k.gzindex<(65535&k.gzhead.extra.length)&&(k.pending!==k.pending_buf_size||(k.gzhead.hcrc&&k.pending>w&&(h.adler=s(h.adler,k.pending_buf,k.pending-w,w)),j(h),w=k.pending,k.pending!==k.pending_buf_size));)fe(k,255&k.gzhead.extra[k.gzindex]),k.gzindex++;k.gzhead.hcrc&&k.pending>w&&(h.adler=s(h.adler,k.pending_buf,k.pending-w,w)),k.gzindex===k.gzhead.extra.length&&(k.gzindex=0,k.status=73)}else k.status=73;if(k.status===73)if(k.gzhead.name){w=k.pending;do{if(k.pending===k.pending_buf_size&&(k.gzhead.hcrc&&k.pending>w&&(h.adler=s(h.adler,k.pending_buf,k.pending-w,w)),j(h),w=k.pending,k.pending===k.pending_buf_size)){D=1;break}D=k.gzindexw&&(h.adler=s(h.adler,k.pending_buf,k.pending-w,w)),D===0&&(k.gzindex=0,k.status=91)}else k.status=91;if(k.status===91)if(k.gzhead.comment){w=k.pending;do{if(k.pending===k.pending_buf_size&&(k.gzhead.hcrc&&k.pending>w&&(h.adler=s(h.adler,k.pending_buf,k.pending-w,w)),j(h),w=k.pending,k.pending===k.pending_buf_size)){D=1;break}D=k.gzindexw&&(h.adler=s(h.adler,k.pending_buf,k.pending-w,w)),D===0&&(k.status=103)}else k.status=103;if(k.status===103&&(k.gzhead.hcrc?(k.pending+2>k.pending_buf_size&&j(h),k.pending+2<=k.pending_buf_size&&(fe(k,255&h.adler),fe(k,h.adler>>8&255),h.adler=0,k.status=I)):k.status=I),k.pending!==0){if(j(h),h.avail_out===0)return k.last_flush=-1,f}else if(h.avail_in===0&&Z(U)<=Z(z)&&U!==A)return se(h,-5);if(k.status===666&&h.avail_in!==0)return se(h,-5);if(h.avail_in!==0||k.lookahead!==0||U!==d&&k.status!==666){var Y=k.strategy===2?(function(M,J){for(var ae;;){if(M.lookahead===0&&(be(M),M.lookahead===0)){if(J===d)return m;break}if(M.match_length=0,ae=l._tr_tally(M,0,M.window[M.strstart]),M.lookahead--,M.strstart++,ae&&(_(M,!1),M.strm.avail_out===0))return m}return M.insert=0,J===A?(_(M,!0),M.strm.avail_out===0?ne:q):M.last_lit&&(_(M,!1),M.strm.avail_out===0)?m:N})(k,U):k.strategy===3?(function(M,J){for(var ae,ee,pe,Ee,ve=M.window;;){if(M.lookahead<=W){if(be(M),M.lookahead<=W&&J===d)return m;if(M.lookahead===0)break}if(M.match_length=0,M.lookahead>=T&&0M.lookahead&&(M.match_length=M.lookahead)}if(M.match_length>=T?(ae=l._tr_tally(M,1,M.match_length-T),M.lookahead-=M.match_length,M.strstart+=M.match_length,M.match_length=0):(ae=l._tr_tally(M,0,M.window[M.strstart]),M.lookahead--,M.strstart++),ae&&(_(M,!1),M.strm.avail_out===0))return m}return M.insert=0,J===A?(_(M,!0),M.strm.avail_out===0?ne:q):M.last_lit&&(_(M,!1),M.strm.avail_out===0)?m:N})(k,U):a[k.level].func(k,U);if(Y!==ne&&Y!==q||(k.status=666),Y===m||Y===ne)return h.avail_out===0&&(k.last_flush=-1),f;if(Y===N&&(U===1?l._tr_align(k):U!==5&&(l._tr_stored_block(k,0,0,!1),U===3&&(ue(k.head),k.lookahead===0&&(k.strstart=0,k.block_start=0,k.insert=0))),j(h),h.avail_out===0))return k.last_flush=-1,f}return U!==A?f:k.wrap<=0?1:(k.wrap===2?(fe(k,255&h.adler),fe(k,h.adler>>8&255),fe(k,h.adler>>16&255),fe(k,h.adler>>24&255),fe(k,255&h.total_in),fe(k,h.total_in>>8&255),fe(k,h.total_in>>16&255),fe(k,h.total_in>>24&255)):(ce(k,h.adler>>>16),ce(k,65535&h.adler)),j(h),0=z.w_size&&(D===0&&(ue(z.head),z.strstart=0,z.block_start=0,z.insert=0),J=new o.Buf8(z.w_size),o.arraySet(J,U,ae-z.w_size,z.w_size,0),U=J,ae=z.w_size),K=h.avail_in,Y=h.next_in,M=h.input,h.avail_in=ae,h.next_in=0,h.input=U,be(z);z.lookahead>=T;){for(k=z.strstart,w=z.lookahead-(T-1);z.ins_h=(z.ins_h<>>=T=B>>>24,x-=T,(T=B>>>16&255)===0)N[u++]=65535&B;else{if(!(16&T)){if((64&T)==0){B=P[(65535&B)+(b&(1<>>=T,x-=T),x<15&&(b+=m[c++]<>>=T=B>>>24,x-=T,!(16&(T=B>>>16&255))){if((64&T)==0){B=F[(65535&B)+(b&(1<>>=T,x-=T,(T=u-d)>3,b&=(1<<(x-=W<<3))-1,a.next_in=c,a.next_out=u,a.avail_in=c>>24&255)+(C>>>8&65280)+((65280&C)<<8)+((255&C)<<24)}function b(){this.mode=0,this.last=!1,this.wrap=0,this.havedict=!1,this.flags=0,this.dmax=0,this.check=0,this.total=0,this.head=null,this.wbits=0,this.wsize=0,this.whave=0,this.wnext=0,this.window=null,this.hold=0,this.bits=0,this.length=0,this.offset=0,this.extra=0,this.lencode=null,this.distcode=null,this.lenbits=0,this.distbits=0,this.ncode=0,this.nlen=0,this.ndist=0,this.have=0,this.next=null,this.lens=new a.Buf16(320),this.work=new a.Buf16(288),this.lendyn=null,this.distdyn=null,this.sane=0,this.back=0,this.was=0}function x(C){var I;return C&&C.state?(I=C.state,C.total_in=C.total_out=I.total=0,C.msg="",I.wrap&&(C.adler=1&I.wrap),I.mode=p,I.last=0,I.havedict=0,I.dmax=32768,I.head=null,I.hold=0,I.bits=0,I.lencode=I.lendyn=new a.Buf32(g),I.distcode=I.distdyn=new a.Buf32(y),I.sane=1,I.back=-1,A):f}function P(C){var I;return C&&C.state?((I=C.state).wsize=0,I.whave=0,I.wnext=0,x(C)):f}function F(C,I){var m,N;return C&&C.state?(N=C.state,I<0?(m=0,I=-I):(m=1+(I>>4),I<48&&(I&=15)),I&&(I<8||15=q.wsize?(a.arraySet(q.window,I,m-q.wsize,q.wsize,0),q.wnext=0,q.whave=q.wsize):(N<(ne=q.wsize-q.wnext)&&(ne=N),a.arraySet(q.window,I,m-N,ne,q.wnext),(N-=ne)?(a.arraySet(q.window,I,m-N,N,0),q.wnext=N,q.whave=q.wsize):(q.wnext+=ne,q.wnext===q.wsize&&(q.wnext=0),q.whave>>8&255,m.check=l(m.check,D,2,0),_=j=0,m.mode=2;break}if(m.flags=0,m.head&&(m.head.done=!1),!(1&m.wrap)||(((255&j)<<8)+(j>>8))%31){C.msg="incorrect header check",m.mode=30;break}if((15&j)!=8){C.msg="unknown compression method",m.mode=30;break}if(_-=4,h=8+(15&(j>>>=4)),m.wbits===0)m.wbits=h;else if(h>m.wbits){C.msg="invalid window size",m.mode=30;break}m.dmax=1<>8&1),512&m.flags&&(D[0]=255&j,D[1]=j>>>8&255,m.check=l(m.check,D,2,0)),_=j=0,m.mode=3;case 3:for(;_<32;){if(Z===0)break e;Z--,j+=N[q++]<<_,_+=8}m.head&&(m.head.time=j),512&m.flags&&(D[0]=255&j,D[1]=j>>>8&255,D[2]=j>>>16&255,D[3]=j>>>24&255,m.check=l(m.check,D,4,0)),_=j=0,m.mode=4;case 4:for(;_<16;){if(Z===0)break e;Z--,j+=N[q++]<<_,_+=8}m.head&&(m.head.xflags=255&j,m.head.os=j>>8),512&m.flags&&(D[0]=255&j,D[1]=j>>>8&255,m.check=l(m.check,D,2,0)),_=j=0,m.mode=5;case 5:if(1024&m.flags){for(;_<16;){if(Z===0)break e;Z--,j+=N[q++]<<_,_+=8}m.length=j,m.head&&(m.head.extra_len=j),512&m.flags&&(D[0]=255&j,D[1]=j>>>8&255,m.check=l(m.check,D,2,0)),_=j=0}else m.head&&(m.head.extra=null);m.mode=6;case 6:if(1024&m.flags&&(Z<(ie=m.length)&&(ie=Z),ie&&(m.head&&(h=m.head.extra_len-m.length,m.head.extra||(m.head.extra=new Array(m.head.extra_len)),a.arraySet(m.head.extra,N,q,ie,h)),512&m.flags&&(m.check=l(m.check,N,ie,q)),Z-=ie,q+=ie,m.length-=ie),m.length))break e;m.length=0,m.mode=7;case 7:if(2048&m.flags){if(Z===0)break e;for(ie=0;h=N[q+ie++],m.head&&h&&m.length<65536&&(m.head.name+=String.fromCharCode(h)),h&&ie>9&1,m.head.done=!0),C.adler=m.check=0,m.mode=12;break;case 10:for(;_<32;){if(Z===0)break e;Z--,j+=N[q++]<<_,_+=8}C.adler=m.check=v(j),_=j=0,m.mode=11;case 11:if(m.havedict===0)return C.next_out=se,C.avail_out=ue,C.next_in=q,C.avail_in=Z,m.hold=j,m.bits=_,2;C.adler=m.check=1,m.mode=12;case 12:if(I===5||I===6)break e;case 13:if(m.last){j>>>=7&_,_-=7&_,m.mode=27;break}for(;_<3;){if(Z===0)break e;Z--,j+=N[q++]<<_,_+=8}switch(m.last=1&j,_-=1,3&(j>>>=1)){case 0:m.mode=14;break;case 1:if(W(m),m.mode=20,I!==6)break;j>>>=2,_-=2;break e;case 2:m.mode=17;break;case 3:C.msg="invalid block type",m.mode=30}j>>>=2,_-=2;break;case 14:for(j>>>=7&_,_-=7&_;_<32;){if(Z===0)break e;Z--,j+=N[q++]<<_,_+=8}if((65535&j)!=(j>>>16^65535)){C.msg="invalid stored block lengths",m.mode=30;break}if(m.length=65535&j,_=j=0,m.mode=15,I===6)break e;case 15:m.mode=16;case 16:if(ie=m.length){if(Z>>=5,_-=5,m.ndist=1+(31&j),j>>>=5,_-=5,m.ncode=4+(15&j),j>>>=4,_-=4,286>>=3,_-=3}for(;m.have<19;)m.lens[K[m.have++]]=0;if(m.lencode=m.lendyn,m.lenbits=7,z={bits:m.lenbits},U=s(0,m.lens,0,19,m.lencode,0,m.work,z),m.lenbits=z.bits,U){C.msg="invalid code lengths set",m.mode=30;break}m.have=0,m.mode=19;case 19:for(;m.have>>16&255,Oe=65535&w,!((ke=w>>>24)<=_);){if(Z===0)break e;Z--,j+=N[q++]<<_,_+=8}if(Oe<16)j>>>=ke,_-=ke,m.lens[m.have++]=Oe;else{if(Oe===16){for(k=ke+2;_>>=ke,_-=ke,m.have===0){C.msg="invalid bit length repeat",m.mode=30;break}h=m.lens[m.have-1],ie=3+(3&j),j>>>=2,_-=2}else if(Oe===17){for(k=ke+3;_>>=ke)),j>>>=3,_-=3}else{for(k=ke+7;_>>=ke)),j>>>=7,_-=7}if(m.have+ie>m.nlen+m.ndist){C.msg="invalid bit length repeat",m.mode=30;break}for(;ie--;)m.lens[m.have++]=h}}if(m.mode===30)break;if(m.lens[256]===0){C.msg="invalid code -- missing end-of-block",m.mode=30;break}if(m.lenbits=9,z={bits:m.lenbits},U=s(u,m.lens,0,m.nlen,m.lencode,0,m.work,z),m.lenbits=z.bits,U){C.msg="invalid literal/lengths set",m.mode=30;break}if(m.distbits=6,m.distcode=m.distdyn,z={bits:m.distbits},U=s(d,m.lens,m.nlen,m.ndist,m.distcode,0,m.work,z),m.distbits=z.bits,U){C.msg="invalid distances set",m.mode=30;break}if(m.mode=20,I===6)break e;case 20:m.mode=21;case 21:if(6<=Z&&258<=ue){C.next_out=se,C.avail_out=ue,C.next_in=q,C.avail_in=Z,m.hold=j,m.bits=_,c(C,ce),se=C.next_out,ne=C.output,ue=C.avail_out,q=C.next_in,N=C.input,Z=C.avail_in,j=m.hold,_=m.bits,m.mode===12&&(m.back=-1);break}for(m.back=0;xe=(w=m.lencode[j&(1<>>16&255,Oe=65535&w,!((ke=w>>>24)<=_);){if(Z===0)break e;Z--,j+=N[q++]<<_,_+=8}if(xe&&(240&xe)==0){for(we=ke,tt=xe,Be=Oe;xe=(w=m.lencode[Be+((j&(1<>we)])>>>16&255,Oe=65535&w,!(we+(ke=w>>>24)<=_);){if(Z===0)break e;Z--,j+=N[q++]<<_,_+=8}j>>>=we,_-=we,m.back+=we}if(j>>>=ke,_-=ke,m.back+=ke,m.length=Oe,xe===0){m.mode=26;break}if(32&xe){m.back=-1,m.mode=12;break}if(64&xe){C.msg="invalid literal/length code",m.mode=30;break}m.extra=15&xe,m.mode=22;case 22:if(m.extra){for(k=m.extra;_>>=m.extra,_-=m.extra,m.back+=m.extra}m.was=m.length,m.mode=23;case 23:for(;xe=(w=m.distcode[j&(1<>>16&255,Oe=65535&w,!((ke=w>>>24)<=_);){if(Z===0)break e;Z--,j+=N[q++]<<_,_+=8}if((240&xe)==0){for(we=ke,tt=xe,Be=Oe;xe=(w=m.distcode[Be+((j&(1<>we)])>>>16&255,Oe=65535&w,!(we+(ke=w>>>24)<=_);){if(Z===0)break e;Z--,j+=N[q++]<<_,_+=8}j>>>=we,_-=we,m.back+=we}if(j>>>=ke,_-=ke,m.back+=ke,64&xe){C.msg="invalid distance code",m.mode=30;break}m.offset=Oe,m.extra=15&xe,m.mode=24;case 24:if(m.extra){for(k=m.extra;_>>=m.extra,_-=m.extra,m.back+=m.extra}if(m.offset>m.dmax){C.msg="invalid distance too far back",m.mode=30;break}m.mode=25;case 25:if(ue===0)break e;if(ie=ce-ue,m.offset>ie){if((ie=m.offset-ie)>m.whave&&m.sane){C.msg="invalid distance too far back",m.mode=30;break}be=ie>m.wnext?(ie-=m.wnext,m.wsize-ie):m.wnext-ie,ie>m.length&&(ie=m.length),Re=m.window}else Re=ne,be=se-m.offset,ie=m.length;for(ueE?(T=be[Re+y[I]],_[fe+y[I]]):(T=96,0),b=1<>se)+(x-=b)]=B<<24|T<<16|W|0,x!==0;);for(b=1<>=1;if(b!==0?(j&=b-1,j+=b):j=0,I++,--ce[C]==0){if(C===N)break;C=d[A+y[I]]}if(ne>>7)]}function fe(w,D){w.pending_buf[w.pending++]=255&D,w.pending_buf[w.pending++]=D>>>8&255}function ce(w,D,K){w.bi_valid>v-K?(w.bi_buf|=D<>v-w.bi_valid,w.bi_valid+=K-v):(w.bi_buf|=D<>>=1,K<<=1,0<--D;);return K>>>1}function Re(w,D,K){var Y,M,J=new Array(y+1),ae=0;for(Y=1;Y<=y;Y++)J[Y]=ae=ae+K[Y-1]<<1;for(M=0;M<=D;M++){var ee=w[2*M+1];ee!==0&&(w[2*M]=be(J[ee]++,ee))}}function ke(w){var D;for(D=0;D>1;1<=K;K--)we(w,J,K);for(M=pe;K=w.heap[1],w.heap[1]=w.heap[w.heap_len--],we(w,J,1),Y=w.heap[1],w.heap[--w.heap_max]=K,w.heap[--w.heap_max]=Y,J[2*M]=J[2*K]+J[2*Y],w.depth[M]=(w.depth[K]>=w.depth[Y]?w.depth[K]:w.depth[Y])+1,J[2*K+1]=J[2*Y+1]=M,w.heap[1]=M++,we(w,J,1),2<=w.heap_len;);w.heap[--w.heap_max]=w.heap[1],(function(ve,Ie){var rt,Me,G,oe,me,De,te=Ie.dyn_tree,ge=Ie.max_code,L=Ie.stat_desc.static_tree,ye=Ie.stat_desc.has_stree,Ge=Ie.stat_desc.extra_bits,S=Ie.stat_desc.extra_base,de=Ie.stat_desc.max_length,Ae=0;for(oe=0;oe<=y;oe++)ve.bl_count[oe]=0;for(te[2*ve.heap[ve.heap_max]+1]=0,rt=ve.heap_max+1;rt>=7;M>>=1)if(1&Ee&&ee.dyn_ltree[2*pe]!==0)return o;if(ee.dyn_ltree[18]!==0||ee.dyn_ltree[20]!==0||ee.dyn_ltree[26]!==0)return l;for(pe=32;pe>>3,(J=w.static_len+3+7>>>3)<=M&&(M=J)):M=J=K+5,K+4<=M&&D!==-1?k(w,D,K,Y):w.strategy===4||J===M?(ce(w,2+(Y?1:0),3),tt(w,H,C)):(ce(w,4+(Y?1:0),3),(function(ee,pe,Ee,ve){var Ie;for(ce(ee,pe-257,5),ce(ee,Ee-1,5),ce(ee,ve-4,4),Ie=0;Ie>>8&255,w.pending_buf[w.d_buf+2*w.last_lit+1]=255&D,w.pending_buf[w.l_buf+w.last_lit]=255&K,w.last_lit++,D===0?w.dyn_ltree[2*K]++:(w.matches++,D--,w.dyn_ltree[2*(m[K]+d+1)]++,w.dyn_dtree[2*_(D)]++),w.last_lit===w.lit_bufsize-1},i._tr_align=function(w){ce(w,2,3),ie(w,x,H),(function(D){D.bi_valid===16?(fe(D,D.bi_buf),D.bi_buf=0,D.bi_valid=0):8<=D.bi_valid&&(D.pending_buf[D.pending++]=255&D.bi_buf,D.bi_buf>>=8,D.bi_valid-=8)})(w)}},{"../utils/common":41}],53:[function(t,n,i){"use strict";n.exports=function(){this.input=null,this.next_in=0,this.avail_in=0,this.total_in=0,this.output=null,this.next_out=0,this.avail_out=0,this.total_out=0,this.msg="",this.state=null,this.data_type=2,this.adler=0}},{}],54:[function(t,n,i){(function(a){(function(o,l){"use strict";if(!o.setImmediate){var c,s,u,d,A=1,f={},p=!1,g=o.document,y=Object.getPrototypeOf&&Object.getPrototypeOf(o);y=y&&y.setTimeout?y:o,c={}.toString.call(o.process)==="[object process]"?function(P){process.nextTick(function(){b(P)})}:(function(){if(o.postMessage&&!o.importScripts){var P=!0,F=o.onmessage;return o.onmessage=function(){P=!1},o.postMessage("","*"),o.onmessage=F,P}})()?(d="setImmediate$"+Math.random()+"$",o.addEventListener?o.addEventListener("message",x,!1):o.attachEvent("onmessage",x),function(P){o.postMessage(d+P,"*")}):o.MessageChannel?((u=new MessageChannel).port1.onmessage=function(P){b(P.data)},function(P){u.port2.postMessage(P)}):g&&"onreadystatechange"in g.createElement("script")?(s=g.documentElement,function(P){var F=g.createElement("script");F.onreadystatechange=function(){b(P),F.onreadystatechange=null,s.removeChild(F),F=null},s.appendChild(F)}):function(P){setTimeout(b,0,P)},y.setImmediate=function(P){typeof P!="function"&&(P=new Function(""+P));for(var F=new Array(arguments.length-1),R=0;R"u"?a===void 0?this:a:self)}).call(this,typeof global<"u"?global:typeof self<"u"?self:typeof window<"u"?window:{})},{}]},{},[10])(10)})}}),cp=Nt({"node_modules/.pnpm/jszip@3.10.1/node_modules/jszip/dist/jszip.min.js"(e,r){(function(t){typeof e=="object"&&typeof r<"u"?r.exports=t():typeof define=="function"&&define.amd?define([],t):(typeof window<"u"?window:typeof global<"u"?global:typeof self<"u"?self:this).JSZip=t()})(function(){return(function t(n,i,a){function o(s,u){if(!i[s]){if(!n[s]){var d=typeof An=="function"&&An;if(!u&&d)return d(s,!0);if(l)return l(s,!0);var A=new Error("Cannot find module '"+s+"'");throw A.code="MODULE_NOT_FOUND",A}var f=i[s]={exports:{}};n[s][0].call(f.exports,function(p){var g=n[s][1][p];return o(g||p)},f,f.exports,t,n,i,a)}return i[s].exports}for(var l=typeof An=="function"&&An,c=0;c>2,f=(3&s)<<4|u>>4,p=1>6:64,g=2>4,u=(15&A)<<4|(f=l.indexOf(c.charAt(g++)))>>2,d=(3&f)<<6|(p=l.indexOf(c.charAt(g++))),b[y++]=s,f!==64&&(b[y++]=u),p!==64&&(b[y++]=d);return b}},{"./support":30,"./utils":32}],2:[function(t,n,i){"use strict";var a=t("./external"),o=t("./stream/DataWorker"),l=t("./stream/Crc32Probe"),c=t("./stream/DataLengthProbe");function s(u,d,A,f,p){this.compressedSize=u,this.uncompressedSize=d,this.crc32=A,this.compression=f,this.compressedContent=p}s.prototype={getContentWorker:function(){var u=new o(a.Promise.resolve(this.compressedContent)).pipe(this.compression.uncompressWorker()).pipe(new c("data_length")),d=this;return u.on("end",function(){if(this.streamInfo.data_length!==d.uncompressedSize)throw new Error("Bug : uncompressed data size mismatch")}),u},getCompressedWorker:function(){return new o(a.Promise.resolve(this.compressedContent)).withStreamInfo("compressedSize",this.compressedSize).withStreamInfo("uncompressedSize",this.uncompressedSize).withStreamInfo("crc32",this.crc32).withStreamInfo("compression",this.compression)}},s.createWorkerFrom=function(u,d,A){return u.pipe(new l).pipe(new c("uncompressedSize")).pipe(d.compressWorker(A)).pipe(new c("compressedSize")).withStreamInfo("compression",d)},n.exports=s},{"./external":6,"./stream/Crc32Probe":25,"./stream/DataLengthProbe":26,"./stream/DataWorker":27}],3:[function(t,n,i){"use strict";var a=t("./stream/GenericWorker");i.STORE={magic:"\0\0",compressWorker:function(){return new a("STORE compression")},uncompressWorker:function(){return new a("STORE decompression")}},i.DEFLATE=t("./flate")},{"./flate":7,"./stream/GenericWorker":28}],4:[function(t,n,i){"use strict";var a=t("./utils"),o=(function(){for(var l,c=[],s=0;s<256;s++){l=s;for(var u=0;u<8;u++)l=1&l?3988292384^l>>>1:l>>>1;c[s]=l}return c})();n.exports=function(l,c){return l!==void 0&&l.length?a.getTypeOf(l)!=="string"?(function(s,u,d,A){var f=o,p=A+d;s^=-1;for(var g=A;g>>8^f[255&(s^u[g])];return-1^s})(0|c,l,l.length,0):(function(s,u,d,A){var f=o,p=A+d;s^=-1;for(var g=A;g>>8^f[255&(s^u.charCodeAt(g))];return-1^s})(0|c,l,l.length,0):0}},{"./utils":32}],5:[function(t,n,i){"use strict";i.base64=!1,i.binary=!1,i.dir=!1,i.createFolders=!0,i.date=null,i.compression=null,i.compressionOptions=null,i.comment=null,i.unixPermissions=null,i.dosPermissions=null},{}],6:[function(t,n,i){"use strict";var a=null;a=typeof Promise<"u"?Promise:t("lie"),n.exports={Promise:a}},{lie:37}],7:[function(t,n,i){"use strict";var a=typeof Uint8Array<"u"&&typeof Uint16Array<"u"&&typeof Uint32Array<"u",o=t("pako"),l=t("./utils"),c=t("./stream/GenericWorker"),s=a?"uint8array":"array";function u(d,A){c.call(this,"FlateWorker/"+d),this._pako=null,this._pakoAction=d,this._pakoOptions=A,this.meta={}}i.magic="\b\0",l.inherits(u,c),u.prototype.processChunk=function(d){this.meta=d.meta,this._pako===null&&this._createPako(),this._pako.push(l.transformTo(s,d.data),!1)},u.prototype.flush=function(){c.prototype.flush.call(this),this._pako===null&&this._createPako(),this._pako.push([],!0)},u.prototype.cleanUp=function(){c.prototype.cleanUp.call(this),this._pako=null},u.prototype._createPako=function(){this._pako=new o[this._pakoAction]({raw:!0,level:this._pakoOptions.level||-1});var d=this;this._pako.onData=function(A){d.push({data:A,meta:d.meta})}},i.compressWorker=function(d){return new u("Deflate",d)},i.uncompressWorker=function(){return new u("Inflate",{})}},{"./stream/GenericWorker":28,"./utils":32,pako:38}],8:[function(t,n,i){"use strict";function a(f,p){var g,y="";for(g=0;g>>=8;return y}function o(f,p,g,y,v,b){var x,P,F=f.file,R=f.compression,E=b!==s.utf8encode,B=l.transformTo("string",b(F.name)),T=l.transformTo("string",s.utf8encode(F.name)),W=F.comment,H=l.transformTo("string",b(W)),C=l.transformTo("string",s.utf8encode(W)),I=T.length!==F.name.length,m=C.length!==W.length,N="",ne="",q="",se=F.dir,Z=F.date,ue={crc32:0,compressedSize:0,uncompressedSize:0};p&&!g||(ue.crc32=f.crc32,ue.compressedSize=f.compressedSize,ue.uncompressedSize=f.uncompressedSize);var j=0;p&&(j|=8),E||!I&&!m||(j|=2048);var _=0,fe=0;se&&(_|=16),v==="UNIX"?(fe=798,_|=(function(ie,be){var Re=ie;return ie||(Re=be?16893:33204),(65535&Re)<<16})(F.unixPermissions,se)):(fe=20,_|=(function(ie){return 63&(ie||0)})(F.dosPermissions)),x=Z.getUTCHours(),x<<=6,x|=Z.getUTCMinutes(),x<<=5,x|=Z.getUTCSeconds()/2,P=Z.getUTCFullYear()-1980,P<<=4,P|=Z.getUTCMonth()+1,P<<=5,P|=Z.getUTCDate(),I&&(ne=a(1,1)+a(u(B),4)+T,N+="up"+a(ne.length,2)+ne),m&&(q=a(1,1)+a(u(H),4)+C,N+="uc"+a(q.length,2)+q);var ce="";return ce+=` -\0`,ce+=a(j,2),ce+=R.magic,ce+=a(x,2),ce+=a(P,2),ce+=a(ue.crc32,4),ce+=a(ue.compressedSize,4),ce+=a(ue.uncompressedSize,4),ce+=a(B.length,2),ce+=a(N.length,2),{fileRecord:d.LOCAL_FILE_HEADER+ce+B+N,dirRecord:d.CENTRAL_FILE_HEADER+a(fe,2)+ce+a(H.length,2)+"\0\0\0\0"+a(_,4)+a(y,4)+B+N+H}}var l=t("../utils"),c=t("../stream/GenericWorker"),s=t("../utf8"),u=t("../crc32"),d=t("../signature");function A(f,p,g,y){c.call(this,"ZipFileWorker"),this.bytesWritten=0,this.zipComment=p,this.zipPlatform=g,this.encodeFileName=y,this.streamFiles=f,this.accumulate=!1,this.contentBuffer=[],this.dirRecords=[],this.currentSourceOffset=0,this.entriesCount=0,this.currentFile=null,this._sources=[]}l.inherits(A,c),A.prototype.push=function(f){var p=f.meta.percent||0,g=this.entriesCount,y=this._sources.length;this.accumulate?this.contentBuffer.push(f):(this.bytesWritten+=f.data.length,c.prototype.push.call(this,{data:f.data,meta:{currentFile:this.currentFile,percent:g?(p+100*(g-y-1))/g:100}}))},A.prototype.openedSource=function(f){this.currentSourceOffset=this.bytesWritten,this.currentFile=f.file.name;var p=this.streamFiles&&!f.file.dir;if(p){var g=o(f,p,!1,this.currentSourceOffset,this.zipPlatform,this.encodeFileName);this.push({data:g.fileRecord,meta:{percent:0}})}else this.accumulate=!0},A.prototype.closedSource=function(f){this.accumulate=!1;var p=this.streamFiles&&!f.file.dir,g=o(f,p,!0,this.currentSourceOffset,this.zipPlatform,this.encodeFileName);if(this.dirRecords.push(g.dirRecord),p)this.push({data:(function(y){return d.DATA_DESCRIPTOR+a(y.crc32,4)+a(y.compressedSize,4)+a(y.uncompressedSize,4)})(f),meta:{percent:100}});else for(this.push({data:g.fileRecord,meta:{percent:0}});this.contentBuffer.length;)this.push(this.contentBuffer.shift());this.currentFile=null},A.prototype.flush=function(){for(var f=this.bytesWritten,p=0;p=this.index;c--)s=(s<<8)+this.byteAt(c);return this.index+=l,s},readString:function(l){return a.transformTo("string",this.readData(l))},readData:function(){},lastIndexOfSignature:function(){},readAndCheckSignature:function(){},readDate:function(){var l=this.readInt(4);return new Date(Date.UTC(1980+(l>>25&127),(l>>21&15)-1,l>>16&31,l>>11&31,l>>5&63,(31&l)<<1))}},n.exports=o},{"../utils":32}],19:[function(t,n,i){"use strict";var a=t("./Uint8ArrayReader");function o(l){a.call(this,l)}t("../utils").inherits(o,a),o.prototype.readData=function(l){this.checkOffset(l);var c=this.data.slice(this.zero+this.index,this.zero+this.index+l);return this.index+=l,c},n.exports=o},{"../utils":32,"./Uint8ArrayReader":21}],20:[function(t,n,i){"use strict";var a=t("./DataReader");function o(l){a.call(this,l)}t("../utils").inherits(o,a),o.prototype.byteAt=function(l){return this.data.charCodeAt(this.zero+l)},o.prototype.lastIndexOfSignature=function(l){return this.data.lastIndexOf(l)-this.zero},o.prototype.readAndCheckSignature=function(l){return l===this.readData(4)},o.prototype.readData=function(l){this.checkOffset(l);var c=this.data.slice(this.zero+this.index,this.zero+this.index+l);return this.index+=l,c},n.exports=o},{"../utils":32,"./DataReader":18}],21:[function(t,n,i){"use strict";var a=t("./ArrayReader");function o(l){a.call(this,l)}t("../utils").inherits(o,a),o.prototype.readData=function(l){if(this.checkOffset(l),l===0)return new Uint8Array(0);var c=this.data.subarray(this.zero+this.index,this.zero+this.index+l);return this.index+=l,c},n.exports=o},{"../utils":32,"./ArrayReader":17}],22:[function(t,n,i){"use strict";var a=t("../utils"),o=t("../support"),l=t("./ArrayReader"),c=t("./StringReader"),s=t("./NodeBufferReader"),u=t("./Uint8ArrayReader");n.exports=function(d){var A=a.getTypeOf(d);return a.checkSupport(A),A!=="string"||o.uint8array?A==="nodebuffer"?new s(d):o.uint8array?new u(a.transformTo("uint8array",d)):new l(a.transformTo("array",d)):new c(d)}},{"../support":30,"../utils":32,"./ArrayReader":17,"./NodeBufferReader":19,"./StringReader":20,"./Uint8ArrayReader":21}],23:[function(t,n,i){"use strict";i.LOCAL_FILE_HEADER="PK",i.CENTRAL_FILE_HEADER="PK",i.CENTRAL_DIRECTORY_END="PK",i.ZIP64_CENTRAL_DIRECTORY_LOCATOR="PK\x07",i.ZIP64_CENTRAL_DIRECTORY_END="PK",i.DATA_DESCRIPTOR="PK\x07\b"},{}],24:[function(t,n,i){"use strict";var a=t("./GenericWorker"),o=t("../utils");function l(c){a.call(this,"ConvertWorker to "+c),this.destType=c}o.inherits(l,a),l.prototype.processChunk=function(c){this.push({data:o.transformTo(this.destType,c.data),meta:c.meta})},n.exports=l},{"../utils":32,"./GenericWorker":28}],25:[function(t,n,i){"use strict";var a=t("./GenericWorker"),o=t("../crc32");function l(){a.call(this,"Crc32Probe"),this.withStreamInfo("crc32",0)}t("../utils").inherits(l,a),l.prototype.processChunk=function(c){this.streamInfo.crc32=o(c.data,this.streamInfo.crc32||0),this.push(c)},n.exports=l},{"../crc32":4,"../utils":32,"./GenericWorker":28}],26:[function(t,n,i){"use strict";var a=t("../utils"),o=t("./GenericWorker");function l(c){o.call(this,"DataLengthProbe for "+c),this.propName=c,this.withStreamInfo(c,0)}a.inherits(l,o),l.prototype.processChunk=function(c){if(c){var s=this.streamInfo[this.propName]||0;this.streamInfo[this.propName]=s+c.data.length}o.prototype.processChunk.call(this,c)},n.exports=l},{"../utils":32,"./GenericWorker":28}],27:[function(t,n,i){"use strict";var a=t("../utils"),o=t("./GenericWorker");function l(c){o.call(this,"DataWorker");var s=this;this.dataIsReady=!1,this.index=0,this.max=0,this.data=null,this.type="",this._tickScheduled=!1,c.then(function(u){s.dataIsReady=!0,s.data=u,s.max=u&&u.length||0,s.type=a.getTypeOf(u),s.isPaused||s._tickAndRepeat()},function(u){s.error(u)})}a.inherits(l,o),l.prototype.cleanUp=function(){o.prototype.cleanUp.call(this),this.data=null},l.prototype.resume=function(){return!!o.prototype.resume.call(this)&&(!this._tickScheduled&&this.dataIsReady&&(this._tickScheduled=!0,a.delay(this._tickAndRepeat,[],this)),!0)},l.prototype._tickAndRepeat=function(){this._tickScheduled=!1,this.isPaused||this.isFinished||(this._tick(),this.isFinished||(a.delay(this._tickAndRepeat,[],this),this._tickScheduled=!0))},l.prototype._tick=function(){if(this.isPaused||this.isFinished)return!1;var c=null,s=Math.min(this.max,this.index+16384);if(this.index>=this.max)return this.end();switch(this.type){case"string":c=this.data.substring(this.index,s);break;case"uint8array":c=this.data.subarray(this.index,s);break;case"array":case"nodebuffer":c=this.data.slice(this.index,s)}return this.index=s,this.push({data:c,meta:{percent:this.max?this.index/this.max*100:0}})},n.exports=l},{"../utils":32,"./GenericWorker":28}],28:[function(t,n,i){"use strict";function a(o){this.name=o||"default",this.streamInfo={},this.generatedError=null,this.extraStreamInfo={},this.isPaused=!0,this.isFinished=!1,this.isLocked=!1,this._listeners={data:[],end:[],error:[]},this.previous=null}a.prototype={push:function(o){this.emit("data",o)},end:function(){if(this.isFinished)return!1;this.flush();try{this.emit("end"),this.cleanUp(),this.isFinished=!0}catch(o){this.emit("error",o)}return!0},error:function(o){return!this.isFinished&&(this.isPaused?this.generatedError=o:(this.isFinished=!0,this.emit("error",o),this.previous&&this.previous.error(o),this.cleanUp()),!0)},on:function(o,l){return this._listeners[o].push(l),this},cleanUp:function(){this.streamInfo=this.generatedError=this.extraStreamInfo=null,this._listeners=[]},emit:function(o,l){if(this._listeners[o])for(var c=0;c "+o:o}},n.exports=a},{}],29:[function(t,n,i){"use strict";var a=t("../utils"),o=t("./ConvertWorker"),l=t("./GenericWorker"),c=t("../base64"),s=t("../support"),u=t("../external"),d=null;if(s.nodestream)try{d=t("../nodejs/NodejsStreamOutputAdapter")}catch{}function A(p,g){return new u.Promise(function(y,v){var b=[],x=p._internalType,P=p._outputType,F=p._mimeType;p.on("data",function(R,E){b.push(R),g&&g(E)}).on("error",function(R){b=[],v(R)}).on("end",function(){try{var R=(function(E,B,T){switch(E){case"blob":return a.newBlob(a.transformTo("arraybuffer",B),T);case"base64":return c.encode(B);default:return a.transformTo(E,B)}})(P,(function(E,B){var T,W=0,H=null,C=0;for(T=0;T"u")i.blob=!1;else{var a=new ArrayBuffer(0);try{i.blob=new Blob([a],{type:"application/zip"}).size===0}catch{try{var o=new(self.BlobBuilder||self.WebKitBlobBuilder||self.MozBlobBuilder||self.MSBlobBuilder);o.append(a),i.blob=o.getBlob("application/zip").size===0}catch{i.blob=!1}}}try{i.nodestream=!!t("readable-stream").Readable}catch{i.nodestream=!1}},{"readable-stream":16}],31:[function(t,n,i){"use strict";for(var a=t("./utils"),o=t("./support"),l=t("./nodejsUtils"),c=t("./stream/GenericWorker"),s=new Array(256),u=0;u<256;u++)s[u]=252<=u?6:248<=u?5:240<=u?4:224<=u?3:192<=u?2:1;s[254]=s[254]=1;function d(){c.call(this,"utf-8 decode"),this.leftOver=null}function A(){c.call(this,"utf-8 encode")}i.utf8encode=function(f){return o.nodebuffer?l.newBufferFrom(f,"utf-8"):(function(p){var g,y,v,b,x,P=p.length,F=0;for(b=0;b>>6:(y<65536?g[x++]=224|y>>>12:(g[x++]=240|y>>>18,g[x++]=128|y>>>12&63),g[x++]=128|y>>>6&63),g[x++]=128|63&y);return g})(f)},i.utf8decode=function(f){return o.nodebuffer?a.transformTo("nodebuffer",f).toString("utf-8"):(function(p){var g,y,v,b,x=p.length,P=new Array(2*x);for(g=y=0;g>10&1023,P[y++]=56320|1023&v)}return P.length!==y&&(P.subarray?P=P.subarray(0,y):P.length=y),a.applyFromCharCode(P)})(f=a.transformTo(o.uint8array?"uint8array":"array",f))},a.inherits(d,c),d.prototype.processChunk=function(f){var p=a.transformTo(o.uint8array?"uint8array":"array",f.data);if(this.leftOver&&this.leftOver.length){if(o.uint8array){var g=p;(p=new Uint8Array(g.length+this.leftOver.length)).set(this.leftOver,0),p.set(g,this.leftOver.length)}else p=this.leftOver.concat(p);this.leftOver=null}var y=(function(b,x){var P;for((x=x||b.length)>b.length&&(x=b.length),P=x-1;0<=P&&(192&b[P])==128;)P--;return P<0||P===0?x:P+s[b[P]]>x?P:x})(p),v=p;y!==p.length&&(o.uint8array?(v=p.subarray(0,y),this.leftOver=p.subarray(y,p.length)):(v=p.slice(0,y),this.leftOver=p.slice(y,p.length))),this.push({data:i.utf8decode(v),meta:f.meta})},d.prototype.flush=function(){this.leftOver&&this.leftOver.length&&(this.push({data:i.utf8decode(this.leftOver),meta:{}}),this.leftOver=null)},i.Utf8DecodeWorker=d,a.inherits(A,c),A.prototype.processChunk=function(f){this.push({data:i.utf8encode(f.data),meta:f.meta})},i.Utf8EncodeWorker=A},{"./nodejsUtils":14,"./stream/GenericWorker":28,"./support":30,"./utils":32}],32:[function(t,n,i){"use strict";var a=t("./support"),o=t("./base64"),l=t("./nodejsUtils"),c=t("./external");function s(g){return g}function u(g,y){for(var v=0;v>8;this.dir=!!(16&this.externalFileAttributes),f==0&&(this.dosPermissions=63&this.externalFileAttributes),f==3&&(this.unixPermissions=this.externalFileAttributes>>16&65535),this.dir||this.fileNameStr.slice(-1)!=="/"||(this.dir=!0)},parseZIP64ExtraField:function(){if(this.extraFields[1]){var f=a(this.extraFields[1].value);this.uncompressedSize===o.MAX_VALUE_32BITS&&(this.uncompressedSize=f.readInt(8)),this.compressedSize===o.MAX_VALUE_32BITS&&(this.compressedSize=f.readInt(8)),this.localHeaderOffset===o.MAX_VALUE_32BITS&&(this.localHeaderOffset=f.readInt(8)),this.diskNumberStart===o.MAX_VALUE_32BITS&&(this.diskNumberStart=f.readInt(4))}},readExtraFields:function(f){var p,g,y,v=f.index+this.extraFieldsLength;for(this.extraFields||(this.extraFields={});f.index+4>>6:(f<65536?A[y++]=224|f>>>12:(A[y++]=240|f>>>18,A[y++]=128|f>>>12&63),A[y++]=128|f>>>6&63),A[y++]=128|63&f);return A},i.buf2binstring=function(d){return u(d,d.length)},i.binstring2buf=function(d){for(var A=new a.Buf8(d.length),f=0,p=A.length;f>10&1023,b[p++]=56320|1023&g)}return u(b,p)},i.utf8border=function(d,A){var f;for((A=A||d.length)>d.length&&(A=d.length),f=A-1;0<=f&&(192&d[f])==128;)f--;return f<0||f===0?A:f+c[d[f]]>A?f:A}},{"./common":41}],43:[function(t,n,i){"use strict";n.exports=function(a,o,l,c){for(var s=65535&a|0,u=a>>>16&65535|0,d=0;l!==0;){for(l-=d=2e3>>1:o>>>1;l[c]=o}return l})();n.exports=function(o,l,c,s){var u=a,d=s+c;o^=-1;for(var A=s;A>>8^u[255&(o^l[A])];return-1^o}},{}],46:[function(t,n,i){"use strict";var a,o=t("../utils/common"),l=t("./trees"),c=t("./adler32"),s=t("./crc32"),u=t("./messages"),d=0,A=4,f=0,p=-2,g=-1,y=4,v=2,b=8,x=9,P=286,F=30,R=19,E=2*P+1,B=15,T=3,W=258,H=W+T+1,C=42,I=113,m=1,N=2,ne=3,q=4;function se(h,U){return h.msg=u[U],U}function Z(h){return(h<<1)-(4h.avail_out&&(z=h.avail_out),z!==0&&(o.arraySet(h.output,U.pending_buf,U.pending_out,z,h.next_out),h.next_out+=z,U.pending_out+=z,h.total_out+=z,h.avail_out-=z,U.pending-=z,U.pending===0&&(U.pending_out=0))}function _(h,U){l._tr_flush_block(h,0<=h.block_start?h.block_start:-1,h.strstart-h.block_start,U),h.block_start=h.strstart,j(h.strm)}function fe(h,U){h.pending_buf[h.pending++]=U}function ce(h,U){h.pending_buf[h.pending++]=U>>>8&255,h.pending_buf[h.pending++]=255&U}function ie(h,U){var z,k,w=h.max_chain_length,D=h.strstart,K=h.prev_length,Y=h.nice_match,M=h.strstart>h.w_size-H?h.strstart-(h.w_size-H):0,J=h.window,ae=h.w_mask,ee=h.prev,pe=h.strstart+W,Ee=J[D+K-1],ve=J[D+K];h.prev_length>=h.good_match&&(w>>=2),Y>h.lookahead&&(Y=h.lookahead);do if(J[(z=U)+K]===ve&&J[z+K-1]===Ee&&J[z]===J[D]&&J[++z]===J[D+1]){D+=2,z++;do;while(J[++D]===J[++z]&&J[++D]===J[++z]&&J[++D]===J[++z]&&J[++D]===J[++z]&&J[++D]===J[++z]&&J[++D]===J[++z]&&J[++D]===J[++z]&&J[++D]===J[++z]&&DM&&--w!=0);return K<=h.lookahead?K:h.lookahead}function be(h){var U,z,k,w,D,K,Y,M,J,ae,ee=h.w_size;do{if(w=h.window_size-h.lookahead-h.strstart,h.strstart>=ee+(ee-H)){for(o.arraySet(h.window,h.window,ee,ee,0),h.match_start-=ee,h.strstart-=ee,h.block_start-=ee,U=z=h.hash_size;k=h.head[--U],h.head[U]=ee<=k?k-ee:0,--z;);for(U=z=ee;k=h.prev[--U],h.prev[U]=ee<=k?k-ee:0,--z;);w+=ee}if(h.strm.avail_in===0)break;if(K=h.strm,Y=h.window,M=h.strstart+h.lookahead,J=w,ae=void 0,ae=K.avail_in,J=T)for(D=h.strstart-h.insert,h.ins_h=h.window[D],h.ins_h=(h.ins_h<=T&&(h.ins_h=(h.ins_h<=T)if(k=l._tr_tally(h,h.strstart-h.match_start,h.match_length-T),h.lookahead-=h.match_length,h.match_length<=h.max_lazy_match&&h.lookahead>=T){for(h.match_length--;h.strstart++,h.ins_h=(h.ins_h<=T&&(h.ins_h=(h.ins_h<=T&&h.match_length<=h.prev_length){for(w=h.strstart+h.lookahead-T,k=l._tr_tally(h,h.strstart-1-h.prev_match,h.prev_length-T),h.lookahead-=h.prev_length-1,h.prev_length-=2;++h.strstart<=w&&(h.ins_h=(h.ins_h<h.pending_buf_size-5&&(z=h.pending_buf_size-5);;){if(h.lookahead<=1){if(be(h),h.lookahead===0&&U===d)return m;if(h.lookahead===0)break}h.strstart+=h.lookahead,h.lookahead=0;var k=h.block_start+z;if((h.strstart===0||h.strstart>=k)&&(h.lookahead=h.strstart-k,h.strstart=k,_(h,!1),h.strm.avail_out===0)||h.strstart-h.block_start>=h.w_size-H&&(_(h,!1),h.strm.avail_out===0))return m}return h.insert=0,U===A?(_(h,!0),h.strm.avail_out===0?ne:q):(h.strstart>h.block_start&&(_(h,!1),h.strm.avail_out),m)}),new xe(4,4,8,4,Re),new xe(4,5,16,8,Re),new xe(4,6,32,32,Re),new xe(4,4,16,16,ke),new xe(8,16,32,32,ke),new xe(8,16,128,128,ke),new xe(8,32,128,256,ke),new xe(32,128,258,1024,ke),new xe(32,258,258,4096,ke)],i.deflateInit=function(h,U){return Be(h,U,b,15,8,0)},i.deflateInit2=Be,i.deflateReset=tt,i.deflateResetKeep=we,i.deflateSetHeader=function(h,U){return h&&h.state?h.state.wrap!==2?p:(h.state.gzhead=U,f):p},i.deflate=function(h,U){var z,k,w,D;if(!h||!h.state||5>8&255),fe(k,k.gzhead.time>>16&255),fe(k,k.gzhead.time>>24&255),fe(k,k.level===9?2:2<=k.strategy||k.level<2?4:0),fe(k,255&k.gzhead.os),k.gzhead.extra&&k.gzhead.extra.length&&(fe(k,255&k.gzhead.extra.length),fe(k,k.gzhead.extra.length>>8&255)),k.gzhead.hcrc&&(h.adler=s(h.adler,k.pending_buf,k.pending,0)),k.gzindex=0,k.status=69):(fe(k,0),fe(k,0),fe(k,0),fe(k,0),fe(k,0),fe(k,k.level===9?2:2<=k.strategy||k.level<2?4:0),fe(k,3),k.status=I);else{var K=b+(k.w_bits-8<<4)<<8;K|=(2<=k.strategy||k.level<2?0:k.level<6?1:k.level===6?2:3)<<6,k.strstart!==0&&(K|=32),K+=31-K%31,k.status=I,ce(k,K),k.strstart!==0&&(ce(k,h.adler>>>16),ce(k,65535&h.adler)),h.adler=1}if(k.status===69)if(k.gzhead.extra){for(w=k.pending;k.gzindex<(65535&k.gzhead.extra.length)&&(k.pending!==k.pending_buf_size||(k.gzhead.hcrc&&k.pending>w&&(h.adler=s(h.adler,k.pending_buf,k.pending-w,w)),j(h),w=k.pending,k.pending!==k.pending_buf_size));)fe(k,255&k.gzhead.extra[k.gzindex]),k.gzindex++;k.gzhead.hcrc&&k.pending>w&&(h.adler=s(h.adler,k.pending_buf,k.pending-w,w)),k.gzindex===k.gzhead.extra.length&&(k.gzindex=0,k.status=73)}else k.status=73;if(k.status===73)if(k.gzhead.name){w=k.pending;do{if(k.pending===k.pending_buf_size&&(k.gzhead.hcrc&&k.pending>w&&(h.adler=s(h.adler,k.pending_buf,k.pending-w,w)),j(h),w=k.pending,k.pending===k.pending_buf_size)){D=1;break}D=k.gzindexw&&(h.adler=s(h.adler,k.pending_buf,k.pending-w,w)),D===0&&(k.gzindex=0,k.status=91)}else k.status=91;if(k.status===91)if(k.gzhead.comment){w=k.pending;do{if(k.pending===k.pending_buf_size&&(k.gzhead.hcrc&&k.pending>w&&(h.adler=s(h.adler,k.pending_buf,k.pending-w,w)),j(h),w=k.pending,k.pending===k.pending_buf_size)){D=1;break}D=k.gzindexw&&(h.adler=s(h.adler,k.pending_buf,k.pending-w,w)),D===0&&(k.status=103)}else k.status=103;if(k.status===103&&(k.gzhead.hcrc?(k.pending+2>k.pending_buf_size&&j(h),k.pending+2<=k.pending_buf_size&&(fe(k,255&h.adler),fe(k,h.adler>>8&255),h.adler=0,k.status=I)):k.status=I),k.pending!==0){if(j(h),h.avail_out===0)return k.last_flush=-1,f}else if(h.avail_in===0&&Z(U)<=Z(z)&&U!==A)return se(h,-5);if(k.status===666&&h.avail_in!==0)return se(h,-5);if(h.avail_in!==0||k.lookahead!==0||U!==d&&k.status!==666){var Y=k.strategy===2?(function(M,J){for(var ae;;){if(M.lookahead===0&&(be(M),M.lookahead===0)){if(J===d)return m;break}if(M.match_length=0,ae=l._tr_tally(M,0,M.window[M.strstart]),M.lookahead--,M.strstart++,ae&&(_(M,!1),M.strm.avail_out===0))return m}return M.insert=0,J===A?(_(M,!0),M.strm.avail_out===0?ne:q):M.last_lit&&(_(M,!1),M.strm.avail_out===0)?m:N})(k,U):k.strategy===3?(function(M,J){for(var ae,ee,pe,Ee,ve=M.window;;){if(M.lookahead<=W){if(be(M),M.lookahead<=W&&J===d)return m;if(M.lookahead===0)break}if(M.match_length=0,M.lookahead>=T&&0M.lookahead&&(M.match_length=M.lookahead)}if(M.match_length>=T?(ae=l._tr_tally(M,1,M.match_length-T),M.lookahead-=M.match_length,M.strstart+=M.match_length,M.match_length=0):(ae=l._tr_tally(M,0,M.window[M.strstart]),M.lookahead--,M.strstart++),ae&&(_(M,!1),M.strm.avail_out===0))return m}return M.insert=0,J===A?(_(M,!0),M.strm.avail_out===0?ne:q):M.last_lit&&(_(M,!1),M.strm.avail_out===0)?m:N})(k,U):a[k.level].func(k,U);if(Y!==ne&&Y!==q||(k.status=666),Y===m||Y===ne)return h.avail_out===0&&(k.last_flush=-1),f;if(Y===N&&(U===1?l._tr_align(k):U!==5&&(l._tr_stored_block(k,0,0,!1),U===3&&(ue(k.head),k.lookahead===0&&(k.strstart=0,k.block_start=0,k.insert=0))),j(h),h.avail_out===0))return k.last_flush=-1,f}return U!==A?f:k.wrap<=0?1:(k.wrap===2?(fe(k,255&h.adler),fe(k,h.adler>>8&255),fe(k,h.adler>>16&255),fe(k,h.adler>>24&255),fe(k,255&h.total_in),fe(k,h.total_in>>8&255),fe(k,h.total_in>>16&255),fe(k,h.total_in>>24&255)):(ce(k,h.adler>>>16),ce(k,65535&h.adler)),j(h),0=z.w_size&&(D===0&&(ue(z.head),z.strstart=0,z.block_start=0,z.insert=0),J=new o.Buf8(z.w_size),o.arraySet(J,U,ae-z.w_size,z.w_size,0),U=J,ae=z.w_size),K=h.avail_in,Y=h.next_in,M=h.input,h.avail_in=ae,h.next_in=0,h.input=U,be(z);z.lookahead>=T;){for(k=z.strstart,w=z.lookahead-(T-1);z.ins_h=(z.ins_h<>>=T=B>>>24,x-=T,(T=B>>>16&255)===0)N[u++]=65535&B;else{if(!(16&T)){if((64&T)==0){B=P[(65535&B)+(b&(1<>>=T,x-=T),x<15&&(b+=m[c++]<>>=T=B>>>24,x-=T,!(16&(T=B>>>16&255))){if((64&T)==0){B=F[(65535&B)+(b&(1<>>=T,x-=T,(T=u-d)>3,b&=(1<<(x-=W<<3))-1,a.next_in=c,a.next_out=u,a.avail_in=c>>24&255)+(C>>>8&65280)+((65280&C)<<8)+((255&C)<<24)}function b(){this.mode=0,this.last=!1,this.wrap=0,this.havedict=!1,this.flags=0,this.dmax=0,this.check=0,this.total=0,this.head=null,this.wbits=0,this.wsize=0,this.whave=0,this.wnext=0,this.window=null,this.hold=0,this.bits=0,this.length=0,this.offset=0,this.extra=0,this.lencode=null,this.distcode=null,this.lenbits=0,this.distbits=0,this.ncode=0,this.nlen=0,this.ndist=0,this.have=0,this.next=null,this.lens=new a.Buf16(320),this.work=new a.Buf16(288),this.lendyn=null,this.distdyn=null,this.sane=0,this.back=0,this.was=0}function x(C){var I;return C&&C.state?(I=C.state,C.total_in=C.total_out=I.total=0,C.msg="",I.wrap&&(C.adler=1&I.wrap),I.mode=p,I.last=0,I.havedict=0,I.dmax=32768,I.head=null,I.hold=0,I.bits=0,I.lencode=I.lendyn=new a.Buf32(g),I.distcode=I.distdyn=new a.Buf32(y),I.sane=1,I.back=-1,A):f}function P(C){var I;return C&&C.state?((I=C.state).wsize=0,I.whave=0,I.wnext=0,x(C)):f}function F(C,I){var m,N;return C&&C.state?(N=C.state,I<0?(m=0,I=-I):(m=1+(I>>4),I<48&&(I&=15)),I&&(I<8||15=q.wsize?(a.arraySet(q.window,I,m-q.wsize,q.wsize,0),q.wnext=0,q.whave=q.wsize):(N<(ne=q.wsize-q.wnext)&&(ne=N),a.arraySet(q.window,I,m-N,ne,q.wnext),(N-=ne)?(a.arraySet(q.window,I,m-N,N,0),q.wnext=N,q.whave=q.wsize):(q.wnext+=ne,q.wnext===q.wsize&&(q.wnext=0),q.whave>>8&255,m.check=l(m.check,D,2,0),_=j=0,m.mode=2;break}if(m.flags=0,m.head&&(m.head.done=!1),!(1&m.wrap)||(((255&j)<<8)+(j>>8))%31){C.msg="incorrect header check",m.mode=30;break}if((15&j)!=8){C.msg="unknown compression method",m.mode=30;break}if(_-=4,h=8+(15&(j>>>=4)),m.wbits===0)m.wbits=h;else if(h>m.wbits){C.msg="invalid window size",m.mode=30;break}m.dmax=1<>8&1),512&m.flags&&(D[0]=255&j,D[1]=j>>>8&255,m.check=l(m.check,D,2,0)),_=j=0,m.mode=3;case 3:for(;_<32;){if(Z===0)break e;Z--,j+=N[q++]<<_,_+=8}m.head&&(m.head.time=j),512&m.flags&&(D[0]=255&j,D[1]=j>>>8&255,D[2]=j>>>16&255,D[3]=j>>>24&255,m.check=l(m.check,D,4,0)),_=j=0,m.mode=4;case 4:for(;_<16;){if(Z===0)break e;Z--,j+=N[q++]<<_,_+=8}m.head&&(m.head.xflags=255&j,m.head.os=j>>8),512&m.flags&&(D[0]=255&j,D[1]=j>>>8&255,m.check=l(m.check,D,2,0)),_=j=0,m.mode=5;case 5:if(1024&m.flags){for(;_<16;){if(Z===0)break e;Z--,j+=N[q++]<<_,_+=8}m.length=j,m.head&&(m.head.extra_len=j),512&m.flags&&(D[0]=255&j,D[1]=j>>>8&255,m.check=l(m.check,D,2,0)),_=j=0}else m.head&&(m.head.extra=null);m.mode=6;case 6:if(1024&m.flags&&(Z<(ie=m.length)&&(ie=Z),ie&&(m.head&&(h=m.head.extra_len-m.length,m.head.extra||(m.head.extra=new Array(m.head.extra_len)),a.arraySet(m.head.extra,N,q,ie,h)),512&m.flags&&(m.check=l(m.check,N,ie,q)),Z-=ie,q+=ie,m.length-=ie),m.length))break e;m.length=0,m.mode=7;case 7:if(2048&m.flags){if(Z===0)break e;for(ie=0;h=N[q+ie++],m.head&&h&&m.length<65536&&(m.head.name+=String.fromCharCode(h)),h&&ie>9&1,m.head.done=!0),C.adler=m.check=0,m.mode=12;break;case 10:for(;_<32;){if(Z===0)break e;Z--,j+=N[q++]<<_,_+=8}C.adler=m.check=v(j),_=j=0,m.mode=11;case 11:if(m.havedict===0)return C.next_out=se,C.avail_out=ue,C.next_in=q,C.avail_in=Z,m.hold=j,m.bits=_,2;C.adler=m.check=1,m.mode=12;case 12:if(I===5||I===6)break e;case 13:if(m.last){j>>>=7&_,_-=7&_,m.mode=27;break}for(;_<3;){if(Z===0)break e;Z--,j+=N[q++]<<_,_+=8}switch(m.last=1&j,_-=1,3&(j>>>=1)){case 0:m.mode=14;break;case 1:if(W(m),m.mode=20,I!==6)break;j>>>=2,_-=2;break e;case 2:m.mode=17;break;case 3:C.msg="invalid block type",m.mode=30}j>>>=2,_-=2;break;case 14:for(j>>>=7&_,_-=7&_;_<32;){if(Z===0)break e;Z--,j+=N[q++]<<_,_+=8}if((65535&j)!=(j>>>16^65535)){C.msg="invalid stored block lengths",m.mode=30;break}if(m.length=65535&j,_=j=0,m.mode=15,I===6)break e;case 15:m.mode=16;case 16:if(ie=m.length){if(Z>>=5,_-=5,m.ndist=1+(31&j),j>>>=5,_-=5,m.ncode=4+(15&j),j>>>=4,_-=4,286>>=3,_-=3}for(;m.have<19;)m.lens[K[m.have++]]=0;if(m.lencode=m.lendyn,m.lenbits=7,z={bits:m.lenbits},U=s(0,m.lens,0,19,m.lencode,0,m.work,z),m.lenbits=z.bits,U){C.msg="invalid code lengths set",m.mode=30;break}m.have=0,m.mode=19;case 19:for(;m.have>>16&255,Oe=65535&w,!((ke=w>>>24)<=_);){if(Z===0)break e;Z--,j+=N[q++]<<_,_+=8}if(Oe<16)j>>>=ke,_-=ke,m.lens[m.have++]=Oe;else{if(Oe===16){for(k=ke+2;_>>=ke,_-=ke,m.have===0){C.msg="invalid bit length repeat",m.mode=30;break}h=m.lens[m.have-1],ie=3+(3&j),j>>>=2,_-=2}else if(Oe===17){for(k=ke+3;_>>=ke)),j>>>=3,_-=3}else{for(k=ke+7;_>>=ke)),j>>>=7,_-=7}if(m.have+ie>m.nlen+m.ndist){C.msg="invalid bit length repeat",m.mode=30;break}for(;ie--;)m.lens[m.have++]=h}}if(m.mode===30)break;if(m.lens[256]===0){C.msg="invalid code -- missing end-of-block",m.mode=30;break}if(m.lenbits=9,z={bits:m.lenbits},U=s(u,m.lens,0,m.nlen,m.lencode,0,m.work,z),m.lenbits=z.bits,U){C.msg="invalid literal/lengths set",m.mode=30;break}if(m.distbits=6,m.distcode=m.distdyn,z={bits:m.distbits},U=s(d,m.lens,m.nlen,m.ndist,m.distcode,0,m.work,z),m.distbits=z.bits,U){C.msg="invalid distances set",m.mode=30;break}if(m.mode=20,I===6)break e;case 20:m.mode=21;case 21:if(6<=Z&&258<=ue){C.next_out=se,C.avail_out=ue,C.next_in=q,C.avail_in=Z,m.hold=j,m.bits=_,c(C,ce),se=C.next_out,ne=C.output,ue=C.avail_out,q=C.next_in,N=C.input,Z=C.avail_in,j=m.hold,_=m.bits,m.mode===12&&(m.back=-1);break}for(m.back=0;xe=(w=m.lencode[j&(1<>>16&255,Oe=65535&w,!((ke=w>>>24)<=_);){if(Z===0)break e;Z--,j+=N[q++]<<_,_+=8}if(xe&&(240&xe)==0){for(we=ke,tt=xe,Be=Oe;xe=(w=m.lencode[Be+((j&(1<>we)])>>>16&255,Oe=65535&w,!(we+(ke=w>>>24)<=_);){if(Z===0)break e;Z--,j+=N[q++]<<_,_+=8}j>>>=we,_-=we,m.back+=we}if(j>>>=ke,_-=ke,m.back+=ke,m.length=Oe,xe===0){m.mode=26;break}if(32&xe){m.back=-1,m.mode=12;break}if(64&xe){C.msg="invalid literal/length code",m.mode=30;break}m.extra=15&xe,m.mode=22;case 22:if(m.extra){for(k=m.extra;_>>=m.extra,_-=m.extra,m.back+=m.extra}m.was=m.length,m.mode=23;case 23:for(;xe=(w=m.distcode[j&(1<>>16&255,Oe=65535&w,!((ke=w>>>24)<=_);){if(Z===0)break e;Z--,j+=N[q++]<<_,_+=8}if((240&xe)==0){for(we=ke,tt=xe,Be=Oe;xe=(w=m.distcode[Be+((j&(1<>we)])>>>16&255,Oe=65535&w,!(we+(ke=w>>>24)<=_);){if(Z===0)break e;Z--,j+=N[q++]<<_,_+=8}j>>>=we,_-=we,m.back+=we}if(j>>>=ke,_-=ke,m.back+=ke,64&xe){C.msg="invalid distance code",m.mode=30;break}m.offset=Oe,m.extra=15&xe,m.mode=24;case 24:if(m.extra){for(k=m.extra;_>>=m.extra,_-=m.extra,m.back+=m.extra}if(m.offset>m.dmax){C.msg="invalid distance too far back",m.mode=30;break}m.mode=25;case 25:if(ue===0)break e;if(ie=ce-ue,m.offset>ie){if((ie=m.offset-ie)>m.whave&&m.sane){C.msg="invalid distance too far back",m.mode=30;break}be=ie>m.wnext?(ie-=m.wnext,m.wsize-ie):m.wnext-ie,ie>m.length&&(ie=m.length),Re=m.window}else Re=ne,be=se-m.offset,ie=m.length;for(ueE?(T=be[Re+y[I]],_[fe+y[I]]):(T=96,0),b=1<>se)+(x-=b)]=B<<24|T<<16|W|0,x!==0;);for(b=1<>=1;if(b!==0?(j&=b-1,j+=b):j=0,I++,--ce[C]==0){if(C===N)break;C=d[A+y[I]]}if(ne>>7)]}function fe(w,D){w.pending_buf[w.pending++]=255&D,w.pending_buf[w.pending++]=D>>>8&255}function ce(w,D,K){w.bi_valid>v-K?(w.bi_buf|=D<>v-w.bi_valid,w.bi_valid+=K-v):(w.bi_buf|=D<>>=1,K<<=1,0<--D;);return K>>>1}function Re(w,D,K){var Y,M,J=new Array(y+1),ae=0;for(Y=1;Y<=y;Y++)J[Y]=ae=ae+K[Y-1]<<1;for(M=0;M<=D;M++){var ee=w[2*M+1];ee!==0&&(w[2*M]=be(J[ee]++,ee))}}function ke(w){var D;for(D=0;D>1;1<=K;K--)we(w,J,K);for(M=pe;K=w.heap[1],w.heap[1]=w.heap[w.heap_len--],we(w,J,1),Y=w.heap[1],w.heap[--w.heap_max]=K,w.heap[--w.heap_max]=Y,J[2*M]=J[2*K]+J[2*Y],w.depth[M]=(w.depth[K]>=w.depth[Y]?w.depth[K]:w.depth[Y])+1,J[2*K+1]=J[2*Y+1]=M,w.heap[1]=M++,we(w,J,1),2<=w.heap_len;);w.heap[--w.heap_max]=w.heap[1],(function(ve,Ie){var rt,Me,G,oe,me,De,te=Ie.dyn_tree,ge=Ie.max_code,L=Ie.stat_desc.static_tree,ye=Ie.stat_desc.has_stree,Ge=Ie.stat_desc.extra_bits,S=Ie.stat_desc.extra_base,de=Ie.stat_desc.max_length,Ae=0;for(oe=0;oe<=y;oe++)ve.bl_count[oe]=0;for(te[2*ve.heap[ve.heap_max]+1]=0,rt=ve.heap_max+1;rt>=7;M>>=1)if(1&Ee&&ee.dyn_ltree[2*pe]!==0)return o;if(ee.dyn_ltree[18]!==0||ee.dyn_ltree[20]!==0||ee.dyn_ltree[26]!==0)return l;for(pe=32;pe>>3,(J=w.static_len+3+7>>>3)<=M&&(M=J)):M=J=K+5,K+4<=M&&D!==-1?k(w,D,K,Y):w.strategy===4||J===M?(ce(w,2+(Y?1:0),3),tt(w,H,C)):(ce(w,4+(Y?1:0),3),(function(ee,pe,Ee,ve){var Ie;for(ce(ee,pe-257,5),ce(ee,Ee-1,5),ce(ee,ve-4,4),Ie=0;Ie>>8&255,w.pending_buf[w.d_buf+2*w.last_lit+1]=255&D,w.pending_buf[w.l_buf+w.last_lit]=255&K,w.last_lit++,D===0?w.dyn_ltree[2*K]++:(w.matches++,D--,w.dyn_ltree[2*(m[K]+d+1)]++,w.dyn_dtree[2*_(D)]++),w.last_lit===w.lit_bufsize-1},i._tr_align=function(w){ce(w,2,3),ie(w,x,H),(function(D){D.bi_valid===16?(fe(D,D.bi_buf),D.bi_buf=0,D.bi_valid=0):8<=D.bi_valid&&(D.pending_buf[D.pending++]=255&D.bi_buf,D.bi_buf>>=8,D.bi_valid-=8)})(w)}},{"../utils/common":41}],53:[function(t,n,i){"use strict";n.exports=function(){this.input=null,this.next_in=0,this.avail_in=0,this.total_in=0,this.output=null,this.next_out=0,this.avail_out=0,this.total_out=0,this.msg="",this.state=null,this.data_type=2,this.adler=0}},{}],54:[function(t,n,i){(function(a){(function(o,l){"use strict";if(!o.setImmediate){var c,s,u,d,A=1,f={},p=!1,g=o.document,y=Object.getPrototypeOf&&Object.getPrototypeOf(o);y=y&&y.setTimeout?y:o,c={}.toString.call(o.process)==="[object process]"?function(P){process.nextTick(function(){b(P)})}:(function(){if(o.postMessage&&!o.importScripts){var P=!0,F=o.onmessage;return o.onmessage=function(){P=!1},o.postMessage("","*"),o.onmessage=F,P}})()?(d="setImmediate$"+Math.random()+"$",o.addEventListener?o.addEventListener("message",x,!1):o.attachEvent("onmessage",x),function(P){o.postMessage(d+P,"*")}):o.MessageChannel?((u=new MessageChannel).port1.onmessage=function(P){b(P.data)},function(P){u.port2.postMessage(P)}):g&&"onreadystatechange"in g.createElement("script")?(s=g.documentElement,function(P){var F=g.createElement("script");F.onreadystatechange=function(){b(P),F.onreadystatechange=null,s.removeChild(F),F=null},s.appendChild(F)}):function(P){setTimeout(b,0,P)},y.setImmediate=function(P){typeof P!="function"&&(P=new Function(""+P));for(var F=new Array(arguments.length-1),R=0;R"u"?a===void 0?this:a:self)}).call(this,typeof global<"u"?global:typeof self<"u"?self:typeof window<"u"?window:{})},{}]},{},[10])(10)})}}),Mu=Nt({"(disabled):node:fs"(){}}),up=Nt({"(disabled):node:https"(){}}),Ls=function(e,r){return Ls=Object.setPrototypeOf||{__proto__:[]}instanceof Array&&function(t,n){t.__proto__=n}||function(t,n){for(var i in n)n.hasOwnProperty(i)&&(t[i]=n[i])},Ls(e,r)};function Te(e,r){Ls(e,r);function t(){this.constructor=e}e.prototype=r===null?Object.create(r):(t.prototype=r.prototype,new t)}var Je=function(){return Je=Object.assign||function(r){for(var t,n=1,i=arguments.length;n0&&a[a.length-1])&&(s[0]===6||s[0]===2)){t=0;continue}if(s[0]===3&&(!a||s[1]>a[0]&&s[1]>2],r+=ii[(e[n]&3)<<4|e[n+1]>>4],r+=ii[(e[n+1]&15)<<2|e[n+2]>>6],r+=ii[e[n+2]&63];return t%3===2?r=r.substring(0,r.length-1)+"=":t%3===1&&(r=r.substring(0,r.length-2)+"=="),r},qc=function(e){var r=e.length*.75,t=e.length,n,i=0,a,o,l,c;e[e.length-1]==="="&&(r--,e[e.length-2]==="="&&r--);var s=new Uint8Array(r);for(n=0;n>4,s[i++]=(o&15)<<4|l>>2,s[i++]=(l&3)<<6|c&63;return s},hp=/^(data)?:?([\w\/\+]+)?;?(charset=[\w-]+|base64)?.*,/i,pp=function(e){var r=e.trim(),t=r.substring(0,100),n=t.match(hp);if(!n)return qc(r);var i=n[0],a=r.substring(i.length);return qc(a)},Ye=function(e){return e.charCodeAt(0)},Ap=function(e){return e.codePointAt(0)},na=function(e,r){return Br(e.toString(16),r,"0").toUpperCase()},wo=function(e){return na(e,2)},Xr=function(e){return String.fromCharCode(e)},gp=function(e){return Xr(parseInt(e,16))},Br=function(e,r,t){for(var n="",i=0,a=r-e.length;i=55296&&t<=56319&&e.length>i&&(n=e.charCodeAt(i),n>=56320&&n<=57343&&(a=2)),[e.slice(r,r+a),a]},yp=function(e){for(var r=[],t=0,n=e.length;tt&&s(),o+=A,l+=f}}return s(),c},xp=/^D:(\d\d\d\d)(\d\d)?(\d\d)?(\d\d)?(\d\d)?(\d\d)?([+\-Z])?(\d\d)?'?(\d\d)?'?$/,Gu=function(e){var r=e.match(xp);if(r){var t=r[1],n=r[2],i=n===void 0?"01":n,a=r[3],o=a===void 0?"01":a,l=r[4],c=l===void 0?"00":l,s=r[5],u=s===void 0?"00":s,d=r[6],A=d===void 0?"00":d,f=r[7],p=f===void 0?"Z":f,g=r[8],y=g===void 0?"00":g,v=r[9],b=v===void 0?"00":v,x=p==="Z"?"Z":""+p+y+":"+b,P=new Date(t+"-"+i+"-"+o+"T"+c+":"+u+":"+A+x);return P}},rl=function(e,r){for(var t,n=0,i;n=0&&e<=65535},Lp=function(e){return e>=65536&&e<=1114111},qu=function(e){return Math.floor((e-65536)/1024)+55296},Vu=function(e){return(e-65536)%1024+56320},mn;(function(e){e.BigEndian="BigEndian",e.LittleEndian="LittleEndian"})(mn||(mn={}));var Bi="\uFFFD".codePointAt(0),Xu=function(e,r){if(r===void 0&&(r=!0),e.length<=1)return String.fromCodePoint(Bi);for(var t=r?Ip(e):mn.BigEndian,n=r?2:0,i=[];e.length-n>=2;){var a=Xc(e[n++],e[n++],t);if(Np(a))if(e.length-n<2)i.push(Bi);else{var o=Xc(e[n++],e[n++],t);Vc(o)?i.push(a,o):i.push(Bi)}else Vc(a)?(n+=2,i.push(Bi)):i.push(a)}return n=55296&&e<=56319},Vc=function(e){return e>=56320&&e<=57343},Xc=function(e,r,t){if(t===mn.LittleEndian)return r<<8|e;if(t===mn.BigEndian)return e<<8|r;throw new Error("Invalid byteOrder: "+t)},Ip=function(e){return Hu(e)?mn.BigEndian:Ku(e)?mn.LittleEndian:mn.BigEndian},Hu=function(e){return e[0]===254&&e[1]===255},Ku=function(e){return e[0]===255&&e[1]===254},Qu=function(e){return Hu(e)||Ku(e)},zp=function(e){var r=String(e);if(Math.abs(e)<1){var t=parseInt(e.toString().split("e-")[1]);if(t){var n=e<0;n&&(e*=-1),e*=Math.pow(10,t-1),r="0."+new Array(t).join("0")+e.toString().substring(2),n&&(r="-"+r)}}else{var t=parseInt(e.toString().split("+")[1]);t>20&&(t-=20,e/=Math.pow(10,t),r=e.toString()+new Array(t+1).join("0"))}return r},Ka=function(e){return Math.ceil(e.toString(2).length/8)},Jn=function(e){for(var r=new Uint8Array(Ka(e)),t=1;t<=r.length;t++)r[t-1]=e>>(r.length-t)*8;return r},aa=function(e){throw new Error(e)},Mp=Qr(bo()),Hc="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/",Mi=new Uint8Array(256);for(Rr=0;Rr>4,s[i++]=(o&15)<<4|l>>2,s[i++]=(l&3)<<6|c&63;return s},_p=function(e){for(var r="",t=0;tn)throw new Error(zt(r)+" must be at least "+t+" and at most "+n+", but was actually "+e)},Dr=function(e,r,t,n){le(e,r,["number","undefined"]),typeof e=="number"&&cr(e,r,t,n)},Ju=function(e,r,t){if(le(e,r,["number"]),e%t!==0)throw new Error(zt(r)+" must be a multiple of "+t+", but was actually "+e)},fA=function(e,r){if(!Number.isInteger(e))throw new Error(zt(r)+" must be an integer, but was actually "+e)},So=function(e,r){if(![1,0].includes(Math.sign(e)))throw new Error(zt(r)+" must be a positive number or 0, but was actually "+e)},$e=new Uint16Array(256);for(Ot=0;Ot<256;Ot++)$e[Ot]=Ot;var Ot;$e[22]=Ye("");$e[24]=Ye("\u02D8");$e[25]=Ye("\u02C7");$e[26]=Ye("\u02C6");$e[27]=Ye("\u02D9");$e[28]=Ye("\u02DD");$e[29]=Ye("\u02DB");$e[30]=Ye("\u02DA");$e[31]=Ye("\u02DC");$e[127]=Ye("\uFFFD");$e[128]=Ye("\u2022");$e[129]=Ye("\u2020");$e[130]=Ye("\u2021");$e[131]=Ye("\u2026");$e[132]=Ye("\u2014");$e[133]=Ye("\u2013");$e[134]=Ye("\u0192");$e[135]=Ye("\u2044");$e[136]=Ye("\u2039");$e[137]=Ye("\u203A");$e[138]=Ye("\u2212");$e[139]=Ye("\u2030");$e[140]=Ye("\u201E");$e[141]=Ye("\u201C");$e[142]=Ye("\u201D");$e[143]=Ye("\u2018");$e[144]=Ye("\u2019");$e[145]=Ye("\u201A");$e[146]=Ye("\u2122");$e[147]=Ye("\uFB01");$e[148]=Ye("\uFB02");$e[149]=Ye("\u0141");$e[150]=Ye("\u0152");$e[151]=Ye("\u0160");$e[152]=Ye("\u0178");$e[153]=Ye("\u017D");$e[154]=Ye("\u0131");$e[155]=Ye("\u0142");$e[156]=Ye("\u0153");$e[157]=Ye("\u0161");$e[158]=Ye("\u017E");$e[159]=Ye("\uFFFD");$e[160]=Ye("\u20AC");$e[173]=Ye("\uFFFD");var $u=function(e){for(var r=new Array(e.length),t=0,n=e.length;t=Q.ExclamationPoint&&e<=Q.Tilde&&!ol[e]},eu={},tu=new Map,OA=(function(e){Te(r,e);function r(t,n){var i=this;if(t!==eu)throw new nl("PDFName");i=e.call(this)||this;for(var a="/",o=0,l=n.length;o=Q.Zero&&s<=Q.Nine||s>=Q.a&&s<=Q.f||s>=Q.A&&s<=Q.F?(n+=c,(n.length===2||!(u>="0"&&u<="9"||u>="a"&&u<="f"||u>="A"&&u<="F"))&&(a(parseInt(n,16)),n="")):a(s):s===Q.Hash?i=!0:a(s)}return new Uint8Array(t)},r.prototype.decodeText=function(){var t=this.asBytes();return String.fromCharCode.apply(String,Array.from(t))},r.prototype.asString=function(){return this.encodedName},r.prototype.value=function(){return this.encodedName},r.prototype.clone=function(){return this},r.prototype.toString=function(){return this.encodedName},r.prototype.sizeInBytes=function(){return this.encodedName.length},r.prototype.copyBytesInto=function(t,n){return n+=Vt(this.encodedName,t,n),this.encodedName.length},r.of=function(t){var n=zA(t),i=tu.get(n);return i||(i=new r(eu,n),tu.set(n,i)),i},r.Length=r.of("Length"),r.FlateDecode=r.of("FlateDecode"),r.Resources=r.of("Resources"),r.Font=r.of("Font"),r.XObject=r.of("XObject"),r.ExtGState=r.of("ExtGState"),r.Contents=r.of("Contents"),r.Type=r.of("Type"),r.Parent=r.of("Parent"),r.MediaBox=r.of("MediaBox"),r.Page=r.of("Page"),r.Annots=r.of("Annots"),r.TrimBox=r.of("TrimBox"),r.ArtBox=r.of("ArtBox"),r.BleedBox=r.of("BleedBox"),r.CropBox=r.of("CropBox"),r.Rotate=r.of("Rotate"),r.Title=r.of("Title"),r.Author=r.of("Author"),r.Subject=r.of("Subject"),r.Creator=r.of("Creator"),r.Keywords=r.of("Keywords"),r.Producer=r.of("Producer"),r.CreationDate=r.of("CreationDate"),r.ModDate=r.of("ModDate"),r})($t),X=OA,_A=(function(e){Te(r,e);function r(){return e!==null&&e.apply(this,arguments)||this}return r.prototype.asNull=function(){return null},r.prototype.clone=function(){return this},r.prototype.toString=function(){return"null"},r.prototype.sizeInBytes=function(){return 4},r.prototype.copyBytesInto=function(t,n){return t[n++]=Q.n,t[n++]=Q.u,t[n++]=Q.l,t[n++]=Q.l,4},r})($t),rr=new _A,UA=(function(e){Te(r,e);function r(t,n){var i=e.call(this)||this;return i.dict=t,i.context=n,i}return r.prototype.keys=function(){return Array.from(this.dict.keys())},r.prototype.values=function(){return Array.from(this.dict.values())},r.prototype.entries=function(){return Array.from(this.dict.entries())},r.prototype.set=function(t,n){this.dict.set(t,n)},r.prototype.get=function(t,n){n===void 0&&(n=!1);var i=this.dict.get(t);if(!(i===rr&&!n))return i},r.prototype.has=function(t){var n=this.dict.get(t);return n!==void 0&&n!==rr},r.prototype.lookupMaybe=function(t){for(var n,i=[],a=1;a`}function ws(e){return e?.html?Nr(e.html):$h(e)}var ep=Object.create,Eu=Object.defineProperty,tp=Object.getOwnPropertyDescriptor,Bu=Object.getOwnPropertyNames,rp=Object.getPrototypeOf,np=Object.prototype.hasOwnProperty,An=(e=>typeof Dn<"u"?Dn:typeof Proxy<"u"?new Proxy(e,{get:(r,t)=>(typeof Dn<"u"?Dn:r)[t]}):e)(function(e){if(typeof Dn<"u")return Dn.apply(this,arguments);throw Error('Dynamic require of "'+e+'" is not supported')}),Lt=(e,r)=>function(){return r||(0,e[Bu(e)[0]])((r={exports:{}}).exports,r),r.exports},ip=(e,r,t,n)=>{if(r&&typeof r=="object"||typeof r=="function")for(let i of Bu(r))!np.call(e,i)&&i!==t&&Eu(e,i,{get:()=>r[i],enumerable:!(n=tp(r,i))||n.enumerable});return e},Qr=(e,r,t)=>(t=e!=null?ep(rp(e)):{},ip(r||!e||!e.__esModule?Eu(t,"default",{value:e,enumerable:!0}):t,e)),Sn=Lt({"../../../node_modules/pako/lib/utils/common.js"(e){"use strict";var r=typeof Uint8Array<"u"&&typeof Uint16Array<"u"&&typeof Int32Array<"u";function t(a,o){return Object.prototype.hasOwnProperty.call(a,o)}e.assign=function(a){for(var o=Array.prototype.slice.call(arguments,1);o.length;){var l=o.shift();if(l){if(typeof l!="object")throw new TypeError(l+"must be non-object");for(var c in l)t(l,c)&&(a[c]=l[c])}}return a},e.shrinkBuf=function(a,o){return a.length===o?a:a.subarray?a.subarray(0,o):(a.length=o,a)};var n={arraySet:function(a,o,l,c,s){if(o.subarray&&a.subarray){a.set(o.subarray(l,l+c),s);return}for(var u=0;u=0;)G[oe]=0}var l=0,c=1,s=2,u=3,d=258,A=29,f=256,p=f+1+A,g=30,y=19,v=2*p+1,b=15,x=16,P=7,F=256,R=16,E=17,B=18,T=[0,0,0,0,0,0,0,0,1,1,1,1,2,2,2,2,3,3,3,3,4,4,4,4,5,5,5,5,0],W=[0,0,0,0,1,1,2,2,3,3,4,4,5,5,6,6,7,7,8,8,9,9,10,10,11,11,12,12,13,13],H=[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,2,3,7],C=[16,17,18,0,8,7,9,6,10,5,11,4,12,3,13,2,14,1,15],I=512,m=new Array((p+2)*2);o(m);var N=new Array(g*2);o(N);var ne=new Array(I);o(ne);var q=new Array(d-u+1);o(q);var se=new Array(A);o(se);var Z=new Array(g);o(Z);function ue(G,oe,me,De,te){this.static_tree=G,this.extra_bits=oe,this.extra_base=me,this.elems=De,this.max_length=te,this.has_stree=G&&G.length}var j,_,fe;function ce(G,oe){this.dyn_tree=G,this.max_code=0,this.stat_desc=oe}function ie(G){return G<256?ne[G]:ne[256+(G>>>7)]}function be(G,oe){G.pending_buf[G.pending++]=oe&255,G.pending_buf[G.pending++]=oe>>>8&255}function Re(G,oe,me){G.bi_valid>x-me?(G.bi_buf|=oe<>x-G.bi_valid,G.bi_valid+=me-x):(G.bi_buf|=oe<>>=1,me<<=1;while(--oe>0);return me>>>1}function Oe(G){G.bi_valid===16?(be(G,G.bi_buf),G.bi_buf=0,G.bi_valid=0):G.bi_valid>=8&&(G.pending_buf[G.pending++]=G.bi_buf&255,G.bi_buf>>=8,G.bi_valid-=8)}function we(G,oe){var me=oe.dyn_tree,De=oe.max_code,te=oe.stat_desc.static_tree,ge=oe.stat_desc.has_stree,L=oe.stat_desc.extra_bits,ye=oe.stat_desc.extra_base,Ge=oe.stat_desc.max_length,S,de,Ae,O,re,he,_e=0;for(O=0;O<=b;O++)G.bl_count[O]=0;for(me[G.heap[G.heap_max]*2+1]=0,S=G.heap_max+1;SGe&&(O=Ge,_e++),me[de*2+1]=O,!(de>De)&&(G.bl_count[O]++,re=0,de>=ye&&(re=L[de-ye]),he=me[de*2],G.opt_len+=he*(O+re),ge&&(G.static_len+=he*(te[de*2+1]+re)));if(_e!==0){do{for(O=Ge-1;G.bl_count[O]===0;)O--;G.bl_count[O]--,G.bl_count[O+1]+=2,G.bl_count[Ge]--,_e-=2}while(_e>0);for(O=Ge;O!==0;O--)for(de=G.bl_count[O];de!==0;)Ae=G.heap[--S],!(Ae>De)&&(me[Ae*2+1]!==O&&(G.opt_len+=(O-me[Ae*2+1])*me[Ae*2],me[Ae*2+1]=O),de--)}}function tt(G,oe,me){var De=new Array(b+1),te=0,ge,L;for(ge=1;ge<=b;ge++)De[ge]=te=te+me[ge-1]<<1;for(L=0;L<=oe;L++){var ye=G[L*2+1];ye!==0&&(G[L*2]=xe(De[ye]++,ye))}}function Be(){var G,oe,me,De,te,ge=new Array(b+1);for(me=0,De=0;De>=7;De8?be(G,G.bi_buf):G.bi_valid>0&&(G.pending_buf[G.pending++]=G.bi_buf),G.bi_buf=0,G.bi_valid=0}function z(G,oe,me,De){U(G),De&&(be(G,me),be(G,~me)),r.arraySet(G.pending_buf,G.window,oe,me,G.pending),G.pending+=me}function k(G,oe,me,De){var te=oe*2,ge=me*2;return G[te]>1;L>=1;L--)w(G,me,L);S=ge;do L=G.heap[1],G.heap[1]=G.heap[G.heap_len--],w(G,me,1),ye=G.heap[1],G.heap[--G.heap_max]=L,G.heap[--G.heap_max]=ye,me[S*2]=me[L*2]+me[ye*2],G.depth[S]=(G.depth[L]>=G.depth[ye]?G.depth[L]:G.depth[ye])+1,me[L*2+1]=me[ye*2+1]=S,G.heap[1]=S++,w(G,me,1);while(G.heap_len>=2);G.heap[--G.heap_max]=G.heap[1],we(G,oe),tt(me,Ge,G.bl_count)}function Y(G,oe,me){var De,te=-1,ge,L=oe[1],ye=0,Ge=7,S=4;for(L===0&&(Ge=138,S=3),oe[(me+1)*2+1]=65535,De=0;De<=me;De++)ge=L,L=oe[(De+1)*2+1],!(++ye=3&&G.bl_tree[C[oe]*2+1]===0;oe--);return G.opt_len+=3*(oe+1)+5+5+4,oe}function ae(G,oe,me,De){var te;for(Re(G,oe-257,5),Re(G,me-1,5),Re(G,De-4,4),te=0;te>>=1)if(oe&1&&G.dyn_ltree[me*2]!==0)return n;if(G.dyn_ltree[18]!==0||G.dyn_ltree[20]!==0||G.dyn_ltree[26]!==0)return i;for(me=32;me0?(G.strm.data_type===a&&(G.strm.data_type=ee(G)),K(G,G.l_desc),K(G,G.d_desc),L=J(G),te=G.opt_len+3+7>>>3,ge=G.static_len+3+7>>>3,ge<=te&&(te=ge)):te=ge=me+5,me+4<=te&&oe!==-1?ve(G,oe,me,De):G.strategy===t||ge===te?(Re(G,(c<<1)+(De?1:0),3),D(G,m,N)):(Re(G,(s<<1)+(De?1:0),3),ae(G,G.l_desc.max_code+1,G.d_desc.max_code+1,L+1),D(G,G.dyn_ltree,G.dyn_dtree)),h(G),De&&U(G)}function Me(G,oe,me){return G.pending_buf[G.d_buf+G.last_lit*2]=oe>>>8&255,G.pending_buf[G.d_buf+G.last_lit*2+1]=oe&255,G.pending_buf[G.l_buf+G.last_lit]=me&255,G.last_lit++,oe===0?G.dyn_ltree[me*2]++:(G.matches++,oe--,G.dyn_ltree[(q[me]+f+1)*2]++,G.dyn_dtree[ie(oe)*2]++),G.last_lit===G.lit_bufsize-1}e._tr_init=Ee,e._tr_stored_block=ve,e._tr_flush_block=rt,e._tr_tally=Me,e._tr_align=Ie}}),Ru=Lt({"../../../node_modules/pako/lib/zlib/adler32.js"(e,r){"use strict";function t(n,i,a,o){for(var l=n&65535|0,c=n>>>16&65535|0,s=0;a!==0;){s=a>2e3?2e3:a,a-=s;do l=l+i[o++]|0,c=c+l|0;while(--s);l%=65521,c%=65521}return l|c<<16|0}r.exports=t}}),Lu=Lt({"../../../node_modules/pako/lib/zlib/crc32.js"(e,r){"use strict";function t(){for(var a,o=[],l=0;l<256;l++){a=l;for(var c=0;c<8;c++)a=a&1?3988292384^a>>>1:a>>>1;o[l]=a}return o}var n=t();function i(a,o,l,c){var s=n,u=c+l;a^=-1;for(var d=c;d>>8^s[(a^o[d])&255];return a^-1}r.exports=i}}),rl=Lt({"../../../node_modules/pako/lib/zlib/messages.js"(e,r){"use strict";r.exports={2:"need dictionary",1:"stream end",0:"","-1":"file error","-2":"stream error","-3":"data error","-4":"insufficient memory","-5":"buffer error","-6":"incompatible version"}}}),op=Lt({"../../../node_modules/pako/lib/zlib/deflate.js"(e){"use strict";var r=Sn(),t=ap(),n=Ru(),i=Lu(),a=rl(),o=0,l=1,c=3,s=4,u=5,d=0,A=1,f=-2,p=-3,g=-5,y=-1,v=1,b=2,x=3,P=4,F=0,R=2,E=8,B=9,T=15,W=8,H=29,C=256,I=C+1+H,m=30,N=19,ne=2*I+1,q=15,se=3,Z=258,ue=Z+se+1,j=32,_=42,fe=69,ce=73,ie=91,be=103,Re=113,ke=666,xe=1,Oe=2,we=3,tt=4,Be=3;function h(S,de){return S.msg=a[de],de}function U(S){return(S<<1)-(S>4?9:0)}function z(S){for(var de=S.length;--de>=0;)S[de]=0}function k(S){var de=S.state,Ae=de.pending;Ae>S.avail_out&&(Ae=S.avail_out),Ae!==0&&(r.arraySet(S.output,de.pending_buf,de.pending_out,Ae,S.next_out),S.next_out+=Ae,de.pending_out+=Ae,S.total_out+=Ae,S.avail_out-=Ae,de.pending-=Ae,de.pending===0&&(de.pending_out=0))}function w(S,de){t._tr_flush_block(S,S.block_start>=0?S.block_start:-1,S.strstart-S.block_start,de),S.block_start=S.strstart,k(S.strm)}function D(S,de){S.pending_buf[S.pending++]=de}function K(S,de){S.pending_buf[S.pending++]=de>>>8&255,S.pending_buf[S.pending++]=de&255}function Y(S,de,Ae,O){var re=S.avail_in;return re>O&&(re=O),re===0?0:(S.avail_in-=re,r.arraySet(de,S.input,S.next_in,re,Ae),S.state.wrap===1?S.adler=n(S.adler,de,re,Ae):S.state.wrap===2&&(S.adler=i(S.adler,de,re,Ae)),S.next_in+=re,S.total_in+=re,re)}function M(S,de){var Ae=S.max_chain_length,O=S.strstart,re,he,_e=S.prev_length,Ne=S.nice_match,ze=S.strstart>S.w_size-ue?S.strstart-(S.w_size-ue):0,At=S.window,Or=S.w_mask,Dt=S.prev,gt=S.strstart+Z,Nt=At[O+_e-1],Ht=At[O+_e];S.prev_length>=S.good_match&&(Ae>>=2),Ne>S.lookahead&&(Ne=S.lookahead);do if(re=de,!(At[re+_e]!==Ht||At[re+_e-1]!==Nt||At[re]!==At[O]||At[++re]!==At[O+1])){O+=2,re++;do;while(At[++O]===At[++re]&&At[++O]===At[++re]&&At[++O]===At[++re]&&At[++O]===At[++re]&&At[++O]===At[++re]&&At[++O]===At[++re]&&At[++O]===At[++re]&&At[++O]===At[++re]&&O_e){if(S.match_start=de,_e=he,he>=Ne)break;Nt=At[O+_e-1],Ht=At[O+_e]}}while((de=Dt[de&Or])>ze&&--Ae!==0);return _e<=S.lookahead?_e:S.lookahead}function J(S){var de=S.w_size,Ae,O,re,he,_e;do{if(he=S.window_size-S.lookahead-S.strstart,S.strstart>=de+(de-ue)){r.arraySet(S.window,S.window,de,de,0),S.match_start-=de,S.strstart-=de,S.block_start-=de,O=S.hash_size,Ae=O;do re=S.head[--Ae],S.head[Ae]=re>=de?re-de:0;while(--O);O=de,Ae=O;do re=S.prev[--Ae],S.prev[Ae]=re>=de?re-de:0;while(--O);he+=de}if(S.strm.avail_in===0)break;if(O=Y(S.strm,S.window,S.strstart+S.lookahead,he),S.lookahead+=O,S.lookahead+S.insert>=se)for(_e=S.strstart-S.insert,S.ins_h=S.window[_e],S.ins_h=(S.ins_h<S.pending_buf_size-5&&(Ae=S.pending_buf_size-5);;){if(S.lookahead<=1){if(J(S),S.lookahead===0&&de===o)return xe;if(S.lookahead===0)break}S.strstart+=S.lookahead,S.lookahead=0;var O=S.block_start+Ae;if((S.strstart===0||S.strstart>=O)&&(S.lookahead=S.strstart-O,S.strstart=O,w(S,!1),S.strm.avail_out===0)||S.strstart-S.block_start>=S.w_size-ue&&(w(S,!1),S.strm.avail_out===0))return xe}return S.insert=0,de===s?(w(S,!0),S.strm.avail_out===0?we:tt):(S.strstart>S.block_start&&(w(S,!1),S.strm.avail_out===0),xe)}function ee(S,de){for(var Ae,O;;){if(S.lookahead=se&&(S.ins_h=(S.ins_h<=se)if(O=t._tr_tally(S,S.strstart-S.match_start,S.match_length-se),S.lookahead-=S.match_length,S.match_length<=S.max_lazy_match&&S.lookahead>=se){S.match_length--;do S.strstart++,S.ins_h=(S.ins_h<=se&&(S.ins_h=(S.ins_h<4096)&&(S.match_length=se-1)),S.prev_length>=se&&S.match_length<=S.prev_length){re=S.strstart+S.lookahead-se,O=t._tr_tally(S,S.strstart-1-S.prev_match,S.prev_length-se),S.lookahead-=S.prev_length-1,S.prev_length-=2;do++S.strstart<=re&&(S.ins_h=(S.ins_h<=se&&S.strstart>0&&(re=S.strstart-1,O=_e[re],O===_e[++re]&&O===_e[++re]&&O===_e[++re])){he=S.strstart+Z;do;while(O===_e[++re]&&O===_e[++re]&&O===_e[++re]&&O===_e[++re]&&O===_e[++re]&&O===_e[++re]&&O===_e[++re]&&O===_e[++re]&&reS.lookahead&&(S.match_length=S.lookahead)}if(S.match_length>=se?(Ae=t._tr_tally(S,1,S.match_length-se),S.lookahead-=S.match_length,S.strstart+=S.match_length,S.match_length=0):(Ae=t._tr_tally(S,0,S.window[S.strstart]),S.lookahead--,S.strstart++),Ae&&(w(S,!1),S.strm.avail_out===0))return xe}return S.insert=0,de===s?(w(S,!0),S.strm.avail_out===0?we:tt):S.last_lit&&(w(S,!1),S.strm.avail_out===0)?xe:Oe}function ve(S,de){for(var Ae;;){if(S.lookahead===0&&(J(S),S.lookahead===0)){if(de===o)return xe;break}if(S.match_length=0,Ae=t._tr_tally(S,0,S.window[S.strstart]),S.lookahead--,S.strstart++,Ae&&(w(S,!1),S.strm.avail_out===0))return xe}return S.insert=0,de===s?(w(S,!0),S.strm.avail_out===0?we:tt):S.last_lit&&(w(S,!1),S.strm.avail_out===0)?xe:Oe}function Ie(S,de,Ae,O,re){this.good_length=S,this.max_lazy=de,this.nice_length=Ae,this.max_chain=O,this.func=re}var rt;rt=[new Ie(0,0,0,0,ae),new Ie(4,4,8,4,ee),new Ie(4,5,16,8,ee),new Ie(4,6,32,32,ee),new Ie(4,4,16,16,pe),new Ie(8,16,32,32,pe),new Ie(8,16,128,128,pe),new Ie(8,32,128,256,pe),new Ie(32,128,258,1024,pe),new Ie(32,258,258,4096,pe)];function Me(S){S.window_size=2*S.w_size,z(S.head),S.max_lazy_match=rt[S.level].max_lazy,S.good_match=rt[S.level].good_length,S.nice_match=rt[S.level].nice_length,S.max_chain_length=rt[S.level].max_chain,S.strstart=0,S.block_start=0,S.lookahead=0,S.insert=0,S.match_length=S.prev_length=se-1,S.match_available=0,S.ins_h=0}function G(){this.strm=null,this.status=0,this.pending_buf=null,this.pending_buf_size=0,this.pending_out=0,this.pending=0,this.wrap=0,this.gzhead=null,this.gzindex=0,this.method=E,this.last_flush=-1,this.w_size=0,this.w_bits=0,this.w_mask=0,this.window=null,this.window_size=0,this.prev=null,this.head=null,this.ins_h=0,this.hash_size=0,this.hash_bits=0,this.hash_mask=0,this.hash_shift=0,this.block_start=0,this.match_length=0,this.prev_match=0,this.match_available=0,this.strstart=0,this.match_start=0,this.lookahead=0,this.prev_length=0,this.max_chain_length=0,this.max_lazy_match=0,this.level=0,this.strategy=0,this.good_match=0,this.nice_match=0,this.dyn_ltree=new r.Buf16(ne*2),this.dyn_dtree=new r.Buf16((2*m+1)*2),this.bl_tree=new r.Buf16((2*N+1)*2),z(this.dyn_ltree),z(this.dyn_dtree),z(this.bl_tree),this.l_desc=null,this.d_desc=null,this.bl_desc=null,this.bl_count=new r.Buf16(q+1),this.heap=new r.Buf16(2*I+1),z(this.heap),this.heap_len=0,this.heap_max=0,this.depth=new r.Buf16(2*I+1),z(this.depth),this.l_buf=0,this.lit_bufsize=0,this.last_lit=0,this.d_buf=0,this.opt_len=0,this.static_len=0,this.matches=0,this.insert=0,this.bi_buf=0,this.bi_valid=0}function oe(S){var de;return!S||!S.state?h(S,f):(S.total_in=S.total_out=0,S.data_type=R,de=S.state,de.pending=0,de.pending_out=0,de.wrap<0&&(de.wrap=-de.wrap),de.status=de.wrap?_:Re,S.adler=de.wrap===2?0:1,de.last_flush=o,t._tr_init(de),d)}function me(S){var de=oe(S);return de===d&&Me(S.state),de}function De(S,de){return!S||!S.state||S.state.wrap!==2?f:(S.state.gzhead=de,d)}function te(S,de,Ae,O,re,he){if(!S)return f;var _e=1;if(de===y&&(de=6),O<0?(_e=0,O=-O):O>15&&(_e=2,O-=16),re<1||re>B||Ae!==E||O<8||O>15||de<0||de>9||he<0||he>P)return h(S,f);O===8&&(O=9);var Ne=new G;return S.state=Ne,Ne.strm=S,Ne.wrap=_e,Ne.gzhead=null,Ne.w_bits=O,Ne.w_size=1<u||de<0)return S?h(S,f):f;if(O=S.state,!S.output||!S.input&&S.avail_in!==0||O.status===ke&&de!==s)return h(S,S.avail_out===0?g:f);if(O.strm=S,Ae=O.last_flush,O.last_flush=de,O.status===_)if(O.wrap===2)S.adler=0,D(O,31),D(O,139),D(O,8),O.gzhead?(D(O,(O.gzhead.text?1:0)+(O.gzhead.hcrc?2:0)+(O.gzhead.extra?4:0)+(O.gzhead.name?8:0)+(O.gzhead.comment?16:0)),D(O,O.gzhead.time&255),D(O,O.gzhead.time>>8&255),D(O,O.gzhead.time>>16&255),D(O,O.gzhead.time>>24&255),D(O,O.level===9?2:O.strategy>=b||O.level<2?4:0),D(O,O.gzhead.os&255),O.gzhead.extra&&O.gzhead.extra.length&&(D(O,O.gzhead.extra.length&255),D(O,O.gzhead.extra.length>>8&255)),O.gzhead.hcrc&&(S.adler=i(S.adler,O.pending_buf,O.pending,0)),O.gzindex=0,O.status=fe):(D(O,0),D(O,0),D(O,0),D(O,0),D(O,0),D(O,O.level===9?2:O.strategy>=b||O.level<2?4:0),D(O,Be),O.status=Re);else{var _e=E+(O.w_bits-8<<4)<<8,Ne=-1;O.strategy>=b||O.level<2?Ne=0:O.level<6?Ne=1:O.level===6?Ne=2:Ne=3,_e|=Ne<<6,O.strstart!==0&&(_e|=j),_e+=31-_e%31,O.status=Re,K(O,_e),O.strstart!==0&&(K(O,S.adler>>>16),K(O,S.adler&65535)),S.adler=1}if(O.status===fe)if(O.gzhead.extra){for(re=O.pending;O.gzindex<(O.gzhead.extra.length&65535)&&!(O.pending===O.pending_buf_size&&(O.gzhead.hcrc&&O.pending>re&&(S.adler=i(S.adler,O.pending_buf,O.pending-re,re)),k(S),re=O.pending,O.pending===O.pending_buf_size));)D(O,O.gzhead.extra[O.gzindex]&255),O.gzindex++;O.gzhead.hcrc&&O.pending>re&&(S.adler=i(S.adler,O.pending_buf,O.pending-re,re)),O.gzindex===O.gzhead.extra.length&&(O.gzindex=0,O.status=ce)}else O.status=ce;if(O.status===ce)if(O.gzhead.name){re=O.pending;do{if(O.pending===O.pending_buf_size&&(O.gzhead.hcrc&&O.pending>re&&(S.adler=i(S.adler,O.pending_buf,O.pending-re,re)),k(S),re=O.pending,O.pending===O.pending_buf_size)){he=1;break}O.gzindexre&&(S.adler=i(S.adler,O.pending_buf,O.pending-re,re)),he===0&&(O.gzindex=0,O.status=ie)}else O.status=ie;if(O.status===ie)if(O.gzhead.comment){re=O.pending;do{if(O.pending===O.pending_buf_size&&(O.gzhead.hcrc&&O.pending>re&&(S.adler=i(S.adler,O.pending_buf,O.pending-re,re)),k(S),re=O.pending,O.pending===O.pending_buf_size)){he=1;break}O.gzindexre&&(S.adler=i(S.adler,O.pending_buf,O.pending-re,re)),he===0&&(O.status=be)}else O.status=be;if(O.status===be&&(O.gzhead.hcrc?(O.pending+2>O.pending_buf_size&&k(S),O.pending+2<=O.pending_buf_size&&(D(O,S.adler&255),D(O,S.adler>>8&255),S.adler=0,O.status=Re)):O.status=Re),O.pending!==0){if(k(S),S.avail_out===0)return O.last_flush=-1,d}else if(S.avail_in===0&&U(de)<=U(Ae)&&de!==s)return h(S,g);if(O.status===ke&&S.avail_in!==0)return h(S,g);if(S.avail_in!==0||O.lookahead!==0||de!==o&&O.status!==ke){var ze=O.strategy===b?ve(O,de):O.strategy===x?Ee(O,de):rt[O.level].func(O,de);if((ze===we||ze===tt)&&(O.status=ke),ze===xe||ze===we)return S.avail_out===0&&(O.last_flush=-1),d;if(ze===Oe&&(de===l?t._tr_align(O):de!==u&&(t._tr_stored_block(O,0,0,!1),de===c&&(z(O.head),O.lookahead===0&&(O.strstart=0,O.block_start=0,O.insert=0))),k(S),S.avail_out===0))return O.last_flush=-1,d}return de!==s?d:O.wrap<=0?A:(O.wrap===2?(D(O,S.adler&255),D(O,S.adler>>8&255),D(O,S.adler>>16&255),D(O,S.adler>>24&255),D(O,S.total_in&255),D(O,S.total_in>>8&255),D(O,S.total_in>>16&255),D(O,S.total_in>>24&255)):(K(O,S.adler>>>16),K(O,S.adler&65535)),k(S),O.wrap>0&&(O.wrap=-O.wrap),O.pending!==0?d:A)}function ye(S){var de;return!S||!S.state?f:(de=S.state.status,de!==_&&de!==fe&&de!==ce&&de!==ie&&de!==be&&de!==Re&&de!==ke?h(S,f):(S.state=null,de===Re?h(S,p):d))}function Ge(S,de){var Ae=de.length,O,re,he,_e,Ne,ze,At,Or;if(!S||!S.state||(O=S.state,_e=O.wrap,_e===2||_e===1&&O.status!==_||O.lookahead))return f;for(_e===1&&(S.adler=n(S.adler,de,Ae,0)),O.wrap=0,Ae>=O.w_size&&(_e===0&&(z(O.head),O.strstart=0,O.block_start=0,O.insert=0),Or=new r.Buf8(O.w_size),r.arraySet(Or,de,Ae-O.w_size,O.w_size,0),de=Or,Ae=O.w_size),Ne=S.avail_in,ze=S.next_in,At=S.input,S.avail_in=Ae,S.next_in=0,S.input=de,J(O);O.lookahead>=se;){re=O.strstart,he=O.lookahead-(se-1);do O.ins_h=(O.ins_h<=252?6:a>=248?5:a>=240?4:a>=224?3:a>=192?2:1;var a;i[254]=i[254]=1,e.string2buf=function(l){var c,s,u,d,A,f=l.length,p=0;for(d=0;d>>6,c[A++]=128|s&63):s<65536?(c[A++]=224|s>>>12,c[A++]=128|s>>>6&63,c[A++]=128|s&63):(c[A++]=240|s>>>18,c[A++]=128|s>>>12&63,c[A++]=128|s>>>6&63,c[A++]=128|s&63);return c};function o(l,c){if(c<65534&&(l.subarray&&n||!l.subarray&&t))return String.fromCharCode.apply(null,r.shrinkBuf(l,c));for(var s="",u=0;u4){p[u++]=65533,s+=A-1;continue}for(d&=A===2?31:A===3?15:7;A>1&&s1){p[u++]=65533;continue}d<65536?p[u++]=d:(d-=65536,p[u++]=55296|d>>10&1023,p[u++]=56320|d&1023)}return o(p,u)},e.utf8border=function(l,c){var s;for(c=c||l.length,c>l.length&&(c=l.length),s=c-1;s>=0&&(l[s]&192)===128;)s--;return s<0||s===0?c:s+i[l[s]]>c?s:c}}}),Iu=Lt({"../../../node_modules/pako/lib/zlib/zstream.js"(e,r){"use strict";function t(){this.input=null,this.next_in=0,this.avail_in=0,this.total_in=0,this.output=null,this.next_out=0,this.avail_out=0,this.total_out=0,this.msg="",this.state=null,this.data_type=2,this.adler=0}r.exports=t}}),sp=Lt({"../../../node_modules/pako/lib/deflate.js"(e){"use strict";var r=op(),t=Sn(),n=Nu(),i=rl(),a=Iu(),o=Object.prototype.toString,l=0,c=4,s=0,u=1,d=2,A=-1,f=0,p=8;function g(x){if(!(this instanceof g))return new g(x);this.options=t.assign({level:A,method:p,chunkSize:16384,windowBits:15,memLevel:8,strategy:f,to:""},x||{});var P=this.options;P.raw&&P.windowBits>0?P.windowBits=-P.windowBits:P.gzip&&P.windowBits>0&&P.windowBits<16&&(P.windowBits+=16),this.err=0,this.msg="",this.ended=!1,this.chunks=[],this.strm=new a,this.strm.avail_out=0;var F=r.deflateInit2(this.strm,P.level,P.method,P.windowBits,P.memLevel,P.strategy);if(F!==s)throw new Error(i[F]);if(P.header&&r.deflateSetHeader(this.strm,P.header),P.dictionary){var R;if(typeof P.dictionary=="string"?R=n.string2buf(P.dictionary):o.call(P.dictionary)==="[object ArrayBuffer]"?R=new Uint8Array(P.dictionary):R=P.dictionary,F=r.deflateSetDictionary(this.strm,R),F!==s)throw new Error(i[F]);this._dict_set=!0}}g.prototype.push=function(x,P){var F=this.strm,R=this.options.chunkSize,E,B;if(this.ended)return!1;B=P===~~P?P:P===!0?c:l,typeof x=="string"?F.input=n.string2buf(x):o.call(x)==="[object ArrayBuffer]"?F.input=new Uint8Array(x):F.input=x,F.next_in=0,F.avail_in=F.input.length;do{if(F.avail_out===0&&(F.output=new t.Buf8(R),F.next_out=0,F.avail_out=R),E=r.deflate(F,B),E!==u&&E!==s)return this.onEnd(E),this.ended=!0,!1;(F.avail_out===0||F.avail_in===0&&(B===c||B===d))&&(this.options.to==="string"?this.onData(n.buf2binstring(t.shrinkBuf(F.output,F.next_out))):this.onData(t.shrinkBuf(F.output,F.next_out)))}while((F.avail_in>0||F.avail_out===0)&&E!==u);return B===c?(E=r.deflateEnd(this.strm),this.onEnd(E),this.ended=!0,E===s):(B===d&&(this.onEnd(s),F.avail_out=0),!0)},g.prototype.onData=function(x){this.chunks.push(x)},g.prototype.onEnd=function(x){x===s&&(this.options.to==="string"?this.result=this.chunks.join(""):this.result=t.flattenChunks(this.chunks)),this.chunks=[],this.err=x,this.msg=this.strm.msg};function y(x,P){var F=new g(P);if(F.push(x,!0),F.err)throw F.msg||i[F.err];return F.result}function v(x,P){return P=P||{},P.raw=!0,y(x,P)}function b(x,P){return P=P||{},P.gzip=!0,y(x,P)}e.Deflate=g,e.deflate=y,e.deflateRaw=v,e.gzip=b}}),lp=Lt({"../../../node_modules/pako/lib/zlib/inffast.js"(e,r){"use strict";var t=30,n=12;r.exports=function(a,o){var l,c,s,u,d,A,f,p,g,y,v,b,x,P,F,R,E,B,T,W,H,C,I,m,N;l=a.state,c=a.next_in,m=a.input,s=c+(a.avail_in-5),u=a.next_out,N=a.output,d=u-(o-a.avail_out),A=u+(a.avail_out-257),f=l.dmax,p=l.wsize,g=l.whave,y=l.wnext,v=l.window,b=l.hold,x=l.bits,P=l.lencode,F=l.distcode,R=(1<>>24,b>>>=T,x-=T,T=B>>>16&255,T===0)N[u++]=B&65535;else if(T&16){W=B&65535,T&=15,T&&(x>>=T,x-=T),x<15&&(b+=m[c++]<>>24,b>>>=T,x-=T,T=B>>>16&255,T&16){if(H=B&65535,T&=15,xf){a.msg="invalid distance too far back",l.mode=t;break e}if(b>>>=T,x-=T,T=u-d,H>T){if(T=H-T,T>g&&l.sane){a.msg="invalid distance too far back",l.mode=t;break e}if(C=0,I=v,y===0){if(C+=p-T,T2;)N[u++]=I[C++],N[u++]=I[C++],N[u++]=I[C++],W-=3;W&&(N[u++]=I[C++],W>1&&(N[u++]=I[C++]))}else{C=u-H;do N[u++]=N[C++],N[u++]=N[C++],N[u++]=N[C++],W-=3;while(W>2);W&&(N[u++]=N[C++],W>1&&(N[u++]=N[C++]))}}else if((T&64)===0){B=F[(B&65535)+(b&(1<>3,c-=W,x-=W<<3,b&=(1<=1&&ie[W]===0;W--);if(H>W&&(H=W),W===0)return b[x++]=1<<24|64<<16|0,b[x++]=1<<24|64<<16|0,F.bits=1,0;for(T=1;T0&&(p===o||W!==1))return-1;for(be[1]=0,E=1;Ei||p===c&&N>a)return 1;for(;;){xe=E-I,P[B]ce?(Oe=Re[ke+P[B]],we=_[fe+P[B]]):(Oe=96,we=0),q=1<>I)+se]=xe<<24|Oe<<16|we|0;while(se!==0);for(q=1<>=1;if(q!==0?(ne&=q-1,ne+=q):ne=0,B++,--ie[E]===0){if(E===W)break;E=g[y+P[B]]}if(E>H&&(ne&ue)!==Z){for(I===0&&(I=H),j+=T,C=E-I,m=1<i||p===c&&N>a)return 1;Z=ne&ue,b[Z]=H<<24|C<<16|j-x|0}}return ne!==0&&(b[j+ne]=E-I<<24|64<<16|0),F.bits=H,0}}}),up=Lt({"../../../node_modules/pako/lib/zlib/inflate.js"(e){"use strict";var r=Sn(),t=Ru(),n=Lu(),i=lp(),a=cp(),o=0,l=1,c=2,s=4,u=5,d=6,A=0,f=1,p=2,g=-2,y=-3,v=-4,b=-5,x=8,P=1,F=2,R=3,E=4,B=5,T=6,W=7,H=8,C=9,I=10,m=11,N=12,ne=13,q=14,se=15,Z=16,ue=17,j=18,_=19,fe=20,ce=21,ie=22,be=23,Re=24,ke=25,xe=26,Oe=27,we=28,tt=29,Be=30,h=31,U=32,z=852,k=592,w=15,D=w;function K(te){return(te>>>24&255)+(te>>>8&65280)+((te&65280)<<8)+((te&255)<<24)}function Y(){this.mode=0,this.last=!1,this.wrap=0,this.havedict=!1,this.flags=0,this.dmax=0,this.check=0,this.total=0,this.head=null,this.wbits=0,this.wsize=0,this.whave=0,this.wnext=0,this.window=null,this.hold=0,this.bits=0,this.length=0,this.offset=0,this.extra=0,this.lencode=null,this.distcode=null,this.lenbits=0,this.distbits=0,this.ncode=0,this.nlen=0,this.ndist=0,this.have=0,this.next=null,this.lens=new r.Buf16(320),this.work=new r.Buf16(288),this.lendyn=null,this.distdyn=null,this.sane=0,this.back=0,this.was=0}function M(te){var ge;return!te||!te.state?g:(ge=te.state,te.total_in=te.total_out=ge.total=0,te.msg="",ge.wrap&&(te.adler=ge.wrap&1),ge.mode=P,ge.last=0,ge.havedict=0,ge.dmax=32768,ge.head=null,ge.hold=0,ge.bits=0,ge.lencode=ge.lendyn=new r.Buf32(z),ge.distcode=ge.distdyn=new r.Buf32(k),ge.sane=1,ge.back=-1,A)}function J(te){var ge;return!te||!te.state?g:(ge=te.state,ge.wsize=0,ge.whave=0,ge.wnext=0,M(te))}function ae(te,ge){var L,ye;return!te||!te.state||(ye=te.state,ge<0?(L=0,ge=-ge):(L=(ge>>4)+1,ge<48&&(ge&=15)),ge&&(ge<8||ge>15))?g:(ye.window!==null&&ye.wbits!==ge&&(ye.window=null),ye.wrap=L,ye.wbits=ge,J(te))}function ee(te,ge){var L,ye;return te?(ye=new Y,te.state=ye,ye.window=null,L=ae(te,ge),L!==A&&(te.state=null),L):g}function pe(te){return ee(te,D)}var Ee=!0,ve,Ie;function rt(te){if(Ee){var ge;for(ve=new r.Buf32(512),Ie=new r.Buf32(32),ge=0;ge<144;)te.lens[ge++]=8;for(;ge<256;)te.lens[ge++]=9;for(;ge<280;)te.lens[ge++]=7;for(;ge<288;)te.lens[ge++]=8;for(a(l,te.lens,0,288,ve,0,te.work,{bits:9}),ge=0;ge<32;)te.lens[ge++]=5;a(c,te.lens,0,32,Ie,0,te.work,{bits:5}),Ee=!1}te.lencode=ve,te.lenbits=9,te.distcode=Ie,te.distbits=5}function Me(te,ge,L,ye){var Ge,S=te.state;return S.window===null&&(S.wsize=1<=S.wsize?(r.arraySet(S.window,ge,L-S.wsize,S.wsize,0),S.wnext=0,S.whave=S.wsize):(Ge=S.wsize-S.wnext,Ge>ye&&(Ge=ye),r.arraySet(S.window,ge,L-ye,Ge,S.wnext),ye-=Ge,ye?(r.arraySet(S.window,ge,L-ye,ye,0),S.wnext=ye,S.whave=S.wsize):(S.wnext+=Ge,S.wnext===S.wsize&&(S.wnext=0),S.whave>>8&255,L.check=n(L.check,jt,2,0),re=0,he=0,L.mode=F;break}if(L.flags=0,L.head&&(L.head.done=!1),!(L.wrap&1)||(((re&255)<<8)+(re>>8))%31){te.msg="incorrect header check",L.mode=Be;break}if((re&15)!==x){te.msg="unknown compression method",L.mode=Be;break}if(re>>>=4,he-=4,Et=(re&15)+8,L.wbits===0)L.wbits=Et;else if(Et>L.wbits){te.msg="invalid window size",L.mode=Be;break}L.dmax=1<>8&1),L.flags&512&&(jt[0]=re&255,jt[1]=re>>>8&255,L.check=n(L.check,jt,2,0)),re=0,he=0,L.mode=R;case R:for(;he<32;){if(Ae===0)break e;Ae--,re+=ye[S++]<>>8&255,jt[2]=re>>>16&255,jt[3]=re>>>24&255,L.check=n(L.check,jt,4,0)),re=0,he=0,L.mode=E;case E:for(;he<16;){if(Ae===0)break e;Ae--,re+=ye[S++]<>8),L.flags&512&&(jt[0]=re&255,jt[1]=re>>>8&255,L.check=n(L.check,jt,2,0)),re=0,he=0,L.mode=B;case B:if(L.flags&1024){for(;he<16;){if(Ae===0)break e;Ae--,re+=ye[S++]<>>8&255,L.check=n(L.check,jt,2,0)),re=0,he=0}else L.head&&(L.head.extra=null);L.mode=T;case T:if(L.flags&1024&&(ze=L.length,ze>Ae&&(ze=Ae),ze&&(L.head&&(Et=L.head.extra_len-L.length,L.head.extra||(L.head.extra=new Array(L.head.extra_len)),r.arraySet(L.head.extra,ye,S,ze,Et)),L.flags&512&&(L.check=n(L.check,ye,ze,S)),Ae-=ze,S+=ze,L.length-=ze),L.length))break e;L.length=0,L.mode=W;case W:if(L.flags&2048){if(Ae===0)break e;ze=0;do Et=ye[S+ze++],L.head&&Et&&L.length<65536&&(L.head.name+=String.fromCharCode(Et));while(Et&&ze>9&1,L.head.done=!0),te.adler=L.check=0,L.mode=N;break;case I:for(;he<32;){if(Ae===0)break e;Ae--,re+=ye[S++]<>>=he&7,he-=he&7,L.mode=Oe;break}for(;he<3;){if(Ae===0)break e;Ae--,re+=ye[S++]<>>=1,he-=1,re&3){case 0:L.mode=q;break;case 1:if(rt(L),L.mode=fe,ge===d){re>>>=2,he-=2;break e}break;case 2:L.mode=ue;break;case 3:te.msg="invalid block type",L.mode=Be}re>>>=2,he-=2;break;case q:for(re>>>=he&7,he-=he&7;he<32;){if(Ae===0)break e;Ae--,re+=ye[S++]<>>16^65535)){te.msg="invalid stored block lengths",L.mode=Be;break}if(L.length=re&65535,re=0,he=0,L.mode=se,ge===d)break e;case se:L.mode=Z;case Z:if(ze=L.length,ze){if(ze>Ae&&(ze=Ae),ze>O&&(ze=O),ze===0)break e;r.arraySet(Ge,ye,S,ze,de),Ae-=ze,S+=ze,O-=ze,de+=ze,L.length-=ze;break}L.mode=N;break;case ue:for(;he<14;){if(Ae===0)break e;Ae--,re+=ye[S++]<>>=5,he-=5,L.ndist=(re&31)+1,re>>>=5,he-=5,L.ncode=(re&15)+4,re>>>=4,he-=4,L.nlen>286||L.ndist>30){te.msg="too many length or distance symbols",L.mode=Be;break}L.have=0,L.mode=j;case j:for(;L.have>>=3,he-=3}for(;L.have<19;)L.lens[tc[L.have++]]=0;if(L.lencode=L.lendyn,L.lenbits=7,_r={bits:L.lenbits},hr=a(o,L.lens,0,19,L.lencode,0,L.work,_r),L.lenbits=_r.bits,hr){te.msg="invalid code lengths set",L.mode=Be;break}L.have=0,L.mode=_;case _:for(;L.have>>24,Nt=Dt>>>16&255,Ht=Dt&65535,!(gt<=he);){if(Ae===0)break e;Ae--,re+=ye[S++]<>>=gt,he-=gt,L.lens[L.have++]=Ht;else{if(Ht===16){for(Rr=gt+2;he>>=gt,he-=gt,L.have===0){te.msg="invalid bit length repeat",L.mode=Be;break}Et=L.lens[L.have-1],ze=3+(re&3),re>>>=2,he-=2}else if(Ht===17){for(Rr=gt+3;he>>=gt,he-=gt,Et=0,ze=3+(re&7),re>>>=3,he-=3}else{for(Rr=gt+7;he>>=gt,he-=gt,Et=0,ze=11+(re&127),re>>>=7,he-=7}if(L.have+ze>L.nlen+L.ndist){te.msg="invalid bit length repeat",L.mode=Be;break}for(;ze--;)L.lens[L.have++]=Et}}if(L.mode===Be)break;if(L.lens[256]===0){te.msg="invalid code -- missing end-of-block",L.mode=Be;break}if(L.lenbits=9,_r={bits:L.lenbits},hr=a(l,L.lens,0,L.nlen,L.lencode,0,L.work,_r),L.lenbits=_r.bits,hr){te.msg="invalid literal/lengths set",L.mode=Be;break}if(L.distbits=6,L.distcode=L.distdyn,_r={bits:L.distbits},hr=a(c,L.lens,L.nlen,L.ndist,L.distcode,0,L.work,_r),L.distbits=_r.bits,hr){te.msg="invalid distances set",L.mode=Be;break}if(L.mode=fe,ge===d)break e;case fe:L.mode=ce;case ce:if(Ae>=6&&O>=258){te.next_out=de,te.avail_out=O,te.next_in=S,te.avail_in=Ae,L.hold=re,L.bits=he,i(te,Ne),de=te.next_out,Ge=te.output,O=te.avail_out,S=te.next_in,ye=te.input,Ae=te.avail_in,re=L.hold,he=L.bits,L.mode===N&&(L.back=-1);break}for(L.back=0;Dt=L.lencode[re&(1<>>24,Nt=Dt>>>16&255,Ht=Dt&65535,!(gt<=he);){if(Ae===0)break e;Ae--,re+=ye[S++]<>tr)],gt=Dt>>>24,Nt=Dt>>>16&255,Ht=Dt&65535,!(tr+gt<=he);){if(Ae===0)break e;Ae--,re+=ye[S++]<>>=tr,he-=tr,L.back+=tr}if(re>>>=gt,he-=gt,L.back+=gt,L.length=Ht,Nt===0){L.mode=xe;break}if(Nt&32){L.back=-1,L.mode=N;break}if(Nt&64){te.msg="invalid literal/length code",L.mode=Be;break}L.extra=Nt&15,L.mode=ie;case ie:if(L.extra){for(Rr=L.extra;he>>=L.extra,he-=L.extra,L.back+=L.extra}L.was=L.length,L.mode=be;case be:for(;Dt=L.distcode[re&(1<>>24,Nt=Dt>>>16&255,Ht=Dt&65535,!(gt<=he);){if(Ae===0)break e;Ae--,re+=ye[S++]<>tr)],gt=Dt>>>24,Nt=Dt>>>16&255,Ht=Dt&65535,!(tr+gt<=he);){if(Ae===0)break e;Ae--,re+=ye[S++]<>>=tr,he-=tr,L.back+=tr}if(re>>>=gt,he-=gt,L.back+=gt,Nt&64){te.msg="invalid distance code",L.mode=Be;break}L.offset=Ht,L.extra=Nt&15,L.mode=Re;case Re:if(L.extra){for(Rr=L.extra;he>>=L.extra,he-=L.extra,L.back+=L.extra}if(L.offset>L.dmax){te.msg="invalid distance too far back",L.mode=Be;break}L.mode=ke;case ke:if(O===0)break e;if(ze=Ne-O,L.offset>ze){if(ze=L.offset-ze,ze>L.whave&&L.sane){te.msg="invalid distance too far back",L.mode=Be;break}ze>L.wnext?(ze-=L.wnext,At=L.wsize-ze):At=L.wnext-ze,ze>L.length&&(ze=L.length),Or=L.window}else Or=Ge,At=de-L.offset,ze=L.length;ze>O&&(ze=O),O-=ze,L.length-=ze;do Ge[de++]=Or[At++];while(--ze);L.length===0&&(L.mode=ce);break;case xe:if(O===0)break e;Ge[de++]=L.length,O--,L.mode=ce;break;case Oe:if(L.wrap){for(;he<32;){if(Ae===0)break e;Ae--,re|=ye[S++]<=0&&f.windowBits<16&&(f.windowBits=-f.windowBits,f.windowBits===0&&(f.windowBits=-15)),f.windowBits>=0&&f.windowBits<16&&!(A&&A.windowBits)&&(f.windowBits+=32),f.windowBits>15&&f.windowBits<48&&(f.windowBits&15)===0&&(f.windowBits|=15),this.err=0,this.msg="",this.ended=!1,this.chunks=[],this.strm=new o,this.strm.avail_out=0;var p=r.inflateInit2(this.strm,f.windowBits);if(p!==i.Z_OK)throw new Error(a[p]);if(this.header=new l,r.inflateGetHeader(this.strm,this.header),f.dictionary&&(typeof f.dictionary=="string"?f.dictionary=n.string2buf(f.dictionary):c.call(f.dictionary)==="[object ArrayBuffer]"&&(f.dictionary=new Uint8Array(f.dictionary)),f.raw&&(p=r.inflateSetDictionary(this.strm,f.dictionary),p!==i.Z_OK)))throw new Error(a[p])}s.prototype.push=function(A,f){var p=this.strm,g=this.options.chunkSize,y=this.options.dictionary,v,b,x,P,F,R=!1;if(this.ended)return!1;b=f===~~f?f:f===!0?i.Z_FINISH:i.Z_NO_FLUSH,typeof A=="string"?p.input=n.binstring2buf(A):c.call(A)==="[object ArrayBuffer]"?p.input=new Uint8Array(A):p.input=A,p.next_in=0,p.avail_in=p.input.length;do{if(p.avail_out===0&&(p.output=new t.Buf8(g),p.next_out=0,p.avail_out=g),v=r.inflate(p,i.Z_NO_FLUSH),v===i.Z_NEED_DICT&&y&&(v=r.inflateSetDictionary(this.strm,y)),v===i.Z_BUF_ERROR&&R===!0&&(v=i.Z_OK,R=!1),v!==i.Z_STREAM_END&&v!==i.Z_OK)return this.onEnd(v),this.ended=!0,!1;p.next_out&&(p.avail_out===0||v===i.Z_STREAM_END||p.avail_in===0&&(b===i.Z_FINISH||b===i.Z_SYNC_FLUSH))&&(this.options.to==="string"?(x=n.utf8border(p.output,p.next_out),P=p.next_out-x,F=n.buf2string(p.output,x),p.next_out=P,p.avail_out=g-P,P&&t.arraySet(p.output,p.output,x,P,0),this.onData(F)):this.onData(t.shrinkBuf(p.output,p.next_out))),p.avail_in===0&&p.avail_out===0&&(R=!0)}while((p.avail_in>0||p.avail_out===0)&&v!==i.Z_STREAM_END);return v===i.Z_STREAM_END&&(b=i.Z_FINISH),b===i.Z_FINISH?(v=r.inflateEnd(this.strm),this.onEnd(v),this.ended=!0,v===i.Z_OK):(b===i.Z_SYNC_FLUSH&&(this.onEnd(i.Z_OK),p.avail_out=0),!0)},s.prototype.onData=function(A){this.chunks.push(A)},s.prototype.onEnd=function(A){A===i.Z_OK&&(this.options.to==="string"?this.result=this.chunks.join(""):this.result=t.flattenChunks(this.chunks)),this.chunks=[],this.err=A,this.msg=this.strm.msg};function u(A,f){var p=new s(f);if(p.push(A,!0),p.err)throw p.msg||a[p.err];return p.result}function d(A,f){return f=f||{},f.raw=!0,u(A,f)}e.Inflate=s,e.inflate=u,e.inflateRaw=d,e.ungzip=u}}),xo=Lt({"../../../node_modules/pako/index.js"(e,r){"use strict";var t=Sn().assign,n=sp(),i=fp(),a=zu(),o={};t(o,n,i,a),r.exports=o}}),hp=Lt({"node_modules/jszip/dist/jszip.min.js"(e,r){(function(t){typeof e=="object"&&typeof r<"u"?r.exports=t():typeof define=="function"&&define.amd?define([],t):(typeof window<"u"?window:typeof global<"u"?global:typeof self<"u"?self:this).JSZip=t()})(function(){return(function t(n,i,a){function o(s,u){if(!i[s]){if(!n[s]){var d=typeof An=="function"&&An;if(!u&&d)return d(s,!0);if(l)return l(s,!0);var A=new Error("Cannot find module '"+s+"'");throw A.code="MODULE_NOT_FOUND",A}var f=i[s]={exports:{}};n[s][0].call(f.exports,function(p){var g=n[s][1][p];return o(g||p)},f,f.exports,t,n,i,a)}return i[s].exports}for(var l=typeof An=="function"&&An,c=0;c>2,f=(3&s)<<4|u>>4,p=1>6:64,g=2>4,u=(15&A)<<4|(f=l.indexOf(c.charAt(g++)))>>2,d=(3&f)<<6|(p=l.indexOf(c.charAt(g++))),b[y++]=s,f!==64&&(b[y++]=u),p!==64&&(b[y++]=d);return b}},{"./support":30,"./utils":32}],2:[function(t,n,i){"use strict";var a=t("./external"),o=t("./stream/DataWorker"),l=t("./stream/Crc32Probe"),c=t("./stream/DataLengthProbe");function s(u,d,A,f,p){this.compressedSize=u,this.uncompressedSize=d,this.crc32=A,this.compression=f,this.compressedContent=p}s.prototype={getContentWorker:function(){var u=new o(a.Promise.resolve(this.compressedContent)).pipe(this.compression.uncompressWorker()).pipe(new c("data_length")),d=this;return u.on("end",function(){if(this.streamInfo.data_length!==d.uncompressedSize)throw new Error("Bug : uncompressed data size mismatch")}),u},getCompressedWorker:function(){return new o(a.Promise.resolve(this.compressedContent)).withStreamInfo("compressedSize",this.compressedSize).withStreamInfo("uncompressedSize",this.uncompressedSize).withStreamInfo("crc32",this.crc32).withStreamInfo("compression",this.compression)}},s.createWorkerFrom=function(u,d,A){return u.pipe(new l).pipe(new c("uncompressedSize")).pipe(d.compressWorker(A)).pipe(new c("compressedSize")).withStreamInfo("compression",d)},n.exports=s},{"./external":6,"./stream/Crc32Probe":25,"./stream/DataLengthProbe":26,"./stream/DataWorker":27}],3:[function(t,n,i){"use strict";var a=t("./stream/GenericWorker");i.STORE={magic:"\0\0",compressWorker:function(){return new a("STORE compression")},uncompressWorker:function(){return new a("STORE decompression")}},i.DEFLATE=t("./flate")},{"./flate":7,"./stream/GenericWorker":28}],4:[function(t,n,i){"use strict";var a=t("./utils"),o=(function(){for(var l,c=[],s=0;s<256;s++){l=s;for(var u=0;u<8;u++)l=1&l?3988292384^l>>>1:l>>>1;c[s]=l}return c})();n.exports=function(l,c){return l!==void 0&&l.length?a.getTypeOf(l)!=="string"?(function(s,u,d,A){var f=o,p=A+d;s^=-1;for(var g=A;g>>8^f[255&(s^u[g])];return-1^s})(0|c,l,l.length,0):(function(s,u,d,A){var f=o,p=A+d;s^=-1;for(var g=A;g>>8^f[255&(s^u.charCodeAt(g))];return-1^s})(0|c,l,l.length,0):0}},{"./utils":32}],5:[function(t,n,i){"use strict";i.base64=!1,i.binary=!1,i.dir=!1,i.createFolders=!0,i.date=null,i.compression=null,i.compressionOptions=null,i.comment=null,i.unixPermissions=null,i.dosPermissions=null},{}],6:[function(t,n,i){"use strict";var a=null;a=typeof Promise<"u"?Promise:t("lie"),n.exports={Promise:a}},{lie:37}],7:[function(t,n,i){"use strict";var a=typeof Uint8Array<"u"&&typeof Uint16Array<"u"&&typeof Uint32Array<"u",o=t("pako"),l=t("./utils"),c=t("./stream/GenericWorker"),s=a?"uint8array":"array";function u(d,A){c.call(this,"FlateWorker/"+d),this._pako=null,this._pakoAction=d,this._pakoOptions=A,this.meta={}}i.magic="\b\0",l.inherits(u,c),u.prototype.processChunk=function(d){this.meta=d.meta,this._pako===null&&this._createPako(),this._pako.push(l.transformTo(s,d.data),!1)},u.prototype.flush=function(){c.prototype.flush.call(this),this._pako===null&&this._createPako(),this._pako.push([],!0)},u.prototype.cleanUp=function(){c.prototype.cleanUp.call(this),this._pako=null},u.prototype._createPako=function(){this._pako=new o[this._pakoAction]({raw:!0,level:this._pakoOptions.level||-1});var d=this;this._pako.onData=function(A){d.push({data:A,meta:d.meta})}},i.compressWorker=function(d){return new u("Deflate",d)},i.uncompressWorker=function(){return new u("Inflate",{})}},{"./stream/GenericWorker":28,"./utils":32,pako:38}],8:[function(t,n,i){"use strict";function a(f,p){var g,y="";for(g=0;g>>=8;return y}function o(f,p,g,y,v,b){var x,P,F=f.file,R=f.compression,E=b!==s.utf8encode,B=l.transformTo("string",b(F.name)),T=l.transformTo("string",s.utf8encode(F.name)),W=F.comment,H=l.transformTo("string",b(W)),C=l.transformTo("string",s.utf8encode(W)),I=T.length!==F.name.length,m=C.length!==W.length,N="",ne="",q="",se=F.dir,Z=F.date,ue={crc32:0,compressedSize:0,uncompressedSize:0};p&&!g||(ue.crc32=f.crc32,ue.compressedSize=f.compressedSize,ue.uncompressedSize=f.uncompressedSize);var j=0;p&&(j|=8),E||!I&&!m||(j|=2048);var _=0,fe=0;se&&(_|=16),v==="UNIX"?(fe=798,_|=(function(ie,be){var Re=ie;return ie||(Re=be?16893:33204),(65535&Re)<<16})(F.unixPermissions,se)):(fe=20,_|=(function(ie){return 63&(ie||0)})(F.dosPermissions)),x=Z.getUTCHours(),x<<=6,x|=Z.getUTCMinutes(),x<<=5,x|=Z.getUTCSeconds()/2,P=Z.getUTCFullYear()-1980,P<<=4,P|=Z.getUTCMonth()+1,P<<=5,P|=Z.getUTCDate(),I&&(ne=a(1,1)+a(u(B),4)+T,N+="up"+a(ne.length,2)+ne),m&&(q=a(1,1)+a(u(H),4)+C,N+="uc"+a(q.length,2)+q);var ce="";return ce+=` +\0`,ce+=a(j,2),ce+=R.magic,ce+=a(x,2),ce+=a(P,2),ce+=a(ue.crc32,4),ce+=a(ue.compressedSize,4),ce+=a(ue.uncompressedSize,4),ce+=a(B.length,2),ce+=a(N.length,2),{fileRecord:d.LOCAL_FILE_HEADER+ce+B+N,dirRecord:d.CENTRAL_FILE_HEADER+a(fe,2)+ce+a(H.length,2)+"\0\0\0\0"+a(_,4)+a(y,4)+B+N+H}}var l=t("../utils"),c=t("../stream/GenericWorker"),s=t("../utf8"),u=t("../crc32"),d=t("../signature");function A(f,p,g,y){c.call(this,"ZipFileWorker"),this.bytesWritten=0,this.zipComment=p,this.zipPlatform=g,this.encodeFileName=y,this.streamFiles=f,this.accumulate=!1,this.contentBuffer=[],this.dirRecords=[],this.currentSourceOffset=0,this.entriesCount=0,this.currentFile=null,this._sources=[]}l.inherits(A,c),A.prototype.push=function(f){var p=f.meta.percent||0,g=this.entriesCount,y=this._sources.length;this.accumulate?this.contentBuffer.push(f):(this.bytesWritten+=f.data.length,c.prototype.push.call(this,{data:f.data,meta:{currentFile:this.currentFile,percent:g?(p+100*(g-y-1))/g:100}}))},A.prototype.openedSource=function(f){this.currentSourceOffset=this.bytesWritten,this.currentFile=f.file.name;var p=this.streamFiles&&!f.file.dir;if(p){var g=o(f,p,!1,this.currentSourceOffset,this.zipPlatform,this.encodeFileName);this.push({data:g.fileRecord,meta:{percent:0}})}else this.accumulate=!0},A.prototype.closedSource=function(f){this.accumulate=!1;var p=this.streamFiles&&!f.file.dir,g=o(f,p,!0,this.currentSourceOffset,this.zipPlatform,this.encodeFileName);if(this.dirRecords.push(g.dirRecord),p)this.push({data:(function(y){return d.DATA_DESCRIPTOR+a(y.crc32,4)+a(y.compressedSize,4)+a(y.uncompressedSize,4)})(f),meta:{percent:100}});else for(this.push({data:g.fileRecord,meta:{percent:0}});this.contentBuffer.length;)this.push(this.contentBuffer.shift());this.currentFile=null},A.prototype.flush=function(){for(var f=this.bytesWritten,p=0;p=this.index;c--)s=(s<<8)+this.byteAt(c);return this.index+=l,s},readString:function(l){return a.transformTo("string",this.readData(l))},readData:function(){},lastIndexOfSignature:function(){},readAndCheckSignature:function(){},readDate:function(){var l=this.readInt(4);return new Date(Date.UTC(1980+(l>>25&127),(l>>21&15)-1,l>>16&31,l>>11&31,l>>5&63,(31&l)<<1))}},n.exports=o},{"../utils":32}],19:[function(t,n,i){"use strict";var a=t("./Uint8ArrayReader");function o(l){a.call(this,l)}t("../utils").inherits(o,a),o.prototype.readData=function(l){this.checkOffset(l);var c=this.data.slice(this.zero+this.index,this.zero+this.index+l);return this.index+=l,c},n.exports=o},{"../utils":32,"./Uint8ArrayReader":21}],20:[function(t,n,i){"use strict";var a=t("./DataReader");function o(l){a.call(this,l)}t("../utils").inherits(o,a),o.prototype.byteAt=function(l){return this.data.charCodeAt(this.zero+l)},o.prototype.lastIndexOfSignature=function(l){return this.data.lastIndexOf(l)-this.zero},o.prototype.readAndCheckSignature=function(l){return l===this.readData(4)},o.prototype.readData=function(l){this.checkOffset(l);var c=this.data.slice(this.zero+this.index,this.zero+this.index+l);return this.index+=l,c},n.exports=o},{"../utils":32,"./DataReader":18}],21:[function(t,n,i){"use strict";var a=t("./ArrayReader");function o(l){a.call(this,l)}t("../utils").inherits(o,a),o.prototype.readData=function(l){if(this.checkOffset(l),l===0)return new Uint8Array(0);var c=this.data.subarray(this.zero+this.index,this.zero+this.index+l);return this.index+=l,c},n.exports=o},{"../utils":32,"./ArrayReader":17}],22:[function(t,n,i){"use strict";var a=t("../utils"),o=t("../support"),l=t("./ArrayReader"),c=t("./StringReader"),s=t("./NodeBufferReader"),u=t("./Uint8ArrayReader");n.exports=function(d){var A=a.getTypeOf(d);return a.checkSupport(A),A!=="string"||o.uint8array?A==="nodebuffer"?new s(d):o.uint8array?new u(a.transformTo("uint8array",d)):new l(a.transformTo("array",d)):new c(d)}},{"../support":30,"../utils":32,"./ArrayReader":17,"./NodeBufferReader":19,"./StringReader":20,"./Uint8ArrayReader":21}],23:[function(t,n,i){"use strict";i.LOCAL_FILE_HEADER="PK",i.CENTRAL_FILE_HEADER="PK",i.CENTRAL_DIRECTORY_END="PK",i.ZIP64_CENTRAL_DIRECTORY_LOCATOR="PK\x07",i.ZIP64_CENTRAL_DIRECTORY_END="PK",i.DATA_DESCRIPTOR="PK\x07\b"},{}],24:[function(t,n,i){"use strict";var a=t("./GenericWorker"),o=t("../utils");function l(c){a.call(this,"ConvertWorker to "+c),this.destType=c}o.inherits(l,a),l.prototype.processChunk=function(c){this.push({data:o.transformTo(this.destType,c.data),meta:c.meta})},n.exports=l},{"../utils":32,"./GenericWorker":28}],25:[function(t,n,i){"use strict";var a=t("./GenericWorker"),o=t("../crc32");function l(){a.call(this,"Crc32Probe"),this.withStreamInfo("crc32",0)}t("../utils").inherits(l,a),l.prototype.processChunk=function(c){this.streamInfo.crc32=o(c.data,this.streamInfo.crc32||0),this.push(c)},n.exports=l},{"../crc32":4,"../utils":32,"./GenericWorker":28}],26:[function(t,n,i){"use strict";var a=t("../utils"),o=t("./GenericWorker");function l(c){o.call(this,"DataLengthProbe for "+c),this.propName=c,this.withStreamInfo(c,0)}a.inherits(l,o),l.prototype.processChunk=function(c){if(c){var s=this.streamInfo[this.propName]||0;this.streamInfo[this.propName]=s+c.data.length}o.prototype.processChunk.call(this,c)},n.exports=l},{"../utils":32,"./GenericWorker":28}],27:[function(t,n,i){"use strict";var a=t("../utils"),o=t("./GenericWorker");function l(c){o.call(this,"DataWorker");var s=this;this.dataIsReady=!1,this.index=0,this.max=0,this.data=null,this.type="",this._tickScheduled=!1,c.then(function(u){s.dataIsReady=!0,s.data=u,s.max=u&&u.length||0,s.type=a.getTypeOf(u),s.isPaused||s._tickAndRepeat()},function(u){s.error(u)})}a.inherits(l,o),l.prototype.cleanUp=function(){o.prototype.cleanUp.call(this),this.data=null},l.prototype.resume=function(){return!!o.prototype.resume.call(this)&&(!this._tickScheduled&&this.dataIsReady&&(this._tickScheduled=!0,a.delay(this._tickAndRepeat,[],this)),!0)},l.prototype._tickAndRepeat=function(){this._tickScheduled=!1,this.isPaused||this.isFinished||(this._tick(),this.isFinished||(a.delay(this._tickAndRepeat,[],this),this._tickScheduled=!0))},l.prototype._tick=function(){if(this.isPaused||this.isFinished)return!1;var c=null,s=Math.min(this.max,this.index+16384);if(this.index>=this.max)return this.end();switch(this.type){case"string":c=this.data.substring(this.index,s);break;case"uint8array":c=this.data.subarray(this.index,s);break;case"array":case"nodebuffer":c=this.data.slice(this.index,s)}return this.index=s,this.push({data:c,meta:{percent:this.max?this.index/this.max*100:0}})},n.exports=l},{"../utils":32,"./GenericWorker":28}],28:[function(t,n,i){"use strict";function a(o){this.name=o||"default",this.streamInfo={},this.generatedError=null,this.extraStreamInfo={},this.isPaused=!0,this.isFinished=!1,this.isLocked=!1,this._listeners={data:[],end:[],error:[]},this.previous=null}a.prototype={push:function(o){this.emit("data",o)},end:function(){if(this.isFinished)return!1;this.flush();try{this.emit("end"),this.cleanUp(),this.isFinished=!0}catch(o){this.emit("error",o)}return!0},error:function(o){return!this.isFinished&&(this.isPaused?this.generatedError=o:(this.isFinished=!0,this.emit("error",o),this.previous&&this.previous.error(o),this.cleanUp()),!0)},on:function(o,l){return this._listeners[o].push(l),this},cleanUp:function(){this.streamInfo=this.generatedError=this.extraStreamInfo=null,this._listeners=[]},emit:function(o,l){if(this._listeners[o])for(var c=0;c "+o:o}},n.exports=a},{}],29:[function(t,n,i){"use strict";var a=t("../utils"),o=t("./ConvertWorker"),l=t("./GenericWorker"),c=t("../base64"),s=t("../support"),u=t("../external"),d=null;if(s.nodestream)try{d=t("../nodejs/NodejsStreamOutputAdapter")}catch{}function A(p,g){return new u.Promise(function(y,v){var b=[],x=p._internalType,P=p._outputType,F=p._mimeType;p.on("data",function(R,E){b.push(R),g&&g(E)}).on("error",function(R){b=[],v(R)}).on("end",function(){try{var R=(function(E,B,T){switch(E){case"blob":return a.newBlob(a.transformTo("arraybuffer",B),T);case"base64":return c.encode(B);default:return a.transformTo(E,B)}})(P,(function(E,B){var T,W=0,H=null,C=0;for(T=0;T"u")i.blob=!1;else{var a=new ArrayBuffer(0);try{i.blob=new Blob([a],{type:"application/zip"}).size===0}catch{try{var o=new(self.BlobBuilder||self.WebKitBlobBuilder||self.MozBlobBuilder||self.MSBlobBuilder);o.append(a),i.blob=o.getBlob("application/zip").size===0}catch{i.blob=!1}}}try{i.nodestream=!!t("readable-stream").Readable}catch{i.nodestream=!1}},{"readable-stream":16}],31:[function(t,n,i){"use strict";for(var a=t("./utils"),o=t("./support"),l=t("./nodejsUtils"),c=t("./stream/GenericWorker"),s=new Array(256),u=0;u<256;u++)s[u]=252<=u?6:248<=u?5:240<=u?4:224<=u?3:192<=u?2:1;s[254]=s[254]=1;function d(){c.call(this,"utf-8 decode"),this.leftOver=null}function A(){c.call(this,"utf-8 encode")}i.utf8encode=function(f){return o.nodebuffer?l.newBufferFrom(f,"utf-8"):(function(p){var g,y,v,b,x,P=p.length,F=0;for(b=0;b>>6:(y<65536?g[x++]=224|y>>>12:(g[x++]=240|y>>>18,g[x++]=128|y>>>12&63),g[x++]=128|y>>>6&63),g[x++]=128|63&y);return g})(f)},i.utf8decode=function(f){return o.nodebuffer?a.transformTo("nodebuffer",f).toString("utf-8"):(function(p){var g,y,v,b,x=p.length,P=new Array(2*x);for(g=y=0;g>10&1023,P[y++]=56320|1023&v)}return P.length!==y&&(P.subarray?P=P.subarray(0,y):P.length=y),a.applyFromCharCode(P)})(f=a.transformTo(o.uint8array?"uint8array":"array",f))},a.inherits(d,c),d.prototype.processChunk=function(f){var p=a.transformTo(o.uint8array?"uint8array":"array",f.data);if(this.leftOver&&this.leftOver.length){if(o.uint8array){var g=p;(p=new Uint8Array(g.length+this.leftOver.length)).set(this.leftOver,0),p.set(g,this.leftOver.length)}else p=this.leftOver.concat(p);this.leftOver=null}var y=(function(b,x){var P;for((x=x||b.length)>b.length&&(x=b.length),P=x-1;0<=P&&(192&b[P])==128;)P--;return P<0||P===0?x:P+s[b[P]]>x?P:x})(p),v=p;y!==p.length&&(o.uint8array?(v=p.subarray(0,y),this.leftOver=p.subarray(y,p.length)):(v=p.slice(0,y),this.leftOver=p.slice(y,p.length))),this.push({data:i.utf8decode(v),meta:f.meta})},d.prototype.flush=function(){this.leftOver&&this.leftOver.length&&(this.push({data:i.utf8decode(this.leftOver),meta:{}}),this.leftOver=null)},i.Utf8DecodeWorker=d,a.inherits(A,c),A.prototype.processChunk=function(f){this.push({data:i.utf8encode(f.data),meta:f.meta})},i.Utf8EncodeWorker=A},{"./nodejsUtils":14,"./stream/GenericWorker":28,"./support":30,"./utils":32}],32:[function(t,n,i){"use strict";var a=t("./support"),o=t("./base64"),l=t("./nodejsUtils"),c=t("./external");function s(g){return g}function u(g,y){for(var v=0;v>8;this.dir=!!(16&this.externalFileAttributes),f==0&&(this.dosPermissions=63&this.externalFileAttributes),f==3&&(this.unixPermissions=this.externalFileAttributes>>16&65535),this.dir||this.fileNameStr.slice(-1)!=="/"||(this.dir=!0)},parseZIP64ExtraField:function(){if(this.extraFields[1]){var f=a(this.extraFields[1].value);this.uncompressedSize===o.MAX_VALUE_32BITS&&(this.uncompressedSize=f.readInt(8)),this.compressedSize===o.MAX_VALUE_32BITS&&(this.compressedSize=f.readInt(8)),this.localHeaderOffset===o.MAX_VALUE_32BITS&&(this.localHeaderOffset=f.readInt(8)),this.diskNumberStart===o.MAX_VALUE_32BITS&&(this.diskNumberStart=f.readInt(4))}},readExtraFields:function(f){var p,g,y,v=f.index+this.extraFieldsLength;for(this.extraFields||(this.extraFields={});f.index+4>>6:(f<65536?A[y++]=224|f>>>12:(A[y++]=240|f>>>18,A[y++]=128|f>>>12&63),A[y++]=128|f>>>6&63),A[y++]=128|63&f);return A},i.buf2binstring=function(d){return u(d,d.length)},i.binstring2buf=function(d){for(var A=new a.Buf8(d.length),f=0,p=A.length;f>10&1023,b[p++]=56320|1023&g)}return u(b,p)},i.utf8border=function(d,A){var f;for((A=A||d.length)>d.length&&(A=d.length),f=A-1;0<=f&&(192&d[f])==128;)f--;return f<0||f===0?A:f+c[d[f]]>A?f:A}},{"./common":41}],43:[function(t,n,i){"use strict";n.exports=function(a,o,l,c){for(var s=65535&a|0,u=a>>>16&65535|0,d=0;l!==0;){for(l-=d=2e3>>1:o>>>1;l[c]=o}return l})();n.exports=function(o,l,c,s){var u=a,d=s+c;o^=-1;for(var A=s;A>>8^u[255&(o^l[A])];return-1^o}},{}],46:[function(t,n,i){"use strict";var a,o=t("../utils/common"),l=t("./trees"),c=t("./adler32"),s=t("./crc32"),u=t("./messages"),d=0,A=4,f=0,p=-2,g=-1,y=4,v=2,b=8,x=9,P=286,F=30,R=19,E=2*P+1,B=15,T=3,W=258,H=W+T+1,C=42,I=113,m=1,N=2,ne=3,q=4;function se(h,U){return h.msg=u[U],U}function Z(h){return(h<<1)-(4h.avail_out&&(z=h.avail_out),z!==0&&(o.arraySet(h.output,U.pending_buf,U.pending_out,z,h.next_out),h.next_out+=z,U.pending_out+=z,h.total_out+=z,h.avail_out-=z,U.pending-=z,U.pending===0&&(U.pending_out=0))}function _(h,U){l._tr_flush_block(h,0<=h.block_start?h.block_start:-1,h.strstart-h.block_start,U),h.block_start=h.strstart,j(h.strm)}function fe(h,U){h.pending_buf[h.pending++]=U}function ce(h,U){h.pending_buf[h.pending++]=U>>>8&255,h.pending_buf[h.pending++]=255&U}function ie(h,U){var z,k,w=h.max_chain_length,D=h.strstart,K=h.prev_length,Y=h.nice_match,M=h.strstart>h.w_size-H?h.strstart-(h.w_size-H):0,J=h.window,ae=h.w_mask,ee=h.prev,pe=h.strstart+W,Ee=J[D+K-1],ve=J[D+K];h.prev_length>=h.good_match&&(w>>=2),Y>h.lookahead&&(Y=h.lookahead);do if(J[(z=U)+K]===ve&&J[z+K-1]===Ee&&J[z]===J[D]&&J[++z]===J[D+1]){D+=2,z++;do;while(J[++D]===J[++z]&&J[++D]===J[++z]&&J[++D]===J[++z]&&J[++D]===J[++z]&&J[++D]===J[++z]&&J[++D]===J[++z]&&J[++D]===J[++z]&&J[++D]===J[++z]&&DM&&--w!=0);return K<=h.lookahead?K:h.lookahead}function be(h){var U,z,k,w,D,K,Y,M,J,ae,ee=h.w_size;do{if(w=h.window_size-h.lookahead-h.strstart,h.strstart>=ee+(ee-H)){for(o.arraySet(h.window,h.window,ee,ee,0),h.match_start-=ee,h.strstart-=ee,h.block_start-=ee,U=z=h.hash_size;k=h.head[--U],h.head[U]=ee<=k?k-ee:0,--z;);for(U=z=ee;k=h.prev[--U],h.prev[U]=ee<=k?k-ee:0,--z;);w+=ee}if(h.strm.avail_in===0)break;if(K=h.strm,Y=h.window,M=h.strstart+h.lookahead,J=w,ae=void 0,ae=K.avail_in,J=T)for(D=h.strstart-h.insert,h.ins_h=h.window[D],h.ins_h=(h.ins_h<=T&&(h.ins_h=(h.ins_h<=T)if(k=l._tr_tally(h,h.strstart-h.match_start,h.match_length-T),h.lookahead-=h.match_length,h.match_length<=h.max_lazy_match&&h.lookahead>=T){for(h.match_length--;h.strstart++,h.ins_h=(h.ins_h<=T&&(h.ins_h=(h.ins_h<=T&&h.match_length<=h.prev_length){for(w=h.strstart+h.lookahead-T,k=l._tr_tally(h,h.strstart-1-h.prev_match,h.prev_length-T),h.lookahead-=h.prev_length-1,h.prev_length-=2;++h.strstart<=w&&(h.ins_h=(h.ins_h<h.pending_buf_size-5&&(z=h.pending_buf_size-5);;){if(h.lookahead<=1){if(be(h),h.lookahead===0&&U===d)return m;if(h.lookahead===0)break}h.strstart+=h.lookahead,h.lookahead=0;var k=h.block_start+z;if((h.strstart===0||h.strstart>=k)&&(h.lookahead=h.strstart-k,h.strstart=k,_(h,!1),h.strm.avail_out===0)||h.strstart-h.block_start>=h.w_size-H&&(_(h,!1),h.strm.avail_out===0))return m}return h.insert=0,U===A?(_(h,!0),h.strm.avail_out===0?ne:q):(h.strstart>h.block_start&&(_(h,!1),h.strm.avail_out),m)}),new xe(4,4,8,4,Re),new xe(4,5,16,8,Re),new xe(4,6,32,32,Re),new xe(4,4,16,16,ke),new xe(8,16,32,32,ke),new xe(8,16,128,128,ke),new xe(8,32,128,256,ke),new xe(32,128,258,1024,ke),new xe(32,258,258,4096,ke)],i.deflateInit=function(h,U){return Be(h,U,b,15,8,0)},i.deflateInit2=Be,i.deflateReset=tt,i.deflateResetKeep=we,i.deflateSetHeader=function(h,U){return h&&h.state?h.state.wrap!==2?p:(h.state.gzhead=U,f):p},i.deflate=function(h,U){var z,k,w,D;if(!h||!h.state||5>8&255),fe(k,k.gzhead.time>>16&255),fe(k,k.gzhead.time>>24&255),fe(k,k.level===9?2:2<=k.strategy||k.level<2?4:0),fe(k,255&k.gzhead.os),k.gzhead.extra&&k.gzhead.extra.length&&(fe(k,255&k.gzhead.extra.length),fe(k,k.gzhead.extra.length>>8&255)),k.gzhead.hcrc&&(h.adler=s(h.adler,k.pending_buf,k.pending,0)),k.gzindex=0,k.status=69):(fe(k,0),fe(k,0),fe(k,0),fe(k,0),fe(k,0),fe(k,k.level===9?2:2<=k.strategy||k.level<2?4:0),fe(k,3),k.status=I);else{var K=b+(k.w_bits-8<<4)<<8;K|=(2<=k.strategy||k.level<2?0:k.level<6?1:k.level===6?2:3)<<6,k.strstart!==0&&(K|=32),K+=31-K%31,k.status=I,ce(k,K),k.strstart!==0&&(ce(k,h.adler>>>16),ce(k,65535&h.adler)),h.adler=1}if(k.status===69)if(k.gzhead.extra){for(w=k.pending;k.gzindex<(65535&k.gzhead.extra.length)&&(k.pending!==k.pending_buf_size||(k.gzhead.hcrc&&k.pending>w&&(h.adler=s(h.adler,k.pending_buf,k.pending-w,w)),j(h),w=k.pending,k.pending!==k.pending_buf_size));)fe(k,255&k.gzhead.extra[k.gzindex]),k.gzindex++;k.gzhead.hcrc&&k.pending>w&&(h.adler=s(h.adler,k.pending_buf,k.pending-w,w)),k.gzindex===k.gzhead.extra.length&&(k.gzindex=0,k.status=73)}else k.status=73;if(k.status===73)if(k.gzhead.name){w=k.pending;do{if(k.pending===k.pending_buf_size&&(k.gzhead.hcrc&&k.pending>w&&(h.adler=s(h.adler,k.pending_buf,k.pending-w,w)),j(h),w=k.pending,k.pending===k.pending_buf_size)){D=1;break}D=k.gzindexw&&(h.adler=s(h.adler,k.pending_buf,k.pending-w,w)),D===0&&(k.gzindex=0,k.status=91)}else k.status=91;if(k.status===91)if(k.gzhead.comment){w=k.pending;do{if(k.pending===k.pending_buf_size&&(k.gzhead.hcrc&&k.pending>w&&(h.adler=s(h.adler,k.pending_buf,k.pending-w,w)),j(h),w=k.pending,k.pending===k.pending_buf_size)){D=1;break}D=k.gzindexw&&(h.adler=s(h.adler,k.pending_buf,k.pending-w,w)),D===0&&(k.status=103)}else k.status=103;if(k.status===103&&(k.gzhead.hcrc?(k.pending+2>k.pending_buf_size&&j(h),k.pending+2<=k.pending_buf_size&&(fe(k,255&h.adler),fe(k,h.adler>>8&255),h.adler=0,k.status=I)):k.status=I),k.pending!==0){if(j(h),h.avail_out===0)return k.last_flush=-1,f}else if(h.avail_in===0&&Z(U)<=Z(z)&&U!==A)return se(h,-5);if(k.status===666&&h.avail_in!==0)return se(h,-5);if(h.avail_in!==0||k.lookahead!==0||U!==d&&k.status!==666){var Y=k.strategy===2?(function(M,J){for(var ae;;){if(M.lookahead===0&&(be(M),M.lookahead===0)){if(J===d)return m;break}if(M.match_length=0,ae=l._tr_tally(M,0,M.window[M.strstart]),M.lookahead--,M.strstart++,ae&&(_(M,!1),M.strm.avail_out===0))return m}return M.insert=0,J===A?(_(M,!0),M.strm.avail_out===0?ne:q):M.last_lit&&(_(M,!1),M.strm.avail_out===0)?m:N})(k,U):k.strategy===3?(function(M,J){for(var ae,ee,pe,Ee,ve=M.window;;){if(M.lookahead<=W){if(be(M),M.lookahead<=W&&J===d)return m;if(M.lookahead===0)break}if(M.match_length=0,M.lookahead>=T&&0M.lookahead&&(M.match_length=M.lookahead)}if(M.match_length>=T?(ae=l._tr_tally(M,1,M.match_length-T),M.lookahead-=M.match_length,M.strstart+=M.match_length,M.match_length=0):(ae=l._tr_tally(M,0,M.window[M.strstart]),M.lookahead--,M.strstart++),ae&&(_(M,!1),M.strm.avail_out===0))return m}return M.insert=0,J===A?(_(M,!0),M.strm.avail_out===0?ne:q):M.last_lit&&(_(M,!1),M.strm.avail_out===0)?m:N})(k,U):a[k.level].func(k,U);if(Y!==ne&&Y!==q||(k.status=666),Y===m||Y===ne)return h.avail_out===0&&(k.last_flush=-1),f;if(Y===N&&(U===1?l._tr_align(k):U!==5&&(l._tr_stored_block(k,0,0,!1),U===3&&(ue(k.head),k.lookahead===0&&(k.strstart=0,k.block_start=0,k.insert=0))),j(h),h.avail_out===0))return k.last_flush=-1,f}return U!==A?f:k.wrap<=0?1:(k.wrap===2?(fe(k,255&h.adler),fe(k,h.adler>>8&255),fe(k,h.adler>>16&255),fe(k,h.adler>>24&255),fe(k,255&h.total_in),fe(k,h.total_in>>8&255),fe(k,h.total_in>>16&255),fe(k,h.total_in>>24&255)):(ce(k,h.adler>>>16),ce(k,65535&h.adler)),j(h),0=z.w_size&&(D===0&&(ue(z.head),z.strstart=0,z.block_start=0,z.insert=0),J=new o.Buf8(z.w_size),o.arraySet(J,U,ae-z.w_size,z.w_size,0),U=J,ae=z.w_size),K=h.avail_in,Y=h.next_in,M=h.input,h.avail_in=ae,h.next_in=0,h.input=U,be(z);z.lookahead>=T;){for(k=z.strstart,w=z.lookahead-(T-1);z.ins_h=(z.ins_h<>>=T=B>>>24,x-=T,(T=B>>>16&255)===0)N[u++]=65535&B;else{if(!(16&T)){if((64&T)==0){B=P[(65535&B)+(b&(1<>>=T,x-=T),x<15&&(b+=m[c++]<>>=T=B>>>24,x-=T,!(16&(T=B>>>16&255))){if((64&T)==0){B=F[(65535&B)+(b&(1<>>=T,x-=T,(T=u-d)>3,b&=(1<<(x-=W<<3))-1,a.next_in=c,a.next_out=u,a.avail_in=c>>24&255)+(C>>>8&65280)+((65280&C)<<8)+((255&C)<<24)}function b(){this.mode=0,this.last=!1,this.wrap=0,this.havedict=!1,this.flags=0,this.dmax=0,this.check=0,this.total=0,this.head=null,this.wbits=0,this.wsize=0,this.whave=0,this.wnext=0,this.window=null,this.hold=0,this.bits=0,this.length=0,this.offset=0,this.extra=0,this.lencode=null,this.distcode=null,this.lenbits=0,this.distbits=0,this.ncode=0,this.nlen=0,this.ndist=0,this.have=0,this.next=null,this.lens=new a.Buf16(320),this.work=new a.Buf16(288),this.lendyn=null,this.distdyn=null,this.sane=0,this.back=0,this.was=0}function x(C){var I;return C&&C.state?(I=C.state,C.total_in=C.total_out=I.total=0,C.msg="",I.wrap&&(C.adler=1&I.wrap),I.mode=p,I.last=0,I.havedict=0,I.dmax=32768,I.head=null,I.hold=0,I.bits=0,I.lencode=I.lendyn=new a.Buf32(g),I.distcode=I.distdyn=new a.Buf32(y),I.sane=1,I.back=-1,A):f}function P(C){var I;return C&&C.state?((I=C.state).wsize=0,I.whave=0,I.wnext=0,x(C)):f}function F(C,I){var m,N;return C&&C.state?(N=C.state,I<0?(m=0,I=-I):(m=1+(I>>4),I<48&&(I&=15)),I&&(I<8||15=q.wsize?(a.arraySet(q.window,I,m-q.wsize,q.wsize,0),q.wnext=0,q.whave=q.wsize):(N<(ne=q.wsize-q.wnext)&&(ne=N),a.arraySet(q.window,I,m-N,ne,q.wnext),(N-=ne)?(a.arraySet(q.window,I,m-N,N,0),q.wnext=N,q.whave=q.wsize):(q.wnext+=ne,q.wnext===q.wsize&&(q.wnext=0),q.whave>>8&255,m.check=l(m.check,D,2,0),_=j=0,m.mode=2;break}if(m.flags=0,m.head&&(m.head.done=!1),!(1&m.wrap)||(((255&j)<<8)+(j>>8))%31){C.msg="incorrect header check",m.mode=30;break}if((15&j)!=8){C.msg="unknown compression method",m.mode=30;break}if(_-=4,h=8+(15&(j>>>=4)),m.wbits===0)m.wbits=h;else if(h>m.wbits){C.msg="invalid window size",m.mode=30;break}m.dmax=1<>8&1),512&m.flags&&(D[0]=255&j,D[1]=j>>>8&255,m.check=l(m.check,D,2,0)),_=j=0,m.mode=3;case 3:for(;_<32;){if(Z===0)break e;Z--,j+=N[q++]<<_,_+=8}m.head&&(m.head.time=j),512&m.flags&&(D[0]=255&j,D[1]=j>>>8&255,D[2]=j>>>16&255,D[3]=j>>>24&255,m.check=l(m.check,D,4,0)),_=j=0,m.mode=4;case 4:for(;_<16;){if(Z===0)break e;Z--,j+=N[q++]<<_,_+=8}m.head&&(m.head.xflags=255&j,m.head.os=j>>8),512&m.flags&&(D[0]=255&j,D[1]=j>>>8&255,m.check=l(m.check,D,2,0)),_=j=0,m.mode=5;case 5:if(1024&m.flags){for(;_<16;){if(Z===0)break e;Z--,j+=N[q++]<<_,_+=8}m.length=j,m.head&&(m.head.extra_len=j),512&m.flags&&(D[0]=255&j,D[1]=j>>>8&255,m.check=l(m.check,D,2,0)),_=j=0}else m.head&&(m.head.extra=null);m.mode=6;case 6:if(1024&m.flags&&(Z<(ie=m.length)&&(ie=Z),ie&&(m.head&&(h=m.head.extra_len-m.length,m.head.extra||(m.head.extra=new Array(m.head.extra_len)),a.arraySet(m.head.extra,N,q,ie,h)),512&m.flags&&(m.check=l(m.check,N,ie,q)),Z-=ie,q+=ie,m.length-=ie),m.length))break e;m.length=0,m.mode=7;case 7:if(2048&m.flags){if(Z===0)break e;for(ie=0;h=N[q+ie++],m.head&&h&&m.length<65536&&(m.head.name+=String.fromCharCode(h)),h&&ie>9&1,m.head.done=!0),C.adler=m.check=0,m.mode=12;break;case 10:for(;_<32;){if(Z===0)break e;Z--,j+=N[q++]<<_,_+=8}C.adler=m.check=v(j),_=j=0,m.mode=11;case 11:if(m.havedict===0)return C.next_out=se,C.avail_out=ue,C.next_in=q,C.avail_in=Z,m.hold=j,m.bits=_,2;C.adler=m.check=1,m.mode=12;case 12:if(I===5||I===6)break e;case 13:if(m.last){j>>>=7&_,_-=7&_,m.mode=27;break}for(;_<3;){if(Z===0)break e;Z--,j+=N[q++]<<_,_+=8}switch(m.last=1&j,_-=1,3&(j>>>=1)){case 0:m.mode=14;break;case 1:if(W(m),m.mode=20,I!==6)break;j>>>=2,_-=2;break e;case 2:m.mode=17;break;case 3:C.msg="invalid block type",m.mode=30}j>>>=2,_-=2;break;case 14:for(j>>>=7&_,_-=7&_;_<32;){if(Z===0)break e;Z--,j+=N[q++]<<_,_+=8}if((65535&j)!=(j>>>16^65535)){C.msg="invalid stored block lengths",m.mode=30;break}if(m.length=65535&j,_=j=0,m.mode=15,I===6)break e;case 15:m.mode=16;case 16:if(ie=m.length){if(Z>>=5,_-=5,m.ndist=1+(31&j),j>>>=5,_-=5,m.ncode=4+(15&j),j>>>=4,_-=4,286>>=3,_-=3}for(;m.have<19;)m.lens[K[m.have++]]=0;if(m.lencode=m.lendyn,m.lenbits=7,z={bits:m.lenbits},U=s(0,m.lens,0,19,m.lencode,0,m.work,z),m.lenbits=z.bits,U){C.msg="invalid code lengths set",m.mode=30;break}m.have=0,m.mode=19;case 19:for(;m.have>>16&255,Oe=65535&w,!((ke=w>>>24)<=_);){if(Z===0)break e;Z--,j+=N[q++]<<_,_+=8}if(Oe<16)j>>>=ke,_-=ke,m.lens[m.have++]=Oe;else{if(Oe===16){for(k=ke+2;_>>=ke,_-=ke,m.have===0){C.msg="invalid bit length repeat",m.mode=30;break}h=m.lens[m.have-1],ie=3+(3&j),j>>>=2,_-=2}else if(Oe===17){for(k=ke+3;_>>=ke)),j>>>=3,_-=3}else{for(k=ke+7;_>>=ke)),j>>>=7,_-=7}if(m.have+ie>m.nlen+m.ndist){C.msg="invalid bit length repeat",m.mode=30;break}for(;ie--;)m.lens[m.have++]=h}}if(m.mode===30)break;if(m.lens[256]===0){C.msg="invalid code -- missing end-of-block",m.mode=30;break}if(m.lenbits=9,z={bits:m.lenbits},U=s(u,m.lens,0,m.nlen,m.lencode,0,m.work,z),m.lenbits=z.bits,U){C.msg="invalid literal/lengths set",m.mode=30;break}if(m.distbits=6,m.distcode=m.distdyn,z={bits:m.distbits},U=s(d,m.lens,m.nlen,m.ndist,m.distcode,0,m.work,z),m.distbits=z.bits,U){C.msg="invalid distances set",m.mode=30;break}if(m.mode=20,I===6)break e;case 20:m.mode=21;case 21:if(6<=Z&&258<=ue){C.next_out=se,C.avail_out=ue,C.next_in=q,C.avail_in=Z,m.hold=j,m.bits=_,c(C,ce),se=C.next_out,ne=C.output,ue=C.avail_out,q=C.next_in,N=C.input,Z=C.avail_in,j=m.hold,_=m.bits,m.mode===12&&(m.back=-1);break}for(m.back=0;xe=(w=m.lencode[j&(1<>>16&255,Oe=65535&w,!((ke=w>>>24)<=_);){if(Z===0)break e;Z--,j+=N[q++]<<_,_+=8}if(xe&&(240&xe)==0){for(we=ke,tt=xe,Be=Oe;xe=(w=m.lencode[Be+((j&(1<>we)])>>>16&255,Oe=65535&w,!(we+(ke=w>>>24)<=_);){if(Z===0)break e;Z--,j+=N[q++]<<_,_+=8}j>>>=we,_-=we,m.back+=we}if(j>>>=ke,_-=ke,m.back+=ke,m.length=Oe,xe===0){m.mode=26;break}if(32&xe){m.back=-1,m.mode=12;break}if(64&xe){C.msg="invalid literal/length code",m.mode=30;break}m.extra=15&xe,m.mode=22;case 22:if(m.extra){for(k=m.extra;_>>=m.extra,_-=m.extra,m.back+=m.extra}m.was=m.length,m.mode=23;case 23:for(;xe=(w=m.distcode[j&(1<>>16&255,Oe=65535&w,!((ke=w>>>24)<=_);){if(Z===0)break e;Z--,j+=N[q++]<<_,_+=8}if((240&xe)==0){for(we=ke,tt=xe,Be=Oe;xe=(w=m.distcode[Be+((j&(1<>we)])>>>16&255,Oe=65535&w,!(we+(ke=w>>>24)<=_);){if(Z===0)break e;Z--,j+=N[q++]<<_,_+=8}j>>>=we,_-=we,m.back+=we}if(j>>>=ke,_-=ke,m.back+=ke,64&xe){C.msg="invalid distance code",m.mode=30;break}m.offset=Oe,m.extra=15&xe,m.mode=24;case 24:if(m.extra){for(k=m.extra;_>>=m.extra,_-=m.extra,m.back+=m.extra}if(m.offset>m.dmax){C.msg="invalid distance too far back",m.mode=30;break}m.mode=25;case 25:if(ue===0)break e;if(ie=ce-ue,m.offset>ie){if((ie=m.offset-ie)>m.whave&&m.sane){C.msg="invalid distance too far back",m.mode=30;break}be=ie>m.wnext?(ie-=m.wnext,m.wsize-ie):m.wnext-ie,ie>m.length&&(ie=m.length),Re=m.window}else Re=ne,be=se-m.offset,ie=m.length;for(ueE?(T=be[Re+y[I]],_[fe+y[I]]):(T=96,0),b=1<>se)+(x-=b)]=B<<24|T<<16|W|0,x!==0;);for(b=1<>=1;if(b!==0?(j&=b-1,j+=b):j=0,I++,--ce[C]==0){if(C===N)break;C=d[A+y[I]]}if(ne>>7)]}function fe(w,D){w.pending_buf[w.pending++]=255&D,w.pending_buf[w.pending++]=D>>>8&255}function ce(w,D,K){w.bi_valid>v-K?(w.bi_buf|=D<>v-w.bi_valid,w.bi_valid+=K-v):(w.bi_buf|=D<>>=1,K<<=1,0<--D;);return K>>>1}function Re(w,D,K){var Y,M,J=new Array(y+1),ae=0;for(Y=1;Y<=y;Y++)J[Y]=ae=ae+K[Y-1]<<1;for(M=0;M<=D;M++){var ee=w[2*M+1];ee!==0&&(w[2*M]=be(J[ee]++,ee))}}function ke(w){var D;for(D=0;D>1;1<=K;K--)we(w,J,K);for(M=pe;K=w.heap[1],w.heap[1]=w.heap[w.heap_len--],we(w,J,1),Y=w.heap[1],w.heap[--w.heap_max]=K,w.heap[--w.heap_max]=Y,J[2*M]=J[2*K]+J[2*Y],w.depth[M]=(w.depth[K]>=w.depth[Y]?w.depth[K]:w.depth[Y])+1,J[2*K+1]=J[2*Y+1]=M,w.heap[1]=M++,we(w,J,1),2<=w.heap_len;);w.heap[--w.heap_max]=w.heap[1],(function(ve,Ie){var rt,Me,G,oe,me,De,te=Ie.dyn_tree,ge=Ie.max_code,L=Ie.stat_desc.static_tree,ye=Ie.stat_desc.has_stree,Ge=Ie.stat_desc.extra_bits,S=Ie.stat_desc.extra_base,de=Ie.stat_desc.max_length,Ae=0;for(oe=0;oe<=y;oe++)ve.bl_count[oe]=0;for(te[2*ve.heap[ve.heap_max]+1]=0,rt=ve.heap_max+1;rt>=7;M>>=1)if(1&Ee&&ee.dyn_ltree[2*pe]!==0)return o;if(ee.dyn_ltree[18]!==0||ee.dyn_ltree[20]!==0||ee.dyn_ltree[26]!==0)return l;for(pe=32;pe>>3,(J=w.static_len+3+7>>>3)<=M&&(M=J)):M=J=K+5,K+4<=M&&D!==-1?k(w,D,K,Y):w.strategy===4||J===M?(ce(w,2+(Y?1:0),3),tt(w,H,C)):(ce(w,4+(Y?1:0),3),(function(ee,pe,Ee,ve){var Ie;for(ce(ee,pe-257,5),ce(ee,Ee-1,5),ce(ee,ve-4,4),Ie=0;Ie>>8&255,w.pending_buf[w.d_buf+2*w.last_lit+1]=255&D,w.pending_buf[w.l_buf+w.last_lit]=255&K,w.last_lit++,D===0?w.dyn_ltree[2*K]++:(w.matches++,D--,w.dyn_ltree[2*(m[K]+d+1)]++,w.dyn_dtree[2*_(D)]++),w.last_lit===w.lit_bufsize-1},i._tr_align=function(w){ce(w,2,3),ie(w,x,H),(function(D){D.bi_valid===16?(fe(D,D.bi_buf),D.bi_buf=0,D.bi_valid=0):8<=D.bi_valid&&(D.pending_buf[D.pending++]=255&D.bi_buf,D.bi_buf>>=8,D.bi_valid-=8)})(w)}},{"../utils/common":41}],53:[function(t,n,i){"use strict";n.exports=function(){this.input=null,this.next_in=0,this.avail_in=0,this.total_in=0,this.output=null,this.next_out=0,this.avail_out=0,this.total_out=0,this.msg="",this.state=null,this.data_type=2,this.adler=0}},{}],54:[function(t,n,i){(function(a){(function(o,l){"use strict";if(!o.setImmediate){var c,s,u,d,A=1,f={},p=!1,g=o.document,y=Object.getPrototypeOf&&Object.getPrototypeOf(o);y=y&&y.setTimeout?y:o,c={}.toString.call(o.process)==="[object process]"?function(P){process.nextTick(function(){b(P)})}:(function(){if(o.postMessage&&!o.importScripts){var P=!0,F=o.onmessage;return o.onmessage=function(){P=!1},o.postMessage("","*"),o.onmessage=F,P}})()?(d="setImmediate$"+Math.random()+"$",o.addEventListener?o.addEventListener("message",x,!1):o.attachEvent("onmessage",x),function(P){o.postMessage(d+P,"*")}):o.MessageChannel?((u=new MessageChannel).port1.onmessage=function(P){b(P.data)},function(P){u.port2.postMessage(P)}):g&&"onreadystatechange"in g.createElement("script")?(s=g.documentElement,function(P){var F=g.createElement("script");F.onreadystatechange=function(){b(P),F.onreadystatechange=null,s.removeChild(F),F=null},s.appendChild(F)}):function(P){setTimeout(b,0,P)},y.setImmediate=function(P){typeof P!="function"&&(P=new Function(""+P));for(var F=new Array(arguments.length-1),R=0;R"u"?a===void 0?this:a:self)}).call(this,typeof global<"u"?global:typeof self<"u"?self:typeof window<"u"?window:{})},{}]},{},[10])(10)})}}),pp=Lt({"node_modules/.pnpm/jszip@3.10.1/node_modules/jszip/dist/jszip.min.js"(e,r){(function(t){typeof e=="object"&&typeof r<"u"?r.exports=t():typeof define=="function"&&define.amd?define([],t):(typeof window<"u"?window:typeof global<"u"?global:typeof self<"u"?self:this).JSZip=t()})(function(){return(function t(n,i,a){function o(s,u){if(!i[s]){if(!n[s]){var d=typeof An=="function"&&An;if(!u&&d)return d(s,!0);if(l)return l(s,!0);var A=new Error("Cannot find module '"+s+"'");throw A.code="MODULE_NOT_FOUND",A}var f=i[s]={exports:{}};n[s][0].call(f.exports,function(p){var g=n[s][1][p];return o(g||p)},f,f.exports,t,n,i,a)}return i[s].exports}for(var l=typeof An=="function"&&An,c=0;c>2,f=(3&s)<<4|u>>4,p=1>6:64,g=2>4,u=(15&A)<<4|(f=l.indexOf(c.charAt(g++)))>>2,d=(3&f)<<6|(p=l.indexOf(c.charAt(g++))),b[y++]=s,f!==64&&(b[y++]=u),p!==64&&(b[y++]=d);return b}},{"./support":30,"./utils":32}],2:[function(t,n,i){"use strict";var a=t("./external"),o=t("./stream/DataWorker"),l=t("./stream/Crc32Probe"),c=t("./stream/DataLengthProbe");function s(u,d,A,f,p){this.compressedSize=u,this.uncompressedSize=d,this.crc32=A,this.compression=f,this.compressedContent=p}s.prototype={getContentWorker:function(){var u=new o(a.Promise.resolve(this.compressedContent)).pipe(this.compression.uncompressWorker()).pipe(new c("data_length")),d=this;return u.on("end",function(){if(this.streamInfo.data_length!==d.uncompressedSize)throw new Error("Bug : uncompressed data size mismatch")}),u},getCompressedWorker:function(){return new o(a.Promise.resolve(this.compressedContent)).withStreamInfo("compressedSize",this.compressedSize).withStreamInfo("uncompressedSize",this.uncompressedSize).withStreamInfo("crc32",this.crc32).withStreamInfo("compression",this.compression)}},s.createWorkerFrom=function(u,d,A){return u.pipe(new l).pipe(new c("uncompressedSize")).pipe(d.compressWorker(A)).pipe(new c("compressedSize")).withStreamInfo("compression",d)},n.exports=s},{"./external":6,"./stream/Crc32Probe":25,"./stream/DataLengthProbe":26,"./stream/DataWorker":27}],3:[function(t,n,i){"use strict";var a=t("./stream/GenericWorker");i.STORE={magic:"\0\0",compressWorker:function(){return new a("STORE compression")},uncompressWorker:function(){return new a("STORE decompression")}},i.DEFLATE=t("./flate")},{"./flate":7,"./stream/GenericWorker":28}],4:[function(t,n,i){"use strict";var a=t("./utils"),o=(function(){for(var l,c=[],s=0;s<256;s++){l=s;for(var u=0;u<8;u++)l=1&l?3988292384^l>>>1:l>>>1;c[s]=l}return c})();n.exports=function(l,c){return l!==void 0&&l.length?a.getTypeOf(l)!=="string"?(function(s,u,d,A){var f=o,p=A+d;s^=-1;for(var g=A;g>>8^f[255&(s^u[g])];return-1^s})(0|c,l,l.length,0):(function(s,u,d,A){var f=o,p=A+d;s^=-1;for(var g=A;g>>8^f[255&(s^u.charCodeAt(g))];return-1^s})(0|c,l,l.length,0):0}},{"./utils":32}],5:[function(t,n,i){"use strict";i.base64=!1,i.binary=!1,i.dir=!1,i.createFolders=!0,i.date=null,i.compression=null,i.compressionOptions=null,i.comment=null,i.unixPermissions=null,i.dosPermissions=null},{}],6:[function(t,n,i){"use strict";var a=null;a=typeof Promise<"u"?Promise:t("lie"),n.exports={Promise:a}},{lie:37}],7:[function(t,n,i){"use strict";var a=typeof Uint8Array<"u"&&typeof Uint16Array<"u"&&typeof Uint32Array<"u",o=t("pako"),l=t("./utils"),c=t("./stream/GenericWorker"),s=a?"uint8array":"array";function u(d,A){c.call(this,"FlateWorker/"+d),this._pako=null,this._pakoAction=d,this._pakoOptions=A,this.meta={}}i.magic="\b\0",l.inherits(u,c),u.prototype.processChunk=function(d){this.meta=d.meta,this._pako===null&&this._createPako(),this._pako.push(l.transformTo(s,d.data),!1)},u.prototype.flush=function(){c.prototype.flush.call(this),this._pako===null&&this._createPako(),this._pako.push([],!0)},u.prototype.cleanUp=function(){c.prototype.cleanUp.call(this),this._pako=null},u.prototype._createPako=function(){this._pako=new o[this._pakoAction]({raw:!0,level:this._pakoOptions.level||-1});var d=this;this._pako.onData=function(A){d.push({data:A,meta:d.meta})}},i.compressWorker=function(d){return new u("Deflate",d)},i.uncompressWorker=function(){return new u("Inflate",{})}},{"./stream/GenericWorker":28,"./utils":32,pako:38}],8:[function(t,n,i){"use strict";function a(f,p){var g,y="";for(g=0;g>>=8;return y}function o(f,p,g,y,v,b){var x,P,F=f.file,R=f.compression,E=b!==s.utf8encode,B=l.transformTo("string",b(F.name)),T=l.transformTo("string",s.utf8encode(F.name)),W=F.comment,H=l.transformTo("string",b(W)),C=l.transformTo("string",s.utf8encode(W)),I=T.length!==F.name.length,m=C.length!==W.length,N="",ne="",q="",se=F.dir,Z=F.date,ue={crc32:0,compressedSize:0,uncompressedSize:0};p&&!g||(ue.crc32=f.crc32,ue.compressedSize=f.compressedSize,ue.uncompressedSize=f.uncompressedSize);var j=0;p&&(j|=8),E||!I&&!m||(j|=2048);var _=0,fe=0;se&&(_|=16),v==="UNIX"?(fe=798,_|=(function(ie,be){var Re=ie;return ie||(Re=be?16893:33204),(65535&Re)<<16})(F.unixPermissions,se)):(fe=20,_|=(function(ie){return 63&(ie||0)})(F.dosPermissions)),x=Z.getUTCHours(),x<<=6,x|=Z.getUTCMinutes(),x<<=5,x|=Z.getUTCSeconds()/2,P=Z.getUTCFullYear()-1980,P<<=4,P|=Z.getUTCMonth()+1,P<<=5,P|=Z.getUTCDate(),I&&(ne=a(1,1)+a(u(B),4)+T,N+="up"+a(ne.length,2)+ne),m&&(q=a(1,1)+a(u(H),4)+C,N+="uc"+a(q.length,2)+q);var ce="";return ce+=` +\0`,ce+=a(j,2),ce+=R.magic,ce+=a(x,2),ce+=a(P,2),ce+=a(ue.crc32,4),ce+=a(ue.compressedSize,4),ce+=a(ue.uncompressedSize,4),ce+=a(B.length,2),ce+=a(N.length,2),{fileRecord:d.LOCAL_FILE_HEADER+ce+B+N,dirRecord:d.CENTRAL_FILE_HEADER+a(fe,2)+ce+a(H.length,2)+"\0\0\0\0"+a(_,4)+a(y,4)+B+N+H}}var l=t("../utils"),c=t("../stream/GenericWorker"),s=t("../utf8"),u=t("../crc32"),d=t("../signature");function A(f,p,g,y){c.call(this,"ZipFileWorker"),this.bytesWritten=0,this.zipComment=p,this.zipPlatform=g,this.encodeFileName=y,this.streamFiles=f,this.accumulate=!1,this.contentBuffer=[],this.dirRecords=[],this.currentSourceOffset=0,this.entriesCount=0,this.currentFile=null,this._sources=[]}l.inherits(A,c),A.prototype.push=function(f){var p=f.meta.percent||0,g=this.entriesCount,y=this._sources.length;this.accumulate?this.contentBuffer.push(f):(this.bytesWritten+=f.data.length,c.prototype.push.call(this,{data:f.data,meta:{currentFile:this.currentFile,percent:g?(p+100*(g-y-1))/g:100}}))},A.prototype.openedSource=function(f){this.currentSourceOffset=this.bytesWritten,this.currentFile=f.file.name;var p=this.streamFiles&&!f.file.dir;if(p){var g=o(f,p,!1,this.currentSourceOffset,this.zipPlatform,this.encodeFileName);this.push({data:g.fileRecord,meta:{percent:0}})}else this.accumulate=!0},A.prototype.closedSource=function(f){this.accumulate=!1;var p=this.streamFiles&&!f.file.dir,g=o(f,p,!0,this.currentSourceOffset,this.zipPlatform,this.encodeFileName);if(this.dirRecords.push(g.dirRecord),p)this.push({data:(function(y){return d.DATA_DESCRIPTOR+a(y.crc32,4)+a(y.compressedSize,4)+a(y.uncompressedSize,4)})(f),meta:{percent:100}});else for(this.push({data:g.fileRecord,meta:{percent:0}});this.contentBuffer.length;)this.push(this.contentBuffer.shift());this.currentFile=null},A.prototype.flush=function(){for(var f=this.bytesWritten,p=0;p=this.index;c--)s=(s<<8)+this.byteAt(c);return this.index+=l,s},readString:function(l){return a.transformTo("string",this.readData(l))},readData:function(){},lastIndexOfSignature:function(){},readAndCheckSignature:function(){},readDate:function(){var l=this.readInt(4);return new Date(Date.UTC(1980+(l>>25&127),(l>>21&15)-1,l>>16&31,l>>11&31,l>>5&63,(31&l)<<1))}},n.exports=o},{"../utils":32}],19:[function(t,n,i){"use strict";var a=t("./Uint8ArrayReader");function o(l){a.call(this,l)}t("../utils").inherits(o,a),o.prototype.readData=function(l){this.checkOffset(l);var c=this.data.slice(this.zero+this.index,this.zero+this.index+l);return this.index+=l,c},n.exports=o},{"../utils":32,"./Uint8ArrayReader":21}],20:[function(t,n,i){"use strict";var a=t("./DataReader");function o(l){a.call(this,l)}t("../utils").inherits(o,a),o.prototype.byteAt=function(l){return this.data.charCodeAt(this.zero+l)},o.prototype.lastIndexOfSignature=function(l){return this.data.lastIndexOf(l)-this.zero},o.prototype.readAndCheckSignature=function(l){return l===this.readData(4)},o.prototype.readData=function(l){this.checkOffset(l);var c=this.data.slice(this.zero+this.index,this.zero+this.index+l);return this.index+=l,c},n.exports=o},{"../utils":32,"./DataReader":18}],21:[function(t,n,i){"use strict";var a=t("./ArrayReader");function o(l){a.call(this,l)}t("../utils").inherits(o,a),o.prototype.readData=function(l){if(this.checkOffset(l),l===0)return new Uint8Array(0);var c=this.data.subarray(this.zero+this.index,this.zero+this.index+l);return this.index+=l,c},n.exports=o},{"../utils":32,"./ArrayReader":17}],22:[function(t,n,i){"use strict";var a=t("../utils"),o=t("../support"),l=t("./ArrayReader"),c=t("./StringReader"),s=t("./NodeBufferReader"),u=t("./Uint8ArrayReader");n.exports=function(d){var A=a.getTypeOf(d);return a.checkSupport(A),A!=="string"||o.uint8array?A==="nodebuffer"?new s(d):o.uint8array?new u(a.transformTo("uint8array",d)):new l(a.transformTo("array",d)):new c(d)}},{"../support":30,"../utils":32,"./ArrayReader":17,"./NodeBufferReader":19,"./StringReader":20,"./Uint8ArrayReader":21}],23:[function(t,n,i){"use strict";i.LOCAL_FILE_HEADER="PK",i.CENTRAL_FILE_HEADER="PK",i.CENTRAL_DIRECTORY_END="PK",i.ZIP64_CENTRAL_DIRECTORY_LOCATOR="PK\x07",i.ZIP64_CENTRAL_DIRECTORY_END="PK",i.DATA_DESCRIPTOR="PK\x07\b"},{}],24:[function(t,n,i){"use strict";var a=t("./GenericWorker"),o=t("../utils");function l(c){a.call(this,"ConvertWorker to "+c),this.destType=c}o.inherits(l,a),l.prototype.processChunk=function(c){this.push({data:o.transformTo(this.destType,c.data),meta:c.meta})},n.exports=l},{"../utils":32,"./GenericWorker":28}],25:[function(t,n,i){"use strict";var a=t("./GenericWorker"),o=t("../crc32");function l(){a.call(this,"Crc32Probe"),this.withStreamInfo("crc32",0)}t("../utils").inherits(l,a),l.prototype.processChunk=function(c){this.streamInfo.crc32=o(c.data,this.streamInfo.crc32||0),this.push(c)},n.exports=l},{"../crc32":4,"../utils":32,"./GenericWorker":28}],26:[function(t,n,i){"use strict";var a=t("../utils"),o=t("./GenericWorker");function l(c){o.call(this,"DataLengthProbe for "+c),this.propName=c,this.withStreamInfo(c,0)}a.inherits(l,o),l.prototype.processChunk=function(c){if(c){var s=this.streamInfo[this.propName]||0;this.streamInfo[this.propName]=s+c.data.length}o.prototype.processChunk.call(this,c)},n.exports=l},{"../utils":32,"./GenericWorker":28}],27:[function(t,n,i){"use strict";var a=t("../utils"),o=t("./GenericWorker");function l(c){o.call(this,"DataWorker");var s=this;this.dataIsReady=!1,this.index=0,this.max=0,this.data=null,this.type="",this._tickScheduled=!1,c.then(function(u){s.dataIsReady=!0,s.data=u,s.max=u&&u.length||0,s.type=a.getTypeOf(u),s.isPaused||s._tickAndRepeat()},function(u){s.error(u)})}a.inherits(l,o),l.prototype.cleanUp=function(){o.prototype.cleanUp.call(this),this.data=null},l.prototype.resume=function(){return!!o.prototype.resume.call(this)&&(!this._tickScheduled&&this.dataIsReady&&(this._tickScheduled=!0,a.delay(this._tickAndRepeat,[],this)),!0)},l.prototype._tickAndRepeat=function(){this._tickScheduled=!1,this.isPaused||this.isFinished||(this._tick(),this.isFinished||(a.delay(this._tickAndRepeat,[],this),this._tickScheduled=!0))},l.prototype._tick=function(){if(this.isPaused||this.isFinished)return!1;var c=null,s=Math.min(this.max,this.index+16384);if(this.index>=this.max)return this.end();switch(this.type){case"string":c=this.data.substring(this.index,s);break;case"uint8array":c=this.data.subarray(this.index,s);break;case"array":case"nodebuffer":c=this.data.slice(this.index,s)}return this.index=s,this.push({data:c,meta:{percent:this.max?this.index/this.max*100:0}})},n.exports=l},{"../utils":32,"./GenericWorker":28}],28:[function(t,n,i){"use strict";function a(o){this.name=o||"default",this.streamInfo={},this.generatedError=null,this.extraStreamInfo={},this.isPaused=!0,this.isFinished=!1,this.isLocked=!1,this._listeners={data:[],end:[],error:[]},this.previous=null}a.prototype={push:function(o){this.emit("data",o)},end:function(){if(this.isFinished)return!1;this.flush();try{this.emit("end"),this.cleanUp(),this.isFinished=!0}catch(o){this.emit("error",o)}return!0},error:function(o){return!this.isFinished&&(this.isPaused?this.generatedError=o:(this.isFinished=!0,this.emit("error",o),this.previous&&this.previous.error(o),this.cleanUp()),!0)},on:function(o,l){return this._listeners[o].push(l),this},cleanUp:function(){this.streamInfo=this.generatedError=this.extraStreamInfo=null,this._listeners=[]},emit:function(o,l){if(this._listeners[o])for(var c=0;c "+o:o}},n.exports=a},{}],29:[function(t,n,i){"use strict";var a=t("../utils"),o=t("./ConvertWorker"),l=t("./GenericWorker"),c=t("../base64"),s=t("../support"),u=t("../external"),d=null;if(s.nodestream)try{d=t("../nodejs/NodejsStreamOutputAdapter")}catch{}function A(p,g){return new u.Promise(function(y,v){var b=[],x=p._internalType,P=p._outputType,F=p._mimeType;p.on("data",function(R,E){b.push(R),g&&g(E)}).on("error",function(R){b=[],v(R)}).on("end",function(){try{var R=(function(E,B,T){switch(E){case"blob":return a.newBlob(a.transformTo("arraybuffer",B),T);case"base64":return c.encode(B);default:return a.transformTo(E,B)}})(P,(function(E,B){var T,W=0,H=null,C=0;for(T=0;T"u")i.blob=!1;else{var a=new ArrayBuffer(0);try{i.blob=new Blob([a],{type:"application/zip"}).size===0}catch{try{var o=new(self.BlobBuilder||self.WebKitBlobBuilder||self.MozBlobBuilder||self.MSBlobBuilder);o.append(a),i.blob=o.getBlob("application/zip").size===0}catch{i.blob=!1}}}try{i.nodestream=!!t("readable-stream").Readable}catch{i.nodestream=!1}},{"readable-stream":16}],31:[function(t,n,i){"use strict";for(var a=t("./utils"),o=t("./support"),l=t("./nodejsUtils"),c=t("./stream/GenericWorker"),s=new Array(256),u=0;u<256;u++)s[u]=252<=u?6:248<=u?5:240<=u?4:224<=u?3:192<=u?2:1;s[254]=s[254]=1;function d(){c.call(this,"utf-8 decode"),this.leftOver=null}function A(){c.call(this,"utf-8 encode")}i.utf8encode=function(f){return o.nodebuffer?l.newBufferFrom(f,"utf-8"):(function(p){var g,y,v,b,x,P=p.length,F=0;for(b=0;b>>6:(y<65536?g[x++]=224|y>>>12:(g[x++]=240|y>>>18,g[x++]=128|y>>>12&63),g[x++]=128|y>>>6&63),g[x++]=128|63&y);return g})(f)},i.utf8decode=function(f){return o.nodebuffer?a.transformTo("nodebuffer",f).toString("utf-8"):(function(p){var g,y,v,b,x=p.length,P=new Array(2*x);for(g=y=0;g>10&1023,P[y++]=56320|1023&v)}return P.length!==y&&(P.subarray?P=P.subarray(0,y):P.length=y),a.applyFromCharCode(P)})(f=a.transformTo(o.uint8array?"uint8array":"array",f))},a.inherits(d,c),d.prototype.processChunk=function(f){var p=a.transformTo(o.uint8array?"uint8array":"array",f.data);if(this.leftOver&&this.leftOver.length){if(o.uint8array){var g=p;(p=new Uint8Array(g.length+this.leftOver.length)).set(this.leftOver,0),p.set(g,this.leftOver.length)}else p=this.leftOver.concat(p);this.leftOver=null}var y=(function(b,x){var P;for((x=x||b.length)>b.length&&(x=b.length),P=x-1;0<=P&&(192&b[P])==128;)P--;return P<0||P===0?x:P+s[b[P]]>x?P:x})(p),v=p;y!==p.length&&(o.uint8array?(v=p.subarray(0,y),this.leftOver=p.subarray(y,p.length)):(v=p.slice(0,y),this.leftOver=p.slice(y,p.length))),this.push({data:i.utf8decode(v),meta:f.meta})},d.prototype.flush=function(){this.leftOver&&this.leftOver.length&&(this.push({data:i.utf8decode(this.leftOver),meta:{}}),this.leftOver=null)},i.Utf8DecodeWorker=d,a.inherits(A,c),A.prototype.processChunk=function(f){this.push({data:i.utf8encode(f.data),meta:f.meta})},i.Utf8EncodeWorker=A},{"./nodejsUtils":14,"./stream/GenericWorker":28,"./support":30,"./utils":32}],32:[function(t,n,i){"use strict";var a=t("./support"),o=t("./base64"),l=t("./nodejsUtils"),c=t("./external");function s(g){return g}function u(g,y){for(var v=0;v>8;this.dir=!!(16&this.externalFileAttributes),f==0&&(this.dosPermissions=63&this.externalFileAttributes),f==3&&(this.unixPermissions=this.externalFileAttributes>>16&65535),this.dir||this.fileNameStr.slice(-1)!=="/"||(this.dir=!0)},parseZIP64ExtraField:function(){if(this.extraFields[1]){var f=a(this.extraFields[1].value);this.uncompressedSize===o.MAX_VALUE_32BITS&&(this.uncompressedSize=f.readInt(8)),this.compressedSize===o.MAX_VALUE_32BITS&&(this.compressedSize=f.readInt(8)),this.localHeaderOffset===o.MAX_VALUE_32BITS&&(this.localHeaderOffset=f.readInt(8)),this.diskNumberStart===o.MAX_VALUE_32BITS&&(this.diskNumberStart=f.readInt(4))}},readExtraFields:function(f){var p,g,y,v=f.index+this.extraFieldsLength;for(this.extraFields||(this.extraFields={});f.index+4>>6:(f<65536?A[y++]=224|f>>>12:(A[y++]=240|f>>>18,A[y++]=128|f>>>12&63),A[y++]=128|f>>>6&63),A[y++]=128|63&f);return A},i.buf2binstring=function(d){return u(d,d.length)},i.binstring2buf=function(d){for(var A=new a.Buf8(d.length),f=0,p=A.length;f>10&1023,b[p++]=56320|1023&g)}return u(b,p)},i.utf8border=function(d,A){var f;for((A=A||d.length)>d.length&&(A=d.length),f=A-1;0<=f&&(192&d[f])==128;)f--;return f<0||f===0?A:f+c[d[f]]>A?f:A}},{"./common":41}],43:[function(t,n,i){"use strict";n.exports=function(a,o,l,c){for(var s=65535&a|0,u=a>>>16&65535|0,d=0;l!==0;){for(l-=d=2e3>>1:o>>>1;l[c]=o}return l})();n.exports=function(o,l,c,s){var u=a,d=s+c;o^=-1;for(var A=s;A>>8^u[255&(o^l[A])];return-1^o}},{}],46:[function(t,n,i){"use strict";var a,o=t("../utils/common"),l=t("./trees"),c=t("./adler32"),s=t("./crc32"),u=t("./messages"),d=0,A=4,f=0,p=-2,g=-1,y=4,v=2,b=8,x=9,P=286,F=30,R=19,E=2*P+1,B=15,T=3,W=258,H=W+T+1,C=42,I=113,m=1,N=2,ne=3,q=4;function se(h,U){return h.msg=u[U],U}function Z(h){return(h<<1)-(4h.avail_out&&(z=h.avail_out),z!==0&&(o.arraySet(h.output,U.pending_buf,U.pending_out,z,h.next_out),h.next_out+=z,U.pending_out+=z,h.total_out+=z,h.avail_out-=z,U.pending-=z,U.pending===0&&(U.pending_out=0))}function _(h,U){l._tr_flush_block(h,0<=h.block_start?h.block_start:-1,h.strstart-h.block_start,U),h.block_start=h.strstart,j(h.strm)}function fe(h,U){h.pending_buf[h.pending++]=U}function ce(h,U){h.pending_buf[h.pending++]=U>>>8&255,h.pending_buf[h.pending++]=255&U}function ie(h,U){var z,k,w=h.max_chain_length,D=h.strstart,K=h.prev_length,Y=h.nice_match,M=h.strstart>h.w_size-H?h.strstart-(h.w_size-H):0,J=h.window,ae=h.w_mask,ee=h.prev,pe=h.strstart+W,Ee=J[D+K-1],ve=J[D+K];h.prev_length>=h.good_match&&(w>>=2),Y>h.lookahead&&(Y=h.lookahead);do if(J[(z=U)+K]===ve&&J[z+K-1]===Ee&&J[z]===J[D]&&J[++z]===J[D+1]){D+=2,z++;do;while(J[++D]===J[++z]&&J[++D]===J[++z]&&J[++D]===J[++z]&&J[++D]===J[++z]&&J[++D]===J[++z]&&J[++D]===J[++z]&&J[++D]===J[++z]&&J[++D]===J[++z]&&DM&&--w!=0);return K<=h.lookahead?K:h.lookahead}function be(h){var U,z,k,w,D,K,Y,M,J,ae,ee=h.w_size;do{if(w=h.window_size-h.lookahead-h.strstart,h.strstart>=ee+(ee-H)){for(o.arraySet(h.window,h.window,ee,ee,0),h.match_start-=ee,h.strstart-=ee,h.block_start-=ee,U=z=h.hash_size;k=h.head[--U],h.head[U]=ee<=k?k-ee:0,--z;);for(U=z=ee;k=h.prev[--U],h.prev[U]=ee<=k?k-ee:0,--z;);w+=ee}if(h.strm.avail_in===0)break;if(K=h.strm,Y=h.window,M=h.strstart+h.lookahead,J=w,ae=void 0,ae=K.avail_in,J=T)for(D=h.strstart-h.insert,h.ins_h=h.window[D],h.ins_h=(h.ins_h<=T&&(h.ins_h=(h.ins_h<=T)if(k=l._tr_tally(h,h.strstart-h.match_start,h.match_length-T),h.lookahead-=h.match_length,h.match_length<=h.max_lazy_match&&h.lookahead>=T){for(h.match_length--;h.strstart++,h.ins_h=(h.ins_h<=T&&(h.ins_h=(h.ins_h<=T&&h.match_length<=h.prev_length){for(w=h.strstart+h.lookahead-T,k=l._tr_tally(h,h.strstart-1-h.prev_match,h.prev_length-T),h.lookahead-=h.prev_length-1,h.prev_length-=2;++h.strstart<=w&&(h.ins_h=(h.ins_h<h.pending_buf_size-5&&(z=h.pending_buf_size-5);;){if(h.lookahead<=1){if(be(h),h.lookahead===0&&U===d)return m;if(h.lookahead===0)break}h.strstart+=h.lookahead,h.lookahead=0;var k=h.block_start+z;if((h.strstart===0||h.strstart>=k)&&(h.lookahead=h.strstart-k,h.strstart=k,_(h,!1),h.strm.avail_out===0)||h.strstart-h.block_start>=h.w_size-H&&(_(h,!1),h.strm.avail_out===0))return m}return h.insert=0,U===A?(_(h,!0),h.strm.avail_out===0?ne:q):(h.strstart>h.block_start&&(_(h,!1),h.strm.avail_out),m)}),new xe(4,4,8,4,Re),new xe(4,5,16,8,Re),new xe(4,6,32,32,Re),new xe(4,4,16,16,ke),new xe(8,16,32,32,ke),new xe(8,16,128,128,ke),new xe(8,32,128,256,ke),new xe(32,128,258,1024,ke),new xe(32,258,258,4096,ke)],i.deflateInit=function(h,U){return Be(h,U,b,15,8,0)},i.deflateInit2=Be,i.deflateReset=tt,i.deflateResetKeep=we,i.deflateSetHeader=function(h,U){return h&&h.state?h.state.wrap!==2?p:(h.state.gzhead=U,f):p},i.deflate=function(h,U){var z,k,w,D;if(!h||!h.state||5>8&255),fe(k,k.gzhead.time>>16&255),fe(k,k.gzhead.time>>24&255),fe(k,k.level===9?2:2<=k.strategy||k.level<2?4:0),fe(k,255&k.gzhead.os),k.gzhead.extra&&k.gzhead.extra.length&&(fe(k,255&k.gzhead.extra.length),fe(k,k.gzhead.extra.length>>8&255)),k.gzhead.hcrc&&(h.adler=s(h.adler,k.pending_buf,k.pending,0)),k.gzindex=0,k.status=69):(fe(k,0),fe(k,0),fe(k,0),fe(k,0),fe(k,0),fe(k,k.level===9?2:2<=k.strategy||k.level<2?4:0),fe(k,3),k.status=I);else{var K=b+(k.w_bits-8<<4)<<8;K|=(2<=k.strategy||k.level<2?0:k.level<6?1:k.level===6?2:3)<<6,k.strstart!==0&&(K|=32),K+=31-K%31,k.status=I,ce(k,K),k.strstart!==0&&(ce(k,h.adler>>>16),ce(k,65535&h.adler)),h.adler=1}if(k.status===69)if(k.gzhead.extra){for(w=k.pending;k.gzindex<(65535&k.gzhead.extra.length)&&(k.pending!==k.pending_buf_size||(k.gzhead.hcrc&&k.pending>w&&(h.adler=s(h.adler,k.pending_buf,k.pending-w,w)),j(h),w=k.pending,k.pending!==k.pending_buf_size));)fe(k,255&k.gzhead.extra[k.gzindex]),k.gzindex++;k.gzhead.hcrc&&k.pending>w&&(h.adler=s(h.adler,k.pending_buf,k.pending-w,w)),k.gzindex===k.gzhead.extra.length&&(k.gzindex=0,k.status=73)}else k.status=73;if(k.status===73)if(k.gzhead.name){w=k.pending;do{if(k.pending===k.pending_buf_size&&(k.gzhead.hcrc&&k.pending>w&&(h.adler=s(h.adler,k.pending_buf,k.pending-w,w)),j(h),w=k.pending,k.pending===k.pending_buf_size)){D=1;break}D=k.gzindexw&&(h.adler=s(h.adler,k.pending_buf,k.pending-w,w)),D===0&&(k.gzindex=0,k.status=91)}else k.status=91;if(k.status===91)if(k.gzhead.comment){w=k.pending;do{if(k.pending===k.pending_buf_size&&(k.gzhead.hcrc&&k.pending>w&&(h.adler=s(h.adler,k.pending_buf,k.pending-w,w)),j(h),w=k.pending,k.pending===k.pending_buf_size)){D=1;break}D=k.gzindexw&&(h.adler=s(h.adler,k.pending_buf,k.pending-w,w)),D===0&&(k.status=103)}else k.status=103;if(k.status===103&&(k.gzhead.hcrc?(k.pending+2>k.pending_buf_size&&j(h),k.pending+2<=k.pending_buf_size&&(fe(k,255&h.adler),fe(k,h.adler>>8&255),h.adler=0,k.status=I)):k.status=I),k.pending!==0){if(j(h),h.avail_out===0)return k.last_flush=-1,f}else if(h.avail_in===0&&Z(U)<=Z(z)&&U!==A)return se(h,-5);if(k.status===666&&h.avail_in!==0)return se(h,-5);if(h.avail_in!==0||k.lookahead!==0||U!==d&&k.status!==666){var Y=k.strategy===2?(function(M,J){for(var ae;;){if(M.lookahead===0&&(be(M),M.lookahead===0)){if(J===d)return m;break}if(M.match_length=0,ae=l._tr_tally(M,0,M.window[M.strstart]),M.lookahead--,M.strstart++,ae&&(_(M,!1),M.strm.avail_out===0))return m}return M.insert=0,J===A?(_(M,!0),M.strm.avail_out===0?ne:q):M.last_lit&&(_(M,!1),M.strm.avail_out===0)?m:N})(k,U):k.strategy===3?(function(M,J){for(var ae,ee,pe,Ee,ve=M.window;;){if(M.lookahead<=W){if(be(M),M.lookahead<=W&&J===d)return m;if(M.lookahead===0)break}if(M.match_length=0,M.lookahead>=T&&0M.lookahead&&(M.match_length=M.lookahead)}if(M.match_length>=T?(ae=l._tr_tally(M,1,M.match_length-T),M.lookahead-=M.match_length,M.strstart+=M.match_length,M.match_length=0):(ae=l._tr_tally(M,0,M.window[M.strstart]),M.lookahead--,M.strstart++),ae&&(_(M,!1),M.strm.avail_out===0))return m}return M.insert=0,J===A?(_(M,!0),M.strm.avail_out===0?ne:q):M.last_lit&&(_(M,!1),M.strm.avail_out===0)?m:N})(k,U):a[k.level].func(k,U);if(Y!==ne&&Y!==q||(k.status=666),Y===m||Y===ne)return h.avail_out===0&&(k.last_flush=-1),f;if(Y===N&&(U===1?l._tr_align(k):U!==5&&(l._tr_stored_block(k,0,0,!1),U===3&&(ue(k.head),k.lookahead===0&&(k.strstart=0,k.block_start=0,k.insert=0))),j(h),h.avail_out===0))return k.last_flush=-1,f}return U!==A?f:k.wrap<=0?1:(k.wrap===2?(fe(k,255&h.adler),fe(k,h.adler>>8&255),fe(k,h.adler>>16&255),fe(k,h.adler>>24&255),fe(k,255&h.total_in),fe(k,h.total_in>>8&255),fe(k,h.total_in>>16&255),fe(k,h.total_in>>24&255)):(ce(k,h.adler>>>16),ce(k,65535&h.adler)),j(h),0=z.w_size&&(D===0&&(ue(z.head),z.strstart=0,z.block_start=0,z.insert=0),J=new o.Buf8(z.w_size),o.arraySet(J,U,ae-z.w_size,z.w_size,0),U=J,ae=z.w_size),K=h.avail_in,Y=h.next_in,M=h.input,h.avail_in=ae,h.next_in=0,h.input=U,be(z);z.lookahead>=T;){for(k=z.strstart,w=z.lookahead-(T-1);z.ins_h=(z.ins_h<>>=T=B>>>24,x-=T,(T=B>>>16&255)===0)N[u++]=65535&B;else{if(!(16&T)){if((64&T)==0){B=P[(65535&B)+(b&(1<>>=T,x-=T),x<15&&(b+=m[c++]<>>=T=B>>>24,x-=T,!(16&(T=B>>>16&255))){if((64&T)==0){B=F[(65535&B)+(b&(1<>>=T,x-=T,(T=u-d)>3,b&=(1<<(x-=W<<3))-1,a.next_in=c,a.next_out=u,a.avail_in=c>>24&255)+(C>>>8&65280)+((65280&C)<<8)+((255&C)<<24)}function b(){this.mode=0,this.last=!1,this.wrap=0,this.havedict=!1,this.flags=0,this.dmax=0,this.check=0,this.total=0,this.head=null,this.wbits=0,this.wsize=0,this.whave=0,this.wnext=0,this.window=null,this.hold=0,this.bits=0,this.length=0,this.offset=0,this.extra=0,this.lencode=null,this.distcode=null,this.lenbits=0,this.distbits=0,this.ncode=0,this.nlen=0,this.ndist=0,this.have=0,this.next=null,this.lens=new a.Buf16(320),this.work=new a.Buf16(288),this.lendyn=null,this.distdyn=null,this.sane=0,this.back=0,this.was=0}function x(C){var I;return C&&C.state?(I=C.state,C.total_in=C.total_out=I.total=0,C.msg="",I.wrap&&(C.adler=1&I.wrap),I.mode=p,I.last=0,I.havedict=0,I.dmax=32768,I.head=null,I.hold=0,I.bits=0,I.lencode=I.lendyn=new a.Buf32(g),I.distcode=I.distdyn=new a.Buf32(y),I.sane=1,I.back=-1,A):f}function P(C){var I;return C&&C.state?((I=C.state).wsize=0,I.whave=0,I.wnext=0,x(C)):f}function F(C,I){var m,N;return C&&C.state?(N=C.state,I<0?(m=0,I=-I):(m=1+(I>>4),I<48&&(I&=15)),I&&(I<8||15=q.wsize?(a.arraySet(q.window,I,m-q.wsize,q.wsize,0),q.wnext=0,q.whave=q.wsize):(N<(ne=q.wsize-q.wnext)&&(ne=N),a.arraySet(q.window,I,m-N,ne,q.wnext),(N-=ne)?(a.arraySet(q.window,I,m-N,N,0),q.wnext=N,q.whave=q.wsize):(q.wnext+=ne,q.wnext===q.wsize&&(q.wnext=0),q.whave>>8&255,m.check=l(m.check,D,2,0),_=j=0,m.mode=2;break}if(m.flags=0,m.head&&(m.head.done=!1),!(1&m.wrap)||(((255&j)<<8)+(j>>8))%31){C.msg="incorrect header check",m.mode=30;break}if((15&j)!=8){C.msg="unknown compression method",m.mode=30;break}if(_-=4,h=8+(15&(j>>>=4)),m.wbits===0)m.wbits=h;else if(h>m.wbits){C.msg="invalid window size",m.mode=30;break}m.dmax=1<>8&1),512&m.flags&&(D[0]=255&j,D[1]=j>>>8&255,m.check=l(m.check,D,2,0)),_=j=0,m.mode=3;case 3:for(;_<32;){if(Z===0)break e;Z--,j+=N[q++]<<_,_+=8}m.head&&(m.head.time=j),512&m.flags&&(D[0]=255&j,D[1]=j>>>8&255,D[2]=j>>>16&255,D[3]=j>>>24&255,m.check=l(m.check,D,4,0)),_=j=0,m.mode=4;case 4:for(;_<16;){if(Z===0)break e;Z--,j+=N[q++]<<_,_+=8}m.head&&(m.head.xflags=255&j,m.head.os=j>>8),512&m.flags&&(D[0]=255&j,D[1]=j>>>8&255,m.check=l(m.check,D,2,0)),_=j=0,m.mode=5;case 5:if(1024&m.flags){for(;_<16;){if(Z===0)break e;Z--,j+=N[q++]<<_,_+=8}m.length=j,m.head&&(m.head.extra_len=j),512&m.flags&&(D[0]=255&j,D[1]=j>>>8&255,m.check=l(m.check,D,2,0)),_=j=0}else m.head&&(m.head.extra=null);m.mode=6;case 6:if(1024&m.flags&&(Z<(ie=m.length)&&(ie=Z),ie&&(m.head&&(h=m.head.extra_len-m.length,m.head.extra||(m.head.extra=new Array(m.head.extra_len)),a.arraySet(m.head.extra,N,q,ie,h)),512&m.flags&&(m.check=l(m.check,N,ie,q)),Z-=ie,q+=ie,m.length-=ie),m.length))break e;m.length=0,m.mode=7;case 7:if(2048&m.flags){if(Z===0)break e;for(ie=0;h=N[q+ie++],m.head&&h&&m.length<65536&&(m.head.name+=String.fromCharCode(h)),h&&ie>9&1,m.head.done=!0),C.adler=m.check=0,m.mode=12;break;case 10:for(;_<32;){if(Z===0)break e;Z--,j+=N[q++]<<_,_+=8}C.adler=m.check=v(j),_=j=0,m.mode=11;case 11:if(m.havedict===0)return C.next_out=se,C.avail_out=ue,C.next_in=q,C.avail_in=Z,m.hold=j,m.bits=_,2;C.adler=m.check=1,m.mode=12;case 12:if(I===5||I===6)break e;case 13:if(m.last){j>>>=7&_,_-=7&_,m.mode=27;break}for(;_<3;){if(Z===0)break e;Z--,j+=N[q++]<<_,_+=8}switch(m.last=1&j,_-=1,3&(j>>>=1)){case 0:m.mode=14;break;case 1:if(W(m),m.mode=20,I!==6)break;j>>>=2,_-=2;break e;case 2:m.mode=17;break;case 3:C.msg="invalid block type",m.mode=30}j>>>=2,_-=2;break;case 14:for(j>>>=7&_,_-=7&_;_<32;){if(Z===0)break e;Z--,j+=N[q++]<<_,_+=8}if((65535&j)!=(j>>>16^65535)){C.msg="invalid stored block lengths",m.mode=30;break}if(m.length=65535&j,_=j=0,m.mode=15,I===6)break e;case 15:m.mode=16;case 16:if(ie=m.length){if(Z>>=5,_-=5,m.ndist=1+(31&j),j>>>=5,_-=5,m.ncode=4+(15&j),j>>>=4,_-=4,286>>=3,_-=3}for(;m.have<19;)m.lens[K[m.have++]]=0;if(m.lencode=m.lendyn,m.lenbits=7,z={bits:m.lenbits},U=s(0,m.lens,0,19,m.lencode,0,m.work,z),m.lenbits=z.bits,U){C.msg="invalid code lengths set",m.mode=30;break}m.have=0,m.mode=19;case 19:for(;m.have>>16&255,Oe=65535&w,!((ke=w>>>24)<=_);){if(Z===0)break e;Z--,j+=N[q++]<<_,_+=8}if(Oe<16)j>>>=ke,_-=ke,m.lens[m.have++]=Oe;else{if(Oe===16){for(k=ke+2;_>>=ke,_-=ke,m.have===0){C.msg="invalid bit length repeat",m.mode=30;break}h=m.lens[m.have-1],ie=3+(3&j),j>>>=2,_-=2}else if(Oe===17){for(k=ke+3;_>>=ke)),j>>>=3,_-=3}else{for(k=ke+7;_>>=ke)),j>>>=7,_-=7}if(m.have+ie>m.nlen+m.ndist){C.msg="invalid bit length repeat",m.mode=30;break}for(;ie--;)m.lens[m.have++]=h}}if(m.mode===30)break;if(m.lens[256]===0){C.msg="invalid code -- missing end-of-block",m.mode=30;break}if(m.lenbits=9,z={bits:m.lenbits},U=s(u,m.lens,0,m.nlen,m.lencode,0,m.work,z),m.lenbits=z.bits,U){C.msg="invalid literal/lengths set",m.mode=30;break}if(m.distbits=6,m.distcode=m.distdyn,z={bits:m.distbits},U=s(d,m.lens,m.nlen,m.ndist,m.distcode,0,m.work,z),m.distbits=z.bits,U){C.msg="invalid distances set",m.mode=30;break}if(m.mode=20,I===6)break e;case 20:m.mode=21;case 21:if(6<=Z&&258<=ue){C.next_out=se,C.avail_out=ue,C.next_in=q,C.avail_in=Z,m.hold=j,m.bits=_,c(C,ce),se=C.next_out,ne=C.output,ue=C.avail_out,q=C.next_in,N=C.input,Z=C.avail_in,j=m.hold,_=m.bits,m.mode===12&&(m.back=-1);break}for(m.back=0;xe=(w=m.lencode[j&(1<>>16&255,Oe=65535&w,!((ke=w>>>24)<=_);){if(Z===0)break e;Z--,j+=N[q++]<<_,_+=8}if(xe&&(240&xe)==0){for(we=ke,tt=xe,Be=Oe;xe=(w=m.lencode[Be+((j&(1<>we)])>>>16&255,Oe=65535&w,!(we+(ke=w>>>24)<=_);){if(Z===0)break e;Z--,j+=N[q++]<<_,_+=8}j>>>=we,_-=we,m.back+=we}if(j>>>=ke,_-=ke,m.back+=ke,m.length=Oe,xe===0){m.mode=26;break}if(32&xe){m.back=-1,m.mode=12;break}if(64&xe){C.msg="invalid literal/length code",m.mode=30;break}m.extra=15&xe,m.mode=22;case 22:if(m.extra){for(k=m.extra;_>>=m.extra,_-=m.extra,m.back+=m.extra}m.was=m.length,m.mode=23;case 23:for(;xe=(w=m.distcode[j&(1<>>16&255,Oe=65535&w,!((ke=w>>>24)<=_);){if(Z===0)break e;Z--,j+=N[q++]<<_,_+=8}if((240&xe)==0){for(we=ke,tt=xe,Be=Oe;xe=(w=m.distcode[Be+((j&(1<>we)])>>>16&255,Oe=65535&w,!(we+(ke=w>>>24)<=_);){if(Z===0)break e;Z--,j+=N[q++]<<_,_+=8}j>>>=we,_-=we,m.back+=we}if(j>>>=ke,_-=ke,m.back+=ke,64&xe){C.msg="invalid distance code",m.mode=30;break}m.offset=Oe,m.extra=15&xe,m.mode=24;case 24:if(m.extra){for(k=m.extra;_>>=m.extra,_-=m.extra,m.back+=m.extra}if(m.offset>m.dmax){C.msg="invalid distance too far back",m.mode=30;break}m.mode=25;case 25:if(ue===0)break e;if(ie=ce-ue,m.offset>ie){if((ie=m.offset-ie)>m.whave&&m.sane){C.msg="invalid distance too far back",m.mode=30;break}be=ie>m.wnext?(ie-=m.wnext,m.wsize-ie):m.wnext-ie,ie>m.length&&(ie=m.length),Re=m.window}else Re=ne,be=se-m.offset,ie=m.length;for(ueE?(T=be[Re+y[I]],_[fe+y[I]]):(T=96,0),b=1<>se)+(x-=b)]=B<<24|T<<16|W|0,x!==0;);for(b=1<>=1;if(b!==0?(j&=b-1,j+=b):j=0,I++,--ce[C]==0){if(C===N)break;C=d[A+y[I]]}if(ne>>7)]}function fe(w,D){w.pending_buf[w.pending++]=255&D,w.pending_buf[w.pending++]=D>>>8&255}function ce(w,D,K){w.bi_valid>v-K?(w.bi_buf|=D<>v-w.bi_valid,w.bi_valid+=K-v):(w.bi_buf|=D<>>=1,K<<=1,0<--D;);return K>>>1}function Re(w,D,K){var Y,M,J=new Array(y+1),ae=0;for(Y=1;Y<=y;Y++)J[Y]=ae=ae+K[Y-1]<<1;for(M=0;M<=D;M++){var ee=w[2*M+1];ee!==0&&(w[2*M]=be(J[ee]++,ee))}}function ke(w){var D;for(D=0;D>1;1<=K;K--)we(w,J,K);for(M=pe;K=w.heap[1],w.heap[1]=w.heap[w.heap_len--],we(w,J,1),Y=w.heap[1],w.heap[--w.heap_max]=K,w.heap[--w.heap_max]=Y,J[2*M]=J[2*K]+J[2*Y],w.depth[M]=(w.depth[K]>=w.depth[Y]?w.depth[K]:w.depth[Y])+1,J[2*K+1]=J[2*Y+1]=M,w.heap[1]=M++,we(w,J,1),2<=w.heap_len;);w.heap[--w.heap_max]=w.heap[1],(function(ve,Ie){var rt,Me,G,oe,me,De,te=Ie.dyn_tree,ge=Ie.max_code,L=Ie.stat_desc.static_tree,ye=Ie.stat_desc.has_stree,Ge=Ie.stat_desc.extra_bits,S=Ie.stat_desc.extra_base,de=Ie.stat_desc.max_length,Ae=0;for(oe=0;oe<=y;oe++)ve.bl_count[oe]=0;for(te[2*ve.heap[ve.heap_max]+1]=0,rt=ve.heap_max+1;rt>=7;M>>=1)if(1&Ee&&ee.dyn_ltree[2*pe]!==0)return o;if(ee.dyn_ltree[18]!==0||ee.dyn_ltree[20]!==0||ee.dyn_ltree[26]!==0)return l;for(pe=32;pe>>3,(J=w.static_len+3+7>>>3)<=M&&(M=J)):M=J=K+5,K+4<=M&&D!==-1?k(w,D,K,Y):w.strategy===4||J===M?(ce(w,2+(Y?1:0),3),tt(w,H,C)):(ce(w,4+(Y?1:0),3),(function(ee,pe,Ee,ve){var Ie;for(ce(ee,pe-257,5),ce(ee,Ee-1,5),ce(ee,ve-4,4),Ie=0;Ie>>8&255,w.pending_buf[w.d_buf+2*w.last_lit+1]=255&D,w.pending_buf[w.l_buf+w.last_lit]=255&K,w.last_lit++,D===0?w.dyn_ltree[2*K]++:(w.matches++,D--,w.dyn_ltree[2*(m[K]+d+1)]++,w.dyn_dtree[2*_(D)]++),w.last_lit===w.lit_bufsize-1},i._tr_align=function(w){ce(w,2,3),ie(w,x,H),(function(D){D.bi_valid===16?(fe(D,D.bi_buf),D.bi_buf=0,D.bi_valid=0):8<=D.bi_valid&&(D.pending_buf[D.pending++]=255&D.bi_buf,D.bi_buf>>=8,D.bi_valid-=8)})(w)}},{"../utils/common":41}],53:[function(t,n,i){"use strict";n.exports=function(){this.input=null,this.next_in=0,this.avail_in=0,this.total_in=0,this.output=null,this.next_out=0,this.avail_out=0,this.total_out=0,this.msg="",this.state=null,this.data_type=2,this.adler=0}},{}],54:[function(t,n,i){(function(a){(function(o,l){"use strict";if(!o.setImmediate){var c,s,u,d,A=1,f={},p=!1,g=o.document,y=Object.getPrototypeOf&&Object.getPrototypeOf(o);y=y&&y.setTimeout?y:o,c={}.toString.call(o.process)==="[object process]"?function(P){process.nextTick(function(){b(P)})}:(function(){if(o.postMessage&&!o.importScripts){var P=!0,F=o.onmessage;return o.onmessage=function(){P=!1},o.postMessage("","*"),o.onmessage=F,P}})()?(d="setImmediate$"+Math.random()+"$",o.addEventListener?o.addEventListener("message",x,!1):o.attachEvent("onmessage",x),function(P){o.postMessage(d+P,"*")}):o.MessageChannel?((u=new MessageChannel).port1.onmessage=function(P){b(P.data)},function(P){u.port2.postMessage(P)}):g&&"onreadystatechange"in g.createElement("script")?(s=g.documentElement,function(P){var F=g.createElement("script");F.onreadystatechange=function(){b(P),F.onreadystatechange=null,s.removeChild(F),F=null},s.appendChild(F)}):function(P){setTimeout(b,0,P)},y.setImmediate=function(P){typeof P!="function"&&(P=new Function(""+P));for(var F=new Array(arguments.length-1),R=0;R"u"?a===void 0?this:a:self)}).call(this,typeof global<"u"?global:typeof self<"u"?self:typeof window<"u"?window:{})},{}]},{},[10])(10)})}}),Mu=Lt({"(disabled):node:fs"(){}}),Ap=Lt({"(disabled):node:https"(){}}),Ns=function(e,r){return Ns=Object.setPrototypeOf||{__proto__:[]}instanceof Array&&function(t,n){t.__proto__=n}||function(t,n){for(var i in n)n.hasOwnProperty(i)&&(t[i]=n[i])},Ns(e,r)};function Te(e,r){Ns(e,r);function t(){this.constructor=e}e.prototype=r===null?Object.create(r):(t.prototype=r.prototype,new t)}var Je=function(){return Je=Object.assign||function(r){for(var t,n=1,i=arguments.length;n0&&a[a.length-1])&&(s[0]===6||s[0]===2)){t=0;continue}if(s[0]===3&&(!a||s[1]>a[0]&&s[1]>2],r+=ii[(e[n]&3)<<4|e[n+1]>>4],r+=ii[(e[n+1]&15)<<2|e[n+2]>>6],r+=ii[e[n+2]&63];return t%3===2?r=r.substring(0,r.length-1)+"=":t%3===1&&(r=r.substring(0,r.length-2)+"=="),r},qc=function(e){var r=e.length*.75,t=e.length,n,i=0,a,o,l,c;e[e.length-1]==="="&&(r--,e[e.length-2]==="="&&r--);var s=new Uint8Array(r);for(n=0;n>4,s[i++]=(o&15)<<4|l>>2,s[i++]=(l&3)<<6|c&63;return s},vp=/^(data)?:?([\w\/\+]+)?;?(charset=[\w-]+|base64)?.*,/i,yp=function(e){var r=e.trim(),t=r.substring(0,100),n=t.match(vp);if(!n)return qc(r);var i=n[0],a=r.substring(i.length);return qc(a)},Ye=function(e){return e.charCodeAt(0)},bp=function(e){return e.codePointAt(0)},ia=function(e,r){return Er(e.toString(16),r,"0").toUpperCase()},So=function(e){return ia(e,2)},Xr=function(e){return String.fromCharCode(e)},wp=function(e){return Xr(parseInt(e,16))},Er=function(e,r,t){for(var n="",i=0,a=r-e.length;i=55296&&t<=56319&&e.length>i&&(n=e.charCodeAt(i),n>=56320&&n<=57343&&(a=2)),[e.slice(r,r+a),a]},kp=function(e){for(var r=[],t=0,n=e.length;tt&&s(),o+=A,l+=f}}return s(),c},Fp=/^D:(\d\d\d\d)(\d\d)?(\d\d)?(\d\d)?(\d\d)?(\d\d)?([+\-Z])?(\d\d)?'?(\d\d)?'?$/,Gu=function(e){var r=e.match(Fp);if(r){var t=r[1],n=r[2],i=n===void 0?"01":n,a=r[3],o=a===void 0?"01":a,l=r[4],c=l===void 0?"00":l,s=r[5],u=s===void 0?"00":s,d=r[6],A=d===void 0?"00":d,f=r[7],p=f===void 0?"Z":f,g=r[8],y=g===void 0?"00":g,v=r[9],b=v===void 0?"00":v,x=p==="Z"?"Z":""+p+y+":"+b,P=new Date(t+"-"+i+"-"+o+"T"+c+":"+u+":"+A+x);return P}},nl=function(e,r){for(var t,n=0,i;n=0&&e<=65535},Op=function(e){return e>=65536&&e<=1114111},qu=function(e){return Math.floor((e-65536)/1024)+55296},Vu=function(e){return(e-65536)%1024+56320},mn;(function(e){e.BigEndian="BigEndian",e.LittleEndian="LittleEndian"})(mn||(mn={}));var Ri="\uFFFD".codePointAt(0),Xu=function(e,r){if(r===void 0&&(r=!0),e.length<=1)return String.fromCodePoint(Ri);for(var t=r?Up(e):mn.BigEndian,n=r?2:0,i=[];e.length-n>=2;){var a=Xc(e[n++],e[n++],t);if(_p(a))if(e.length-n<2)i.push(Ri);else{var o=Xc(e[n++],e[n++],t);Vc(o)?i.push(a,o):i.push(Ri)}else Vc(a)?(n+=2,i.push(Ri)):i.push(a)}return n=55296&&e<=56319},Vc=function(e){return e>=56320&&e<=57343},Xc=function(e,r,t){if(t===mn.LittleEndian)return r<<8|e;if(t===mn.BigEndian)return e<<8|r;throw new Error("Invalid byteOrder: "+t)},Up=function(e){return Hu(e)?mn.BigEndian:Ku(e)?mn.LittleEndian:mn.BigEndian},Hu=function(e){return e[0]===254&&e[1]===255},Ku=function(e){return e[0]===255&&e[1]===254},Qu=function(e){return Hu(e)||Ku(e)},Wp=function(e){var r=String(e);if(Math.abs(e)<1){var t=parseInt(e.toString().split("e-")[1]);if(t){var n=e<0;n&&(e*=-1),e*=Math.pow(10,t-1),r="0."+new Array(t).join("0")+e.toString().substring(2),n&&(r="-"+r)}}else{var t=parseInt(e.toString().split("+")[1]);t>20&&(t-=20,e/=Math.pow(10,t),r=e.toString()+new Array(t+1).join("0"))}return r},Za=function(e){return Math.ceil(e.toString(2).length/8)},Jn=function(e){for(var r=new Uint8Array(Za(e)),t=1;t<=r.length;t++)r[t-1]=e>>(r.length-t)*8;return r},oa=function(e){throw new Error(e)},Gp=Qr(xo()),Hc="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/",Oi=new Uint8Array(256);for(Br=0;Br>4,s[i++]=(o&15)<<4|l>>2,s[i++]=(l&3)<<6|c&63;return s},qp=function(e){for(var r="",t=0;tn)throw new Error(It(r)+" must be at least "+t+" and at most "+n+", but was actually "+e)},Tr=function(e,r,t,n){le(e,r,["number","undefined"]),typeof e=="number"&&cr(e,r,t,n)},Ju=function(e,r,t){if(le(e,r,["number"]),e%t!==0)throw new Error(It(r)+" must be a multiple of "+t+", but was actually "+e)},mA=function(e,r){if(!Number.isInteger(e))throw new Error(It(r)+" must be an integer, but was actually "+e)},Co=function(e,r){if(![1,0].includes(Math.sign(e)))throw new Error(It(r)+" must be a positive number or 0, but was actually "+e)},$e=new Uint16Array(256);for(Mt=0;Mt<256;Mt++)$e[Mt]=Mt;var Mt;$e[22]=Ye("");$e[24]=Ye("\u02D8");$e[25]=Ye("\u02C7");$e[26]=Ye("\u02C6");$e[27]=Ye("\u02D9");$e[28]=Ye("\u02DD");$e[29]=Ye("\u02DB");$e[30]=Ye("\u02DA");$e[31]=Ye("\u02DC");$e[127]=Ye("\uFFFD");$e[128]=Ye("\u2022");$e[129]=Ye("\u2020");$e[130]=Ye("\u2021");$e[131]=Ye("\u2026");$e[132]=Ye("\u2014");$e[133]=Ye("\u2013");$e[134]=Ye("\u0192");$e[135]=Ye("\u2044");$e[136]=Ye("\u2039");$e[137]=Ye("\u203A");$e[138]=Ye("\u2212");$e[139]=Ye("\u2030");$e[140]=Ye("\u201E");$e[141]=Ye("\u201C");$e[142]=Ye("\u201D");$e[143]=Ye("\u2018");$e[144]=Ye("\u2019");$e[145]=Ye("\u201A");$e[146]=Ye("\u2122");$e[147]=Ye("\uFB01");$e[148]=Ye("\uFB02");$e[149]=Ye("\u0141");$e[150]=Ye("\u0152");$e[151]=Ye("\u0160");$e[152]=Ye("\u0178");$e[153]=Ye("\u017D");$e[154]=Ye("\u0131");$e[155]=Ye("\u0142");$e[156]=Ye("\u0153");$e[157]=Ye("\u0161");$e[158]=Ye("\u017E");$e[159]=Ye("\uFFFD");$e[160]=Ye("\u20AC");$e[173]=Ye("\uFFFD");var $u=function(e){for(var r=new Array(e.length),t=0,n=e.length;t=Q.ExclamationPoint&&e<=Q.Tilde&&!sl[e]},eu={},tu=new Map,jA=(function(e){Te(r,e);function r(t,n){var i=this;if(t!==eu)throw new il("PDFName");i=e.call(this)||this;for(var a="/",o=0,l=n.length;o=Q.Zero&&s<=Q.Nine||s>=Q.a&&s<=Q.f||s>=Q.A&&s<=Q.F?(n+=c,(n.length===2||!(u>="0"&&u<="9"||u>="a"&&u<="f"||u>="A"&&u<="F"))&&(a(parseInt(n,16)),n="")):a(s):s===Q.Hash?i=!0:a(s)}return new Uint8Array(t)},r.prototype.decodeText=function(){var t=this.asBytes();return String.fromCharCode.apply(String,Array.from(t))},r.prototype.asString=function(){return this.encodedName},r.prototype.value=function(){return this.encodedName},r.prototype.clone=function(){return this},r.prototype.toString=function(){return this.encodedName},r.prototype.sizeInBytes=function(){return this.encodedName.length},r.prototype.copyBytesInto=function(t,n){return n+=Vt(this.encodedName,t,n),this.encodedName.length},r.of=function(t){var n=WA(t),i=tu.get(n);return i||(i=new r(eu,n),tu.set(n,i)),i},r.Length=r.of("Length"),r.FlateDecode=r.of("FlateDecode"),r.Resources=r.of("Resources"),r.Font=r.of("Font"),r.XObject=r.of("XObject"),r.ExtGState=r.of("ExtGState"),r.Contents=r.of("Contents"),r.Type=r.of("Type"),r.Parent=r.of("Parent"),r.MediaBox=r.of("MediaBox"),r.Page=r.of("Page"),r.Annots=r.of("Annots"),r.TrimBox=r.of("TrimBox"),r.ArtBox=r.of("ArtBox"),r.BleedBox=r.of("BleedBox"),r.CropBox=r.of("CropBox"),r.Rotate=r.of("Rotate"),r.Title=r.of("Title"),r.Author=r.of("Author"),r.Subject=r.of("Subject"),r.Creator=r.of("Creator"),r.Keywords=r.of("Keywords"),r.Producer=r.of("Producer"),r.CreationDate=r.of("CreationDate"),r.ModDate=r.of("ModDate"),r})($t),V=jA,qA=(function(e){Te(r,e);function r(){return e!==null&&e.apply(this,arguments)||this}return r.prototype.asNull=function(){return null},r.prototype.clone=function(){return this},r.prototype.toString=function(){return"null"},r.prototype.sizeInBytes=function(){return 4},r.prototype.copyBytesInto=function(t,n){return t[n++]=Q.n,t[n++]=Q.u,t[n++]=Q.l,t[n++]=Q.l,4},r})($t),rr=new qA,VA=(function(e){Te(r,e);function r(t,n){var i=e.call(this)||this;return i.dict=t,i.context=n,i}return r.prototype.keys=function(){return Array.from(this.dict.keys())},r.prototype.values=function(){return Array.from(this.dict.values())},r.prototype.entries=function(){return Array.from(this.dict.entries())},r.prototype.set=function(t,n){this.dict.set(t,n)},r.prototype.get=function(t,n){n===void 0&&(n=!1);var i=this.dict.get(t);if(!(i===rr&&!n))return i},r.prototype.has=function(t){var n=this.dict.get(t);return n!==void 0&&n!==rr},r.prototype.lookupMaybe=function(t){for(var n,i=[],a=1;athis.largestObjectNumber&&(this.largestObjectNumber=r.objectNumber)},e.prototype.nextRef=function(){return this.largestObjectNumber+=1,xt.of(this.largestObjectNumber)},e.prototype.register=function(r){var t=this.nextRef();return this.assign(t,r),t},e.prototype.delete=function(r){return this.indirectObjects.delete(r)},e.prototype.lookupMaybe=function(r){for(var t=[],n=1;nthis.largestObjectNumber&&(this.largestObjectNumber=r.objectNumber)},e.prototype.nextRef=function(){return this.largestObjectNumber+=1,xt.of(this.largestObjectNumber)},e.prototype.register=function(r){var t=this.nextRef();return this.assign(t,r),t},e.prototype.delete=function(r){return this.indirectObjects.delete(r)},e.prototype.lookupMaybe=function(r){for(var t=[],n=1;n1?(this.subsections.push([r]),this.chunkIdx+=1,this.chunkLength=1):(t.push(r),this.chunkLength+=1)},e.create=function(){return new e({ref:xt.of(0,65535),offset:0,deleted:!0})},e.createEmpty=function(){return new e},e})(),td=$A,eg=(function(){function e(r){this.lastXRefOffset=String(r)}return e.prototype.toString=function(){return`startxref +`;for(var a=0,o=i.length;a1?(this.subsections.push([r]),this.chunkIdx+=1,this.chunkLength=1):(t.push(r),this.chunkLength+=1)},e.create=function(){return new e({ref:xt.of(0,65535),offset:0,deleted:!0})},e.createEmpty=function(){return new e},e})(),td=ig,ag=(function(){function e(r){this.lastXRefOffset=String(r)}return e.prototype.toString=function(){return`startxref `+this.lastXRefOffset+` -%%EOF`},e.prototype.sizeInBytes=function(){return 16+this.lastXRefOffset.length},e.prototype.copyBytesInto=function(r,t){var n=t;return r[t++]=Q.s,r[t++]=Q.t,r[t++]=Q.a,r[t++]=Q.r,r[t++]=Q.t,r[t++]=Q.x,r[t++]=Q.r,r[t++]=Q.e,r[t++]=Q.f,r[t++]=Q.Newline,t+=Vt(this.lastXRefOffset,r,t),r[t++]=Q.Newline,r[t++]=Q.Percent,r[t++]=Q.Percent,r[t++]=Q.E,r[t++]=Q.O,r[t++]=Q.F,t-n},e.forLastCrossRefSectionOffset=function(r){return new e(r)},e})(),ll=eg,tg=(function(){function e(r){this.dict=r}return e.prototype.toString=function(){return`trailer -`+this.dict.toString()},e.prototype.sizeInBytes=function(){return 8+this.dict.sizeInBytes()},e.prototype.copyBytesInto=function(r,t){var n=t;return r[t++]=Q.t,r[t++]=Q.r,r[t++]=Q.a,r[t++]=Q.i,r[t++]=Q.l,r[t++]=Q.e,r[t++]=Q.r,r[t++]=Q.Newline,t+=this.dict.copyBytesInto(r,t),t-n},e.of=function(r){return new e(r)},e})(),rg=tg,ng=(function(e){Te(r,e);function r(t,n,i){i===void 0&&(i=!0);var a=e.call(this,t.obj({}),i)||this;return a.objects=n,a.offsets=a.computeObjectOffsets(),a.offsetsString=a.computeOffsetsString(),a.dict.set(X.of("Type"),X.of("ObjStm")),a.dict.set(X.of("N"),Qe.of(a.objects.length)),a.dict.set(X.of("First"),Qe.of(a.offsetsString.length)),a}return r.prototype.getObjectsCount=function(){return this.objects.length},r.prototype.clone=function(t){return r.withContextAndObjects(t||this.dict.context,this.objects.slice(),this.encode)},r.prototype.getContentsString=function(){for(var t=this.offsetsString,n=0,i=this.objects.length;n1&&(o.push(l),o.push(u.ref.objectNumber),l=0),l+=1}return o.push(l),o},a.computeEntryTuples=function(){for(var o=new Array(a.entries.length),l=0,c=a.entries.length;ll[0]&&(l[0]=p),g>l[1]&&(l[1]=g),y>l[2]&&(l[2]=y)}return l},a.entries=n||[],a.entryTuplesCache=Hr.populatedBy(a.computeEntryTuples),a.maxByteWidthsCache=Hr.populatedBy(a.computeMaxEntryByteWidths),a.indexCache=Hr.populatedBy(a.computeIndex),t.set(X.of("Type"),X.of("XRef")),a}return r.prototype.addDeletedEntry=function(t,n){var i=fn.Deleted;this.entries.push({type:i,ref:t,nextFreeObjectNumber:n}),this.entryTuplesCache.invalidate(),this.maxByteWidthsCache.invalidate(),this.indexCache.invalidate(),this.contentsCache.invalidate()},r.prototype.addUncompressedEntry=function(t,n){var i=fn.Uncompressed;this.entries.push({type:i,ref:t,offset:n}),this.entryTuplesCache.invalidate(),this.maxByteWidthsCache.invalidate(),this.indexCache.invalidate(),this.contentsCache.invalidate()},r.prototype.addCompressedEntry=function(t,n,i){var a=fn.Compressed;this.entries.push({type:a,ref:t,objectStreamRef:n,index:i}),this.entryTuplesCache.invalidate(),this.maxByteWidthsCache.invalidate(),this.indexCache.invalidate(),this.contentsCache.invalidate()},r.prototype.clone=function(t){var n=this,i=n.dict,a=n.entries,o=n.encode;return r.of(i.clone(t),a.slice(),o)},r.prototype.getContentsString=function(){for(var t=this.entryTuplesCache.access(),n=this.maxByteWidthsCache.access(),i="",a=0,o=t.length;a=0;p--)i+=(d[p]||0).toString(2);for(var p=n[1]-1;p>=0;p--)i+=(A[p]||0).toString(2);for(var p=n[2]-1;p>=0;p--)i+=(f[p]||0).toString(2)}return i},r.prototype.getUnencodedContents=function(){for(var t=this.entryTuplesCache.access(),n=this.maxByteWidthsCache.access(),i=new Uint8Array(this.getUnencodedContentsSize()),a=0,o=0,l=t.length;o=0;g--)i[a++]=A[g]||0;for(var g=n[1]-1;g>=0;g--)i[a++]=f[g]||0;for(var g=n[2]-1;g>=0;g--)i[a++]=p[g]||0}return i},r.prototype.getUnencodedContentsSize=function(){var t=this.maxByteWidthsCache.access(),n=Fp(t);return n*this.entries.length},r.prototype.updateDict=function(){e.prototype.updateDict.call(this);var t=this.maxByteWidthsCache.access(),n=this.indexCache.access(),i=this.dict.context;this.dict.set(X.of("W"),i.obj(t)),this.dict.set(X.of("Index"),i.obj(n))},r.create=function(t,n){n===void 0&&(n=!0);var i=new r(t,[],n);return i.addDeletedEntry(xt.of(0,65535),0),i},r.of=function(t,n,i){return i===void 0&&(i=!0),new r(t,n,i)},r})(sl),sg=og,lg=(function(e){Te(r,e);function r(t,n,i,a){var o=e.call(this,t,n)||this;return o.encodeStreams=i,o.objectsPerStream=a,o}return r.prototype.computeBufferSize=function(){return it(this,void 0,void 0,function(){var t,n,i,a,o,l,c,s,p,g,u,v,d,A,y,f,p,g,y,v,b,x,P,F;return at(this,function(R){switch(R.label){case 0:t=this.context.largestObjectNumber+1,n=ko.forVersion(1,7),i=n.sizeInBytes()+2,a=sg.create(this.createTrailerDict(),this.encodeStreams),o=[],l=[],c=[],s=this.context.enumerateIndirectObjects(),p=0,g=s.length,R.label=1;case 1:return p"},r.prototype.sizeInBytes=function(){return this.value.length+2},r.prototype.copyBytesInto=function(t,n){return t[n++]=Q.LessThan,n+=Vt(this.value,t,n),t[n++]=Q.GreaterThan,this.value.length+2},r.of=function(t){return new r(t)},r.fromText=function(t){for(var n=Bp(t),i="",a=0,o=n.length;a1&&(o.push(l),o.push(u.ref.objectNumber),l=0),l+=1}return o.push(l),o},a.computeEntryTuples=function(){for(var o=new Array(a.entries.length),l=0,c=a.entries.length;ll[0]&&(l[0]=p),g>l[1]&&(l[1]=g),y>l[2]&&(l[2]=y)}return l},a.entries=n||[],a.entryTuplesCache=Hr.populatedBy(a.computeEntryTuples),a.maxByteWidthsCache=Hr.populatedBy(a.computeMaxEntryByteWidths),a.indexCache=Hr.populatedBy(a.computeIndex),t.set(V.of("Type"),V.of("XRef")),a}return r.prototype.addDeletedEntry=function(t,n){var i=fn.Deleted;this.entries.push({type:i,ref:t,nextFreeObjectNumber:n}),this.entryTuplesCache.invalidate(),this.maxByteWidthsCache.invalidate(),this.indexCache.invalidate(),this.contentsCache.invalidate()},r.prototype.addUncompressedEntry=function(t,n){var i=fn.Uncompressed;this.entries.push({type:i,ref:t,offset:n}),this.entryTuplesCache.invalidate(),this.maxByteWidthsCache.invalidate(),this.indexCache.invalidate(),this.contentsCache.invalidate()},r.prototype.addCompressedEntry=function(t,n,i){var a=fn.Compressed;this.entries.push({type:a,ref:t,objectStreamRef:n,index:i}),this.entryTuplesCache.invalidate(),this.maxByteWidthsCache.invalidate(),this.indexCache.invalidate(),this.contentsCache.invalidate()},r.prototype.clone=function(t){var n=this,i=n.dict,a=n.entries,o=n.encode;return r.of(i.clone(t),a.slice(),o)},r.prototype.getContentsString=function(){for(var t=this.entryTuplesCache.access(),n=this.maxByteWidthsCache.access(),i="",a=0,o=t.length;a=0;p--)i+=(d[p]||0).toString(2);for(var p=n[1]-1;p>=0;p--)i+=(A[p]||0).toString(2);for(var p=n[2]-1;p>=0;p--)i+=(f[p]||0).toString(2)}return i},r.prototype.getUnencodedContents=function(){for(var t=this.entryTuplesCache.access(),n=this.maxByteWidthsCache.access(),i=new Uint8Array(this.getUnencodedContentsSize()),a=0,o=0,l=t.length;o=0;g--)i[a++]=A[g]||0;for(var g=n[1]-1;g>=0;g--)i[a++]=f[g]||0;for(var g=n[2]-1;g>=0;g--)i[a++]=p[g]||0}return i},r.prototype.getUnencodedContentsSize=function(){var t=this.maxByteWidthsCache.access(),n=Rp(t);return n*this.entries.length},r.prototype.updateDict=function(){e.prototype.updateDict.call(this);var t=this.maxByteWidthsCache.access(),n=this.indexCache.access(),i=this.dict.context;this.dict.set(V.of("W"),i.obj(t)),this.dict.set(V.of("Index"),i.obj(n))},r.create=function(t,n){n===void 0&&(n=!0);var i=new r(t,[],n);return i.addDeletedEntry(xt.of(0,65535),0),i},r.of=function(t,n,i){return i===void 0&&(i=!0),new r(t,n,i)},r})(ll),fg=dg,hg=(function(e){Te(r,e);function r(t,n,i,a){var o=e.call(this,t,n)||this;return o.encodeStreams=i,o.objectsPerStream=a,o}return r.prototype.computeBufferSize=function(){return it(this,void 0,void 0,function(){var t,n,i,a,o,l,c,s,p,g,u,v,d,A,y,f,p,g,y,v,b,x,P,F;return at(this,function(R){switch(R.label){case 0:t=this.context.largestObjectNumber+1,n=Po.forVersion(1,7),i=n.sizeInBytes()+2,a=fg.create(this.createTrailerDict(),this.encodeStreams),o=[],l=[],c=[],s=this.context.enumerateIndirectObjects(),p=0,g=s.length,R.label=1;case 1:return p"},r.prototype.sizeInBytes=function(){return this.value.length+2},r.prototype.copyBytesInto=function(t,n){return t[n++]=Q.LessThan,n+=Vt(this.value,t,n),t[n++]=Q.GreaterThan,this.value.length+2},r.of=function(t){return new r(t)},r.fromText=function(t){for(var n=zp(t),i="",a=0,o=n.length;a"},Qa=function(e){return na(e,4)},pg=function(e){if(Rp(e))return Qa(e);if(Lp(e)){var r=qu(e),t=Vu(e);return""+Qa(r)+Qa(t)}var n=wo(e),i="0x"+n+" is not a valid UTF-8 or UTF-16 codepoint.";throw new Error(i)},Ag=function(e){var r=0,t=function(n){r|=1<=Q.Zero&&s<=Q.Seven?(n+=c,(n.length===3||!(u>="0"&&u<="7"))&&(a(parseInt(n,8)),n="")):a(s):s===Q.BackSlash?i=!0:a(s)}return new Uint8Array(t)},r.prototype.decodeText=function(){var t=this.asBytes();return Qu(t)?Xu(t):$u(t)},r.prototype.decodeDate=function(){var t=this.decodeText(),n=Gu(t);if(!n)throw new ed(t);return n},r.prototype.asString=function(){return this.value},r.prototype.clone=function(){return r.of(this.value)},r.prototype.toString=function(){return"("+this.value+")"},r.prototype.sizeInBytes=function(){return this.value.length+2},r.prototype.copyBytesInto=function(t,n){return t[n++]=Q.LeftParen,n+=Vt(this.value,t,n),t[n++]=Q.RightParen,this.value.length+2},r.of=function(t){return new r(t)},r.fromDate=function(t){var n=Br(String(t.getUTCFullYear()),4,"0"),i=Br(String(t.getUTCMonth()+1),2,"0"),a=Br(String(t.getUTCDate()),2,"0"),o=Br(String(t.getUTCHours()),2,"0"),l=Br(String(t.getUTCMinutes()),2,"0"),c=Br(String(t.getUTCSeconds()),2,"0");return new r("D:"+n+i+a+o+l+c+"Z")},r})($t),wt=mg,vg=(function(){function e(r,t,n,i){var a=this;this.allGlyphsInFontSortedById=function(){for(var o=new Array(a.font.characterSet.length),l=0,c=o.length;l>3)]>>7-((p&7)<<0)&1,C=3*H;l[P]=F[C],l[P+1]=F[C+1],l[P+2]=F[C+2],l[P+3]=H>2)]>>6-((p&3)<<1)&3,C=3*H;l[P]=F[C],l[P+1]=F[C+1],l[P+2]=F[C+2],l[P+3]=H>1)]>>4-((p&1)<<2)&15,C=3*H;l[P]=F[C],l[P+1]=F[C+1],l[P+2]=F[C+2],l[P+3]=H>>3)]>>>7-(q&7)&1),se=m==v*255?0:255;c[ne+q]=se<<24|m<<16|m<<8|m}else if(u==2)for(var q=0;q>>2)]>>>6-((q&3)<<1)&3),se=m==v*85?0:255;c[ne+q]=se<<24|m<<16|m<<8|m}else if(u==4)for(var q=0;q>>1)]>>>4-((q&1)<<2)&15),se=m==v*17?0:255;c[ne+q]=se<<24|m<<16|m<<8|m}else if(u==8)for(var q=0;q>>2<<3));i==0;){if(i=y(r,A,1),a=y(r,A+1,2),A+=3,a==0){(A&7)!=0&&(A+=8-(A&7));var B=(A>>>3)+4,T=r[B-4]|r[B-3]<<8;E&&(t=e.H.W(t,d+T)),t.set(new n(r.buffer,r.byteOffset+B,T),d),A=B+T<<3,d+=T;continue}if(E&&(t=e.H.W(t,d+(1<<17))),a==1&&(f=R.J,p=R.h,s=511,u=31),a==2){o=v(r,A,5)+257,l=v(r,A+5,5)+1,c=v(r,A+10,4)+4,A+=14;for(var W=A,H=1,C=0;C<38;C+=2)R.Q[C]=0,R.Q[C+1]=0;for(var C=0;CH&&(H=I)}A+=3*c,x(R.Q,H),P(R.Q,H,R.u),f=R.w,p=R.d,A=b(R.u,(1<>>4;if(!(q>>>8))t[d++]=q;else{if(q==256)break;var se=d+q-254;if(q>264){var Z=R.q[q-257];se=d+(Z>>>3)+v(r,A,Z&7),A+=Z&7}var ue=p[F(r,A)&u];A+=ue&15;var j=ue>>>4,_=R.c[j],fe=(_>>>4)+y(r,A,_&15);for(A+=_&15;d>>4;if(d<=15)o[s]=d,s++;else{var A=0,f=0;d==16?(f=3+l(i,a,2),a+=2,A=o[s-1]):d==17?(f=3+l(i,a,3),a+=3):d==18&&(f=11+l(i,a,7),a+=7);for(var p=s+f;s>>1;oa&&(a=c),o++}for(;o>1,s=r[l+1],u=c<<4|s,d=t-s,A=r[l]<>>15-t;n[p]=u,A++}},e.H.l=function(r,t){for(var n=e.H.m.r,i=15-t,a=0;a>>i}},e.H.M=function(r,t,n){n=n<<(t&7);var i=t>>>3;r[i]|=n,r[i+1]|=n>>>8},e.H.I=function(r,t,n){n=n<<(t&7);var i=t>>>3;r[i]|=n,r[i+1]|=n>>>8,r[i+2]|=n>>>16},e.H.e=function(r,t,n){return(r[t>>>3]|r[(t>>>3)+1]<<8)>>>(t&7)&(1<>>3]|r[(t>>>3)+1]<<8|r[(t>>>3)+2]<<16)>>>(t&7)&(1<>>3]|r[(t>>>3)+1]<<8|r[(t>>>3)+2]<<16)>>>(t&7)},e.H.i=function(r,t){return(r[t>>>3]|r[(t>>>3)+1]<<8|r[(t>>>3)+2]<<16|r[(t>>>3)+3]<<24)>>>(t&7)},e.H.m=(function(){var r=Uint16Array,t=Uint32Array;return{K:new r(16),j:new r(16),X:[16,17,18,0,8,7,9,6,10,5,11,4,12,3,13,2,14,1,15],S:[3,4,5,6,7,8,9,10,11,13,15,17,19,23,27,31,35,43,51,59,67,83,99,115,131,163,195,227,258,999,999,999],T:[0,0,0,0,0,0,0,0,1,1,1,1,2,2,2,2,3,3,3,3,4,4,4,4,5,5,5,5,0,0,0,0],q:new r(32),p:[1,2,3,4,5,7,9,13,17,25,33,49,65,97,129,193,257,385,513,769,1025,1537,2049,3073,4097,6145,8193,12289,16385,24577,65535,65535],z:[0,0,0,0,1,1,2,2,3,3,4,4,5,5,6,6,7,7,8,8,9,9,10,10,11,11,12,12,13,13,0,0],c:new t(32),J:new r(512),_:[],h:new r(32),$:[],w:new r(32768),C:[],v:[],d:new r(32768),D:[],u:new r(512),Q:[],r:new r(32768),s:new t(286),Y:new t(30),a:new t(19),t:new t(15e3),k:new r(65536),g:new r(32768)}})(),(function(){for(var r=e.H.m,t=32768,n=0;n>>1|(i&1431655765)<<1,i=(i&3435973836)>>>2|(i&858993459)<<2,i=(i&4042322160)>>>4|(i&252645135)<<4,i=(i&4278255360)>>>8|(i&16711935)<<8,r.r[n]=(i>>>16|i<<16)>>>17}function a(o,l,c){for(;l--!=0;)o.push(0,c)}for(var n=0;n<32;n++)r.q[n]=r.S[n]<<3|r.T[n],r.c[n]=r.p[n]<<4|r.z[n];a(r._,144,8),a(r._,112,9),a(r._,24,7),a(r._,8,8),e.H.n(r._,9),e.H.A(r._,9,r.J),e.H.l(r._,9),a(r.$,32,5),e.H.n(r.$,5),e.H.A(r.$,5,r.h),e.H.l(r.$,5),a(r.Q,19,0),a(r.C,286,0),a(r.D,30,0),a(r.v,320,0)})(),e.H.N})();Se.decode._readInterlace=function(e,r){for(var t=r.width,n=r.height,i=Se.decode._getBPP(r),a=i>>3,o=Math.ceil(t*i/8),l=new Uint8Array(n*o),c=0,s=[0,0,4,0,2,0,1],u=[0,4,0,2,0,1,0],d=[8,8,8,4,4,2,2],A=[8,8,4,4,2,2,1],f=0;f<7;){for(var p=d[f],g=A[f],y=0,v=0,b=s[f];b>3];T=T>>7-(B&7)&1,l[R*o+(E>>3)]|=T<<7-((E&7)<<0)}if(i==2){var T=e[B>>3];T=T>>6-(B&7)&3,l[R*o+(E>>2)]|=T<<6-((E&3)<<1)}if(i==4){var T=e[B>>3];T=T>>4-(B&7)&15,l[R*o+(E>>1)]|=T<<4-((E&1)<<2)}if(i>=8)for(var W=R*o+E*a,H=0;H>3)+H];B+=i,E+=g}F++,R+=p}y*v!=0&&(c+=v*(1+P)),f=f+1}return l};Se.decode._getBPP=function(e){var r=[1,null,3,1,2,null,4][e.ctype];return r*e.depth};Se.decode._filterZero=function(e,r,t,n,i){var a=Se.decode._getBPP(r),o=Math.ceil(n*a/8),l=Se.decode._paeth;a=Math.ceil(a/8);var c=0,s=1,u=e[t],d=0;if(u>1&&(e[t]=[0,0,1][u-2]),u==3)for(d=a;d>>1)&255;for(var A=0;A>>1);for(;d>>1)}else{for(;d>8&255,e[r+1]=t&255},readUint:function(e,r){return e[r]*(256*256*256)+(e[r+1]<<16|e[r+2]<<8|e[r+3])},writeUint:function(e,r,t){e[r]=t>>24&255,e[r+1]=t>>16&255,e[r+2]=t>>8&255,e[r+3]=t&255},readASCII:function(e,r,t){for(var n="",i=0;i=0&&l>=0?(d=f*r+p<<2,A=(l+f)*i+o+p<<2):(d=(-l+f)*r-o+p<<2,A=f*i+p<<2),c==0)n[A]=e[d],n[A+1]=e[d+1],n[A+2]=e[d+2],n[A+3]=e[d+3];else if(c==1){var g=e[d+3]*.00392156862745098,y=e[d]*g,v=e[d+1]*g,b=e[d+2]*g,x=n[A+3]*(1/255),P=n[A]*x,F=n[A+1]*x,R=n[A+2]*x,E=1-g,B=g+x*E,T=B==0?0:1/B;n[A+3]=255*B,n[A+0]=(y+P*E)*T,n[A+1]=(v+F*E)*T,n[A+2]=(b+R*E)*T}else if(c==2){var g=e[d+3],y=e[d],v=e[d+1],b=e[d+2],x=n[A+3],P=n[A],F=n[A+1],R=n[A+2];g==x&&y==P&&v==F&&b==R?(n[A]=0,n[A+1]=0,n[A+2]=0,n[A+3]=0):(n[A]=y,n[A+1]=v,n[A+2]=b,n[A+3]=g)}else if(c==3){var g=e[d+3],y=e[d],v=e[d+1],b=e[d+2],x=n[A+3],P=n[A],F=n[A+1],R=n[A+2];if(g==x&&y==P&&v==F&&b==R)continue;if(g<220&&x>20)return!1}return!0};Se.encode=function(e,r,t,n,i,a,o){n==null&&(n=0),o==null&&(o=!1);var l=Se.encode.compress(e,r,t,n,[!1,!1,!1,0,o]);return Se.encode.compressPNG(l,-1),Se.encode._main(l,r,t,i,a)};Se.encodeLL=function(e,r,t,n,i,a,o,l){for(var c={ctype:0+(n==1?0:2)+(i==0?0:4),depth:a,frames:[]},s=Date.now(),u=(n+i)*a,d=u*r,A=0;A1,d=!1,A=33+(u?20:0);if(i.sRGB!=null&&(A+=13),i.pHYs!=null&&(A+=21),e.ctype==3){for(var f=e.plte.length,p=0;p>>24!=255&&(d=!0);A+=8+f*3+4+(d?8+f*1+4:0)}for(var g=0;g>>8&255,E=P>>>16&255;v[s+x+0]=F,v[s+x+1]=R,v[s+x+2]=E}if(s+=f*3,o(v,s,a(v,s-f*3-4,f*3+4)),s+=4,d){o(v,s,f),s+=4,c(v,s,"tRNS"),s+=4;for(var p=0;p>>24&255;s+=f,o(v,s,a(v,s-f-4,f+4)),s+=4}}for(var B=0,g=0;g>2,C>>2));for(var f=0;fN&&q==m[y-N])ne[y]=ne[y-N];else{var se=x[q];if(se==null&&(x[q]=se=P.length,P.push(q),P.length>=300))break;ne[y]=se}}}var Z=P.length;Z<=256&&s==!1&&(Z<=2?d=1:Z<=4?d=2:Z<=16?d=4:d=8,d=Math.max(d,c));for(var f=0;f>1)]|=ke[Oe+we]<<4-(we&1)*4;else if(d==2)for(var we=0;we>2)]|=ke[Oe+we]<<6-(we&3)*2;else if(d==1)for(var we=0;we>3)]|=ke[Oe+we]<<7-(we&7)*1}fe=Re,u=3,be=1}else if(v==!1&&b.length==1){for(var Re=new Uint8Array(N*_*3),tt=N*_,y=0;yE&&(E=W),TB&&(B=T))}E==-1&&(F=R=E=B=0),i&&((F&1)==1&&F--,(R&1)==1&&R--);var C=(E-F+1)*(B-R+1);Cy&&(y=P),Fv&&(v=F))}y==-1&&(p=g=y=v=0),o&&((p&1)==1&&p--,(g&1)==1&&g--),a={x:p,y:g,width:y-p+1,height:v-g+1};var B=n[i];B.rect=a,B.blend=1,B.img=new Uint8Array(a.width*a.height*4),n[i-1].dispose==0?(Se._copyTile(s,r,t,B.img,a.width,a.height,-a.x,-a.y,0),Se.encode._prepareDiff(A,r,t,B.img,a)):Se._copyTile(A,r,t,B.img,a.width,a.height,-a.x,-a.y,0)};Se.encode._prepareDiff=function(e,r,t,n,i){Se._copyTile(e,r,t,n,i.width,i.height,-i.x,-i.y,2)};Se.encode._filterZero=function(e,r,t,n,i,a,o){var l=[],c=[0,1,2,3,4];a!=-1?c=[a]:(r*n>5e5||t==1)&&(c=[0]);var s;o&&(s={level:0});for(var u=o&&UZIP!=null?UZIP:Cg.default,d=0;d>1)+256&255;if(a==4)for(var s=i;s>1)&255;for(var s=i;s>1)&255}if(a==4){for(var s=0;s>>1:t=t>>>1;e[r]=t}return e})(),update:function(e,r,t,n){for(var i=0;i>>8;return e},crc:function(e,r,t){return Se.crc.update(4294967295,e,r,t)^4294967295}};Se.quantize=function(e,r){for(var t=new Uint8Array(e),n=t.slice(0),i=new Uint32Array(n.buffer),a=Se.quantize.getKDtree(n,r),o=a[0],l=a[1],c=Se.quantize.planeDst,s=t,u=i,d=s.length,A=new Uint8Array(t.length>>2),f=0;f>2]=b.ind,u[f>>2]=b.est.rgba}return{abuf:n.buffer,inds:A,plte:l}};Se.quantize.getKDtree=function(e,r,t){t==null&&(t=1e-4);var n=new Uint32Array(e.buffer),i={i0:0,i1:e.length,bst:null,est:null,tdst:0,left:null,right:null};i.bst=Se.quantize.stats(e,i.i0,i.i1),i.est=Se.quantize.estats(i.bst);for(var a=[i];a.lengtho&&(o=a[c].est.L,l=c);if(o=u||s.i1<=u;if(d){s.est.L=0;continue}var A={i0:s.i0,i1:u,bst:null,est:null,tdst:0,left:null,right:null};A.bst=Se.quantize.stats(e,A.i0,A.i1),A.est=Se.quantize.estats(A.bst);var f={i0:u,i1:s.i1,bst:null,est:null,tdst:0,left:null,right:null};f.bst={R:[],m:[],N:s.bst.N-A.bst.N};for(var c=0;c<16;c++)f.bst.R[c]=s.bst.R[c]-A.bst.R[c];for(var c=0;c<4;c++)f.bst.m[c]=s.bst.m[c]-A.bst.m[c];f.est=Se.quantize.estats(f.bst),s.left=A,s.right=f,a[l]=A,a.push(f)}a.sort(function(p,g){return g.bst.N-p.bst.N});for(var c=0;c0&&(o=e.right,l=e.left);var c=Se.quantize.getNearest(o,r,t,n,i);if(c.tdst<=a*a)return c;var s=Se.quantize.getNearest(l,r,t,n,i);return s.tdsta;)n-=4;if(t>=n)break;var c=r[t>>2];r[t>>2]=r[n>>2],r[n>>2]=c,t+=4,n-=4}for(;o(e,t,i)>a;)t-=4;return t+4};Se.quantize.vecDot=function(e,r,t){return e[r]*t[0]+e[r+1]*t[1]+e[r+2]*t[2]+e[r+3]*t[3]};Se.quantize.stats=function(e,r,t){for(var n=[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],i=[0,0,0,0],a=t-r>>2,o=r;o>>0}};Se.M4={multVec:function(e,r){return[e[0]*r[0]+e[1]*r[1]+e[2]*r[2]+e[3]*r[3],e[4]*r[0]+e[5]*r[1]+e[6]*r[2]+e[7]*r[3],e[8]*r[0]+e[9]*r[1]+e[10]*r[2]+e[11]*r[3],e[12]*r[0]+e[13]*r[1]+e[14]*r[2]+e[15]*r[3]]},dot:function(e,r){return e[0]*r[0]+e[1]*r[1]+e[2]*r[2]+e[3]*r[3]},sml:function(e,r){return[e*r[0],e*r[1],e*r[2],e*r[3]]}};Se.encode.concatRGBA=function(e){for(var r=0,t=0;t1)throw new Error("Animated PNGs are not supported");var i=new Uint8Array(n[0]),a=Fg(i),o=a.rgbChannel,l=a.alphaChannel;this.rgbChannel=o;var c=l.some(function(s){return s<255});c&&(this.alphaChannel=l),this.type=Pg(t.ctype),this.width=t.width,this.height=t.height,this.bitsPerComponent=8}return e.load=function(r){return new e(r)},e})(),Dg=(function(){function e(r){this.image=r,this.bitsPerComponent=r.bitsPerComponent,this.width=r.width,this.height=r.height,this.colorSpace="DeviceRGB"}return e.for=function(r){return it(this,void 0,void 0,function(){var t;return at(this,function(n){return t=Tg.load(r),[2,new e(t)]})})},e.prototype.embedIntoContext=function(r,t){return it(this,void 0,void 0,function(){var n,i;return at(this,function(a){return n=this.embedAlphaChannel(r),i=r.flateStream(this.image.rgbChannel,{Type:"XObject",Subtype:"Image",BitsPerComponent:this.image.bitsPerComponent,Width:this.image.width,Height:this.image.height,ColorSpace:this.colorSpace,SMask:n}),t?(r.assign(t,i),[2,t]):[2,r.register(i)]})})},e.prototype.embedAlphaChannel=function(r){if(this.image.alphaChannel){var t=r.flateStream(this.image.alphaChannel,{Type:"XObject",Subtype:"Image",Height:this.image.height,Width:this.image.width,BitsPerComponent:this.image.bitsPerComponent,ColorSpace:"DeviceGray",Decode:[0,1]});return r.register(t)}},e})(),od=Dg,Eg=(function(){function e(r,t,n){this.bytes=r,this.start=t||0,this.pos=this.start,this.end=t&&n?t+n:this.bytes.length}return Object.defineProperty(e.prototype,"length",{get:function(){return this.end-this.start},enumerable:!1,configurable:!0}),Object.defineProperty(e.prototype,"isEmpty",{get:function(){return this.length===0},enumerable:!1,configurable:!0}),e.prototype.getByte=function(){return this.pos>=this.end?-1:this.bytes[this.pos++]},e.prototype.getUint16=function(){var r=this.getByte(),t=this.getByte();return r===-1||t===-1?-1:(r<<8)+t},e.prototype.getInt32=function(){var r=this.getByte(),t=this.getByte(),n=this.getByte(),i=this.getByte();return(r<<24)+(t<<16)+(n<<8)+i},e.prototype.getBytes=function(r,t){t===void 0&&(t=!1);var n=this.bytes,i=this.pos,a=this.end;if(r){var l=i+r;l>a&&(l=a),this.pos=l;var o=n.subarray(i,l);return t?new Uint8ClampedArray(o):o}else{var o=n.subarray(i,a);return t?new Uint8ClampedArray(o):o}},e.prototype.peekByte=function(){var r=this.getByte();return this.pos--,r},e.prototype.peekBytes=function(r,t){t===void 0&&(t=!1);var n=this.getBytes(r,t);return this.pos-=n.length,n},e.prototype.skip=function(r){r||(r=1),this.pos+=r},e.prototype.reset=function(){this.pos=this.start},e.prototype.moveStart=function(){this.start=this.pos},e.prototype.makeSubStream=function(r,t){return new e(this.bytes,r,t)},e.prototype.decode=function(){return this.bytes},e})(),sd=Eg,Bg=new Uint8Array(0),Rg=(function(){function e(r){if(this.pos=0,this.bufferLength=0,this.eof=!1,this.buffer=Bg,this.minBufferLength=512,r)for(;this.minBufferLengtha&&(n=a)}else{for(;!this.eof;)this.readBlock();n=this.bufferLength}this.pos=n;var o=this.buffer.subarray(i,n);return t&&!(o instanceof Uint8ClampedArray)?new Uint8ClampedArray(o):o},e.prototype.peekByte=function(){var r=this.getByte();return this.pos--,r},e.prototype.peekBytes=function(r,t){t===void 0&&(t=!1);var n=this.getBytes(r,t);return this.pos-=n.length,n},e.prototype.skip=function(r){r||(r=1),this.pos+=r},e.prototype.reset=function(){this.pos=0},e.prototype.makeSubStream=function(r,t){for(var n=r+t;this.bufferLength<=n&&!this.eof;)this.readBlock();return new sd(this.buffer,r,t)},e.prototype.decode=function(){for(;!this.eof;)this.readBlock();return this.buffer.subarray(0,this.bufferLength)},e.prototype.readBlock=function(){throw new xr(this.constructor.name,"readBlock")},e.prototype.ensureBuffer=function(r){var t=this.buffer;if(r<=t.byteLength)return t;for(var n=this.minBufferLength;n=0;--s)c[l+s]=d&255,d>>=8}},r})(oa),Ng=Lg,Ig=(function(e){Te(r,e);function r(t,n){var i=e.call(this,n)||this;return i.stream=t,i.firstDigit=-1,n&&(n=.5*n),i}return r.prototype.readBlock=function(){var t=8e3,n=this.stream.getBytes(t);if(!n.length){this.eof=!0;return}for(var i=n.length+1>>1,a=this.ensureBuffer(this.bufferLength+i),o=this.bufferLength,l=this.firstDigit,c=0,s=n.length;c=48&&u<=57)d=u&15;else if(u>=65&&u<=70||u>=97&&u<=102)d=(u&15)+9;else if(u===62){this.eof=!0;break}else continue;l<0?l=d:(a[o++]=l<<4|d,l=-1)}l>=0&&this.eof&&(a[o++]=l<<4,l=-1),this.firstDigit=l,this.bufferLength=o},r})(oa),zg=Ig,cu=new Int32Array([16,17,18,0,8,7,9,6,10,5,11,4,12,3,13,2,14,1,15]),Mg=new Int32Array([3,4,5,6,7,8,9,10,65547,65549,65551,65553,131091,131095,131099,131103,196643,196651,196659,196667,262211,262227,262243,262259,327811,327843,327875,327907,258,258,258]),Og=new Int32Array([1,2,3,4,65541,65543,131081,131085,196625,196633,262177,262193,327745,327777,393345,393409,459009,459137,524801,525057,590849,591361,657409,658433,724993,727041,794625,798721,868353,876545]),_g=[new Int32Array([459008,524368,524304,524568,459024,524400,524336,590016,459016,524384,524320,589984,524288,524416,524352,590048,459012,524376,524312,589968,459028,524408,524344,590032,459020,524392,524328,59e4,524296,524424,524360,590064,459010,524372,524308,524572,459026,524404,524340,590024,459018,524388,524324,589992,524292,524420,524356,590056,459014,524380,524316,589976,459030,524412,524348,590040,459022,524396,524332,590008,524300,524428,524364,590072,459009,524370,524306,524570,459025,524402,524338,590020,459017,524386,524322,589988,524290,524418,524354,590052,459013,524378,524314,589972,459029,524410,524346,590036,459021,524394,524330,590004,524298,524426,524362,590068,459011,524374,524310,524574,459027,524406,524342,590028,459019,524390,524326,589996,524294,524422,524358,590060,459015,524382,524318,589980,459031,524414,524350,590044,459023,524398,524334,590012,524302,524430,524366,590076,459008,524369,524305,524569,459024,524401,524337,590018,459016,524385,524321,589986,524289,524417,524353,590050,459012,524377,524313,589970,459028,524409,524345,590034,459020,524393,524329,590002,524297,524425,524361,590066,459010,524373,524309,524573,459026,524405,524341,590026,459018,524389,524325,589994,524293,524421,524357,590058,459014,524381,524317,589978,459030,524413,524349,590042,459022,524397,524333,590010,524301,524429,524365,590074,459009,524371,524307,524571,459025,524403,524339,590022,459017,524387,524323,589990,524291,524419,524355,590054,459013,524379,524315,589974,459029,524411,524347,590038,459021,524395,524331,590006,524299,524427,524363,590070,459011,524375,524311,524575,459027,524407,524343,590030,459019,524391,524327,589998,524295,524423,524359,590062,459015,524383,524319,589982,459031,524415,524351,590046,459023,524399,524335,590014,524303,524431,524367,590078,459008,524368,524304,524568,459024,524400,524336,590017,459016,524384,524320,589985,524288,524416,524352,590049,459012,524376,524312,589969,459028,524408,524344,590033,459020,524392,524328,590001,524296,524424,524360,590065,459010,524372,524308,524572,459026,524404,524340,590025,459018,524388,524324,589993,524292,524420,524356,590057,459014,524380,524316,589977,459030,524412,524348,590041,459022,524396,524332,590009,524300,524428,524364,590073,459009,524370,524306,524570,459025,524402,524338,590021,459017,524386,524322,589989,524290,524418,524354,590053,459013,524378,524314,589973,459029,524410,524346,590037,459021,524394,524330,590005,524298,524426,524362,590069,459011,524374,524310,524574,459027,524406,524342,590029,459019,524390,524326,589997,524294,524422,524358,590061,459015,524382,524318,589981,459031,524414,524350,590045,459023,524398,524334,590013,524302,524430,524366,590077,459008,524369,524305,524569,459024,524401,524337,590019,459016,524385,524321,589987,524289,524417,524353,590051,459012,524377,524313,589971,459028,524409,524345,590035,459020,524393,524329,590003,524297,524425,524361,590067,459010,524373,524309,524573,459026,524405,524341,590027,459018,524389,524325,589995,524293,524421,524357,590059,459014,524381,524317,589979,459030,524413,524349,590043,459022,524397,524333,590011,524301,524429,524365,590075,459009,524371,524307,524571,459025,524403,524339,590023,459017,524387,524323,589991,524291,524419,524355,590055,459013,524379,524315,589975,459029,524411,524347,590039,459021,524395,524331,590007,524299,524427,524363,590071,459011,524375,524311,524575,459027,524407,524343,590031,459019,524391,524327,589999,524295,524423,524359,590063,459015,524383,524319,589983,459031,524415,524351,590047,459023,524399,524335,590015,524303,524431,524367,590079]),9],Ug=[new Int32Array([327680,327696,327688,327704,327684,327700,327692,327708,327682,327698,327690,327706,327686,327702,327694,0,327681,327697,327689,327705,327685,327701,327693,327709,327683,327699,327691,327707,327687,327703,327695,0]),5],Wg=(function(e){Te(r,e);function r(t,n){var i=e.call(this,n)||this;i.stream=t;var a=t.getByte(),o=t.getByte();if(a===-1||o===-1)throw new Error("Invalid header in flate stream: "+a+", "+o);if((a&15)!==8)throw new Error("Unknown compression method in flate stream: "+a+", "+o);if(((a<<8)+o)%31!==0)throw new Error("Bad FCHECK in flate stream: "+a+", "+o);if(o&32)throw new Error("FDICT bit set in flate stream: "+a+", "+o);return i.codeSize=0,i.codeBuf=0,i}return r.prototype.readBlock=function(){var t,n,i=this.stream,a=this.getBits(3);if(a&1&&(this.eof=!0),a>>=1,a===0){var o=void 0;if((o=i.getByte())===-1)throw new Error("Bad block header in flate stream");var l=o;if((o=i.getByte())===-1)throw new Error("Bad block header in flate stream");if(l|=o<<8,(o=i.getByte())===-1)throw new Error("Bad block header in flate stream");var c=o;if((o=i.getByte())===-1)throw new Error("Bad block header in flate stream");if(c|=o<<8,c!==(~l&65535)&&(l!==0||c!==0))throw new Error("Bad uncompressed block length in flate stream");this.codeBuf=0,this.codeSize=0;var s=this.bufferLength;t=this.ensureBuffer(s+l);var u=s+l;if(this.bufferLength=u,l===0)i.peekByte()===-1&&(this.eof=!0);else for(var d=s;d0;)F[b++]=B}A=this.generateHuffmanTable(F.subarray(0,p)),f=this.generateHuffmanTable(F.subarray(p,P))}else throw new Error("Unknown block type in flate stream");t=this.buffer;for(var H=t?t.length:0,C=this.bufferLength;;){var I=this.getCode(A);if(I<256){C+1>=H&&(t=this.ensureBuffer(C+1),H=t.length),t[C++]=I;continue}if(I===256){this.bufferLength=C;return}I-=257,I=Mg[I];var m=I>>16;m>0&&(m=this.getBits(m)),n=(I&65535)+m,I=this.getCode(f),I=Og[I],m=I>>16,m>0&&(m=this.getBits(m));var N=(I&65535)+m;C+n>=H&&(t=this.ensureBuffer(C+n),H=t.length);for(var ne=0;ne>t,this.codeSize=i-=t,o},r.prototype.getCode=function(t){for(var n=this.stream,i=t[0],a=t[1],o=this.codeSize,l=this.codeBuf,c;o>16,d=s&65535;if(u<1||o>u,this.codeSize=o-u,d},r.prototype.generateHuffmanTable=function(t){var n=t.length,i=0,a;for(a=0;ai&&(i=t[a]);for(var o=1<>=1;for(a=A;a0;if(!F||F<256)y[0]=F,v=1;else if(F>=258)if(F=0;o--)y[o]=d[l],l=f[l];else y[v++]=y[0];else if(F===256){p=9,u=258,v=0;continue}else{this.eof=!0,delete this.lzwState;break}if(R&&(f[u]=g,A[u]=A[g]+1,d[u]=y[0],u++,p=u+s&u+s-1?p:Math.min(Math.log(u+s)/.6931471805599453+1,12)|0),g=F,b+=v,n>>n&(1<0){var o=this.stream.getBytes(a);n.set(o,i),i+=a}}else{a=257-a;var l=t[1];n=this.ensureBuffer(i+a+1);for(var c=0;cn.size())throw new ao(t,0,n.size());n.remove(t)}else{if(t!==0)throw new ao(t,0,0);this.setKids([])}},r.prototype.normalizedEntries=function(){var t=this.Kids();return t||(t=this.dict.context.obj([this.ref]),this.dict.set(X.of("Kids"),t)),{Kids:t}},r.fromDict=function(t,n){return new r(t,n)},r})(ud),pi=im,am=(function(e){Te(r,e);function r(){return e!==null&&e.apply(this,arguments)||this}return r.prototype.Opt=function(){return this.dict.lookupMaybe(X.of("Opt"),wt,qe,vt)},r.prototype.setOpt=function(t){this.dict.set(X.of("Opt"),this.dict.context.obj(t))},r.prototype.getExportValues=function(){var t=this.Opt();if(t){if(t instanceof wt||t instanceof qe)return[t];for(var n=[],i=0,a=t.size();in.size())throw new ao(t,0,n.size());n.remove(t)}},r.prototype.normalizeExportValues=function(){for(var t,n,i,a,o=(t=this.getExportValues())!==null&&t!==void 0?t:[],l=[],c=this.getWidgets(),s=0,u=c.length;s1){if(!this.hasFlag(bt.MultiSelect))throw new yA;this.dict.set(X.of("V"),this.dict.context.obj(t))}this.updateSelectedIndices(t)},r.prototype.valuesAreValid=function(t){for(var n=this.getOptions(),i=function(c,s){var u=t[c].decodeText();if(!n.find(function(d){return u===(d.display||d.value).decodeText()}))return{value:!1}},a=0,o=t.length;a1){for(var n=new Array(t.length),i=this.getOptions(),a=function(c,s){var u=t[c].decodeText();n[c]=i.findIndex(function(d){return u===(d.display||d.value).decodeText()})},o=0,l=t.length;o0){var l=o.lookup(0,wt,qe),c=o.lookupMaybe(1,wt,qe);n.push({value:l,display:c||l})}}return n}return[]},r})(pi),dd=sm,lm=(function(e){Te(r,e);function r(){return e!==null&&e.apply(this,arguments)||this}return r.fromDict=function(t,n){return new r(t,n)},r.create=function(t){var n=t.obj({FT:"Ch",Ff:bt.Combo,Kids:[]}),i=t.register(n);return new r(n,i)},r})(dd),Po=lm,cm=(function(e){Te(r,e);function r(){return e!==null&&e.apply(this,arguments)||this}return r.prototype.addField=function(t){var n=this.normalizedEntries().Kids;n?.push(t)},r.prototype.normalizedEntries=function(){var t=this.Kids();return t||(t=this.dict.context.obj([]),this.dict.set(X.of("Kids"),t)),{Kids:t}},r.fromDict=function(t,n){return new r(t,n)},r.create=function(t){var n=t.obj({}),i=t.register(n);return new r(n,i)},r})(ud),lo=cm,um=(function(e){Te(r,e);function r(){return e!==null&&e.apply(this,arguments)||this}return r.fromDict=function(t,n){return new r(t,n)},r})(pi),dl=um,dm=(function(e){Te(r,e);function r(){return e!==null&&e.apply(this,arguments)||this}return r.prototype.MaxLen=function(){var t=this.dict.lookup(X.of("MaxLen"));if(t instanceof Qe)return t},r.prototype.Q=function(){var t=this.dict.lookup(X.of("Q"));if(t instanceof Qe)return t},r.prototype.setMaxLength=function(t){this.dict.set(X.of("MaxLen"),Qe.of(t))},r.prototype.removeMaxLength=function(){this.dict.delete(X.of("MaxLen"))},r.prototype.getMaxLength=function(){var t;return(t=this.MaxLen())===null||t===void 0?void 0:t.asNumber()},r.prototype.setQuadding=function(t){this.dict.set(X.of("Q"),Qe.of(t))},r.prototype.getQuadding=function(){var t;return(t=this.Q())===null||t===void 0?void 0:t.asNumber()},r.prototype.setValue=function(t){this.dict.set(X.of("V"),t)},r.prototype.removeValue=function(){this.dict.delete(X.of("V"))},r.prototype.getValue=function(){var t=this.V();if(t instanceof wt||t instanceof qe)return t},r.fromDict=function(t,n){return new r(t,n)},r.create=function(t){var n=t.obj({FT:"Tx",Kids:[]}),i=t.register(n);return new r(n,i)},r})(pi),Fo=dm,fm=(function(e){Te(r,e);function r(){return e!==null&&e.apply(this,arguments)||this}return r.fromDict=function(t,n){return new r(t,n)},r.create=function(t){var n=t.obj({FT:"Btn",Ff:yr.PushButton,Kids:[]}),i=t.register(n);return new r(n,i)},r})(ul),To=fm,hm=(function(e){Te(r,e);function r(){return e!==null&&e.apply(this,arguments)||this}return r.prototype.setValue=function(t){var n=this.getOnValues();if(!n.includes(t)&&t!==X.of("Off"))throw new al;this.dict.set(X.of("V"),t);for(var i=this.getWidgets(),a=0,o=i.length;aa)throw new Yc(n,a);for(var o=n,l=0,c=i.size();lo)return u.insertLeafNode(t,o)||s;o-=u.Count().asNumber()}u instanceof Kr&&(o-=1)}if(o===0){this.insertLeafKid(i.size(),t);return}throw new Jc(n,"insertLeafNode")},r.prototype.removeLeafNode=function(t,n){n===void 0&&(n=!0);var i=this.Kids(),a=this.Count().asNumber();if(t>=a)throw new Yc(t,a);for(var o=t,l=0,c=i.size();lo){u.removeLeafNode(o,n),n&&u.Kids().size()===0&&i.remove(l);return}else o-=u.Count().asNumber();if(u instanceof Kr)if(o===0){this.removeKid(l);return}else o-=1}throw new Jc(t,"removeLeafNode")},r.prototype.ascend=function(t){t(this);var n=this.Parent();n&&n.ascend(t)},r.prototype.traverse=function(t){for(var n=this.Kids(),i=0,a=n.size();iNumber.MAX_SAFE_INTEGER)if(this.capNumbers){var i="Parsed number that is too large for some PDF readers: "+r+", using Number.MAX_SAFE_INTEGER instead.";return console.warn(i),Number.MAX_SAFE_INTEGER}else{var i="Parsed number that is too large for some PDF readers: "+r+", not capping.";console.warn(i)}return n},e.prototype.skipWhitespace=function(){for(;!this.bytes.done()&&Yr[this.bytes.peek()];)this.bytes.next()},e.prototype.skipLine=function(){for(;!this.bytes.done();){var r=this.bytes.peek();if(r===fu||r===hu)return;this.bytes.next()}},e.prototype.skipComment=function(){if(this.bytes.peek()!==Q.Percent)return!1;for(;!this.bytes.done();){var r=this.bytes.peek();if(r===fu||r===hu)return!0;this.bytes.next()}return!0},e.prototype.skipWhitespaceAndComments=function(){for(this.skipWhitespace();this.skipComment();)this.skipWhitespace()},e.prototype.matchKeyword=function(r){for(var t=this.bytes.offset(),n=0,i=r.length;n=this.length},e.prototype.offset=function(){return this.idx},e.prototype.slice=function(r,t){return this.bytes.slice(r,t)},e.prototype.position=function(){return{line:this.line,column:this.column,offset:this.idx}},e.of=function(r){return new e(r)},e.fromPDFRawStream=function(r){return e.of(ld(r).decode())},e})(),Ro=km,Cm=Q.Space,Ri=Q.CarriageReturn,Li=Q.Newline,Ni=[Q.s,Q.t,Q.r,Q.e,Q.a,Q.m],Wa=[Q.e,Q.n,Q.d,Q.s,Q.t,Q.r,Q.e,Q.a,Q.m],Ct={header:[Q.Percent,Q.P,Q.D,Q.F,Q.Dash],eof:[Q.Percent,Q.Percent,Q.E,Q.O,Q.F],obj:[Q.o,Q.b,Q.j],endobj:[Q.e,Q.n,Q.d,Q.o,Q.b,Q.j],xref:[Q.x,Q.r,Q.e,Q.f],trailer:[Q.t,Q.r,Q.a,Q.i,Q.l,Q.e,Q.r],startxref:[Q.s,Q.t,Q.a,Q.r,Q.t,Q.x,Q.r,Q.e,Q.f],true:[Q.t,Q.r,Q.u,Q.e],false:[Q.f,Q.a,Q.l,Q.s,Q.e],null:[Q.n,Q.u,Q.l,Q.l],stream:Ni,streamEOF1:ht(Ni,[Cm,Ri,Li]),streamEOF2:ht(Ni,[Ri,Li]),streamEOF3:ht(Ni,[Ri]),streamEOF4:ht(Ni,[Li]),endstream:Wa,EOF1endstream:ht([Ri,Li],Wa),EOF2endstream:ht([Ri],Wa),EOF3endstream:ht([Li],Wa)},Pm=(function(e){Te(r,e);function r(t,n,i){i===void 0&&(i=!1);var a=e.call(this,t,i)||this;return a.context=n,a}return r.prototype.parseObject=function(){if(this.skipWhitespaceAndComments(),this.matchKeyword(Ct.true))return Yi.True;if(this.matchKeyword(Ct.false))return Yi.False;if(this.matchKeyword(Ct.null))return rr;var t=this.bytes.peek();if(t===Q.LessThan&&this.bytes.peekAhead(1)===Q.LessThan)return this.parseDictOrStream();if(t===Q.LessThan)return this.parseHexString();if(t===Q.LeftParen)return this.parseString();if(t===Q.ForwardSlash)return this.parseName();if(t===Q.LeftSquareBracket)return this.parseArray();if(pl[t])return this.parseNumberOrRef();throw new SA(this.bytes.position(),t)},r.prototype.parseNumberOrRef=function(){var t=this.parseRawNumber();this.skipWhitespaceAndComments();var n=this.bytes.offset();if(Kt[this.bytes.peek()]){var i=this.parseRawNumber();if(this.skipWhitespaceAndComments(),this.bytes.peek()===Q.R)return this.bytes.assertNext(Q.R),xt.of(t,i)}return this.bytes.moveTo(n),Qe.of(t)},r.prototype.parseHexString=function(){var t="";for(this.bytes.assertNext(Q.LessThan);!this.bytes.done()&&this.bytes.peek()!==Q.GreaterThan;)t+=Xr(this.bytes.next());return this.bytes.assertNext(Q.GreaterThan),qe.of(t)},r.prototype.parseString=function(){for(var t=0,n=!1,i="";!this.bytes.done();){var a=this.bytes.next();if(i+=Xr(a),n||(a===Q.LeftParen&&(t+=1),a===Q.RightParen&&(t-=1)),a===Q.BackSlash?n=!n:n&&(n=!1),t===0)return wt.of(i.substring(1,i.length-1))}throw new PA(this.bytes.position())},r.prototype.parseName=function(){this.bytes.assertNext(Q.ForwardSlash);for(var t="";!this.bytes.done();){var n=this.bytes.peek();if(Yr[n]||Pr[n])break;t+=Xr(n),this.bytes.next()}return X.of(t)},r.prototype.parseArray=function(){this.bytes.assertNext(Q.LeftSquareBracket),this.skipWhitespaceAndComments();for(var t=vt.withContext(this.context);this.bytes.peek()!==Q.RightSquareBracket;){var n=this.parseObject();t.push(n),this.skipWhitespaceAndComments()}return this.bytes.assertNext(Q.RightSquareBracket),t},r.prototype.parseDict=function(){this.bytes.assertNext(Q.LessThan),this.bytes.assertNext(Q.LessThan),this.skipWhitespaceAndComments();for(var t=new Map;!this.bytes.done()&&this.bytes.peek()!==Q.GreaterThan&&this.bytes.peekAhead(1)!==Q.GreaterThan;){var n=this.parseName(),i=this.parseObject();t.set(n,i),this.skipWhitespaceAndComments()}this.skipWhitespaceAndComments(),this.bytes.assertNext(Q.GreaterThan),this.bytes.assertNext(Q.GreaterThan);var a=t.get(X.of("Type"));return a===X.of("Catalog")?pd.fromMapWithContext(t,this.context):a===X.of("Pages")?Ad.fromMapWithContext(t,this.context):a===X.of("Page")?Kr.fromMapWithContext(t,this.context):et.fromMapWithContext(t,this.context)},r.prototype.parseDictOrStream=function(){var t=this.bytes.position(),n=this.parseDict();if(this.skipWhitespaceAndComments(),!this.matchKeyword(Ct.streamEOF1)&&!this.matchKeyword(Ct.streamEOF2)&&!this.matchKeyword(Ct.streamEOF3)&&!this.matchKeyword(Ct.streamEOF4)&&!this.matchKeyword(Ct.stream))return n;var i=this.bytes.offset(),a,o=n.get(X.of("Length"));o instanceof Qe?(a=i+o.asNumber(),this.bytes.moveTo(a),this.skipWhitespaceAndComments(),this.matchKeyword(Ct.endstream)||(this.bytes.moveTo(i),a=this.findEndOfStreamFallback(t))):a=this.findEndOfStreamFallback(t);var l=this.bytes.slice(i,a);return Ji.of(n,l)},r.prototype.findEndOfStreamFallback=function(t){for(var n=1,i=this.bytes.offset();!this.bytes.done()&&(i=this.bytes.offset(),this.matchKeyword(Ct.stream)?n+=1:this.matchKeyword(Ct.EOF1endstream)||this.matchKeyword(Ct.EOF2endstream)||this.matchKeyword(Ct.EOF3endstream)||this.matchKeyword(Ct.endstream)?n-=1:this.bytes.next(),n!==0););if(n!==0)throw new CA(t);return i},r.forBytes=function(t,n,i){return new r(Ro.of(t),n,i)},r.forByteStream=function(t,n,i){return i===void 0&&(i=!1),new r(t,n,i)},r})(Sm),gd=Pm,Fm=(function(e){Te(r,e);function r(t,n){var i=e.call(this,Ro.fromPDFRawStream(t),t.dict.context)||this,a=t.dict;return i.alreadyParsed=!1,i.shouldWaitForTick=n||(function(){return!1}),i.firstOffset=a.lookup(X.of("First"),Qe).asNumber(),i.objectCount=a.lookup(X.of("N"),Qe).asNumber(),i}return r.prototype.parseIntoContext=function(){return it(this,void 0,void 0,function(){var t,n,i,a,o,l,c,s;return at(this,function(u){switch(u.label){case 0:if(this.alreadyParsed)throw new il("PDFObjectStreamParser","parseIntoContext");this.alreadyParsed=!0,t=this.parseOffsetsAndObjectNumbers(),n=0,i=t.length,u.label=1;case 1:return n=Q.Space&&n<=Q.Tilde;if(i&&(this.matchKeyword(Ct.xref)||this.matchKeyword(Ct.trailer)||this.matchKeyword(Ct.startxref)||this.matchIndirectObjectHeader())){this.bytes.moveTo(t);break}this.bytes.next()}},r.prototype.skipBinaryHeaderComment=function(){this.skipWhitespaceAndComments();try{var t=this.bytes.offset();this.parseIndirectObjectHeader(),this.bytes.moveTo(t)}catch{this.bytes.next(),this.skipWhitespaceAndComments()}},r.forBytesWithOptions=function(t,n,i,a){return new r(t,n,i,a)},r})(gd),Rm=Bm,zr=function(e){return 1<0&&(n[n.length]=+i),t[t.length]={cmd:r,args:n},n=[],i="",a=!1),r=s;else if([" ",","].includes(s)||s==="-"&&i.length>0&&i[i.length-1]!=="e"||s==="."&&a){if(i.length===0)continue;n.length===o?(t[t.length]={cmd:r,args:n},n=[+i],r==="M"&&(r="L"),r==="m"&&(r="l")):n[n.length]=+i,a=s===".",i=["-","."].includes(s)?s:""}else i+=s,s==="."&&(a=!0)}return i.length>0&&(n.length===o?(t[t.length]={cmd:r,args:n},n=[+i],r==="M"&&(r="L"),r==="m"&&(r="l")):n[n.length]=+i),t[t.length]={cmd:r,args:n},t},Vm=function(e){Ue=We=ct=ut=Oi=_i=0;for(var r=[],t=0;t1&&(A=Math.sqrt(A),t*=A,n*=A);var f=d/t,p=u/t,g=-u/n,y=d/n,v=f*l+p*c,b=g*l+y*c,x=f*e+p*r,P=g*e+y*r,F=(x-v)*(x-v)+(P-b)*(P-b),R=1/F-.25;R<0&&(R=0);var E=Math.sqrt(R);a===i&&(E=-E);var B=.5*(v+x)-E*(P-b),T=.5*(b+P)+E*(x-v),W=Math.atan2(b-T,v-B),H=Math.atan2(P-T,x-B),C=H-W;C<0&&a===1?C+=2*Math.PI:C>0&&a===0&&(C-=2*Math.PI);for(var I=Math.ceil(Math.abs(C/(Math.PI*.5+.001))),m=[],N=0;Ne.length)return i-1;var y=r.heightAtSize(i),v=y+y*.2,b=v*a;if(b>Math.abs(t.height))return i-1;i+=1}return i},hv=function(e,r,t,n){for(var i=t.width/n,a=t.height,o=Nd,l=yp(e);oi*.75;if(d)return o-1}var A=r.heightAtSize(o,{descender:!1});if(A>a)return o-1;o+=1}return o},pv=function(e){for(var r=e.length;r>0;r--)if(/\s/.test(e[r]))return r},Av=function(e,r,t,n){for(var i,a=e.length;a>0;){var o=e.substring(0,a),l=t.encodeText(o),c=t.widthOfTextAtSize(o,n);if(cA&&(A=E+F),p+l>f&&(f=p+l),s.push({text:x,encoded:P,width:F,height:l,x:E,y:p}),v=R?.trim()}return{fontSize:n,lineHeight:c,lines:s,bounds:{x:u,y:d,width:A-u,height:f-d}}},gv=function(e,r){var t=r.fontSize,n=r.font,i=r.bounds,a=r.cellCount,o=Uu(ia(e));if(o.length>a)throw new uv(o.length,a);(t===void 0||t===0)&&(t=hv(o,n,i,a));for(var l=i.width/a,c=n.heightAtSize(t,{descender:!1}),s=i.y+(i.height/2-c/2),u=[],d=i.x,A=i.y,f=i.x+i.width,p=i.y+i.height,g=0,y=0;gf&&(f=E+F),s+c>p&&(p=s+c),u.push({text:o,encoded:P,width:F,height:c,x:E,y:s}),g+=1,y+=x}return{fontSize:t,cells:u,bounds:{x:d,y:A,width:f-d,height:p-A}}},Ao=function(e,r){var t=r.alignment,n=r.fontSize,i=r.font,a=r.bounds,o=Uu(ia(e));(n===void 0||n===0)&&(n=zd([o],i,a));var l=i.encodeText(o),c=i.widthOfTextAtSize(o,n),s=i.heightAtSize(n,{descender:!1}),u=t===Mt.Left?a.x:t===Mt.Center?a.x+a.width/2-c/2:t===Mt.Right?a.x+a.width-c:a.x,d=a.y+(a.height/2-s/2);return{fontSize:n,line:{text:o,encoded:l,width:c,height:s,x:u,y:d},bounds:{x:u,y:d,width:c,height:s}}},gi=function(e){return"normal"in e?e:{normal:e}},mv=/\/([^\0\t\n\f\r\ ]+)[\0\t\n\f\r\ ]+(\d*\.\d+|\d+)[\0\t\n\f\r\ ]+Tf/,wn=function(e){var r,t,n=(r=e.getDefaultAppearance())!==null&&r!==void 0?r:"",i=(t=rl(n,mv).match)!==null&&t!==void 0?t:[],a=Number(i[2]);return isFinite(a)?a:void 0},vv=/(\d*\.\d+|\d+)[\0\t\n\f\r\ ]*(\d*\.\d+|\d+)?[\0\t\n\f\r\ ]*(\d*\.\d+|\d+)?[\0\t\n\f\r\ ]*(\d*\.\d+|\d+)?[\0\t\n\f\r\ ]+(g|rg|k)/,kr=function(e){var r,t=(r=e.getDefaultAppearance())!==null&&r!==void 0?r:"",n=rl(t,vv).match,i=n??[],a=i[1],o=i[2],l=i[3],c=i[4],s=i[5];if(s==="g"&&a)return Dd(Number(a));if(s==="rg"&&a&&o&&l)return St(Number(a),Number(o),Number(l));if(s==="k"&&a&&o&&l&&c)return Ed(Number(a),Number(o),Number(l),Number(c))},Cr=function(e,r,t,n){var i;n===void 0&&(n=0);var a=[jn(r).toString(),vl((i=t?.name)!==null&&i!==void 0?i:"dummy__noop",n).toString()].join(` -`);e.setDefaultAppearance(a)},yv=function(e,r){var t,n,i,a=kr(r),o=kr(e.acroField),l=r.getRectangle(),c=r.getAppearanceCharacteristics(),s=r.getBorderStyle(),u=(t=s?.getWidth())!==null&&t!==void 0?t:0,d=Jr(c?.getRotation()),A=Wn(l,d),f=A.width,p=A.height,g=Cn(Je(Je({},l),{rotation:d})),y=St(0,0,0),v=(n=er(c?.getBorderColor()))!==null&&n!==void 0?n:y,b=er(c?.getBackgroundColor()),x=er(c?.getBackgroundColor(),.8),P=(i=a??o)!==null&&i!==void 0?i:y;Cr(a?r:e.acroField,P);var F={x:0+u/2,y:0+u/2,width:f-u,height:p-u,thickness:1.5,borderWidth:u,borderColor:v,markColor:P};return{normal:{on:ht(g,qa(Je(Je({},F),{color:b,filled:!0}))),off:ht(g,qa(Je(Je({},F),{color:b,filled:!1})))},down:{on:ht(g,qa(Je(Je({},F),{color:x,filled:!0}))),off:ht(g,qa(Je(Je({},F),{color:x,filled:!1})))}}},bv=function(e,r){var t,n,i,a=kr(r),o=kr(e.acroField),l=r.getRectangle(),c=r.getAppearanceCharacteristics(),s=r.getBorderStyle(),u=(t=s?.getWidth())!==null&&t!==void 0?t:0,d=Jr(c?.getRotation()),A=Wn(l,d),f=A.width,p=A.height,g=Cn(Je(Je({},l),{rotation:d})),y=St(0,0,0),v=(n=er(c?.getBorderColor()))!==null&&n!==void 0?n:y,b=er(c?.getBackgroundColor()),x=er(c?.getBackgroundColor(),.8),P=(i=a??o)!==null&&i!==void 0?i:y;Cr(a?r:e.acroField,P);var F={x:f/2,y:p/2,width:f-u,height:p-u,borderWidth:u,borderColor:v,dotColor:P};return{normal:{on:ht(g,Va(Je(Je({},F),{color:b,filled:!0}))),off:ht(g,Va(Je(Je({},F),{color:b,filled:!1})))},down:{on:ht(g,Va(Je(Je({},F),{color:x,filled:!0}))),off:ht(g,Va(Je(Je({},F),{color:x,filled:!1})))}}},wv=function(e,r,t){var n,i,a,o,l,c=kr(r),s=kr(e.acroField),u=wn(r),d=wn(e.acroField),A=r.getRectangle(),f=r.getAppearanceCharacteristics(),p=r.getBorderStyle(),g=f?.getCaptions(),y=(n=g?.normal)!==null&&n!==void 0?n:"",v=(a=(i=g?.down)!==null&&i!==void 0?i:y)!==null&&a!==void 0?a:"",b=(o=p?.getWidth())!==null&&o!==void 0?o:0,x=Jr(f?.getRotation()),P=Wn(A,x),F=P.width,R=P.height,E=Cn(Je(Je({},A),{rotation:x})),B=St(0,0,0),T=er(f?.getBorderColor()),W=er(f?.getBackgroundColor()),H=er(f?.getBackgroundColor(),.8),C={x:b,y:b,width:F-b*2,height:R-b*2},I=Ao(y,{alignment:Mt.Center,fontSize:u??d,font:t,bounds:C}),m=Ao(v,{alignment:Mt.Center,fontSize:u??d,font:t,bounds:C}),N=Math.min(I.fontSize,m.fontSize),ne=(l=c??s)!==null&&l!==void 0?l:B;Cr(c||u!==void 0?r:e.acroField,ne,t,N);var q={x:0+b/2,y:0+b/2,width:F-b,height:R-b,borderWidth:b,borderColor:T,textColor:ne,font:t.name,fontSize:N};return{normal:ht(E,bu(Je(Je({},q),{color:W,textLines:[I.line]}))),down:ht(E,bu(Je(Je({},q),{color:H,textLines:[m.line]})))}},xv=function(e,r,t){var n,i,a,o,l=kr(r),c=kr(e.acroField),s=wn(r),u=wn(e.acroField),d=r.getRectangle(),A=r.getAppearanceCharacteristics(),f=r.getBorderStyle(),p=(n=e.getText())!==null&&n!==void 0?n:"",g=(i=f?.getWidth())!==null&&i!==void 0?i:0,y=Jr(A?.getRotation()),v=Wn(d,y),b=v.width,x=v.height,P=Cn(Je(Je({},d),{rotation:y})),F=St(0,0,0),R=er(A?.getBorderColor()),E=er(A?.getBackgroundColor()),B,T,W=e.isCombed()?0:1,H={x:g+W,y:g+W,width:b-(g+W)*2,height:x-(g+W)*2};if(e.isMultiline()){var C=Md(p,{alignment:e.getAlignment(),fontSize:s??u,font:t,bounds:H});B=C.lines,T=C.fontSize}else if(e.isCombed()){var C=gv(p,{fontSize:s??u,font:t,bounds:H,cellCount:(a=e.getMaxLength())!==null&&a!==void 0?a:0});B=C.cells,T=C.fontSize}else{var C=Ao(p,{alignment:e.getAlignment(),fontSize:s??u,font:t,bounds:H});B=[C.line],T=C.fontSize}var I=(o=l??c)!==null&&o!==void 0?o:F;Cr(l||s!==void 0?r:e.acroField,I,t,T);var m={x:0+g/2,y:0+g/2,width:b-g,height:x-g,borderWidth:g??0,borderColor:R,textColor:I,font:t.name,fontSize:T,color:E,textLines:B,padding:W};return ht(P,Rd(m))},Sv=function(e,r,t){var n,i,a,o=kr(r),l=kr(e.acroField),c=wn(r),s=wn(e.acroField),u=r.getRectangle(),d=r.getAppearanceCharacteristics(),A=r.getBorderStyle(),f=(n=e.getSelected()[0])!==null&&n!==void 0?n:"",p=(i=A?.getWidth())!==null&&i!==void 0?i:0,g=Jr(d?.getRotation()),y=Wn(u,g),v=y.width,b=y.height,x=Cn(Je(Je({},u),{rotation:g})),P=St(0,0,0),F=er(d?.getBorderColor()),R=er(d?.getBackgroundColor()),E=1,B={x:p+E,y:p+E,width:v-(p+E)*2,height:b-(p+E)*2},T=Ao(f,{alignment:Mt.Left,fontSize:c??s,font:t,bounds:B}),W=T.line,H=T.fontSize,C=(a=o??l)!==null&&a!==void 0?a:P;Cr(o||c!==void 0?r:e.acroField,C,t,H);var I={x:0+p/2,y:0+p/2,width:v-p,height:b-p,borderWidth:p??0,borderColor:F,textColor:C,font:t.name,fontSize:H,color:R,textLines:[W],padding:E};return ht(x,Rd(I))},kv=function(e,r,t){var n,i,a=kr(r),o=kr(e.acroField),l=wn(r),c=wn(e.acroField),s=r.getRectangle(),u=r.getAppearanceCharacteristics(),d=r.getBorderStyle(),A=(n=d?.getWidth())!==null&&n!==void 0?n:0,f=Jr(u?.getRotation()),p=Wn(s,f),g=p.width,y=p.height,v=Cn(Je(Je({},s),{rotation:f})),b=St(0,0,0),x=er(u?.getBorderColor()),P=er(u?.getBackgroundColor()),F=e.getOptions(),R=e.getSelected();e.isSorted()&&F.sort();for(var E="",B=0,T=F.length;B1||i.length===1&&n)&&this.enableMultiselect();for(var l=new Array(i.length),c=0,s=i.length;c1||i.length===1&&n)&&this.enableMultiselect();for(var o=new Array(i.length),l=0,c=i.length;ln)throw new dv(t.length,n,this.getName());this.markAsDirty(),this.disableRichFormatting(),t?this.acroField.setValue(qe.fromText(t)):this.acroField.removeValue()},r.prototype.getAlignment=function(){var t=this.acroField.getQuadding();return t===0?Mt.Left:t===1?Mt.Center:t===2?Mt.Right:Mt.Left},r.prototype.setAlignment=function(t){gn(t,"alignment",Mt),this.markAsDirty(),this.acroField.setQuadding(t)},r.prototype.getMaxLength=function(){return this.acroField.getMaxLength()},r.prototype.setMaxLength=function(t){if(Dr(t,"maxLength",0,Number.MAX_SAFE_INTEGER),this.markAsDirty(),t===void 0)this.acroField.removeMaxLength();else{var n=this.getText();if(n&&n.length>t)throw new fv(n.length,t,this.getName());this.acroField.setMaxLength(t)}},r.prototype.removeMaxLength=function(){this.markAsDirty(),this.acroField.removeMaxLength()},r.prototype.setImage=function(t){for(var n=this.getAlignment(),i=n===Mt.Center?vn.Center:n===Mt.Right?vn.Right:vn.Left,a=this.acroField.getWidgets(),o=0,l=a.length;o=100?e:typeof e=="string"&&e.includes("%")?Math.round(r&&r==="X"?parseFloat(e)/100*t.width:r&&r==="Y"?parseFloat(e)/100*t.height:parseFloat(e)/100*t.width):0}function to(e){return e.replace(/[xy]/g,function(r){let t=Math.random()*16|0;return(r==="x"?t:t&3|8).toString(16)})}function Ve(e){return typeof e>"u"||e==null?"":e.toString().replace(/&/g,"&").replace(//g,">").replace(/"/g,""").replace(/'/g,"'")}function lt(e){return typeof e=="number"&&e>100?e:(typeof e=="string"&&(e=Number(e.replace(/in*/gi,""))),Math.round(Ke*e))}function je(e){let r=Number(e)||0;return isNaN(r)?0:Math.round(r*ua)}function Un(e){return e=e||0,Math.round((e>360?e-360:e)*6e4)}function Fs(e){let r=e.toString(16);return r.length===1?"0"+r:r}function Ts(e,r,t){return(Fs(e)+Fs(r)+Fs(t)).toUpperCase()}function ft(e,r){let t=(e||"").replace("#","");!Ps.test(t)&&t!==or.background1&&t!==or.background2&&t!==or.text1&&t!==or.text2&&t!==or.accent1&&t!==or.accent2&&t!==or.accent3&&t!==or.accent4&&t!==or.accent5&&t!==or.accent6&&(console.warn(`"${t}" is not a valid scheme color or hex RGB! "${nr}" used instead. Only provide 6-digit RGB or 'pptx.SchemeColor' values!`),t=nr);let n=Ps.test(t)?"srgbClr":"schemeClr",i='val="'+(Ps.test(t)?t.toUpperCase():t)+'"';return r?`${r}`:``}function r0(e,r){let t="",n=Object.assign(Object.assign({},r),e),i=Math.round(n.size*ua),a=n.color,o=Math.round(n.opacity*1e5);return t+=``,t+=ft(a,``),t+="",t}function lr(e){let r="solid",t="",n="",i="";return e&&(typeof e=="string"?t=e:(e.type&&(r=e.type),e.color&&(t=e.color),e.alpha&&(n+=``),e.transparency&&(n+=``)),r==="solid"?i+=`${ft(t,n)}`:i+=""),i}function Vr(e){return e._rels.length+e._relsChart.length+e._relsMedia.length+1}function kl(e){if(!(!e||typeof e!="object"))return e.type!=="outer"&&e.type!=="inner"&&e.type!=="none"&&(console.warn("Warning: shadow.type options are `outer`, `inner` or `none`."),e.type="outer"),e.angle&&((isNaN(Number(e.angle))||e.angle<0||e.angle>359)&&(console.warn("Warning: shadow.angle can only be 0-359"),e.angle=270),e.angle=Math.round(Number(e.angle))),e.opacity&&((isNaN(Number(e.opacity))||e.opacity<0||e.opacity>1)&&(console.warn("Warning: shadow.opacity can only be 0-1"),e.opacity=.75),e.opacity=Number(e.opacity)),e.color&&e.color.startsWith("#")&&(console.warn('Warning: shadow.color should not include hash (#) character, , e.g. "FF0000"'),e.color=e.color.replace("#","")),e}function n0(e,r,t){var n,i;let a=2.3+(!((n=e.options)===null||n===void 0)&&n.autoPageCharWeight?e.options.autoPageCharWeight:0),o=Math.floor(r/ua*Ke)/((!((i=e.options)===null||i===void 0)&&i.fontSize?e.options.fontSize:wr)/a),l=[],c=[],s=[],u=[];e.text&&e.text.toString().trim().length===0?c.push({_type:He.tablecell,text:" "}):typeof e.text=="number"||typeof e.text=="string"?c.push({_type:He.tablecell,text:(e.text||"").toString().trim()}):Array.isArray(e.text)&&(c=e.text);let d=[];return c.forEach(A=>{var f;typeof A.text=="string"&&(A.text.split(` +end`},au=function(){for(var e=[],r=0;r"},Ya=function(e){return ia(e,4)},yg=function(e){if(Mp(e))return Ya(e);if(Op(e)){var r=qu(e),t=Vu(e);return""+Ya(r)+Ya(t)}var n=So(e),i="0x"+n+" is not a valid UTF-8 or UTF-16 codepoint.";throw new Error(i)},bg=function(e){var r=0,t=function(n){r|=1<=Q.Zero&&s<=Q.Seven?(n+=c,(n.length===3||!(u>="0"&&u<="7"))&&(a(parseInt(n,8)),n="")):a(s):s===Q.BackSlash?i=!0:a(s)}return new Uint8Array(t)},r.prototype.decodeText=function(){var t=this.asBytes();return Qu(t)?Xu(t):$u(t)},r.prototype.decodeDate=function(){var t=this.decodeText(),n=Gu(t);if(!n)throw new ed(t);return n},r.prototype.asString=function(){return this.value},r.prototype.clone=function(){return r.of(this.value)},r.prototype.toString=function(){return"("+this.value+")"},r.prototype.sizeInBytes=function(){return this.value.length+2},r.prototype.copyBytesInto=function(t,n){return t[n++]=Q.LeftParen,n+=Vt(this.value,t,n),t[n++]=Q.RightParen,this.value.length+2},r.of=function(t){return new r(t)},r.fromDate=function(t){var n=Er(String(t.getUTCFullYear()),4,"0"),i=Er(String(t.getUTCMonth()+1),2,"0"),a=Er(String(t.getUTCDate()),2,"0"),o=Er(String(t.getUTCHours()),2,"0"),l=Er(String(t.getUTCMinutes()),2,"0"),c=Er(String(t.getUTCSeconds()),2,"0");return new r("D:"+n+i+a+o+l+c+"Z")},r})($t),wt=xg,Sg=(function(){function e(r,t,n,i){var a=this;this.allGlyphsInFontSortedById=function(){for(var o=new Array(a.font.characterSet.length),l=0,c=o.length;l>3)]>>7-((p&7)<<0)&1,C=3*H;l[P]=F[C],l[P+1]=F[C+1],l[P+2]=F[C+2],l[P+3]=H>2)]>>6-((p&3)<<1)&3,C=3*H;l[P]=F[C],l[P+1]=F[C+1],l[P+2]=F[C+2],l[P+3]=H>1)]>>4-((p&1)<<2)&15,C=3*H;l[P]=F[C],l[P+1]=F[C+1],l[P+2]=F[C+2],l[P+3]=H>>3)]>>>7-(q&7)&1),se=m==v*255?0:255;c[ne+q]=se<<24|m<<16|m<<8|m}else if(u==2)for(var q=0;q>>2)]>>>6-((q&3)<<1)&3),se=m==v*85?0:255;c[ne+q]=se<<24|m<<16|m<<8|m}else if(u==4)for(var q=0;q>>1)]>>>4-((q&1)<<2)&15),se=m==v*17?0:255;c[ne+q]=se<<24|m<<16|m<<8|m}else if(u==8)for(var q=0;q>>2<<3));i==0;){if(i=y(r,A,1),a=y(r,A+1,2),A+=3,a==0){(A&7)!=0&&(A+=8-(A&7));var B=(A>>>3)+4,T=r[B-4]|r[B-3]<<8;E&&(t=e.H.W(t,d+T)),t.set(new n(r.buffer,r.byteOffset+B,T),d),A=B+T<<3,d+=T;continue}if(E&&(t=e.H.W(t,d+(1<<17))),a==1&&(f=R.J,p=R.h,s=511,u=31),a==2){o=v(r,A,5)+257,l=v(r,A+5,5)+1,c=v(r,A+10,4)+4,A+=14;for(var W=A,H=1,C=0;C<38;C+=2)R.Q[C]=0,R.Q[C+1]=0;for(var C=0;CH&&(H=I)}A+=3*c,x(R.Q,H),P(R.Q,H,R.u),f=R.w,p=R.d,A=b(R.u,(1<>>4;if(!(q>>>8))t[d++]=q;else{if(q==256)break;var se=d+q-254;if(q>264){var Z=R.q[q-257];se=d+(Z>>>3)+v(r,A,Z&7),A+=Z&7}var ue=p[F(r,A)&u];A+=ue&15;var j=ue>>>4,_=R.c[j],fe=(_>>>4)+y(r,A,_&15);for(A+=_&15;d>>4;if(d<=15)o[s]=d,s++;else{var A=0,f=0;d==16?(f=3+l(i,a,2),a+=2,A=o[s-1]):d==17?(f=3+l(i,a,3),a+=3):d==18&&(f=11+l(i,a,7),a+=7);for(var p=s+f;s>>1;oa&&(a=c),o++}for(;o>1,s=r[l+1],u=c<<4|s,d=t-s,A=r[l]<>>15-t;n[p]=u,A++}},e.H.l=function(r,t){for(var n=e.H.m.r,i=15-t,a=0;a>>i}},e.H.M=function(r,t,n){n=n<<(t&7);var i=t>>>3;r[i]|=n,r[i+1]|=n>>>8},e.H.I=function(r,t,n){n=n<<(t&7);var i=t>>>3;r[i]|=n,r[i+1]|=n>>>8,r[i+2]|=n>>>16},e.H.e=function(r,t,n){return(r[t>>>3]|r[(t>>>3)+1]<<8)>>>(t&7)&(1<>>3]|r[(t>>>3)+1]<<8|r[(t>>>3)+2]<<16)>>>(t&7)&(1<>>3]|r[(t>>>3)+1]<<8|r[(t>>>3)+2]<<16)>>>(t&7)},e.H.i=function(r,t){return(r[t>>>3]|r[(t>>>3)+1]<<8|r[(t>>>3)+2]<<16|r[(t>>>3)+3]<<24)>>>(t&7)},e.H.m=(function(){var r=Uint16Array,t=Uint32Array;return{K:new r(16),j:new r(16),X:[16,17,18,0,8,7,9,6,10,5,11,4,12,3,13,2,14,1,15],S:[3,4,5,6,7,8,9,10,11,13,15,17,19,23,27,31,35,43,51,59,67,83,99,115,131,163,195,227,258,999,999,999],T:[0,0,0,0,0,0,0,0,1,1,1,1,2,2,2,2,3,3,3,3,4,4,4,4,5,5,5,5,0,0,0,0],q:new r(32),p:[1,2,3,4,5,7,9,13,17,25,33,49,65,97,129,193,257,385,513,769,1025,1537,2049,3073,4097,6145,8193,12289,16385,24577,65535,65535],z:[0,0,0,0,1,1,2,2,3,3,4,4,5,5,6,6,7,7,8,8,9,9,10,10,11,11,12,12,13,13,0,0],c:new t(32),J:new r(512),_:[],h:new r(32),$:[],w:new r(32768),C:[],v:[],d:new r(32768),D:[],u:new r(512),Q:[],r:new r(32768),s:new t(286),Y:new t(30),a:new t(19),t:new t(15e3),k:new r(65536),g:new r(32768)}})(),(function(){for(var r=e.H.m,t=32768,n=0;n>>1|(i&1431655765)<<1,i=(i&3435973836)>>>2|(i&858993459)<<2,i=(i&4042322160)>>>4|(i&252645135)<<4,i=(i&4278255360)>>>8|(i&16711935)<<8,r.r[n]=(i>>>16|i<<16)>>>17}function a(o,l,c){for(;l--!=0;)o.push(0,c)}for(var n=0;n<32;n++)r.q[n]=r.S[n]<<3|r.T[n],r.c[n]=r.p[n]<<4|r.z[n];a(r._,144,8),a(r._,112,9),a(r._,24,7),a(r._,8,8),e.H.n(r._,9),e.H.A(r._,9,r.J),e.H.l(r._,9),a(r.$,32,5),e.H.n(r.$,5),e.H.A(r.$,5,r.h),e.H.l(r.$,5),a(r.Q,19,0),a(r.C,286,0),a(r.D,30,0),a(r.v,320,0)})(),e.H.N})();Se.decode._readInterlace=function(e,r){for(var t=r.width,n=r.height,i=Se.decode._getBPP(r),a=i>>3,o=Math.ceil(t*i/8),l=new Uint8Array(n*o),c=0,s=[0,0,4,0,2,0,1],u=[0,4,0,2,0,1,0],d=[8,8,8,4,4,2,2],A=[8,8,4,4,2,2,1],f=0;f<7;){for(var p=d[f],g=A[f],y=0,v=0,b=s[f];b>3];T=T>>7-(B&7)&1,l[R*o+(E>>3)]|=T<<7-((E&7)<<0)}if(i==2){var T=e[B>>3];T=T>>6-(B&7)&3,l[R*o+(E>>2)]|=T<<6-((E&3)<<1)}if(i==4){var T=e[B>>3];T=T>>4-(B&7)&15,l[R*o+(E>>1)]|=T<<4-((E&1)<<2)}if(i>=8)for(var W=R*o+E*a,H=0;H>3)+H];B+=i,E+=g}F++,R+=p}y*v!=0&&(c+=v*(1+P)),f=f+1}return l};Se.decode._getBPP=function(e){var r=[1,null,3,1,2,null,4][e.ctype];return r*e.depth};Se.decode._filterZero=function(e,r,t,n,i){var a=Se.decode._getBPP(r),o=Math.ceil(n*a/8),l=Se.decode._paeth;a=Math.ceil(a/8);var c=0,s=1,u=e[t],d=0;if(u>1&&(e[t]=[0,0,1][u-2]),u==3)for(d=a;d>>1)&255;for(var A=0;A>>1);for(;d>>1)}else{for(;d>8&255,e[r+1]=t&255},readUint:function(e,r){return e[r]*(256*256*256)+(e[r+1]<<16|e[r+2]<<8|e[r+3])},writeUint:function(e,r,t){e[r]=t>>24&255,e[r+1]=t>>16&255,e[r+2]=t>>8&255,e[r+3]=t&255},readASCII:function(e,r,t){for(var n="",i=0;i=0&&l>=0?(d=f*r+p<<2,A=(l+f)*i+o+p<<2):(d=(-l+f)*r-o+p<<2,A=f*i+p<<2),c==0)n[A]=e[d],n[A+1]=e[d+1],n[A+2]=e[d+2],n[A+3]=e[d+3];else if(c==1){var g=e[d+3]*.00392156862745098,y=e[d]*g,v=e[d+1]*g,b=e[d+2]*g,x=n[A+3]*(1/255),P=n[A]*x,F=n[A+1]*x,R=n[A+2]*x,E=1-g,B=g+x*E,T=B==0?0:1/B;n[A+3]=255*B,n[A+0]=(y+P*E)*T,n[A+1]=(v+F*E)*T,n[A+2]=(b+R*E)*T}else if(c==2){var g=e[d+3],y=e[d],v=e[d+1],b=e[d+2],x=n[A+3],P=n[A],F=n[A+1],R=n[A+2];g==x&&y==P&&v==F&&b==R?(n[A]=0,n[A+1]=0,n[A+2]=0,n[A+3]=0):(n[A]=y,n[A+1]=v,n[A+2]=b,n[A+3]=g)}else if(c==3){var g=e[d+3],y=e[d],v=e[d+1],b=e[d+2],x=n[A+3],P=n[A],F=n[A+1],R=n[A+2];if(g==x&&y==P&&v==F&&b==R)continue;if(g<220&&x>20)return!1}return!0};Se.encode=function(e,r,t,n,i,a,o){n==null&&(n=0),o==null&&(o=!1);var l=Se.encode.compress(e,r,t,n,[!1,!1,!1,0,o]);return Se.encode.compressPNG(l,-1),Se.encode._main(l,r,t,i,a)};Se.encodeLL=function(e,r,t,n,i,a,o,l){for(var c={ctype:0+(n==1?0:2)+(i==0?0:4),depth:a,frames:[]},s=Date.now(),u=(n+i)*a,d=u*r,A=0;A1,d=!1,A=33+(u?20:0);if(i.sRGB!=null&&(A+=13),i.pHYs!=null&&(A+=21),e.ctype==3){for(var f=e.plte.length,p=0;p>>24!=255&&(d=!0);A+=8+f*3+4+(d?8+f*1+4:0)}for(var g=0;g>>8&255,E=P>>>16&255;v[s+x+0]=F,v[s+x+1]=R,v[s+x+2]=E}if(s+=f*3,o(v,s,a(v,s-f*3-4,f*3+4)),s+=4,d){o(v,s,f),s+=4,c(v,s,"tRNS"),s+=4;for(var p=0;p>>24&255;s+=f,o(v,s,a(v,s-f-4,f+4)),s+=4}}for(var B=0,g=0;g>2,C>>2));for(var f=0;fN&&q==m[y-N])ne[y]=ne[y-N];else{var se=x[q];if(se==null&&(x[q]=se=P.length,P.push(q),P.length>=300))break;ne[y]=se}}}var Z=P.length;Z<=256&&s==!1&&(Z<=2?d=1:Z<=4?d=2:Z<=16?d=4:d=8,d=Math.max(d,c));for(var f=0;f>1)]|=ke[Oe+we]<<4-(we&1)*4;else if(d==2)for(var we=0;we>2)]|=ke[Oe+we]<<6-(we&3)*2;else if(d==1)for(var we=0;we>3)]|=ke[Oe+we]<<7-(we&7)*1}fe=Re,u=3,be=1}else if(v==!1&&b.length==1){for(var Re=new Uint8Array(N*_*3),tt=N*_,y=0;yE&&(E=W),TB&&(B=T))}E==-1&&(F=R=E=B=0),i&&((F&1)==1&&F--,(R&1)==1&&R--);var C=(E-F+1)*(B-R+1);Cy&&(y=P),Fv&&(v=F))}y==-1&&(p=g=y=v=0),o&&((p&1)==1&&p--,(g&1)==1&&g--),a={x:p,y:g,width:y-p+1,height:v-g+1};var B=n[i];B.rect=a,B.blend=1,B.img=new Uint8Array(a.width*a.height*4),n[i-1].dispose==0?(Se._copyTile(s,r,t,B.img,a.width,a.height,-a.x,-a.y,0),Se.encode._prepareDiff(A,r,t,B.img,a)):Se._copyTile(A,r,t,B.img,a.width,a.height,-a.x,-a.y,0)};Se.encode._prepareDiff=function(e,r,t,n,i){Se._copyTile(e,r,t,n,i.width,i.height,-i.x,-i.y,2)};Se.encode._filterZero=function(e,r,t,n,i,a,o){var l=[],c=[0,1,2,3,4];a!=-1?c=[a]:(r*n>5e5||t==1)&&(c=[0]);var s;o&&(s={level:0});for(var u=o&&UZIP!=null?UZIP:Eg.default,d=0;d>1)+256&255;if(a==4)for(var s=i;s>1)&255;for(var s=i;s>1)&255}if(a==4){for(var s=0;s>>1:t=t>>>1;e[r]=t}return e})(),update:function(e,r,t,n){for(var i=0;i>>8;return e},crc:function(e,r,t){return Se.crc.update(4294967295,e,r,t)^4294967295}};Se.quantize=function(e,r){for(var t=new Uint8Array(e),n=t.slice(0),i=new Uint32Array(n.buffer),a=Se.quantize.getKDtree(n,r),o=a[0],l=a[1],c=Se.quantize.planeDst,s=t,u=i,d=s.length,A=new Uint8Array(t.length>>2),f=0;f>2]=b.ind,u[f>>2]=b.est.rgba}return{abuf:n.buffer,inds:A,plte:l}};Se.quantize.getKDtree=function(e,r,t){t==null&&(t=1e-4);var n=new Uint32Array(e.buffer),i={i0:0,i1:e.length,bst:null,est:null,tdst:0,left:null,right:null};i.bst=Se.quantize.stats(e,i.i0,i.i1),i.est=Se.quantize.estats(i.bst);for(var a=[i];a.lengtho&&(o=a[c].est.L,l=c);if(o=u||s.i1<=u;if(d){s.est.L=0;continue}var A={i0:s.i0,i1:u,bst:null,est:null,tdst:0,left:null,right:null};A.bst=Se.quantize.stats(e,A.i0,A.i1),A.est=Se.quantize.estats(A.bst);var f={i0:u,i1:s.i1,bst:null,est:null,tdst:0,left:null,right:null};f.bst={R:[],m:[],N:s.bst.N-A.bst.N};for(var c=0;c<16;c++)f.bst.R[c]=s.bst.R[c]-A.bst.R[c];for(var c=0;c<4;c++)f.bst.m[c]=s.bst.m[c]-A.bst.m[c];f.est=Se.quantize.estats(f.bst),s.left=A,s.right=f,a[l]=A,a.push(f)}a.sort(function(p,g){return g.bst.N-p.bst.N});for(var c=0;c0&&(o=e.right,l=e.left);var c=Se.quantize.getNearest(o,r,t,n,i);if(c.tdst<=a*a)return c;var s=Se.quantize.getNearest(l,r,t,n,i);return s.tdsta;)n-=4;if(t>=n)break;var c=r[t>>2];r[t>>2]=r[n>>2],r[n>>2]=c,t+=4,n-=4}for(;o(e,t,i)>a;)t-=4;return t+4};Se.quantize.vecDot=function(e,r,t){return e[r]*t[0]+e[r+1]*t[1]+e[r+2]*t[2]+e[r+3]*t[3]};Se.quantize.stats=function(e,r,t){for(var n=[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],i=[0,0,0,0],a=t-r>>2,o=r;o>>0}};Se.M4={multVec:function(e,r){return[e[0]*r[0]+e[1]*r[1]+e[2]*r[2]+e[3]*r[3],e[4]*r[0]+e[5]*r[1]+e[6]*r[2]+e[7]*r[3],e[8]*r[0]+e[9]*r[1]+e[10]*r[2]+e[11]*r[3],e[12]*r[0]+e[13]*r[1]+e[14]*r[2]+e[15]*r[3]]},dot:function(e,r){return e[0]*r[0]+e[1]*r[1]+e[2]*r[2]+e[3]*r[3]},sml:function(e,r){return[e*r[0],e*r[1],e*r[2],e*r[3]]}};Se.encode.concatRGBA=function(e){for(var r=0,t=0;t1)throw new Error("Animated PNGs are not supported");var i=new Uint8Array(n[0]),a=Rg(i),o=a.rgbChannel,l=a.alphaChannel;this.rgbChannel=o;var c=l.some(function(s){return s<255});c&&(this.alphaChannel=l),this.type=Bg(t.ctype),this.width=t.width,this.height=t.height,this.bitsPerComponent=8}return e.load=function(r){return new e(r)},e})(),Ng=(function(){function e(r){this.image=r,this.bitsPerComponent=r.bitsPerComponent,this.width=r.width,this.height=r.height,this.colorSpace="DeviceRGB"}return e.for=function(r){return it(this,void 0,void 0,function(){var t;return at(this,function(n){return t=Lg.load(r),[2,new e(t)]})})},e.prototype.embedIntoContext=function(r,t){return it(this,void 0,void 0,function(){var n,i;return at(this,function(a){return n=this.embedAlphaChannel(r),i=r.flateStream(this.image.rgbChannel,{Type:"XObject",Subtype:"Image",BitsPerComponent:this.image.bitsPerComponent,Width:this.image.width,Height:this.image.height,ColorSpace:this.colorSpace,SMask:n}),t?(r.assign(t,i),[2,t]):[2,r.register(i)]})})},e.prototype.embedAlphaChannel=function(r){if(this.image.alphaChannel){var t=r.flateStream(this.image.alphaChannel,{Type:"XObject",Subtype:"Image",Height:this.image.height,Width:this.image.width,BitsPerComponent:this.image.bitsPerComponent,ColorSpace:"DeviceGray",Decode:[0,1]});return r.register(t)}},e})(),od=Ng,Ig=(function(){function e(r,t,n){this.bytes=r,this.start=t||0,this.pos=this.start,this.end=t&&n?t+n:this.bytes.length}return Object.defineProperty(e.prototype,"length",{get:function(){return this.end-this.start},enumerable:!1,configurable:!0}),Object.defineProperty(e.prototype,"isEmpty",{get:function(){return this.length===0},enumerable:!1,configurable:!0}),e.prototype.getByte=function(){return this.pos>=this.end?-1:this.bytes[this.pos++]},e.prototype.getUint16=function(){var r=this.getByte(),t=this.getByte();return r===-1||t===-1?-1:(r<<8)+t},e.prototype.getInt32=function(){var r=this.getByte(),t=this.getByte(),n=this.getByte(),i=this.getByte();return(r<<24)+(t<<16)+(n<<8)+i},e.prototype.getBytes=function(r,t){t===void 0&&(t=!1);var n=this.bytes,i=this.pos,a=this.end;if(r){var l=i+r;l>a&&(l=a),this.pos=l;var o=n.subarray(i,l);return t?new Uint8ClampedArray(o):o}else{var o=n.subarray(i,a);return t?new Uint8ClampedArray(o):o}},e.prototype.peekByte=function(){var r=this.getByte();return this.pos--,r},e.prototype.peekBytes=function(r,t){t===void 0&&(t=!1);var n=this.getBytes(r,t);return this.pos-=n.length,n},e.prototype.skip=function(r){r||(r=1),this.pos+=r},e.prototype.reset=function(){this.pos=this.start},e.prototype.moveStart=function(){this.start=this.pos},e.prototype.makeSubStream=function(r,t){return new e(this.bytes,r,t)},e.prototype.decode=function(){return this.bytes},e})(),sd=Ig,zg=new Uint8Array(0),Mg=(function(){function e(r){if(this.pos=0,this.bufferLength=0,this.eof=!1,this.buffer=zg,this.minBufferLength=512,r)for(;this.minBufferLengtha&&(n=a)}else{for(;!this.eof;)this.readBlock();n=this.bufferLength}this.pos=n;var o=this.buffer.subarray(i,n);return t&&!(o instanceof Uint8ClampedArray)?new Uint8ClampedArray(o):o},e.prototype.peekByte=function(){var r=this.getByte();return this.pos--,r},e.prototype.peekBytes=function(r,t){t===void 0&&(t=!1);var n=this.getBytes(r,t);return this.pos-=n.length,n},e.prototype.skip=function(r){r||(r=1),this.pos+=r},e.prototype.reset=function(){this.pos=0},e.prototype.makeSubStream=function(r,t){for(var n=r+t;this.bufferLength<=n&&!this.eof;)this.readBlock();return new sd(this.buffer,r,t)},e.prototype.decode=function(){for(;!this.eof;)this.readBlock();return this.buffer.subarray(0,this.bufferLength)},e.prototype.readBlock=function(){throw new br(this.constructor.name,"readBlock")},e.prototype.ensureBuffer=function(r){var t=this.buffer;if(r<=t.byteLength)return t;for(var n=this.minBufferLength;n=0;--s)c[l+s]=d&255,d>>=8}},r})(sa),_g=Og,Ug=(function(e){Te(r,e);function r(t,n){var i=e.call(this,n)||this;return i.stream=t,i.firstDigit=-1,n&&(n=.5*n),i}return r.prototype.readBlock=function(){var t=8e3,n=this.stream.getBytes(t);if(!n.length){this.eof=!0;return}for(var i=n.length+1>>1,a=this.ensureBuffer(this.bufferLength+i),o=this.bufferLength,l=this.firstDigit,c=0,s=n.length;c=48&&u<=57)d=u&15;else if(u>=65&&u<=70||u>=97&&u<=102)d=(u&15)+9;else if(u===62){this.eof=!0;break}else continue;l<0?l=d:(a[o++]=l<<4|d,l=-1)}l>=0&&this.eof&&(a[o++]=l<<4,l=-1),this.firstDigit=l,this.bufferLength=o},r})(sa),Wg=Ug,cu=new Int32Array([16,17,18,0,8,7,9,6,10,5,11,4,12,3,13,2,14,1,15]),Gg=new Int32Array([3,4,5,6,7,8,9,10,65547,65549,65551,65553,131091,131095,131099,131103,196643,196651,196659,196667,262211,262227,262243,262259,327811,327843,327875,327907,258,258,258]),jg=new Int32Array([1,2,3,4,65541,65543,131081,131085,196625,196633,262177,262193,327745,327777,393345,393409,459009,459137,524801,525057,590849,591361,657409,658433,724993,727041,794625,798721,868353,876545]),qg=[new Int32Array([459008,524368,524304,524568,459024,524400,524336,590016,459016,524384,524320,589984,524288,524416,524352,590048,459012,524376,524312,589968,459028,524408,524344,590032,459020,524392,524328,59e4,524296,524424,524360,590064,459010,524372,524308,524572,459026,524404,524340,590024,459018,524388,524324,589992,524292,524420,524356,590056,459014,524380,524316,589976,459030,524412,524348,590040,459022,524396,524332,590008,524300,524428,524364,590072,459009,524370,524306,524570,459025,524402,524338,590020,459017,524386,524322,589988,524290,524418,524354,590052,459013,524378,524314,589972,459029,524410,524346,590036,459021,524394,524330,590004,524298,524426,524362,590068,459011,524374,524310,524574,459027,524406,524342,590028,459019,524390,524326,589996,524294,524422,524358,590060,459015,524382,524318,589980,459031,524414,524350,590044,459023,524398,524334,590012,524302,524430,524366,590076,459008,524369,524305,524569,459024,524401,524337,590018,459016,524385,524321,589986,524289,524417,524353,590050,459012,524377,524313,589970,459028,524409,524345,590034,459020,524393,524329,590002,524297,524425,524361,590066,459010,524373,524309,524573,459026,524405,524341,590026,459018,524389,524325,589994,524293,524421,524357,590058,459014,524381,524317,589978,459030,524413,524349,590042,459022,524397,524333,590010,524301,524429,524365,590074,459009,524371,524307,524571,459025,524403,524339,590022,459017,524387,524323,589990,524291,524419,524355,590054,459013,524379,524315,589974,459029,524411,524347,590038,459021,524395,524331,590006,524299,524427,524363,590070,459011,524375,524311,524575,459027,524407,524343,590030,459019,524391,524327,589998,524295,524423,524359,590062,459015,524383,524319,589982,459031,524415,524351,590046,459023,524399,524335,590014,524303,524431,524367,590078,459008,524368,524304,524568,459024,524400,524336,590017,459016,524384,524320,589985,524288,524416,524352,590049,459012,524376,524312,589969,459028,524408,524344,590033,459020,524392,524328,590001,524296,524424,524360,590065,459010,524372,524308,524572,459026,524404,524340,590025,459018,524388,524324,589993,524292,524420,524356,590057,459014,524380,524316,589977,459030,524412,524348,590041,459022,524396,524332,590009,524300,524428,524364,590073,459009,524370,524306,524570,459025,524402,524338,590021,459017,524386,524322,589989,524290,524418,524354,590053,459013,524378,524314,589973,459029,524410,524346,590037,459021,524394,524330,590005,524298,524426,524362,590069,459011,524374,524310,524574,459027,524406,524342,590029,459019,524390,524326,589997,524294,524422,524358,590061,459015,524382,524318,589981,459031,524414,524350,590045,459023,524398,524334,590013,524302,524430,524366,590077,459008,524369,524305,524569,459024,524401,524337,590019,459016,524385,524321,589987,524289,524417,524353,590051,459012,524377,524313,589971,459028,524409,524345,590035,459020,524393,524329,590003,524297,524425,524361,590067,459010,524373,524309,524573,459026,524405,524341,590027,459018,524389,524325,589995,524293,524421,524357,590059,459014,524381,524317,589979,459030,524413,524349,590043,459022,524397,524333,590011,524301,524429,524365,590075,459009,524371,524307,524571,459025,524403,524339,590023,459017,524387,524323,589991,524291,524419,524355,590055,459013,524379,524315,589975,459029,524411,524347,590039,459021,524395,524331,590007,524299,524427,524363,590071,459011,524375,524311,524575,459027,524407,524343,590031,459019,524391,524327,589999,524295,524423,524359,590063,459015,524383,524319,589983,459031,524415,524351,590047,459023,524399,524335,590015,524303,524431,524367,590079]),9],Vg=[new Int32Array([327680,327696,327688,327704,327684,327700,327692,327708,327682,327698,327690,327706,327686,327702,327694,0,327681,327697,327689,327705,327685,327701,327693,327709,327683,327699,327691,327707,327687,327703,327695,0]),5],Xg=(function(e){Te(r,e);function r(t,n){var i=e.call(this,n)||this;i.stream=t;var a=t.getByte(),o=t.getByte();if(a===-1||o===-1)throw new Error("Invalid header in flate stream: "+a+", "+o);if((a&15)!==8)throw new Error("Unknown compression method in flate stream: "+a+", "+o);if(((a<<8)+o)%31!==0)throw new Error("Bad FCHECK in flate stream: "+a+", "+o);if(o&32)throw new Error("FDICT bit set in flate stream: "+a+", "+o);return i.codeSize=0,i.codeBuf=0,i}return r.prototype.readBlock=function(){var t,n,i=this.stream,a=this.getBits(3);if(a&1&&(this.eof=!0),a>>=1,a===0){var o=void 0;if((o=i.getByte())===-1)throw new Error("Bad block header in flate stream");var l=o;if((o=i.getByte())===-1)throw new Error("Bad block header in flate stream");if(l|=o<<8,(o=i.getByte())===-1)throw new Error("Bad block header in flate stream");var c=o;if((o=i.getByte())===-1)throw new Error("Bad block header in flate stream");if(c|=o<<8,c!==(~l&65535)&&(l!==0||c!==0))throw new Error("Bad uncompressed block length in flate stream");this.codeBuf=0,this.codeSize=0;var s=this.bufferLength;t=this.ensureBuffer(s+l);var u=s+l;if(this.bufferLength=u,l===0)i.peekByte()===-1&&(this.eof=!0);else for(var d=s;d0;)F[b++]=B}A=this.generateHuffmanTable(F.subarray(0,p)),f=this.generateHuffmanTable(F.subarray(p,P))}else throw new Error("Unknown block type in flate stream");t=this.buffer;for(var H=t?t.length:0,C=this.bufferLength;;){var I=this.getCode(A);if(I<256){C+1>=H&&(t=this.ensureBuffer(C+1),H=t.length),t[C++]=I;continue}if(I===256){this.bufferLength=C;return}I-=257,I=Gg[I];var m=I>>16;m>0&&(m=this.getBits(m)),n=(I&65535)+m,I=this.getCode(f),I=jg[I],m=I>>16,m>0&&(m=this.getBits(m));var N=(I&65535)+m;C+n>=H&&(t=this.ensureBuffer(C+n),H=t.length);for(var ne=0;ne>t,this.codeSize=i-=t,o},r.prototype.getCode=function(t){for(var n=this.stream,i=t[0],a=t[1],o=this.codeSize,l=this.codeBuf,c;o>16,d=s&65535;if(u<1||o>u,this.codeSize=o-u,d},r.prototype.generateHuffmanTable=function(t){var n=t.length,i=0,a;for(a=0;ai&&(i=t[a]);for(var o=1<>=1;for(a=A;a0;if(!F||F<256)y[0]=F,v=1;else if(F>=258)if(F=0;o--)y[o]=d[l],l=f[l];else y[v++]=y[0];else if(F===256){p=9,u=258,v=0;continue}else{this.eof=!0,delete this.lzwState;break}if(R&&(f[u]=g,A[u]=A[g]+1,d[u]=y[0],u++,p=u+s&u+s-1?p:Math.min(Math.log(u+s)/.6931471805599453+1,12)|0),g=F,b+=v,n>>n&(1<0){var o=this.stream.getBytes(a);n.set(o,i),i+=a}}else{a=257-a;var l=t[1];n=this.ensureBuffer(i+a+1);for(var c=0;cn.size())throw new so(t,0,n.size());n.remove(t)}else{if(t!==0)throw new so(t,0,0);this.setKids([])}},r.prototype.normalizedEntries=function(){var t=this.Kids();return t||(t=this.dict.context.obj([this.ref]),this.dict.set(V.of("Kids"),t)),{Kids:t}},r.fromDict=function(t,n){return new r(t,n)},r})(ud),pi=cm,um=(function(e){Te(r,e);function r(){return e!==null&&e.apply(this,arguments)||this}return r.prototype.Opt=function(){return this.dict.lookupMaybe(V.of("Opt"),wt,qe,vt)},r.prototype.setOpt=function(t){this.dict.set(V.of("Opt"),this.dict.context.obj(t))},r.prototype.getExportValues=function(){var t=this.Opt();if(t){if(t instanceof wt||t instanceof qe)return[t];for(var n=[],i=0,a=t.size();in.size())throw new so(t,0,n.size());n.remove(t)}},r.prototype.normalizeExportValues=function(){for(var t,n,i,a,o=(t=this.getExportValues())!==null&&t!==void 0?t:[],l=[],c=this.getWidgets(),s=0,u=c.length;s1){if(!this.hasFlag(bt.MultiSelect))throw new kA;this.dict.set(V.of("V"),this.dict.context.obj(t))}this.updateSelectedIndices(t)},r.prototype.valuesAreValid=function(t){for(var n=this.getOptions(),i=function(c,s){var u=t[c].decodeText();if(!n.find(function(d){return u===(d.display||d.value).decodeText()}))return{value:!1}},a=0,o=t.length;a1){for(var n=new Array(t.length),i=this.getOptions(),a=function(c,s){var u=t[c].decodeText();n[c]=i.findIndex(function(d){return u===(d.display||d.value).decodeText()})},o=0,l=t.length;o0){var l=o.lookup(0,wt,qe),c=o.lookupMaybe(1,wt,qe);n.push({value:l,display:c||l})}}return n}return[]},r})(pi),dd=fm,hm=(function(e){Te(r,e);function r(){return e!==null&&e.apply(this,arguments)||this}return r.fromDict=function(t,n){return new r(t,n)},r.create=function(t){var n=t.obj({FT:"Ch",Ff:bt.Combo,Kids:[]}),i=t.register(n);return new r(n,i)},r})(dd),To=hm,pm=(function(e){Te(r,e);function r(){return e!==null&&e.apply(this,arguments)||this}return r.prototype.addField=function(t){var n=this.normalizedEntries().Kids;n?.push(t)},r.prototype.normalizedEntries=function(){var t=this.Kids();return t||(t=this.dict.context.obj([]),this.dict.set(V.of("Kids"),t)),{Kids:t}},r.fromDict=function(t,n){return new r(t,n)},r.create=function(t){var n=t.obj({}),i=t.register(n);return new r(n,i)},r})(ud),uo=pm,Am=(function(e){Te(r,e);function r(){return e!==null&&e.apply(this,arguments)||this}return r.fromDict=function(t,n){return new r(t,n)},r})(pi),fl=Am,gm=(function(e){Te(r,e);function r(){return e!==null&&e.apply(this,arguments)||this}return r.prototype.MaxLen=function(){var t=this.dict.lookup(V.of("MaxLen"));if(t instanceof Qe)return t},r.prototype.Q=function(){var t=this.dict.lookup(V.of("Q"));if(t instanceof Qe)return t},r.prototype.setMaxLength=function(t){this.dict.set(V.of("MaxLen"),Qe.of(t))},r.prototype.removeMaxLength=function(){this.dict.delete(V.of("MaxLen"))},r.prototype.getMaxLength=function(){var t;return(t=this.MaxLen())===null||t===void 0?void 0:t.asNumber()},r.prototype.setQuadding=function(t){this.dict.set(V.of("Q"),Qe.of(t))},r.prototype.getQuadding=function(){var t;return(t=this.Q())===null||t===void 0?void 0:t.asNumber()},r.prototype.setValue=function(t){this.dict.set(V.of("V"),t)},r.prototype.removeValue=function(){this.dict.delete(V.of("V"))},r.prototype.getValue=function(){var t=this.V();if(t instanceof wt||t instanceof qe)return t},r.fromDict=function(t,n){return new r(t,n)},r.create=function(t){var n=t.obj({FT:"Tx",Kids:[]}),i=t.register(n);return new r(n,i)},r})(pi),Do=gm,mm=(function(e){Te(r,e);function r(){return e!==null&&e.apply(this,arguments)||this}return r.fromDict=function(t,n){return new r(t,n)},r.create=function(t){var n=t.obj({FT:"Btn",Ff:mr.PushButton,Kids:[]}),i=t.register(n);return new r(n,i)},r})(dl),Eo=mm,vm=(function(e){Te(r,e);function r(){return e!==null&&e.apply(this,arguments)||this}return r.prototype.setValue=function(t){var n=this.getOnValues();if(!n.includes(t)&&t!==V.of("Off"))throw new ol;this.dict.set(V.of("V"),t);for(var i=this.getWidgets(),a=0,o=i.length;aa)throw new Yc(n,a);for(var o=n,l=0,c=i.size();lo)return u.insertLeafNode(t,o)||s;o-=u.Count().asNumber()}u instanceof Kr&&(o-=1)}if(o===0){this.insertLeafKid(i.size(),t);return}throw new Jc(n,"insertLeafNode")},r.prototype.removeLeafNode=function(t,n){n===void 0&&(n=!0);var i=this.Kids(),a=this.Count().asNumber();if(t>=a)throw new Yc(t,a);for(var o=t,l=0,c=i.size();lo){u.removeLeafNode(o,n),n&&u.Kids().size()===0&&i.remove(l);return}else o-=u.Count().asNumber();if(u instanceof Kr)if(o===0){this.removeKid(l);return}else o-=1}throw new Jc(t,"removeLeafNode")},r.prototype.ascend=function(t){t(this);var n=this.Parent();n&&n.ascend(t)},r.prototype.traverse=function(t){for(var n=this.Kids(),i=0,a=n.size();iNumber.MAX_SAFE_INTEGER)if(this.capNumbers){var i="Parsed number that is too large for some PDF readers: "+r+", using Number.MAX_SAFE_INTEGER instead.";return console.warn(i),Number.MAX_SAFE_INTEGER}else{var i="Parsed number that is too large for some PDF readers: "+r+", not capping.";console.warn(i)}return n},e.prototype.skipWhitespace=function(){for(;!this.bytes.done()&&Yr[this.bytes.peek()];)this.bytes.next()},e.prototype.skipLine=function(){for(;!this.bytes.done();){var r=this.bytes.peek();if(r===fu||r===hu)return;this.bytes.next()}},e.prototype.skipComment=function(){if(this.bytes.peek()!==Q.Percent)return!1;for(;!this.bytes.done();){var r=this.bytes.peek();if(r===fu||r===hu)return!0;this.bytes.next()}return!0},e.prototype.skipWhitespaceAndComments=function(){for(this.skipWhitespace();this.skipComment();)this.skipWhitespace()},e.prototype.matchKeyword=function(r){for(var t=this.bytes.offset(),n=0,i=r.length;n=this.length},e.prototype.offset=function(){return this.idx},e.prototype.slice=function(r,t){return this.bytes.slice(r,t)},e.prototype.position=function(){return{line:this.line,column:this.column,offset:this.idx}},e.of=function(r){return new e(r)},e.fromPDFRawStream=function(r){return e.of(ld(r).decode())},e})(),No=Dm,Em=Q.Space,Li=Q.CarriageReturn,Ni=Q.Newline,Ii=[Q.s,Q.t,Q.r,Q.e,Q.a,Q.m],ja=[Q.e,Q.n,Q.d,Q.s,Q.t,Q.r,Q.e,Q.a,Q.m],Ct={header:[Q.Percent,Q.P,Q.D,Q.F,Q.Dash],eof:[Q.Percent,Q.Percent,Q.E,Q.O,Q.F],obj:[Q.o,Q.b,Q.j],endobj:[Q.e,Q.n,Q.d,Q.o,Q.b,Q.j],xref:[Q.x,Q.r,Q.e,Q.f],trailer:[Q.t,Q.r,Q.a,Q.i,Q.l,Q.e,Q.r],startxref:[Q.s,Q.t,Q.a,Q.r,Q.t,Q.x,Q.r,Q.e,Q.f],true:[Q.t,Q.r,Q.u,Q.e],false:[Q.f,Q.a,Q.l,Q.s,Q.e],null:[Q.n,Q.u,Q.l,Q.l],stream:Ii,streamEOF1:ht(Ii,[Em,Li,Ni]),streamEOF2:ht(Ii,[Li,Ni]),streamEOF3:ht(Ii,[Li]),streamEOF4:ht(Ii,[Ni]),endstream:ja,EOF1endstream:ht([Li,Ni],ja),EOF2endstream:ht([Li],ja),EOF3endstream:ht([Ni],ja)},Bm=(function(e){Te(r,e);function r(t,n,i){i===void 0&&(i=!1);var a=e.call(this,t,i)||this;return a.context=n,a}return r.prototype.parseObject=function(){if(this.skipWhitespaceAndComments(),this.matchKeyword(Ct.true))return Ji.True;if(this.matchKeyword(Ct.false))return Ji.False;if(this.matchKeyword(Ct.null))return rr;var t=this.bytes.peek();if(t===Q.LessThan&&this.bytes.peekAhead(1)===Q.LessThan)return this.parseDictOrStream();if(t===Q.LessThan)return this.parseHexString();if(t===Q.LeftParen)return this.parseString();if(t===Q.ForwardSlash)return this.parseName();if(t===Q.LeftSquareBracket)return this.parseArray();if(Al[t])return this.parseNumberOrRef();throw new TA(this.bytes.position(),t)},r.prototype.parseNumberOrRef=function(){var t=this.parseRawNumber();this.skipWhitespaceAndComments();var n=this.bytes.offset();if(Kt[this.bytes.peek()]){var i=this.parseRawNumber();if(this.skipWhitespaceAndComments(),this.bytes.peek()===Q.R)return this.bytes.assertNext(Q.R),xt.of(t,i)}return this.bytes.moveTo(n),Qe.of(t)},r.prototype.parseHexString=function(){var t="";for(this.bytes.assertNext(Q.LessThan);!this.bytes.done()&&this.bytes.peek()!==Q.GreaterThan;)t+=Xr(this.bytes.next());return this.bytes.assertNext(Q.GreaterThan),qe.of(t)},r.prototype.parseString=function(){for(var t=0,n=!1,i="";!this.bytes.done();){var a=this.bytes.next();if(i+=Xr(a),n||(a===Q.LeftParen&&(t+=1),a===Q.RightParen&&(t-=1)),a===Q.BackSlash?n=!n:n&&(n=!1),t===0)return wt.of(i.substring(1,i.length-1))}throw new BA(this.bytes.position())},r.prototype.parseName=function(){this.bytes.assertNext(Q.ForwardSlash);for(var t="";!this.bytes.done();){var n=this.bytes.peek();if(Yr[n]||kr[n])break;t+=Xr(n),this.bytes.next()}return V.of(t)},r.prototype.parseArray=function(){this.bytes.assertNext(Q.LeftSquareBracket),this.skipWhitespaceAndComments();for(var t=vt.withContext(this.context);this.bytes.peek()!==Q.RightSquareBracket;){var n=this.parseObject();t.push(n),this.skipWhitespaceAndComments()}return this.bytes.assertNext(Q.RightSquareBracket),t},r.prototype.parseDict=function(){this.bytes.assertNext(Q.LessThan),this.bytes.assertNext(Q.LessThan),this.skipWhitespaceAndComments();for(var t=new Map;!this.bytes.done()&&this.bytes.peek()!==Q.GreaterThan&&this.bytes.peekAhead(1)!==Q.GreaterThan;){var n=this.parseName(),i=this.parseObject();t.set(n,i),this.skipWhitespaceAndComments()}this.skipWhitespaceAndComments(),this.bytes.assertNext(Q.GreaterThan),this.bytes.assertNext(Q.GreaterThan);var a=t.get(V.of("Type"));return a===V.of("Catalog")?pd.fromMapWithContext(t,this.context):a===V.of("Pages")?Ad.fromMapWithContext(t,this.context):a===V.of("Page")?Kr.fromMapWithContext(t,this.context):et.fromMapWithContext(t,this.context)},r.prototype.parseDictOrStream=function(){var t=this.bytes.position(),n=this.parseDict();if(this.skipWhitespaceAndComments(),!this.matchKeyword(Ct.streamEOF1)&&!this.matchKeyword(Ct.streamEOF2)&&!this.matchKeyword(Ct.streamEOF3)&&!this.matchKeyword(Ct.streamEOF4)&&!this.matchKeyword(Ct.stream))return n;var i=this.bytes.offset(),a,o=n.get(V.of("Length"));o instanceof Qe?(a=i+o.asNumber(),this.bytes.moveTo(a),this.skipWhitespaceAndComments(),this.matchKeyword(Ct.endstream)||(this.bytes.moveTo(i),a=this.findEndOfStreamFallback(t))):a=this.findEndOfStreamFallback(t);var l=this.bytes.slice(i,a);return $i.of(n,l)},r.prototype.findEndOfStreamFallback=function(t){for(var n=1,i=this.bytes.offset();!this.bytes.done()&&(i=this.bytes.offset(),this.matchKeyword(Ct.stream)?n+=1:this.matchKeyword(Ct.EOF1endstream)||this.matchKeyword(Ct.EOF2endstream)||this.matchKeyword(Ct.EOF3endstream)||this.matchKeyword(Ct.endstream)?n-=1:this.bytes.next(),n!==0););if(n!==0)throw new EA(t);return i},r.forBytes=function(t,n,i){return new r(No.of(t),n,i)},r.forByteStream=function(t,n,i){return i===void 0&&(i=!1),new r(t,n,i)},r})(Tm),gd=Bm,Rm=(function(e){Te(r,e);function r(t,n){var i=e.call(this,No.fromPDFRawStream(t),t.dict.context)||this,a=t.dict;return i.alreadyParsed=!1,i.shouldWaitForTick=n||(function(){return!1}),i.firstOffset=a.lookup(V.of("First"),Qe).asNumber(),i.objectCount=a.lookup(V.of("N"),Qe).asNumber(),i}return r.prototype.parseIntoContext=function(){return it(this,void 0,void 0,function(){var t,n,i,a,o,l,c,s;return at(this,function(u){switch(u.label){case 0:if(this.alreadyParsed)throw new al("PDFObjectStreamParser","parseIntoContext");this.alreadyParsed=!0,t=this.parseOffsetsAndObjectNumbers(),n=0,i=t.length,u.label=1;case 1:return n=Q.Space&&n<=Q.Tilde;if(i&&(this.matchKeyword(Ct.xref)||this.matchKeyword(Ct.trailer)||this.matchKeyword(Ct.startxref)||this.matchIndirectObjectHeader())){this.bytes.moveTo(t);break}this.bytes.next()}},r.prototype.skipBinaryHeaderComment=function(){this.skipWhitespaceAndComments();try{var t=this.bytes.offset();this.parseIndirectObjectHeader(),this.bytes.moveTo(t)}catch{this.bytes.next(),this.skipWhitespaceAndComments()}},r.forBytesWithOptions=function(t,n,i,a){return new r(t,n,i,a)},r})(gd),Mm=zm,Ir=function(e){return 1<0&&(n[n.length]=+i),t[t.length]={cmd:r,args:n},n=[],i="",a=!1),r=s;else if([" ",","].includes(s)||s==="-"&&i.length>0&&i[i.length-1]!=="e"||s==="."&&a){if(i.length===0)continue;n.length===o?(t[t.length]={cmd:r,args:n},n=[+i],r==="M"&&(r="L"),r==="m"&&(r="l")):n[n.length]=+i,a=s===".",i=["-","."].includes(s)?s:""}else i+=s,s==="."&&(a=!0)}return i.length>0&&(n.length===o?(t[t.length]={cmd:r,args:n},n=[+i],r==="M"&&(r="L"),r==="m"&&(r="l")):n[n.length]=+i),t[t.length]={cmd:r,args:n},t},Zm=function(e){Ue=We=ct=ut=_i=Ui=0;for(var r=[],t=0;t1&&(A=Math.sqrt(A),t*=A,n*=A);var f=d/t,p=u/t,g=-u/n,y=d/n,v=f*l+p*c,b=g*l+y*c,x=f*e+p*r,P=g*e+y*r,F=(x-v)*(x-v)+(P-b)*(P-b),R=1/F-.25;R<0&&(R=0);var E=Math.sqrt(R);a===i&&(E=-E);var B=.5*(v+x)-E*(P-b),T=.5*(b+P)+E*(x-v),W=Math.atan2(b-T,v-B),H=Math.atan2(P-T,x-B),C=H-W;C<0&&a===1?C+=2*Math.PI:C>0&&a===0&&(C-=2*Math.PI);for(var I=Math.ceil(Math.abs(C/(Math.PI*.5+.001))),m=[],N=0;Ne.length)return i-1;var y=r.heightAtSize(i),v=y+y*.2,b=v*a;if(b>Math.abs(t.height))return i-1;i+=1}return i},vv=function(e,r,t,n){for(var i=t.width/n,a=t.height,o=Nd,l=kp(e);oi*.75;if(d)return o-1}var A=r.heightAtSize(o,{descender:!1});if(A>a)return o-1;o+=1}return o},yv=function(e){for(var r=e.length;r>0;r--)if(/\s/.test(e[r]))return r},bv=function(e,r,t,n){for(var i,a=e.length;a>0;){var o=e.substring(0,a),l=t.encodeText(o),c=t.widthOfTextAtSize(o,n);if(cA&&(A=E+F),p+l>f&&(f=p+l),s.push({text:x,encoded:P,width:F,height:l,x:E,y:p}),v=R?.trim()}return{fontSize:n,lineHeight:c,lines:s,bounds:{x:u,y:d,width:A-u,height:f-d}}},wv=function(e,r){var t=r.fontSize,n=r.font,i=r.bounds,a=r.cellCount,o=Uu(aa(e));if(o.length>a)throw new Av(o.length,a);(t===void 0||t===0)&&(t=vv(o,n,i,a));for(var l=i.width/a,c=n.heightAtSize(t,{descender:!1}),s=i.y+(i.height/2-c/2),u=[],d=i.x,A=i.y,f=i.x+i.width,p=i.y+i.height,g=0,y=0;gf&&(f=E+F),s+c>p&&(p=s+c),u.push({text:o,encoded:P,width:F,height:c,x:E,y:s}),g+=1,y+=x}return{fontSize:t,cells:u,bounds:{x:d,y:A,width:f-d,height:p-A}}},mo=function(e,r){var t=r.alignment,n=r.fontSize,i=r.font,a=r.bounds,o=Uu(aa(e));(n===void 0||n===0)&&(n=zd([o],i,a));var l=i.encodeText(o),c=i.widthOfTextAtSize(o,n),s=i.heightAtSize(n,{descender:!1}),u=t===zt.Left?a.x:t===zt.Center?a.x+a.width/2-c/2:t===zt.Right?a.x+a.width-c:a.x,d=a.y+(a.height/2-s/2);return{fontSize:n,line:{text:o,encoded:l,width:c,height:s,x:u,y:d},bounds:{x:u,y:d,width:c,height:s}}},gi=function(e){return"normal"in e?e:{normal:e}},xv=/\/([^\0\t\n\f\r\ ]+)[\0\t\n\f\r\ ]+(\d*\.\d+|\d+)[\0\t\n\f\r\ ]+Tf/,wn=function(e){var r,t,n=(r=e.getDefaultAppearance())!==null&&r!==void 0?r:"",i=(t=nl(n,xv).match)!==null&&t!==void 0?t:[],a=Number(i[2]);return isFinite(a)?a:void 0},Sv=/(\d*\.\d+|\d+)[\0\t\n\f\r\ ]*(\d*\.\d+|\d+)?[\0\t\n\f\r\ ]*(\d*\.\d+|\d+)?[\0\t\n\f\r\ ]*(\d*\.\d+|\d+)?[\0\t\n\f\r\ ]+(g|rg|k)/,xr=function(e){var r,t=(r=e.getDefaultAppearance())!==null&&r!==void 0?r:"",n=nl(t,Sv).match,i=n??[],a=i[1],o=i[2],l=i[3],c=i[4],s=i[5];if(s==="g"&&a)return Dd(Number(a));if(s==="rg"&&a&&o&&l)return St(Number(a),Number(o),Number(l));if(s==="k"&&a&&o&&l&&c)return Ed(Number(a),Number(o),Number(l),Number(c))},Sr=function(e,r,t,n){var i;n===void 0&&(n=0);var a=[jn(r).toString(),yl((i=t?.name)!==null&&i!==void 0?i:"dummy__noop",n).toString()].join(` +`);e.setDefaultAppearance(a)},kv=function(e,r){var t,n,i,a=xr(r),o=xr(e.acroField),l=r.getRectangle(),c=r.getAppearanceCharacteristics(),s=r.getBorderStyle(),u=(t=s?.getWidth())!==null&&t!==void 0?t:0,d=Jr(c?.getRotation()),A=Wn(l,d),f=A.width,p=A.height,g=Cn(Je(Je({},l),{rotation:d})),y=St(0,0,0),v=(n=er(c?.getBorderColor()))!==null&&n!==void 0?n:y,b=er(c?.getBackgroundColor()),x=er(c?.getBackgroundColor(),.8),P=(i=a??o)!==null&&i!==void 0?i:y;Sr(a?r:e.acroField,P);var F={x:0+u/2,y:0+u/2,width:f-u,height:p-u,thickness:1.5,borderWidth:u,borderColor:v,markColor:P};return{normal:{on:ht(g,Xa(Je(Je({},F),{color:b,filled:!0}))),off:ht(g,Xa(Je(Je({},F),{color:b,filled:!1})))},down:{on:ht(g,Xa(Je(Je({},F),{color:x,filled:!0}))),off:ht(g,Xa(Je(Je({},F),{color:x,filled:!1})))}}},Cv=function(e,r){var t,n,i,a=xr(r),o=xr(e.acroField),l=r.getRectangle(),c=r.getAppearanceCharacteristics(),s=r.getBorderStyle(),u=(t=s?.getWidth())!==null&&t!==void 0?t:0,d=Jr(c?.getRotation()),A=Wn(l,d),f=A.width,p=A.height,g=Cn(Je(Je({},l),{rotation:d})),y=St(0,0,0),v=(n=er(c?.getBorderColor()))!==null&&n!==void 0?n:y,b=er(c?.getBackgroundColor()),x=er(c?.getBackgroundColor(),.8),P=(i=a??o)!==null&&i!==void 0?i:y;Sr(a?r:e.acroField,P);var F={x:f/2,y:p/2,width:f-u,height:p-u,borderWidth:u,borderColor:v,dotColor:P};return{normal:{on:ht(g,Ha(Je(Je({},F),{color:b,filled:!0}))),off:ht(g,Ha(Je(Je({},F),{color:b,filled:!1})))},down:{on:ht(g,Ha(Je(Je({},F),{color:x,filled:!0}))),off:ht(g,Ha(Je(Je({},F),{color:x,filled:!1})))}}},Pv=function(e,r,t){var n,i,a,o,l,c=xr(r),s=xr(e.acroField),u=wn(r),d=wn(e.acroField),A=r.getRectangle(),f=r.getAppearanceCharacteristics(),p=r.getBorderStyle(),g=f?.getCaptions(),y=(n=g?.normal)!==null&&n!==void 0?n:"",v=(a=(i=g?.down)!==null&&i!==void 0?i:y)!==null&&a!==void 0?a:"",b=(o=p?.getWidth())!==null&&o!==void 0?o:0,x=Jr(f?.getRotation()),P=Wn(A,x),F=P.width,R=P.height,E=Cn(Je(Je({},A),{rotation:x})),B=St(0,0,0),T=er(f?.getBorderColor()),W=er(f?.getBackgroundColor()),H=er(f?.getBackgroundColor(),.8),C={x:b,y:b,width:F-b*2,height:R-b*2},I=mo(y,{alignment:zt.Center,fontSize:u??d,font:t,bounds:C}),m=mo(v,{alignment:zt.Center,fontSize:u??d,font:t,bounds:C}),N=Math.min(I.fontSize,m.fontSize),ne=(l=c??s)!==null&&l!==void 0?l:B;Sr(c||u!==void 0?r:e.acroField,ne,t,N);var q={x:0+b/2,y:0+b/2,width:F-b,height:R-b,borderWidth:b,borderColor:T,textColor:ne,font:t.name,fontSize:N};return{normal:ht(E,bu(Je(Je({},q),{color:W,textLines:[I.line]}))),down:ht(E,bu(Je(Je({},q),{color:H,textLines:[m.line]})))}},Fv=function(e,r,t){var n,i,a,o,l=xr(r),c=xr(e.acroField),s=wn(r),u=wn(e.acroField),d=r.getRectangle(),A=r.getAppearanceCharacteristics(),f=r.getBorderStyle(),p=(n=e.getText())!==null&&n!==void 0?n:"",g=(i=f?.getWidth())!==null&&i!==void 0?i:0,y=Jr(A?.getRotation()),v=Wn(d,y),b=v.width,x=v.height,P=Cn(Je(Je({},d),{rotation:y})),F=St(0,0,0),R=er(A?.getBorderColor()),E=er(A?.getBackgroundColor()),B,T,W=e.isCombed()?0:1,H={x:g+W,y:g+W,width:b-(g+W)*2,height:x-(g+W)*2};if(e.isMultiline()){var C=Md(p,{alignment:e.getAlignment(),fontSize:s??u,font:t,bounds:H});B=C.lines,T=C.fontSize}else if(e.isCombed()){var C=wv(p,{fontSize:s??u,font:t,bounds:H,cellCount:(a=e.getMaxLength())!==null&&a!==void 0?a:0});B=C.cells,T=C.fontSize}else{var C=mo(p,{alignment:e.getAlignment(),fontSize:s??u,font:t,bounds:H});B=[C.line],T=C.fontSize}var I=(o=l??c)!==null&&o!==void 0?o:F;Sr(l||s!==void 0?r:e.acroField,I,t,T);var m={x:0+g/2,y:0+g/2,width:b-g,height:x-g,borderWidth:g??0,borderColor:R,textColor:I,font:t.name,fontSize:T,color:E,textLines:B,padding:W};return ht(P,Rd(m))},Tv=function(e,r,t){var n,i,a,o=xr(r),l=xr(e.acroField),c=wn(r),s=wn(e.acroField),u=r.getRectangle(),d=r.getAppearanceCharacteristics(),A=r.getBorderStyle(),f=(n=e.getSelected()[0])!==null&&n!==void 0?n:"",p=(i=A?.getWidth())!==null&&i!==void 0?i:0,g=Jr(d?.getRotation()),y=Wn(u,g),v=y.width,b=y.height,x=Cn(Je(Je({},u),{rotation:g})),P=St(0,0,0),F=er(d?.getBorderColor()),R=er(d?.getBackgroundColor()),E=1,B={x:p+E,y:p+E,width:v-(p+E)*2,height:b-(p+E)*2},T=mo(f,{alignment:zt.Left,fontSize:c??s,font:t,bounds:B}),W=T.line,H=T.fontSize,C=(a=o??l)!==null&&a!==void 0?a:P;Sr(o||c!==void 0?r:e.acroField,C,t,H);var I={x:0+p/2,y:0+p/2,width:v-p,height:b-p,borderWidth:p??0,borderColor:F,textColor:C,font:t.name,fontSize:H,color:R,textLines:[W],padding:E};return ht(x,Rd(I))},Dv=function(e,r,t){var n,i,a=xr(r),o=xr(e.acroField),l=wn(r),c=wn(e.acroField),s=r.getRectangle(),u=r.getAppearanceCharacteristics(),d=r.getBorderStyle(),A=(n=d?.getWidth())!==null&&n!==void 0?n:0,f=Jr(u?.getRotation()),p=Wn(s,f),g=p.width,y=p.height,v=Cn(Je(Je({},s),{rotation:f})),b=St(0,0,0),x=er(u?.getBorderColor()),P=er(u?.getBackgroundColor()),F=e.getOptions(),R=e.getSelected();e.isSorted()&&F.sort();for(var E="",B=0,T=F.length;B1||i.length===1&&n)&&this.enableMultiselect();for(var l=new Array(i.length),c=0,s=i.length;c1||i.length===1&&n)&&this.enableMultiselect();for(var o=new Array(i.length),l=0,c=i.length;ln)throw new gv(t.length,n,this.getName());this.markAsDirty(),this.disableRichFormatting(),t?this.acroField.setValue(qe.fromText(t)):this.acroField.removeValue()},r.prototype.getAlignment=function(){var t=this.acroField.getQuadding();return t===0?zt.Left:t===1?zt.Center:t===2?zt.Right:zt.Left},r.prototype.setAlignment=function(t){gn(t,"alignment",zt),this.markAsDirty(),this.acroField.setQuadding(t)},r.prototype.getMaxLength=function(){return this.acroField.getMaxLength()},r.prototype.setMaxLength=function(t){if(Tr(t,"maxLength",0,Number.MAX_SAFE_INTEGER),this.markAsDirty(),t===void 0)this.acroField.removeMaxLength();else{var n=this.getText();if(n&&n.length>t)throw new mv(n.length,t,this.getName());this.acroField.setMaxLength(t)}},r.prototype.removeMaxLength=function(){this.markAsDirty(),this.acroField.removeMaxLength()},r.prototype.setImage=function(t){for(var n=this.getAlignment(),i=n===zt.Center?vn.Center:n===zt.Right?vn.Right:vn.Left,a=this.acroField.getWidgets(),o=0,l=a.length;o=100?e:typeof e=="string"&&e.includes("%")?Math.round(r&&r==="X"?parseFloat(e)/100*t.width:r&&r==="Y"?parseFloat(e)/100*t.height:parseFloat(e)/100*t.width):0}function no(e){return e.replace(/[xy]/g,function(r){let t=Math.random()*16|0;return(r==="x"?t:t&3|8).toString(16)})}function Ve(e){return typeof e>"u"||e==null?"":e.toString().replace(/&/g,"&").replace(//g,">").replace(/"/g,""").replace(/'/g,"'")}function lt(e){return typeof e=="number"&&e>100?e:(typeof e=="string"&&(e=Number(e.replace(/in*/gi,""))),Math.round(Ke*e))}function je(e){let r=Number(e)||0;return isNaN(r)?0:Math.round(r*da)}function Un(e){return e=e||0,Math.round((e>360?e-360:e)*6e4)}function Ts(e){let r=e.toString(16);return r.length===1?"0"+r:r}function Ds(e,r,t){return(Ts(e)+Ts(r)+Ts(t)).toUpperCase()}function ft(e,r){let t=(e||"").replace("#","");!Fs.test(t)&&t!==or.background1&&t!==or.background2&&t!==or.text1&&t!==or.text2&&t!==or.accent1&&t!==or.accent2&&t!==or.accent3&&t!==or.accent4&&t!==or.accent5&&t!==or.accent6&&(console.warn(`"${t}" is not a valid scheme color or hex RGB! "${nr}" used instead. Only provide 6-digit RGB or 'pptx.SchemeColor' values!`),t=nr);let n=Fs.test(t)?"srgbClr":"schemeClr",i='val="'+(Fs.test(t)?t.toUpperCase():t)+'"';return r?`${r}`:``}function s0(e,r){let t="",n=Object.assign(Object.assign({},r),e),i=Math.round(n.size*da),a=n.color,o=Math.round(n.opacity*1e5);return t+=``,t+=ft(a,``),t+="",t}function lr(e){let r="solid",t="",n="",i="";return e&&(typeof e=="string"?t=e:(e.type&&(r=e.type),e.color&&(t=e.color),e.alpha&&(n+=``),e.transparency&&(n+=``)),r==="solid"?i+=`${ft(t,n)}`:i+=""),i}function Vr(e){return e._rels.length+e._relsChart.length+e._relsMedia.length+1}function Cl(e){if(!(!e||typeof e!="object"))return e.type!=="outer"&&e.type!=="inner"&&e.type!=="none"&&(console.warn("Warning: shadow.type options are `outer`, `inner` or `none`."),e.type="outer"),e.angle&&((isNaN(Number(e.angle))||e.angle<0||e.angle>359)&&(console.warn("Warning: shadow.angle can only be 0-359"),e.angle=270),e.angle=Math.round(Number(e.angle))),e.opacity&&((isNaN(Number(e.opacity))||e.opacity<0||e.opacity>1)&&(console.warn("Warning: shadow.opacity can only be 0-1"),e.opacity=.75),e.opacity=Number(e.opacity)),e.color&&e.color.startsWith("#")&&(console.warn('Warning: shadow.color should not include hash (#) character, , e.g. "FF0000"'),e.color=e.color.replace("#","")),e}function l0(e,r,t){var n,i;let a=2.3+(!((n=e.options)===null||n===void 0)&&n.autoPageCharWeight?e.options.autoPageCharWeight:0),o=Math.floor(r/da*Ke)/((!((i=e.options)===null||i===void 0)&&i.fontSize?e.options.fontSize:yr)/a),l=[],c=[],s=[],u=[];e.text&&e.text.toString().trim().length===0?c.push({_type:He.tablecell,text:" "}):typeof e.text=="number"||typeof e.text=="string"?c.push({_type:He.tablecell,text:(e.text||"").toString().trim()}):Array.isArray(e.text)&&(c=e.text);let d=[];return c.forEach(A=>{var f;typeof A.text=="string"&&(A.text.split(` `).length>1?A.text.split(` -`).forEach(p=>{d.push({_type:He.tablecell,text:p,options:Object.assign(Object.assign({},A.options),{breakLine:!0})})}):d.push({_type:He.tablecell,text:A.text.trim(),options:A.options}),!((f=A.options)===null||f===void 0)&&f.breakLine&&(s.push(d),d=[])),d.length>0&&(s.push(d),d=[])}),s.forEach(A=>{A.forEach(f=>{let p=[],y=String(f.text).split(" ");y.forEach((v,b)=>{let x=Object.assign({},f.options);x?.breakLine&&(x.breakLine=b+1===y.length),p.push({_type:He.tablecell,text:v+(b+1{let f=[],p="";A.forEach(g=>{p.length+g.text.length>o&&(l.push(f),f=[],p=""),f.push(g),p+=g.text.toString()}),f.length>0&&l.push(f)}),l}function qd(e=[],r={},t,n){let i=ra,a=Ke*1,o=Ke*1,l=0,c=0,s=[],u=nt(r.x,"X",t),d=nt(r.y,"Y",t),A=nt(r.w,"X",t),f=nt(r.h,"Y",t),p=A;function g(){let v=0;s.length===0&&(v=d||lt(i[0])),s.length>0&&(v=lt(r.autoPageSlideStartY||r.newSlideStartY||i[0])),o=(f||t.height)-v-lt(i[2]),s.length>1&&(typeof r.autoPageSlideStartY=="number"?o=(f||t.height)-lt(r.autoPageSlideStartY+i[2]):typeof r.newSlideStartY=="number"?o=(f||t.height)-lt(r.newSlideStartY+i[2]):d&&(o=(f||t.height)-lt((d/Ke{b||(b={_type:He.tablecell});let x=b.options||null;c+=Number(x?.colspan?x.colspan:1)}),r.verbose&&console.log(`| numCols ......................................... = ${c}`),!A&&r.colW&&(p=Array.isArray(r.colW)?r.colW.reduce((v,b)=>v+b)*Ke:r.colW*c||0,r.verbose&&console.log(`| tableCalcW ...................................... = ${p/Ke}`)),a=p||lt((u?u/Ke:i[1])+i[3]),r.verbose&&console.log(`| emuSlideTabW .................................... = ${(a/Ke).toFixed(1)}`),!r.colW||!Array.isArray(r.colW))if(r.colW&&!isNaN(Number(r.colW))){let v=[];(e[0]||[]).forEach(()=>v.push(r.colW)),r.colW=[],v.forEach(x=>{Array.isArray(r.colW)&&r.colW.push(x)})}else{r.colW=[];for(let v=0;v{let x=[],P=0,F=0,R=[];v.forEach(W=>{var H,C,I,m;R.push({_type:He.tablecell,text:[],options:W.options}),W.options.margin&&W.options.margin[0]>=1?(!((H=W.options)===null||H===void 0)&&H.margin&&W.options.margin[0]&&je(W.options.margin[0])>P?P=je(W.options.margin[0]):r?.margin&&r.margin[0]&&je(r.margin[0])>P&&(P=je(r.margin[0])),!((C=W.options)===null||C===void 0)&&C.margin&&W.options.margin[2]&&je(W.options.margin[2])>F?F=je(W.options.margin[2]):r?.margin&&r.margin[2]&&je(r.margin[2])>F&&(F=je(r.margin[2]))):(!((I=W.options)===null||I===void 0)&&I.margin&&W.options.margin[0]&<(W.options.margin[0])>P?P=lt(W.options.margin[0]):r?.margin&&r.margin[0]&<(r.margin[0])>P&&(P=lt(r.margin[0])),!((m=W.options)===null||m===void 0)&&m.margin&&W.options.margin[2]&<(W.options.margin[2])>F?F=lt(W.options.margin[2]):r?.margin&&r.margin[2]&<(r.margin[2])>F&&(F=lt(r.margin[2])))}),g(),l+=P+F,r.verbose&&b===0&&console.log(`| SLIDE [${s.length}]: emuSlideTabH ...... = ${(o/Ke).toFixed(1)} `),v.forEach((W,H)=>{var C;let I={_type:He.tablecell,_lines:null,_lineHeight:lt((!((C=W.options)===null||C===void 0)&&C.fontSize?W.options.fontSize:r.fontSize?r.fontSize:wr)*(Zv+(r.autoPageLineWeight?r.autoPageLineWeight:0))/100),text:[],options:W.options};I.options.rowspan&&(I._lineHeight=0),I.options.autoPageCharWeight=r.autoPageCharWeight?r.autoPageCharWeight:null;let m=r.colW[H];W.options.colspan&&Array.isArray(r.colW)&&(m=r.colW.filter((N,ne)=>ne>=H&&neN+ne)),I._lines=n0(W,m),x.push(I)}),r.verbose&&console.log(` +`).forEach(p=>{d.push({_type:He.tablecell,text:p,options:Object.assign(Object.assign({},A.options),{breakLine:!0})})}):d.push({_type:He.tablecell,text:A.text.trim(),options:A.options}),!((f=A.options)===null||f===void 0)&&f.breakLine&&(s.push(d),d=[])),d.length>0&&(s.push(d),d=[])}),s.forEach(A=>{A.forEach(f=>{let p=[],y=String(f.text).split(" ");y.forEach((v,b)=>{let x=Object.assign({},f.options);x?.breakLine&&(x.breakLine=b+1===y.length),p.push({_type:He.tablecell,text:v+(b+1{let f=[],p="";A.forEach(g=>{p.length+g.text.length>o&&(l.push(f),f=[],p=""),f.push(g),p+=g.text.toString()}),f.length>0&&l.push(f)}),l}function qd(e=[],r={},t,n){let i=na,a=Ke*1,o=Ke*1,l=0,c=0,s=[],u=nt(r.x,"X",t),d=nt(r.y,"Y",t),A=nt(r.w,"X",t),f=nt(r.h,"Y",t),p=A;function g(){let v=0;s.length===0&&(v=d||lt(i[0])),s.length>0&&(v=lt(r.autoPageSlideStartY||r.newSlideStartY||i[0])),o=(f||t.height)-v-lt(i[2]),s.length>1&&(typeof r.autoPageSlideStartY=="number"?o=(f||t.height)-lt(r.autoPageSlideStartY+i[2]):typeof r.newSlideStartY=="number"?o=(f||t.height)-lt(r.newSlideStartY+i[2]):d&&(o=(f||t.height)-lt((d/Ke{b||(b={_type:He.tablecell});let x=b.options||null;c+=Number(x?.colspan?x.colspan:1)}),r.verbose&&console.log(`| numCols ......................................... = ${c}`),!A&&r.colW&&(p=Array.isArray(r.colW)?r.colW.reduce((v,b)=>v+b)*Ke:r.colW*c||0,r.verbose&&console.log(`| tableCalcW ...................................... = ${p/Ke}`)),a=p||lt((u?u/Ke:i[1])+i[3]),r.verbose&&console.log(`| emuSlideTabW .................................... = ${(a/Ke).toFixed(1)}`),!r.colW||!Array.isArray(r.colW))if(r.colW&&!isNaN(Number(r.colW))){let v=[];(e[0]||[]).forEach(()=>v.push(r.colW)),r.colW=[],v.forEach(x=>{Array.isArray(r.colW)&&r.colW.push(x)})}else{r.colW=[];for(let v=0;v{let x=[],P=0,F=0,R=[];v.forEach(W=>{var H,C,I,m;R.push({_type:He.tablecell,text:[],options:W.options}),W.options.margin&&W.options.margin[0]>=1?(!((H=W.options)===null||H===void 0)&&H.margin&&W.options.margin[0]&&je(W.options.margin[0])>P?P=je(W.options.margin[0]):r?.margin&&r.margin[0]&&je(r.margin[0])>P&&(P=je(r.margin[0])),!((C=W.options)===null||C===void 0)&&C.margin&&W.options.margin[2]&&je(W.options.margin[2])>F?F=je(W.options.margin[2]):r?.margin&&r.margin[2]&&je(r.margin[2])>F&&(F=je(r.margin[2]))):(!((I=W.options)===null||I===void 0)&&I.margin&&W.options.margin[0]&<(W.options.margin[0])>P?P=lt(W.options.margin[0]):r?.margin&&r.margin[0]&<(r.margin[0])>P&&(P=lt(r.margin[0])),!((m=W.options)===null||m===void 0)&&m.margin&&W.options.margin[2]&<(W.options.margin[2])>F?F=lt(W.options.margin[2]):r?.margin&&r.margin[2]&<(r.margin[2])>F&&(F=lt(r.margin[2])))}),g(),l+=P+F,r.verbose&&b===0&&console.log(`| SLIDE [${s.length}]: emuSlideTabH ...... = ${(o/Ke).toFixed(1)} `),v.forEach((W,H)=>{var C;let I={_type:He.tablecell,_lines:null,_lineHeight:lt((!((C=W.options)===null||C===void 0)&&C.fontSize?W.options.fontSize:r.fontSize?r.fontSize:yr)*(t0+(r.autoPageLineWeight?r.autoPageLineWeight:0))/100),text:[],options:W.options};I.options.rowspan&&(I._lineHeight=0),I.options.autoPageCharWeight=r.autoPageCharWeight?r.autoPageCharWeight:null;let m=r.colW[H];W.options.colspan&&Array.isArray(r.colW)&&(m=r.colW.filter((N,ne)=>ne>=H&&neN+ne)),I._lines=l0(W,m),x.push(I)}),r.verbose&&console.log(` | SLIDE [${s.length}]: ROW [${b}]: START...`);let E=0,B=0,T=!1;for(;!T;){let W=x[E],H=R[E];x.forEach(m=>{m._lineHeight>=B&&(B=m._lineHeight)}),l+B>o&&(r.verbose&&(console.log(` |-----------------------------------------------------------------------|`),console.log(`|-- NEW SLIDE CREATED (currTabH+currLineH > maxH) => ${(l/Ke).toFixed(2)} + ${(W._lineHeight/Ke).toFixed(2)} > ${o/Ke}`),console.log(`|-----------------------------------------------------------------------| `)),R.length>0&&R.map(N=>N.text.length).reduce((N,ne)=>N+ne)>0&&y.rows.push(R),s.push(y),y={rows:[]},R=[],v.forEach(N=>R.push({_type:He.tablecell,text:[],options:N.options})),g(),l+=P+F,r.verbose&&console.log(`| SLIDE [${s.length}]: emuSlideTabH ...... = ${(o/Ke).toFixed(1)} `),l=0,(r.addHeaderToEach||r.autoPageRepeatHeader)&&r._arrObjTabHeadRows&&r._arrObjTabHeadRows.forEach(N=>{let ne=[],q=0;N.forEach(se=>{ne.push(se),se._lineHeight>q&&(q=se._lineHeight)}),y.rows.push(ne),l+=q}),H=R[E]);let C=W._lines.shift();Array.isArray(H.text)&&(C?H.text=H.text.concat(C):H.text.length===0&&(H.text=H.text.concat({_type:He.tablecell,text:""}))),E===x.length-1&&(l+=B),E=Em._lines.length).reduce((m,N)=>m+N)===0&&(T=!0)}R.length>0&&y.rows.push(R),r.verbose&&console.log(`- SLIDE [${s.length}]: ROW [${b}]: ...COMPLETE ...... emuTabCurrH = ${(l/Ke).toFixed(2)} ( emuSlideTabH = ${(o/Ke).toFixed(2)} )`)}),s.push(y),r.verbose&&(console.log(` |================================================|`),console.log(`| FINAL: tableRowSlides.length = ${s.length}`),s.forEach(v=>console.log(v)),console.log(`|================================================| -`)),s}function i0(e,r,t={},n){let i=t||{};i.slideMargin=i.slideMargin||i.slideMargin===0?i.slideMargin:.5;let a=i.w||e.presLayout.width,o=[],l=[],c=[],s=[],u=[],d=[.5,.5,.5,.5],A=0;if(!document.getElementById(r))throw new Error('tableToSlides: Table ID "'+r+'" does not exist!');n?._margin?(Array.isArray(n._margin)?d=n._margin:isNaN(n._margin)||(d=[n._margin,n._margin,n._margin,n._margin]),i.slideMargin=d):i?.slideMargin&&(Array.isArray(i.slideMargin)?d=i.slideMargin:isNaN(i.slideMargin)||(d=[i.slideMargin,i.slideMargin,i.slideMargin,i.slideMargin])),a=(i.w?lt(i.w):e.presLayout.width)-lt(d[1]+d[3]),i.verbose&&(console.log("[[VERBOSE MODE]]"),console.log("|-- `tableToSlides` ----------------------------------------------------|"),console.log(`| tableProps.h .................................... = ${i.h}`),console.log(`| tableProps.w .................................... = ${i.w}`),console.log(`| pptx.presLayout.width ........................... = ${(e.presLayout.width/Ke).toFixed(1)}`),console.log(`| pptx.presLayout.height .......................... = ${(e.presLayout.height/Ke).toFixed(1)}`),console.log(`| emuSlideTabW .................................... = ${(a/Ke).toFixed(1)}`));let f=document.querySelectorAll(`#${r} tr:first-child th`);f.length===0&&(f=document.querySelectorAll(`#${r} tr:first-child td`)),f.forEach(g=>{let y=g;if(y.getAttribute("colspan"))for(let v=0;v{A+=g}),u.forEach((g,y)=>{let v=Number((Number(a)*(g/A*100)/100/Ke).toFixed(2)),b=0,x=document.querySelector(`#${r} thead tr:first-child th:nth-child(${y+1})`);x&&(b=Number(x.getAttribute("data-pptx-min-width")));let P=document.querySelector(`#${r} thead tr:first-child th:nth-child(${y+1})`);P&&(b=Number(P.getAttribute("data-pptx-width"))),s.push(b>v?b:v)}),i.verbose&&console.log(`| arrColW ......................................... = [${s.join(", ")}]`),["thead","tbody","tfoot"].forEach(g=>{document.querySelectorAll(`#${r} ${g} tr`).forEach(y=>{let v=y,b=[];switch(Array.from(v.cells).forEach(x=>{let P=window.getComputedStyle(x).getPropertyValue("color").replace(/\s+/gi,"").replace("rgba(","").replace("rgb(","").replace(")","").split(","),F=window.getComputedStyle(x).getPropertyValue("background-color").replace(/\s+/gi,"").replace("rgba(","").replace("rgb(","").replace(")","").split(",");(window.getComputedStyle(x).getPropertyValue("background-color")==="rgba(0, 0, 0, 0)"||window.getComputedStyle(x).getPropertyValue("transparent"))&&(F=["255","255","255"]);let R={align:null,bold:window.getComputedStyle(x).getPropertyValue("font-weight")==="bold"||Number(window.getComputedStyle(x).getPropertyValue("font-weight"))>=500,border:null,color:Ts(Number(P[0]),Number(P[1]),Number(P[2])),fill:{color:Ts(Number(F[0]),Number(F[1]),Number(F[2]))},fontFace:(window.getComputedStyle(x).getPropertyValue("font-family")||"").split(",")[0].replace(/"/g,"").replace("inherit","").replace("initial","")||null,fontSize:Number(window.getComputedStyle(x).getPropertyValue("font-size").replace(/[a-z]/gi,"")),margin:null,colspan:Number(x.getAttribute("colspan"))||null,rowspan:Number(x.getAttribute("rowspan"))||null,valign:null};if(["left","center","right","start","end"].includes(window.getComputedStyle(x).getPropertyValue("text-align"))){let E=window.getComputedStyle(x).getPropertyValue("text-align").replace("start","left").replace("end","right");R.align=E==="center"?"center":E==="left"?"left":E==="right"?"right":null}if(["top","middle","bottom"].includes(window.getComputedStyle(x).getPropertyValue("vertical-align"))){let E=window.getComputedStyle(x).getPropertyValue("vertical-align");R.valign=E==="top"?"top":E==="middle"?"middle":E==="bottom"?"bottom":null}window.getComputedStyle(x).getPropertyValue("padding-left")&&(R.margin=[0,0,0,0],["padding-top","padding-right","padding-bottom","padding-left"].forEach((B,T)=>{R.margin[T]=Math.round(Number(window.getComputedStyle(x).getPropertyValue(B).replace(/\D/gi,"")))})),(window.getComputedStyle(x).getPropertyValue("border-top-width")||window.getComputedStyle(x).getPropertyValue("border-right-width")||window.getComputedStyle(x).getPropertyValue("border-bottom-width")||window.getComputedStyle(x).getPropertyValue("border-left-width"))&&(R.border=[null,null,null,null],["top","right","bottom","left"].forEach((B,T)=>{let W=Math.round(Number(window.getComputedStyle(x).getPropertyValue("border-"+B+"-width").replace("px",""))),H=[];H=window.getComputedStyle(x).getPropertyValue("border-"+B+"-color").replace(/\s+/gi,"").replace("rgba(","").replace("rgb(","").replace(")","").split(",");let C=Ts(Number(H[0]),Number(H[1]),Number(H[2]));R.border[T]={pt:W,color:C}})),b.push({_type:He.tablecell,text:x.innerText,options:R})}),g){case"thead":o.push(b);break;case"tbody":l.push(b);break;case"tfoot":c.push(b);break;default:console.log(`table parsing: unexpected table part: ${g}`);break}})}),i._arrObjTabHeadRows=o||null,i.colW=s,qd([...o,...l,...c],i,e.presLayout,n).forEach((g,y)=>{let v=e.addSlide({masterName:i.masterSlideName||null});y===0&&(i.y=i.y||d[0]),y>0&&(i.y=i.autoPageSlideStartY||i.newSlideStartY||d[0]),i.verbose&&console.log(`| opts.autoPageSlideStartY: ${i.autoPageSlideStartY} / arrInchMargins[0]: ${d[0]} => opts.y = ${i.y}`),v.addTable(g.rows,{x:i.x||d[3],y:i.y,w:Number(a)/Ke,colW:s,autoPage:!1}),i.addImage&&(i.addImage.options=i.addImage.options||{},!i.addImage.image||!i.addImage.image.path&&!i.addImage.image.data?console.warn("Warning: tableToSlides.addImage requires either `path` or `data`"):v.addImage({path:i.addImage.image.path,data:i.addImage.image.data,x:i.addImage.options.x,y:i.addImage.options.y,w:i.addImage.options.w,h:i.addImage.options.h})),i.addShape&&v.addShape(i.addShape.shapeName,i.addShape.options||{}),i.addTable&&v.addTable(i.addTable.rows,i.addTable.options||{}),i.addText&&v.addText(i.addText.text,i.addText.options||{})})}var a0=0;function o0(e,r){e.bkgd&&(r.bkgd=e.bkgd),e.objects&&Array.isArray(e.objects)&&e.objects.length>0&&e.objects.forEach((t,n)=>{let i=Object.keys(t)[0],a=r;hn[i]&&i==="chart"?Vd(a,t[i].type,t[i].data,t[i].opts):hn[i]&&i==="image"?Xd(a,t[i]):hn[i]&&i==="line"?Js(a,xn.LINE,t[i]):hn[i]&&i==="rect"?Js(a,xn.RECTANGLE,t[i]):hn[i]&&i==="text"?vo(a,[{text:t[i].text}],t[i].options,!1):hn[i]&&i==="placeholder"&&(t[i].options.placeholder=t[i].options.name,delete t[i].options.name,t[i].options._placeholderType=t[i].options.type,delete t[i].options.type,t[i].options._placeholderIdx=100+n,vo(a,[{text:t[i].text}],t[i].options,!0))}),e.slideNumber&&typeof e.slideNumber=="object"&&(r._slideNumberProps=e.slideNumber)}function Vd(e,r,t,n){var i;function a(d){!d||d.style==="none"||(d.size!==void 0&&(isNaN(Number(d.size))||d.size<=0)&&(console.warn("Warning: chart.gridLine.size must be greater than 0."),delete d.size),d.style&&!["solid","dash","dot"].includes(d.style)&&(console.warn("Warning: chart.gridLine.style options: `solid`, `dash`, `dot`."),delete d.style),d.cap&&!["flat","square","round"].includes(d.cap)&&(console.warn("Warning: chart.gridLine.cap options: `flat`, `square`, `round`."),delete d.cap))}let o=++a0,l={_type:null,text:null,options:null,chartRid:null},c=null,s=[];Array.isArray(r)?(r.forEach(d=>{s=s.concat(d.data)}),c=t||n):(s=t,c=n),s.forEach((d,A)=>{d._dataIndex=A,d.labels!==void 0&&!Array.isArray(d.labels[0])&&(d.labels=[d.labels])});let u=c&&typeof c=="object"?c:{};if(u._type=r,u.x=typeof u.x<"u"&&u.x!=null&&!isNaN(Number(u.x))?u.x:1,u.y=typeof u.y<"u"&&u.y!=null&&!isNaN(Number(u.y))?u.y:1,u.w=u.w||"50%",u.h=u.h||"50%",u.objectName=u.objectName?Ve(u.objectName):`Chart ${e._slideObjects.filter(d=>d._type===He.chart).length}`,["bar","col"].includes(u.barDir||"")||(u.barDir="col"),u._type===Ce.AREA&&(["stacked","standard","percentStacked"].includes(u.barGrouping||"")||(u.barGrouping="standard")),u._type===Ce.BAR&&(["clustered","stacked","percentStacked"].includes(u.barGrouping||"")||(u.barGrouping="clustered")),u._type===Ce.BAR3D&&(["clustered","stacked","standard","percentStacked"].includes(u.barGrouping||"")||(u.barGrouping="standard")),!((i=u.barGrouping)===null||i===void 0)&&i.includes("tacked")&&(u.barGapWidthPct||(u.barGapWidthPct=50)),u.dataLabelPosition&&((u._type===Ce.AREA||u._type===Ce.BAR3D||u._type===Ce.DOUGHNUT||u._type===Ce.RADAR)&&delete u.dataLabelPosition,u._type===Ce.PIE&&(["bestFit","ctr","inEnd","outEnd"].includes(u.dataLabelPosition)||delete u.dataLabelPosition),(u._type===Ce.BUBBLE||u._type===Ce.BUBBLE3D||u._type===Ce.LINE||u._type===Ce.SCATTER)&&(["b","ctr","l","r","t"].includes(u.dataLabelPosition)||delete u.dataLabelPosition),u._type===Ce.BAR&&(["stacked","percentStacked"].includes(u.barGrouping||"")||["ctr","inBase","inEnd"].includes(u.dataLabelPosition)||delete u.dataLabelPosition,["clustered"].includes(u.barGrouping||"")||["ctr","inBase","inEnd","outEnd"].includes(u.dataLabelPosition)||delete u.dataLabelPosition)),u.dataLabelBkgrdColors=u.dataLabelBkgrdColors||!u.dataLabelBkgrdColors?u.dataLabelBkgrdColors:!1,["b","l","r","t","tr"].includes(u.legendPos||"")||(u.legendPos="r"),["cone","coneToMax","box","cylinder","pyramid","pyramidToMax"].includes(u.bar3DShape||"")||(u.bar3DShape="box"),["circle","dash","diamond","dot","none","square","triangle"].includes(u.lineDataSymbol||"")||(u.lineDataSymbol="circle"),["gap","span"].includes(u.displayBlanksAs||"")||(u.displayBlanksAs="span"),["standard","marker","filled"].includes(u.radarStyle||"")||(u.radarStyle="standard"),u.lineDataSymbolSize=u.lineDataSymbolSize&&!isNaN(u.lineDataSymbolSize)?u.lineDataSymbolSize:6,u.lineDataSymbolLineSize=u.lineDataSymbolLineSize&&!isNaN(u.lineDataSymbolLineSize)?je(u.lineDataSymbolLineSize):je(.75),u.layout&&["x","y","w","h"].forEach(d=>{let A=u.layout[d];(isNaN(Number(A))||A<0||A>1)&&(console.warn("Warning: chart.layout."+d+" can only be 0-1"),delete u.layout[d])}),u.catGridLine=u.catGridLine||(u._type===Ce.SCATTER?{color:"D9D9D9",size:1}:{style:"none"}),u.valGridLine=u.valGridLine||(u._type===Ce.SCATTER?{color:"D9D9D9",size:1}:{}),u.serGridLine=u.serGridLine||(u._type===Ce.SCATTER?{color:"D9D9D9",size:1}:{style:"none"}),a(u.catGridLine),a(u.valGridLine),a(u.serGridLine),kl(u.shadow),u.showDataTable=u.showDataTable||!u.showDataTable?u.showDataTable:!1,u.showDataTableHorzBorder=u.showDataTableHorzBorder||!u.showDataTableHorzBorder?u.showDataTableHorzBorder:!0,u.showDataTableVertBorder=u.showDataTableVertBorder||!u.showDataTableVertBorder?u.showDataTableVertBorder:!0,u.showDataTableOutline=u.showDataTableOutline||!u.showDataTableOutline?u.showDataTableOutline:!0,u.showDataTableKeys=u.showDataTableKeys||!u.showDataTableKeys?u.showDataTableKeys:!0,u.showLabel=u.showLabel||!u.showLabel?u.showLabel:!1,u.showLegend=u.showLegend||!u.showLegend?u.showLegend:!1,u.showPercent=u.showPercent||!u.showPercent?u.showPercent:!0,u.showTitle=u.showTitle||!u.showTitle?u.showTitle:!1,u.showValue=u.showValue||!u.showValue?u.showValue:!1,u.showLeaderLines=u.showLeaderLines||!u.showLeaderLines?u.showLeaderLines:!1,u.catAxisLineShow=typeof u.catAxisLineShow<"u"?u.catAxisLineShow:!0,u.valAxisLineShow=typeof u.valAxisLineShow<"u"?u.valAxisLineShow:!0,u.serAxisLineShow=typeof u.serAxisLineShow<"u"?u.serAxisLineShow:!0,u.v3DRotX=!isNaN(u.v3DRotX)&&u.v3DRotX>=-90&&u.v3DRotX<=90?u.v3DRotX:30,u.v3DRotY=!isNaN(u.v3DRotY)&&u.v3DRotY>=0&&u.v3DRotY<=360?u.v3DRotY:30,u.v3DRAngAx=u.v3DRAngAx||!u.v3DRAngAx?u.v3DRAngAx:!0,u.v3DPerspective=!isNaN(u.v3DPerspective)&&u.v3DPerspective>=0&&u.v3DPerspective<=240?u.v3DPerspective:30,u.barGapWidthPct=!isNaN(u.barGapWidthPct)&&u.barGapWidthPct>=0&&u.barGapWidthPct<=1e3?u.barGapWidthPct:150,u.barGapDepthPct=!isNaN(u.barGapDepthPct)&&u.barGapDepthPct>=0&&u.barGapDepthPct<=1e3?u.barGapDepthPct:150,u.chartColors=Array.isArray(u.chartColors)?u.chartColors:u._type===Ce.PIE||u._type===Ce.DOUGHNUT?e0:ji,u.chartColorsOpacity=u.chartColorsOpacity&&!isNaN(u.chartColorsOpacity)?u.chartColorsOpacity:null,u.border=u.border&&typeof u.border=="object"?u.border:null,u.border&&(!u.border.pt||isNaN(u.border.pt))&&(u.border.pt=ri.pt),u.border&&(!u.border.color||typeof u.border.color!="string")&&(u.border.color=ri.color),u.plotArea=u.plotArea||{},u.plotArea.border=u.plotArea.border&&typeof u.plotArea.border=="object"?u.plotArea.border:null,u.plotArea.border&&(!u.plotArea.border.pt||isNaN(u.plotArea.border.pt))&&(u.plotArea.border.pt=ri.pt),u.plotArea.border&&(!u.plotArea.border.color||typeof u.plotArea.border.color!="string")&&(u.plotArea.border.color=ri.color),u.border&&(u.plotArea.border=u.border),u.plotArea.fill=u.plotArea.fill||{color:null,transparency:null},u.fill&&(u.plotArea.fill.color=u.fill),u.chartArea=u.chartArea||{},u.chartArea.border=u.chartArea.border&&typeof u.chartArea.border=="object"?u.chartArea.border:null,u.chartArea.border&&(u.chartArea.border={color:u.chartArea.border.color||ri.color,pt:u.chartArea.border.pt||ri.pt}),u.chartArea.roundedCorners=typeof u.chartArea.roundedCorners=="boolean"?u.chartArea.roundedCorners:!0,u.dataBorder=u.dataBorder&&typeof u.dataBorder=="object"?u.dataBorder:null,u.dataBorder&&(!u.dataBorder.pt||isNaN(u.dataBorder.pt))&&(u.dataBorder.pt=.75),u.dataBorder&&u.dataBorder.color){let d=typeof u.dataBorder.color=="string"&&u.dataBorder.color.length===6&&/^[0-9A-Fa-f]{6}$/.test(u.dataBorder.color),A=Object.values(mo).includes(u.dataBorder.color);!d&&!A&&(u.dataBorder.color="F9F9F9")}return!u.dataLabelFormatCode&&u._type===Ce.SCATTER&&(u.dataLabelFormatCode="General"),!u.dataLabelFormatCode&&(u._type===Ce.PIE||u._type===Ce.DOUGHNUT)&&(u.dataLabelFormatCode=u.showPercent?"0%":"General"),u.dataLabelFormatCode=u.dataLabelFormatCode&&typeof u.dataLabelFormatCode=="string"?u.dataLabelFormatCode:"#,##0",!u.dataLabelFormatScatter&&u._type===Ce.SCATTER&&(u.dataLabelFormatScatter="custom"),u.lineSize=typeof u.lineSize=="number"?u.lineSize:2,u.valAxisMajorUnit=typeof u.valAxisMajorUnit=="number"?u.valAxisMajorUnit:null,u._type===Ce.AREA||u._type===Ce.BAR||u._type===Ce.BAR3D||u._type===Ce.LINE?u.catAxisMultiLevelLabels=!!u.catAxisMultiLevelLabels:delete u.catAxisMultiLevelLabels,l._type="chart",l.options=u,l.chartRid=Vr(e),e._relsChart.push({rId:Vr(e),data:s,opts:u,type:u._type,globalId:o,fileName:`chart${o}.xml`,Target:`/ppt/charts/chart${o}.xml`}),e._slideObjects.push(l),l}function Xd(e,r){let t={_type:null,text:null,options:null,image:null,imageRid:null,hyperlink:null},n=r.x||0,i=r.y||0,a=r.w||0,o=r.h||0,l=r.sizing||null,c=r.hyperlink||"",s=r.data||"",u=r.path||"",d=Vr(e),A=r.objectName?Ve(r.objectName):`Image ${e._slideObjects.filter(p=>p._type===He.image).length}`;if(!u&&!s)return console.error("ERROR: addImage() requires either 'data' or 'path' parameter!"),null;if(u&&typeof u!="string")return console.error(`ERROR: addImage() 'path' should be a string, ex: {path:'/img/sample.png'} - you sent ${String(u)}`),null;if(s&&typeof s!="string")return console.error(`ERROR: addImage() 'data' should be a string, ex: {data:'image/png;base64,NMP[...]'} - you sent ${String(s)}`),null;if(s&&typeof s=="string"&&!s.toLowerCase().includes("base64,"))return console.error("ERROR: Image `data` value lacks a base64 header! Ex: 'image/png;base64,NMP[...]')"),null;let f=(u.substring(u.lastIndexOf("/")+1).split("?")[0].split(".").pop().split("#")[0]||"png").toLowerCase();if(s&&/image\/(\w+);/.exec(s)&&/image\/(\w+);/.exec(s).length>0?f=/image\/(\w+);/.exec(s)[1]:s?.toLowerCase().includes("image/svg+xml")&&(f="svg"),t._type=He.image,t.image=u||"preencoded.png",t.options={x:n||0,y:i||0,w:a||1,h:o||1,altText:r.altText||"",rounding:typeof r.rounding=="boolean"?r.rounding:!1,sizing:l,placeholder:r.placeholder,rotate:r.rotate||0,flipV:r.flipV||!1,flipH:r.flipH||!1,transparency:r.transparency||0,objectName:A,shadow:kl(r.shadow)},f==="svg")e._relsMedia.push({path:u||s+"png",type:"image/png",extn:"png",data:s||"",rId:d,Target:`../media/image-${e._slideNum}-${e._relsMedia.length+1}.png`,isSvgPng:!0,svgSize:{w:nt(t.options.w,"X",e._presLayout),h:nt(t.options.h,"Y",e._presLayout)}}),t.imageRid=d,e._relsMedia.push({path:u||s,type:"image/svg+xml",extn:f,data:s||"",rId:d+1,Target:`../media/image-${e._slideNum}-${e._relsMedia.length+1}.${f}`}),t.imageRid=d+1;else{let p=e._relsMedia.filter(g=>g.path&&g.path===u&&g.type==="image/"+f&&!g.isDuplicate)[0];e._relsMedia.push({path:u||"preencoded."+f,type:"image/"+f,extn:f,data:s||"",rId:d,isDuplicate:!!p?.Target,Target:p?.Target?p.Target:`../media/image-${e._slideNum}-${e._relsMedia.length+1}.${f}`}),t.imageRid=d}if(typeof c=="object"){if(!c.url&&!c.slide)throw new Error("ERROR: `hyperlink` option requires either: `url` or `slide`");d++,e._rels.push({type:He.hyperlink,data:c.slide?"slide":"dummy",rId:d,Target:c.url||c.slide.toString()}),c._rId=d,t.hyperlink=c}e._slideObjects.push(t)}function s0(e,r){let t=r.x||0,n=r.y||0,i=r.w||2,a=r.h||2,o=r.data||"",l=r.link||"",c=r.path||"",s=r.type||"audio",u="",d=r.cover||t0,A=r.objectName?Ve(r.objectName):`Media ${e._slideObjects.filter(p=>p._type===He.media).length}`,f={_type:He.media};if(!c&&!o&&s!=="online")throw new Error("addMedia() error: either `data` or `path` are required!");if(o&&!o.toLowerCase().includes("base64,"))throw new Error("addMedia() error: `data` value lacks a base64 header! Ex: 'video/mpeg;base64,NMP[...]')");if(!d.toLowerCase().includes("base64,"))throw new Error("addMedia() error: `cover` value lacks a base64 header! Ex: 'data:image/png;base64,iV[...]')");if(s==="online"&&!l)throw new Error("addMedia() error: online videos require `link` value");if(u=r.extn||(o?o.split(";")[0].split("/")[1]:c.split(".").pop())||"mp3",f.mtype=s,f.media=c||"preencoded.mov",f.options={},f.options.x=t,f.options.y=n,f.options.w=i,f.options.h=a,f.options.objectName=A,s==="online"){let p=Vr(e);e._relsMedia.push({path:c||"preencoded"+u,data:"dummy",type:"online",extn:u,rId:p,Target:l}),f.mediaRid=p,e._relsMedia.push({path:"preencoded.png",data:d,type:"image/png",extn:"png",rId:Vr(e),Target:`../media/image-${e._slideNum}-${e._relsMedia.length+1}.png`})}else{let p=e._relsMedia.filter(y=>y.path&&y.path===c&&y.type===s+"/"+u&&!y.isDuplicate)[0],g=Vr(e);e._relsMedia.push({path:c||"preencoded"+u,type:s+"/"+u,extn:u,data:o||"",rId:g,isDuplicate:!!p?.Target,Target:p?.Target?p.Target:`../media/media-${e._slideNum}-${e._relsMedia.length+1}.${u}`}),f.mediaRid=g,e._relsMedia.push({path:c||"preencoded"+u,type:s+"/"+u,extn:u,data:o||"",rId:Vr(e),isDuplicate:!!p?.Target,Target:p?.Target?p.Target:`../media/media-${e._slideNum}-${e._relsMedia.length+0}.${u}`}),e._relsMedia.push({path:"preencoded.png",type:"image/png",extn:"png",data:d,rId:Vr(e),Target:`../media/image-${e._slideNum}-${e._relsMedia.length+1}.png`})}e._slideObjects.push(f)}function l0(e,r){e._slideObjects.push({_type:He.notes,text:[{text:r}]})}function Js(e,r,t){let n=typeof t=="object"?t:{};n.line=n.line||{type:"none"};let i={_type:He.text,shape:r||xn.RECTANGLE,options:n,text:null};if(!r)throw new Error("Missing/Invalid shape parameter! Example: `addShape(pptxgen.shapes.LINE, {x:1, y:1, w:1, h:1});`");let a={type:n.line.type||"solid",color:n.line.color||Wd,transparency:n.line.transparency||0,width:n.line.width||1,dashType:n.line.dashType||"solid",beginArrowType:n.line.beginArrowType||null,endArrowType:n.line.endArrowType||null};if(typeof n.line=="object"&&n.line.type!=="none"&&(n.line=a),n.x=n.x||(n.x===0?0:1),n.y=n.y||(n.y===0?0:1),n.w=n.w||(n.w===0?0:1),n.h=n.h||(n.h===0?0:1),n.objectName=n.objectName?Ve(n.objectName):`Shape ${e._slideObjects.filter(o=>o._type===He.text).length}`,typeof n.line=="string"){let o=a;o.color=String(n.line),n.line=o}typeof n.lineSize=="number"&&(n.line.width=n.lineSize),typeof n.lineDash=="string"&&(n.line.dashType=n.lineDash),typeof n.lineHead=="string"&&(n.line.beginArrowType=n.lineHead),typeof n.lineTail=="string"&&(n.line.endArrowType=n.lineTail),hi(e,i),e._slideObjects.push(i)}function c0(e,r,t,n,i,a,o){let l=[e],c=t&&typeof t=="object"?t:{};c.objectName=c.objectName?Ve(c.objectName):`Table ${e._slideObjects.filter(A=>A._type===He.table).length}`;{if(r===null||r.length===0||!Array.isArray(r))throw new Error("addTable: Array expected! EX: 'slide.addTable( [rows], {options} );' (https://gitbrent.github.io/PptxGenJS/docs/api-tables.html)");if(!r[0]||!Array.isArray(r[0]))throw new Error("addTable: 'rows' should be an array of cells! EX: 'slide.addTable( [ ['A'], ['B'], {text:'C',options:{align:'center'}} ] );' (https://gitbrent.github.io/PptxGenJS/docs/api-tables.html)")}let s=[];r.forEach(A=>{let f=[];Array.isArray(A)?A.forEach(p=>{let g={_type:He.tablecell,text:"",options:typeof p=="object"&&p.options?p.options:{}};typeof p=="string"||typeof p=="number"?g.text=p.toString():p.text&&(typeof p.text=="string"||typeof p.text=="number"?g.text=p.text.toString():p.text&&(g.text=p.text),p.options&&typeof p.options=="object"&&(g.options=p.options)),g.options.border=g.options.border||c.border||[{type:"none"},{type:"none"},{type:"none"},{type:"none"}];let y=g.options.border;!Array.isArray(y)&&typeof y=="object"&&(g.options.border=[y,y,y,y]),g.options.border[0]||(g.options.border[0]={type:"none"}),g.options.border[1]||(g.options.border[1]={type:"none"}),g.options.border[2]||(g.options.border[2]={type:"none"}),g.options.border[3]||(g.options.border[3]={type:"none"}),[0,1,2,3].forEach(b=>{g.options.border[b]={type:g.options.border[b].type||ti.type,color:g.options.border[b].color||ti.color,pt:typeof g.options.border[b].pt=="number"?g.options.border[b].pt:ti.pt}}),f.push(g)}):(console.log("addTable: tableRows has a bad row. A row should be an array of cells. You provided:"),console.log(A)),s.push(f)}),c.x=nt(c.x||(c.x===0?0:Ke/2),"X",i),c.y=nt(c.y||(c.y===0?0:Ke/2),"Y",i),c.h&&(c.h=nt(c.h,"Y",i)),c.fontSize=c.fontSize||wr,c.margin=c.margin===0||c.margin?c.margin:Ud,typeof c.margin=="number"&&(c.margin=[Number(c.margin),Number(c.margin),Number(c.margin),Number(c.margin)]),JSON.stringify({arrRows:s}).indexOf("hyperlink")===-1&&(c.color||(c.color=c.color||nr)),typeof c.border=="string"?(console.warn("addTable `border` option must be an object. Ex: `{border: {type:'none'}}`"),c.border=null):Array.isArray(c.border)&&[0,1,2,3].forEach(A=>{c.border[A]=c.border[A]?{type:c.border[A].type||ti.type,color:c.border[A].color||ti.color,pt:c.border[A].pt||ti.pt}:{type:"none"}}),c.autoPage=typeof c.autoPage=="boolean"?c.autoPage:!1,c.autoPageRepeatHeader=typeof c.autoPageRepeatHeader=="boolean"?c.autoPageRepeatHeader:!1,c.autoPageHeaderRows=typeof c.autoPageHeaderRows<"u"&&!isNaN(Number(c.autoPageHeaderRows))?Number(c.autoPageHeaderRows):1,c.autoPageLineWeight=typeof c.autoPageLineWeight<"u"&&!isNaN(Number(c.autoPageLineWeight))?Number(c.autoPageLineWeight):0,c.autoPageLineWeight&&(c.autoPageLineWeight>1?c.autoPageLineWeight=1:c.autoPageLineWeight<-1&&(c.autoPageLineWeight=-1));let u=ra;if(n&&typeof n._margin<"u"&&(Array.isArray(n._margin)?u=n._margin:isNaN(Number(n._margin))||(u=[Number(n._margin),Number(n._margin),Number(n._margin),Number(n._margin)])),c.colW){let A=s[0].reduce((f,p)=>{var g;return!((g=p?.options)===null||g===void 0)&&g.colspan&&typeof p.options.colspan=="number"?f+=p.options.colspan:f+=1,f},0);typeof c.colW=="string"||typeof c.colW=="number"||c.colW&&Array.isArray(c.colW)&&c.colW.length===1&&A>1?(c.w=Math.floor(Number(c.colW)*A),c.colW=null):c.colW&&Array.isArray(c.colW)&&c.colW.length!==A&&(console.warn("addTable: mismatch: (colW.length != data.length) Therefore, defaulting to evenly distributed col widths."),c.colW=null)}else c.w?c.w=nt(c.w,"X",i):c.w=Math.floor(i._sizeW/Ke-u[1]-u[3]);c.x&&c.x<20&&(c.x=lt(c.x)),c.y&&c.y<20&&(c.y=lt(c.y)),c.w&&typeof c.w=="number"&&c.w<20&&(c.w=lt(c.w)),c.h&&typeof c.h=="number"&&c.h<20&&(c.h=lt(c.h)),s.forEach(A=>{A.forEach((f,p)=>{typeof f=="number"||typeof f=="string"?A[p]={_type:He.tablecell,text:String(A[p]),options:c}:typeof f=="object"&&(typeof f.text=="number"?A[p].text=A[p].text.toString():(typeof f.text>"u"||f.text===null)&&(A[p].text=""),A[p].options=f.options||{},A[p]._type=He.tablecell)})});let d=[];return c&&!c.autoPage?(hi(e,s),e._slideObjects.push({_type:He.table,arrTabRows:s,options:Object.assign({},c)})):(c.autoPageRepeatHeader&&(c._arrObjTabHeadRows=s.filter((A,f)=>f{o(e._slideNum+f)||l.push(a({masterName:n?._name||null})),f>0&&(c.y=lt(c.autoPageSlideStartY||c.newSlideStartY||u[0]));{let p=o(e._slideNum+f);c.autoPage=!1,hi(p,A.rows),p.addTable(A.rows,Object.assign({},c)),f>0&&d.push(p)}})),d}function vo(e,r,t,n){let i={_type:n?He.placeholder:He.text,shape:t?.shape||xn.RECTANGLE,text:!r||r.length===0?[{text:"",options:null}]:r,options:t||{}};function a(o){{if(o.placeholder||(o.color=o.color||i.options.color||e.color||nr),(o.placeholder||n)&&(o.bullet=o.bullet||!1),o.placeholder&&e._slideLayout&&e._slideLayout._slideObjects){let l=e._slideLayout._slideObjects.filter(c=>c._type==="placeholder"&&c.options&&c.options.placeholder&&c.options.placeholder===o.placeholder)[0];l?.options&&(o=Object.assign(Object.assign({},o),l.options))}if(o.objectName=o.objectName?Ve(o.objectName):`Text ${e._slideObjects.filter(l=>l._type===He.text).length}`,o.shape===xn.LINE){let l={type:o.line.type||"solid",color:o.line.color||Wd,transparency:o.line.transparency||0,width:o.line.width||1,dashType:o.line.dashType||"solid",beginArrowType:o.line.beginArrowType||null,endArrowType:o.line.endArrowType||null};if(typeof o.line=="object"&&(o.line=l),typeof o.line=="string"){let c=l;typeof o.line=="string"&&(c.color=o.line),o.line=c}typeof o.lineSize=="number"&&(o.line.width=o.lineSize),typeof o.lineDash=="string"&&(o.line.dashType=o.lineDash),typeof o.lineHead=="string"&&(o.line.beginArrowType=o.lineHead),typeof o.lineTail=="string"&&(o.line.endArrowType=o.lineTail)}o.line=o.line||{},o.lineSpacing=o.lineSpacing&&!isNaN(o.lineSpacing)?o.lineSpacing:null,o.lineSpacingMultiple=o.lineSpacingMultiple&&!isNaN(o.lineSpacingMultiple)?o.lineSpacingMultiple:null,o._bodyProp=o._bodyProp||{},o._bodyProp.autoFit=o.autoFit||!1,o._bodyProp.anchor=o.placeholder?null:si.ctr,o._bodyProp.vert=o.vert||null,o._bodyProp.wrap=typeof o.wrap=="boolean"?o.wrap:!0,(o.inset&&!isNaN(Number(o.inset))||o.inset===0)&&(o._bodyProp.lIns=lt(o.inset),o._bodyProp.rIns=lt(o.inset),o._bodyProp.tIns=lt(o.inset),o._bodyProp.bIns=lt(o.inset)),typeof o.underline=="boolean"&&o.underline===!0&&(o.underline={style:"sng"})}return(o.align||"").toLowerCase().indexOf("c")===0?o._bodyProp.align=oi.center:(o.align||"").toLowerCase().indexOf("l")===0?o._bodyProp.align=oi.left:(o.align||"").toLowerCase().indexOf("r")===0?o._bodyProp.align=oi.right:(o.align||"").toLowerCase().indexOf("j")===0&&(o._bodyProp.align=oi.justify),(o.valign||"").toLowerCase().indexOf("b")===0?o._bodyProp.anchor=si.b:(o.valign||"").toLowerCase().indexOf("m")===0?o._bodyProp.anchor=si.ctr:(o.valign||"").toLowerCase().indexOf("t")===0&&(o._bodyProp.anchor=si.t),kl(o.shadow),o}i.options=a(i.options),i.text.forEach(o=>o.options=a(o.options||{})),hi(e,i.text||""),e._slideObjects.push(i)}function u0(e){(e._slideLayout._slideObjects||[]).forEach(r=>{r._type===He.placeholder&&e._slideObjects.filter(t=>t.options&&t.options.placeholder===r.options.placeholder).length===0&&vo(e,[{text:""}],r.options,!1)})}function Hd(e,r){var t;if(r.bkgd&&(r.background||(r.background={}),typeof r.bkgd=="string"?r.background.color=r.bkgd:(r.bkgd.data&&(r.background.data=r.bkgd.data),r.bkgd.path&&(r.background.path=r.bkgd.path),r.bkgd.src&&(r.background.path=r.bkgd.src))),!((t=r.background)===null||t===void 0)&&t.fill&&(r.background.color=r.background.fill),e&&(e.path||e.data)){e.path=e.path||"preencoded.png";let n=(e.path.split(".").pop()||"png").split("?")[0];n==="jpg"&&(n="jpeg"),r._relsMedia=r._relsMedia||[];let i=r._relsMedia.length+1;r._relsMedia.push({path:e.path,type:He.image,extn:n,data:e.data||null,rId:i,Target:`../media/${(r._name||"").replace(/\s+/gi,"-")}-image-${r._relsMedia.length+1}.${n}`}),r._bkgdImgRid=i}}function hi(e,r,t){let n=[];typeof r=="string"||typeof r=="number"||(Array.isArray(r)?n=r:typeof r=="object"&&(n=[r]),n.forEach((i,a)=>{if(t&&t[a]&&t[a].hyperlink&&(i.options=Object.assign(Object.assign({},i.options),t[a])),Array.isArray(i)){let o=[];i.forEach(l=>{l.options&&!l.text.options&&o.push(l.options)}),hi(e,i,o)}else if(Array.isArray(i.text))hi(e,i.text,t&&t[a]?[t[a]]:void 0);else if(i&&typeof i=="object"&&i.options&&i.options.hyperlink&&!i.options.hyperlink._rId)if(typeof i.options.hyperlink!="object")console.log("ERROR: text `hyperlink` option should be an object. Ex: `hyperlink: {url:'https://github.com'}` ");else if(!i.options.hyperlink.url&&!i.options.hyperlink.slide)console.log("ERROR: 'hyperlink requires either: `url` or `slide`'");else{let o=Vr(e);e._rels.push({type:He.hyperlink,data:i.options.hyperlink.slide?"slide":"dummy",rId:o,Target:Ve(i.options.hyperlink.url)||i.options.hyperlink.slide.toString()}),i.options.hyperlink._rId=o}else i&&typeof i=="object"&&i.options&&i.options.hyperlink&&i.options.hyperlink._rId&&e._rels.filter(o=>o.rId===i.options.hyperlink._rId).length===0&&e._rels.push({type:He.hyperlink,data:i.options.hyperlink.slide?"slide":"dummy",rId:i.options.hyperlink._rId,Target:Ve(i.options.hyperlink.url)||i.options.hyperlink.slide.toString()})}))}var d0=class{constructor(e){var r;this.addSlide=e.addSlide,this.getSlide=e.getSlide,this._name=`Slide ${e.slideNumber}`,this._presLayout=e.presLayout,this._rId=e.slideRId,this._rels=[],this._relsChart=[],this._relsMedia=[],this._setSlideNum=e.setSlideNum,this._slideId=e.slideId,this._slideLayout=e.slideLayout||null,this._slideNum=e.slideNumber,this._slideObjects=[],this._slideNumberProps=!((r=this._slideLayout)===null||r===void 0)&&r._slideNumberProps?this._slideLayout._slideNumberProps:null}set bkgd(e){this._bkgd=e,(!this._background||!this._background.color)&&(this._background||(this._background={}),typeof e=="string"&&(this._background.color=e))}get bkgd(){return this._bkgd}set background(e){this._background=e,e&&Hd(e,this)}get background(){return this._background}set color(e){this._color=e}get color(){return this._color}set hidden(e){this._hidden=e}get hidden(){return this._hidden}set slideNumber(e){this._slideNumberProps=e,this._setSlideNum(e)}get slideNumber(){return this._slideNumberProps}get newAutoPagedSlides(){return this._newAutoPagedSlides}addChart(e,r,t){let n=t||{};return n._type=e,Vd(this,e,r,t),this}addImage(e){return Xd(this,e),this}addMedia(e){return s0(this,e),this}addNotes(e){return l0(this,e),this}addShape(e,r){return Js(this,e,r),this}addTable(e,r){return this._newAutoPagedSlides=c0(this,e,r,this._slideLayout,this._presLayout,this.addSlide,this.getSlide),this}addText(e,r){return vo(this,typeof e=="string"||typeof e=="number"?[{text:e,options:r}]:e,r,!1),this}};function f0(e,r){return sr(this,void 0,void 0,function*(){let t=e.data;return yield new Promise((n,i)=>{var a,o;let l=new _d.default,c=(t.length-1)*2+1,s=((o=(a=t[0])===null||a===void 0?void 0:a.labels)===null||o===void 0?void 0:o.length)>1;l.folder("_rels"),l.folder("docProps"),l.folder("xl/_rels"),l.folder("xl/tables"),l.folder("xl/theme"),l.folder("xl/worksheets"),l.folder("xl/worksheets/_rels"),l.file("[Content_Types].xml",` +`)),s}function c0(e,r,t={},n){let i=t||{};i.slideMargin=i.slideMargin||i.slideMargin===0?i.slideMargin:.5;let a=i.w||e.presLayout.width,o=[],l=[],c=[],s=[],u=[],d=[.5,.5,.5,.5],A=0;if(!document.getElementById(r))throw new Error('tableToSlides: Table ID "'+r+'" does not exist!');n?._margin?(Array.isArray(n._margin)?d=n._margin:isNaN(n._margin)||(d=[n._margin,n._margin,n._margin,n._margin]),i.slideMargin=d):i?.slideMargin&&(Array.isArray(i.slideMargin)?d=i.slideMargin:isNaN(i.slideMargin)||(d=[i.slideMargin,i.slideMargin,i.slideMargin,i.slideMargin])),a=(i.w?lt(i.w):e.presLayout.width)-lt(d[1]+d[3]),i.verbose&&(console.log("[[VERBOSE MODE]]"),console.log("|-- `tableToSlides` ----------------------------------------------------|"),console.log(`| tableProps.h .................................... = ${i.h}`),console.log(`| tableProps.w .................................... = ${i.w}`),console.log(`| pptx.presLayout.width ........................... = ${(e.presLayout.width/Ke).toFixed(1)}`),console.log(`| pptx.presLayout.height .......................... = ${(e.presLayout.height/Ke).toFixed(1)}`),console.log(`| emuSlideTabW .................................... = ${(a/Ke).toFixed(1)}`));let f=document.querySelectorAll(`#${r} tr:first-child th`);f.length===0&&(f=document.querySelectorAll(`#${r} tr:first-child td`)),f.forEach(g=>{let y=g;if(y.getAttribute("colspan"))for(let v=0;v{A+=g}),u.forEach((g,y)=>{let v=Number((Number(a)*(g/A*100)/100/Ke).toFixed(2)),b=0,x=document.querySelector(`#${r} thead tr:first-child th:nth-child(${y+1})`);x&&(b=Number(x.getAttribute("data-pptx-min-width")));let P=document.querySelector(`#${r} thead tr:first-child th:nth-child(${y+1})`);P&&(b=Number(P.getAttribute("data-pptx-width"))),s.push(b>v?b:v)}),i.verbose&&console.log(`| arrColW ......................................... = [${s.join(", ")}]`),["thead","tbody","tfoot"].forEach(g=>{document.querySelectorAll(`#${r} ${g} tr`).forEach(y=>{let v=y,b=[];switch(Array.from(v.cells).forEach(x=>{let P=window.getComputedStyle(x).getPropertyValue("color").replace(/\s+/gi,"").replace("rgba(","").replace("rgb(","").replace(")","").split(","),F=window.getComputedStyle(x).getPropertyValue("background-color").replace(/\s+/gi,"").replace("rgba(","").replace("rgb(","").replace(")","").split(",");(window.getComputedStyle(x).getPropertyValue("background-color")==="rgba(0, 0, 0, 0)"||window.getComputedStyle(x).getPropertyValue("transparent"))&&(F=["255","255","255"]);let R={align:null,bold:window.getComputedStyle(x).getPropertyValue("font-weight")==="bold"||Number(window.getComputedStyle(x).getPropertyValue("font-weight"))>=500,border:null,color:Ds(Number(P[0]),Number(P[1]),Number(P[2])),fill:{color:Ds(Number(F[0]),Number(F[1]),Number(F[2]))},fontFace:(window.getComputedStyle(x).getPropertyValue("font-family")||"").split(",")[0].replace(/"/g,"").replace("inherit","").replace("initial","")||null,fontSize:Number(window.getComputedStyle(x).getPropertyValue("font-size").replace(/[a-z]/gi,"")),margin:null,colspan:Number(x.getAttribute("colspan"))||null,rowspan:Number(x.getAttribute("rowspan"))||null,valign:null};if(["left","center","right","start","end"].includes(window.getComputedStyle(x).getPropertyValue("text-align"))){let E=window.getComputedStyle(x).getPropertyValue("text-align").replace("start","left").replace("end","right");R.align=E==="center"?"center":E==="left"?"left":E==="right"?"right":null}if(["top","middle","bottom"].includes(window.getComputedStyle(x).getPropertyValue("vertical-align"))){let E=window.getComputedStyle(x).getPropertyValue("vertical-align");R.valign=E==="top"?"top":E==="middle"?"middle":E==="bottom"?"bottom":null}window.getComputedStyle(x).getPropertyValue("padding-left")&&(R.margin=[0,0,0,0],["padding-top","padding-right","padding-bottom","padding-left"].forEach((B,T)=>{R.margin[T]=Math.round(Number(window.getComputedStyle(x).getPropertyValue(B).replace(/\D/gi,"")))})),(window.getComputedStyle(x).getPropertyValue("border-top-width")||window.getComputedStyle(x).getPropertyValue("border-right-width")||window.getComputedStyle(x).getPropertyValue("border-bottom-width")||window.getComputedStyle(x).getPropertyValue("border-left-width"))&&(R.border=[null,null,null,null],["top","right","bottom","left"].forEach((B,T)=>{let W=Math.round(Number(window.getComputedStyle(x).getPropertyValue("border-"+B+"-width").replace("px",""))),H=[];H=window.getComputedStyle(x).getPropertyValue("border-"+B+"-color").replace(/\s+/gi,"").replace("rgba(","").replace("rgb(","").replace(")","").split(",");let C=Ds(Number(H[0]),Number(H[1]),Number(H[2]));R.border[T]={pt:W,color:C}})),b.push({_type:He.tablecell,text:x.innerText,options:R})}),g){case"thead":o.push(b);break;case"tbody":l.push(b);break;case"tfoot":c.push(b);break;default:console.log(`table parsing: unexpected table part: ${g}`);break}})}),i._arrObjTabHeadRows=o||null,i.colW=s,qd([...o,...l,...c],i,e.presLayout,n).forEach((g,y)=>{let v=e.addSlide({masterName:i.masterSlideName||null});y===0&&(i.y=i.y||d[0]),y>0&&(i.y=i.autoPageSlideStartY||i.newSlideStartY||d[0]),i.verbose&&console.log(`| opts.autoPageSlideStartY: ${i.autoPageSlideStartY} / arrInchMargins[0]: ${d[0]} => opts.y = ${i.y}`),v.addTable(g.rows,{x:i.x||d[3],y:i.y,w:Number(a)/Ke,colW:s,autoPage:!1}),i.addImage&&(i.addImage.options=i.addImage.options||{},!i.addImage.image||!i.addImage.image.path&&!i.addImage.image.data?console.warn("Warning: tableToSlides.addImage requires either `path` or `data`"):v.addImage({path:i.addImage.image.path,data:i.addImage.image.data,x:i.addImage.options.x,y:i.addImage.options.y,w:i.addImage.options.w,h:i.addImage.options.h})),i.addShape&&v.addShape(i.addShape.shapeName,i.addShape.options||{}),i.addTable&&v.addTable(i.addTable.rows,i.addTable.options||{}),i.addText&&v.addText(i.addText.text,i.addText.options||{})})}var u0=0;function d0(e,r){e.bkgd&&(r.bkgd=e.bkgd),e.objects&&Array.isArray(e.objects)&&e.objects.length>0&&e.objects.forEach((t,n)=>{let i=Object.keys(t)[0],a=r;hn[i]&&i==="chart"?Vd(a,t[i].type,t[i].data,t[i].opts):hn[i]&&i==="image"?Xd(a,t[i]):hn[i]&&i==="line"?$s(a,xn.LINE,t[i]):hn[i]&&i==="rect"?$s(a,xn.RECTANGLE,t[i]):hn[i]&&i==="text"?bo(a,[{text:t[i].text}],t[i].options,!1):hn[i]&&i==="placeholder"&&(t[i].options.placeholder=t[i].options.name,delete t[i].options.name,t[i].options._placeholderType=t[i].options.type,delete t[i].options.type,t[i].options._placeholderIdx=100+n,bo(a,[{text:t[i].text}],t[i].options,!0))}),e.slideNumber&&typeof e.slideNumber=="object"&&(r._slideNumberProps=e.slideNumber)}function Vd(e,r,t,n){var i;function a(d){!d||d.style==="none"||(d.size!==void 0&&(isNaN(Number(d.size))||d.size<=0)&&(console.warn("Warning: chart.gridLine.size must be greater than 0."),delete d.size),d.style&&!["solid","dash","dot"].includes(d.style)&&(console.warn("Warning: chart.gridLine.style options: `solid`, `dash`, `dot`."),delete d.style),d.cap&&!["flat","square","round"].includes(d.cap)&&(console.warn("Warning: chart.gridLine.cap options: `flat`, `square`, `round`."),delete d.cap))}let o=++u0,l={_type:null,text:null,options:null,chartRid:null},c=null,s=[];Array.isArray(r)?(r.forEach(d=>{s=s.concat(d.data)}),c=t||n):(s=t,c=n),s.forEach((d,A)=>{d._dataIndex=A,d.labels!==void 0&&!Array.isArray(d.labels[0])&&(d.labels=[d.labels])});let u=c&&typeof c=="object"?c:{};if(u._type=r,u.x=typeof u.x<"u"&&u.x!=null&&!isNaN(Number(u.x))?u.x:1,u.y=typeof u.y<"u"&&u.y!=null&&!isNaN(Number(u.y))?u.y:1,u.w=u.w||"50%",u.h=u.h||"50%",u.objectName=u.objectName?Ve(u.objectName):`Chart ${e._slideObjects.filter(d=>d._type===He.chart).length}`,["bar","col"].includes(u.barDir||"")||(u.barDir="col"),u._type===Ce.AREA&&(["stacked","standard","percentStacked"].includes(u.barGrouping||"")||(u.barGrouping="standard")),u._type===Ce.BAR&&(["clustered","stacked","percentStacked"].includes(u.barGrouping||"")||(u.barGrouping="clustered")),u._type===Ce.BAR3D&&(["clustered","stacked","standard","percentStacked"].includes(u.barGrouping||"")||(u.barGrouping="standard")),!((i=u.barGrouping)===null||i===void 0)&&i.includes("tacked")&&(u.barGapWidthPct||(u.barGapWidthPct=50)),u.dataLabelPosition&&((u._type===Ce.AREA||u._type===Ce.BAR3D||u._type===Ce.DOUGHNUT||u._type===Ce.RADAR)&&delete u.dataLabelPosition,u._type===Ce.PIE&&(["bestFit","ctr","inEnd","outEnd"].includes(u.dataLabelPosition)||delete u.dataLabelPosition),(u._type===Ce.BUBBLE||u._type===Ce.BUBBLE3D||u._type===Ce.LINE||u._type===Ce.SCATTER)&&(["b","ctr","l","r","t"].includes(u.dataLabelPosition)||delete u.dataLabelPosition),u._type===Ce.BAR&&(["stacked","percentStacked"].includes(u.barGrouping||"")||["ctr","inBase","inEnd"].includes(u.dataLabelPosition)||delete u.dataLabelPosition,["clustered"].includes(u.barGrouping||"")||["ctr","inBase","inEnd","outEnd"].includes(u.dataLabelPosition)||delete u.dataLabelPosition)),u.dataLabelBkgrdColors=u.dataLabelBkgrdColors||!u.dataLabelBkgrdColors?u.dataLabelBkgrdColors:!1,["b","l","r","t","tr"].includes(u.legendPos||"")||(u.legendPos="r"),["cone","coneToMax","box","cylinder","pyramid","pyramidToMax"].includes(u.bar3DShape||"")||(u.bar3DShape="box"),["circle","dash","diamond","dot","none","square","triangle"].includes(u.lineDataSymbol||"")||(u.lineDataSymbol="circle"),["gap","span"].includes(u.displayBlanksAs||"")||(u.displayBlanksAs="span"),["standard","marker","filled"].includes(u.radarStyle||"")||(u.radarStyle="standard"),u.lineDataSymbolSize=u.lineDataSymbolSize&&!isNaN(u.lineDataSymbolSize)?u.lineDataSymbolSize:6,u.lineDataSymbolLineSize=u.lineDataSymbolLineSize&&!isNaN(u.lineDataSymbolLineSize)?je(u.lineDataSymbolLineSize):je(.75),u.layout&&["x","y","w","h"].forEach(d=>{let A=u.layout[d];(isNaN(Number(A))||A<0||A>1)&&(console.warn("Warning: chart.layout."+d+" can only be 0-1"),delete u.layout[d])}),u.catGridLine=u.catGridLine||(u._type===Ce.SCATTER?{color:"D9D9D9",size:1}:{style:"none"}),u.valGridLine=u.valGridLine||(u._type===Ce.SCATTER?{color:"D9D9D9",size:1}:{}),u.serGridLine=u.serGridLine||(u._type===Ce.SCATTER?{color:"D9D9D9",size:1}:{style:"none"}),a(u.catGridLine),a(u.valGridLine),a(u.serGridLine),Cl(u.shadow),u.showDataTable=u.showDataTable||!u.showDataTable?u.showDataTable:!1,u.showDataTableHorzBorder=u.showDataTableHorzBorder||!u.showDataTableHorzBorder?u.showDataTableHorzBorder:!0,u.showDataTableVertBorder=u.showDataTableVertBorder||!u.showDataTableVertBorder?u.showDataTableVertBorder:!0,u.showDataTableOutline=u.showDataTableOutline||!u.showDataTableOutline?u.showDataTableOutline:!0,u.showDataTableKeys=u.showDataTableKeys||!u.showDataTableKeys?u.showDataTableKeys:!0,u.showLabel=u.showLabel||!u.showLabel?u.showLabel:!1,u.showLegend=u.showLegend||!u.showLegend?u.showLegend:!1,u.showPercent=u.showPercent||!u.showPercent?u.showPercent:!0,u.showTitle=u.showTitle||!u.showTitle?u.showTitle:!1,u.showValue=u.showValue||!u.showValue?u.showValue:!1,u.showLeaderLines=u.showLeaderLines||!u.showLeaderLines?u.showLeaderLines:!1,u.catAxisLineShow=typeof u.catAxisLineShow<"u"?u.catAxisLineShow:!0,u.valAxisLineShow=typeof u.valAxisLineShow<"u"?u.valAxisLineShow:!0,u.serAxisLineShow=typeof u.serAxisLineShow<"u"?u.serAxisLineShow:!0,u.v3DRotX=!isNaN(u.v3DRotX)&&u.v3DRotX>=-90&&u.v3DRotX<=90?u.v3DRotX:30,u.v3DRotY=!isNaN(u.v3DRotY)&&u.v3DRotY>=0&&u.v3DRotY<=360?u.v3DRotY:30,u.v3DRAngAx=u.v3DRAngAx||!u.v3DRAngAx?u.v3DRAngAx:!0,u.v3DPerspective=!isNaN(u.v3DPerspective)&&u.v3DPerspective>=0&&u.v3DPerspective<=240?u.v3DPerspective:30,u.barGapWidthPct=!isNaN(u.barGapWidthPct)&&u.barGapWidthPct>=0&&u.barGapWidthPct<=1e3?u.barGapWidthPct:150,u.barGapDepthPct=!isNaN(u.barGapDepthPct)&&u.barGapDepthPct>=0&&u.barGapDepthPct<=1e3?u.barGapDepthPct:150,u.chartColors=Array.isArray(u.chartColors)?u.chartColors:u._type===Ce.PIE||u._type===Ce.DOUGHNUT?a0:qi,u.chartColorsOpacity=u.chartColorsOpacity&&!isNaN(u.chartColorsOpacity)?u.chartColorsOpacity:null,u.border=u.border&&typeof u.border=="object"?u.border:null,u.border&&(!u.border.pt||isNaN(u.border.pt))&&(u.border.pt=ri.pt),u.border&&(!u.border.color||typeof u.border.color!="string")&&(u.border.color=ri.color),u.plotArea=u.plotArea||{},u.plotArea.border=u.plotArea.border&&typeof u.plotArea.border=="object"?u.plotArea.border:null,u.plotArea.border&&(!u.plotArea.border.pt||isNaN(u.plotArea.border.pt))&&(u.plotArea.border.pt=ri.pt),u.plotArea.border&&(!u.plotArea.border.color||typeof u.plotArea.border.color!="string")&&(u.plotArea.border.color=ri.color),u.border&&(u.plotArea.border=u.border),u.plotArea.fill=u.plotArea.fill||{color:null,transparency:null},u.fill&&(u.plotArea.fill.color=u.fill),u.chartArea=u.chartArea||{},u.chartArea.border=u.chartArea.border&&typeof u.chartArea.border=="object"?u.chartArea.border:null,u.chartArea.border&&(u.chartArea.border={color:u.chartArea.border.color||ri.color,pt:u.chartArea.border.pt||ri.pt}),u.chartArea.roundedCorners=typeof u.chartArea.roundedCorners=="boolean"?u.chartArea.roundedCorners:!0,u.dataBorder=u.dataBorder&&typeof u.dataBorder=="object"?u.dataBorder:null,u.dataBorder&&(!u.dataBorder.pt||isNaN(u.dataBorder.pt))&&(u.dataBorder.pt=.75),u.dataBorder&&u.dataBorder.color){let d=typeof u.dataBorder.color=="string"&&u.dataBorder.color.length===6&&/^[0-9A-Fa-f]{6}$/.test(u.dataBorder.color),A=Object.values(yo).includes(u.dataBorder.color);!d&&!A&&(u.dataBorder.color="F9F9F9")}return!u.dataLabelFormatCode&&u._type===Ce.SCATTER&&(u.dataLabelFormatCode="General"),!u.dataLabelFormatCode&&(u._type===Ce.PIE||u._type===Ce.DOUGHNUT)&&(u.dataLabelFormatCode=u.showPercent?"0%":"General"),u.dataLabelFormatCode=u.dataLabelFormatCode&&typeof u.dataLabelFormatCode=="string"?u.dataLabelFormatCode:"#,##0",!u.dataLabelFormatScatter&&u._type===Ce.SCATTER&&(u.dataLabelFormatScatter="custom"),u.lineSize=typeof u.lineSize=="number"?u.lineSize:2,u.valAxisMajorUnit=typeof u.valAxisMajorUnit=="number"?u.valAxisMajorUnit:null,u._type===Ce.AREA||u._type===Ce.BAR||u._type===Ce.BAR3D||u._type===Ce.LINE?u.catAxisMultiLevelLabels=!!u.catAxisMultiLevelLabels:delete u.catAxisMultiLevelLabels,l._type="chart",l.options=u,l.chartRid=Vr(e),e._relsChart.push({rId:Vr(e),data:s,opts:u,type:u._type,globalId:o,fileName:`chart${o}.xml`,Target:`/ppt/charts/chart${o}.xml`}),e._slideObjects.push(l),l}function Xd(e,r){let t={_type:null,text:null,options:null,image:null,imageRid:null,hyperlink:null},n=r.x||0,i=r.y||0,a=r.w||0,o=r.h||0,l=r.sizing||null,c=r.hyperlink||"",s=r.data||"",u=r.path||"",d=Vr(e),A=r.objectName?Ve(r.objectName):`Image ${e._slideObjects.filter(p=>p._type===He.image).length}`;if(!u&&!s)return console.error("ERROR: addImage() requires either 'data' or 'path' parameter!"),null;if(u&&typeof u!="string")return console.error(`ERROR: addImage() 'path' should be a string, ex: {path:'/img/sample.png'} - you sent ${String(u)}`),null;if(s&&typeof s!="string")return console.error(`ERROR: addImage() 'data' should be a string, ex: {data:'image/png;base64,NMP[...]'} - you sent ${String(s)}`),null;if(s&&typeof s=="string"&&!s.toLowerCase().includes("base64,"))return console.error("ERROR: Image `data` value lacks a base64 header! Ex: 'image/png;base64,NMP[...]')"),null;let f=(u.substring(u.lastIndexOf("/")+1).split("?")[0].split(".").pop().split("#")[0]||"png").toLowerCase();if(s&&/image\/(\w+);/.exec(s)&&/image\/(\w+);/.exec(s).length>0?f=/image\/(\w+);/.exec(s)[1]:s?.toLowerCase().includes("image/svg+xml")&&(f="svg"),t._type=He.image,t.image=u||"preencoded.png",t.options={x:n||0,y:i||0,w:a||1,h:o||1,altText:r.altText||"",rounding:typeof r.rounding=="boolean"?r.rounding:!1,sizing:l,placeholder:r.placeholder,rotate:r.rotate||0,flipV:r.flipV||!1,flipH:r.flipH||!1,transparency:r.transparency||0,objectName:A,shadow:Cl(r.shadow)},f==="svg")e._relsMedia.push({path:u||s+"png",type:"image/png",extn:"png",data:s||"",rId:d,Target:`../media/image-${e._slideNum}-${e._relsMedia.length+1}.png`,isSvgPng:!0,svgSize:{w:nt(t.options.w,"X",e._presLayout),h:nt(t.options.h,"Y",e._presLayout)}}),t.imageRid=d,e._relsMedia.push({path:u||s,type:"image/svg+xml",extn:f,data:s||"",rId:d+1,Target:`../media/image-${e._slideNum}-${e._relsMedia.length+1}.${f}`}),t.imageRid=d+1;else{let p=e._relsMedia.filter(g=>g.path&&g.path===u&&g.type==="image/"+f&&!g.isDuplicate)[0];e._relsMedia.push({path:u||"preencoded."+f,type:"image/"+f,extn:f,data:s||"",rId:d,isDuplicate:!!p?.Target,Target:p?.Target?p.Target:`../media/image-${e._slideNum}-${e._relsMedia.length+1}.${f}`}),t.imageRid=d}if(typeof c=="object"){if(!c.url&&!c.slide)throw new Error("ERROR: `hyperlink` option requires either: `url` or `slide`");d++,e._rels.push({type:He.hyperlink,data:c.slide?"slide":"dummy",rId:d,Target:c.url||c.slide.toString()}),c._rId=d,t.hyperlink=c}e._slideObjects.push(t)}function f0(e,r){let t=r.x||0,n=r.y||0,i=r.w||2,a=r.h||2,o=r.data||"",l=r.link||"",c=r.path||"",s=r.type||"audio",u="",d=r.cover||o0,A=r.objectName?Ve(r.objectName):`Media ${e._slideObjects.filter(p=>p._type===He.media).length}`,f={_type:He.media};if(!c&&!o&&s!=="online")throw new Error("addMedia() error: either `data` or `path` are required!");if(o&&!o.toLowerCase().includes("base64,"))throw new Error("addMedia() error: `data` value lacks a base64 header! Ex: 'video/mpeg;base64,NMP[...]')");if(!d.toLowerCase().includes("base64,"))throw new Error("addMedia() error: `cover` value lacks a base64 header! Ex: 'data:image/png;base64,iV[...]')");if(s==="online"&&!l)throw new Error("addMedia() error: online videos require `link` value");if(u=r.extn||(o?o.split(";")[0].split("/")[1]:c.split(".").pop())||"mp3",f.mtype=s,f.media=c||"preencoded.mov",f.options={},f.options.x=t,f.options.y=n,f.options.w=i,f.options.h=a,f.options.objectName=A,s==="online"){let p=Vr(e);e._relsMedia.push({path:c||"preencoded"+u,data:"dummy",type:"online",extn:u,rId:p,Target:l}),f.mediaRid=p,e._relsMedia.push({path:"preencoded.png",data:d,type:"image/png",extn:"png",rId:Vr(e),Target:`../media/image-${e._slideNum}-${e._relsMedia.length+1}.png`})}else{let p=e._relsMedia.filter(y=>y.path&&y.path===c&&y.type===s+"/"+u&&!y.isDuplicate)[0],g=Vr(e);e._relsMedia.push({path:c||"preencoded"+u,type:s+"/"+u,extn:u,data:o||"",rId:g,isDuplicate:!!p?.Target,Target:p?.Target?p.Target:`../media/media-${e._slideNum}-${e._relsMedia.length+1}.${u}`}),f.mediaRid=g,e._relsMedia.push({path:c||"preencoded"+u,type:s+"/"+u,extn:u,data:o||"",rId:Vr(e),isDuplicate:!!p?.Target,Target:p?.Target?p.Target:`../media/media-${e._slideNum}-${e._relsMedia.length+0}.${u}`}),e._relsMedia.push({path:"preencoded.png",type:"image/png",extn:"png",data:d,rId:Vr(e),Target:`../media/image-${e._slideNum}-${e._relsMedia.length+1}.png`})}e._slideObjects.push(f)}function h0(e,r){e._slideObjects.push({_type:He.notes,text:[{text:r}]})}function $s(e,r,t){let n=typeof t=="object"?t:{};n.line=n.line||{type:"none"};let i={_type:He.text,shape:r||xn.RECTANGLE,options:n,text:null};if(!r)throw new Error("Missing/Invalid shape parameter! Example: `addShape(pptxgen.shapes.LINE, {x:1, y:1, w:1, h:1});`");let a={type:n.line.type||"solid",color:n.line.color||Wd,transparency:n.line.transparency||0,width:n.line.width||1,dashType:n.line.dashType||"solid",beginArrowType:n.line.beginArrowType||null,endArrowType:n.line.endArrowType||null};if(typeof n.line=="object"&&n.line.type!=="none"&&(n.line=a),n.x=n.x||(n.x===0?0:1),n.y=n.y||(n.y===0?0:1),n.w=n.w||(n.w===0?0:1),n.h=n.h||(n.h===0?0:1),n.objectName=n.objectName?Ve(n.objectName):`Shape ${e._slideObjects.filter(o=>o._type===He.text).length}`,typeof n.line=="string"){let o=a;o.color=String(n.line),n.line=o}typeof n.lineSize=="number"&&(n.line.width=n.lineSize),typeof n.lineDash=="string"&&(n.line.dashType=n.lineDash),typeof n.lineHead=="string"&&(n.line.beginArrowType=n.lineHead),typeof n.lineTail=="string"&&(n.line.endArrowType=n.lineTail),hi(e,i),e._slideObjects.push(i)}function p0(e,r,t,n,i,a,o){let l=[e],c=t&&typeof t=="object"?t:{};c.objectName=c.objectName?Ve(c.objectName):`Table ${e._slideObjects.filter(A=>A._type===He.table).length}`;{if(r===null||r.length===0||!Array.isArray(r))throw new Error("addTable: Array expected! EX: 'slide.addTable( [rows], {options} );' (https://gitbrent.github.io/PptxGenJS/docs/api-tables.html)");if(!r[0]||!Array.isArray(r[0]))throw new Error("addTable: 'rows' should be an array of cells! EX: 'slide.addTable( [ ['A'], ['B'], {text:'C',options:{align:'center'}} ] );' (https://gitbrent.github.io/PptxGenJS/docs/api-tables.html)")}let s=[];r.forEach(A=>{let f=[];Array.isArray(A)?A.forEach(p=>{let g={_type:He.tablecell,text:"",options:typeof p=="object"&&p.options?p.options:{}};typeof p=="string"||typeof p=="number"?g.text=p.toString():p.text&&(typeof p.text=="string"||typeof p.text=="number"?g.text=p.text.toString():p.text&&(g.text=p.text),p.options&&typeof p.options=="object"&&(g.options=p.options)),g.options.border=g.options.border||c.border||[{type:"none"},{type:"none"},{type:"none"},{type:"none"}];let y=g.options.border;!Array.isArray(y)&&typeof y=="object"&&(g.options.border=[y,y,y,y]),g.options.border[0]||(g.options.border[0]={type:"none"}),g.options.border[1]||(g.options.border[1]={type:"none"}),g.options.border[2]||(g.options.border[2]={type:"none"}),g.options.border[3]||(g.options.border[3]={type:"none"}),[0,1,2,3].forEach(b=>{g.options.border[b]={type:g.options.border[b].type||ti.type,color:g.options.border[b].color||ti.color,pt:typeof g.options.border[b].pt=="number"?g.options.border[b].pt:ti.pt}}),f.push(g)}):(console.log("addTable: tableRows has a bad row. A row should be an array of cells. You provided:"),console.log(A)),s.push(f)}),c.x=nt(c.x||(c.x===0?0:Ke/2),"X",i),c.y=nt(c.y||(c.y===0?0:Ke/2),"Y",i),c.h&&(c.h=nt(c.h,"Y",i)),c.fontSize=c.fontSize||yr,c.margin=c.margin===0||c.margin?c.margin:Ud,typeof c.margin=="number"&&(c.margin=[Number(c.margin),Number(c.margin),Number(c.margin),Number(c.margin)]),JSON.stringify({arrRows:s}).indexOf("hyperlink")===-1&&(c.color||(c.color=c.color||nr)),typeof c.border=="string"?(console.warn("addTable `border` option must be an object. Ex: `{border: {type:'none'}}`"),c.border=null):Array.isArray(c.border)&&[0,1,2,3].forEach(A=>{c.border[A]=c.border[A]?{type:c.border[A].type||ti.type,color:c.border[A].color||ti.color,pt:c.border[A].pt||ti.pt}:{type:"none"}}),c.autoPage=typeof c.autoPage=="boolean"?c.autoPage:!1,c.autoPageRepeatHeader=typeof c.autoPageRepeatHeader=="boolean"?c.autoPageRepeatHeader:!1,c.autoPageHeaderRows=typeof c.autoPageHeaderRows<"u"&&!isNaN(Number(c.autoPageHeaderRows))?Number(c.autoPageHeaderRows):1,c.autoPageLineWeight=typeof c.autoPageLineWeight<"u"&&!isNaN(Number(c.autoPageLineWeight))?Number(c.autoPageLineWeight):0,c.autoPageLineWeight&&(c.autoPageLineWeight>1?c.autoPageLineWeight=1:c.autoPageLineWeight<-1&&(c.autoPageLineWeight=-1));let u=na;if(n&&typeof n._margin<"u"&&(Array.isArray(n._margin)?u=n._margin:isNaN(Number(n._margin))||(u=[Number(n._margin),Number(n._margin),Number(n._margin),Number(n._margin)])),c.colW){let A=s[0].reduce((f,p)=>{var g;return!((g=p?.options)===null||g===void 0)&&g.colspan&&typeof p.options.colspan=="number"?f+=p.options.colspan:f+=1,f},0);typeof c.colW=="string"||typeof c.colW=="number"||c.colW&&Array.isArray(c.colW)&&c.colW.length===1&&A>1?(c.w=Math.floor(Number(c.colW)*A),c.colW=null):c.colW&&Array.isArray(c.colW)&&c.colW.length!==A&&(console.warn("addTable: mismatch: (colW.length != data.length) Therefore, defaulting to evenly distributed col widths."),c.colW=null)}else c.w?c.w=nt(c.w,"X",i):c.w=Math.floor(i._sizeW/Ke-u[1]-u[3]);c.x&&c.x<20&&(c.x=lt(c.x)),c.y&&c.y<20&&(c.y=lt(c.y)),c.w&&typeof c.w=="number"&&c.w<20&&(c.w=lt(c.w)),c.h&&typeof c.h=="number"&&c.h<20&&(c.h=lt(c.h)),s.forEach(A=>{A.forEach((f,p)=>{typeof f=="number"||typeof f=="string"?A[p]={_type:He.tablecell,text:String(A[p]),options:c}:typeof f=="object"&&(typeof f.text=="number"?A[p].text=A[p].text.toString():(typeof f.text>"u"||f.text===null)&&(A[p].text=""),A[p].options=f.options||{},A[p]._type=He.tablecell)})});let d=[];return c&&!c.autoPage?(hi(e,s),e._slideObjects.push({_type:He.table,arrTabRows:s,options:Object.assign({},c)})):(c.autoPageRepeatHeader&&(c._arrObjTabHeadRows=s.filter((A,f)=>f{o(e._slideNum+f)||l.push(a({masterName:n?._name||null})),f>0&&(c.y=lt(c.autoPageSlideStartY||c.newSlideStartY||u[0]));{let p=o(e._slideNum+f);c.autoPage=!1,hi(p,A.rows),p.addTable(A.rows,Object.assign({},c)),f>0&&d.push(p)}})),d}function bo(e,r,t,n){let i={_type:n?He.placeholder:He.text,shape:t?.shape||xn.RECTANGLE,text:!r||r.length===0?[{text:"",options:null}]:r,options:t||{}};function a(o){{if(o.placeholder||(o.color=o.color||i.options.color||e.color||nr),(o.placeholder||n)&&(o.bullet=o.bullet||!1),o.placeholder&&e._slideLayout&&e._slideLayout._slideObjects){let l=e._slideLayout._slideObjects.filter(c=>c._type==="placeholder"&&c.options&&c.options.placeholder&&c.options.placeholder===o.placeholder)[0];l?.options&&(o=Object.assign(Object.assign({},o),l.options))}if(o.objectName=o.objectName?Ve(o.objectName):`Text ${e._slideObjects.filter(l=>l._type===He.text).length}`,o.shape===xn.LINE){let l={type:o.line.type||"solid",color:o.line.color||Wd,transparency:o.line.transparency||0,width:o.line.width||1,dashType:o.line.dashType||"solid",beginArrowType:o.line.beginArrowType||null,endArrowType:o.line.endArrowType||null};if(typeof o.line=="object"&&(o.line=l),typeof o.line=="string"){let c=l;typeof o.line=="string"&&(c.color=o.line),o.line=c}typeof o.lineSize=="number"&&(o.line.width=o.lineSize),typeof o.lineDash=="string"&&(o.line.dashType=o.lineDash),typeof o.lineHead=="string"&&(o.line.beginArrowType=o.lineHead),typeof o.lineTail=="string"&&(o.line.endArrowType=o.lineTail)}o.line=o.line||{},o.lineSpacing=o.lineSpacing&&!isNaN(o.lineSpacing)?o.lineSpacing:null,o.lineSpacingMultiple=o.lineSpacingMultiple&&!isNaN(o.lineSpacingMultiple)?o.lineSpacingMultiple:null,o._bodyProp=o._bodyProp||{},o._bodyProp.autoFit=o.autoFit||!1,o._bodyProp.anchor=o.placeholder?null:si.ctr,o._bodyProp.vert=o.vert||null,o._bodyProp.wrap=typeof o.wrap=="boolean"?o.wrap:!0,(o.inset&&!isNaN(Number(o.inset))||o.inset===0)&&(o._bodyProp.lIns=lt(o.inset),o._bodyProp.rIns=lt(o.inset),o._bodyProp.tIns=lt(o.inset),o._bodyProp.bIns=lt(o.inset)),typeof o.underline=="boolean"&&o.underline===!0&&(o.underline={style:"sng"})}return(o.align||"").toLowerCase().indexOf("c")===0?o._bodyProp.align=oi.center:(o.align||"").toLowerCase().indexOf("l")===0?o._bodyProp.align=oi.left:(o.align||"").toLowerCase().indexOf("r")===0?o._bodyProp.align=oi.right:(o.align||"").toLowerCase().indexOf("j")===0&&(o._bodyProp.align=oi.justify),(o.valign||"").toLowerCase().indexOf("b")===0?o._bodyProp.anchor=si.b:(o.valign||"").toLowerCase().indexOf("m")===0?o._bodyProp.anchor=si.ctr:(o.valign||"").toLowerCase().indexOf("t")===0&&(o._bodyProp.anchor=si.t),Cl(o.shadow),o}i.options=a(i.options),i.text.forEach(o=>o.options=a(o.options||{})),hi(e,i.text||""),e._slideObjects.push(i)}function A0(e){(e._slideLayout._slideObjects||[]).forEach(r=>{r._type===He.placeholder&&e._slideObjects.filter(t=>t.options&&t.options.placeholder===r.options.placeholder).length===0&&bo(e,[{text:""}],r.options,!1)})}function Hd(e,r){var t;if(r.bkgd&&(r.background||(r.background={}),typeof r.bkgd=="string"?r.background.color=r.bkgd:(r.bkgd.data&&(r.background.data=r.bkgd.data),r.bkgd.path&&(r.background.path=r.bkgd.path),r.bkgd.src&&(r.background.path=r.bkgd.src))),!((t=r.background)===null||t===void 0)&&t.fill&&(r.background.color=r.background.fill),e&&(e.path||e.data)){e.path=e.path||"preencoded.png";let n=(e.path.split(".").pop()||"png").split("?")[0];n==="jpg"&&(n="jpeg"),r._relsMedia=r._relsMedia||[];let i=r._relsMedia.length+1;r._relsMedia.push({path:e.path,type:He.image,extn:n,data:e.data||null,rId:i,Target:`../media/${(r._name||"").replace(/\s+/gi,"-")}-image-${r._relsMedia.length+1}.${n}`}),r._bkgdImgRid=i}}function hi(e,r,t){let n=[];typeof r=="string"||typeof r=="number"||(Array.isArray(r)?n=r:typeof r=="object"&&(n=[r]),n.forEach((i,a)=>{if(t&&t[a]&&t[a].hyperlink&&(i.options=Object.assign(Object.assign({},i.options),t[a])),Array.isArray(i)){let o=[];i.forEach(l=>{l.options&&!l.text.options&&o.push(l.options)}),hi(e,i,o)}else if(Array.isArray(i.text))hi(e,i.text,t&&t[a]?[t[a]]:void 0);else if(i&&typeof i=="object"&&i.options&&i.options.hyperlink&&!i.options.hyperlink._rId)if(typeof i.options.hyperlink!="object")console.log("ERROR: text `hyperlink` option should be an object. Ex: `hyperlink: {url:'https://github.com'}` ");else if(!i.options.hyperlink.url&&!i.options.hyperlink.slide)console.log("ERROR: 'hyperlink requires either: `url` or `slide`'");else{let o=Vr(e);e._rels.push({type:He.hyperlink,data:i.options.hyperlink.slide?"slide":"dummy",rId:o,Target:Ve(i.options.hyperlink.url)||i.options.hyperlink.slide.toString()}),i.options.hyperlink._rId=o}else i&&typeof i=="object"&&i.options&&i.options.hyperlink&&i.options.hyperlink._rId&&e._rels.filter(o=>o.rId===i.options.hyperlink._rId).length===0&&e._rels.push({type:He.hyperlink,data:i.options.hyperlink.slide?"slide":"dummy",rId:i.options.hyperlink._rId,Target:Ve(i.options.hyperlink.url)||i.options.hyperlink.slide.toString()})}))}var g0=class{constructor(e){var r;this.addSlide=e.addSlide,this.getSlide=e.getSlide,this._name=`Slide ${e.slideNumber}`,this._presLayout=e.presLayout,this._rId=e.slideRId,this._rels=[],this._relsChart=[],this._relsMedia=[],this._setSlideNum=e.setSlideNum,this._slideId=e.slideId,this._slideLayout=e.slideLayout||null,this._slideNum=e.slideNumber,this._slideObjects=[],this._slideNumberProps=!((r=this._slideLayout)===null||r===void 0)&&r._slideNumberProps?this._slideLayout._slideNumberProps:null}set bkgd(e){this._bkgd=e,(!this._background||!this._background.color)&&(this._background||(this._background={}),typeof e=="string"&&(this._background.color=e))}get bkgd(){return this._bkgd}set background(e){this._background=e,e&&Hd(e,this)}get background(){return this._background}set color(e){this._color=e}get color(){return this._color}set hidden(e){this._hidden=e}get hidden(){return this._hidden}set slideNumber(e){this._slideNumberProps=e,this._setSlideNum(e)}get slideNumber(){return this._slideNumberProps}get newAutoPagedSlides(){return this._newAutoPagedSlides}addChart(e,r,t){let n=t||{};return n._type=e,Vd(this,e,r,t),this}addImage(e){return Xd(this,e),this}addMedia(e){return f0(this,e),this}addNotes(e){return h0(this,e),this}addShape(e,r){return $s(this,e,r),this}addTable(e,r){return this._newAutoPagedSlides=p0(this,e,r,this._slideLayout,this._presLayout,this.addSlide,this.getSlide),this}addText(e,r){return bo(this,typeof e=="string"||typeof e=="number"?[{text:e,options:r}]:e,r,!1),this}};function m0(e,r){return sr(this,void 0,void 0,function*(){let t=e.data;return yield new Promise((n,i)=>{var a,o;let l=new _d.default,c=(t.length-1)*2+1,s=((o=(a=t[0])===null||a===void 0?void 0:a.labels)===null||o===void 0?void 0:o.length)>1;l.folder("_rels"),l.folder("docProps"),l.folder("xl/_rels"),l.folder("xl/tables"),l.folder("xl/theme"),l.folder("xl/worksheets"),l.folder("xl/worksheets/_rels"),l.file("[Content_Types].xml",` `),l.file("_rels/.rels",` `),l.file("docProps/app.xml",`Microsoft Macintosh Excel0falseWorksheets1Sheet1falsefalsefalse16.0300 `),l.file("docProps/core.xml",'PptxGenJSPptxGenJS'+new Date().toISOString()+''+new Date().toISOString()+""),l.file("xl/_rels/workbook.xml.rels",''),l.file("xl/styles.xml",` @@ -172,7 +212,7 @@ end`},au=function(){for(var e=[],r=0;r `);{let u='';if(e.opts._type===Ce.BUBBLE||e.opts._type===Ce.BUBBLE3D)u+=``;else if(e.opts._type===Ce.SCATTER)u+=``;else if(s){let d=t.length;t[0].labels.forEach(A=>d+=A.filter(f=>f&&f!=="").length),u+=``,u+=""}else{let d=t.length+t[0].labels.length*t[0].labels[0].length+t[0].labels.length,A=t.length+t[0].labels.length*t[0].labels[0].length+1;u+=``,u+=''}e.opts._type===Ce.BUBBLE||e.opts._type===Ce.BUBBLE3D?t.forEach((d,A)=>{A===0?u+="X-Axis":(u+=`${Ve(d.name||`Y-Axis${A}`)}`,u+=`${Ve(`Size${A}`)}`)}):t.forEach(d=>{u+=`${Ve((d.name||" ").replace("X-Axis","X-Values"))}`}),e.opts._type!==Ce.BUBBLE&&e.opts._type!==Ce.BUBBLE3D&&e.opts._type!==Ce.SCATTER&&t[0].labels.slice().reverse().forEach(d=>{d.filter(A=>A&&A!=="").forEach(A=>{u+=`${Ve(A)}`})}),u+=` `,l.file("xl/sharedStrings.xml",u)}{let u='';if(e.opts._type===Ce.BUBBLE||e.opts._type===Ce.BUBBLE3D){u+=``,u+=``;let d=1;t.forEach((A,f)=>{f===0?u+=``:(u+=``,d++,u+=``)})}else e.opts._type===Ce.SCATTER?(u+=`
          `,u+=``,t.forEach((d,A)=>{u+=``})):(u+=`
          `,u+=``,t[0].labels.forEach((d,A)=>{u+=``}),t.forEach((d,A)=>{u+=``}));u+="",u+='',u+="
          ",l.file("xl/tables/table1.xml",u)}{let u='';if(u+='',e.opts._type===Ce.BUBBLE||e.opts._type===Ce.BUBBLE3D?u+=``:e.opts._type===Ce.SCATTER?u+=``:u+=``,u+='',u+='',e.opts._type===Ce.BUBBLE||e.opts._type===Ce.BUBBLE3D){u+="",u+=``,u+='0';for(let d=1;d${d}`;u+="",t[0].values.forEach((d,A)=>{u+=``,u+=`${d}`;let f=2;for(let p=1;p${t[p].values[A]||""}`,f++,u+=`${t[p].sizes[A]||""}`,f++;u+=""})}else if(e.opts._type===Ce.SCATTER){u+="",u+=``;for(let d=0;d${d}`;u+="",t[0].values.forEach((d,A)=>{u+=``,u+=`${d}`;for(let f=1;f${t[f].values[A]||t[f].values[A]===0?t[f].values[A]:""}`;u+=""})}else if(u+="",s){u+=``;for(let p=0;p0`;for(let p=t[0].labels.length-1;p${p}`;u+="";let d=t.length,A=t[0].labels[0].length,f=t[0].labels.length;for(let p=0;p`;let g=d,y=t[0].labels.slice().reverse();y.forEach((v,b)=>{if(v[p]){let P=b===0?1:y[b-1].filter(F=>F&&F!=="").length;g+=P,u+=`${g}`}});for(let v=0;v${t[v].values[p]||0}`;u+=""}}else{u+=``,t[0].labels.forEach((d,A)=>{u+=`0`});for(let d=0;d${d+1}`;u+="",t[0].labels[0].forEach((d,A)=>{u+=``;for(let f=t[0].labels.length-1;f>=0;f--)u+=``,u+=`${t.length+A+1}`,u+="";for(let f=0;f${t[f].values[A]||""}`;u+=""})}u+="",u+='',u+=` -`,l.file("xl/worksheets/sheet1.xml",u)}l.generateAsync({type:"base64"}).then(u=>{r.file(`ppt/embeddings/Microsoft_Excel_Worksheet${e.globalId}.xlsx`,u,{base64:!0}),r.file("ppt/charts/_rels/"+e.fileName+".rels",``),r.file(`ppt/charts/${e.fileName}`,h0(e)),n("")}).catch(u=>{i(u)})})})}function h0(e){var r,t,n,i;let a='',o=!1;if(a+='',a+='',a+=``,a+="",e.opts.showTitle?(a+=Mo({title:e.opts.title||"Chart Title",color:e.opts.titleColor,fontFace:e.opts.titleFontFace,fontSize:e.opts.titleFontSize||Jv,titleAlign:e.opts.titleAlign,titleBold:e.opts.titleBold,titlePos:e.opts.titlePos,titleRotate:e.opts.titleRotate},e.opts.x,e.opts.y),a+=''):a+='',e.opts._type===Ce.BAR3D&&(a+=``),a+="",e.opts.layout?(a+="",a+=" ",a+=' ',a+=' ',a+=' ',a+=' ',a+=' ',a+=' ',a+=' ',a+=" ",a+=""):a+="",Array.isArray(e.opts._type)?e.opts._type.forEach(l=>{let c=Object.assign(Object.assign({},e.opts),l.options),s=c.secondaryValAxis?eo:qr,u=c.secondaryCatAxis?Xs:Gi;o=o||c.secondaryValAxis,a+=xu(l.type,l.data,c,s,u)}):a+=xu(e.opts._type,e.data,e.opts,qr,Gi),e.opts._type!==Ce.PIE&&e.opts._type!==Ce.DOUGHNUT){if(e.opts.valAxes&&e.opts.valAxes.length>1&&!o)throw new Error("Secondary axis must be used by one of the multiple charts");if(e.opts.catAxes){if(!e.opts.valAxes||e.opts.valAxes.length!==e.opts.catAxes.length)throw new Error("There must be the same number of value and category axes.");a+=Ds(Object.assign(Object.assign({},e.opts),e.opts.catAxes[0]),Gi,qr)}else a+=Ds(e.opts,Gi,qr);e.opts.valAxes?(a+=Es(Object.assign(Object.assign({},e.opts),e.opts.valAxes[0]),qr),e.opts.valAxes[1]&&(a+=Es(Object.assign(Object.assign({},e.opts),e.opts.valAxes[1]),eo))):(a+=Es(e.opts,qr),e.opts._type===Ce.BAR3D&&(a+=p0(e.opts,Gd,qr))),!((r=e.opts)===null||r===void 0)&&r.catAxes&&(!((t=e.opts)===null||t===void 0)&&t.catAxes[1])&&(a+=Ds(Object.assign(Object.assign({},e.opts),e.opts.catAxes[1]),Xs,eo))}return e.opts.showDataTable&&(a+="",a+=` `,a+=` `,a+=` `,a+=` `,a+=" ",a+=" ",a+=' ',a+=" ",a+=" ",a+=" ",a+=' ',a+=" ",a+=" ",a+=' ',a+=` `,a+=' ',a+=' ',a+=' ',a+=' ',a+=" ",a+=" ",a+=' ',a+=" ",a+=" ",a+=""),a+=" ",a+=!((n=e.opts.plotArea.fill)===null||n===void 0)&&n.color?lr(e.opts.plotArea.fill):"",a+=e.opts.plotArea.border?`${lr(e.opts.plotArea.border.color)}`:"",a+=" ",a+=" ",a+="",e.opts.showLegend&&(a+="",a+='',a+='',(e.opts.legendFontFace||e.opts.legendFontSize||e.opts.legendColor)&&(a+="",a+=" ",a+=" ",a+=" ",a+=" ",a+=e.opts.legendFontSize?``:"",e.opts.legendColor&&(a+=lr(e.opts.legendColor)),e.opts.legendFontFace&&(a+=''),e.opts.legendFontFace&&(a+=''),a+=" ",a+=" ",a+=' ',a+=" ",a+=""),a+=""),a+=' ',a+=' ',e.opts._type===Ce.SCATTER&&(a+=''),a+="",a+="",a+=!((i=e.opts.chartArea.fill)===null||i===void 0)&&i.color?lr(e.opts.chartArea.fill):"",a+=e.opts.chartArea.border?`${lr(e.opts.chartArea.border.color)}`:"",a+=" ",a+="",a+='',a+="",a}function xu(e,r,t,n,i,a){let o=-1,l=1,c=null,s="";switch(e){case Ce.AREA:case Ce.BAR:case Ce.BAR3D:case Ce.LINE:case Ce.RADAR:s+=``,e===Ce.AREA&&t.barGrouping==="stacked"&&(s+=''),(e===Ce.BAR||e===Ce.BAR3D)&&(s+='',s+=''),e===Ce.RADAR&&(s+=''),s+='',r.forEach(u=>{var d;o++,s+="",s+=` `,s+=" ",s+=" ",s+=" Sheet1!$"+yt(u._dataIndex+u.labels.length+1)+"$1",s+=' '+Ve(u.name)+"",s+=" ",s+=" ";let A=t.chartColors?t.chartColors[o%t.chartColors.length]:null;s+=" ",A==="transparent"?s+="":t.chartColorsOpacity?s+=""+ft(A,``)+"":s+=""+ft(A)+"",e===Ce.LINE||e===Ce.RADAR?t.lineSize===0?s+="":(s+=`${ft(A)}`,s+=''):t.dataBorder&&(s+=`${ft(t.dataBorder.color)}`),s+=Mn(t.shadow,zn),s+=" ",s+=' ',e!==Ce.RADAR&&(s+="",s+=``,t.dataLabelBkgrdColors&&(s+=`${ft(A)}`),s+="",s+=``,s+=`${ft(t.dataLabelColor||nr)}`,s+=``,s+="",t.dataLabelPosition&&(s+=``),s+='',s+=``,s+=``,s+=``,s+=""),(e===Ce.LINE||e===Ce.RADAR)&&(s+="",s+=' ',t.lineDataSymbolSize&&(s+=``),s+=" ",s+=` ${ft(t.chartColors[u._dataIndex+1>t.chartColors.length?Math.floor(Math.random()*t.chartColors.length):u._dataIndex])}`,s+=` ${ft(t.lineDataSymbolLineColor||A)}`,s+=" ",s+=" ",s+=""),(e===Ce.BAR||e===Ce.BAR3D)&&r.length===1&&(t.chartColors&&t.chartColors!==ji&&t.chartColors.length>1||!((d=t.invertedColors)===null||d===void 0)&&d.length)&&u.values.forEach((f,p)=>{let g=f<0?t.invertedColors||t.chartColors||ji:t.chartColors||[];s+=" ",s+=` `,s+=' ',s+=' ',s+=" ",t.lineSize===0?s+="":e===Ce.BAR?(s+="",s+=' ',s+=""):(s+="",s+=" ",s+=' ',s+=" ",s+=""),s+=Mn(t.shadow,zn),s+=" ",s+=" "}),s+="",t.catLabelFormatCode?(s+=" ",s+=` Sheet1!$A$2:$A$${u.labels[0].length+1}`,s+=" ",s+=" "+(t.catLabelFormatCode||"General")+"",s+=` `,u.labels[0].forEach((f,p)=>s+=`${Ve(f)}`),s+=" ",s+=" "):(s+=" ",s+=` Sheet1!$A$2:$${yt(u.labels.length)}$${u.labels[0].length+1}`,s+=" ",s+=` `,u.labels.forEach(f=>{s+="",f.forEach((p,g)=>s+=`${Ve(p)}`),s+=""}),s+=" ",s+=" "),s+="",s+="",s+=" ",s+=`Sheet1!$${yt(u._dataIndex+u.labels.length+1)}$2:$${yt(u._dataIndex+u.labels.length+1)}$${u.labels[0].length+1}`,s+=" ",s+=" "+(t.valLabelFormatCode||t.dataTableFormatCode||"General")+"",s+=` `,u.values.forEach((f,p)=>s+=`${f||f===0?f:""}`),s+=" ",s+=" ",s+="",e===Ce.LINE&&(s+=''),s+=""}),s+=" ",s+=` `,s+=" ",s+=" ",s+=" ",s+=" ",s+=` `,s+=" "+ft(t.dataLabelColor||nr)+"",s+=' ',s+=" ",s+=" ",s+=" ",t.dataLabelPosition&&(s+=' '),s+=' ',s+=' ',s+=' ',s+=' ',s+=' ',s+=' ',s+=` `,s+=" ",e===Ce.BAR?(s+=` `,s+=` `):e===Ce.BAR3D?(s+=` `,s+=` `,s+=' '):e===Ce.LINE&&(s+=' '),s+=``,s+=``;break;case Ce.SCATTER:s+="",s+='',s+='',o=-1,r.filter((u,d)=>d>0).forEach((u,d)=>{o++,s+="",s+=` `,s+=` `,s+=" ",s+=" ",s+=` Sheet1!$${yt(d+2)}$1`,s+=' '+Ve(u.name)+"",s+=" ",s+=" ",s+=" ";{let A=t.chartColors[o%t.chartColors.length];A==="transparent"?s+="":t.chartColorsOpacity?s+=""+ft(A,'')+"":s+=""+ft(A)+"",t.lineSize===0?s+="":(s+=`${ft(A)}`,s+=``),s+=Mn(t.shadow,zn)}if(s+=" ",s+="",s+=' ',t.lineDataSymbolSize&&(s+=``),s+="",s+=`${ft(t.chartColors[d+1>t.chartColors.length?Math.floor(Math.random()*t.chartColors.length):d])}`,s+=`${ft(t.lineDataSymbolLineColor||t.chartColors[o%t.chartColors.length])}`,s+="",s+="",s+="",t.showLabel){let A=to("-xxxx-xxxx-xxxx-xxxxxxxxxxxx");u.labels[0]&&(t.dataLabelFormatScatter==="custom"||t.dataLabelFormatScatter==="customXY")&&(s+="",u.labels[0].forEach((f,p)=>{(t.dataLabelFormatScatter==="custom"||t.dataLabelFormatScatter==="customXY")&&(s+=" ",s+=` `,s+=" ",s+=" ",s+=" ",s+=" ",s+=" ",s+=" ",s+=" ",s+=" ",s+=" ",s+=" ",s+=" ",s+=' ',s+=" "+Ve(f)+"",s+=" ",t.dataLabelFormatScatter==="customXY"&&!/^ *$/.test(f)&&(s+=" ",s+=' ',s+=" (",s+=" ",s+=' ',s+=' ',s+=" ",s+=" ",s+=" ",s+=" ["+Ve(u.name)+"",s+=" ",s+=" ",s+=' ',s+=" , ",s+=" ",s+=' ',s+=' ',s+=" ",s+=" ",s+=" ",s+=" ["+Ve(u.name)+"]",s+=" ",s+=" ",s+=' ',s+=" )",s+=" ",s+=' '),s+=" ",s+=" ",s+=" ",s+=" ",s+=" ",s+=" ",s+=" ",s+=" ",s+=" ",s+=" ",t.dataLabelPosition&&(s+=' '),s+=' ',s+=' ',s+=' ',s+=' ',s+=' ',s+=' ',s+=' ',s+=" ",s+=' ',s+=' ',s+=` `,s+=" ",s+=" ",s+="")}),s+=""),t.dataLabelFormatScatter==="XY"&&(s+="",s+=" ",s+=" ",s+=" ",s+=" ",s+=" ",s+=" ",s+=" ",s+=" ",s+=" ",s+=" ",s+=" ",s+=" ",s+=" ",s+=" ",s+=" ",s+=" ",s+=' ',s+=" ",s+=" ",t.dataLabelPosition&&(s+=' '),s+=' ',s+=` `,s+=` `,s+=` `,s+=' ',s+=' ',s+=" ",s+=' ',s+=' ',s+=" ",s+=" ",s+="")}r.length===1&&t.chartColors!==ji&&u.values.forEach((A,f)=>{let p=A<0?t.invertedColors||t.chartColors||ji:t.chartColors||[];s+=" ",s+=` `,s+=' ',s+=' ',s+=" ",t.lineSize===0?s+="":(s+="",s+=' ',s+=""),s+=Mn(t.shadow,zn),s+=" ",s+=" "}),s+="",s+=" ",s+=` Sheet1!$A$2:$A$${r[0].values.length+1}`,s+=" ",s+=" General",s+=` `,r[0].values.forEach((A,f)=>{s+=`${A||A===0?A:""}`}),s+=" ",s+=" ",s+="",s+="",s+=" ",s+=` Sheet1!$${yt(d+2)}$2:$${yt(d+2)}$${r[0].values.length+1}`,s+=" ",s+=" General",s+=` `,r[0].values.forEach((A,f)=>{s+=`${u.values[f]||u.values[f]===0?u.values[f]:""}`}),s+=" ",s+=" ",s+="",s+='',s+=""}),s+=" ",s+=` `,s+=" ",s+=" ",s+=" ",s+=" ",s+=` `,s+=" "+ft(t.dataLabelColor||nr)+"",s+=' ',s+=" ",s+=" ",s+=" ",t.dataLabelPosition&&(s+=' '),s+=' ',s+=' ',s+=' ',s+=' ',s+=' ',s+=' ',s+=" ",s+=``,s+="";break;case Ce.BUBBLE:case Ce.BUBBLE3D:s+="",s+='',o=-1,r.filter((u,d)=>d>0).forEach((u,d)=>{o++,s+="",s+=` `,s+=` `,s+=" ",s+=" ",s+=" Sheet1!$"+yt(l+1)+"$1",s+=' '+Ve(u.name)+"",s+=" ",s+=" ";{s+="";let A=t.chartColors[o%t.chartColors.length];A==="transparent"?s+="":t.chartColorsOpacity?s+=`${ft(A,'')}`:s+=""+ft(A)+"",t.lineSize===0?s+="":t.dataBorder?s+=`${ft(t.dataBorder.color)}`:(s+=`${ft(A)}`,s+=``),s+=Mn(t.shadow,zn),s+=""}s+="",s+=" ",s+=` Sheet1!$A$2:$A$${r[0].values.length+1}`,s+=" ",s+=" General",s+=` `,r[0].values.forEach((A,f)=>{s+=`${A||A===0?A:""}`}),s+=" ",s+=" ",s+="",s+="",s+=" ",s+=`Sheet1!$${yt(l+1)}$2:$${yt(l+1)}$${r[0].values.length+1}`,l++,s+=" ",s+=" General",s+=` `,r[0].values.forEach((A,f)=>{s+=`${u.values[f]||u.values[f]===0?u.values[f]:""}`}),s+=" ",s+=" ",s+="",s+=" ",s+=" ",s+=`Sheet1!$${yt(l+1)}$2:$${yt(l+1)}$${u.sizes.length+1}`,l++,s+=" ",s+=" General",s+=` `,u.sizes.forEach((A,f)=>{s+=`${A||""}`}),s+=" ",s+=" ",s+=" ",s+=' ',s+=""}),s+="",s+=``,s+="",s+=``,s+=`${ft(t.dataLabelColor||nr)}`,s+=``,s+="",t.dataLabelPosition&&(s+=``),s+='',s+=``,s+=``,s+="",s+=' ',s+=' ',s+=" ",s+="",s+="",s+=``,s+="";break;case Ce.DOUGHNUT:case Ce.PIE:c=r[0],s+="",s+=' ',s+="",s+=' ',s+=' ',s+=" ",s+=" ",s+=" Sheet1!$B$1",s+=" ",s+=' ',s+=' '+Ve(c.name)+"",s+=" ",s+=" ",s+=" ",s+=" ",s+=' ',s+=' ',t.dataNoEffects?s+="":s+=Mn(t.shadow,zn),s+=" ",c.labels[0].forEach((u,d)=>{s+="",s+=` `,s+=' ',s+=" ",s+=`${ft(t.chartColors[d+1>t.chartColors.length?Math.floor(Math.random()*t.chartColors.length):d])}`,t.dataBorder&&(s+=`${ft(t.dataBorder.color)}`),s+=Mn(t.shadow,zn),s+=" ",s+=""}),s+="",c.labels[0].forEach((u,d)=>{s+="",s+=` `,s+=` `,s+=" ",s+=" ",s+=" ",s+=` `,s+=" "+ft(t.dataLabelColor||nr)+"",s+=` `,s+=" ",s+=" ",s+=" ",e===Ce.PIE&&t.dataLabelPosition&&(s+=``),s+=' ',s+=' ',s+=' ',s+=' ',s+=' ',s+=' ',s+=" "}),s+=` `,s+=" ",s+=" ",s+=" ",s+=" ",s+=" ",s+=` `,s+=' ',s+=" ",s+=" ",s+=" ",s+=" ",s+=e===Ce.PIE?'':"",s+=' ',s+=' ',s+=' ',s+=' ',s+=' ',s+=' ',s+=` `,s+="",s+="",s+=" ",s+=` Sheet1!$A$2:$A$${c.labels[0].length+1}`,s+=" ",s+=` `,c.labels[0].forEach((u,d)=>{s+=`${Ve(u)}`}),s+=" ",s+=" ",s+="",s+=" ",s+=" ",s+=` Sheet1!$B$2:$B$${c.labels[0].length+1}`,s+=" ",s+=` `,c.values.forEach((u,d)=>{s+=`${u||u===0?u:""}`}),s+=" ",s+=" ",s+=" ",s+=" ",s+=` `,e===Ce.DOUGHNUT&&(s+=``),s+="";break;default:s+="";break}return s}function Ds(e,r,t){let n="";return e._type===Ce.SCATTER||e._type===Ce.BUBBLE||e._type===Ce.BUBBLE3D?n+="":n+="",n+=' ',n+=" ",n+='',(e.catAxisMaxVal||e.catAxisMaxVal===0)&&(n+=``),(e.catAxisMinVal||e.catAxisMinVal===0)&&(n+=``),n+="",n+=' ',n+=' ',n+=e.catGridLine.style!=="none"?Cl(e.catGridLine):"",e.showCatAxisTitle&&(n+=Mo({color:e.catAxisTitleColor,fontFace:e.catAxisTitleFontFace,fontSize:e.catAxisTitleFontSize,titleRotate:e.catAxisTitleRotate,title:e.catAxisTitle||"Axis Title"})),e._type===Ce.SCATTER||e._type===Ce.BUBBLE||e._type===Ce.BUBBLE3D?n+=' ':n+=' ',e._type===Ce.SCATTER?(n+=' ',n+=' ',n+=' '):(n+=' ',n+=' ',n+=' '),n+=" ",n+=` `,n+=e.catAxisLineShow?""+ft(e.catAxisLineColor||_n.color)+"":"",n+=' ',n+=" ",n+=" ",n+=" ",n+=" ",e.catAxisLabelRotate?n+=``:n+="",n+=" ",n+=" ",n+=" ",n+=` `,n+=" "+ft(e.catAxisLabelColor||nr)+"",n+=' ',n+=" ",n+=" ",n+=' ',n+=" ",n+=" ",n+=' ',n+=` `,n+=' ',n+=' ',n+=` `,e.catAxisLabelFrequency&&(n+=' '),(e.catLabelFormatCode||e._type===Ce.SCATTER||e._type===Ce.BUBBLE||e._type===Ce.BUBBLE3D)&&(e.catLabelFormatCode&&(["catAxisBaseTimeUnit","catAxisMajorTimeUnit","catAxisMinorTimeUnit"].forEach(i=>{e[i]&&(typeof e[i]!="string"||!["days","months","years"].includes(e[i].toLowerCase()))&&(console.warn(`"${i}" must be one of: 'days','months','years' !`),e[i]=null)}),e.catAxisBaseTimeUnit&&(n+=''),e.catAxisMajorTimeUnit&&(n+=''),e.catAxisMinorTimeUnit&&(n+='')),e.catAxisMajorUnit&&(n+=``),e.catAxisMinorUnit&&(n+=``)),e._type===Ce.SCATTER||e._type===Ce.BUBBLE||e._type===Ce.BUBBLE3D?n+="":n+="",n}function Es(e,r){let t=r===qr?e.barDir==="col"?"l":"b":e.barDir!=="col"?"r":"t";r===eo&&(t="r");let n=r===qr?Gi:Xs,i="";return i+="",i+=' ',i+=" ",e.valAxisLogScaleBase&&(i+=``),i+='',(e.valAxisMaxVal||e.valAxisMaxVal===0)&&(i+=``),(e.valAxisMinVal||e.valAxisMinVal===0)&&(i+=``),i+=" ",i+=` `,i+=' ',e.valGridLine.style!=="none"&&(i+=Cl(e.valGridLine)),e.showValAxisTitle&&(i+=Mo({color:e.valAxisTitleColor,fontFace:e.valAxisTitleFontFace,fontSize:e.valAxisTitleFontSize,titleRotate:e.valAxisTitleRotate,title:e.valAxisTitle||"Axis Title"})),i+=``,e._type===Ce.SCATTER?(i+=' ',i+=' ',i+=' '):(i+=' ',i+=' ',i+=' '),i+=" ",i+=` `,i+=e.valAxisLineShow?""+ft(e.valAxisLineColor||_n.color)+"":"",i+=' ',i+=" ",i+=" ",i+=" ",i+=" ",i+=` `,i+=" ",i+=" ",i+=" ",i+=` `,i+=" "+ft(e.valAxisLabelColor||nr)+"",i+=' ',i+=" ",i+=" ",i+=' ',i+=" ",i+=" ",i+=' ',typeof e.catAxisCrossesAt=="number"?i+=` `:typeof e.catAxisCrossesAt=="string"?i+=' ':i+=' ',i+=' ',e.valAxisMajorUnit&&(i+=` `),e.valAxisDisplayUnit&&(i+=`${e.valAxisDisplayUnitLabel?"":""}`),i+="",i}function p0(e,r,t){let n="";return n+="",n+=' ',n+=' ',n+=' ',n+=' ',n+=e.serGridLine.style!=="none"?Cl(e.serGridLine):"",e.showSerAxisTitle&&(n+=Mo({color:e.serAxisTitleColor,fontFace:e.serAxisTitleFontFace,fontSize:e.serAxisTitleFontSize,titleRotate:e.serAxisTitleRotate,title:e.serAxisTitle||"Axis Title"})),n+=` `,n+=' ',n+=' ',n+=` `,n+=" ",n+=' ',n+=e.serAxisLineShow?`${ft(e.serAxisLineColor||_n.color)}`:"",n+=' ',n+=" ",n+=" ",n+=" ",n+=" ",n+=" ",n+=" ",n+=" ",n+=" ",n+=` `,n+=` ${ft(e.serAxisLabelColor||nr)}`,n+=` `,n+=" ",n+=" ",n+=' ',n+=" ",n+=" ",n+=' ',n+=' ',e.serAxisLabelFrequency&&(n+=' '),e.serLabelFormatCode&&(["serAxisBaseTimeUnit","serAxisMajorTimeUnit","serAxisMinorTimeUnit"].forEach(i=>{e[i]&&(typeof e[i]!="string"||!["days","months","years"].includes(i.toLowerCase()))&&(console.warn(`"${i}" must be one of: 'days','months','years' !`),e[i]=null)}),e.serAxisBaseTimeUnit&&(n+=` `),e.serAxisMajorTimeUnit&&(n+=` `),e.serAxisMinorTimeUnit&&(n+=` `),e.serAxisMajorUnit&&(n+=` `),e.serAxisMinorUnit&&(n+=` `)),n+="",n}function Mo(e,r,t){let n=e.titleAlign==="left"||e.titleAlign==="right"?``:"",i=e.titleRotate?``:"",a=e.fontSize?`sz="${Math.round(e.fontSize*100)}"`:"",o=e.titleBold?1:0,l="";if(e.titlePos&&typeof e.titlePos.x=="number"&&typeof e.titlePos.y=="number"){let c=e.titlePos.x+r,s=e.titlePos.y+t,u=c===0?0:c*(c/5)/10;u>=1&&(u=u/10),u>=.1&&(u=u/10);let d=s===0?0:s*(s/5)/10;d>=1&&(d=d/10),d>=.1&&(d=d/10),l=``}return` +`,l.file("xl/worksheets/sheet1.xml",u)}l.generateAsync({type:"base64"}).then(u=>{r.file(`ppt/embeddings/Microsoft_Excel_Worksheet${e.globalId}.xlsx`,u,{base64:!0}),r.file("ppt/charts/_rels/"+e.fileName+".rels",``),r.file(`ppt/charts/${e.fileName}`,v0(e)),n("")}).catch(u=>{i(u)})})})}function v0(e){var r,t,n,i;let a='',o=!1;if(a+='',a+='',a+=``,a+="",e.opts.showTitle?(a+=_o({title:e.opts.title||"Chart Title",color:e.opts.titleColor,fontFace:e.opts.titleFontFace,fontSize:e.opts.titleFontSize||n0,titleAlign:e.opts.titleAlign,titleBold:e.opts.titleBold,titlePos:e.opts.titlePos,titleRotate:e.opts.titleRotate},e.opts.x,e.opts.y),a+=''):a+='',e.opts._type===Ce.BAR3D&&(a+=``),a+="",e.opts.layout?(a+="",a+=" ",a+=' ',a+=' ',a+=' ',a+=' ',a+=' ',a+=' ',a+=' ',a+=" ",a+=""):a+="",Array.isArray(e.opts._type)?e.opts._type.forEach(l=>{let c=Object.assign(Object.assign({},e.opts),l.options),s=c.secondaryValAxis?ro:qr,u=c.secondaryCatAxis?Hs:ji;o=o||c.secondaryValAxis,a+=xu(l.type,l.data,c,s,u)}):a+=xu(e.opts._type,e.data,e.opts,qr,ji),e.opts._type!==Ce.PIE&&e.opts._type!==Ce.DOUGHNUT){if(e.opts.valAxes&&e.opts.valAxes.length>1&&!o)throw new Error("Secondary axis must be used by one of the multiple charts");if(e.opts.catAxes){if(!e.opts.valAxes||e.opts.valAxes.length!==e.opts.catAxes.length)throw new Error("There must be the same number of value and category axes.");a+=Es(Object.assign(Object.assign({},e.opts),e.opts.catAxes[0]),ji,qr)}else a+=Es(e.opts,ji,qr);e.opts.valAxes?(a+=Bs(Object.assign(Object.assign({},e.opts),e.opts.valAxes[0]),qr),e.opts.valAxes[1]&&(a+=Bs(Object.assign(Object.assign({},e.opts),e.opts.valAxes[1]),ro))):(a+=Bs(e.opts,qr),e.opts._type===Ce.BAR3D&&(a+=y0(e.opts,Gd,qr))),!((r=e.opts)===null||r===void 0)&&r.catAxes&&(!((t=e.opts)===null||t===void 0)&&t.catAxes[1])&&(a+=Es(Object.assign(Object.assign({},e.opts),e.opts.catAxes[1]),Hs,ro))}return e.opts.showDataTable&&(a+="",a+=` `,a+=` `,a+=` `,a+=` `,a+=" ",a+=" ",a+=' ',a+=" ",a+=" ",a+=" ",a+=' ',a+=" ",a+=" ",a+=' ',a+=` `,a+=' ',a+=' ',a+=' ',a+=' ',a+=" ",a+=" ",a+=' ',a+=" ",a+=" ",a+=""),a+=" ",a+=!((n=e.opts.plotArea.fill)===null||n===void 0)&&n.color?lr(e.opts.plotArea.fill):"",a+=e.opts.plotArea.border?`${lr(e.opts.plotArea.border.color)}`:"",a+=" ",a+=" ",a+="",e.opts.showLegend&&(a+="",a+='',a+='',(e.opts.legendFontFace||e.opts.legendFontSize||e.opts.legendColor)&&(a+="",a+=" ",a+=" ",a+=" ",a+=" ",a+=e.opts.legendFontSize?``:"",e.opts.legendColor&&(a+=lr(e.opts.legendColor)),e.opts.legendFontFace&&(a+=''),e.opts.legendFontFace&&(a+=''),a+=" ",a+=" ",a+=' ',a+=" ",a+=""),a+=""),a+=' ',a+=' ',e.opts._type===Ce.SCATTER&&(a+=''),a+="",a+="",a+=!((i=e.opts.chartArea.fill)===null||i===void 0)&&i.color?lr(e.opts.chartArea.fill):"",a+=e.opts.chartArea.border?`${lr(e.opts.chartArea.border.color)}`:"",a+=" ",a+="",a+='',a+="",a}function xu(e,r,t,n,i,a){let o=-1,l=1,c=null,s="";switch(e){case Ce.AREA:case Ce.BAR:case Ce.BAR3D:case Ce.LINE:case Ce.RADAR:s+=``,e===Ce.AREA&&t.barGrouping==="stacked"&&(s+=''),(e===Ce.BAR||e===Ce.BAR3D)&&(s+='',s+=''),e===Ce.RADAR&&(s+=''),s+='',r.forEach(u=>{var d;o++,s+="",s+=` `,s+=" ",s+=" ",s+=" Sheet1!$"+yt(u._dataIndex+u.labels.length+1)+"$1",s+=' '+Ve(u.name)+"",s+=" ",s+=" ";let A=t.chartColors?t.chartColors[o%t.chartColors.length]:null;s+=" ",A==="transparent"?s+="":t.chartColorsOpacity?s+=""+ft(A,``)+"":s+=""+ft(A)+"",e===Ce.LINE||e===Ce.RADAR?t.lineSize===0?s+="":(s+=`${ft(A)}`,s+=''):t.dataBorder&&(s+=`${ft(t.dataBorder.color)}`),s+=Mn(t.shadow,zn),s+=" ",s+=' ',e!==Ce.RADAR&&(s+="",s+=``,t.dataLabelBkgrdColors&&(s+=`${ft(A)}`),s+="",s+=``,s+=`${ft(t.dataLabelColor||nr)}`,s+=``,s+="",t.dataLabelPosition&&(s+=``),s+='',s+=``,s+=``,s+=``,s+=""),(e===Ce.LINE||e===Ce.RADAR)&&(s+="",s+=' ',t.lineDataSymbolSize&&(s+=``),s+=" ",s+=` ${ft(t.chartColors[u._dataIndex+1>t.chartColors.length?Math.floor(Math.random()*t.chartColors.length):u._dataIndex])}`,s+=` ${ft(t.lineDataSymbolLineColor||A)}`,s+=" ",s+=" ",s+=""),(e===Ce.BAR||e===Ce.BAR3D)&&r.length===1&&(t.chartColors&&t.chartColors!==qi&&t.chartColors.length>1||!((d=t.invertedColors)===null||d===void 0)&&d.length)&&u.values.forEach((f,p)=>{let g=f<0?t.invertedColors||t.chartColors||qi:t.chartColors||[];s+=" ",s+=` `,s+=' ',s+=' ',s+=" ",t.lineSize===0?s+="":e===Ce.BAR?(s+="",s+=' ',s+=""):(s+="",s+=" ",s+=' ',s+=" ",s+=""),s+=Mn(t.shadow,zn),s+=" ",s+=" "}),s+="",t.catLabelFormatCode?(s+=" ",s+=` Sheet1!$A$2:$A$${u.labels[0].length+1}`,s+=" ",s+=" "+(t.catLabelFormatCode||"General")+"",s+=` `,u.labels[0].forEach((f,p)=>s+=`${Ve(f)}`),s+=" ",s+=" "):(s+=" ",s+=` Sheet1!$A$2:$${yt(u.labels.length)}$${u.labels[0].length+1}`,s+=" ",s+=` `,u.labels.forEach(f=>{s+="",f.forEach((p,g)=>s+=`${Ve(p)}`),s+=""}),s+=" ",s+=" "),s+="",s+="",s+=" ",s+=`Sheet1!$${yt(u._dataIndex+u.labels.length+1)}$2:$${yt(u._dataIndex+u.labels.length+1)}$${u.labels[0].length+1}`,s+=" ",s+=" "+(t.valLabelFormatCode||t.dataTableFormatCode||"General")+"",s+=` `,u.values.forEach((f,p)=>s+=`${f||f===0?f:""}`),s+=" ",s+=" ",s+="",e===Ce.LINE&&(s+=''),s+=""}),s+=" ",s+=` `,s+=" ",s+=" ",s+=" ",s+=" ",s+=` `,s+=" "+ft(t.dataLabelColor||nr)+"",s+=' ',s+=" ",s+=" ",s+=" ",t.dataLabelPosition&&(s+=' '),s+=' ',s+=' ',s+=' ',s+=' ',s+=' ',s+=' ',s+=` `,s+=" ",e===Ce.BAR?(s+=` `,s+=` `):e===Ce.BAR3D?(s+=` `,s+=` `,s+=' '):e===Ce.LINE&&(s+=' '),s+=``,s+=``;break;case Ce.SCATTER:s+="",s+='',s+='',o=-1,r.filter((u,d)=>d>0).forEach((u,d)=>{o++,s+="",s+=` `,s+=` `,s+=" ",s+=" ",s+=` Sheet1!$${yt(d+2)}$1`,s+=' '+Ve(u.name)+"",s+=" ",s+=" ",s+=" ";{let A=t.chartColors[o%t.chartColors.length];A==="transparent"?s+="":t.chartColorsOpacity?s+=""+ft(A,'')+"":s+=""+ft(A)+"",t.lineSize===0?s+="":(s+=`${ft(A)}`,s+=``),s+=Mn(t.shadow,zn)}if(s+=" ",s+="",s+=' ',t.lineDataSymbolSize&&(s+=``),s+="",s+=`${ft(t.chartColors[d+1>t.chartColors.length?Math.floor(Math.random()*t.chartColors.length):d])}`,s+=`${ft(t.lineDataSymbolLineColor||t.chartColors[o%t.chartColors.length])}`,s+="",s+="",s+="",t.showLabel){let A=no("-xxxx-xxxx-xxxx-xxxxxxxxxxxx");u.labels[0]&&(t.dataLabelFormatScatter==="custom"||t.dataLabelFormatScatter==="customXY")&&(s+="",u.labels[0].forEach((f,p)=>{(t.dataLabelFormatScatter==="custom"||t.dataLabelFormatScatter==="customXY")&&(s+=" ",s+=` `,s+=" ",s+=" ",s+=" ",s+=" ",s+=" ",s+=" ",s+=" ",s+=" ",s+=" ",s+=" ",s+=" ",s+=' ',s+=" "+Ve(f)+"",s+=" ",t.dataLabelFormatScatter==="customXY"&&!/^ *$/.test(f)&&(s+=" ",s+=' ',s+=" (",s+=" ",s+=' ',s+=' ',s+=" ",s+=" ",s+=" ",s+=" ["+Ve(u.name)+"",s+=" ",s+=" ",s+=' ',s+=" , ",s+=" ",s+=' ',s+=' ',s+=" ",s+=" ",s+=" ",s+=" ["+Ve(u.name)+"]",s+=" ",s+=" ",s+=' ',s+=" )",s+=" ",s+=' '),s+=" ",s+=" ",s+=" ",s+=" ",s+=" ",s+=" ",s+=" ",s+=" ",s+=" ",s+=" ",t.dataLabelPosition&&(s+=' '),s+=' ',s+=' ',s+=' ',s+=' ',s+=' ',s+=' ',s+=' ',s+=" ",s+=' ',s+=' ',s+=` `,s+=" ",s+=" ",s+="")}),s+=""),t.dataLabelFormatScatter==="XY"&&(s+="",s+=" ",s+=" ",s+=" ",s+=" ",s+=" ",s+=" ",s+=" ",s+=" ",s+=" ",s+=" ",s+=" ",s+=" ",s+=" ",s+=" ",s+=" ",s+=" ",s+=' ',s+=" ",s+=" ",t.dataLabelPosition&&(s+=' '),s+=' ',s+=` `,s+=` `,s+=` `,s+=' ',s+=' ',s+=" ",s+=' ',s+=' ',s+=" ",s+=" ",s+="")}r.length===1&&t.chartColors!==qi&&u.values.forEach((A,f)=>{let p=A<0?t.invertedColors||t.chartColors||qi:t.chartColors||[];s+=" ",s+=` `,s+=' ',s+=' ',s+=" ",t.lineSize===0?s+="":(s+="",s+=' ',s+=""),s+=Mn(t.shadow,zn),s+=" ",s+=" "}),s+="",s+=" ",s+=` Sheet1!$A$2:$A$${r[0].values.length+1}`,s+=" ",s+=" General",s+=` `,r[0].values.forEach((A,f)=>{s+=`${A||A===0?A:""}`}),s+=" ",s+=" ",s+="",s+="",s+=" ",s+=` Sheet1!$${yt(d+2)}$2:$${yt(d+2)}$${r[0].values.length+1}`,s+=" ",s+=" General",s+=` `,r[0].values.forEach((A,f)=>{s+=`${u.values[f]||u.values[f]===0?u.values[f]:""}`}),s+=" ",s+=" ",s+="",s+='',s+=""}),s+=" ",s+=` `,s+=" ",s+=" ",s+=" ",s+=" ",s+=` `,s+=" "+ft(t.dataLabelColor||nr)+"",s+=' ',s+=" ",s+=" ",s+=" ",t.dataLabelPosition&&(s+=' '),s+=' ',s+=' ',s+=' ',s+=' ',s+=' ',s+=' ',s+=" ",s+=``,s+="";break;case Ce.BUBBLE:case Ce.BUBBLE3D:s+="",s+='',o=-1,r.filter((u,d)=>d>0).forEach((u,d)=>{o++,s+="",s+=` `,s+=` `,s+=" ",s+=" ",s+=" Sheet1!$"+yt(l+1)+"$1",s+=' '+Ve(u.name)+"",s+=" ",s+=" ";{s+="";let A=t.chartColors[o%t.chartColors.length];A==="transparent"?s+="":t.chartColorsOpacity?s+=`${ft(A,'')}`:s+=""+ft(A)+"",t.lineSize===0?s+="":t.dataBorder?s+=`${ft(t.dataBorder.color)}`:(s+=`${ft(A)}`,s+=``),s+=Mn(t.shadow,zn),s+=""}s+="",s+=" ",s+=` Sheet1!$A$2:$A$${r[0].values.length+1}`,s+=" ",s+=" General",s+=` `,r[0].values.forEach((A,f)=>{s+=`${A||A===0?A:""}`}),s+=" ",s+=" ",s+="",s+="",s+=" ",s+=`Sheet1!$${yt(l+1)}$2:$${yt(l+1)}$${r[0].values.length+1}`,l++,s+=" ",s+=" General",s+=` `,r[0].values.forEach((A,f)=>{s+=`${u.values[f]||u.values[f]===0?u.values[f]:""}`}),s+=" ",s+=" ",s+="",s+=" ",s+=" ",s+=`Sheet1!$${yt(l+1)}$2:$${yt(l+1)}$${u.sizes.length+1}`,l++,s+=" ",s+=" General",s+=` `,u.sizes.forEach((A,f)=>{s+=`${A||""}`}),s+=" ",s+=" ",s+=" ",s+=' ',s+=""}),s+="",s+=``,s+="",s+=``,s+=`${ft(t.dataLabelColor||nr)}`,s+=``,s+="",t.dataLabelPosition&&(s+=``),s+='',s+=``,s+=``,s+="",s+=' ',s+=' ',s+=" ",s+="",s+="",s+=``,s+="";break;case Ce.DOUGHNUT:case Ce.PIE:c=r[0],s+="",s+=' ',s+="",s+=' ',s+=' ',s+=" ",s+=" ",s+=" Sheet1!$B$1",s+=" ",s+=' ',s+=' '+Ve(c.name)+"",s+=" ",s+=" ",s+=" ",s+=" ",s+=' ',s+=' ',t.dataNoEffects?s+="":s+=Mn(t.shadow,zn),s+=" ",c.labels[0].forEach((u,d)=>{s+="",s+=` `,s+=' ',s+=" ",s+=`${ft(t.chartColors[d+1>t.chartColors.length?Math.floor(Math.random()*t.chartColors.length):d])}`,t.dataBorder&&(s+=`${ft(t.dataBorder.color)}`),s+=Mn(t.shadow,zn),s+=" ",s+=""}),s+="",c.labels[0].forEach((u,d)=>{s+="",s+=` `,s+=` `,s+=" ",s+=" ",s+=" ",s+=` `,s+=" "+ft(t.dataLabelColor||nr)+"",s+=` `,s+=" ",s+=" ",s+=" ",e===Ce.PIE&&t.dataLabelPosition&&(s+=``),s+=' ',s+=' ',s+=' ',s+=' ',s+=' ',s+=' ',s+=" "}),s+=` `,s+=" ",s+=" ",s+=" ",s+=" ",s+=" ",s+=` `,s+=' ',s+=" ",s+=" ",s+=" ",s+=" ",s+=e===Ce.PIE?'':"",s+=' ',s+=' ',s+=' ',s+=' ',s+=' ',s+=' ',s+=` `,s+="",s+="",s+=" ",s+=` Sheet1!$A$2:$A$${c.labels[0].length+1}`,s+=" ",s+=` `,c.labels[0].forEach((u,d)=>{s+=`${Ve(u)}`}),s+=" ",s+=" ",s+="",s+=" ",s+=" ",s+=` Sheet1!$B$2:$B$${c.labels[0].length+1}`,s+=" ",s+=` `,c.values.forEach((u,d)=>{s+=`${u||u===0?u:""}`}),s+=" ",s+=" ",s+=" ",s+=" ",s+=` `,e===Ce.DOUGHNUT&&(s+=``),s+="";break;default:s+="";break}return s}function Es(e,r,t){let n="";return e._type===Ce.SCATTER||e._type===Ce.BUBBLE||e._type===Ce.BUBBLE3D?n+="":n+="",n+=' ',n+=" ",n+='',(e.catAxisMaxVal||e.catAxisMaxVal===0)&&(n+=``),(e.catAxisMinVal||e.catAxisMinVal===0)&&(n+=``),n+="",n+=' ',n+=' ',n+=e.catGridLine.style!=="none"?Pl(e.catGridLine):"",e.showCatAxisTitle&&(n+=_o({color:e.catAxisTitleColor,fontFace:e.catAxisTitleFontFace,fontSize:e.catAxisTitleFontSize,titleRotate:e.catAxisTitleRotate,title:e.catAxisTitle||"Axis Title"})),e._type===Ce.SCATTER||e._type===Ce.BUBBLE||e._type===Ce.BUBBLE3D?n+=' ':n+=' ',e._type===Ce.SCATTER?(n+=' ',n+=' ',n+=' '):(n+=' ',n+=' ',n+=' '),n+=" ",n+=` `,n+=e.catAxisLineShow?""+ft(e.catAxisLineColor||_n.color)+"":"",n+=' ',n+=" ",n+=" ",n+=" ",n+=" ",e.catAxisLabelRotate?n+=``:n+="",n+=" ",n+=" ",n+=" ",n+=` `,n+=" "+ft(e.catAxisLabelColor||nr)+"",n+=' ',n+=" ",n+=" ",n+=' ',n+=" ",n+=" ",n+=' ',n+=` `,n+=' ',n+=' ',n+=` `,e.catAxisLabelFrequency&&(n+=' '),(e.catLabelFormatCode||e._type===Ce.SCATTER||e._type===Ce.BUBBLE||e._type===Ce.BUBBLE3D)&&(e.catLabelFormatCode&&(["catAxisBaseTimeUnit","catAxisMajorTimeUnit","catAxisMinorTimeUnit"].forEach(i=>{e[i]&&(typeof e[i]!="string"||!["days","months","years"].includes(e[i].toLowerCase()))&&(console.warn(`"${i}" must be one of: 'days','months','years' !`),e[i]=null)}),e.catAxisBaseTimeUnit&&(n+=''),e.catAxisMajorTimeUnit&&(n+=''),e.catAxisMinorTimeUnit&&(n+='')),e.catAxisMajorUnit&&(n+=``),e.catAxisMinorUnit&&(n+=``)),e._type===Ce.SCATTER||e._type===Ce.BUBBLE||e._type===Ce.BUBBLE3D?n+="":n+="",n}function Bs(e,r){let t=r===qr?e.barDir==="col"?"l":"b":e.barDir!=="col"?"r":"t";r===ro&&(t="r");let n=r===qr?ji:Hs,i="";return i+="",i+=' ',i+=" ",e.valAxisLogScaleBase&&(i+=``),i+='',(e.valAxisMaxVal||e.valAxisMaxVal===0)&&(i+=``),(e.valAxisMinVal||e.valAxisMinVal===0)&&(i+=``),i+=" ",i+=` `,i+=' ',e.valGridLine.style!=="none"&&(i+=Pl(e.valGridLine)),e.showValAxisTitle&&(i+=_o({color:e.valAxisTitleColor,fontFace:e.valAxisTitleFontFace,fontSize:e.valAxisTitleFontSize,titleRotate:e.valAxisTitleRotate,title:e.valAxisTitle||"Axis Title"})),i+=``,e._type===Ce.SCATTER?(i+=' ',i+=' ',i+=' '):(i+=' ',i+=' ',i+=' '),i+=" ",i+=` `,i+=e.valAxisLineShow?""+ft(e.valAxisLineColor||_n.color)+"":"",i+=' ',i+=" ",i+=" ",i+=" ",i+=" ",i+=` `,i+=" ",i+=" ",i+=" ",i+=` `,i+=" "+ft(e.valAxisLabelColor||nr)+"",i+=' ',i+=" ",i+=" ",i+=' ',i+=" ",i+=" ",i+=' ',typeof e.catAxisCrossesAt=="number"?i+=` `:typeof e.catAxisCrossesAt=="string"?i+=' ':i+=' ',i+=' ',e.valAxisMajorUnit&&(i+=` `),e.valAxisDisplayUnit&&(i+=`${e.valAxisDisplayUnitLabel?"":""}`),i+="",i}function y0(e,r,t){let n="";return n+="",n+=' ',n+=' ',n+=' ',n+=' ',n+=e.serGridLine.style!=="none"?Pl(e.serGridLine):"",e.showSerAxisTitle&&(n+=_o({color:e.serAxisTitleColor,fontFace:e.serAxisTitleFontFace,fontSize:e.serAxisTitleFontSize,titleRotate:e.serAxisTitleRotate,title:e.serAxisTitle||"Axis Title"})),n+=` `,n+=' ',n+=' ',n+=` `,n+=" ",n+=' ',n+=e.serAxisLineShow?`${ft(e.serAxisLineColor||_n.color)}`:"",n+=' ',n+=" ",n+=" ",n+=" ",n+=" ",n+=" ",n+=" ",n+=" ",n+=" ",n+=` `,n+=` ${ft(e.serAxisLabelColor||nr)}`,n+=` `,n+=" ",n+=" ",n+=' ',n+=" ",n+=" ",n+=' ',n+=' ',e.serAxisLabelFrequency&&(n+=' '),e.serLabelFormatCode&&(["serAxisBaseTimeUnit","serAxisMajorTimeUnit","serAxisMinorTimeUnit"].forEach(i=>{e[i]&&(typeof e[i]!="string"||!["days","months","years"].includes(i.toLowerCase()))&&(console.warn(`"${i}" must be one of: 'days','months','years' !`),e[i]=null)}),e.serAxisBaseTimeUnit&&(n+=` `),e.serAxisMajorTimeUnit&&(n+=` `),e.serAxisMinorTimeUnit&&(n+=` `),e.serAxisMajorUnit&&(n+=` `),e.serAxisMinorUnit&&(n+=` `)),n+="",n}function _o(e,r,t){let n=e.titleAlign==="left"||e.titleAlign==="right"?``:"",i=e.titleRotate?``:"",a=e.fontSize?`sz="${Math.round(e.fontSize*100)}"`:"",o=e.titleBold?1:0,l="";if(e.titlePos&&typeof e.titlePos.x=="number"&&typeof e.titlePos.y=="number"){let c=e.titlePos.x+r,s=e.titlePos.y+t,u=c===0?0:c*(c/5)/10;u>=1&&(u=u/10),u>=.1&&(u=u/10);let d=s===0?0:s*(s/5)/10;d>=1&&(d=d/10),d>=.1&&(d=d/10),l=``}return` ${i} @@ -196,23 +236,23 @@ end`},au=function(){for(var e=[],r=0;r ${l} - `}function yt(e){let r="",t=e-1;return t<=25?r=Ii[t]:r=`${Ii[Math.floor(t/Ii.length-1)]}${Ii[t%Ii.length]}`,r}function Mn(e,r){if(e){if(typeof e!="object")return console.warn("`shadow` options must be an object. Ex: `{shadow: {type:'none'}}`"),""}else return"";let t="",n=Object.assign(Object.assign({},r),e),i=n.type||"outer",a=je(n.blur),o=je(n.offset),l=Math.round(n.angle*6e4),c=n.color,s=Math.round(n.opacity*1e5),u=n.rotateWithShape?1:0;return t+=``,t+=``,t+=``,t+=``,t+="",t}function Cl(e){let r="";return r+=" ",r+=` `,r+=' ',r+=' ',r+=" ",r+=" ",r+="",r}function ro(e){if(!e||e==="flat")return"flat";if(e==="square")return"sq";if(e==="round")return"rnd";{let r=e;throw new Error(`Invalid chart line cap: ${r}`)}}function Bs(e){var r,t;let n=typeof process<"u"&&!!(!((r=process.versions)===null||r===void 0)&&r.node)&&((t=process.release)===null||t===void 0?void 0:t.name)==="node",i,a,o=n?()=>sr(this,void 0,void 0,function*(){({default:i}=yield Promise.resolve().then(()=>Qr(Mu()))),{default:a}=yield Promise.resolve().then(()=>Qr(up()))}):()=>sr(this,void 0,void 0,function*(){});n&&o();let l=[],c=e._relsMedia.filter(u=>u.type!=="online"&&!u.data&&(!u.path||u.path&&!u.path.includes("preencoded"))),s=[];return c.forEach(u=>{s.includes(u.path)?u.isDuplicate=!0:(u.isDuplicate=!1,s.push(u.path))}),c.filter(u=>!u.isDuplicate).forEach(u=>{l.push(sr(this,void 0,void 0,function*(){if(a||(yield o()),n&&i&&u.path.indexOf("http")!==0)try{let d=i.readFileSync(u.path);return u.data=Buffer.from(d).toString("base64"),c.filter(A=>A.isDuplicate&&A.path===u.path).forEach(A=>A.data=u.data),"done"}catch(d){throw u.data=ci,c.filter(A=>A.isDuplicate&&A.path===u.path).forEach(A=>A.data=u.data),new Error(`ERROR: Unable to read media: "${u.path}" -${String(d)}`)}return n&&a&&u.path.startsWith("http")?yield new Promise((d,A)=>{a.get(u.path,f=>{let p="";f.setEncoding("binary"),f.on("data",g=>p+=g),f.on("end",()=>{u.data=Buffer.from(p,"binary").toString("base64"),c.filter(g=>g.isDuplicate&&g.path===u.path).forEach(g=>g.data=u.data),d("done")}),f.on("error",()=>{u.data=ci,c.filter(g=>g.isDuplicate&&g.path===u.path).forEach(g=>g.data=u.data),A(new Error(`ERROR! Unable to load image (https.get): ${u.path}`))})})}):yield new Promise((d,A)=>{let f=new XMLHttpRequest;f.onload=()=>{let p=new FileReader;p.onloadend=()=>{u.data=p.result,c.filter(g=>g.isDuplicate&&g.path===u.path).forEach(g=>g.data=u.data),u.isSvgPng?Su(u).then(()=>d("done")).catch(A):d("done")},p.readAsDataURL(f.response)},f.onerror=()=>{u.data=ci,c.filter(p=>p.isDuplicate&&p.path===u.path).forEach(p=>p.data=u.data),A(new Error(`ERROR! Unable to load image (xhr.onerror): ${u.path}`))},f.open("GET",u.path),f.responseType="blob",f.send()})}))}),e._relsMedia.filter(u=>u.isSvgPng&&u.data).forEach(u=>{sr(this,void 0,void 0,function*(){n&&!i&&(yield o()),n&&i?(u.data=ci,l.push(Promise.resolve("done"))):l.push(Su(u))})}),l}function Su(e){return sr(this,void 0,void 0,function*(){return yield new Promise((r,t)=>{let n=new Image;n.onload=()=>{n.width+n.height===0&&n.onerror("h/w=0");let i=document.createElement("CANVAS"),a=i.getContext("2d");i.width=n.width,i.height=n.height,a.drawImage(n,0,0);try{e.data=i.toDataURL(e.type),r("done")}catch(o){n.onerror(o.toString())}i=null},n.onerror=()=>{e.data=ci,t(new Error(`ERROR! Unable to load image (image.onerror): ${e.path}`))},n.src=typeof e.data=="string"?e.data:ci})})}var A0={cover:function(e,r){let t=e.h/e.w,i=r.h/r.w>t,a=i?r.h/t:r.w,o=i?r.h:r.w*t,l=Math.round(1e5*.5*(1-r.w/a)),c=Math.round(1e5*.5*(1-r.h/o));return``},contain:function(e,r){let t=e.h/e.w,i=r.h/r.w>t,a=i?r.w:r.h/t,o=i?r.w*t:r.h,l=Math.round(1e5*.5*(1-r.w/a)),c=Math.round(1e5*.5*(1-r.h/o));return``},crop:function(e,r){let t=r.x,n=e.w-(r.x+r.w),i=r.y,a=e.h-(r.y+r.h),o=Math.round(1e5*(t/e.w)),l=Math.round(1e5*(n/e.w)),c=Math.round(1e5*(i/e.h)),s=Math.round(1e5*(a/e.h));return``}};function Pl(e){var r;let t=e._name?'':"",n=1;return e._bkgdImgRid?t+=``:!((r=e.background)===null||r===void 0)&&r.color?t+=`${lr(e.background)}`:!e.bkgd&&e._name&&e._name===Vs&&(t+=''),t+="",t+='',t+='',t+='',e._slideObjects.forEach((i,a)=>{var o,l,c,s,u,d,A,f;let p=0,g=0,y=nt("75%","X",e._presLayout),v=0,b,x="",P=null,F=null,R=0,E=0,B=null,T=null,W=(o=i.options)===null||o===void 0?void 0:o.sizing,H=(l=i.options)===null||l===void 0?void 0:l.rounding;e._slideLayout!==void 0&&e._slideLayout._slideObjects!==void 0&&i.options&&i.options.placeholder&&(b=e._slideLayout._slideObjects.filter(m=>m.options.placeholder===i.options.placeholder)[0]),i.options=i.options||{},typeof i.options.x<"u"&&(p=nt(i.options.x,"X",e._presLayout)),typeof i.options.y<"u"&&(g=nt(i.options.y,"Y",e._presLayout)),typeof i.options.w<"u"&&(y=nt(i.options.w,"X",e._presLayout)),typeof i.options.h<"u"&&(v=nt(i.options.h,"Y",e._presLayout));let C=y,I=v;switch(b&&((b.options.x||b.options.x===0)&&(p=nt(b.options.x,"X",e._presLayout)),(b.options.y||b.options.y===0)&&(g=nt(b.options.y,"Y",e._presLayout)),(b.options.w||b.options.w===0)&&(y=nt(b.options.w,"X",e._presLayout)),(b.options.h||b.options.h===0)&&(v=nt(b.options.h,"Y",e._presLayout))),i.options.flipH&&(x+=' flipH="1"'),i.options.flipV&&(x+=' flipV="1"'),i.options.rotate&&(x+=` rot="${Un(i.options.rotate)}"`),i._type){case He.table:if(P=i.arrTabRows,F=i.options,R=0,E=0,P[0].forEach(m=>{B=m.options||null,R+=B?.colspan?Number(B.colspan):1}),T=``,T+=' ',T+=``,T+='',Array.isArray(F.colW)){T+="";for(let m=0;m`}T+=""}else{E=F.colW?F.colW:Ke,i.options.w&&!F.colW&&(E=Math.round((typeof i.options.w=="number"?i.options.w:1)/R)),T+="";for(let m=0;m`;T+=""}P.forEach(m=>{var N,ne;for(let q=0;q1){let j=new Array(Z-1).fill(void 0).map(()=>({_type:He.tablecell,options:{rowspan:ue},_hmerge:!0}));m.splice(q+1,0,...j),q+=Z}else q+=1}}),P.forEach((m,N)=>{let ne=P[N+1];ne&&m.forEach((q,se)=>{var Z,ue;let j=q._rowContinue||((Z=q.options)===null||Z===void 0?void 0:Z.rowspan),_=(ue=q.options)===null||ue===void 0?void 0:ue.colspan,fe=q._hmerge;if(j&&j>1){let ce={_type:He.tablecell,options:{colspan:_},_rowContinue:j-1,_vmerge:!0,_hmerge:fe};ne.splice(se,0,ce)}})}),P.forEach((m,N)=>{let ne=0;Array.isArray(F.rowH)&&F.rowH[N]?ne=lt(Number(F.rowH[N])):F.rowH&&!isNaN(Number(F.rowH))?ne=lt(Number(F.rowH)):(i.options.cy||i.options.h)&&(ne=Math.round((i.options.h?lt(i.options.h):typeof i.options.cy=="number"?i.options.cy:1)/P.length)),T+=``,m.forEach(q=>{var se,Z,ue,j,_;let fe=q,ce={rowSpan:((se=fe.options)===null||se===void 0?void 0:se.rowspan)>1?fe.options.rowspan:void 0,gridSpan:((Z=fe.options)===null||Z===void 0?void 0:Z.colspan)>1?fe.options.colspan:void 0,vMerge:fe._vmerge?1:void 0,hMerge:fe._hmerge?1:void 0},ie=Object.keys(ce).map(Be=>[Be,ce[Be]]).filter(([,Be])=>!!Be).map(([Be,h])=>`${String(Be)}="${String(h)}"`).join(" ");if(ie&&(ie=" "+ie),fe._hmerge||fe._vmerge){T+=``;return}let be=fe.options||{};fe.options=be,["align","bold","border","color","fill","fontFace","fontSize","margin","textDirection","underline","valign"].forEach(Be=>{F[Be]&&!be[Be]&&be[Be]!==0&&(be[Be]=F[Be])});let Re=be.valign?` anchor="${be.valign.replace(/^c$/i,"ctr").replace(/^m$/i,"ctr").replace("center","ctr").replace("middle","ctr").replace("top","t").replace("btm","b").replace("bottom","b")}"`:"",ke=be.textDirection&&be.textDirection!=="horz"?` vert="${be.textDirection}"`:"",xe=!((j=(ue=fe._optImp)===null||ue===void 0?void 0:ue.fill)===null||j===void 0)&&j.color?fe._optImp.fill.color:!((_=fe._optImp)===null||_===void 0)&&_.fill&&typeof fe._optImp.fill=="string"?fe._optImp.fill:"";xe=xe||be.fill?be.fill:"";let Oe=xe?lr(xe):"",we=be.margin===0||be.margin?be.margin:Ud;!Array.isArray(we)&&typeof we=="number"&&(we=[we,we,we,we]);let tt="";we[0]>=1?tt=` marL="${je(we[3])}" marR="${je(we[1])}" marT="${je(we[0])}" marB="${je(we[2])}"`:tt=` marL="${lt(we[3])}" marR="${lt(we[1])}" marT="${lt(we[0])}" marB="${lt(we[2])}"`,T+=`${Cu(fe)}`,be.border&&Array.isArray(be.border)&&[{idx:3,name:"lnL"},{idx:1,name:"lnR"},{idx:0,name:"lnT"},{idx:2,name:"lnB"}].forEach(Be=>{be.border[Be.idx].type!=="none"?(T+=``,T+=`${ft(be.border[Be.idx].color)}`,T+=``,T+=``):T+=``}),T+=Oe,T+=" ",T+=" "}),T+=""}),T+=" ",T+=" ",T+=" ",T+="",t+=T,n++;break;case He.text:case He.placeholder:if(!i.options.line&&v===0&&(v=Ke*.3),i.options._bodyProp||(i.options._bodyProp={}),i.options.margin&&Array.isArray(i.options.margin)?(i.options._bodyProp.lIns=je(i.options.margin[0]||0),i.options._bodyProp.rIns=je(i.options.margin[1]||0),i.options._bodyProp.bIns=je(i.options.margin[2]||0),i.options._bodyProp.tIns=je(i.options.margin[3]||0)):typeof i.options.margin=="number"&&(i.options._bodyProp.lIns=je(i.options.margin),i.options._bodyProp.rIns=je(i.options.margin),i.options._bodyProp.bIns=je(i.options.margin),i.options._bodyProp.tIns=je(i.options.margin)),t+="",t+=``,!((c=i.options.hyperlink)===null||c===void 0)&&c.url&&(t+=``),!((s=i.options.hyperlink)===null||s===void 0)&&s.slide&&(t+=``),t+="",t+="':"/>"),t+=`${i._type==="placeholder"?Xa(i):Xa(b)}`,t+="",t+=``,t+=``,t+=``,i.shape==="custGeom")t+="",t+="",t+="",t+="",t+="",t+="",t+='',t+="",t+=``,(d=i.options.points)===null||d===void 0||d.forEach((m,N)=>{if("curve"in m)switch(m.curve.type){case"arc":t+=``;break;case"cubic":t+=` + `}function yt(e){let r="",t=e-1;return t<=25?r=zi[t]:r=`${zi[Math.floor(t/zi.length-1)]}${zi[t%zi.length]}`,r}function Mn(e,r){if(e){if(typeof e!="object")return console.warn("`shadow` options must be an object. Ex: `{shadow: {type:'none'}}`"),""}else return"";let t="",n=Object.assign(Object.assign({},r),e),i=n.type||"outer",a=je(n.blur),o=je(n.offset),l=Math.round(n.angle*6e4),c=n.color,s=Math.round(n.opacity*1e5),u=n.rotateWithShape?1:0;return t+=``,t+=``,t+=``,t+=``,t+="",t}function Pl(e){let r="";return r+=" ",r+=` `,r+=' ',r+=' ',r+=" ",r+=" ",r+="",r}function io(e){if(!e||e==="flat")return"flat";if(e==="square")return"sq";if(e==="round")return"rnd";{let r=e;throw new Error(`Invalid chart line cap: ${r}`)}}function Rs(e){var r,t;let n=typeof process<"u"&&!!(!((r=process.versions)===null||r===void 0)&&r.node)&&((t=process.release)===null||t===void 0?void 0:t.name)==="node",i,a,o=n?()=>sr(this,void 0,void 0,function*(){({default:i}=yield Promise.resolve().then(()=>Qr(Mu()))),{default:a}=yield Promise.resolve().then(()=>Qr(Ap()))}):()=>sr(this,void 0,void 0,function*(){});n&&o();let l=[],c=e._relsMedia.filter(u=>u.type!=="online"&&!u.data&&(!u.path||u.path&&!u.path.includes("preencoded"))),s=[];return c.forEach(u=>{s.includes(u.path)?u.isDuplicate=!0:(u.isDuplicate=!1,s.push(u.path))}),c.filter(u=>!u.isDuplicate).forEach(u=>{l.push(sr(this,void 0,void 0,function*(){if(a||(yield o()),n&&i&&u.path.indexOf("http")!==0)try{let d=i.readFileSync(u.path);return u.data=Buffer.from(d).toString("base64"),c.filter(A=>A.isDuplicate&&A.path===u.path).forEach(A=>A.data=u.data),"done"}catch(d){throw u.data=ci,c.filter(A=>A.isDuplicate&&A.path===u.path).forEach(A=>A.data=u.data),new Error(`ERROR: Unable to read media: "${u.path}" +${String(d)}`)}return n&&a&&u.path.startsWith("http")?yield new Promise((d,A)=>{a.get(u.path,f=>{let p="";f.setEncoding("binary"),f.on("data",g=>p+=g),f.on("end",()=>{u.data=Buffer.from(p,"binary").toString("base64"),c.filter(g=>g.isDuplicate&&g.path===u.path).forEach(g=>g.data=u.data),d("done")}),f.on("error",()=>{u.data=ci,c.filter(g=>g.isDuplicate&&g.path===u.path).forEach(g=>g.data=u.data),A(new Error(`ERROR! Unable to load image (https.get): ${u.path}`))})})}):yield new Promise((d,A)=>{let f=new XMLHttpRequest;f.onload=()=>{let p=new FileReader;p.onloadend=()=>{u.data=p.result,c.filter(g=>g.isDuplicate&&g.path===u.path).forEach(g=>g.data=u.data),u.isSvgPng?Su(u).then(()=>d("done")).catch(A):d("done")},p.readAsDataURL(f.response)},f.onerror=()=>{u.data=ci,c.filter(p=>p.isDuplicate&&p.path===u.path).forEach(p=>p.data=u.data),A(new Error(`ERROR! Unable to load image (xhr.onerror): ${u.path}`))},f.open("GET",u.path),f.responseType="blob",f.send()})}))}),e._relsMedia.filter(u=>u.isSvgPng&&u.data).forEach(u=>{sr(this,void 0,void 0,function*(){n&&!i&&(yield o()),n&&i?(u.data=ci,l.push(Promise.resolve("done"))):l.push(Su(u))})}),l}function Su(e){return sr(this,void 0,void 0,function*(){return yield new Promise((r,t)=>{let n=new Image;n.onload=()=>{n.width+n.height===0&&n.onerror("h/w=0");let i=document.createElement("CANVAS"),a=i.getContext("2d");i.width=n.width,i.height=n.height,a.drawImage(n,0,0);try{e.data=i.toDataURL(e.type),r("done")}catch(o){n.onerror(o.toString())}i=null},n.onerror=()=>{e.data=ci,t(new Error(`ERROR! Unable to load image (image.onerror): ${e.path}`))},n.src=typeof e.data=="string"?e.data:ci})})}var b0={cover:function(e,r){let t=e.h/e.w,i=r.h/r.w>t,a=i?r.h/t:r.w,o=i?r.h:r.w*t,l=Math.round(1e5*.5*(1-r.w/a)),c=Math.round(1e5*.5*(1-r.h/o));return``},contain:function(e,r){let t=e.h/e.w,i=r.h/r.w>t,a=i?r.w:r.h/t,o=i?r.w*t:r.h,l=Math.round(1e5*.5*(1-r.w/a)),c=Math.round(1e5*.5*(1-r.h/o));return``},crop:function(e,r){let t=r.x,n=e.w-(r.x+r.w),i=r.y,a=e.h-(r.y+r.h),o=Math.round(1e5*(t/e.w)),l=Math.round(1e5*(n/e.w)),c=Math.round(1e5*(i/e.h)),s=Math.round(1e5*(a/e.h));return``}};function Fl(e){var r;let t=e._name?'':"",n=1;return e._bkgdImgRid?t+=``:!((r=e.background)===null||r===void 0)&&r.color?t+=`${lr(e.background)}`:!e.bkgd&&e._name&&e._name===Xs&&(t+=''),t+="",t+='',t+='',t+='',e._slideObjects.forEach((i,a)=>{var o,l,c,s,u,d,A,f;let p=0,g=0,y=nt("75%","X",e._presLayout),v=0,b,x="",P=null,F=null,R=0,E=0,B=null,T=null,W=(o=i.options)===null||o===void 0?void 0:o.sizing,H=(l=i.options)===null||l===void 0?void 0:l.rounding;e._slideLayout!==void 0&&e._slideLayout._slideObjects!==void 0&&i.options&&i.options.placeholder&&(b=e._slideLayout._slideObjects.filter(m=>m.options.placeholder===i.options.placeholder)[0]),i.options=i.options||{},typeof i.options.x<"u"&&(p=nt(i.options.x,"X",e._presLayout)),typeof i.options.y<"u"&&(g=nt(i.options.y,"Y",e._presLayout)),typeof i.options.w<"u"&&(y=nt(i.options.w,"X",e._presLayout)),typeof i.options.h<"u"&&(v=nt(i.options.h,"Y",e._presLayout));let C=y,I=v;switch(b&&((b.options.x||b.options.x===0)&&(p=nt(b.options.x,"X",e._presLayout)),(b.options.y||b.options.y===0)&&(g=nt(b.options.y,"Y",e._presLayout)),(b.options.w||b.options.w===0)&&(y=nt(b.options.w,"X",e._presLayout)),(b.options.h||b.options.h===0)&&(v=nt(b.options.h,"Y",e._presLayout))),i.options.flipH&&(x+=' flipH="1"'),i.options.flipV&&(x+=' flipV="1"'),i.options.rotate&&(x+=` rot="${Un(i.options.rotate)}"`),i._type){case He.table:if(P=i.arrTabRows,F=i.options,R=0,E=0,P[0].forEach(m=>{B=m.options||null,R+=B?.colspan?Number(B.colspan):1}),T=``,T+=' ',T+=``,T+='',Array.isArray(F.colW)){T+="";for(let m=0;m`}T+=""}else{E=F.colW?F.colW:Ke,i.options.w&&!F.colW&&(E=Math.round((typeof i.options.w=="number"?i.options.w:1)/R)),T+="";for(let m=0;m`;T+=""}P.forEach(m=>{var N,ne;for(let q=0;q1){let j=new Array(Z-1).fill(void 0).map(()=>({_type:He.tablecell,options:{rowspan:ue},_hmerge:!0}));m.splice(q+1,0,...j),q+=Z}else q+=1}}),P.forEach((m,N)=>{let ne=P[N+1];ne&&m.forEach((q,se)=>{var Z,ue;let j=q._rowContinue||((Z=q.options)===null||Z===void 0?void 0:Z.rowspan),_=(ue=q.options)===null||ue===void 0?void 0:ue.colspan,fe=q._hmerge;if(j&&j>1){let ce={_type:He.tablecell,options:{colspan:_},_rowContinue:j-1,_vmerge:!0,_hmerge:fe};ne.splice(se,0,ce)}})}),P.forEach((m,N)=>{let ne=0;Array.isArray(F.rowH)&&F.rowH[N]?ne=lt(Number(F.rowH[N])):F.rowH&&!isNaN(Number(F.rowH))?ne=lt(Number(F.rowH)):(i.options.cy||i.options.h)&&(ne=Math.round((i.options.h?lt(i.options.h):typeof i.options.cy=="number"?i.options.cy:1)/P.length)),T+=``,m.forEach(q=>{var se,Z,ue,j,_;let fe=q,ce={rowSpan:((se=fe.options)===null||se===void 0?void 0:se.rowspan)>1?fe.options.rowspan:void 0,gridSpan:((Z=fe.options)===null||Z===void 0?void 0:Z.colspan)>1?fe.options.colspan:void 0,vMerge:fe._vmerge?1:void 0,hMerge:fe._hmerge?1:void 0},ie=Object.keys(ce).map(Be=>[Be,ce[Be]]).filter(([,Be])=>!!Be).map(([Be,h])=>`${String(Be)}="${String(h)}"`).join(" ");if(ie&&(ie=" "+ie),fe._hmerge||fe._vmerge){T+=``;return}let be=fe.options||{};fe.options=be,["align","bold","border","color","fill","fontFace","fontSize","margin","textDirection","underline","valign"].forEach(Be=>{F[Be]&&!be[Be]&&be[Be]!==0&&(be[Be]=F[Be])});let Re=be.valign?` anchor="${be.valign.replace(/^c$/i,"ctr").replace(/^m$/i,"ctr").replace("center","ctr").replace("middle","ctr").replace("top","t").replace("btm","b").replace("bottom","b")}"`:"",ke=be.textDirection&&be.textDirection!=="horz"?` vert="${be.textDirection}"`:"",xe=!((j=(ue=fe._optImp)===null||ue===void 0?void 0:ue.fill)===null||j===void 0)&&j.color?fe._optImp.fill.color:!((_=fe._optImp)===null||_===void 0)&&_.fill&&typeof fe._optImp.fill=="string"?fe._optImp.fill:"";xe=xe||be.fill?be.fill:"";let Oe=xe?lr(xe):"",we=be.margin===0||be.margin?be.margin:Ud;!Array.isArray(we)&&typeof we=="number"&&(we=[we,we,we,we]);let tt="";we[0]>=1?tt=` marL="${je(we[3])}" marR="${je(we[1])}" marT="${je(we[0])}" marB="${je(we[2])}"`:tt=` marL="${lt(we[3])}" marR="${lt(we[1])}" marT="${lt(we[0])}" marB="${lt(we[2])}"`,T+=`${Cu(fe)}`,be.border&&Array.isArray(be.border)&&[{idx:3,name:"lnL"},{idx:1,name:"lnR"},{idx:0,name:"lnT"},{idx:2,name:"lnB"}].forEach(Be=>{be.border[Be.idx].type!=="none"?(T+=``,T+=`${ft(be.border[Be.idx].color)}`,T+=``,T+=``):T+=``}),T+=Oe,T+=" ",T+=" "}),T+=""}),T+=" ",T+=" ",T+=" ",T+="",t+=T,n++;break;case He.text:case He.placeholder:if(!i.options.line&&v===0&&(v=Ke*.3),i.options._bodyProp||(i.options._bodyProp={}),i.options.margin&&Array.isArray(i.options.margin)?(i.options._bodyProp.lIns=je(i.options.margin[0]||0),i.options._bodyProp.rIns=je(i.options.margin[1]||0),i.options._bodyProp.bIns=je(i.options.margin[2]||0),i.options._bodyProp.tIns=je(i.options.margin[3]||0)):typeof i.options.margin=="number"&&(i.options._bodyProp.lIns=je(i.options.margin),i.options._bodyProp.rIns=je(i.options.margin),i.options._bodyProp.bIns=je(i.options.margin),i.options._bodyProp.tIns=je(i.options.margin)),t+="",t+=``,!((c=i.options.hyperlink)===null||c===void 0)&&c.url&&(t+=``),!((s=i.options.hyperlink)===null||s===void 0)&&s.slide&&(t+=``),t+="",t+="':"/>"),t+=`${i._type==="placeholder"?Ka(i):Ka(b)}`,t+="",t+=``,t+=``,t+=``,i.shape==="custGeom")t+="",t+="",t+="",t+="",t+="",t+="",t+='',t+="",t+=``,(d=i.options.points)===null||d===void 0||d.forEach((m,N)=>{if("curve"in m)switch(m.curve.type){case"arc":t+=``;break;case"cubic":t+=` `;break;case"quadratic":t+=` - `;break}else"close"in m?t+="":m.moveTo||N===0?t+=``:t+=``}),t+="",t+="",t+="";else{if(t+='',i.options.rectRadius)t+=``;else if(i.options.angleRange){for(let m=0;m<2;m++){let N=i.options.angleRange[m];t+=``}i.options.arcThicknessRatio&&(t+=``)}t+=""}t+=i.options.fill?lr(i.options.fill):"",i.options.line&&(t+=i.options.line.width?``:"",i.options.line.color&&(t+=lr(i.options.line)),i.options.line.dashType&&(t+=``),i.options.line.beginArrowType&&(t+=``),i.options.line.endArrowType&&(t+=``),t+=""),i.options.shadow&&i.options.shadow.type!=="none"&&(i.options.shadow.type=i.options.shadow.type||"outer",i.options.shadow.blur=je(i.options.shadow.blur||8),i.options.shadow.offset=je(i.options.shadow.offset||4),i.options.shadow.angle=Math.round((i.options.shadow.angle||270)*6e4),i.options.shadow.opacity=Math.round((i.options.shadow.opacity||.75)*1e5),i.options.shadow.color=i.options.shadow.color||wu.color,t+="",t+=` `,t+=` `,t+=` `,t+=" ",t+=""),t+="",t+=Cu(i),t+="";break;case He.image:if(t+="",t+=" ",t+=``,!((A=i.hyperlink)===null||A===void 0)&&A.url&&(t+=``),!((f=i.hyperlink)===null||f===void 0)&&f.slide&&(t+=``),t+=" ",t+=' ',t+=" "+Xa(b)+"",t+=" ",t+="",(e._relsMedia||[]).filter(m=>m.rId===i.imageRid)[0]&&(e._relsMedia||[]).filter(m=>m.rId===i.imageRid)[0].extn==="svg"?(t+=``,t+=i.options.transparency?` `:"",t+=" ",t+=' ',t+=` `,t+=" ",t+=" ",t+=""):(t+=``,t+=i.options.transparency?``:"",t+=""),W?.type){let m=W.w?nt(W.w,"X",e._presLayout):y,N=W.h?nt(W.h,"Y",e._presLayout):v,ne=nt(W.x||0,"X",e._presLayout),q=nt(W.y||0,"Y",e._presLayout);t+=A0[W.type]({w:C,h:I},{w:m,h:N,x:ne,y:q}),C=m,I=N}else t+=" ";t+="",t+="",t+=" ",t+=` `,t+=` `,t+=" ",t+=` `,i.options.shadow&&i.options.shadow.type!=="none"&&(i.options.shadow.type=i.options.shadow.type||"outer",i.options.shadow.blur=je(i.options.shadow.blur||8),i.options.shadow.offset=je(i.options.shadow.offset||4),i.options.shadow.angle=Math.round((i.options.shadow.angle||270)*6e4),i.options.shadow.opacity=Math.round((i.options.shadow.opacity||.75)*1e5),i.options.shadow.color=i.options.shadow.color||wu.color,t+="",t+=``,t+=``,t+=``,t+=``,t+=""),t+="",t+="";break;case He.media:i.mtype==="online"?(t+="",t+=" ",t+=``,t+=" ",t+=" ",t+=` `,t+=" ",t+=" ",t+=` `,t+=" ",t+=` `,t+=' ',t+=" ",t+=""):(t+="",t+=" ",t+=``,t+=' ',t+=" ",t+=` `,t+=" ",t+=' ',t+=` `,t+=" ",t+=" ",t+=" ",t+=" ",t+=` `,t+=" ",t+=` `,t+=' ',t+=" ",t+="");break;case He.chart:t+="",t+=" ",t+=` `,t+=" ",t+=` ${Xa(b)}`,t+=" ",t+=` `,t+=' ',t+=' ',t+=` `,t+=" ",t+=" ",t+="";break;default:t+="";break}}),e._slideNumberProps&&(e._slideNumberProps.align||(e._slideNumberProps.align="left"),t+="",t+=" ",t+=' ',t+=' ',t+=" ",t+=" ",t+=` `,t+="",t+="`,e._slideNumberProps.color&&(t+=lr(e._slideNumberProps.color)),e._slideNumberProps.fontFace&&(t+=``),t+=""),t+="",t+="",e._slideNumberProps.align.startsWith("l")?t+='':e._slideNumberProps.align.startsWith("c")?t+='':e._slideNumberProps.align.startsWith("r")?t+='':t+='',t+=``,t+=`${e._slideNum}`,t+=""),t+="",t+="",t}function Fl(e,r){let t=0,n=''+Gt+'';return e._rels.forEach(i=>{t=Math.max(t,i.rId),i.type.toLowerCase().includes("hyperlink")?i.data==="slide"?n+=``:n+=``:i.type.toLowerCase().includes("notesSlide")&&(n+=``)}),(e._relsChart||[]).forEach(i=>{t=Math.max(t,i.rId),n+=``}),(e._relsMedia||[]).forEach(i=>{let a=i.rId.toString();t=Math.max(t,i.rId),i.type.toLowerCase().includes("image")?n+='':i.type.toLowerCase().includes("audio")?n.includes(' Target="'+i.Target+'"')?n+='':n+='':i.type.toLowerCase().includes("video")?n.includes(' Target="'+i.Target+'"')?n+='':n+='':i.type.toLowerCase().includes("online")&&(n.includes(' Target="'+i.Target+'"')?n+='':n+='')}),r.forEach((i,a)=>{n+=``}),n+="",n}function ku(e,r){var t,n;let i="",a="",o="",l="",c=r?"a:lvl1pPr":"a:pPr",s=je(Yv),u=`<${c}${e.options.rtlMode?' rtl="1" ':""}`;{if(e.options.align)switch(e.options.align){case"left":u+=' algn="l"';break;case"right":u+=' algn="r"';break;case"center":u+=' algn="ctr"';break;case"justify":u+=' algn="just"';break;default:u+="";break}if(e.options.lineSpacing?a=``:e.options.lineSpacingMultiple&&(a=``),e.options.indentLevel&&!isNaN(Number(e.options.indentLevel))&&e.options.indentLevel>0&&(u+=` lvl="${e.options.indentLevel}"`),e.options.paraSpaceBefore&&!isNaN(Number(e.options.paraSpaceBefore))&&e.options.paraSpaceBefore>0&&(o+=``),e.options.paraSpaceAfter&&!isNaN(Number(e.options.paraSpaceAfter))&&e.options.paraSpaceAfter>0&&(o+=``),typeof e.options.bullet=="object")if(!((n=(t=e?.options)===null||t===void 0?void 0:t.bullet)===null||n===void 0)&&n.indent&&(s=je(e.options.bullet.indent)),e.options.bullet.type)e.options.bullet.type.toString().toLowerCase()==="number"&&(u+=` marL="${e.options.indentLevel&&e.options.indentLevel>0?s+s*e.options.indentLevel:s}" indent="-${s}"`,i=``);else if(e.options.bullet.characterCode){let d=`&#x${e.options.bullet.characterCode};`;/^[0-9A-Fa-f]{4}$/.test(e.options.bullet.characterCode)||(console.warn("Warning: `bullet.characterCode should be a 4-digit unicode charatcer (ex: 22AB)`!"),d=li.DEFAULT),u+=` marL="${e.options.indentLevel&&e.options.indentLevel>0?s+s*e.options.indentLevel:s}" indent="-${s}"`,i=''}else if(e.options.bullet.code){let d=`&#x${e.options.bullet.code};`;/^[0-9A-Fa-f]{4}$/.test(e.options.bullet.code)||(console.warn("Warning: `bullet.code should be a 4-digit hex code (ex: 22AB)`!"),d=li.DEFAULT),u+=` marL="${e.options.indentLevel&&e.options.indentLevel>0?s+s*e.options.indentLevel:s}" indent="-${s}"`,i=''}else u+=` marL="${e.options.indentLevel&&e.options.indentLevel>0?s+s*e.options.indentLevel:s}" indent="-${s}"`,i=``;else e.options.bullet?(u+=` marL="${e.options.indentLevel&&e.options.indentLevel>0?s+s*e.options.indentLevel:s}" indent="-${s}"`,i=``):e.options.bullet||(u+=' indent="0" marL="0"',i="");e.options.tabStops&&Array.isArray(e.options.tabStops)&&(l=`${e.options.tabStops.map(A=>``).join("")}`),u+=">"+a+o+i+l,r&&(u+=Kd(e.options,!0)),u+=""}return u}function Kd(e,r){var t;let n="",i=r?"a:defRPr":"a:rPr";if(n+="<"+i+' lang="'+(e.lang?e.lang:"en-US")+'"'+(e.lang?' altLang="en-US"':""),n+=e.fontSize?` sz="${Math.round(e.fontSize*100)}"`:"",n+=e?.bold?` b="${e.bold?"1":"0"}"`:"",n+=e?.italic?` i="${e.italic?"1":"0"}"`:"",n+=e?.strike?` strike="${typeof e.strike=="string"?e.strike:"sngStrike"}"`:"",typeof e.underline=="object"&&(!((t=e.underline)===null||t===void 0)&&t.style)?n+=` u="${e.underline.style}"`:typeof e.underline=="string"?n+=` u="${String(e.underline)}"`:e.hyperlink&&(n+=' u="sng"'),e.baseline?n+=` baseline="${Math.round(e.baseline*50)}"`:e.subscript?n+=' baseline="-40000"':e.superscript&&(n+=' baseline="30000"'),n+=e.charSpacing?` spc="${Math.round(e.charSpacing*100)}" kern="0"`:"",n+=' dirty="0">',(e.color||e.fontFace||e.outline||typeof e.underline=="object"&&e.underline.color)&&(e.outline&&typeof e.outline=="object"&&(n+=`${lr(e.outline.color||"FFFFFF")}`),e.color&&(n+=lr({color:e.color,transparency:e.transparency})),e.highlight&&(n+=`${ft(e.highlight)}`),typeof e.underline=="object"&&e.underline.color&&(n+=`${lr(e.underline.color)}`),e.glow&&(n+=`${r0(e.glow,$v)}`),e.fontFace&&(n+=``)),e.hyperlink){if(typeof e.hyperlink!="object")throw new Error("ERROR: text `hyperlink` option should be an object. Ex: `hyperlink:{url:'https://github.com'}` ");if(!e.hyperlink.url&&!e.hyperlink.slide)throw new Error("ERROR: 'hyperlink requires either `url` or `slide`'");e.hyperlink.url?n+=`":"/>"}`:e.hyperlink.slide&&(n+=`":"/>"}`),e.color&&(n+=" ",n+=' ',n+=' ',n+=" ",n+=" ",n+="")}return n+=``,n}function g0(e){return e.text?`${Kd(e.options,!1)}${Ve(e.text)}`:""}function m0(e){let r="":e.options.fit==="resize"&&(r+="")),e.options.shrinkText&&(r+=""),r+=e.options._bodyProp.autoFit?"":"",r+=""):(r+=' wrap="square" rtlCol="0">',r+=""),e._type===He.tablecell?"":r}function Cu(e){let r=e.options||{},t=[],n=[];if(r&&e._type!==He.tablecell&&(typeof e.text>"u"||e.text===null))return"";let i=e._type===He.tablecell?"":"";i+=m0(e),r.h===0&&r.line&&r.align?i+='':e._type==="placeholder"?i+=`${ku(e,!0)}`:i+="",typeof e.text=="string"||typeof e.text=="number"?t.push({text:e.text.toString(),options:r||{}}):e.text&&!Array.isArray(e.text)&&typeof e.text=="object"&&Object.keys(e.text).includes("text")?t.push({text:e.text||"",options:e.options||{}}):Array.isArray(e.text)&&(t=e.text.map(l=>({text:l.text,options:l.options}))),t.forEach((l,c)=>{l.text||(l.text=""),l.options=l.options||r||{},c===0&&l.options&&!l.options.bullet&&r.bullet&&(l.options.bullet=r.bullet),(typeof l.text=="string"||typeof l.text=="number")&&(l.text=l.text.toString().replace(/\r*\n/g,Gt)),l.text.includes(Gt)&&l.text.match(/\n$/g)===null?l.text.split(Gt).forEach(s=>{l.options.breakLine=!0,n.push({text:s,options:l.options})}):n.push(l)});let a=[],o=[];return n.forEach((l,c)=>{o.length>0&&(l.options.align||r.align)?l.options.align!==n[c-1].options.align&&(a.push(o),o=[]):o.length>0&&l.options.bullet&&o.length>0&&(a.push(o),o=[],l.options.breakLine=!1),o.push(l),o.length>0&&l.options.breakLine&&c+1{var c;let s=!1;i+="";let u=`{d.options._lineIdx=A,A>0&&d.options.softBreakBefore&&(i+=""),d.options.align=d.options.align||r.align,d.options.lineSpacing=d.options.lineSpacing||r.lineSpacing,d.options.lineSpacingMultiple=d.options.lineSpacingMultiple||r.lineSpacingMultiple,d.options.indentLevel=d.options.indentLevel||r.indentLevel,d.options.paraSpaceBefore=d.options.paraSpaceBefore||r.paraSpaceBefore,d.options.paraSpaceAfter=d.options.paraSpaceAfter||r.paraSpaceAfter,u=ku(d,!1),i+=u.replace("",""),Object.entries(r).filter(([f])=>!(d.options.hyperlink&&f==="color")).forEach(([f,p])=>{f!=="bullet"&&!d.options[f]&&(d.options[f]=p)}),i+=g0(d),(!d.text&&r.fontSize||d.options.fontSize)&&(s=!0,r.fontSize=r.fontSize||d.options.fontSize)}),e._type===He.tablecell&&(r.fontSize||r.fontFace)?r.fontFace?(i+=`',i+=``,i+=``,i+=``,i+=""):i+=`':s?i+=`':i+=``,i+=""}),i.indexOf("")===-1&&(i+=""),i+=e._type===He.tablecell?"":"",i}function Xa(e){var r,t;if(!e)return"";let n=!((r=e.options)===null||r===void 0)&&r._placeholderIdx?e.options._placeholderIdx:"",i=!((t=e.options)===null||t===void 0)&&t._placeholderType?e.options._placeholderType:"",a=i&&Qi[i]?Qi[i].toString():"";return``;break}else"close"in m?t+="":m.moveTo||N===0?t+=``:t+=``}),t+="",t+="",t+="";else{if(t+='',i.options.rectRadius)t+=``;else if(i.options.angleRange){for(let m=0;m<2;m++){let N=i.options.angleRange[m];t+=``}i.options.arcThicknessRatio&&(t+=``)}t+=""}t+=i.options.fill?lr(i.options.fill):"",i.options.line&&(t+=i.options.line.width?``:"",i.options.line.color&&(t+=lr(i.options.line)),i.options.line.dashType&&(t+=``),i.options.line.beginArrowType&&(t+=``),i.options.line.endArrowType&&(t+=``),t+=""),i.options.shadow&&i.options.shadow.type!=="none"&&(i.options.shadow.type=i.options.shadow.type||"outer",i.options.shadow.blur=je(i.options.shadow.blur||8),i.options.shadow.offset=je(i.options.shadow.offset||4),i.options.shadow.angle=Math.round((i.options.shadow.angle||270)*6e4),i.options.shadow.opacity=Math.round((i.options.shadow.opacity||.75)*1e5),i.options.shadow.color=i.options.shadow.color||wu.color,t+="",t+=` `,t+=` `,t+=` `,t+=" ",t+=""),t+="",t+=Cu(i),t+="";break;case He.image:if(t+="",t+=" ",t+=``,!((A=i.hyperlink)===null||A===void 0)&&A.url&&(t+=``),!((f=i.hyperlink)===null||f===void 0)&&f.slide&&(t+=``),t+=" ",t+=' ',t+=" "+Ka(b)+"",t+=" ",t+="",(e._relsMedia||[]).filter(m=>m.rId===i.imageRid)[0]&&(e._relsMedia||[]).filter(m=>m.rId===i.imageRid)[0].extn==="svg"?(t+=``,t+=i.options.transparency?` `:"",t+=" ",t+=' ',t+=` `,t+=" ",t+=" ",t+=""):(t+=``,t+=i.options.transparency?``:"",t+=""),W?.type){let m=W.w?nt(W.w,"X",e._presLayout):y,N=W.h?nt(W.h,"Y",e._presLayout):v,ne=nt(W.x||0,"X",e._presLayout),q=nt(W.y||0,"Y",e._presLayout);t+=b0[W.type]({w:C,h:I},{w:m,h:N,x:ne,y:q}),C=m,I=N}else t+=" ";t+="",t+="",t+=" ",t+=` `,t+=` `,t+=" ",t+=` `,i.options.shadow&&i.options.shadow.type!=="none"&&(i.options.shadow.type=i.options.shadow.type||"outer",i.options.shadow.blur=je(i.options.shadow.blur||8),i.options.shadow.offset=je(i.options.shadow.offset||4),i.options.shadow.angle=Math.round((i.options.shadow.angle||270)*6e4),i.options.shadow.opacity=Math.round((i.options.shadow.opacity||.75)*1e5),i.options.shadow.color=i.options.shadow.color||wu.color,t+="",t+=``,t+=``,t+=``,t+=``,t+=""),t+="",t+="";break;case He.media:i.mtype==="online"?(t+="",t+=" ",t+=``,t+=" ",t+=" ",t+=` `,t+=" ",t+=" ",t+=` `,t+=" ",t+=` `,t+=' ',t+=" ",t+=""):(t+="",t+=" ",t+=``,t+=' ',t+=" ",t+=` `,t+=" ",t+=' ',t+=` `,t+=" ",t+=" ",t+=" ",t+=" ",t+=` `,t+=" ",t+=` `,t+=' ',t+=" ",t+="");break;case He.chart:t+="",t+=" ",t+=` `,t+=" ",t+=` ${Ka(b)}`,t+=" ",t+=` `,t+=' ',t+=' ',t+=` `,t+=" ",t+=" ",t+="";break;default:t+="";break}}),e._slideNumberProps&&(e._slideNumberProps.align||(e._slideNumberProps.align="left"),t+="",t+=" ",t+=' ',t+=' ',t+=" ",t+=" ",t+=` `,t+="",t+="`,e._slideNumberProps.color&&(t+=lr(e._slideNumberProps.color)),e._slideNumberProps.fontFace&&(t+=``),t+=""),t+="",t+="",e._slideNumberProps.align.startsWith("l")?t+='':e._slideNumberProps.align.startsWith("c")?t+='':e._slideNumberProps.align.startsWith("r")?t+='':t+='',t+=``,t+=`${e._slideNum}`,t+=""),t+="",t+="",t}function Tl(e,r){let t=0,n=''+Wt+'';return e._rels.forEach(i=>{t=Math.max(t,i.rId),i.type.toLowerCase().includes("hyperlink")?i.data==="slide"?n+=``:n+=``:i.type.toLowerCase().includes("notesSlide")&&(n+=``)}),(e._relsChart||[]).forEach(i=>{t=Math.max(t,i.rId),n+=``}),(e._relsMedia||[]).forEach(i=>{let a=i.rId.toString();t=Math.max(t,i.rId),i.type.toLowerCase().includes("image")?n+='':i.type.toLowerCase().includes("audio")?n.includes(' Target="'+i.Target+'"')?n+='':n+='':i.type.toLowerCase().includes("video")?n.includes(' Target="'+i.Target+'"')?n+='':n+='':i.type.toLowerCase().includes("online")&&(n.includes(' Target="'+i.Target+'"')?n+='':n+='')}),r.forEach((i,a)=>{n+=``}),n+="",n}function ku(e,r){var t,n;let i="",a="",o="",l="",c=r?"a:lvl1pPr":"a:pPr",s=je(r0),u=`<${c}${e.options.rtlMode?' rtl="1" ':""}`;{if(e.options.align)switch(e.options.align){case"left":u+=' algn="l"';break;case"right":u+=' algn="r"';break;case"center":u+=' algn="ctr"';break;case"justify":u+=' algn="just"';break;default:u+="";break}if(e.options.lineSpacing?a=``:e.options.lineSpacingMultiple&&(a=``),e.options.indentLevel&&!isNaN(Number(e.options.indentLevel))&&e.options.indentLevel>0&&(u+=` lvl="${e.options.indentLevel}"`),e.options.paraSpaceBefore&&!isNaN(Number(e.options.paraSpaceBefore))&&e.options.paraSpaceBefore>0&&(o+=``),e.options.paraSpaceAfter&&!isNaN(Number(e.options.paraSpaceAfter))&&e.options.paraSpaceAfter>0&&(o+=``),typeof e.options.bullet=="object")if(!((n=(t=e?.options)===null||t===void 0?void 0:t.bullet)===null||n===void 0)&&n.indent&&(s=je(e.options.bullet.indent)),e.options.bullet.type)e.options.bullet.type.toString().toLowerCase()==="number"&&(u+=` marL="${e.options.indentLevel&&e.options.indentLevel>0?s+s*e.options.indentLevel:s}" indent="-${s}"`,i=``);else if(e.options.bullet.characterCode){let d=`&#x${e.options.bullet.characterCode};`;/^[0-9A-Fa-f]{4}$/.test(e.options.bullet.characterCode)||(console.warn("Warning: `bullet.characterCode should be a 4-digit unicode charatcer (ex: 22AB)`!"),d=li.DEFAULT),u+=` marL="${e.options.indentLevel&&e.options.indentLevel>0?s+s*e.options.indentLevel:s}" indent="-${s}"`,i=''}else if(e.options.bullet.code){let d=`&#x${e.options.bullet.code};`;/^[0-9A-Fa-f]{4}$/.test(e.options.bullet.code)||(console.warn("Warning: `bullet.code should be a 4-digit hex code (ex: 22AB)`!"),d=li.DEFAULT),u+=` marL="${e.options.indentLevel&&e.options.indentLevel>0?s+s*e.options.indentLevel:s}" indent="-${s}"`,i=''}else u+=` marL="${e.options.indentLevel&&e.options.indentLevel>0?s+s*e.options.indentLevel:s}" indent="-${s}"`,i=``;else e.options.bullet?(u+=` marL="${e.options.indentLevel&&e.options.indentLevel>0?s+s*e.options.indentLevel:s}" indent="-${s}"`,i=``):e.options.bullet||(u+=' indent="0" marL="0"',i="");e.options.tabStops&&Array.isArray(e.options.tabStops)&&(l=`${e.options.tabStops.map(A=>``).join("")}`),u+=">"+a+o+i+l,r&&(u+=Kd(e.options,!0)),u+=""}return u}function Kd(e,r){var t;let n="",i=r?"a:defRPr":"a:rPr";if(n+="<"+i+' lang="'+(e.lang?e.lang:"en-US")+'"'+(e.lang?' altLang="en-US"':""),n+=e.fontSize?` sz="${Math.round(e.fontSize*100)}"`:"",n+=e?.bold?` b="${e.bold?"1":"0"}"`:"",n+=e?.italic?` i="${e.italic?"1":"0"}"`:"",n+=e?.strike?` strike="${typeof e.strike=="string"?e.strike:"sngStrike"}"`:"",typeof e.underline=="object"&&(!((t=e.underline)===null||t===void 0)&&t.style)?n+=` u="${e.underline.style}"`:typeof e.underline=="string"?n+=` u="${String(e.underline)}"`:e.hyperlink&&(n+=' u="sng"'),e.baseline?n+=` baseline="${Math.round(e.baseline*50)}"`:e.subscript?n+=' baseline="-40000"':e.superscript&&(n+=' baseline="30000"'),n+=e.charSpacing?` spc="${Math.round(e.charSpacing*100)}" kern="0"`:"",n+=' dirty="0">',(e.color||e.fontFace||e.outline||typeof e.underline=="object"&&e.underline.color)&&(e.outline&&typeof e.outline=="object"&&(n+=`${lr(e.outline.color||"FFFFFF")}`),e.color&&(n+=lr({color:e.color,transparency:e.transparency})),e.highlight&&(n+=`${ft(e.highlight)}`),typeof e.underline=="object"&&e.underline.color&&(n+=`${lr(e.underline.color)}`),e.glow&&(n+=`${s0(e.glow,i0)}`),e.fontFace&&(n+=``)),e.hyperlink){if(typeof e.hyperlink!="object")throw new Error("ERROR: text `hyperlink` option should be an object. Ex: `hyperlink:{url:'https://github.com'}` ");if(!e.hyperlink.url&&!e.hyperlink.slide)throw new Error("ERROR: 'hyperlink requires either `url` or `slide`'");e.hyperlink.url?n+=`":"/>"}`:e.hyperlink.slide&&(n+=`":"/>"}`),e.color&&(n+=" ",n+=' ',n+=' ',n+=" ",n+=" ",n+="")}return n+=``,n}function w0(e){return e.text?`${Kd(e.options,!1)}${Ve(e.text)}`:""}function x0(e){let r="":e.options.fit==="resize"&&(r+="")),e.options.shrinkText&&(r+=""),r+=e.options._bodyProp.autoFit?"":"",r+=""):(r+=' wrap="square" rtlCol="0">',r+=""),e._type===He.tablecell?"":r}function Cu(e){let r=e.options||{},t=[],n=[];if(r&&e._type!==He.tablecell&&(typeof e.text>"u"||e.text===null))return"";let i=e._type===He.tablecell?"":"";i+=x0(e),r.h===0&&r.line&&r.align?i+='':e._type==="placeholder"?i+=`${ku(e,!0)}`:i+="",typeof e.text=="string"||typeof e.text=="number"?t.push({text:e.text.toString(),options:r||{}}):e.text&&!Array.isArray(e.text)&&typeof e.text=="object"&&Object.keys(e.text).includes("text")?t.push({text:e.text||"",options:e.options||{}}):Array.isArray(e.text)&&(t=e.text.map(l=>({text:l.text,options:l.options}))),t.forEach((l,c)=>{l.text||(l.text=""),l.options=l.options||r||{},c===0&&l.options&&!l.options.bullet&&r.bullet&&(l.options.bullet=r.bullet),(typeof l.text=="string"||typeof l.text=="number")&&(l.text=l.text.toString().replace(/\r*\n/g,Wt)),l.text.includes(Wt)&&l.text.match(/\n$/g)===null?l.text.split(Wt).forEach(s=>{l.options.breakLine=!0,n.push({text:s,options:l.options})}):n.push(l)});let a=[],o=[];return n.forEach((l,c)=>{o.length>0&&(l.options.align||r.align)?l.options.align!==n[c-1].options.align&&(a.push(o),o=[]):o.length>0&&l.options.bullet&&o.length>0&&(a.push(o),o=[],l.options.breakLine=!1),o.push(l),o.length>0&&l.options.breakLine&&c+1{var c;let s=!1;i+="";let u=`{d.options._lineIdx=A,A>0&&d.options.softBreakBefore&&(i+=""),d.options.align=d.options.align||r.align,d.options.lineSpacing=d.options.lineSpacing||r.lineSpacing,d.options.lineSpacingMultiple=d.options.lineSpacingMultiple||r.lineSpacingMultiple,d.options.indentLevel=d.options.indentLevel||r.indentLevel,d.options.paraSpaceBefore=d.options.paraSpaceBefore||r.paraSpaceBefore,d.options.paraSpaceAfter=d.options.paraSpaceAfter||r.paraSpaceAfter,u=ku(d,!1),i+=u.replace("",""),Object.entries(r).filter(([f])=>!(d.options.hyperlink&&f==="color")).forEach(([f,p])=>{f!=="bullet"&&!d.options[f]&&(d.options[f]=p)}),i+=w0(d),(!d.text&&r.fontSize||d.options.fontSize)&&(s=!0,r.fontSize=r.fontSize||d.options.fontSize)}),e._type===He.tablecell&&(r.fontSize||r.fontFace)?r.fontFace?(i+=`',i+=``,i+=``,i+=``,i+=""):i+=`':s?i+=`':i+=``,i+=""}),i.indexOf("")===-1&&(i+=""),i+=e._type===He.tablecell?"":"",i}function Ka(e){var r,t;if(!e)return"";let n=!((r=e.options)===null||r===void 0)&&r._placeholderIdx?e.options._placeholderIdx:"",i=!((t=e.options)===null||t===void 0)&&t._placeholderType?e.options._placeholderType:"",a=i&&Zi[i]?Zi[i].toString():"";return`0?' hasCustomPrompt="1"':""} - />`}function v0(e,r,t){let n=''+Gt;return n+='',n+='',n+='',n+='',n+='',n+='',n+='',n+='',n+='',n+='',e.forEach(i=>{(i._relsMedia||[]).forEach(a=>{a.type!=="image"&&a.type!=="online"&&a.type!=="chart"&&a.extn!=="m4v"&&!n.includes(a.type)&&(n+='')})}),n+='',n+='',n+='',n+='',e.forEach((i,a)=>{n+=``,n+=``,i._relsChart.forEach(o=>{n+=``})}),n+='',n+='',n+='',n+='',r.forEach((i,a)=>{n+=``,(i._relsChart||[]).forEach(o=>{n+=' '})}),e.forEach((i,a)=>{n+=``}),t._relsChart.forEach(i=>{n+=' '}),t._relsMedia.forEach(i=>{i.type!=="image"&&i.type!=="online"&&i.type!=="chart"&&i.extn!=="m4v"&&!n.includes(i.type)&&(n+=' ')}),n+=' ',n+=' ',n+="",n}function y0(){return`${Gt} + />`}function S0(e,r,t){let n=''+Wt;return n+='',n+='',n+='',n+='',n+='',n+='',n+='',n+='',n+='',n+='',e.forEach(i=>{(i._relsMedia||[]).forEach(a=>{a.type!=="image"&&a.type!=="online"&&a.type!=="chart"&&a.extn!=="m4v"&&!n.includes(a.type)&&(n+='')})}),n+='',n+='',n+='',n+='',e.forEach((i,a)=>{n+=``,n+=``,i._relsChart.forEach(o=>{n+=``})}),n+='',n+='',n+='',n+='',r.forEach((i,a)=>{n+=``,(i._relsChart||[]).forEach(o=>{n+=' '})}),e.forEach((i,a)=>{n+=``}),t._relsChart.forEach(i=>{n+=' '}),t._relsMedia.forEach(i=>{i.type!=="image"&&i.type!=="online"&&i.type!=="chart"&&i.extn!=="m4v"&&!n.includes(i.type)&&(n+=' ')}),n+=' ',n+=' ',n+="",n}function k0(){return`${Wt} - `}function b0(e,r){return`${Gt} + `}function C0(e,r){return`${Wt} 0 0 Microsoft Office PowerPoint @@ -246,7 +286,7 @@ ${String(d)}`)}return n&&a&&u.path.startsWith("http")?yield new Promise((d,A)=>{ false false 16.0000 - `}function w0(e,r,t,n){return` + `}function P0(e,r,t,n){return` ${Ve(e)} ${Ve(r)} @@ -255,36 +295,36 @@ ${String(d)}`)}return n&&a&&u.path.startsWith("http")?yield new Promise((d,A)=>{ ${n} ${new Date().toISOString().replace(/\.\d\d\dZ/,"Z")} ${new Date().toISOString().replace(/\.\d\d\dZ/,"Z")} - `}function x0(e){let r=1,t=''+Gt;t+='',t+='';for(let n=1;n<=e.length;n++)t+=``;return r++,t+=``,t}function S0(e){return`${Gt}${Pl(e)}`}function k0(e){let r="";return e._slideObjects.forEach(t=>{t._type===He.notes&&(r+=t?.text&&t.text[0]?t.text[0].text:"")}),r.replace(/\r*\n/g,Gt)}function C0(){return`${Gt}7/23/19Click to edit Master text stylesSecond levelThird levelFourth levelFifth level\u2039#\u203A`}function P0(e){return`${Gt}${Ve(k0(e))}${e._slideNum}`}function F0(e){return` + `}function F0(e){let r=1,t=''+Wt;t+='',t+='';for(let n=1;n<=e.length;n++)t+=``;return r++,t+=``,t}function T0(e){return`${Wt}${Fl(e)}`}function D0(e){let r="";return e._slideObjects.forEach(t=>{t._type===He.notes&&(r+=t?.text&&t.text[0]?t.text[0].text:"")}),r.replace(/\r*\n/g,Wt)}function E0(){return`${Wt}7/23/19Click to edit Master text stylesSecond levelThird levelFourth levelFifth level\u2039#\u203A`}function B0(e){return`${Wt}${Ve(D0(e))}${e._slideNum}`}function R0(e){return` - ${Pl(e)} - `}function T0(e,r){let t=r.map((i,a)=>``),n=''+Gt;return n+='',n+=Pl(e),n+='',n+=""+t.join("")+"",n+='',n+=' ',n+="",n}function D0(e,r){return Fl(r[e-1],[{target:"../slideMasters/slideMaster1.xml",type:"http://schemas.openxmlformats.org/officeDocument/2006/relationships/slideMaster"}])}function E0(e,r,t){return Fl(e[t-1],[{target:`../slideLayouts/slideLayout${N0(e,r,t)}.xml`,type:"http://schemas.openxmlformats.org/officeDocument/2006/relationships/slideLayout"},{target:`../notesSlides/notesSlide${t}.xml`,type:"http://schemas.openxmlformats.org/officeDocument/2006/relationships/notesSlide"}])}function B0(e){return` + ${Fl(e)} + `}function L0(e,r){let t=r.map((i,a)=>``),n=''+Wt;return n+='',n+=Fl(e),n+='',n+=""+t.join("")+"",n+='',n+=' ',n+="",n}function N0(e,r){return Tl(r[e-1],[{target:"../slideMasters/slideMaster1.xml",type:"http://schemas.openxmlformats.org/officeDocument/2006/relationships/slideMaster"}])}function I0(e,r,t){return Tl(e[t-1],[{target:`../slideLayouts/slideLayout${_0(e,r,t)}.xml`,type:"http://schemas.openxmlformats.org/officeDocument/2006/relationships/slideLayout"},{target:`../notesSlides/notesSlide${t}.xml`,type:"http://schemas.openxmlformats.org/officeDocument/2006/relationships/notesSlide"}])}function z0(e){return` - `}function R0(e,r){let t=r.map((n,i)=>({target:`../slideLayouts/slideLayout${i+1}.xml`,type:"http://schemas.openxmlformats.org/officeDocument/2006/relationships/slideLayout"}));return t.push({target:"../theme/theme1.xml",type:"http://schemas.openxmlformats.org/officeDocument/2006/relationships/theme"}),Fl(e,t)}function L0(){return`${Gt} + `}function M0(e,r){let t=r.map((n,i)=>({target:`../slideLayouts/slideLayout${i+1}.xml`,type:"http://schemas.openxmlformats.org/officeDocument/2006/relationships/slideLayout"}));return t.push({target:"../theme/theme1.xml",type:"http://schemas.openxmlformats.org/officeDocument/2006/relationships/theme"}),Tl(e,t)}function O0(){return`${Wt} - `}function N0(e,r,t){for(let n=0;n`:'',o=!((n=e.theme)===null||n===void 0)&&n.bodyFontFace?``:'';return`${a}${o}`}function z0(e){let r=`${Gt}`;r+='',r+="",e.slides.forEach(t=>r+=``),r+="",r+=``,r+=``,r+=``,r+="";for(let t=1;t<10;t++)r+=``;return r+="",e.sections&&e.sections.length>0&&(r+='',r+='',e.sections.forEach(t=>{r+=``,t._slides.forEach(n=>r+=``),r+=""}),r+="",r+='',r+=""),r+="",r}function M0(){return`${Gt}`}function O0(){return`${Gt}`}function _0(){return`${Gt}`}var U0="4.0.1",W0=class{set layout(e){let r=this.LAYOUTS[e];if(r)this._layout=e,this._presLayout=r;else throw new Error("UNKNOWN-LAYOUT")}get layout(){return this._layout}get version(){return this._version}set author(e){this._author=e}get author(){return this._author}set company(e){this._company=e}get company(){return this._company}set revision(e){this._revision=e}get revision(){return this._revision}set subject(e){this._subject=e}get subject(){return this._subject}set theme(e){this._theme=e}get theme(){return this._theme}set title(e){this._title=e}get title(){return this._title}set rtlMode(e){this._rtlMode=e}get rtlMode(){return this._rtlMode}get masterSlide(){return this._masterSlide}get slides(){return this._slides}get sections(){return this._sections}get slideLayouts(){return this._slideLayouts}get AlignH(){return this._alignH}get AlignV(){return this._alignV}get ChartType(){return this._chartType}get OutputType(){return this._outputType}get presLayout(){return this._presLayout}get SchemeColor(){return this._schemeColor}get ShapeType(){return this._shapeType}get charts(){return this._charts}get colors(){return this._colors}get shapes(){return this._shapes}constructor(){this._version=U0,this._alignH=Zs,this._alignV=Ys,this._chartType=Ks,this._outputType=Hs,this._schemeColor=or,this._shapeType=Qs,this._charts=Ce,this._colors=mo,this._shapes=xn,this.addNewSlide=i=>{let a=this.sections.length>0&&this.sections[this.sections.length-1]._slides.filter(o=>o._slideNum===this.slides[this.slides.length-1]._slideNum).length>0;return i.sectionTitle=a?this.sections[this.sections.length-1].title:null,this.addSlide(i)},this.getSlide=i=>this.slides.filter(a=>a._slideNum===i)[0],this.setSlideNumber=i=>{this.masterSlide._slideNumberProps=i,this.slideLayouts.filter(a=>a._name===Vs)[0]._slideNumberProps=i},this.createChartMediaRels=(i,a,o)=>{i._relsChart.forEach(l=>o.push(f0(l,a))),i._relsMedia.forEach(l=>{if(l.type!=="online"&&l.type!=="hyperlink"){let c=l.data&&typeof l.data=="string"?l.data:"";!c.includes(",")&&!c.includes(";")?c="image/png;base64,"+c:c.includes(",")?c.includes(";")||(c="image/png;"+c):c="image/png;base64,"+c,a.file(l.Target.replace("..","ppt"),c.split(",").pop(),{base64:!0})}})},this.writeFileToBrowser=(i,a)=>sr(this,void 0,void 0,function*(){let o=document.createElement("a");if(o.setAttribute("style","display:none;"),o.dataset.interception="off",document.body.appendChild(o),window.URL.createObjectURL){let l=window.URL.createObjectURL(new Blob([a],{type:"application/vnd.openxmlformats-officedocument.presentationml.presentation"}));return o.href=l,o.download=i,o.click(),setTimeout(()=>{window.URL.revokeObjectURL(l),document.body.removeChild(o)},100),yield Promise.resolve(i)}}),this.exportPresentation=i=>sr(this,void 0,void 0,function*(){let a=[],o=[],l=new _d.default;return this.slides.forEach(c=>{o=o.concat(Bs(c))}),this.slideLayouts.forEach(c=>{o=o.concat(Bs(c))}),o=o.concat(Bs(this.masterSlide)),yield Promise.all(o).then(()=>sr(this,void 0,void 0,function*(){return this.slides.forEach(c=>{c._slideLayout&&u0(c)}),l.folder("_rels"),l.folder("docProps"),l.folder("ppt").folder("_rels"),l.folder("ppt/charts").folder("_rels"),l.folder("ppt/embeddings"),l.folder("ppt/media"),l.folder("ppt/slideLayouts").folder("_rels"),l.folder("ppt/slideMasters").folder("_rels"),l.folder("ppt/slides").folder("_rels"),l.folder("ppt/theme"),l.folder("ppt/notesMasters").folder("_rels"),l.folder("ppt/notesSlides").folder("_rels"),l.file("[Content_Types].xml",v0(this.slides,this.slideLayouts,this.masterSlide)),l.file("_rels/.rels",y0()),l.file("docProps/app.xml",b0(this.slides,this.company)),l.file("docProps/core.xml",w0(this.title,this.subject,this.author,this.revision)),l.file("ppt/_rels/presentation.xml.rels",x0(this.slides)),l.file("ppt/theme/theme1.xml",I0(this)),l.file("ppt/presentation.xml",z0(this)),l.file("ppt/presProps.xml",M0()),l.file("ppt/tableStyles.xml",O0()),l.file("ppt/viewProps.xml",_0()),this.slideLayouts.forEach((c,s)=>{l.file(`ppt/slideLayouts/slideLayout${s+1}.xml`,F0(c)),l.file(`ppt/slideLayouts/_rels/slideLayout${s+1}.xml.rels`,D0(s+1,this.slideLayouts))}),this.slides.forEach((c,s)=>{l.file(`ppt/slides/slide${s+1}.xml`,S0(c)),l.file(`ppt/slides/_rels/slide${s+1}.xml.rels`,E0(this.slides,this.slideLayouts,s+1)),l.file(`ppt/notesSlides/notesSlide${s+1}.xml`,P0(c)),l.file(`ppt/notesSlides/_rels/notesSlide${s+1}.xml.rels`,B0(s+1))}),l.file("ppt/slideMasters/slideMaster1.xml",T0(this.masterSlide,this.slideLayouts)),l.file("ppt/slideMasters/_rels/slideMaster1.xml.rels",R0(this.masterSlide,this.slideLayouts)),l.file("ppt/notesMasters/notesMaster1.xml",C0()),l.file("ppt/notesMasters/_rels/notesMaster1.xml.rels",L0()),this.slideLayouts.forEach(c=>{this.createChartMediaRels(c,l,a)}),this.slides.forEach(c=>{this.createChartMediaRels(c,l,a)}),this.createChartMediaRels(this.masterSlide,l,a),yield Promise.all(a).then(()=>sr(this,void 0,void 0,function*(){return i.outputType==="STREAM"?yield l.generateAsync({type:"nodebuffer",compression:i.compression?"DEFLATE":"STORE"}):i.outputType?yield l.generateAsync({type:i.outputType}):yield l.generateAsync({type:"blob",compression:i.compression?"DEFLATE":"STORE"})}))}))});let e={name:"screen4x3",width:9144e3,height:6858e3},r={name:"screen16x9",width:9144e3,height:5143500},t={name:"screen16x10",width:9144e3,height:5715e3},n={name:"custom",width:12192e3,height:6858e3};this.LAYOUTS={LAYOUT_4x3:e,LAYOUT_16x9:r,LAYOUT_16x10:t,LAYOUT_WIDE:n},this._author="PptxGenJS",this._company="PptxGenJS",this._revision="1",this._subject="PptxGenJS Presentation",this._title="PptxGenJS Presentation",this._presLayout={name:this.LAYOUTS[ni].name,_sizeW:this.LAYOUTS[ni].width,_sizeH:this.LAYOUTS[ni].height,width:this.LAYOUTS[ni].width,height:this.LAYOUTS[ni].height},this._rtlMode=!1,this._slideLayouts=[{_margin:ra,_name:Vs,_presLayout:this._presLayout,_rels:[],_relsChart:[],_relsMedia:[],_slide:null,_slideNum:1e3,_slideNumberProps:null,_slideObjects:[]}],this._slides=[],this._sections=[],this._masterSlide={addChart:null,addImage:null,addMedia:null,addNotes:null,addShape:null,addTable:null,addText:null,_name:null,_presLayout:this._presLayout,_rId:null,_rels:[],_relsChart:[],_relsMedia:[],_slideId:null,_slideLayout:null,_slideNum:null,_slideNumberProps:null,_slideObjects:[]}}stream(e){return sr(this,void 0,void 0,function*(){return yield this.exportPresentation({compression:e?.compression,outputType:"STREAM"})})}write(e){return sr(this,void 0,void 0,function*(){let r=typeof e=="object"&&e?.outputType?e.outputType:e||null,t=typeof e=="object"&&e?.compression?e.compression:!1;return yield this.exportPresentation({compression:t,outputType:r})})}writeFile(e){return sr(this,void 0,void 0,function*(){var r,t;let n=typeof process<"u"&&!!(!((r=process.versions)===null||r===void 0)&&r.node)&&((t=process.release)===null||t===void 0?void 0:t.name)==="node";typeof e=="string"&&(console.warn("[WARNING] writeFile(string) is deprecated - pass { fileName } instead."),e={fileName:e});let{fileName:i="Presentation.pptx",compression:a=!1}=e,o=i.toLowerCase().endsWith(".pptx")?i:`${i}.pptx`,l=n?"nodebuffer":null,c=yield this.exportPresentation({compression:a,outputType:l});if(n){let{promises:s}=yield Promise.resolve().then(()=>Qr(Mu())),{writeFile:u}=s;return yield u(o,c),o}return yield this.writeFileToBrowser(o,c),o})}addSection(e){e?e.title||console.warn("addSection requires a title"):console.warn("addSection requires an argument");let r={_type:"user",_slides:[],title:e.title};e.order?this.sections.splice(e.order,0,r):this._sections.push(r)}addSlide(e){let r=typeof e=="string"?e:e?.masterName?e.masterName:"",t={_name:this.LAYOUTS[ni].name,_presLayout:this.presLayout,_rels:[],_relsChart:[],_relsMedia:[],_slideNum:this.slides.length+1};if(r){let i=this.slideLayouts.filter(a=>a._name===r)[0];i&&(t=i)}let n=new d0({addSlide:this.addNewSlide,getSlide:this.getSlide,presLayout:this.presLayout,setSlideNum:this.setSlideNumber,slideId:this.slides.length+256,slideRId:this.slides.length+2,slideNumber:this.slides.length+1,slideLayout:t});if(this._slides.push(n),e?.sectionTitle){let i=this.sections.filter(a=>a.title===e.sectionTitle)[0];i?i._slides.push(n):console.warn(`addSlide: unable to find section with title: "${e.sectionTitle}"`)}else if(this.sections&&this.sections.length>0&&!e?.sectionTitle){let i=this._sections[this.sections.length-1];i._type==="default"?i._slides.push(n):this._sections.push({title:`Default-${this.sections.filter(a=>a._type==="default").length+1}`,_type:"default",_slides:[n]})}return n}defineLayout(e){e?e.name?e.width?e.height?typeof e.height!="number"?console.warn("defineLayout `height` should be a number (inches)"):typeof e.width!="number"&&console.warn("defineLayout `width` should be a number (inches)"):console.warn("defineLayout requires `height`"):console.warn("defineLayout requires `width`"):console.warn("defineLayout requires `name`"):console.warn("defineLayout requires `{name, width, height}`"),this.LAYOUTS[e.name]={name:e.name,_sizeW:Math.round(Number(e.width)*Ke),_sizeH:Math.round(Number(e.height)*Ke),width:Math.round(Number(e.width)*Ke),height:Math.round(Number(e.height)*Ke)}}defineSlideMaster(e){let r=JSON.parse(JSON.stringify(e));if(!r.title)throw new Error("defineSlideMaster() object argument requires a `title` value. (https://gitbrent.github.io/PptxGenJS/docs/masters.html)");let t={_margin:r.margin||ra,_name:r.title,_presLayout:this.presLayout,_rels:[],_relsChart:[],_relsMedia:[],_slide:null,_slideNum:1e3+this.slideLayouts.length+1,_slideNumberProps:r.slideNumber||null,_slideObjects:[],background:r.background||null,bkgd:r.bkgd||null};o0(r,t),this.slideLayouts.push(t),(r.background||r.bkgd)&&Hd(r.background,t),t._slideNumberProps&&!this.masterSlide._slideNumberProps&&(this.masterSlide._slideNumberProps=t._slideNumberProps)}tableToSlides(e,r={}){i0(this,e,r,r?.masterSlideName?this.slideLayouts.filter(t=>t._name===r.masterSlideName)[0]:null)}},$s=96,Pu=914400,G0=13.333,Fu=7.5;function Rs(e,r){return Math.min(r,Math.max(.15,G0-e-.04))}function Qd(e){let r=String(e||"").trim();return r?r.startsWith("data:")?{data:r}:r.startsWith("file://")?{path:r.replace("file://","")}:{path:r}:null}function j0(e,r){let t=[],n=e.width/$s,i=e.height/$s;if(r.presLayout){let a=r.presLayout.width/Pu,o=r.presLayout.height/Pu;(Math.abs(a-n)>.1||Math.abs(o-i)>.1)&&t.push(`HTML dimensions (${n.toFixed(1)}" \xD7 ${i.toFixed(1)}") don't match presentation layout (${a.toFixed(1)}" \xD7 ${o.toFixed(1)}")`)}return t}function q0(e,r){let t=[],n=r.height/$s,i=.5;for(let a of e.elements){if(!["p","h1","h2","h3","h4","h5","h6","text","list","merged-text"].includes(a.type))continue;let o=a.style?.fontSize||0,l=a.position.y+a.position.h,c=n-l,s=typeof a.text=="string"?a.text:Array.isArray(a.text)?a.text.find(d=>d.text)?.text||"":Array.isArray(a.items)&&a.items.find(d=>d.text)?.text||"";if(!(/^\d{1,2}\s*\/\s*\d{1,2}$/.test(s.trim())||/^(Arknights|数据来源|source:)/i.test(s.trim())||o<=11)&&o>12&&c50?"...":""}`;t.push(`Text box "${d}" ends too close to bottom edge (${c.toFixed(2)}" from bottom, minimum ${i}" required)`)}}return t}async function V0(e,r){if(e.background?.type==="image"&&e.background.path){let t=Qd(e.background.path);t&&(r.background=t)}else e.background?.type==="color"&&e.background.value&&(r.background={color:e.background.value})}function X0(e,r,t){for(let n of e.elements)if(n.type==="image"){let i=Qd(n.src);if(!i)continue;r.addImage({...i,x:n.position.x,y:n.position.y,w:n.position.w,h:n.position.h})}else if(n.type==="line")r.addShape(t.ShapeType.line,{x:n.x1,y:n.y1,w:n.x2-n.x1,h:n.y2-n.y1,line:{color:n.color,width:n.width}});else if(n.type==="shape"){let i={x:n.position.x,y:n.position.y,w:n.position.w,h:n.position.h,shape:n.shape.rectRadius>0?t.ShapeType.roundRect:t.ShapeType.rect};n.shape.fill&&(i.fill={color:n.shape.fill},n.shape.transparency!=null&&(i.fill.transparency=n.shape.transparency)),n.shape.line&&(i.line=n.shape.line),n.shape.rectRadius>0&&(i.rectRadius=n.shape.rectRadius),n.shape.shadow&&(i.shadow=n.shape.shadow),r.addText(n.text||"",i)}else if(n.type==="list"||n.type==="merged-text"){let i={x:n.position.x,y:n.position.y,w:Rs(n.position.x,n.position.w+n.position.w*.04),h:Math.min(n.position.h,Math.max(.15,Fu-n.position.y-.04)),fontSize:n.style.fontSize,fontFace:n.style.fontFace,color:n.style.color,align:n.style.align,valign:"top",lineSpacing:n.style.lineSpacing,paraSpaceBefore:n.style.paraSpaceBefore,paraSpaceAfter:n.style.paraSpaceAfter,margin:n.style.margin,inset:0,shrinkText:!1,autoFit:!1};n.style.transparency!=null&&(i.transparency=n.style.transparency),r.addText(n.items||n.text,i)}else{let i=n.style.lineSpacing||n.style.fontSize*1.2,a=n.position.h<=i*1.5,o=n.style.vert&&n.style.vert!=="horz",l=o?0:n.position.w*(a?.02:.06),c=n.position.x,s=Rs(n.position.x,n.position.w+l),u=n.style.align;!o&&u==="center"?c=n.position.x-(s-n.position.w)/2:!o&&u==="right"&&(c=n.position.x-(s-n.position.w)),c=Math.max(0,c),s=Rs(c,s);let d={x:c,y:n.position.y,w:s,h:Math.min(n.position.h,Math.max(.15,Fu-n.position.y-.04)),fontSize:n.style.fontSize,fontFace:n.style.fontFace,color:n.style.color,bold:n.style.bold,italic:n.style.italic,underline:n.style.underline,valign:o?"mid":n.style.valign||"top",lineSpacing:n.style.lineSpacing,paraSpaceBefore:n.style.paraSpaceBefore,paraSpaceAfter:n.style.paraSpaceAfter,inset:0,shrinkText:!1,autoFit:!1};n.style.align&&(d.align=n.style.align),n.style.margin&&(d.margin=n.style.margin),n.style.rotate!==void 0&&(d.rotate=n.style.rotate),n.style.vert&&(d.vert=n.style.vert),n.style.transparency!=null&&n.style.transparency!==void 0&&(d.transparency=n.style.transparency),r.addText(n.text,d)}}async function H0(e,r,t,n={}){let i=[];r?.errors?.length&&i.push(...r.errors),i.push(...j0(r,t)),i.push(...q0(e,r)),e?.errors?.length&&i.push(...e.errors),i.length&&console.warn("[ppt-live-export] slide validation warnings (export continues):",i.join("; "));let a=n.slide||t.addSlide();return await V0(e,a),X0(e,a,t),{slide:a,placeholders:e.placeholders||[]}}function Zd(e={}){let r=new W0;return r.layout="LAYOUT_WIDE",r.author="PPT Live",r.subject=e.brief?.topic||e.title||"PPT Live deck",r.title=e.title||"PPT Live",r.company="BitFun",r.lang="zh-CN",r.theme={headFontFace:"PingFang SC",bodyFontFace:"PingFang SC",lang:"zh-CN"},r}function Yd(e={}){return[e.notes,e.claim?`Claim: ${e.claim}`:"",e.proofObject?`Proof object: ${e.proofObject}`:"",e.supportNote?`Support note: ${e.supportNote}`:"",e.sourceNote?`Source note: ${e.sourceNote}`:""].filter(Boolean).join(` + `}function _0(e,r,t){for(let n=0;n`:'',o=!((n=e.theme)===null||n===void 0)&&n.bodyFontFace?``:'';return`${a}${o}`}function W0(e){let r=`${Wt}`;r+='',r+="",e.slides.forEach(t=>r+=``),r+="",r+=``,r+=``,r+=``,r+="";for(let t=1;t<10;t++)r+=``;return r+="",e.sections&&e.sections.length>0&&(r+='',r+='',e.sections.forEach(t=>{r+=``,t._slides.forEach(n=>r+=``),r+=""}),r+="",r+='',r+=""),r+="",r}function G0(){return`${Wt}`}function j0(){return`${Wt}`}function q0(){return`${Wt}`}var V0="4.0.1",X0=class{set layout(e){let r=this.LAYOUTS[e];if(r)this._layout=e,this._presLayout=r;else throw new Error("UNKNOWN-LAYOUT")}get layout(){return this._layout}get version(){return this._version}set author(e){this._author=e}get author(){return this._author}set company(e){this._company=e}get company(){return this._company}set revision(e){this._revision=e}get revision(){return this._revision}set subject(e){this._subject=e}get subject(){return this._subject}set theme(e){this._theme=e}get theme(){return this._theme}set title(e){this._title=e}get title(){return this._title}set rtlMode(e){this._rtlMode=e}get rtlMode(){return this._rtlMode}get masterSlide(){return this._masterSlide}get slides(){return this._slides}get sections(){return this._sections}get slideLayouts(){return this._slideLayouts}get AlignH(){return this._alignH}get AlignV(){return this._alignV}get ChartType(){return this._chartType}get OutputType(){return this._outputType}get presLayout(){return this._presLayout}get SchemeColor(){return this._schemeColor}get ShapeType(){return this._shapeType}get charts(){return this._charts}get colors(){return this._colors}get shapes(){return this._shapes}constructor(){this._version=V0,this._alignH=Ys,this._alignV=Js,this._chartType=Qs,this._outputType=Ks,this._schemeColor=or,this._shapeType=Zs,this._charts=Ce,this._colors=yo,this._shapes=xn,this.addNewSlide=i=>{let a=this.sections.length>0&&this.sections[this.sections.length-1]._slides.filter(o=>o._slideNum===this.slides[this.slides.length-1]._slideNum).length>0;return i.sectionTitle=a?this.sections[this.sections.length-1].title:null,this.addSlide(i)},this.getSlide=i=>this.slides.filter(a=>a._slideNum===i)[0],this.setSlideNumber=i=>{this.masterSlide._slideNumberProps=i,this.slideLayouts.filter(a=>a._name===Xs)[0]._slideNumberProps=i},this.createChartMediaRels=(i,a,o)=>{i._relsChart.forEach(l=>o.push(m0(l,a))),i._relsMedia.forEach(l=>{if(l.type!=="online"&&l.type!=="hyperlink"){let c=l.data&&typeof l.data=="string"?l.data:"";!c.includes(",")&&!c.includes(";")?c="image/png;base64,"+c:c.includes(",")?c.includes(";")||(c="image/png;"+c):c="image/png;base64,"+c,a.file(l.Target.replace("..","ppt"),c.split(",").pop(),{base64:!0})}})},this.writeFileToBrowser=(i,a)=>sr(this,void 0,void 0,function*(){let o=document.createElement("a");if(o.setAttribute("style","display:none;"),o.dataset.interception="off",document.body.appendChild(o),window.URL.createObjectURL){let l=window.URL.createObjectURL(new Blob([a],{type:"application/vnd.openxmlformats-officedocument.presentationml.presentation"}));return o.href=l,o.download=i,o.click(),setTimeout(()=>{window.URL.revokeObjectURL(l),document.body.removeChild(o)},100),yield Promise.resolve(i)}}),this.exportPresentation=i=>sr(this,void 0,void 0,function*(){let a=[],o=[],l=new _d.default;return this.slides.forEach(c=>{o=o.concat(Rs(c))}),this.slideLayouts.forEach(c=>{o=o.concat(Rs(c))}),o=o.concat(Rs(this.masterSlide)),yield Promise.all(o).then(()=>sr(this,void 0,void 0,function*(){return this.slides.forEach(c=>{c._slideLayout&&A0(c)}),l.folder("_rels"),l.folder("docProps"),l.folder("ppt").folder("_rels"),l.folder("ppt/charts").folder("_rels"),l.folder("ppt/embeddings"),l.folder("ppt/media"),l.folder("ppt/slideLayouts").folder("_rels"),l.folder("ppt/slideMasters").folder("_rels"),l.folder("ppt/slides").folder("_rels"),l.folder("ppt/theme"),l.folder("ppt/notesMasters").folder("_rels"),l.folder("ppt/notesSlides").folder("_rels"),l.file("[Content_Types].xml",S0(this.slides,this.slideLayouts,this.masterSlide)),l.file("_rels/.rels",k0()),l.file("docProps/app.xml",C0(this.slides,this.company)),l.file("docProps/core.xml",P0(this.title,this.subject,this.author,this.revision)),l.file("ppt/_rels/presentation.xml.rels",F0(this.slides)),l.file("ppt/theme/theme1.xml",U0(this)),l.file("ppt/presentation.xml",W0(this)),l.file("ppt/presProps.xml",G0()),l.file("ppt/tableStyles.xml",j0()),l.file("ppt/viewProps.xml",q0()),this.slideLayouts.forEach((c,s)=>{l.file(`ppt/slideLayouts/slideLayout${s+1}.xml`,R0(c)),l.file(`ppt/slideLayouts/_rels/slideLayout${s+1}.xml.rels`,N0(s+1,this.slideLayouts))}),this.slides.forEach((c,s)=>{l.file(`ppt/slides/slide${s+1}.xml`,T0(c)),l.file(`ppt/slides/_rels/slide${s+1}.xml.rels`,I0(this.slides,this.slideLayouts,s+1)),l.file(`ppt/notesSlides/notesSlide${s+1}.xml`,B0(c)),l.file(`ppt/notesSlides/_rels/notesSlide${s+1}.xml.rels`,z0(s+1))}),l.file("ppt/slideMasters/slideMaster1.xml",L0(this.masterSlide,this.slideLayouts)),l.file("ppt/slideMasters/_rels/slideMaster1.xml.rels",M0(this.masterSlide,this.slideLayouts)),l.file("ppt/notesMasters/notesMaster1.xml",E0()),l.file("ppt/notesMasters/_rels/notesMaster1.xml.rels",O0()),this.slideLayouts.forEach(c=>{this.createChartMediaRels(c,l,a)}),this.slides.forEach(c=>{this.createChartMediaRels(c,l,a)}),this.createChartMediaRels(this.masterSlide,l,a),yield Promise.all(a).then(()=>sr(this,void 0,void 0,function*(){return i.outputType==="STREAM"?yield l.generateAsync({type:"nodebuffer",compression:i.compression?"DEFLATE":"STORE"}):i.outputType?yield l.generateAsync({type:i.outputType}):yield l.generateAsync({type:"blob",compression:i.compression?"DEFLATE":"STORE"})}))}))});let e={name:"screen4x3",width:9144e3,height:6858e3},r={name:"screen16x9",width:9144e3,height:5143500},t={name:"screen16x10",width:9144e3,height:5715e3},n={name:"custom",width:12192e3,height:6858e3};this.LAYOUTS={LAYOUT_4x3:e,LAYOUT_16x9:r,LAYOUT_16x10:t,LAYOUT_WIDE:n},this._author="PptxGenJS",this._company="PptxGenJS",this._revision="1",this._subject="PptxGenJS Presentation",this._title="PptxGenJS Presentation",this._presLayout={name:this.LAYOUTS[ni].name,_sizeW:this.LAYOUTS[ni].width,_sizeH:this.LAYOUTS[ni].height,width:this.LAYOUTS[ni].width,height:this.LAYOUTS[ni].height},this._rtlMode=!1,this._slideLayouts=[{_margin:na,_name:Xs,_presLayout:this._presLayout,_rels:[],_relsChart:[],_relsMedia:[],_slide:null,_slideNum:1e3,_slideNumberProps:null,_slideObjects:[]}],this._slides=[],this._sections=[],this._masterSlide={addChart:null,addImage:null,addMedia:null,addNotes:null,addShape:null,addTable:null,addText:null,_name:null,_presLayout:this._presLayout,_rId:null,_rels:[],_relsChart:[],_relsMedia:[],_slideId:null,_slideLayout:null,_slideNum:null,_slideNumberProps:null,_slideObjects:[]}}stream(e){return sr(this,void 0,void 0,function*(){return yield this.exportPresentation({compression:e?.compression,outputType:"STREAM"})})}write(e){return sr(this,void 0,void 0,function*(){let r=typeof e=="object"&&e?.outputType?e.outputType:e||null,t=typeof e=="object"&&e?.compression?e.compression:!1;return yield this.exportPresentation({compression:t,outputType:r})})}writeFile(e){return sr(this,void 0,void 0,function*(){var r,t;let n=typeof process<"u"&&!!(!((r=process.versions)===null||r===void 0)&&r.node)&&((t=process.release)===null||t===void 0?void 0:t.name)==="node";typeof e=="string"&&(console.warn("[WARNING] writeFile(string) is deprecated - pass { fileName } instead."),e={fileName:e});let{fileName:i="Presentation.pptx",compression:a=!1}=e,o=i.toLowerCase().endsWith(".pptx")?i:`${i}.pptx`,l=n?"nodebuffer":null,c=yield this.exportPresentation({compression:a,outputType:l});if(n){let{promises:s}=yield Promise.resolve().then(()=>Qr(Mu())),{writeFile:u}=s;return yield u(o,c),o}return yield this.writeFileToBrowser(o,c),o})}addSection(e){e?e.title||console.warn("addSection requires a title"):console.warn("addSection requires an argument");let r={_type:"user",_slides:[],title:e.title};e.order?this.sections.splice(e.order,0,r):this._sections.push(r)}addSlide(e){let r=typeof e=="string"?e:e?.masterName?e.masterName:"",t={_name:this.LAYOUTS[ni].name,_presLayout:this.presLayout,_rels:[],_relsChart:[],_relsMedia:[],_slideNum:this.slides.length+1};if(r){let i=this.slideLayouts.filter(a=>a._name===r)[0];i&&(t=i)}let n=new g0({addSlide:this.addNewSlide,getSlide:this.getSlide,presLayout:this.presLayout,setSlideNum:this.setSlideNumber,slideId:this.slides.length+256,slideRId:this.slides.length+2,slideNumber:this.slides.length+1,slideLayout:t});if(this._slides.push(n),e?.sectionTitle){let i=this.sections.filter(a=>a.title===e.sectionTitle)[0];i?i._slides.push(n):console.warn(`addSlide: unable to find section with title: "${e.sectionTitle}"`)}else if(this.sections&&this.sections.length>0&&!e?.sectionTitle){let i=this._sections[this.sections.length-1];i._type==="default"?i._slides.push(n):this._sections.push({title:`Default-${this.sections.filter(a=>a._type==="default").length+1}`,_type:"default",_slides:[n]})}return n}defineLayout(e){e?e.name?e.width?e.height?typeof e.height!="number"?console.warn("defineLayout `height` should be a number (inches)"):typeof e.width!="number"&&console.warn("defineLayout `width` should be a number (inches)"):console.warn("defineLayout requires `height`"):console.warn("defineLayout requires `width`"):console.warn("defineLayout requires `name`"):console.warn("defineLayout requires `{name, width, height}`"),this.LAYOUTS[e.name]={name:e.name,_sizeW:Math.round(Number(e.width)*Ke),_sizeH:Math.round(Number(e.height)*Ke),width:Math.round(Number(e.width)*Ke),height:Math.round(Number(e.height)*Ke)}}defineSlideMaster(e){let r=JSON.parse(JSON.stringify(e));if(!r.title)throw new Error("defineSlideMaster() object argument requires a `title` value. (https://gitbrent.github.io/PptxGenJS/docs/masters.html)");let t={_margin:r.margin||na,_name:r.title,_presLayout:this.presLayout,_rels:[],_relsChart:[],_relsMedia:[],_slide:null,_slideNum:1e3+this.slideLayouts.length+1,_slideNumberProps:r.slideNumber||null,_slideObjects:[],background:r.background||null,bkgd:r.bkgd||null};d0(r,t),this.slideLayouts.push(t),(r.background||r.bkgd)&&Hd(r.background,t),t._slideNumberProps&&!this.masterSlide._slideNumberProps&&(this.masterSlide._slideNumberProps=t._slideNumberProps)}tableToSlides(e,r={}){c0(this,e,r,r?.masterSlideName?this.slideLayouts.filter(t=>t._name===r.masterSlideName)[0]:null)}},el=96,Pu=914400,H0=13.333,Fu=7.5;function Ls(e,r){return Math.min(r,Math.max(.15,H0-e-.04))}function Qd(e){let r=String(e||"").trim();return r?r.startsWith("data:")?{data:r}:r.startsWith("file://")?{path:r.replace("file://","")}:{path:r}:null}function K0(e,r){let t=[],n=e.width/el,i=e.height/el;if(r.presLayout){let a=r.presLayout.width/Pu,o=r.presLayout.height/Pu;(Math.abs(a-n)>.1||Math.abs(o-i)>.1)&&t.push(`HTML dimensions (${n.toFixed(1)}" \xD7 ${i.toFixed(1)}") don't match presentation layout (${a.toFixed(1)}" \xD7 ${o.toFixed(1)}")`)}return t}function Q0(e,r){let t=[],n=r.height/el,i=.5;for(let a of e.elements){if(!["p","h1","h2","h3","h4","h5","h6","text","list","merged-text"].includes(a.type))continue;let o=a.style?.fontSize||0,l=a.position.y+a.position.h,c=n-l,s=typeof a.text=="string"?a.text:Array.isArray(a.text)?a.text.find(d=>d.text)?.text||"":Array.isArray(a.items)&&a.items.find(d=>d.text)?.text||"";if(!(/^\d{1,2}\s*\/\s*\d{1,2}$/.test(s.trim())||/^(Arknights|数据来源|source:)/i.test(s.trim())||o<=11)&&o>12&&c50?"...":""}`;t.push(`Text box "${d}" ends too close to bottom edge (${c.toFixed(2)}" from bottom, minimum ${i}" required)`)}}return t}async function Z0(e,r){if(e.background?.type==="image"&&e.background.path){let t=Qd(e.background.path);t&&(r.background=t)}else e.background?.type==="color"&&e.background.value&&(r.background={color:e.background.value})}function Y0(e,r,t){for(let n of e.elements)if(n.type==="image"){let i=Qd(n.src);if(!i)continue;r.addImage({...i,x:n.position.x,y:n.position.y,w:n.position.w,h:n.position.h})}else if(n.type==="line")r.addShape(t.ShapeType.line,{x:n.x1,y:n.y1,w:n.x2-n.x1,h:n.y2-n.y1,line:{color:n.color,width:n.width}});else if(n.type==="shape"){let i={x:n.position.x,y:n.position.y,w:n.position.w,h:n.position.h,shape:n.shape.rectRadius>0?t.ShapeType.roundRect:t.ShapeType.rect};n.shape.fill&&(i.fill={color:n.shape.fill},n.shape.transparency!=null&&(i.fill.transparency=n.shape.transparency)),n.shape.line&&(i.line=n.shape.line),n.shape.rectRadius>0&&(i.rectRadius=n.shape.rectRadius),n.shape.shadow&&(i.shadow=n.shape.shadow),r.addText(n.text||"",i)}else if(n.type==="list"||n.type==="merged-text"){let i={x:n.position.x,y:n.position.y,w:Ls(n.position.x,n.position.w+n.position.w*.04),h:Math.min(n.position.h,Math.max(.15,Fu-n.position.y-.04)),fontSize:n.style.fontSize,fontFace:n.style.fontFace,color:n.style.color,align:n.style.align,valign:"top",lineSpacing:n.style.lineSpacing,paraSpaceBefore:n.style.paraSpaceBefore,paraSpaceAfter:n.style.paraSpaceAfter,margin:n.style.margin,inset:0,shrinkText:!1,autoFit:!1};n.style.transparency!=null&&(i.transparency=n.style.transparency),r.addText(n.items||n.text,i)}else{let i=n.style.lineSpacing||n.style.fontSize*1.2,a=n.position.h<=i*1.5,o=n.style.vert&&n.style.vert!=="horz",l=o?0:n.position.w*(a?.02:.06),c=n.position.x,s=Ls(n.position.x,n.position.w+l),u=n.style.align;!o&&u==="center"?c=n.position.x-(s-n.position.w)/2:!o&&u==="right"&&(c=n.position.x-(s-n.position.w)),c=Math.max(0,c),s=Ls(c,s);let d={x:c,y:n.position.y,w:s,h:Math.min(n.position.h,Math.max(.15,Fu-n.position.y-.04)),fontSize:n.style.fontSize,fontFace:n.style.fontFace,color:n.style.color,bold:n.style.bold,italic:n.style.italic,underline:n.style.underline,valign:o?"mid":n.style.valign||"top",lineSpacing:n.style.lineSpacing,paraSpaceBefore:n.style.paraSpaceBefore,paraSpaceAfter:n.style.paraSpaceAfter,inset:0,shrinkText:!1,autoFit:!1};n.style.align&&(d.align=n.style.align),n.style.margin&&(d.margin=n.style.margin),n.style.rotate!==void 0&&(d.rotate=n.style.rotate),n.style.vert&&(d.vert=n.style.vert),n.style.transparency!=null&&n.style.transparency!==void 0&&(d.transparency=n.style.transparency),r.addText(n.text,d)}}async function J0(e,r,t,n={}){let i=[];r?.errors?.length&&i.push(...r.errors),i.push(...K0(r,t)),i.push(...Q0(e,r)),e?.errors?.length&&i.push(...e.errors),i.length&&console.warn("[ppt-live-export] slide validation warnings (export continues):",i.join("; "));let a=n.slide||t.addSlide();return await Z0(e,a),Y0(e,a,t),{slide:a,placeholders:e.placeholders||[]}}function Zd(e={}){let r=new X0;return r.layout="LAYOUT_WIDE",r.author="PPT Live",r.subject=e.brief?.topic||e.title||"PPT Live deck",r.title=e.title||"PPT Live",r.company="BitFun",r.lang="zh-CN",r.theme={headFontFace:"PingFang SC",bodyFontFace:"PingFang SC",lang:"zh-CN"},r}function Yd(e={}){return[e.notes,e.claim?`Claim: ${e.claim}`:"",e.proofObject?`Proof object: ${e.proofObject}`:"",e.supportNote?`Support note: ${e.supportNote}`:"",e.sourceNote?`Source note: ${e.sourceNote}`:""].filter(Boolean).join(` -`)}var el=13.333,yo=7.5;function K0(e,r,t,n){r.addShape(e.ShapeType.rect,{x:0,y:0,w:el,h:yo,fill:{color:kt(t.background)},line:{color:kt(t.background),transparency:100}}),r.addShape(e.ShapeType.ellipse,{x:10.3,y:-.45,w:3.8,h:3.8,fill:{color:kt(n%2?t.accent:t.primary),transparency:84},line:{color:kt(t.background),transparency:100}}),r.addShape(e.ShapeType.rect,{x:0,y:0,w:.12,h:yo,fill:{color:kt(t.primary),transparency:0},line:{color:kt(t.primary),transparency:100}})}function Q0(e,r,t,n){t.kicker&&(r.addShape(e.ShapeType.roundRect,{x:.96,y:.48,w:.22,h:.07,fill:{color:kt(n.primary)},line:{color:kt(n.primary),transparency:100}}),r.addText(String(t.kicker).toUpperCase(),{x:1.24,y:.36,w:2.4,h:.28,margin:0,fontFace:"Aptos",fontSize:7,bold:!0,color:kt(n.primary),fit:"shrink"})),t.proofObject&&r.addText(String(t.proofObject),{x:9.26,y:.34,w:2.95,h:.32,margin:.04,fontFace:"Aptos",fontSize:7,bold:!0,color:kt(n.muted),align:"right",fit:"shrink",fill:{color:kt(n.panel),transparency:8},line:{color:kt(n.primary),transparency:78}}),t.sourceNote&&r.addText(String(t.sourceNote),{x:.96,y:7.05,w:10.7,h:.2,margin:0,fontFace:"Aptos",fontSize:6,color:kt(n.muted),fit:"shrink"})}function Z0(e,r,t,n){let i=ey(t),a=t.style||{},o={x:i.x,y:i.y,w:i.w,h:i.h,margin:.08,fit:"shrink",color:kt(pn(a.color,n)),fontFace:"Aptos",fontSize:Du(a.fontSize||22),bold:Number(a.fontWeight||500)>=700,align:a.align||"left",valign:"mid",breakLine:!1};if(t.type==="shape"){r.addShape(e.ShapeType.roundRect,{...i,rectRadius:.08,fill:{color:kt(pn(a.background,n)),transparency:Tl(a.opacity)},line:{color:kt(pn(a.background,n)),transparency:100}}),t.text&&r.addText(t.text,o);return}if(t.type==="list"){let l=(t.items||[]).map(c=>({text:c,options:{bullet:{type:"bullet"},breakLine:!0}}));r.addText(l.length?l:[{text:""}],{...o,valign:"top",paraSpaceAfterPt:6,fit:"shrink"});return}if(t.type==="metric"){Tu(e,r,i,a,n),r.addText(String(t.text||""),{...o,y:i.y+.08,h:i.h*.48,color:kt(pn(a.color||"primary",n)),fontSize:Du(a.fontSize||42),bold:!0}),r.addText(String(t.label||""),{...o,y:i.y+i.h*.56,h:i.h*.34,color:kt(n.muted),fontSize:10,bold:!1,valign:"top"});return}if(t.type==="chart"){Tu(e,r,i,a,n),r.addText(String(t.text||""),{...o,y:i.y+.1,h:.32,fontSize:11,bold:!0}),J0(e,r,t,i,n);return}if(t.type==="media"){r.addShape(e.ShapeType.roundRect,{...i,fill:{color:kt(pn(a.background||"soft",n)),transparency:10},line:{color:kt(n.primary),transparency:55,dash:"dash"}}),r.addText(String(t.text||"Image placeholder"),{...o,align:"center",color:kt(n.muted),fontSize:12});return}Y0(e,r,i,a,n),r.addText(String(t.text||""),o)}function Tu(e,r,t,n,i){r.addShape(e.ShapeType.roundRect,{...t,fill:{color:kt(pn(n.background||"panel",i)),transparency:Tl(n.opacity)},line:{color:kt(i.primary),transparency:82},shadow:{type:"outer",color:"111827",opacity:.12,blur:1,angle:45,distance:1}})}function Y0(e,r,t,n,i){let a=n.background||"transparent";a!=="transparent"&&r.addShape(e.ShapeType.roundRect,{...t,fill:{color:kt(pn(a,i)),transparency:Tl(n.opacity)},line:{color:kt(pn(a,i)),transparency:100}})}function J0(e,r,t,n,i){let a=Array.isArray(t.data)&&t.data.length?t.data:[{label:"A",value:40},{label:"B",value:70}],o=Math.max(1,...a.map(f=>Number(f.value)||0)),l=.1,c=n.x+.18,s=n.y+.68,u=n.w-.36,d=n.h-.95,A=Math.max(.08,(u-l*(a.length-1))/a.length);a.forEach((f,p)=>{let g=Number(f.value)||0,y=Math.max(.15,g/o*d),v=c+p*(A+l),b=s+d-y;r.addShape(e.ShapeType.rect,{x:v,y:b,w:A,h:y,fill:{color:kt(p%2?i.accent:i.primary)},line:{color:kt(p%2?i.accent:i.primary),transparency:100}}),r.addText(String(f.label||""),{x:v-.03,y:s+d+.04,w:A+.06,h:.2,fontSize:7,color:kt(i.muted),align:"center",margin:0,fit:"shrink"})})}function $0(e={}){return{background:e.background||"#fbfcff",ink:e.ink||"#111827",muted:e.muted||"#5b6575",primary:e.primary||"#0f766e",accent:e.accent||"#f97316",panel:e.panel||"#ffffff"}}function ey(e){return{x:Ha(e.x)*el,y:Ha(e.y)*yo,w:Ha(e.w)*el,h:Ha(e.h)*yo}}function Ha(e){return Math.max(0,Math.min(100,Number(e)||0))/100}function Du(e){return Math.max(6,Math.min(66,Math.round((Number(e)||22)*.58)))}function pn(e,r){return!e||e==="transparent"?r.background:e==="ink"?r.ink:e==="muted"?r.muted:e==="primary"?r.primary:e==="accent"?r.accent:e==="panel"?r.panel:e==="soft"?r.primary:e==="background"?r.background:e}function kt(e){let r=String(e||"#111827").trim();return/^#[0-9a-f]{6}$/i.test(r)?r.slice(1).toUpperCase():/^[0-9a-f]{6}$/i.test(r)?r.toUpperCase():/^#[0-9a-f]{3}$/i.test(r)?r.slice(1).split("").map(t=>t+t).join("").toUpperCase():"111827"}function Tl(e){return Math.round((1-Math.max(0,Math.min(1,Number(e??1))))*100)}async function ty(e){let r=Array.isArray(e.slides)&&e.slides.length>0?e.slides:[],t=Zd(e);return r.forEach((n,i)=>{let a=t.addSlide(),o=$0(n.theme);a.background={color:kt(o.background)},K0(t,a,o,i),Q0(t,a,n,o),(n.elements||[]).forEach(c=>Z0(t,a,c,o));let l=Yd(n);l&&typeof a.addNotes=="function"&&a.addNotes(l)}),t}var ry="application/vnd.openxmlformats-officedocument.presentationml.presentation",Jd=new Set(["p","h1","h2","h3","h4","h5","h6","text","list","merged-text"]);function ny(e){return{...e,elements:(e.elements||[]).filter(r=>Jd.has(r.type))}}function iy(e){let t="";for(let n=0;n|]+/g,"-").slice(0,96)}async function $d(e,r){let t=await e.write({outputType:"base64"});return{filename:`${Dl(r.title||"ppt-live")}.pptx`,mimeType:ry,base64:String(t||"").replace(/^data:.*;base64,/,"")}}async function El(e){if((Array.isArray(e?.slides)?e.slides:[]).some(n=>n?.html))throw new Error("HTML slides must use the WebView prepare export path.");let t=await ty(e);return $d(t,e)}async function Bl(e,r){let t=Array.isArray(r)?r:[];if(!t.length)throw new Error("No prepared slides to export");let n=Zd(e),i=Array.isArray(e?.slides)?e.slides:[];for(let a of t){let o=i[a.index]||a.notes||{},l=a.slideData;if(a.rasterBase64){let u=String(a.rasterBase64).replace(/^data:.*;base64,/,"");a.rasterOnly?l={...l,elements:(l.elements||[]).filter(d=>!Jd.has(d.type)),background:{type:"image",path:`data:image/png;base64,${u}`}}:(l=ny(l),l={...l,background:{type:"image",path:`data:image/png;base64,${u}`}})}let c=await H0(l,a.bodyDimensions,n),s=Yd(o);s&&c?.slide&&typeof c.slide.addNotes=="function"&&c.slide.addNotes(s)}return $d(n,e)}async function Rl(e,r){let t=Array.isArray(r)?r:[];if(!t.length)throw new Error("No rendered PDF pages to export");let n=await Zr.create();for(let a of t){let o=String(a||"").replace(/^data:.*;base64,/,""),l=Uint8Array.from(atob(o),u=>u.charCodeAt(0)),c=await Zr.load(l);(await n.copyPages(c,c.getPageIndices())).forEach(u=>n.addPage(u))}let i=await n.save();return{filename:`${Dl(e?.title||"ppt-live")}.pdf`,mimeType:"application/pdf",base64:iy(i)}}async function Ll(e,r){let t=Array.isArray(r)?r:[];if(!t.length)throw new Error("No rendered PNG pages to export");let n=new Kv.default;t.forEach((a,o)=>{let l=typeof a=="string"?a:String(a?.base64||"").replace(/^data:.*;base64,/,""),c=(a?.index??o)+1;n.file(`slide-${String(c).padStart(2,"0")}.png`,l,{base64:!0})});let i=await n.generateAsync({type:"base64",compression:"DEFLATE"});return{filename:`${Dl(e?.title||"ppt-live")}-slides.zip`,mimeType:"application/zip",base64:String(i||"").replace(/^data:.*;base64,/,"")}}function ay(e){if((e.slides||[]).some(t=>t.html))return oy(e);let r=e.slides.map(t=>`
          ${un(t)}
          `).join(` +`)}var tl=13.333,wo=7.5;function $0(e,r,t,n){r.addShape(e.ShapeType.rect,{x:0,y:0,w:tl,h:wo,fill:{color:kt(t.background)},line:{color:kt(t.background),transparency:100}}),r.addShape(e.ShapeType.ellipse,{x:10.3,y:-.45,w:3.8,h:3.8,fill:{color:kt(n%2?t.accent:t.primary),transparency:84},line:{color:kt(t.background),transparency:100}}),r.addShape(e.ShapeType.rect,{x:0,y:0,w:.12,h:wo,fill:{color:kt(t.primary),transparency:0},line:{color:kt(t.primary),transparency:100}})}function ey(e,r,t,n){t.kicker&&(r.addShape(e.ShapeType.roundRect,{x:.96,y:.48,w:.22,h:.07,fill:{color:kt(n.primary)},line:{color:kt(n.primary),transparency:100}}),r.addText(String(t.kicker).toUpperCase(),{x:1.24,y:.36,w:2.4,h:.28,margin:0,fontFace:"Aptos",fontSize:7,bold:!0,color:kt(n.primary),fit:"shrink"})),t.proofObject&&r.addText(String(t.proofObject),{x:9.26,y:.34,w:2.95,h:.32,margin:.04,fontFace:"Aptos",fontSize:7,bold:!0,color:kt(n.muted),align:"right",fit:"shrink",fill:{color:kt(n.panel),transparency:8},line:{color:kt(n.primary),transparency:78}}),t.sourceNote&&r.addText(String(t.sourceNote),{x:.96,y:7.05,w:10.7,h:.2,margin:0,fontFace:"Aptos",fontSize:6,color:kt(n.muted),fit:"shrink"})}function ty(e,r,t,n){let i=ay(t),a=t.style||{},o={x:i.x,y:i.y,w:i.w,h:i.h,margin:.08,fit:"shrink",color:kt(pn(a.color,n)),fontFace:"Aptos",fontSize:Du(a.fontSize||22),bold:Number(a.fontWeight||500)>=700,align:a.align||"left",valign:"mid",breakLine:!1};if(t.type==="shape"){r.addShape(e.ShapeType.roundRect,{...i,rectRadius:.08,fill:{color:kt(pn(a.background,n)),transparency:Dl(a.opacity)},line:{color:kt(pn(a.background,n)),transparency:100}}),t.text&&r.addText(t.text,o);return}if(t.type==="list"){let l=(t.items||[]).map(c=>({text:c,options:{bullet:{type:"bullet"},breakLine:!0}}));r.addText(l.length?l:[{text:""}],{...o,valign:"top",paraSpaceAfterPt:6,fit:"shrink"});return}if(t.type==="metric"){Tu(e,r,i,a,n),r.addText(String(t.text||""),{...o,y:i.y+.08,h:i.h*.48,color:kt(pn(a.color||"primary",n)),fontSize:Du(a.fontSize||42),bold:!0}),r.addText(String(t.label||""),{...o,y:i.y+i.h*.56,h:i.h*.34,color:kt(n.muted),fontSize:10,bold:!1,valign:"top"});return}if(t.type==="chart"){Tu(e,r,i,a,n),r.addText(String(t.text||""),{...o,y:i.y+.1,h:.32,fontSize:11,bold:!0}),ny(e,r,t,i,n);return}if(t.type==="media"){r.addShape(e.ShapeType.roundRect,{...i,fill:{color:kt(pn(a.background||"soft",n)),transparency:10},line:{color:kt(n.primary),transparency:55,dash:"dash"}}),r.addText(String(t.text||"Image placeholder"),{...o,align:"center",color:kt(n.muted),fontSize:12});return}ry(e,r,i,a,n),r.addText(String(t.text||""),o)}function Tu(e,r,t,n,i){r.addShape(e.ShapeType.roundRect,{...t,fill:{color:kt(pn(n.background||"panel",i)),transparency:Dl(n.opacity)},line:{color:kt(i.primary),transparency:82},shadow:{type:"outer",color:"111827",opacity:.12,blur:1,angle:45,distance:1}})}function ry(e,r,t,n,i){let a=n.background||"transparent";a!=="transparent"&&r.addShape(e.ShapeType.roundRect,{...t,fill:{color:kt(pn(a,i)),transparency:Dl(n.opacity)},line:{color:kt(pn(a,i)),transparency:100}})}function ny(e,r,t,n,i){let a=Array.isArray(t.data)&&t.data.length?t.data:[{label:"A",value:40},{label:"B",value:70}],o=Math.max(1,...a.map(f=>Number(f.value)||0)),l=.1,c=n.x+.18,s=n.y+.68,u=n.w-.36,d=n.h-.95,A=Math.max(.08,(u-l*(a.length-1))/a.length);a.forEach((f,p)=>{let g=Number(f.value)||0,y=Math.max(.15,g/o*d),v=c+p*(A+l),b=s+d-y;r.addShape(e.ShapeType.rect,{x:v,y:b,w:A,h:y,fill:{color:kt(p%2?i.accent:i.primary)},line:{color:kt(p%2?i.accent:i.primary),transparency:100}}),r.addText(String(f.label||""),{x:v-.03,y:s+d+.04,w:A+.06,h:.2,fontSize:7,color:kt(i.muted),align:"center",margin:0,fit:"shrink"})})}function iy(e={}){return{background:e.background||"#fbfcff",ink:e.ink||"#111827",muted:e.muted||"#5b6575",primary:e.primary||"#0f766e",accent:e.accent||"#f97316",panel:e.panel||"#ffffff"}}function ay(e){return{x:Qa(e.x)*tl,y:Qa(e.y)*wo,w:Qa(e.w)*tl,h:Qa(e.h)*wo}}function Qa(e){return Math.max(0,Math.min(100,Number(e)||0))/100}function Du(e){return Math.max(6,Math.min(66,Math.round((Number(e)||22)*.58)))}function pn(e,r){return!e||e==="transparent"?r.background:e==="ink"?r.ink:e==="muted"?r.muted:e==="primary"?r.primary:e==="accent"?r.accent:e==="panel"?r.panel:e==="soft"?r.primary:e==="background"?r.background:e}function kt(e){let r=String(e||"#111827").trim();return/^#[0-9a-f]{6}$/i.test(r)?r.slice(1).toUpperCase():/^[0-9a-f]{6}$/i.test(r)?r.toUpperCase():/^#[0-9a-f]{3}$/i.test(r)?r.slice(1).split("").map(t=>t+t).join("").toUpperCase():"111827"}function Dl(e){return Math.round((1-Math.max(0,Math.min(1,Number(e??1))))*100)}async function oy(e){let r=Array.isArray(e.slides)&&e.slides.length>0?e.slides:[],t=Zd(e);return r.forEach((n,i)=>{let a=t.addSlide(),o=iy(n.theme);a.background={color:kt(o.background)},$0(t,a,o,i),ey(t,a,n,o),(n.elements||[]).forEach(c=>ty(t,a,c,o));let l=Yd(n);l&&typeof a.addNotes=="function"&&a.addNotes(l)}),t}var sy="application/vnd.openxmlformats-officedocument.presentationml.presentation",Jd=new Set(["p","h1","h2","h3","h4","h5","h6","text","list","merged-text"]);function ly(e){return{...e,elements:(e.elements||[]).filter(r=>Jd.has(r.type))}}function cy(e){let t="";for(let n=0;n|]+/g,"-").slice(0,96)}async function $d(e,r){let t=await e.write({outputType:"base64"});return{filename:`${El(r.title||"ppt-live")}.pptx`,mimeType:sy,base64:String(t||"").replace(/^data:.*;base64,/,"")}}async function Bl(e){if((Array.isArray(e?.slides)?e.slides:[]).some(n=>n?.html))throw new Error("HTML slides must use the WebView prepare export path.");let t=await oy(e);return $d(t,e)}async function Rl(e,r){let t=Array.isArray(r)?r:[];if(!t.length)throw new Error("No prepared slides to export");let n=Zd(e),i=Array.isArray(e?.slides)?e.slides:[];for(let a of t){let o=i[a.index]||a.notes||{},l=a.slideData;if(a.rasterBase64){let u=String(a.rasterBase64).replace(/^data:.*;base64,/,"");a.rasterOnly?l={...l,elements:(l.elements||[]).filter(d=>!Jd.has(d.type)),background:{type:"image",path:`data:image/png;base64,${u}`}}:(l=ly(l),l={...l,background:{type:"image",path:`data:image/png;base64,${u}`}})}let c=await J0(l,a.bodyDimensions,n),s=Yd(o);s&&c?.slide&&typeof c.slide.addNotes=="function"&&c.slide.addNotes(s)}return $d(n,e)}async function Ll(e,r){let t=Array.isArray(r)?r:[];if(!t.length)throw new Error("No rendered PDF pages to export");let n=await Zr.create();for(let a of t){let o=String(a||"").replace(/^data:.*;base64,/,""),l=Uint8Array.from(atob(o),u=>u.charCodeAt(0)),c=await Zr.load(l);(await n.copyPages(c,c.getPageIndices())).forEach(u=>n.addPage(u))}let i=await n.save();return{filename:`${El(e?.title||"ppt-live")}.pdf`,mimeType:"application/pdf",base64:cy(i)}}async function Nl(e,r){let t=Array.isArray(r)?r:[];if(!t.length)throw new Error("No rendered PNG pages to export");let n=new $v.default;t.forEach((a,o)=>{let l=typeof a=="string"?a:String(a?.base64||"").replace(/^data:.*;base64,/,""),c=(a?.index??o)+1;n.file(`slide-${String(c).padStart(2,"0")}.png`,l,{base64:!0})});let i=await n.generateAsync({type:"base64",compression:"DEFLATE"});return{filename:`${El(e?.title||"ppt-live")}-slides.zip`,mimeType:"application/zip",base64:String(i||"").replace(/^data:.*;base64,/,"")}}function uy(e){if((e.slides||[]).some(t=>t.html))return dy(e);let r=e.slides.map(t=>`
          ${un(t)}
          `).join(` `);return` - + ${Le(e.title||"PPT Live")}
          ${r}
          -`}function oy(e){let r=(e.slides||[]).map((t,n)=>`
          - +`}function dy(e){let r=(e.slides||[]).map((t,n)=>`
          +
          `).join(` `);return` - + @@ -300,7 +340,7 @@ html,body{margin:0;background:#111;color:#fff;font-family:system-ui,-apple-syste
          ${r}
          -`}function Nl(e){let r=new Blob([ay(e)],{type:"text/html;charset=utf-8"}),t=URL.createObjectURL(r),n=document.createElement("a"),i=`${Il(e.title||"ppt-live")}.html`;return n.href=t,n.download=i,document.body.append(n),n.click(),n.remove(),URL.revokeObjectURL(t),i}function ef(e,r,t){let n=atob(e),i=new Uint8Array(n.length);for(let c=0;c|]+/g,"-").slice(0,96)}function sy(){return` +`}function Il(e){let r=new Blob([uy(e)],{type:"text/html;charset=utf-8"}),t=URL.createObjectURL(r),n=document.createElement("a"),i=`${zl(e.title||"ppt-live")}.html`;return n.href=t,n.download=i,document.body.append(n),n.click(),n.remove(),URL.revokeObjectURL(t),i}function ef(e,r,t){let n=atob(e),i=new Uint8Array(n.length);for(let c=0;c|]+/g,"-").slice(0,96)}function fy(){return` body{margin:0;background:#111827;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif} .deck{display:grid;gap:24px;padding:24px}.deck-slide{display:grid;place-items:center;min-height:100vh} .slide{position:relative;width:min(100%,1280px);aspect-ratio:16/9;color:var(--slide-ink);background:var(--slide-bg);overflow:hidden;box-shadow:0 24px 70px rgba(0,0,0,.34)} @@ -311,7 +351,7 @@ body{margin:0;background:#111827;font-family:-apple-system,BlinkMacSystemFont,"S .element-metric strong{font-size:inherit;line-height:.95}.element-metric span{color:var(--slide-muted);font-size:14px;line-height:1.25} .element-media{display:grid;place-items:center;border:1px dashed color-mix(in srgb,var(--slide-primary) 40%,transparent)} .element-chart{display:flex;flex-direction:column;gap:10px}.element-chart b{font-size:16px}.chart-bars{display:flex;align-items:end;gap:8px;flex:1;min-height:0}.chart-bars span{display:flex;flex:1;height:100%;align-items:end;gap:4px;flex-direction:column}.chart-bars i{display:block;width:100%;border-radius:5px 5px 0 0;background:var(--slide-primary)}.chart-bars em{font-size:10px;color:var(--slide-muted);font-style:normal} -`}var Oo='xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" aria-hidden="true" focusable="false"',tf={pptx:``,pdf:`PDF`,html:``,png:``};function rf(e){return tf[e]||tf.html}function nf(e){return{pptx:"#d44726",pdf:"#e01e3c",html:"#0d9488",png:"#2563eb"}[e]||"#475569"}var zl=new Set,af="user::bitfun-system::ppt-design";function ly(e){zl.forEach(r=>{try{r(e)}catch{}})}function cy(e){try{return JSON.stringify(e??{},null,2)}catch{return"{}"}}function uy(e){return Array.isArray(e?.currentDeck?.slides)&&e.currentDeck.slides.length>0}function dy(e={}){let r=[],t=e.fontFamily;t==="serif"?r.push("\u886C\u7EBF\u5B57\u4F53"):t==="sans"&&r.push("\u975E\u886C\u7EBF\u5B57\u4F53");let n=e.density==="loose"?"spacious":e.density;return n==="compact"?r.push("\u7D27\u51D1\u4FE1\u606F\u5BC6\u5EA6"):n==="spacious"&&r.push("\u5BBD\u677E\u7559\u767D"),(e.colorMode||e.theme)==="dark"&&r.push("\u6DF1\u8272\u4E3B\u9898"),e.stylePreset&&r.push(`\u98CE\u683C\u9884\u8BBE: ${e.stylePreset}`),r.length?r.join("\u3001"):""}function fy(e){let r=uy(e),t=dy(e?.style),n=e?.instruction||e?.userInput||"",i=r?`\u4F7F\u7528 PPT-Design skill \u7F16\u8F91\u73B0\u6709 PPT\u3002\u7F16\u8F91\u6307\u4EE4\uFF1A${n||"\uFF08\u89C1 currentDeck \u4E0A\u4E0B\u6587\uFF09"}\u3002`:`\u4F7F\u7528 PPT-Design skill \u751F\u6210 PPT\u3002\u7528\u6237\u9700\u6C42\uFF1A${n||"\uFF08\u89C1 input JSON\uFF09"}\u3002`;return t&&(i+=` +`}var Uo='xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" aria-hidden="true" focusable="false"',tf={pptx:``,pdf:`PDF`,html:``,png:``};function rf(e){return tf[e]||tf.html}function nf(e){return{pptx:"#d44726",pdf:"#e01e3c",html:"#0d9488",png:"#2563eb"}[e]||"#475569"}var Ml=new Set,af="user::bitfun-system::ppt-design";function hy(e){Ml.forEach(r=>{try{r(e)}catch{}})}function py(e){try{return JSON.stringify(e??{},null,2)}catch{return"{}"}}function Ay(e){return Array.isArray(e?.currentDeck?.slides)&&e.currentDeck.slides.length>0}function gy(e={}){let r=[],t=e.fontFamily;t==="serif"?r.push("\u886C\u7EBF\u5B57\u4F53"):t==="sans"&&r.push("\u975E\u886C\u7EBF\u5B57\u4F53");let n=e.density==="loose"?"spacious":e.density;return n==="compact"?r.push("\u7D27\u51D1\u4FE1\u606F\u5BC6\u5EA6"):n==="spacious"&&r.push("\u5BBD\u677E\u7559\u767D"),(e.colorMode||e.theme)==="dark"&&r.push("\u6DF1\u8272\u4E3B\u9898"),e.stylePreset&&r.push(`\u98CE\u683C\u9884\u8BBE: ${e.stylePreset}`),r.length?r.join("\u3001"):""}function my(e){let r=Ay(e),t=gy(e?.style),n=e?.instruction||e?.userInput||"",i=r?`\u4F7F\u7528 PPT-Design skill \u7F16\u8F91\u73B0\u6709 PPT\u3002\u7F16\u8F91\u6307\u4EE4\uFF1A${n||"\uFF08\u89C1 currentDeck \u4E0A\u4E0B\u6587\uFF09"}\u3002`:`\u4F7F\u7528 PPT-Design skill \u751F\u6210 PPT\u3002\u7528\u6237\u9700\u6C42\uFF1A${n||"\uFF08\u89C1 input JSON\uFF09"}\u3002`;return t&&(i+=` \u6837\u5F0F\u504F\u597D\uFF1A${t}\u3002`),i+=` ## \u7EA6\u675F @@ -319,24 +359,25 @@ body{margin:0;background:#111827;font-family:-apple-system,BlinkMacSystemFont,"S - \u7528\u6237\u53EA\u80FD\u770B\u5230 PPT Live UI\uFF0C\u65E0\u6CD5\u56DE\u7B54\u63D0\u95EE\u3002\u5982\u6709\u6B67\u4E49\u81EA\u884C\u5224\u65AD\u6700\u4F18\u65B9\u6848\u5E76\u8BB0\u5F55\u5047\u8BBE\u3002 - \u4E0D\u8981\u8C03\u7528 AskUserQuestion\u3001ControlHub\u3001GenerativeUI\u3001ComputerUse \u7B49\u4EA4\u4E92\u5DE5\u5177\u3002 - \u7814\u7A76\u7528 WebSearch / WebFetch \u5373\u53EF\u3002 +- **\u4E00\u6B21\u5199\u5BF9\uFF0C\u7981\u6B62\u4E8B\u540E\u5BA1\u8BA1**\uFF1A\u6BCF\u9875 HTML \u5728\u5199\u5165\u65F6\u5C31\u8981\u6EE1\u8DB3\u6240\u6709\u7EA6\u675F\uFF08\u753B\u5E03\u5C3A\u5BF8\u3001\u56DB\u6761 OOXML \u786C\u7EA6\u675F\u3001\u9632\u6EA2\u51FA\u9884\u7B97\uFF09\u3002\u6240\u6709\u9875\u9762\u5199\u5B8C\u540E\u4E0D\u5F97\u518D\u9010\u9875 Read\u2192Edit \u8FD4\u5DE5\u6216 Grep \u6279\u91CF\u68C0\u67E5\u3002\u5199\u5B8C\u5373\u7ED3\u675F\u3002 `,r&&(i+="\n## \u7F16\u8F91\u4E0A\u4E0B\u6587\n\n- `currentDeck` \u5DF2\u63D0\u4F9B\u3002\u5C06\u7528\u6237\u6307\u4EE4\u89C6\u4E3A\u5BF9\u73B0\u6709 deck \u7684\u589E\u91CF\u7F16\u8F91\uFF0C\u9664\u975E\u6307\u4EE4\u660E\u786E\u8981\u6C42\u5168\u65B0\u751F\u6210\u3002\n- `currentDeck.slides[].slideNumber` \u662F\u4ECE 1 \u5F00\u59CB\u7684\u9875\u7801\uFF0C\u4E0E\u7528\u6237\u53E3\u8BED\u4E00\u81F4\u3002\n- \u7F16\u8F91\u65F6\u53EA\u91CD\u5199\u53D8\u66F4\u7684 `slides/slide-NN.html` \u6587\u4EF6\uFF0C\u4E0D\u52A8\u5176\u4ED6\u9875\u3002\n"),i+=` Input JSON: \`\`\`json -${cy(e)} +${py(e)} \`\``,e?.continueAfterInterruption&&(i=`\u4E0A\u4E00\u6B21\u751F\u6210\u88AB\u4E2D\u65AD\u4E86\u3002\u8BF7\u7EE7\u7EED\u5B8C\u6210\u4EFB\u52A1\uFF1A\u68C0\u67E5 project.json \u548C\u5DF2\u5199\u7684 slides/ \u6587\u4EF6\uFF0C\u53EA\u8865\u5199\u8FD8\u6CA1\u5B8C\u6210\u7684\u9875\u9762\uFF0C\u4E0D\u8981\u91CD\u5199\u5DF2\u6709\u7684\u9875\u9762\u3002 -${i}`),i}function hy(e){let r=!1,t=()=>{r||(r=!0,e.agent.onEvent(n=>{!n||typeof n!="object"||ly(n)}))};e.backend={protocol:"files",async call(n,i,a={}){if(n!=="ppt.generate")throw new Error(`Unsupported PPT Live action: ${n}`);t();let o=fy(i),l=await e.agent.run(o,{runId:a.idempotencyKey,sessionName:"PPT Live",sessionId:a.sessionId,appDataWorkspace:a.appDataWorkspace});if(!l?.sessionId||!l?.turnId)throw new Error("PPT Live agent backend did not return sessionId/turnId");return{sessionId:l.sessionId,turnId:l.turnId,actionRunId:l.actionRunId||l.turnId}},onEvent(n){zl.add(n)},offEvent(n){zl.delete(n)},async cancel(n,i){await e.agent.cancel(n,i)},async turnText(n,i){return{text:(await e.agent.turnText(n,i))?.text||""}},async cancelStaleRuns(){await e.agent.cancelStaleRuns()}}}function of(e=window.app){!e||e.backend?.call||e.agent?.run&&hy(e)}var V=Bn(),Fn=!1,Fr=null,Tn=[],Xn=0,Uo=!1,pa=!1,Vn=[],Ml=0,Pe=e=>document.getElementById(e),st=()=>window.app||{};of(st());var bf=2500,Vo=new Map;function Ol(e){try{return localStorage.getItem(e)}catch{return Vo.has(e)?Vo.get(e):null}}function _l(e,r){try{localStorage.setItem(e,r)}catch{Vo.set(e,r)}}var Aa={get:async e=>JSON.parse(Ol(e)||"null"),set:async(e,r)=>_l(e,JSON.stringify(r))};function wf(){let e=st();return e.storage?e.storage:Aa}async function xf(e){let r=wf();if(r===Aa||!st().storage)return r.get(e);try{return await Promise.race([r.get(e),new Promise((t,n)=>setTimeout(()=>n(new Error("storage-timeout")),bf))])}catch(t){return st().log?.warn?.("Host storage read timed out, using local fallback",{key:e,error:String(t)}),Aa.get(e)}}async function Ho(e,r){let t=wf();if(t===Aa||!st().storage){await t.set(e,r);return}try{await Promise.race([t.set(e,r),new Promise((n,i)=>setTimeout(()=>i(new Error("storage-timeout")),bf))])}catch(n){st().log?.warn?.("Host storage write timed out, using local fallback",{key:e,error:String(n)}),await Aa.set(e,r)}}async function py(){try{Vn=await Ay();let e=await xf(Si);if(e){V=ln(e),ql(V)&&(V=Bn(),await Ho(Si,{...V,updatedAt:Date.now()}));return}V=Bn(),await Pt(!0)}catch(e){st().log?.warn?.("Failed to load PPT Live state",{error:String(e)}),V=Bn()}}async function Pt(e=!1){V=ln(V),await Ho(Si,{...V,updatedAt:Date.now()}),await jl(e?"autosave":"manual"),e||Zt($("saved"))}async function Ay(){try{let e=await xf(rs);return Array.isArray(e)?e.map(Sf).filter(Boolean).slice(0,40):[]}catch(e){return st().log?.warn?.("Failed to load PPT Live history",{error:String(e)}),[]}}async function jl(e="autosave"){if(!V?.slides?.length||ql(V))return;let r=Date.now();if(e==="autosave"&&Ml&&r-Ml<15e3)return;Ml=r;let t=Sf({id:V.sessionId||ir("deck"),title:V.title||$("blankDeckTitle"),updatedAt:r,slideCount:V.slides.length,reason:e,prompt:V.promptDraft||V.brief?.topic||"",state:sn({...V,generation:{...V.generation,active:!1}})});t&&(Vn=[t,...Vn.filter(n=>n.id!==t.id)].slice(0,40),await Ho(rs,Vn),kf())}function ql(e){let r=Array.isArray(e?.slides)?e.slides:[];return r.length===1&&!r[0]?.html&&String(r[0]?.id||"").startsWith("agent-working-slide")&&String(e?.title||"")===$("agentWorkingTitle")&&!e?.generation?.active}function Sf(e){return!e?.id||!e?.state?null:{id:String(e.id),title:String(e.title||e.state?.title||$("blankDeckTitle")),updatedAt:Number(e.updatedAt||Date.now()),slideCount:Number(e.slideCount||e.state?.slides?.length||0),reason:String(e.reason||"autosave"),prompt:String(e.prompt||e.state?.brief?.topic||""),state:e.state}}function kf(){let e=Pe("historyList");if(e){if(e.innerHTML="",!Vn.length){let r=document.createElement("div");r.className="history-empty",r.textContent=$("historyEmpty"),e.append(r);return}Vn.slice(0,12).forEach(r=>{let t=document.createElement("button");t.type="button",t.className=`history-card${r.id===V.sessionId?" is-active":""}`,t.innerHTML=` +${i}`),i}function vy(e){let r=!1,t=()=>{r||(r=!0,e.agent.onEvent(n=>{!n||typeof n!="object"||hy(n)}))};e.backend={protocol:"files",async call(n,i,a={}){if(n!=="ppt.generate")throw new Error(`Unsupported PPT Live action: ${n}`);t();let o=my(i),l=await e.agent.run(o,{runId:a.idempotencyKey,sessionName:"PPT Live",sessionId:a.sessionId,appDataWorkspace:a.appDataWorkspace});if(!l?.sessionId||!l?.turnId)throw new Error("PPT Live agent backend did not return sessionId/turnId");return{sessionId:l.sessionId,turnId:l.turnId,actionRunId:l.actionRunId||l.turnId}},onEvent(n){Ml.add(n)},offEvent(n){Ml.delete(n)},async cancel(n,i){await e.agent.cancel(n,i)},async turnText(n,i){return{text:(await e.agent.turnText(n,i))?.text||""}},async cancelStaleRuns(){await e.agent.cancelStaleRuns()}}}function of(e=window.app){!e||e.backend?.call||e.agent?.run&&vy(e)}var X=Bn(),Fn=!1,Cr=null,Tn=[],Xn=0,Go=!1,Aa=!1,Vn=[],Ol=0,Pe=e=>document.getElementById(e),st=()=>window.app||{};of(st());var bf=2500,Ho=new Map;function _l(e){try{return localStorage.getItem(e)}catch{return Ho.has(e)?Ho.get(e):null}}function Ul(e,r){try{localStorage.setItem(e,r)}catch{Ho.set(e,r)}}var ga={get:async e=>JSON.parse(_l(e)||"null"),set:async(e,r)=>Ul(e,JSON.stringify(r))};function wf(){let e=st();return e.storage?e.storage:ga}async function xf(e){let r=wf();if(r===ga||!st().storage)return r.get(e);try{return await Promise.race([r.get(e),new Promise((t,n)=>setTimeout(()=>n(new Error("storage-timeout")),bf))])}catch(t){return st().log?.warn?.("Host storage read timed out, using local fallback",{key:e,error:String(t)}),ga.get(e)}}async function Qo(e,r){let t=wf();if(t===ga||!st().storage){await t.set(e,r);return}try{await Promise.race([t.set(e,r),new Promise((n,i)=>setTimeout(()=>i(new Error("storage-timeout")),bf))])}catch(n){st().log?.warn?.("Host storage write timed out, using local fallback",{key:e,error:String(n)}),await ga.set(e,r)}}async function yy(){try{Vn=await by();let e=await xf(Si);if(e){X=ln(e),Vl(X)&&(X=Bn(),await Qo(Si,{...X,updatedAt:Date.now()}));return}X=Bn(),await Pt(!0)}catch(e){st().log?.warn?.("Failed to load PPT Live state",{error:String(e)}),X=Bn()}}async function Pt(e=!1){X=ln(X),await Qo(Si,{...X,updatedAt:Date.now()}),await ql(e?"autosave":"manual"),e||Zt($("saved"))}async function by(){try{let e=await xf(is);return Array.isArray(e)?e.map(Sf).filter(Boolean).slice(0,40):[]}catch(e){return st().log?.warn?.("Failed to load PPT Live history",{error:String(e)}),[]}}async function ql(e="autosave"){if(!X?.slides?.length||Vl(X))return;let r=Date.now();if(e==="autosave"&&Ol&&r-Ol<15e3)return;Ol=r;let t=Sf({id:X.sessionId||ir("deck"),title:X.title||$("blankDeckTitle"),updatedAt:r,slideCount:X.slides.length,reason:e,prompt:X.promptDraft||X.brief?.topic||"",state:sn({...X,generation:{...X.generation,active:!1}})});t&&(Vn=[t,...Vn.filter(n=>n.id!==t.id)].slice(0,40),await Qo(is,Vn),kf())}function Vl(e){let r=Array.isArray(e?.slides)?e.slides:[];return r.length===1&&!r[0]?.html&&String(r[0]?.id||"").startsWith("agent-working-slide")&&String(e?.title||"")===$("agentWorkingTitle")&&!e?.generation?.active}function Sf(e){return!e?.id||!e?.state?null:{id:String(e.id),title:String(e.title||e.state?.title||$("blankDeckTitle")),updatedAt:Number(e.updatedAt||Date.now()),slideCount:Number(e.slideCount||e.state?.slides?.length||0),reason:String(e.reason||"autosave"),prompt:String(e.prompt||e.state?.brief?.topic||""),state:e.state}}function kf(){let e=Pe("historyList");if(e){if(e.innerHTML="",!Vn.length){let r=document.createElement("div");r.className="history-empty",r.textContent=$("historyEmpty"),e.append(r);return}Vn.slice(0,12).forEach(r=>{let t=document.createElement("button");t.type="button",t.className=`history-card${r.id===X.sessionId?" is-active":""}`,t.innerHTML=` ${sf(r.title)} - ${$("historyMeta",{count:r.slideCount,time:my(r.updatedAt)})} + ${$("historyMeta",{count:r.slideCount,time:xy(r.updatedAt)})} ${r.prompt?`${sf(r.prompt)}`:""} - `,t.addEventListener("click",()=>{gy(r.id)}),e.append(t)})}}async function gy(e){let r=Vn.find(t=>t.id===e);r&&(Xn+=1,await $l(),V=ln(sn(r.state)),V.generation.active=!1,Ko(),Yt(),Di(V),Zt($("historyRestored")),await Ho(Si,{...V,updatedAt:Date.now()}))}function my(e){let r=new Date(e);if(Number.isNaN(r.getTime()))return"";let t=String(r.getMonth()+1).padStart(2,"0"),n=String(r.getDate()).padStart(2,"0"),i=String(r.getHours()).padStart(2,"0"),a=String(r.getMinutes()).padStart(2,"0");return`${t}/${n} ${i}:${a}`}function sf(e){return String(e??"").replaceAll("&","&").replaceAll("<","<").replaceAll(">",">").replaceAll('"',""").replaceAll("'","'")}function Zt(e){let r=Pe("statusLine");r&&(r.textContent=e)}function yi(e){let r=Pe("exportStatus");r&&(r.textContent=e)}function wi(e,r){Fn=e,document.querySelector(".ppt-live")?.classList.toggle("is-busy",Fn),document.querySelectorAll("button, input, select, textarea").forEach(n=>{if(!["closePreview","prevPresent","nextPresent"].includes(n.id)){if(n.id==="cancelGeneration"){n.disabled=!Fn,n.hidden=!Fn;return}n.id!=="newDeck"&&(n.disabled=Fn)}});let t=Pe("aiStatusPill");t&&(t.textContent=Fn?$("statusPillBusy"):$("statusPillReady"),t.classList.toggle("is-busy",Fn)),r&&Zt(r)}function hr(e,r,t){V.generation.current=e,V.generation.steps=V.generation.steps.map(n=>({...n,status:n.id===e?r:n.status})),V.generation.active=r==="running"||V.generation.steps.some(n=>n.status==="running"),mr(V),Ir(V),t&&Zt(t)}function Ko(){V.generation.active=!1,V.generation.current="idle",V.generation.draftedCount=0,V.generation.slideTarget=0,V.generation.eventSeq=0,V.generation.steps=V.generation.steps.map(e=>({...e,status:"pending"})),V.generation.events=[],V.generation.agentStream=[],mr(V),Ir(V)}function Tr(e,r="",t="info"){V.generation=ki(V.generation||{});let n=typeof e=="string"?{title:e,detail:r,kind:t}:{...e||{}},i=Et(n.title||n.label||n.message||$("processEventUnknown"),160),a=Et(n.detail??r??"",260),o=String(n.kind||t||"info").toLowerCase().replace(/[^a-z0-9-]/g,"")||"info";if(!i&&!a)return;let l=Array.isArray(V.generation.events)?V.generation.events:[],c=l[l.length-1];if(c&&c.title===i&&c.detail===a&&c.kind===o)c.timestamp=Date.now(),V.generation.events=l;else{let s=l.reduce((d,A)=>Math.max(d,Number(A.seq)||0),0),u=Math.max(Number(V.generation.eventSeq)||0,s)+1;V.generation.eventSeq=u,V.generation.events=[...l,{id:ir("generation-event"),seq:u,title:i||$("processEventUnknown"),detail:a,kind:o,timestamp:Date.now()}].slice(-80)}mr(V),Ir(V)}function $r(e){V.generation=ki(V.generation||{});let r=Array.isArray(V.generation.agentStream)?V.generation.agentStream:[];if(e.kind==="text"){let t=r[r.length-1];if(t&&t.kind==="text"){t.text=String(t.text||"")+String(e.text||""),t.timestamp=Date.now(),V.generation.agentStream=r,mr(V);return}}r.push({id:ir("agent-stream"),timestamp:Date.now(),...e}),V.generation.agentStream=r,mr(V)}function Yt(){V=ln(V),Fc(V,Qt),kf()}function rn(e={}){zc(V,e),V=ln(V)}function Cf(){return Pe("topicInput")?.value.trim()||""}function Pf(){let e=cc().join(` -`);return!V.outline.length||V.outline.join(` -`)===e||V.title===$("defaultDeckTitle")||Vl()}function Vl(){let e=String(V.title||"").trim();return V.slides.length===1&&V.outline.length===1&&V.outline[0]===$("newSlideTitle")&&(e===$("blankDeckTitle")||e===$("newSlideTitle"))}function nn(){return Array.isArray(V.slides)&&V.slides.length>0&&!Pf()&&!Vl()&&!ql(V)}async function vy(){await Xl()}async function yy(){await Xl()}async function Xl(){if(Uo||pa)return;let e=Cf();if(!e){Zt($("promptRequired"));return}Uo=!0;let r=nn();V.promptDraft=e,V.lastSubmittedPrompt=e,rn({includeTopic:!r}),r||(V.brief.topic=e);try{await Yl("auto",e,{includeTopic:!r,persistBeforeRun:!0});return}catch(t){if(Qo(t))return;st().log?.warn?.("PPT Live backend generation failed",{error:String(t)}),Hl(t),Yt(),await Pt(!0)}finally{Uo=!1}}function Ff(e=$("deckReady")){V.generation.active=!1,V.generation.draftedCount=V.slides.length,V.generation.slideTarget=0,V.generation.steps=(V.generation.steps||[]).map(r=>({...r,status:r.status==="error"?"error":"done"})),Zt(e),mr(V),Ir(V)}function by(e=$("backendGenerationFailed"),r=""){V.generation.active=!1,V.generation.steps=(V.generation.steps||[]).map(t=>({...t,status:t.status==="done"?"done":"error"})),Zt(e),Tr({title:e,detail:r||$("agentOnlyRetryHint"),kind:"error"}),wi(!1),mr(V),Ir(V)}function wy(e,r=5){let t=[],n=new Set,i=e;for(let a=0;i&&a({kind:n.kind,title:n.title,url:n.url,text:String(n.text||"").slice(0,6e3)}))}:null);let t=Number(V.brief?.slideTarget)||0;return t>0&&(r.slideTarget=t),r}function Kl({includePreset:e=!0}={}){let r=Jo(V.style?.stylePreset),t=V.style?.colorMode==="dark"?"dark":"light",n={fontFamily:V.style?.fontFamily==="serif"?"serif":"sans",density:En(V.style?.density),colorMode:t,theme:t,palette:tc(r,t)};return e&&(n.stylePreset=V.style?.stylePreset||an),n}function Sy(e){let r=String(e||"").trim();if(!r)return"";try{let t=new DOMParser().parseFromString(r,"text/html");return t.querySelectorAll("style,script,svg").forEach(n=>n.remove()),Et(t.body?.textContent||t.documentElement?.textContent||"",1800)}catch{return Et(r.replace(/<[^>]+>/g," "),1800)}}function ky(e){let r=new Set,t=String(e||""),n=Jt(V);return/(当前|本页|这一页|此页|current\s+(slide|page)|this\s+(slide|page))/i.test(t)&&r.add(n),[/第\s*(\d{1,2})\s*(页|頁|张|張)/gi,/\b(?:slide|page)\s*(\d{1,2})\b/gi,/\b(\d{1,2})\s*(?:slide|slides|page|pages)\b/gi].forEach(a=>{let o=a.exec(t);for(;o;){let l=Number(o[1])-1;l>=0&&la-o)}function Cy(e){let r=ky(e),t=Jt(V),n=new Set(r.length?r:[t]);return{title:V.title,outline:sn(V.outline||[]),slideCount:V.slides.length,activeSlideIndex:t,activeSlideId:V.slides[t]?.id||"",targetHints:r.map(i=>({slideIndex:i,slideNumber:i+1,slideId:V.slides[i]?.id||"",title:V.slides[i]?.title||""})),slides:V.slides.map((i,a)=>{let o=i.html?Sy(i.html):Et((i.elements||[]).flatMap(c=>[c.text,c.label,...Array.isArray(c.items)?c.items:[]]).filter(Boolean).join(` -`),1800),l={slideIndex:a,slideNumber:a+1,id:i.id,title:i.title,kicker:i.kicker,claim:i.claim,proofObject:i.proofObject,supportNote:i.supportNote,sourceNote:i.sourceNote,notes:i.notes,layout:i.layout,visibleText:o,hasHtml:!!i.html};return n.has(a)&&i.html&&(l.html=String(i.html).slice(0,12e3)),l})}}function lf(e,r){let t=r.getBoundingClientRect(),n=mt((e-t.left)/t.width,0,1);return Math.round(n*2)}function Pn(e){Ia(Sa(mt(Math.round(Number(e)),0,2)))}function Py(){Ic(V),V=ln(V)}var da=2,Fy=750;function Ty(e){let r=String(e?.message||e||"");return!(Qo(e)||/Generation stopped/i.test(r)||/backend is unavailable|did not return sessionId/i.test(r)||/permission|workspacePath is required|unsupported PPT Live action/i.test(r))}function Dy(e,r){let t=String(e?.message||e||"");return/rate limit|network|timed? out|connection|temporar|overload|service unavailable|502|503|504/i.test(t)?Math.min(15e3,1e3*2**Math.min(Math.max(0,r-1),4)):Fy}function Ey(e){return/Unknown MiniApp agent session|session workspace does not match/i.test(String(e?.message||e||""))}function By(){let e=st();return e.backend?.protocol==="files"&&!!e.appDataDir&&!!e.fs?.readFile}function Ry(){let e=`deck-${Date.now()}-${Math.random().toString(36).slice(2,8)}`;return{runId:e,workspaceSubdir:`decks/${e}`,dir:`${st().appDataDir}/decks/${e}`}}function Ly(){let e=String(V.agentSession?.workspaceSubdir||"");return!e||!st().appDataDir?null:{runId:String(V.agentSession?.runId||e.split("/").pop()||""),workspaceSubdir:e,dir:`${st().appDataDir}/${e}`}}function Df(e){return`slides/slide-${String(e).padStart(2,"0")}.html`}function Ql(e){return typeof e=="string"?e:String(e?.title||"")}function Ef(e){return Array.isArray(e?.outline)?e.outline.map(Ql).filter(Boolean):[]}function Bf(e){let r=String(e||"").trim();return r?[$("agentWorkingTitle"),$("generationAgentWorking"),$("blankDeckTitle"),$("defaultDeckTitle"),$("newSlideTitle")].includes(r):!0}function ga({plan:e=null,payload:r=null,state:t=null,instruction:n="",slides:i=[]}={}){let a=Array.isArray(e?.outline)?e.outline:Array.isArray(r?.outline)?r.outline:[],o=a.length?Ql(a[0]):"",l=Array.isArray(i)&&i.length?String(i[0]?.title||"").trim():"",c=[r?.deckPatch?.title,r?.patch?.title,r?.title,e?.title,o,t?.brief?.topic,n,t?.promptDraft,l];for(let s of c){let u=String(s||"").trim();if(u&&!Bf(u))return u}return $("blankDeckTitle")}async function Rf(e,r){let t=st().fs;if(!t?.readFile)throw new Error("PPT Live fs API is unavailable");return await t.readFile(`${e.dir}/${r}`)}async function Ny(e,r){try{let t=String(await Rf(e,r)||"");return t.trim()?xi(t):null}catch{return null}}async function Zl(e){return await Ny(e,"project.json")}async function Lf(e,r){try{let t=String(await Rf(e,Df(r))||"").trim();return!t||!/<\/html>\s*$/i.test(t)?null:t}catch{return null}}async function Iy(e,r,t=6,n=120){for(let i=1;i<=t;i+=1){let a=await Lf(e,r);if(a)return a;isetTimeout(o,n))}return null}async function zy(e){let r=await Zl(e);if(!r)throw new Error("PPT Live agent finished without a valid project.json");let t=Array.isArray(r.slide_order)&&r.slide_order.length?r.slide_order:Array.isArray(r.outline)?r.outline.map((i,a)=>`slide-${String(a+1).padStart(2,"0")}`):[],n=[];for(let i=0;i({id:`slide-${String(o+1).padStart(2,"0")}`,title:String(a.title||""),bullets:[],slide_id:`slide-${String(o+1).padStart(2,"0")}`})),i={title:V.title||"",language:Nr(),outline:n,slide_order:n.map(a=>a.slide_id),style:Kl()};await r.writeFile(`${e.dir}/project.json`,`${JSON.stringify(i,null,2)} -`);for(let a=0;atypeof a=="string"?a:a?.name).filter(a=>typeof a=="string"&&a.startsWith("deck-")&&a!==e);for(let a of i)await r.rm(`${t}/${a}`,{recursive:!0})}catch{}}async function Yl(e,r,t={}){if(!st().backend?.call)throw new Error("PPT Live backend is unavailable");if(!pa){pa=!0;try{rn({includeTopic:t.includeTopic!==!1}),Py(),t.persistBeforeRun&&await Pt(!0),await Wy(e,r)}finally{pa=!1}}}async function _y(e,r={},t={}){let n=st(),i=Xn,a=null,o=null,l="",c="",s=!1,u=0,d=null,A=[],f=new Set,p=[],g=new Set,y=Xy(),v={lastEventAt:Date.now()};try{let b=await n.backend.call("ppt.generate",e,{entityId:"deck",idempotencyKey:`ppt-live-${Date.now()}-${Math.random().toString(36).slice(2,8)}`,sessionId:t.sessionId||void 0,appDataWorkspace:t.appDataWorkspace||void 0});if(a=b?.sessionId||null,o=b?.turnId||b?.actionRunId||null,!a||!o)throw new Error("PPT Live backend did not return sessionId/turnId");if(nb(a,o),ha(i))throw new Error("Generation stopped");let x=new Promise((W,H)=>{let C=m=>{let N=m.sessionId,ne=N===a,q=String(m.sourceEvent||"");if(q.endsWith("subagent-session-linked")){m.parentSessionId===a&&N&&(g.add(N),Tr({title:$("eventSubagentStarted"),detail:"",kind:"tool"}),$r({kind:"system",text:`[subagent started] ${String(m.subagentName||m.sessionName||N).slice(0,120)}`}));return}let se=g.has(N);if(!(!ne&&!se)&&!(ne&&m.turnId&&m.turnId!==o)){if(v.lastEventAt=Date.now(),!ne&&N&&g.add(N),q.endsWith("dialog-turn-started"))y.note($("eventTurnStarted"),"","turn");else if(q.endsWith("model-round-started"))ne?r.onToolPhase?.("round"):y.note($("eventSubagentWorking"),"","pulse",8e3),y.touch();else if(q.endsWith("model-round-completed"))y.touch();else if(q.endsWith("tool-event")){let Z=Jl(m.toolEvent||{}),ue=Z.event_type||Z.eventType||"",j=String(Z.tool_name||Z.toolName||"").trim().toLowerCase();if(ne&&(ue==="Started"?p.push({eventType:ue,toolId:Z.tool_id||Z.toolId||"",toolName:Z.tool_name||Z.toolName||"",params:Z.params||{}}):ue==="Completed"?p.push({eventType:ue,toolId:Z.tool_id||Z.toolId||"",toolName:Z.tool_name||Z.toolName||"",result:Z.result||{}}):(ue==="Failed"||ue==="Cancelled")&&p.push({eventType:ue,toolId:Z.tool_id||Z.toolId||"",toolName:Z.tool_name||Z.toolName||"",error:Z.error||Z.message||ue})),ue==="Started"&&j==="task"&&(y.note($("eventToolTaskStarted"),"","tool"),y.touch()),ue==="Started"&&j){let _=Z.params||Z.result||{},fe=Hy(j,_);$r({kind:"tool-start",toolName:j,isSubagent:!ne,text:fe})}else if(ue==="Completed"&&j){let _=Ky(j,Z.result||{});_&&$r({kind:"tool-done",toolName:j,isSubagent:!ne,text:_})}else(ue==="Failed"||ue==="Cancelled")&&$r({kind:"tool-error",toolName:j,isSubagent:!ne,text:Et(String(Z.error||Z.message||ue),200)});if(Vy(Z,f,{isSubagent:!ne})){let _=rb(m,{isSubagent:!ne});_&&Tr(_),y.touch()}if(ne&&(ue==="EarlyDetected"||ue==="Started"))r.onToolPhase?.("detected");else if(ne&&ue==="Completed"){let _=j;if(r.onToolPhase?.("completed"),_==="skill"?y.note($("eventToolSkillReady"),"","phase"):(_==="websearch"||_==="webfetch")&&r.onToolPhase?.("research"),(_==="write"||_==="edit")&&typeof r.onSlideFileWritten=="function"){let ce=If(Z,p).match(/slides\/slide-(\d{2})\.html/i);if(ce){let ie=parseInt(ce[1],10);Number.isFinite(ie)&&ie>0&&Promise.resolve(r.onSlideFileWritten(ie)).catch(()=>{})}}}}else if(q.endsWith("text-chunk")){let Z=String(m.text||"");if(m.contentType==="thinking")c+=Z;else{l+=Z,y.touch(),$r({kind:"text",text:Z});let j=Date.now();j-u>=500&&(u=j,r.onTextProgress?.(l))}}else if(!q.endsWith("token-usage-updated")){if(q.endsWith("dialog-turn-completed")){if(!ne){y.note($("eventSubagentDone"),"","tool"),$r({kind:"system",isSubagent:!0,text:"[subagent done]"});return}$r({kind:"system",text:"[turn completed]"}),s=!0,d={success:m.success,finishReason:m.finishReason||m.finish_reason||"",partialRecoveryReason:m.partialRecoveryReason||m.partial_recovery_reason||""},W({answer:l,thinking:c})}else if(q.endsWith("dialog-turn-failed")||q.endsWith("dialog-turn-cancelled")){if(!ne){y.note($("eventSubagentFailed"),"","error"),$r({kind:"system",isSubagent:!0,text:"[subagent failed]"});return}s=!0,l&&r.onTextProgress?.(l);let Z=Et(m.error||m.message||"");$r({kind:"system",text:q.endsWith("dialog-turn-cancelled")?"[turn cancelled]":"[turn failed]"}),Tr({title:q.endsWith("dialog-turn-cancelled")?$("eventTurnCancelled"):$("eventTurnFailed"),detail:Z,kind:"error"}),H(new Error(Z||q))}}}};n.backend.onEvent(C),A.push(()=>n.backend.offEvent?.(C));let I=setInterval(()=>{if(s||Date.now()-y.lastProgressLogAt<12e3)return;let N=(V.generation?.steps||[]).find(ne=>ne.status==="running");y.note(N?.label?`${N.label}\u2026`:$("generationProgressPulse"),N?.detail||"","pulse",0)},12e3);A.push(()=>clearInterval(I))}),P=t.resultKind!=="text",F=await db(x,a,o,v,{expectJson:P}),R=typeof F=="string"?F:F?.answer||"",E=typeof F=="string"?"":F?.thinking||"";if(ha(i))throw new Error("Generation stopped");if(!P)return{payload:null,text:R,sessionId:a,toolTrace:p,completion:d};let B=await fb(a,o,R,E);if(ha(i))throw new Error("Generation stopped");let T=xi(B);if(ha(i))throw new Error("Generation stopped");return{payload:T,sessionId:a,toolTrace:p,completion:d}}catch(b){if(b&&typeof b=="object"&&a&&(b.pptLiveSessionId=a,b.pptLiveToolTrace=p),!s&&a&&o&&n.backend?.cancel)try{await n.backend.cancel(a,o)}catch(x){st().log?.warn?.("PPT Live backend cancel after failure failed",{sessionId:a,turnId:o,error:String(x)})}throw b}finally{A.forEach(b=>b()),a&&o&&ib(a,o)}}function Uy(e,r){return{operation:e,instruction:r,locale:Nr(),brief:xy(),style:Kl()}}async function Wy(e,r){let t=Xn;wi(!0,$("working")),Ko(),hr("brief","running",$("generationReadingBrief")),Tr({title:$("processEventStarted"),detail:$("processEventWaiting"),kind:"start"}),Gy(e,r);let n=!1,i=By()?Ly()||Ry():null;i&&!V.agentSession?.workspaceSubdir&&(await Oy(i.runId),await My(i));let a={id:V.agentSession?.id||null,project:i},o={value:""},l={touch:()=>{},note:()=>{},lastProgressLogAt:0},c=new Map,s=null;try{let u=null;for(let d=1;d<=da;d+=1)try{d>1&&(Tr({title:$("generationRetryAttempt",{attempt:d,max:da}),detail:Tf(u),kind:"start"}),Zt($("generationRetrying",{attempt:d,max:da})),await new Promise(y=>setTimeout(y,Dy(u,d))));let A={...Uy(e,r),...a?.id?{continueAfterInterruption:!0}:{}};nn()&&(A.currentSlideIndex=Jt(V),A.currentDeck=Cy(r));let{sessionId:p}=await _y(A,{onToolPhase:y=>{y==="detected"?hr("brief","running",$("generationReadingBrief")):y==="completed"?(hr("brief","done"),hr("spine","running",$("generationWritingClaims"))):y==="research"?hr("proof","running",$("generationChoosingProof")):y==="round"&&hr("spine","running",$("generationWritingClaims"))},onTextProgress:y=>tb(y,l,o),onSlideFileWritten:i?async y=>{let v=await Iy(i,y);if(!v)return;c.set(y,v);let b=Array.isArray(s?.outline)?s.outline.length:0;if(!s||y>b){let R=await Zl(i);if(R){s=R;let E=ga({plan:R,state:V,instruction:r});Bf(E)||(V.title=E)}}let x=s||{},P=[...c.entries()].sort((R,E)=>R[0]-E[0]).map(([R,E])=>({id:`ppt-live-slide-${R}`,slideNumber:R,title:typeof x?.outline?.[R-1]=="string"?x.outline[R-1]:x?.outline?.[R-1]?.title||`${$("newSlideTitle")} ${R}`,html:E})),F=ga({plan:x,state:V,instruction:r,slides:P});hr("design","running",$("generationSlideReady",{slide:y,total:P.length})),Zt($("generationRenderingSlide",{slide:y,total:x?.outline?.length||P.length})),Tr({title:$("generationSlideReady",{slide:y,total:x?.outline?.length||P.length}),detail:"",kind:"slide"}),uf({title:F,language:x?.language||"",outline:Ef(x||{}),researchReport:x?.researchReport||null,design:x?.design||{},slides:P},{instruction:r}),V.activeSlideId=`ppt-live-slide-${y}`,V.selectedElementId="",Yt()}:void 0},{sessionId:a?.id||void 0,appDataWorkspace:a?.project?.workspaceSubdir,resultKind:i?"text":void 0});a.id=p||a.id,V.agentSession={id:a.id||"",workspaceSubdir:a?.project?.workspaceSubdir||"",runId:a?.project?.runId||"",skillKey:af},Tr({title:$("generationParsingDeck"),detail:"",kind:"parsing"}),Zt($("generationParsingDeck")),hr("design","running",$("generationDesigningLayouts"));let g=i?await zy(i):null;if(!g)throw new Error("PPT Live agent did not produce a readable deck");uf(g,{instruction:r}),await jl(`agent:${e}`),Tr({title:$("processEventDone"),detail:"",kind:"done"}),hr("spine","done"),hr("proof","done"),hr("design","done"),hr("compile","done",$("generationCompiled")),Ff($("deckReady")),n=!0,Yt(),await Pt(!0);break}catch(A){if(u=A,Ey(A)?a.id=null:A?.pptLiveSessionId&&(a.id=A.pptLiveSessionId),!Ty(A)||d>=da)throw A;st().log?.warn?.("PPT Live cowork generation attempt failed, retrying",{attempt:d,maxAttempts:da,continueInSession:!!a.id,error:String(A)})}}finally{!ha(t)&&(V.generation.active&&!n&&(V.generation.active=!1),wi(!1)),mr(V),Ir(V)}}function Gy(e,r){Zt($("generationAgentWorking")),Tr({title:$("generationAgentWorking"),detail:Et(r||""),kind:"start"}),e==="auto"&&(Pf()||Vl())&&(V.title=$("agentWorkingTitle")),Yt()}var jy=new Set(["ParamsPartial","Queued","Waiting","Progress","Streaming","StreamChunk","Confirmed","Rejected","EarlyDetected","Started"]),qy=new Set(["read","write","grep","glob","list","todowrite","todo_write","skill","bash","shell","edit","delete","apply_patch","strreplace","search_replace"]);function Nf(e,r={}){let t=String(e||"").trim().toLowerCase();return!t||qy.has(t)?"":t==="websearch"?r.isSubagent?$("eventSubagentWebSearchDone"):$("eventToolWebSearchDone"):t==="webfetch"?r.isSubagent?$("eventSubagentWebFetchDone"):$("eventToolWebFetchDone"):t==="task"?$("eventToolTaskDone"):""}function Vy(e,r,t={}){let n=Jl(e),i=n.event_type||n.eventType||"";if(jy.has(i))return!1;let a=String(n.tool_name||n.toolName||"tool").toLowerCase();if(i==="Completed"&&!Nf(a,t))return!1;let o=If(n)||String(n.params&&typeof n.params=="object"&&n.params.command||"").trim(),l=o?`${a}:${o}:${i}`:`${a}:${i}`;return r.has(l)?!1:(r.add(l),i==="Completed"||i==="Failed"||i==="Cancelled"||i==="ConfirmationNeeded")}function Xy(){let e=0,r="";return{get lastProgressLogAt(){return e},touch(){e=Date.now()},note(t,n="",i="phase",a=0){let o=Date.now(),l=t===r;return a>0&&l&&o-eo?.content||"").filter(Boolean).join(" | "),200)}if(t==="execcommand"||t==="bash"||t==="shell")return Et(String(n.cmd||n.command||""),160);let i=Object.values(n).find(a=>typeof a=="string"&&a.trim());return Et(String(i||""),160)}function Ky(e,r={}){let t=String(e||"").toLowerCase(),n=r&&typeof r=="object"?r:{};if(t==="websearch"){let i=Array.isArray(n.results)?n.results:[];return i.length?`${i.length} results`:""}if(t==="webfetch"||t==="mcp__web_reader__webreader"){let i=String(n.content||n.text||n.markdown||"").length;return i?`${i} chars`:""}if(t==="read"){let i=Number(n.lineCount||(Array.isArray(n.lines)?n.lines.length:0));return i?`${i} lines`:""}if(t==="write"||t==="edit")return Et(String(n.message||"written"),80);if(t==="grep"||t==="glob"){let i=Array.isArray(n.matches)?n.matches.length:Array.isArray(n.files)?n.files.length:0;return i?`${i} matches`:"no matches"}return t==="skill"?Et(String(n.skill_key||n.message||"loaded"),80):t==="task"?Et(String(n.result||n.message||""),160):""}function Qy(e){let r=String(e||"");return/"html"\s*:/.test(r)?"design":/"slides"\s*:/.test(r)?"proof":(/"outline"\s*:/.test(r),"spine")}function Zy(e){switch(e){case"proof":return $("generationChoosingProof");case"design":return $("generationDesigningLayouts");default:return $("generationWritingClaims")}}function cf(e,r){let n=new RegExp(`"${r}"\\s*:\\s*\\[`).exec(String(e||""));return n?String(e).slice(n.index+n[0].length):""}function Yy(e){let r=0,t=0,n=!1,i=!1;for(let a=0;a0&&(V.generation.draftedCount=t),mr(V),Ir(V)}function tb(e,r,t){let n=Qy(e),i=Zy(n);hr(n,"running",i),eb(e,n),r.touch()}function rb(e,r={}){let t=Jl(e.toolEvent||{}),n=t.event_type||t.eventType||"ToolEvent",i=String(t.tool_name||t.toolName||"").trim();if(n==="Completed"){let a=Nf(i,r);return a?{title:a,detail:"",kind:"tool"}:null}return n==="Failed"||n==="Cancelled"?{title:$("eventToolFailedUser"),detail:"",kind:"error"}:n==="ConfirmationNeeded"?{title:$("processEventWaiting"),detail:"",kind:"tool"}:null}function If(e,r=[]){let t=e?.params&&typeof e.params=="object"?e.params:{},n=String(t.file_path||t.path||"").trim();if(n)return n;let i=e?.result&&typeof e.result=="object"?e.result:{},a=String(i.file_path||i.path||"").trim();if(a)return a;let o=String(e?.tool_id||e?.toolId||"").trim();if(!o)return"";let l=[...r].reverse().find(s=>s.eventType==="Started"&&String(s.toolId||s.tool_id||"")===o),c=l?.params&&typeof l.params=="object"?l.params:{};return String(c.file_path||c.path||"").trim()}function Jl(e){if(e.event_type||e.eventType||e.tool_name||e.toolName)return e;let t=["EarlyDetected","ParamsPartial","Queued","Waiting","Started","Progress","Streaming","StreamChunk","ConfirmationNeeded","Confirmed","Rejected","Completed","Failed","Cancelled"].find(i=>e&&Object.prototype.hasOwnProperty.call(e,i));return t?{...e[t]||{},event_type:t}:e||{}}function Et(e,r=180){let t=String(e||"").replace(/\s+/g," ").trim();return t?t.length>r?`${t.slice(0,r-1)}...`:t:""}function nb(e,r){if(!e||!r)return;Tn.some(n=>n.sessionId===e&&n.turnId===r)||Tn.push({sessionId:e,turnId:r})}function ib(e,r){Tn=Tn.filter(t=>!(t.sessionId===e&&t.turnId===r))}function ha(e){return e!==Xn}async function $l(){let e=[...Tn];Tn=[],!(!e.length||!st().backend?.cancel)&&await Promise.all(e.map(async r=>{try{await st().backend.cancel(r.sessionId,r.turnId)}catch(t){st().log?.warn?.("PPT Live backend cancel failed",{sessionId:r.sessionId,turnId:r.turnId,error:String(t)})}}))}async function ab(e=!1,r={}){let t=Tn.length>0;Xn+=1,await $l(),V.generation.active=!1,V.generation.steps=V.generation.steps.map(n=>n.status==="running"?{...n,status:"error"}:n),!r.silent&&t&&(Zt(e?$("generationTimedOut"):$("generationStopped")),Tr(e?$("generationTimedOut"):$("generationStopped"))),wi(!1),mr(V),Ir(V),r.silent||await Pt(!0)}async function ob(e=!1){await ab(e)}function uf(e,r={}){if(cb(e,r)){e.researchReport&&df(e.researchReport),e.design?.palette&&typeof e.design.palette=="object"&&(V.deckPalette=e.design.palette);return}let t=ga({payload:e,state:V,instruction:r.instruction||"",slides:e?.slides||[]}),n=ub(e);if(n.length)V.title=t,V.slides=n.map((i,a)=>Ur(i,a,{...V,slides:n})),V.outline=V.slides.map(i=>i.title),V.activeSlideId=V.slides[0]?.id||"",V.selectedElementId="";else{if(!Array.isArray(e?.slides)||e.slides.length===0)throw new Error("PPT Live deck payload has no slides");V.title=t,V.slides=e.slides.map((i,a)=>Ur({...i,html:i.html||i.sourceHtml||i.slideHtml||""},a,{...V,slides:e.slides})),V.outline=V.slides.map(i=>i.title),V.activeSlideId=V.slides[0]?.id||"",V.selectedElementId=V.slides[0]?.elements[0]?.id||""}Array.isArray(e.outline)&&e.outline.length&&(V.outline=e.outline.map(Ql).filter(Boolean)),e.researchReport&&df(e.researchReport),e.design?.palette&&typeof e.design.palette=="object"&&(V.deckPalette=e.design.palette)}function df(e){V.sources={...V.sources,facts:e.verifiedFacts||V.sources?.facts||[],warnings:e.warnings||V.sources?.warnings||[],summary:e.summary||V.sources?.summary||"",fetchedAt:Date.now()}}function sb(e){return Array.isArray(e?.deckPatch?.changes)?e.deckPatch.changes:Array.isArray(e?.patch?.changes)?e.patch.changes:Array.isArray(e?.changes)?e.changes:Array.isArray(e?.patches)?e.patches:[]}function ff(e,r,t=0){let n=String(e?.slideId||e?.id||e?.targetSlideId||e?.targetId||"").trim();if(n){let o=r.findIndex(l=>l.id===n);if(o>=0)return o}let i=Number(e?.slideNumber??e?.pageNumber);if(Number.isFinite(i)&&i>0)return mt(Math.round(i)-1,0,Math.max(0,r.length-1));let a=Number(e?.slideIndex??e?.index??e?.targetSlideIndex);return Number.isFinite(a)?a>=r.length&&a-1>=0&&a-1a.id===t);if(i>=0)return i+1}let n=String(e?.beforeSlideId||"").trim();if(n){let i=r.findIndex(a=>a.id===n);if(i>=0)return i}return e?.afterSlideNumber?mt(Number(e.afterSlideNumber),0,r.length):e?.beforeSlideNumber?mt(Number(e.beforeSlideNumber)-1,0,r.length):e?.slideNumber?mt(Number(e.slideNumber)-1,0,r.length):e?.slideIndex!==void 0?mt(Number(e.slideIndex),0,r.length):Math.min(r.length,Jt(V)+1)}function hf(e,r,t,n){let i=e?.slide||e?.replacement||e?.newSlide||e?.payload||e;if(!i||typeof i!="object")return null;let a={...r||{},...i,id:i.id||i.slideId||r?.id||ir("html-slide"),html:i.html||i.sourceHtml||i.slideHtml||r?.html||""};return Ur(a,t,{...V,slides:n})}function cb(e,r={}){let t=sb(e);if(!t.length)return!1;let n=sn(V.slides||[]),i=[],a=0;if(t.forEach(l=>{let c=String(l?.op||l?.operation||l?.type||"replace_slide").toLowerCase();if(c==="delete_slide"||c==="delete"||c==="remove_slide"||c==="remove"){if(!n.length)return;let A=ff(l,n,Jt(V)),[f]=n.splice(A,1);f?.id&&i.push(f.id),a+=1;return}if(c==="insert_slide"||c==="insert"||c==="add_slide"||c==="add"){let A=lb(l,n),f=hf(l,null,A,n);if(!f)return;n.splice(A,0,f),i.push(f.id),a+=1;return}let s=ff(l,n,Jt(V)),u=n[s],d=hf(l,u,s,n);d&&(n[s]=d,i.push(d.id),a+=1)}),!a)throw new Error("PPT Live deck patch had no applicable changes");V.title=ga({payload:e,state:V,instruction:r.instruction||"",slides:n}),V.slides=n.map((l,c)=>Ur(l,c,{...V,slides:n})),V.outline=Array.isArray(e.outline)&&e.outline.length?e.outline.map(String):V.slides.map(l=>l.title);let o=i.find(l=>V.slides.some(c=>c.id===l));return V.activeSlideId=o||V.slides[Math.min(Jt(V),V.slides.length-1)]?.id||V.slides[0]?.id||"",V.selectedElementId=qt(V)?.elements?.[0]?.id||"",!0}function ub(e){let r=[];return Array.isArray(e?.htmlSlides)&&r.push(...e.htmlSlides),Array.isArray(e?.slides)&&r.push(...e.slides.filter(t=>t?.html||t?.sourceHtml||t?.slideHtml)),r.map((t,n)=>{let i=String(t?.html||t?.sourceHtml||t?.slideHtml||"").trim();return i?{id:t.id||t.slideId||ir("html-slide"),title:String(t.title||t.label||`${$("newSlideTitle")} ${n+1}`),subtitle:String(t.subtitle||""),kicker:String(t.kicker||""),claim:String(t.claim||t.title||""),proofObject:String(t.proofObject||""),supportNote:String(t.supportNote||""),sourceNote:String(t.sourceNote||""),notes:String(t.notes||""),layout:"html",theme:t.theme||{},html:i,elements:[]}:null}).filter(Boolean)}function pf(...e){for(let r of e){let t=String(r||"").trim();if(t)try{return xi(t),t}catch{}}return String(e.find(r=>String(r||"").trim())||"").trim()}async function db(e,r,t,n=null,i={}){let a=st(),o=i.expectJson!==!1;if(!r||!t||!a.backend?.turnText)return e;let l=!1,c=Promise.resolve(e).finally(()=>{l=!0}),s=new Promise((u,d)=>{let A=Date.now(),f=300*1e3,p=10*1e3,g=3600*1e3,y=()=>Number(n?.lastEventAt||A);(async()=>{for(;!l&&Date.now()-Af)break;if(bsetTimeout(x,Math.min(1e3,p-b)));continue}try{let x=await a.backend.turnText(r,t),P=String(x?.text||"").trim();if(P){if(!o){u({answer:P,thinking:""});return}try{xi(P),u({answer:P,thinking:""});return}catch{}}}catch{}await new Promise(x=>setTimeout(x,2e3))}l||d(new Error("PPT Live backend did not publish a final deck JSON"))})()});return Promise.race([c,s])}async function fb(e,r,t,n=""){let i=Date.now(),a=25e3,o=String(t||"").trim(),l=String(n||"").trim(),s=pf(o,l,`${o} -${l}`.trim());if(s)try{return xi(s),s}catch{}let u=st();if(!e||!r||!u.backend?.turnText){if(!s)throw new Error("PPT Live backend produced no text");return s}let d=0;for(;Date.now()-i{setTimeout(()=>g(new Error("turnText timeout")),4e3)})]),f=String(A?.text||"").trim();if(s=pf(f,s,l,o),s)return xi(s),s}catch{}await new Promise(A=>setTimeout(A,500))}if(!s)throw new Error("PPT Live backend produced no text");return s}function xi(e){let r=String(e||"").trim();if(!r)throw new Error("PPT Live backend produced no text");try{return JSON.parse(r)}catch{let t=r.match(/```(?:json)?\s*([\s\S]*?)```/i);if(t)return JSON.parse(t[1]);let n=r.indexOf("{"),i=r.lastIndexOf("}");if(n>=0&&i>n)return JSON.parse(r.slice(n,i+1));throw new Error("PPT Live backend returned invalid JSON")}}function hb(e){let r=String(e?.message||e||"");return/ppt_live:\/\/round-budget-exhausted|exhausted its \d+-round tool budget|tool budget before producing deck JSON/i.test(r)}function pb(e){return String(e||"").includes("timed out")}function Qo(e){let r=String(e||"");return r.includes("dialog-turn-cancelled")||r.includes("Generation stopped")}async function Ab(e,r={}){let t=nn();r.readBrief!==!1&&rn({includeTopic:!t});let n=[e,Cf()].filter(Boolean).join(": ");if(!n){Zt($("promptRequired"));return}try{await Yl("revise_slide",n,{includeTopic:!t})}catch(i){if(Qo(i))return;st().log?.warn?.("PPT Live backend slide revision failed",{action:e,error:String(i)}),Hl(i),await Pt(!0)}}async function gb(){if(rn({includeTopic:!nn()}),(V.slides||[]).some(e=>String(e?.html||"").trim())){let e=`Restyle the existing deck without changing its facts or narrative. Apply these exact settings to every slide HTML: ${JSON.stringify(Kl())}. Preserve each page's informationIntent and visualStrategy while making the deck visually coherent.`;try{await Yl("revise_deck",e,{includeTopic:!1});return}catch(r){if(Qo(r))return;st().log?.warn?.("PPT Live Agent restyle failed",{error:String(r)}),Hl(r),await Pt(!0);return}}V.slides=V.slides.map((e,r)=>Ur({...e,theme:void 0},r,V)),Zt($("deckRestyled")),Yt(),await Pt(!0)}function Ul(){rn({includeTopic:!nn()});let e=new Map(V.slides.map(r=>[r.title,r]));V.slides=V.outline.map((r,t)=>{let n=e.get(r);return n?Ur(n,t,V):os(r,t,V.outline.length,V)}),V.activeSlideId=V.slides[0]?.id||"",V.selectedElementId=V.slides[0]?.elements[0]?.id||"",Yt(),Pt(!0)}async function mb(){Xn+=1,await jl("before-new"),await $l(),V.generation.active=!1,wi(!1),V=vb(),Ko(),Yt(),Di(V),Zt($("blankDeckReady")),await Pt(!0)}function vb(){return ln(Bn())}function yb(e){if(!ns.includes(e))return;let r=qt(V);if(!r)return;let t=ss({...ka(e),x:10+r.elements.length%5*4,y:14+r.elements.length%5*4});r.elements.push(t),V.selectedElementId=t.id,Yt(),Pt(!0)}function bb(){let e=qt(V);!e||!V.selectedElementId||(e.elements=e.elements.filter(r=>r.id!==V.selectedElementId),V.selectedElementId=e.elements[0]?.id||"",Yt(),Pt(!0))}function Af(e){let r=e.elements.find(t=>t.type==="text"&&t.text);r&&(e.title=r.text.slice(0,90),V.outline[Jt(V)]=e.title,Jt(V)===0&&(V.title=e.title))}function gf(){V.presentIndex=Jt(V),zf(),Pe("previewDialog")?.showModal()}function zf(){let e=V.slides[V.presentIndex]||V.slides[0];Pe("presentSlide")&&(Pe("presentSlide").innerHTML=e?un(e):""),Pe("presentCounter")&&(Pe("presentCounter").textContent=`${Math.max(1,V.presentIndex+1)} / ${Math.max(1,V.slides.length)}`),Ln()}function _o(e){V.presentIndex=mt(V.presentIndex+e,0,V.slides.length-1),zf()}function wb(){if(!(V.slides||[]).length)return yi($("exportDeckEmpty")),null;rn({includeTopic:!nn()});let e=Nl(V);return yi($("exportSavedTo",{path:e})),e}function xb(){return rn({includeTopic:!nn()}),(V.slides||[]).length?!0:(yi($("exportDeckEmpty")),!1)}function Mf(e){return{html:{working:$("exportHtmlWorking"),done:$("exportHtmlDone"),failed:$("exportHtmlFailed")},pptx:{working:$("exportPptxWorking"),done:$("exportPptxDone"),failed:$("exportPptxFailed")},pdf:{working:$("exportPdfWorking"),done:$("exportPdfDone"),failed:$("exportPdfFailed")},png:{working:$("exportPngWorking"),done:$("exportPngDone"),failed:$("exportPngFailed")}}[e]||null}function Wl(e,r,t){let n=Mf(t==="pptx"?"pptx":t);if(!n||r<=0)return;let i=Math.min(r,Math.max(1,e+1));Go("loading",`${n.working} (${i}/${r})`)}async function mf(e,r){let t=st();if(!t?.deck?.renderPage)throw new Error("Host WebView export is unavailable in this runtime.");let n=[],i=e.length;for(let[a,o]of e.entries()){Wl(a,i,r);let l=await t.deck.renderPage({html:bs(o),format:r,width:ar.width,height:ar.height});if(!l)throw new Error(`Host WebView returned empty ${r} for slide ${a+1}`);n.push({index:a,base64:String(l).replace(/^data:.*;base64,/,"")})}return n}async function Sb(e){if(e==="html"){rn({includeTopic:!nn()});let o=Nl(V);if(!o)throw new Error($("exportDeckEmpty"));return{filename:o}}let r=V.slides||[];if(!r.length)throw new Error($("exportDeckEmpty"));let t,n=sn(V);if(e==="pptx")if(r.some(o=>o?.html)){let o=st(),l=typeof o?.deck?.renderPage=="function"?async(s,u)=>{Wl(u,r.length,"pptx");let d=await o.deck.renderPage({html:s,format:"png",width:ar.width,height:ar.height});return String(d||"").replace(/^data:.*;base64,/,"")}:null,c=await jc(r,{renderRaster:l,onRasterProgress:s=>Wl(s,r.length,"pptx")});t=await Bl(n,c)}else t=await El(n);else if(e==="pdf"){let o=await mf(r,"pdf");t=await Rl(n,o.map(l=>l.base64))}else if(e==="png"){let o=await mf(r,"png");t=await Ll(n,o)}else throw new Error($("exportFormatUnavailable"));let i=typeof t?.base64=="string"?t.base64.replace(/^data:.*;base64,/,""):"";if(!i)throw new Error(`export${e} returned no data`);let a=t.filename||`${Il(V.title||"ppt-live")}`;return ef(i,a,t.mimeType||"application/octet-stream"),{filename:a}}var Wo=!1,Qt={updateOutline(e,r){V.outline[e]=r,V.slides[e]&&(V.slides[e].title=r),Yt(),Pt(!0)},moveOutline(e,r){let t=e+r;t<0||t>=V.outline.length||([V.outline[e],V.outline[t]]=[V.outline[t],V.outline[e]],Ul())},removeOutline(e){V.outline.length<=1||(V.outline.splice(e,1),Ul())},selectSlide(e){V.activeSlideId=e,V.selectedElementId=qt(V)?.elements[0]?.id||"",Yt(),Pt(!0)},selectElement(e){V.selectedElementId=e,cn(V,Qt),Ma(V,Qt),Pt(!0)},updateElementTextDirect(e,r){let t=qt(V),n=t?.elements.find(i=>i.id===e);n&&(n.text=String(r||"").trim(),Af(t),Nn(V,Qt),renderOutline(V,Qt),Pt(!1))},updateElementListItemDirect(e,r,t){let i=qt(V)?.elements.find(a=>a.id===e);!i||!Array.isArray(i.items)||(i.items[r]=String(t||"").trim(),i.items=i.items.filter(Boolean),cn(V,Qt),Nn(V,Qt),Pt(!1))},updateSlideHtmlDirect(e,r){let t=V.slides.find(i=>i.id===e);if(!t)return;let n=String(r||"");t.html!==n&&(t.html=n,Nn(V,Qt),Pt(!1))},updateSlideNotes(e){let r=qt(V);r&&(r.notes=e),Pt(!0)},updateSlideMethodology(){let e=qt(V);e&&(e.kicker=Pe("slideKickerInput")?.value||e.kicker,e.claim=Pe("slideClaimInput")?.value||e.claim,e.proofObject=Pe("slideProofInput")?.value||e.proofObject,e.supportNote=Pe("slideSupportInput")?.value||e.supportNote,e.sourceNote=Pe("slideSourceInput")?.value||e.sourceNote,cn(V,Qt),Nn(V,Qt),Pt(!0))},updateElementFromInspector(){let e=qt(V),r=Rn(V);!e||!r||(r.text=Pe("elementTextInput")?.value||"",r.items=(Pe("elementItemsInput")?.value||"").split(` -`).map(t=>t.trim()).filter(Boolean),r.data=Cb(Pe("elementDataInput")?.value||""),r.x=mt(Number(Pe("elementXInput")?.value??r.x),0,100),r.y=mt(Number(Pe("elementYInput")?.value??r.y),0,100),r.w=mt(Number(Pe("elementWInput")?.value??r.w),3,100),r.h=mt(Number(Pe("elementHInput")?.value??r.h),3,100),r.style.fontSize=mt(Number(Pe("elementFontInput")?.value??r.style.fontSize),8,88),r.style.fontWeight=mt(Number(Pe("elementWeightInput")?.value??r.style.fontWeight),100,900),r.style.color=Pe("elementColorInput")?.value||r.style.color,r.style.background=Pe("elementBgInput")?.value||r.style.background,Qt.updateSlideMethodology(),e.notes=Pe("slideNotesInput")?.value||e.notes,Af(e),cn(V,Qt),Pt(!0))},beginDrag(e,r){if(e.button!==0)return;let n=qt(V)?.elements.find(a=>a.id===r);if(!n)return;V.selectedElementId=n.id;let i=Pe("slideCanvas").getBoundingClientRect();Fr={resizing:e.target.classList.contains("resize-handle"),startX:e.clientX,startY:e.clientY,rect:i,start:{x:n.x,y:n.y,w:n.w,h:n.h}},e.currentTarget.setPointerCapture?.(e.pointerId),window.addEventListener("pointermove",Of),window.addEventListener("pointerup",kb,{once:!0})}};function Of(e){if(!Fr)return;let r=Rn(V);if(!r)return;let t=(e.clientX-Fr.startX)/Fr.rect.width*100,n=(e.clientY-Fr.startY)/Fr.rect.height*100;Fr.resizing?(r.w=mt(Fr.start.w+t,3,100-r.x),r.h=mt(Fr.start.h+n,3,100-r.y)):(r.x=mt(Fr.start.x+t,0,100-r.w),r.y=mt(Fr.start.y+n,0,100-r.h)),cn(V,Qt),Ma(V,Qt)}function kb(){Fr=null,window.removeEventListener("pointermove",Of),Pt(!0)}function Cb(e){return e.split(` -`).map((r,t)=>{let[n,i]=r.split(":");return{label:(n||`Item ${t+1}`).trim(),value:Number(i||0)}}).filter(r=>r.label)}function Pb(){let e=document.querySelector(".studio-shell");if(!e)return;let r=document.documentElement,t=Number(Ol("pptLiveFilmstripWidth")||0),n=Number(Ol("pptLiveAgentWidth")||0);t>=128&&t<=360&&r.style.setProperty("--filmstrip-width",`${t}px`),n>=240&&n<=460&&r.style.setProperty("--agent-width",`${n}px`);let i=(a,o)=>{let l=e.getBoundingClientRect(),c=128,s=Math.min(360,l.width*.34),u=240,d=Math.min(460,l.width*.42),A=360,f=g=>{if(a==="filmstrip"){let y=Math.max(c,Math.min(s,g.clientX-l.left));if(l.width-y-parseFloat(getComputedStyle(r).getPropertyValue("--agent-width"))-12{e.classList.remove("is-resizing"),document.querySelectorAll(".panel-resizer.is-dragging").forEach(g=>g.classList.remove("is-dragging")),window.removeEventListener("pointermove",f),window.removeEventListener("pointerup",p),window.removeEventListener("pointercancel",p),_l("pptLiveFilmstripWidth",String(parseFloat(getComputedStyle(r).getPropertyValue("--filmstrip-width"))||"")),_l("pptLiveAgentWidth",String(parseFloat(getComputedStyle(r).getPropertyValue("--agent-width"))||"")),Ln()};e.classList.add("is-resizing"),window.addEventListener("pointermove",f),window.addEventListener("pointerup",p,{once:!0}),window.addEventListener("pointercancel",p,{once:!0}),f({clientX:o})};Pe("filmstripResizer")?.addEventListener("pointerdown",a=>{a.button===0&&(a.preventDefault(),a.currentTarget.classList.add("is-dragging"),i("filmstrip",a.clientX))}),Pe("agentResizer")?.addEventListener("pointerdown",a=>{a.button===0&&(a.preventDefault(),a.currentTarget.classList.add("is-dragging"),i("agent",a.clientX))})}function Fb(){let e=null,r=()=>{e&&clearTimeout(e),e=setTimeout(()=>{Ln()},60)};window.addEventListener("resize",r),Pe("toggleHistory")?.addEventListener("click",()=>{let t=Pe("historyDrawer");t&&(t.hidden=!t.hidden)}),Pe("closeHistory")?.addEventListener("click",()=>{let t=Pe("historyDrawer");t&&(t.hidden=!0)}),document.querySelectorAll("[data-sidebar-tab]").forEach(t=>{t.addEventListener("click",()=>{let n=t.dataset.sidebarTab;document.querySelectorAll("[data-sidebar-tab]").forEach(i=>{i.classList.toggle("is-active",i.dataset.sidebarTab===n)}),document.querySelectorAll("[data-sidebar-panel]").forEach(i=>{i.classList.toggle("is-active",i.dataset.sidebarPanel===n)})})}),Pe("topicInput")?.addEventListener("input",()=>{if(nn()){V.promptDraft=Pe("topicInput")?.value||"",Pt(!0);return}rn({includeTopic:!0}),Pt(!0)}),Pe("newDeck")?.addEventListener("click",()=>{mb()}),Pe("cancelGeneration")?.addEventListener("click",()=>{ob(!1)}),Pe("sendPrompt")?.addEventListener("click",()=>{Xl()}),Pe("generateOutline")?.addEventListener("click",()=>{vy()}),Pe("generateDeck")?.addEventListener("click",()=>{yy()}),Pe("addOutlineItem")?.addEventListener("click",()=>{V.outline.push($("newSlideTitle")),Yt(),Pt(!0)}),Pe("syncSlidesFromOutline")?.addEventListener("click",Ul),Pe("deleteElement")?.addEventListener("click",bb),Pe("previewDeck")?.addEventListener("click",gf),Pe("closePreview")?.addEventListener("click",()=>Pe("previewDialog")?.close()),Pe("prevPresent")?.addEventListener("click",()=>_o(-1)),Pe("nextPresent")?.addEventListener("click",()=>_o(1)),Pe("exportHtml")?.addEventListener("click",wb),Pe("restyleDeck")?.addEventListener("click",gb),document.querySelectorAll("[data-add-element]").forEach(t=>{t.addEventListener("click",()=>yb(t.dataset.addElement))}),document.querySelectorAll(".ai-action").forEach(t=>{t.addEventListener("click",()=>{Ab(t.dataset.action)})}),document.querySelectorAll(".segment").forEach(t=>{t.addEventListener("click",()=>{V.mode=t.dataset.mode,V.mode==="present"&&gf(),Yt(),Pt(!0)})}),document.addEventListener("keydown",t=>{Pe("previewDialog")?.open&&((t.key==="ArrowRight"||t.key==="PageDown")&&_o(1),(t.key==="ArrowLeft"||t.key==="PageUp")&&_o(-1),t.key==="Escape"&&Pe("previewDialog")?.close())});try{Pb()}catch(t){st().log?.warn?.("Failed to bind PPT Live panel resizers",{error:String(t)})}if(typeof ResizeObserver<"u"){let t=[document.querySelector(".ppt-live"),document.querySelector(".studio-shell"),document.querySelector(".stage-shell"),document.querySelector(".canvas-area")].filter(Boolean),n=new ResizeObserver(r);t.forEach(i=>n.observe(i))}Eb(),Bb(),Rb(),Mb(),_b()}var en=1,vi=.25,Tb=.25,Db=2;function fa(e){en=mt(e,Tb,Db);let r=document.querySelector(".canvas-stage");r&&(r.style.transform=en===1?"":`scale(${en})`);let t=Pe("zoomValue"),n=Pe("statusZoomValue"),i=Math.round(en*100)+"%";t&&(t.textContent=i),n&&(n.textContent=i)}function Eb(){Pe("zoomIn")?.addEventListener("click",()=>fa(en+vi)),Pe("zoomOut")?.addEventListener("click",()=>fa(en-vi)),Pe("statusZoomIn")?.addEventListener("click",()=>fa(en+vi)),Pe("statusZoomOut")?.addEventListener("click",()=>fa(en-vi)),document.querySelector(".canvas-area")?.addEventListener("wheel",e=>{if(e.ctrlKey||e.metaKey){e.preventDefault();let r=e.deltaY>0?-vi:vi;fa(en+r)}},{passive:!1})}function Bb(){Pe("floatingToolbar")&&document.querySelectorAll(".floating-toolbar-btn").forEach(r=>{r.addEventListener("click",()=>{let t=r.dataset.tool;if(!t)return;let n=qt(V),i=Rn(V);if(!(!n||!i)){switch(t){case"bold":i.fontWeight=i.fontWeight==="700"?"400":"700";break;case"italic":i.fontStyle=i.fontStyle==="italic"?"normal":"italic";break;case"underline":i.textDecoration=i.textDecoration==="underline"?"none":"underline";break;case"align-left":i.align="left";break;case"align-center":i.align="center";break;case"align-right":i.align="right";break;case"duplicate":n.elements.push({...sn(i),id:ir("el"),x:i.x+5,y:i.y+5});break;case"delete":n.elements=n.elements.filter(a=>a.id!==i.id),V.selectedElementId=null;break}cn(V,Qt),Nn(V,Qt),Pt(!0)}})})}function Rb(){document.querySelectorAll(".property-section__header").forEach(n=>{let i=n.closest(".property-section");if(!i)return;let a=()=>{i.classList.toggle("is-collapsed");let o=!i.classList.contains("is-collapsed");n.setAttribute("aria-expanded",String(o))};n.addEventListener("click",a),n.addEventListener("keydown",o=>{(o.key==="Enter"||o.key===" ")&&(o.preventDefault(),a())})});let e=Pe("densitySlider"),r=e?.querySelector(".density-slider__track");e&&r&&(r.addEventListener("pointerdown",n=>{n.preventDefault(),Pn(lf(n.clientX,r)),r.setPointerCapture(n.pointerId)}),r.addEventListener("pointermove",n=>{r.hasPointerCapture(n.pointerId)&&Pn(lf(n.clientX,r))}),r.addEventListener("pointerup",n=>{r.hasPointerCapture(n.pointerId)&&r.releasePointerCapture(n.pointerId)}),r.addEventListener("pointercancel",n=>{r.hasPointerCapture(n.pointerId)&&(r.releasePointerCapture(n.pointerId),Ia(V.style?.density))}),e.querySelectorAll("[data-density-index]").forEach(n=>{n.addEventListener("click",i=>{i.stopPropagation(),Pn(n.dataset.densityIndex)})}),e.addEventListener("keydown",n=>{let i=Pe("densitySlider"),a=Number(i?.dataset.index??1);n.key==="ArrowLeft"||n.key==="ArrowDown"?(n.preventDefault(),Pn(a-1)):n.key==="ArrowRight"||n.key==="ArrowUp"?(n.preventDefault(),Pn(a+1)):n.key==="Home"?(n.preventDefault(),Pn(0)):n.key==="End"&&(n.preventDefault(),Pn(2))})),document.querySelectorAll("[data-font-family]").forEach(n=>{n.addEventListener("click",()=>{La(n.dataset.fontFamily==="serif"?"serif":"sans")})}),document.querySelectorAll("[data-color-mode]").forEach(n=>{n.addEventListener("click",()=>{Na(n.dataset.colorMode==="dark"?"dark":"light")})});let t=Pe("stylePresetSelect");t&&(Gf(),vc(t),Di(V),t.addEventListener("change",()=>{let n=t.value;if(!n)return;let i=Jo(n);i&&(Na(i.colorMode||"light"),La(i.fontFamily||"sans"),Pn(xa(i.density||"standard"))),Ci(t)}))}var tn=0;function _f(){return Pe("formatGrid")?.querySelector(".format-card.is-selected")?.dataset.format||"pptx"}function Lb(){let e=Pe("exportOverlay");e&&(Uf(),tn=Math.max(0,Jt(V)),e.classList.add("is-visible"),e.setAttribute("aria-hidden","false"),Nb(),Xo(),requestAnimationFrame(()=>bi()))}function bi(){Lc(Pe("exportPreviewFrame"))}function Uf(){let e=Pe("exportModalFeedback"),r=Pe("exportModalFeedbackText"),t=Pe("exportModalSpinner");Pe("exportOverlay")?.classList.remove("is-exporting"),e&&(e.hidden=!0,e.classList.remove("is-success","is-error")),r&&(r.textContent=""),t&&(t.hidden=!1),Gl(!1)}function Gl(e){["exportCancel","exportConfirm","closeExport"].forEach(r=>{let t=Pe(r);t&&(t.disabled=e)}),Pe("formatGrid")?.querySelectorAll(".format-card").forEach(r=>{r.tabIndex=e?-1:0,r.style.pointerEvents=e?"none":""}),["exportPreviewPrev","exportPreviewNext"].forEach(r=>{let t=Pe(r);t&&(t.disabled=e)})}function Go(e,r){let t=Pe("exportModalFeedback"),n=Pe("exportModalFeedbackText"),i=Pe("exportModalSpinner");!t||!n||(t.hidden=!1,t.classList.toggle("is-success",e==="success"),t.classList.toggle("is-error",e==="error"),i&&(i.hidden=e!=="loading"),n.textContent=r)}function jo(){let e=Pe("exportOverlay");e&&(e.classList.remove("is-visible"),e.setAttribute("aria-hidden","true"),Uf())}function Nb(){let e=Pe("formatGrid");if(!e)return;let r=[{id:"pptx",name:"PPTX",desc:"Editable PowerPoint"},{id:"pdf",name:"PDF",desc:"Universal format"},{id:"html",name:"HTML",desc:"Interactive web deck"},{id:"png",name:"PNG",desc:"Image sequence"}];e.innerHTML=r.map((t,n)=>` + `,t.addEventListener("click",()=>{wy(r.id)}),e.append(t)})}}async function wy(e){let r=Vn.find(t=>t.id===e);r&&(Xn+=1,await ec(),X=ln(sn(r.state)),X.generation.active=!1,Zo(),Yt(),Ei(X),Zt($("historyRestored")),await Qo(Si,{...X,updatedAt:Date.now()}))}function xy(e){let r=new Date(e);if(Number.isNaN(r.getTime()))return"";let t=String(r.getMonth()+1).padStart(2,"0"),n=String(r.getDate()).padStart(2,"0"),i=String(r.getHours()).padStart(2,"0"),a=String(r.getMinutes()).padStart(2,"0");return`${t}/${n} ${i}:${a}`}function sf(e){return String(e??"").replaceAll("&","&").replaceAll("<","<").replaceAll(">",">").replaceAll('"',""").replaceAll("'","'")}function Zt(e){let r=Pe("statusLine");r&&(r.textContent=e)}function yi(e){let r=Pe("exportStatus");r&&(r.textContent=e)}function wi(e,r){Fn=e,document.querySelector(".ppt-live")?.classList.toggle("is-busy",Fn),document.querySelectorAll("button, input, select, textarea").forEach(n=>{if(!["closePreview","prevPresent","nextPresent"].includes(n.id)){if(n.id==="cancelGeneration"){n.disabled=!Fn,n.hidden=!Fn;return}n.id!=="newDeck"&&(n.disabled=Fn)}});let t=Pe("aiStatusPill");t&&(t.textContent=Fn?$("statusPillBusy"):$("statusPillReady"),t.classList.toggle("is-busy",Fn)),r&&Zt(r)}function Mr(e,r,t){X.generation.current=e,X.generation.steps=X.generation.steps.map(n=>({...n,status:n.id===e?r:n.status})),X.generation.active=r==="running"||X.generation.steps.some(n=>n.status==="running"),Fr(X),Wr(X),t&&Zt(t)}function Zo(){X.generation.active=!1,X.generation.current="idle",X.generation.draftedCount=0,X.generation.slideTarget=0,X.generation.eventSeq=0,X.generation.steps=X.generation.steps.map(e=>({...e,status:"pending"})),X.generation.events=[],X.generation.agentStream=[],Fr(X),Wr(X)}function Pr(e,r="",t="info"){X.generation=ki(X.generation||{});let n=typeof e=="string"?{title:e,detail:r,kind:t}:{...e||{}},i=Gt(n.title||n.label||n.message||$("processEventUnknown"),160),a=Gt(n.detail??r??"",260),o=String(n.kind||t||"info").toLowerCase().replace(/[^a-z0-9-]/g,"")||"info";if(!i&&!a)return;let l=Array.isArray(X.generation.events)?X.generation.events:[],c=l[l.length-1];if(c&&c.title===i&&c.detail===a&&c.kind===o)c.timestamp=Date.now(),X.generation.events=l;else{let s=l.reduce((d,A)=>Math.max(d,Number(A.seq)||0),0),u=Math.max(Number(X.generation.eventSeq)||0,s)+1;X.generation.eventSeq=u,X.generation.events=[...l,{id:ir("generation-event"),seq:u,title:i||$("processEventUnknown"),detail:a,kind:o,timestamp:Date.now()}].slice(-80)}Fr(X),Wr(X)}function $r(e){let r=String(e.toolName||"").toLowerCase();if(r&&Zy.has(r)||e.kind==="system")return;X.generation=ki(X.generation||{});let t=Array.isArray(X.generation.agentStream)?X.generation.agentStream:[];if(e.kind==="text"){let n=t[t.length-1];if(n&&n.kind==="text"){n.text=String(n.text||"")+String(e.text||""),n.timestamp=Date.now(),X.generation.agentStream=t,Fr(X);return}}t.push({id:ir("agent-stream"),timestamp:Date.now(),...e}),X.generation.agentStream=t,Fr(X)}function Yt(){X=ln(X),Tc(X,Qt),kf()}function rn(e={}){zc(X,e),X=ln(X)}function Cf(){return Pe("topicInput")?.value.trim()||""}function Pf(){let e=uc().join(` +`);return!X.outline.length||X.outline.join(` +`)===e||X.title===$("defaultDeckTitle")||Xl()}function Xl(){let e=String(X.title||"").trim();return X.slides.length===1&&X.outline.length===1&&X.outline[0]===$("newSlideTitle")&&(e===$("blankDeckTitle")||e===$("newSlideTitle"))}function nn(){return Array.isArray(X.slides)&&X.slides.length>0&&!Pf()&&!Xl()&&!Vl(X)}async function Sy(){await Hl()}async function ky(){await Hl()}async function Hl(){if(Go||Aa)return;let e=Cf();if(!e){Zt($("promptRequired"));return}Go=!0;let r=nn();X.promptDraft=e,X.lastSubmittedPrompt=e,rn({includeTopic:!r}),r||(X.brief.topic=e);try{await Jl("auto",e,{includeTopic:!r,persistBeforeRun:!0});return}catch(t){if(Yo(t))return;st().log?.warn?.("PPT Live backend generation failed",{error:String(t)}),Kl(t),Yt(),await Pt(!0)}finally{Go=!1}}function Ff(e=$("deckReady")){X.generation.active=!1,X.generation.draftedCount=X.slides.length,X.generation.slideTarget=0,X.generation.steps=(X.generation.steps||[]).map(r=>({...r,status:r.status==="error"?"error":"done"})),Zt(e),Fr(X),Wr(X)}function Cy(e=$("backendGenerationFailed"),r=""){X.generation.active=!1,X.generation.steps=(X.generation.steps||[]).map(t=>({...t,status:t.status==="done"?"done":"error"})),Zt(e),Pr({title:e,detail:r||$("agentOnlyRetryHint"),kind:"error"}),wi(!1),Fr(X),Wr(X)}function Py(e,r=5){let t=[],n=new Set,i=e;for(let a=0;i&&a({kind:n.kind,title:n.title,url:n.url,text:String(n.text||"").slice(0,6e3)}))}:null);let t=Number(X.brief?.slideTarget)||0;return t>0&&(r.slideTarget=t),r}function Ql({includePreset:e=!0}={}){let r=es(X.style?.stylePreset),t=X.style?.colorMode==="dark"?"dark":"light",n={fontFamily:X.style?.fontFamily==="serif"?"serif":"sans",density:En(X.style?.density),colorMode:t,theme:t,palette:rc(r,t)};return e&&(n.stylePreset=X.style?.stylePreset||an),n}function Ty(e){let r=String(e||"").trim();if(!r)return"";try{let t=new DOMParser().parseFromString(r,"text/html");return t.querySelectorAll("style,script,svg").forEach(n=>n.remove()),Gt(t.body?.textContent||t.documentElement?.textContent||"",1800)}catch{return Gt(r.replace(/<[^>]+>/g," "),1800)}}function Dy(e){let r=new Set,t=String(e||""),n=Jt(X);return/(当前|本页|这一页|此页|current\s+(slide|page)|this\s+(slide|page))/i.test(t)&&r.add(n),[/第\s*(\d{1,2})\s*(页|頁|张|張)/gi,/\b(?:slide|page)\s*(\d{1,2})\b/gi,/\b(\d{1,2})\s*(?:slide|slides|page|pages)\b/gi].forEach(a=>{let o=a.exec(t);for(;o;){let l=Number(o[1])-1;l>=0&&la-o)}function Ey(e){let r=Dy(e),t=Jt(X),n=new Set(r.length?r:[t]);return{title:X.title,outline:sn(X.outline||[]),slideCount:X.slides.length,activeSlideIndex:t,activeSlideId:X.slides[t]?.id||"",targetHints:r.map(i=>({slideIndex:i,slideNumber:i+1,slideId:X.slides[i]?.id||"",title:X.slides[i]?.title||""})),slides:X.slides.map((i,a)=>{let o=i.html?Ty(i.html):Gt((i.elements||[]).flatMap(c=>[c.text,c.label,...Array.isArray(c.items)?c.items:[]]).filter(Boolean).join(` +`),1800),l={slideIndex:a,slideNumber:a+1,id:i.id,title:i.title,kicker:i.kicker,claim:i.claim,proofObject:i.proofObject,supportNote:i.supportNote,sourceNote:i.sourceNote,notes:i.notes,layout:i.layout,visibleText:o,hasHtml:!!i.html};return n.has(a)&&i.html&&(l.html=String(i.html).slice(0,12e3)),l})}}function lf(e,r){let t=r.getBoundingClientRect(),n=mt((e-t.left)/t.width,0,1);return Math.round(n*2)}function Pn(e){Ma(ka(mt(Math.round(Number(e)),0,2)))}function By(){Ic(X),X=ln(X)}var fa=2,Ry=750;function Ly(e){let r=String(e?.message||e||"");return!(Yo(e)||/Generation stopped/i.test(r)||/backend is unavailable|did not return sessionId/i.test(r)||/permission|workspacePath is required|unsupported PPT Live action/i.test(r))}function Ny(e,r){let t=String(e?.message||e||"");return/rate limit|network|timed? out|connection|temporar|overload|service unavailable|502|503|504/i.test(t)?Math.min(15e3,1e3*2**Math.min(Math.max(0,r-1),4)):Ry}function Iy(e){return/Unknown MiniApp agent session|session workspace does not match/i.test(String(e?.message||e||""))}function zy(){let e=st();return e.backend?.protocol==="files"&&!!e.appDataDir&&!!e.fs?.readFile}function My(){let e=`deck-${Date.now()}-${Math.random().toString(36).slice(2,8)}`;return{runId:e,workspaceSubdir:`decks/${e}`,dir:`${st().appDataDir}/decks/${e}`}}function Oy(){let e=String(X.agentSession?.workspaceSubdir||"");return!e||!st().appDataDir?null:{runId:String(X.agentSession?.runId||e.split("/").pop()||""),workspaceSubdir:e,dir:`${st().appDataDir}/${e}`}}function Df(e){return`slides/slide-${String(e).padStart(2,"0")}.html`}function Zl(e){return typeof e=="string"?e:String(e?.title||"")}function Ef(e){return Array.isArray(e?.outline)?e.outline.map(Zl).filter(Boolean):[]}function Bf(e){let r=String(e||"").trim();return r?[$("agentWorkingTitle"),$("generationAgentWorking"),$("blankDeckTitle"),$("defaultDeckTitle"),$("newSlideTitle")].includes(r):!0}function ma({plan:e=null,payload:r=null,state:t=null,instruction:n="",slides:i=[]}={}){let a=Array.isArray(e?.outline)?e.outline:Array.isArray(r?.outline)?r.outline:[],o=a.length?Zl(a[0]):"",l=Array.isArray(i)&&i.length?String(i[0]?.title||"").trim():"",c=[r?.deckPatch?.title,r?.patch?.title,r?.title,e?.title,o,t?.brief?.topic,n,t?.promptDraft,l];for(let s of c){let u=String(s||"").trim();if(u&&!Bf(u))return u}return $("blankDeckTitle")}async function Rf(e,r){let t=st().fs;if(!t?.readFile)throw new Error("PPT Live fs API is unavailable");return await t.readFile(`${e.dir}/${r}`)}async function _y(e,r){try{let t=String(await Rf(e,r)||"");return t.trim()?xi(t):null}catch{return null}}async function Yl(e){return await _y(e,"project.json")}async function Lf(e,r){try{let t=String(await Rf(e,Df(r))||"").trim();return!t||!/<\/html>\s*$/i.test(t)?null:t}catch{return null}}async function Uy(e,r,t=6,n=120){for(let i=1;i<=t;i+=1){let a=await Lf(e,r);if(a)return a;isetTimeout(o,n))}return null}async function Wy(e){let r=await Yl(e);if(!r)throw new Error("PPT Live agent finished without a valid project.json");let t=Array.isArray(r.slide_order)&&r.slide_order.length?r.slide_order:Array.isArray(r.outline)?r.outline.map((i,a)=>`slide-${String(a+1).padStart(2,"0")}`):[],n=[];for(let i=0;i({id:`slide-${String(o+1).padStart(2,"0")}`,title:String(a.title||""),bullets:[],slide_id:`slide-${String(o+1).padStart(2,"0")}`})),i={title:X.title||"",language:Lr(),outline:n,slide_order:n.map(a=>a.slide_id),style:Ql()};await r.writeFile(`${e.dir}/project.json`,`${JSON.stringify(i,null,2)} +`);for(let a=0;atypeof a=="string"?a:a?.name).filter(a=>typeof a=="string"&&a.startsWith("deck-")&&a!==e);for(let a of i)await r.rm(`${t}/${a}`,{recursive:!0})}catch{}}async function Jl(e,r,t={}){if(!st().backend?.call)throw new Error("PPT Live backend is unavailable");if(!Aa){Aa=!0;try{rn({includeTopic:t.includeTopic!==!1}),By(),t.persistBeforeRun&&await Pt(!0),await Xy(e,r)}finally{Aa=!1}}}async function qy(e,r={},t={}){let n=st(),i=Xn,a=null,o=null,l="",c="",s=!1,u=0,d=null,A=[],f=new Set,p=[],g=new Set,y=Jy(),v={lastEventAt:Date.now()};try{let b=await n.backend.call("ppt.generate",e,{entityId:"deck",idempotencyKey:`ppt-live-${Date.now()}-${Math.random().toString(36).slice(2,8)}`,sessionId:t.sessionId||void 0,appDataWorkspace:t.appDataWorkspace||void 0});if(a=b?.sessionId||null,o=b?.turnId||b?.actionRunId||null,!a||!o)throw new Error("PPT Live backend did not return sessionId/turnId");if(nb(a,o),pa(i))throw new Error("Generation stopped");let x=new Promise((W,H)=>{let C=m=>{let N=m.sessionId,ne=N===a,q=String(m.sourceEvent||"");if(q.endsWith("subagent-session-linked")){m.parentSessionId===a&&N&&(g.add(N),Pr({title:$("eventSubagentStarted"),detail:"",kind:"tool"}),$r({kind:"system",text:`[subagent started] ${String(m.subagentName||m.sessionName||N).slice(0,120)}`}));return}let se=g.has(N);if(!(!ne&&!se)&&!(ne&&m.turnId&&m.turnId!==o)){if(v.lastEventAt=Date.now(),!ne&&N&&g.add(N),q.endsWith("dialog-turn-started"))y.note($("eventTurnStarted"),"","turn");else if(q.endsWith("model-round-started"))ne?r.onToolPhase?.("round"):y.note($("eventSubagentWorking"),"","pulse",8e3),y.touch();else if(q.endsWith("model-round-completed"))y.touch();else if(q.endsWith("tool-event")){let Z=$l(m.toolEvent||{}),ue=Z.event_type||Z.eventType||"",j=String(Z.tool_name||Z.toolName||"").trim().toLowerCase();if(ne&&(ue==="Started"?p.push({eventType:ue,toolId:Z.tool_id||Z.toolId||"",toolName:Z.tool_name||Z.toolName||"",params:Z.params||{}}):ue==="Completed"?p.push({eventType:ue,toolId:Z.tool_id||Z.toolId||"",toolName:Z.tool_name||Z.toolName||"",result:Z.result||{}}):(ue==="Failed"||ue==="Cancelled")&&p.push({eventType:ue,toolId:Z.tool_id||Z.toolId||"",toolName:Z.tool_name||Z.toolName||"",error:Z.error||Z.message||ue})),ue==="Started"&&j==="task"&&(y.note($("eventToolTaskStarted"),"","tool"),y.touch()),ue==="Started"&&j){let _=Z.params||Z.result||{},fe=$y(j,_);$r({kind:"tool-start",toolName:j,isSubagent:!ne,text:fe})}else if(ue==="Completed"&&j){let _=eb(j,Z.result||{});_&&$r({kind:"tool-done",toolName:j,isSubagent:!ne,text:_})}else(ue==="Failed"||ue==="Cancelled")&&$r({kind:"tool-error",toolName:j,isSubagent:!ne,text:Gt(String(Z.error||Z.message||ue),200)});if(Yy(Z,f,{isSubagent:!ne})){let _=rb(m,{isSubagent:!ne});_&&Pr(_),y.touch()}if(ne&&(ue==="EarlyDetected"||ue==="Started"))r.onToolPhase?.("detected");else if(ne&&ue==="Completed"){let _=j;if(r.onToolPhase?.("completed"),_==="skill"?y.note($("eventToolSkillReady"),"","phase"):(_==="websearch"||_==="webfetch")&&r.onToolPhase?.("research"),(_==="write"||_==="edit")&&typeof r.onSlideFileWritten=="function"){let ce=If(Z,p).match(/slides\/slide-(\d{2})\.html/i);if(ce){let ie=parseInt(ce[1],10);Number.isFinite(ie)&&ie>0&&Promise.resolve(r.onSlideFileWritten(ie)).catch(()=>{})}}}}else if(q.endsWith("text-chunk")){let Z=String(m.text||"");if(m.contentType==="thinking")c+=Z;else{l+=Z,y.touch(),$r({kind:"text",text:Z});let j=Date.now();j-u>=500&&(u=j,r.onTextProgress?.(l))}}else if(!q.endsWith("token-usage-updated")){if(q.endsWith("dialog-turn-completed")){if(!ne){y.note($("eventSubagentDone"),"","tool"),$r({kind:"system",isSubagent:!0,text:"[subagent done]"});return}$r({kind:"system",text:"[turn completed]"}),s=!0,d={success:m.success,finishReason:m.finishReason||m.finish_reason||"",partialRecoveryReason:m.partialRecoveryReason||m.partial_recovery_reason||""},W({answer:l,thinking:c})}else if(q.endsWith("dialog-turn-failed")||q.endsWith("dialog-turn-cancelled")){if(!ne){y.note($("eventSubagentFailed"),"","error"),$r({kind:"system",isSubagent:!0,text:"[subagent failed]"});return}s=!0,l&&r.onTextProgress?.(l);let Z=Gt(m.error||m.message||"");$r({kind:"system",text:q.endsWith("dialog-turn-cancelled")?"[turn cancelled]":"[turn failed]"}),Pr({title:q.endsWith("dialog-turn-cancelled")?$("eventTurnCancelled"):$("eventTurnFailed"),detail:Z,kind:"error"}),H(new Error(Z||q))}}}};n.backend.onEvent(C),A.push(()=>n.backend.offEvent?.(C));let I=setInterval(()=>{if(s||Date.now()-y.lastProgressLogAt<12e3)return;let N=(X.generation?.steps||[]).find(ne=>ne.status==="running");y.note(N?.label?`${N.label}\u2026`:$("generationProgressPulse"),N?.detail||"","pulse",0)},12e3);A.push(()=>clearInterval(I))}),P=t.resultKind!=="text",F=await db(x,a,o,v,{expectJson:P}),R=typeof F=="string"?F:F?.answer||"",E=typeof F=="string"?"":F?.thinking||"";if(pa(i))throw new Error("Generation stopped");if(!P)return{payload:null,text:R,sessionId:a,toolTrace:p,completion:d};let B=await fb(a,o,R,E);if(pa(i))throw new Error("Generation stopped");let T=xi(B);if(pa(i))throw new Error("Generation stopped");return{payload:T,sessionId:a,toolTrace:p,completion:d}}catch(b){if(b&&typeof b=="object"&&a&&(b.pptLiveSessionId=a,b.pptLiveToolTrace=p),!s&&a&&o&&n.backend?.cancel)try{await n.backend.cancel(a,o)}catch(x){st().log?.warn?.("PPT Live backend cancel after failure failed",{sessionId:a,turnId:o,error:String(x)})}throw b}finally{A.forEach(b=>b()),a&&o&&ib(a,o)}}function Vy(e,r){return{operation:e,instruction:r,locale:Lr(),brief:Fy(),style:Ql()}}async function Xy(e,r){let t=Xn;wi(!0,$("working")),Zo(),Mr("brief","running",$("generationReadingBrief")),Pr({title:$("processEventStarted"),detail:$("processEventWaiting"),kind:"start"}),Hy(e,r);let n=!1,i=zy()?Oy()||My():null;i&&!X.agentSession?.workspaceSubdir&&(await jy(i.runId),await Gy(i));let a={id:X.agentSession?.id||null,project:i},o={value:""},l={touch:()=>{},note:()=>{},lastProgressLogAt:0},c=new Map,s=null;try{let u=null;for(let d=1;d<=fa;d+=1)try{d>1&&(Pr({title:$("generationRetryAttempt",{attempt:d,max:fa}),detail:Tf(u),kind:"start"}),Zt($("generationRetrying",{attempt:d,max:fa})),await new Promise(y=>setTimeout(y,Ny(u,d))));let A={...Vy(e,r),...a?.id?{continueAfterInterruption:!0}:{}};nn()&&(A.currentSlideIndex=Jt(X),A.currentDeck=Ey(r));let{sessionId:p}=await qy(A,{onToolPhase:y=>{y==="detected"?Mr("brief","running",$("generationReadingBrief")):y==="completed"?Mr("brief","done"):y==="research"&&Mr("proof","running",$("generationChoosingProof"))},onTextProgress:y=>tb(y,l,o),onSlideFileWritten:i?async y=>{let v=await Uy(i,y);if(!v)return;c.set(y,v);let b=Array.isArray(s?.outline)?s.outline.length:0;if(!s||y>b){let R=await Yl(i);if(R){s=R;let E=ma({plan:R,state:X,instruction:r});Bf(E)||(X.title=E)}}let x=s||{},P=[...c.entries()].sort((R,E)=>R[0]-E[0]).map(([R,E])=>({id:`ppt-live-slide-${R}`,slideNumber:R,title:typeof x?.outline?.[R-1]=="string"?x.outline[R-1]:x?.outline?.[R-1]?.title||`${$("newSlideTitle")} ${R}`,html:E})),F=ma({plan:x,state:X,instruction:r,slides:P});Mr("design","running",$("generationSlideReady",{slide:y,total:P.length})),Zt($("generationRenderingSlide",{slide:y,total:x?.outline?.length||P.length})),Pr({title:$("generationSlideReady",{slide:y,total:x?.outline?.length||P.length}),detail:"",kind:"slide"}),uf({title:F,language:x?.language||"",outline:Ef(x||{}),researchReport:x?.researchReport||null,design:x?.design||{},slides:P},{instruction:r}),X.activeSlideId=`ppt-live-slide-${y}`,X.selectedElementId="",Yt()}:void 0},{sessionId:a?.id||void 0,appDataWorkspace:a?.project?.workspaceSubdir,resultKind:i?"text":void 0});a.id=p||a.id,X.agentSession={id:a.id||"",workspaceSubdir:a?.project?.workspaceSubdir||"",runId:a?.project?.runId||"",skillKey:af},Pr({title:$("generationParsingDeck"),detail:"",kind:"parsing"}),Zt($("generationParsingDeck")),Mr("design","running",$("generationDesigningLayouts"));let g=i?await Wy(i):null;if(!g)throw new Error("PPT Live agent did not produce a readable deck");uf(g,{instruction:r}),await ql(`agent:${e}`),Pr({title:$("processEventDone"),detail:"",kind:"done"}),Mr("spine","done"),Mr("proof","done"),Mr("design","done"),Mr("compile","done",$("generationCompiled")),Ff($("deckReady")),n=!0,Yt(),await Pt(!0);break}catch(A){if(u=A,Iy(A)?a.id=null:A?.pptLiveSessionId&&(a.id=A.pptLiveSessionId),!Ly(A)||d>=fa)throw A;st().log?.warn?.("PPT Live cowork generation attempt failed, retrying",{attempt:d,maxAttempts:fa,continueInSession:!!a.id,error:String(A)})}}finally{!pa(t)&&(X.generation.active&&!n&&(X.generation.active=!1),wi(!1)),Fr(X),Wr(X)}}function Hy(e,r){Zt($("generationAgentWorking")),Pr({title:$("generationAgentWorking"),detail:Gt(r||""),kind:"start"}),e==="auto"&&(Pf()||Xl())&&(X.title=$("agentWorkingTitle")),Yt()}var Ky=new Set(["ParamsPartial","Queued","Waiting","Progress","Streaming","StreamChunk","Confirmed","Rejected","EarlyDetected","Started"]),Qy=new Set(["read","write","grep","glob","list","todowrite","todo_write","skill","bash","shell","edit","delete","apply_patch","strreplace","search_replace"]),Zy=new Set(["todowrite","todo_write","grep","glob","ls","list","execcommand","bash","shell"]);function cf(e){if(!e)return"";let r=String(e).replace(/\\/g,"/").split("/").filter(Boolean);return r.length<=2?r.join("/"):r.slice(-2).join("/")}function Nf(e,r={}){let t=String(e||"").trim().toLowerCase();return!t||Qy.has(t)?"":t==="websearch"?r.isSubagent?$("eventSubagentWebSearchDone"):$("eventToolWebSearchDone"):t==="webfetch"?r.isSubagent?$("eventSubagentWebFetchDone"):$("eventToolWebFetchDone"):t==="task"?$("eventToolTaskDone"):""}function Yy(e,r,t={}){let n=$l(e),i=n.event_type||n.eventType||"";if(Ky.has(i))return!1;let a=String(n.tool_name||n.toolName||"tool").toLowerCase();if(i==="Completed"&&!Nf(a,t))return!1;let o=If(n)||String(n.params&&typeof n.params=="object"&&n.params.command||"").trim(),l=o?`${a}:${o}:${i}`:`${a}:${i}`;return r.has(l)?!1:(r.add(l),i==="Completed"||i==="Failed"||i==="Cancelled"||i==="ConfirmationNeeded")}function Jy(){let e=0,r="";return{get lastProgressLogAt(){return e},touch(){e=Date.now()},note(t,n="",i="phase",a=0){let o=Date.now(),l=t===r;return a>0&&l&&o-eo?.content||"").filter(Boolean).join(" | "),200)}if(t==="execcommand"||t==="bash"||t==="shell")return Gt(String(n.cmd||n.command||""),160);let i=Object.values(n).find(a=>typeof a=="string"&&a.trim());return Gt(String(i||""),160)}function eb(e,r={}){let t=String(e||"").toLowerCase(),n=r&&typeof r=="object"?r:{};if(t==="websearch"){let i=Array.isArray(n.results)?n.results:[];return i.length?`${i.length} \u6761\u7ED3\u679C`:""}if(t==="webfetch"||t==="mcp__web_reader__webreader"){let i=String(n.content||n.text||n.markdown||"").length;return i?`${i>1e3?Math.round(i/1e3)+"k":i} \u5B57\u7B26`:""}if(t==="read"){let i=Number(n.lineCount||(Array.isArray(n.lines)?n.lines.length:0));return i?`${i} \u884C`:""}return t==="write"||t==="edit"||t==="grep"||t==="glob"||t==="skill"?"":t==="task"?Gt(String(n.result||n.message||""),160):""}function tb(e,r,t){r.touch()}function rb(e,r={}){let t=$l(e.toolEvent||{}),n=t.event_type||t.eventType||"ToolEvent",i=String(t.tool_name||t.toolName||"").trim();if(n==="Completed"){let a=Nf(i,r);return a?{title:a,detail:"",kind:"tool"}:null}return n==="Failed"||n==="Cancelled"?{title:$("eventToolFailedUser"),detail:"",kind:"error"}:n==="ConfirmationNeeded"?{title:$("processEventWaiting"),detail:"",kind:"tool"}:null}function If(e,r=[]){let t=e?.params&&typeof e.params=="object"?e.params:{},n=String(t.file_path||t.path||"").trim();if(n)return n;let i=e?.result&&typeof e.result=="object"?e.result:{},a=String(i.file_path||i.path||"").trim();if(a)return a;let o=String(e?.tool_id||e?.toolId||"").trim();if(!o)return"";let l=[...r].reverse().find(s=>s.eventType==="Started"&&String(s.toolId||s.tool_id||"")===o),c=l?.params&&typeof l.params=="object"?l.params:{};return String(c.file_path||c.path||"").trim()}function $l(e){if(e.event_type||e.eventType||e.tool_name||e.toolName)return e;let t=["EarlyDetected","ParamsPartial","Queued","Waiting","Started","Progress","Streaming","StreamChunk","ConfirmationNeeded","Confirmed","Rejected","Completed","Failed","Cancelled"].find(i=>e&&Object.prototype.hasOwnProperty.call(e,i));return t?{...e[t]||{},event_type:t}:e||{}}function Gt(e,r=180){let t=String(e||"").replace(/\s+/g," ").trim();return t?t.length>r?`${t.slice(0,r-1)}...`:t:""}function nb(e,r){if(!e||!r)return;Tn.some(n=>n.sessionId===e&&n.turnId===r)||Tn.push({sessionId:e,turnId:r})}function ib(e,r){Tn=Tn.filter(t=>!(t.sessionId===e&&t.turnId===r))}function pa(e){return e!==Xn}async function ec(){let e=[...Tn];Tn=[],!(!e.length||!st().backend?.cancel)&&await Promise.all(e.map(async r=>{try{await st().backend.cancel(r.sessionId,r.turnId)}catch(t){st().log?.warn?.("PPT Live backend cancel failed",{sessionId:r.sessionId,turnId:r.turnId,error:String(t)})}}))}async function ab(e=!1,r={}){let t=Tn.length>0;Xn+=1,await ec(),X.generation.active=!1,X.generation.steps=X.generation.steps.map(n=>n.status==="running"?{...n,status:"error"}:n),!r.silent&&t&&(Zt(e?$("generationTimedOut"):$("generationStopped")),Pr(e?$("generationTimedOut"):$("generationStopped"))),wi(!1),Fr(X),Wr(X),r.silent||await Pt(!0)}async function ob(e=!1){await ab(e)}function uf(e,r={}){if(cb(e,r)){e.researchReport&&df(e.researchReport),e.design?.palette&&typeof e.design.palette=="object"&&(X.deckPalette=e.design.palette);return}let t=ma({payload:e,state:X,instruction:r.instruction||"",slides:e?.slides||[]}),n=ub(e);if(n.length)X.title=t,X.slides=n.map((i,a)=>Ur(i,a,{...X,slides:n})),X.outline=X.slides.map(i=>i.title),X.activeSlideId=X.slides[0]?.id||"",X.selectedElementId="";else{if(!Array.isArray(e?.slides)||e.slides.length===0)throw new Error("PPT Live deck payload has no slides");X.title=t,X.slides=e.slides.map((i,a)=>Ur({...i,html:i.html||i.sourceHtml||i.slideHtml||""},a,{...X,slides:e.slides})),X.outline=X.slides.map(i=>i.title),X.activeSlideId=X.slides[0]?.id||"",X.selectedElementId=X.slides[0]?.elements[0]?.id||""}Array.isArray(e.outline)&&e.outline.length&&(X.outline=e.outline.map(Zl).filter(Boolean)),e.researchReport&&df(e.researchReport),e.design?.palette&&typeof e.design.palette=="object"&&(X.deckPalette=e.design.palette)}function df(e){X.sources={...X.sources,facts:e.verifiedFacts||X.sources?.facts||[],warnings:e.warnings||X.sources?.warnings||[],summary:e.summary||X.sources?.summary||"",fetchedAt:Date.now()}}function sb(e){return Array.isArray(e?.deckPatch?.changes)?e.deckPatch.changes:Array.isArray(e?.patch?.changes)?e.patch.changes:Array.isArray(e?.changes)?e.changes:Array.isArray(e?.patches)?e.patches:[]}function ff(e,r,t=0){let n=String(e?.slideId||e?.id||e?.targetSlideId||e?.targetId||"").trim();if(n){let o=r.findIndex(l=>l.id===n);if(o>=0)return o}let i=Number(e?.slideNumber??e?.pageNumber);if(Number.isFinite(i)&&i>0)return mt(Math.round(i)-1,0,Math.max(0,r.length-1));let a=Number(e?.slideIndex??e?.index??e?.targetSlideIndex);return Number.isFinite(a)?a>=r.length&&a-1>=0&&a-1a.id===t);if(i>=0)return i+1}let n=String(e?.beforeSlideId||"").trim();if(n){let i=r.findIndex(a=>a.id===n);if(i>=0)return i}return e?.afterSlideNumber?mt(Number(e.afterSlideNumber),0,r.length):e?.beforeSlideNumber?mt(Number(e.beforeSlideNumber)-1,0,r.length):e?.slideNumber?mt(Number(e.slideNumber)-1,0,r.length):e?.slideIndex!==void 0?mt(Number(e.slideIndex),0,r.length):Math.min(r.length,Jt(X)+1)}function hf(e,r,t,n){let i=e?.slide||e?.replacement||e?.newSlide||e?.payload||e;if(!i||typeof i!="object")return null;let a={...r||{},...i,id:i.id||i.slideId||r?.id||ir("html-slide"),html:i.html||i.sourceHtml||i.slideHtml||r?.html||""};return Ur(a,t,{...X,slides:n})}function cb(e,r={}){let t=sb(e);if(!t.length)return!1;let n=sn(X.slides||[]),i=[],a=0;if(t.forEach(l=>{let c=String(l?.op||l?.operation||l?.type||"replace_slide").toLowerCase();if(c==="delete_slide"||c==="delete"||c==="remove_slide"||c==="remove"){if(!n.length)return;let A=ff(l,n,Jt(X)),[f]=n.splice(A,1);f?.id&&i.push(f.id),a+=1;return}if(c==="insert_slide"||c==="insert"||c==="add_slide"||c==="add"){let A=lb(l,n),f=hf(l,null,A,n);if(!f)return;n.splice(A,0,f),i.push(f.id),a+=1;return}let s=ff(l,n,Jt(X)),u=n[s],d=hf(l,u,s,n);d&&(n[s]=d,i.push(d.id),a+=1)}),!a)throw new Error("PPT Live deck patch had no applicable changes");X.title=ma({payload:e,state:X,instruction:r.instruction||"",slides:n}),X.slides=n.map((l,c)=>Ur(l,c,{...X,slides:n})),X.outline=Array.isArray(e.outline)&&e.outline.length?e.outline.map(String):X.slides.map(l=>l.title);let o=i.find(l=>X.slides.some(c=>c.id===l));return X.activeSlideId=o||X.slides[Math.min(Jt(X),X.slides.length-1)]?.id||X.slides[0]?.id||"",X.selectedElementId=qt(X)?.elements?.[0]?.id||"",!0}function ub(e){let r=[];return Array.isArray(e?.htmlSlides)&&r.push(...e.htmlSlides),Array.isArray(e?.slides)&&r.push(...e.slides.filter(t=>t?.html||t?.sourceHtml||t?.slideHtml)),r.map((t,n)=>{let i=String(t?.html||t?.sourceHtml||t?.slideHtml||"").trim();return i?{id:t.id||t.slideId||ir("html-slide"),title:String(t.title||t.label||`${$("newSlideTitle")} ${n+1}`),subtitle:String(t.subtitle||""),kicker:String(t.kicker||""),claim:String(t.claim||t.title||""),proofObject:String(t.proofObject||""),supportNote:String(t.supportNote||""),sourceNote:String(t.sourceNote||""),notes:String(t.notes||""),layout:"html",theme:t.theme||{},html:i,elements:[]}:null}).filter(Boolean)}function pf(...e){for(let r of e){let t=String(r||"").trim();if(t)try{return xi(t),t}catch{}}return String(e.find(r=>String(r||"").trim())||"").trim()}async function db(e,r,t,n=null,i={}){let a=st(),o=i.expectJson!==!1;if(!r||!t||!a.backend?.turnText)return e;let l=!1,c=Promise.resolve(e).finally(()=>{l=!0}),s=new Promise((u,d)=>{let A=Date.now(),f=300*1e3,p=10*1e3,g=3600*1e3,y=()=>Number(n?.lastEventAt||A);(async()=>{for(;!l&&Date.now()-Af)break;if(bsetTimeout(x,Math.min(1e3,p-b)));continue}try{let x=await a.backend.turnText(r,t),P=String(x?.text||"").trim();if(P){if(!o){u({answer:P,thinking:""});return}try{xi(P),u({answer:P,thinking:""});return}catch{}}}catch{}await new Promise(x=>setTimeout(x,2e3))}l||d(new Error("PPT Live backend did not publish a final deck JSON"))})()});return Promise.race([c,s])}async function fb(e,r,t,n=""){let i=Date.now(),a=25e3,o=String(t||"").trim(),l=String(n||"").trim(),s=pf(o,l,`${o} +${l}`.trim());if(s)try{return xi(s),s}catch{}let u=st();if(!e||!r||!u.backend?.turnText){if(!s)throw new Error("PPT Live backend produced no text");return s}let d=0;for(;Date.now()-i{setTimeout(()=>g(new Error("turnText timeout")),4e3)})]),f=String(A?.text||"").trim();if(s=pf(f,s,l,o),s)return xi(s),s}catch{}await new Promise(A=>setTimeout(A,500))}if(!s)throw new Error("PPT Live backend produced no text");return s}function xi(e){let r=String(e||"").trim();if(!r)throw new Error("PPT Live backend produced no text");try{return JSON.parse(r)}catch{let t=r.match(/```(?:json)?\s*([\s\S]*?)```/i);if(t)return JSON.parse(t[1]);let n=r.indexOf("{"),i=r.lastIndexOf("}");if(n>=0&&i>n)return JSON.parse(r.slice(n,i+1));throw new Error("PPT Live backend returned invalid JSON")}}function hb(e){let r=String(e?.message||e||"");return/ppt_live:\/\/round-budget-exhausted|exhausted its \d+-round tool budget|tool budget before producing deck JSON/i.test(r)}function pb(e){return String(e||"").includes("timed out")}function Yo(e){let r=String(e||"");return r.includes("dialog-turn-cancelled")||r.includes("Generation stopped")}async function Ab(e,r={}){let t=nn();r.readBrief!==!1&&rn({includeTopic:!t});let n=[e,Cf()].filter(Boolean).join(": ");if(!n){Zt($("promptRequired"));return}try{await Jl("revise_slide",n,{includeTopic:!t})}catch(i){if(Yo(i))return;st().log?.warn?.("PPT Live backend slide revision failed",{action:e,error:String(i)}),Kl(i),await Pt(!0)}}async function gb(){if(rn({includeTopic:!nn()}),(X.slides||[]).some(e=>String(e?.html||"").trim())){let e=`Restyle the existing deck without changing its facts or narrative. Apply these exact settings to every slide HTML: ${JSON.stringify(Ql())}. Preserve each page's informationIntent and visualStrategy while making the deck visually coherent.`;try{await Jl("revise_deck",e,{includeTopic:!1});return}catch(r){if(Yo(r))return;st().log?.warn?.("PPT Live Agent restyle failed",{error:String(r)}),Kl(r),await Pt(!0);return}}X.slides=X.slides.map((e,r)=>Ur({...e,theme:void 0},r,X)),Zt($("deckRestyled")),Yt(),await Pt(!0)}function Wl(){rn({includeTopic:!nn()});let e=new Map(X.slides.map(r=>[r.title,r]));X.slides=X.outline.map((r,t)=>{let n=e.get(r);return n?Ur(n,t,X):ls(r,t,X.outline.length,X)}),X.activeSlideId=X.slides[0]?.id||"",X.selectedElementId=X.slides[0]?.elements[0]?.id||"",Yt(),Pt(!0)}async function mb(){Xn+=1,await ql("before-new"),await ec(),X.generation.active=!1,wi(!1),X=vb(),Zo(),Yt(),Ei(X),Zt($("blankDeckReady")),await Pt(!0)}function vb(){return ln(Bn())}function yb(e){if(!as.includes(e))return;let r=qt(X);if(!r)return;let t=cs({...Ca(e),x:10+r.elements.length%5*4,y:14+r.elements.length%5*4});r.elements.push(t),X.selectedElementId=t.id,Yt(),Pt(!0)}function bb(){let e=qt(X);!e||!X.selectedElementId||(e.elements=e.elements.filter(r=>r.id!==X.selectedElementId),X.selectedElementId=e.elements[0]?.id||"",Yt(),Pt(!0))}function Af(e){let r=e.elements.find(t=>t.type==="text"&&t.text);r&&(e.title=r.text.slice(0,90),X.outline[Jt(X)]=e.title,Jt(X)===0&&(X.title=e.title))}function gf(){X.presentIndex=Jt(X),zf(),Pe("previewDialog")?.showModal()}function zf(){let e=X.slides[X.presentIndex]||X.slides[0];Pe("presentSlide")&&(Pe("presentSlide").innerHTML=e?un(e):""),Pe("presentCounter")&&(Pe("presentCounter").textContent=`${Math.max(1,X.presentIndex+1)} / ${Math.max(1,X.slides.length)}`),Ln()}function Wo(e){X.presentIndex=mt(X.presentIndex+e,0,X.slides.length-1),zf()}function wb(){if(!(X.slides||[]).length)return yi($("exportDeckEmpty")),null;rn({includeTopic:!nn()});let e=Il(X);return yi($("exportSavedTo",{path:e})),e}function xb(){return rn({includeTopic:!nn()}),(X.slides||[]).length?!0:(yi($("exportDeckEmpty")),!1)}function Mf(e){return{html:{working:$("exportHtmlWorking"),done:$("exportHtmlDone"),failed:$("exportHtmlFailed")},pptx:{working:$("exportPptxWorking"),done:$("exportPptxDone"),failed:$("exportPptxFailed")},pdf:{working:$("exportPdfWorking"),done:$("exportPdfDone"),failed:$("exportPdfFailed")},png:{working:$("exportPngWorking"),done:$("exportPngDone"),failed:$("exportPngFailed")}}[e]||null}function Gl(e,r,t){let n=Mf(t==="pptx"?"pptx":t);if(!n||r<=0)return;let i=Math.min(r,Math.max(1,e+1));qo("loading",`${n.working} (${i}/${r})`)}async function mf(e,r){let t=st();if(!t?.deck?.renderPage)throw new Error("Host WebView export is unavailable in this runtime.");let n=[],i=e.length;for(let[a,o]of e.entries()){Gl(a,i,r);let l=await t.deck.renderPage({html:ws(o),format:r,width:ar.width,height:ar.height});if(!l)throw new Error(`Host WebView returned empty ${r} for slide ${a+1}`);n.push({index:a,base64:String(l).replace(/^data:.*;base64,/,"")})}return n}async function Sb(e){if(e==="html"){rn({includeTopic:!nn()});let o=Il(X);if(!o)throw new Error($("exportDeckEmpty"));return{filename:o}}let r=X.slides||[];if(!r.length)throw new Error($("exportDeckEmpty"));let t,n=sn(X);if(e==="pptx")if(r.some(o=>o?.html)){let o=st(),l=typeof o?.deck?.renderPage=="function"?async(s,u)=>{Gl(u,r.length,"pptx");let d=await o.deck.renderPage({html:s,format:"png",width:ar.width,height:ar.height});return String(d||"").replace(/^data:.*;base64,/,"")}:null,c=await jc(r,{renderRaster:l,onRasterProgress:s=>Gl(s,r.length,"pptx")});t=await Rl(n,c)}else t=await Bl(n);else if(e==="pdf"){let o=await mf(r,"pdf");t=await Ll(n,o.map(l=>l.base64))}else if(e==="png"){let o=await mf(r,"png");t=await Nl(n,o)}else throw new Error($("exportFormatUnavailable"));let i=typeof t?.base64=="string"?t.base64.replace(/^data:.*;base64,/,""):"";if(!i)throw new Error(`export${e} returned no data`);let a=t.filename||`${zl(X.title||"ppt-live")}`;return ef(i,a,t.mimeType||"application/octet-stream"),{filename:a}}var jo=!1,Qt={updateOutline(e,r){X.outline[e]=r,X.slides[e]&&(X.slides[e].title=r),Yt(),Pt(!0)},moveOutline(e,r){let t=e+r;t<0||t>=X.outline.length||([X.outline[e],X.outline[t]]=[X.outline[t],X.outline[e]],Wl())},removeOutline(e){X.outline.length<=1||(X.outline.splice(e,1),Wl())},selectSlide(e){X.activeSlideId=e,X.selectedElementId=qt(X)?.elements[0]?.id||"",Yt(),Pt(!0)},selectElement(e){X.selectedElementId=e,cn(X,Qt),_a(X,Qt),Pt(!0)},updateElementTextDirect(e,r){let t=qt(X),n=t?.elements.find(i=>i.id===e);n&&(n.text=String(r||"").trim(),Af(t),Nn(X,Qt),renderOutline(X,Qt),Pt(!1))},updateElementListItemDirect(e,r,t){let i=qt(X)?.elements.find(a=>a.id===e);!i||!Array.isArray(i.items)||(i.items[r]=String(t||"").trim(),i.items=i.items.filter(Boolean),cn(X,Qt),Nn(X,Qt),Pt(!1))},updateSlideHtmlDirect(e,r){let t=X.slides.find(i=>i.id===e);if(!t)return;let n=String(r||"");t.html!==n&&(t.html=n,Nn(X,Qt),Pt(!1))},updateSlideNotes(e){let r=qt(X);r&&(r.notes=e),Pt(!0)},updateSlideMethodology(){let e=qt(X);e&&(e.kicker=Pe("slideKickerInput")?.value||e.kicker,e.claim=Pe("slideClaimInput")?.value||e.claim,e.proofObject=Pe("slideProofInput")?.value||e.proofObject,e.supportNote=Pe("slideSupportInput")?.value||e.supportNote,e.sourceNote=Pe("slideSourceInput")?.value||e.sourceNote,cn(X,Qt),Nn(X,Qt),Pt(!0))},updateElementFromInspector(){let e=qt(X),r=Rn(X);!e||!r||(r.text=Pe("elementTextInput")?.value||"",r.items=(Pe("elementItemsInput")?.value||"").split(` +`).map(t=>t.trim()).filter(Boolean),r.data=Cb(Pe("elementDataInput")?.value||""),r.x=mt(Number(Pe("elementXInput")?.value??r.x),0,100),r.y=mt(Number(Pe("elementYInput")?.value??r.y),0,100),r.w=mt(Number(Pe("elementWInput")?.value??r.w),3,100),r.h=mt(Number(Pe("elementHInput")?.value??r.h),3,100),r.style.fontSize=mt(Number(Pe("elementFontInput")?.value??r.style.fontSize),8,88),r.style.fontWeight=mt(Number(Pe("elementWeightInput")?.value??r.style.fontWeight),100,900),r.style.color=Pe("elementColorInput")?.value||r.style.color,r.style.background=Pe("elementBgInput")?.value||r.style.background,Qt.updateSlideMethodology(),e.notes=Pe("slideNotesInput")?.value||e.notes,Af(e),cn(X,Qt),Pt(!0))},beginDrag(e,r){if(e.button!==0)return;let n=qt(X)?.elements.find(a=>a.id===r);if(!n)return;X.selectedElementId=n.id;let i=Pe("slideCanvas").getBoundingClientRect();Cr={resizing:e.target.classList.contains("resize-handle"),startX:e.clientX,startY:e.clientY,rect:i,start:{x:n.x,y:n.y,w:n.w,h:n.h}},e.currentTarget.setPointerCapture?.(e.pointerId),window.addEventListener("pointermove",Of),window.addEventListener("pointerup",kb,{once:!0})}};function Of(e){if(!Cr)return;let r=Rn(X);if(!r)return;let t=(e.clientX-Cr.startX)/Cr.rect.width*100,n=(e.clientY-Cr.startY)/Cr.rect.height*100;Cr.resizing?(r.w=mt(Cr.start.w+t,3,100-r.x),r.h=mt(Cr.start.h+n,3,100-r.y)):(r.x=mt(Cr.start.x+t,0,100-r.w),r.y=mt(Cr.start.y+n,0,100-r.h)),cn(X,Qt),_a(X,Qt)}function kb(){Cr=null,window.removeEventListener("pointermove",Of),Pt(!0)}function Cb(e){return e.split(` +`).map((r,t)=>{let[n,i]=r.split(":");return{label:(n||`Item ${t+1}`).trim(),value:Number(i||0)}}).filter(r=>r.label)}function Pb(){let e=document.querySelector(".studio-shell");if(!e)return;let r=document.documentElement,t=Number(_l("pptLiveFilmstripWidth")||0),n=Number(_l("pptLiveAgentWidth")||0);t>=128&&t<=360&&r.style.setProperty("--filmstrip-width",`${t}px`),n>=240&&n<=460&&r.style.setProperty("--agent-width",`${n}px`);let i=(a,o)=>{let l=e.getBoundingClientRect(),c=128,s=Math.min(360,l.width*.34),u=240,d=Math.min(460,l.width*.42),A=360,f=g=>{if(a==="filmstrip"){let y=Math.max(c,Math.min(s,g.clientX-l.left));if(l.width-y-parseFloat(getComputedStyle(r).getPropertyValue("--agent-width"))-12{e.classList.remove("is-resizing"),document.querySelectorAll(".panel-resizer.is-dragging").forEach(g=>g.classList.remove("is-dragging")),window.removeEventListener("pointermove",f),window.removeEventListener("pointerup",p),window.removeEventListener("pointercancel",p),Ul("pptLiveFilmstripWidth",String(parseFloat(getComputedStyle(r).getPropertyValue("--filmstrip-width"))||"")),Ul("pptLiveAgentWidth",String(parseFloat(getComputedStyle(r).getPropertyValue("--agent-width"))||"")),Ln()};e.classList.add("is-resizing"),window.addEventListener("pointermove",f),window.addEventListener("pointerup",p,{once:!0}),window.addEventListener("pointercancel",p,{once:!0}),f({clientX:o})};Pe("filmstripResizer")?.addEventListener("pointerdown",a=>{a.button===0&&(a.preventDefault(),a.currentTarget.classList.add("is-dragging"),i("filmstrip",a.clientX))}),Pe("agentResizer")?.addEventListener("pointerdown",a=>{a.button===0&&(a.preventDefault(),a.currentTarget.classList.add("is-dragging"),i("agent",a.clientX))})}function Fb(){let e=null,r=()=>{e&&clearTimeout(e),e=setTimeout(()=>{Ln()},60)};window.addEventListener("resize",r),Pe("toggleHistory")?.addEventListener("click",()=>{let t=Pe("historyDrawer");t&&(t.hidden=!t.hidden)}),Pe("closeHistory")?.addEventListener("click",()=>{let t=Pe("historyDrawer");t&&(t.hidden=!0)}),document.querySelectorAll("[data-sidebar-tab]").forEach(t=>{t.addEventListener("click",()=>{let n=t.dataset.sidebarTab;document.querySelectorAll("[data-sidebar-tab]").forEach(i=>{i.classList.toggle("is-active",i.dataset.sidebarTab===n)}),document.querySelectorAll("[data-sidebar-panel]").forEach(i=>{i.classList.toggle("is-active",i.dataset.sidebarPanel===n)})})}),Pe("topicInput")?.addEventListener("input",()=>{if(nn()){X.promptDraft=Pe("topicInput")?.value||"",Pt(!0);return}rn({includeTopic:!0}),Pt(!0)}),Pe("newDeck")?.addEventListener("click",()=>{mb()}),Pe("cancelGeneration")?.addEventListener("click",()=>{ob(!1)}),Pe("sendPrompt")?.addEventListener("click",()=>{Hl()}),Pe("generateOutline")?.addEventListener("click",()=>{Sy()}),Pe("generateDeck")?.addEventListener("click",()=>{ky()}),Pe("addOutlineItem")?.addEventListener("click",()=>{X.outline.push($("newSlideTitle")),Yt(),Pt(!0)}),Pe("syncSlidesFromOutline")?.addEventListener("click",Wl),Pe("deleteElement")?.addEventListener("click",bb),Pe("previewDeck")?.addEventListener("click",gf),Pe("closePreview")?.addEventListener("click",()=>Pe("previewDialog")?.close()),Pe("prevPresent")?.addEventListener("click",()=>Wo(-1)),Pe("nextPresent")?.addEventListener("click",()=>Wo(1)),Pe("exportHtml")?.addEventListener("click",wb),Pe("restyleDeck")?.addEventListener("click",gb),document.querySelectorAll("[data-add-element]").forEach(t=>{t.addEventListener("click",()=>yb(t.dataset.addElement))}),document.querySelectorAll(".ai-action").forEach(t=>{t.addEventListener("click",()=>{Ab(t.dataset.action)})}),document.querySelectorAll(".segment").forEach(t=>{t.addEventListener("click",()=>{X.mode=t.dataset.mode,X.mode==="present"&&gf(),Yt(),Pt(!0)})}),document.addEventListener("keydown",t=>{Pe("previewDialog")?.open&&((t.key==="ArrowRight"||t.key==="PageDown")&&Wo(1),(t.key==="ArrowLeft"||t.key==="PageUp")&&Wo(-1),t.key==="Escape"&&Pe("previewDialog")?.close())});try{Pb()}catch(t){st().log?.warn?.("Failed to bind PPT Live panel resizers",{error:String(t)})}if(typeof ResizeObserver<"u"){let t=[document.querySelector(".ppt-live"),document.querySelector(".studio-shell"),document.querySelector(".stage-shell"),document.querySelector(".canvas-area")].filter(Boolean),n=new ResizeObserver(r);t.forEach(i=>n.observe(i))}Eb(),Bb(),Rb(),Mb(),_b()}var en=1,vi=.25,Tb=.25,Db=2;function ha(e){en=mt(e,Tb,Db);let r=document.querySelector(".canvas-stage");r&&(r.style.transform=en===1?"":`scale(${en})`);let t=Pe("zoomValue"),n=Pe("statusZoomValue"),i=Math.round(en*100)+"%";t&&(t.textContent=i),n&&(n.textContent=i)}function Eb(){Pe("zoomIn")?.addEventListener("click",()=>ha(en+vi)),Pe("zoomOut")?.addEventListener("click",()=>ha(en-vi)),Pe("statusZoomIn")?.addEventListener("click",()=>ha(en+vi)),Pe("statusZoomOut")?.addEventListener("click",()=>ha(en-vi)),document.querySelector(".canvas-area")?.addEventListener("wheel",e=>{if(e.ctrlKey||e.metaKey){e.preventDefault();let r=e.deltaY>0?-vi:vi;ha(en+r)}},{passive:!1})}function Bb(){Pe("floatingToolbar")&&document.querySelectorAll(".floating-toolbar-btn").forEach(r=>{r.addEventListener("click",()=>{let t=r.dataset.tool;if(!t)return;let n=qt(X),i=Rn(X);if(!(!n||!i)){switch(t){case"bold":i.fontWeight=i.fontWeight==="700"?"400":"700";break;case"italic":i.fontStyle=i.fontStyle==="italic"?"normal":"italic";break;case"underline":i.textDecoration=i.textDecoration==="underline"?"none":"underline";break;case"align-left":i.align="left";break;case"align-center":i.align="center";break;case"align-right":i.align="right";break;case"duplicate":n.elements.push({...sn(i),id:ir("el"),x:i.x+5,y:i.y+5});break;case"delete":n.elements=n.elements.filter(a=>a.id!==i.id),X.selectedElementId=null;break}cn(X,Qt),Nn(X,Qt),Pt(!0)}})})}function Rb(){document.querySelectorAll(".property-section__header").forEach(n=>{let i=n.closest(".property-section");if(!i)return;let a=()=>{i.classList.toggle("is-collapsed");let o=!i.classList.contains("is-collapsed");n.setAttribute("aria-expanded",String(o))};n.addEventListener("click",a),n.addEventListener("keydown",o=>{(o.key==="Enter"||o.key===" ")&&(o.preventDefault(),a())})});let e=Pe("densitySlider"),r=e?.querySelector(".density-slider__track");e&&r&&(r.addEventListener("pointerdown",n=>{n.preventDefault(),Pn(lf(n.clientX,r)),r.setPointerCapture(n.pointerId)}),r.addEventListener("pointermove",n=>{r.hasPointerCapture(n.pointerId)&&Pn(lf(n.clientX,r))}),r.addEventListener("pointerup",n=>{r.hasPointerCapture(n.pointerId)&&r.releasePointerCapture(n.pointerId)}),r.addEventListener("pointercancel",n=>{r.hasPointerCapture(n.pointerId)&&(r.releasePointerCapture(n.pointerId),Ma(X.style?.density))}),e.querySelectorAll("[data-density-index]").forEach(n=>{n.addEventListener("click",i=>{i.stopPropagation(),Pn(n.dataset.densityIndex)})}),e.addEventListener("keydown",n=>{let i=Pe("densitySlider"),a=Number(i?.dataset.index??1);n.key==="ArrowLeft"||n.key==="ArrowDown"?(n.preventDefault(),Pn(a-1)):n.key==="ArrowRight"||n.key==="ArrowUp"?(n.preventDefault(),Pn(a+1)):n.key==="Home"?(n.preventDefault(),Pn(0)):n.key==="End"&&(n.preventDefault(),Pn(2))})),document.querySelectorAll("[data-font-family]").forEach(n=>{n.addEventListener("click",()=>{Ia(n.dataset.fontFamily==="serif"?"serif":"sans")})}),document.querySelectorAll("[data-color-mode]").forEach(n=>{n.addEventListener("click",()=>{za(n.dataset.colorMode==="dark"?"dark":"light")})});let t=Pe("stylePresetSelect");t&&(Gf(),yc(t),Ei(X),t.addEventListener("change",()=>{let n=t.value;if(!n)return;let i=es(n);i&&(za(i.colorMode||"light"),Ia(i.fontFamily||"sans"),Pn(Sa(i.density||"standard"))),Ci(t)}))}var tn=0;function _f(){return Pe("formatGrid")?.querySelector(".format-card.is-selected")?.dataset.format||"pptx"}function Lb(){let e=Pe("exportOverlay");e&&(Uf(),tn=Math.max(0,Jt(X)),e.classList.add("is-visible"),e.setAttribute("aria-hidden","false"),Nb(),Ko(),requestAnimationFrame(()=>bi()))}function bi(){Nc(Pe("exportPreviewFrame"))}function Uf(){let e=Pe("exportModalFeedback"),r=Pe("exportModalFeedbackText"),t=Pe("exportModalSpinner");Pe("exportOverlay")?.classList.remove("is-exporting"),e&&(e.hidden=!0,e.classList.remove("is-success","is-error")),r&&(r.textContent=""),t&&(t.hidden=!1),jl(!1)}function jl(e){["exportCancel","exportConfirm","closeExport"].forEach(r=>{let t=Pe(r);t&&(t.disabled=e)}),Pe("formatGrid")?.querySelectorAll(".format-card").forEach(r=>{r.tabIndex=e?-1:0,r.style.pointerEvents=e?"none":""}),["exportPreviewPrev","exportPreviewNext"].forEach(r=>{let t=Pe(r);t&&(t.disabled=e)})}function qo(e,r){let t=Pe("exportModalFeedback"),n=Pe("exportModalFeedbackText"),i=Pe("exportModalSpinner");!t||!n||(t.hidden=!1,t.classList.toggle("is-success",e==="success"),t.classList.toggle("is-error",e==="error"),i&&(i.hidden=e!=="loading"),n.textContent=r)}function Vo(){let e=Pe("exportOverlay");e&&(e.classList.remove("is-visible"),e.setAttribute("aria-hidden","true"),Uf())}function Nb(){let e=Pe("formatGrid");if(!e)return;let r=[{id:"pptx",name:"PPTX",desc:"Editable PowerPoint"},{id:"pdf",name:"PDF",desc:"Universal format"},{id:"html",name:"HTML",desc:"Interactive web deck"},{id:"png",name:"PNG",desc:"Image sequence"}];e.innerHTML=r.map((t,n)=>`
          @@ -344,4 +385,4 @@ ${l}`.trim());if(s)try{return xi(s),s}catch{}let u=st();if(!e||!r||!u.backend?.t ${t.name} ${t.desc}
          - `).join(""),e.querySelectorAll(".format-card").forEach(t=>{let n=()=>{e.querySelectorAll(".format-card").forEach(i=>i.classList.remove("is-selected")),t.classList.add("is-selected"),Xo()};t.addEventListener("click",n),t.addEventListener("keydown",i=>{(i.key==="Enter"||i.key===" ")&&(i.preventDefault(),n())})})}function Ib(e,r){if(!e||!r)return;e.innerHTML="";let t=document.createElement("div");t.className="export-preview__viewport";let n=document.createElement("div");if(n.className="export-preview__scale",r.html)n.appendChild(Rc(r.html));else{let i=document.createElement("div");i.className="export-preview__element-stage",i.innerHTML=un(r),n.append(i)}t.append(n),e.append(t),requestAnimationFrame(()=>{bi(),requestAnimationFrame(()=>bi())})}function Xo(){let e=Pe("exportPreviewInfo"),r=Pe("exportPreviewCounter"),t=Pe("exportPreviewFrame"),n=V.slides||[],i=_f().toUpperCase(),a=Math.max(1,n.length);if(tn=mt(tn,0,Math.max(0,n.length-1)),e&&(e.textContent=`${i} \xB7 ${n.length} slides`),r&&(r.textContent=`${tn+1} / ${a}`),!t)return;let o=n[tn];if(!o){t.innerHTML=`
          ${Le($("slidesEmptyHint"))}
          `;return}Ib(t,o)}async function zb(){if(Wo||!xb())return;let e=_f(),r=Mf(e);if(!r){yi($("exportFormatUnavailable"));return}Wo=!0,Pe("exportOverlay")?.classList.add("is-exporting"),Gl(!0),Go("loading",r.working);let t=Pe("exportPreviewFrame"),n=t?.innerHTML||"";try{let{filename:i}=await Sb(e),a=$("exportSavedTo",{path:i});Pe("exportOverlay")?.classList.remove("is-exporting"),Go("success",a),yi(a),await new Promise(o=>setTimeout(o,1600)),jo()}catch(i){let a=i instanceof Error?i.message:String(i);st().log?.error?.(`PPT Live ${e} export failed`,{error:a}),Pe("exportOverlay")?.classList.remove("is-exporting"),Go("error",`${r.failed} ${a}`),yi(`${r.failed} ${a}`)}finally{t&&n&&(t.innerHTML=n),Gl(!1),Wo=!1}}function Mb(){if(Pe("exportPptx")?.addEventListener("click",()=>Lb()),Pe("closeExport")?.addEventListener("click",jo),Pe("exportCancel")?.addEventListener("click",jo),Pe("exportConfirm")?.addEventListener("click",()=>{zb()}),Pe("exportOverlay")?.addEventListener("click",e=>{e.target===Pe("exportOverlay")&&!Wo&&jo()}),Pe("exportPreviewPrev")?.addEventListener("click",()=>{tn=Math.max(0,tn-1),Xo(),requestAnimationFrame(()=>bi())}),Pe("exportPreviewNext")?.addEventListener("click",()=>{let e=(V.slides||[]).length-1;tn=Math.min(e,tn+1),Xo(),requestAnimationFrame(()=>bi())}),typeof ResizeObserver<"u"){let e=Pe("exportPreviewFrame");e&&new ResizeObserver(()=>{Pe("exportOverlay")?.classList.contains("is-visible")&&bi()}).observe(e)}}var vf="pptLiveTheme";function Wf(e){return e==="dark"||e==="light"?e:window.matchMedia?.("(prefers-color-scheme: dark)")?.matches?"dark":"light"}function Ob(){let e=document.documentElement.getAttribute("data-theme-type")||document.documentElement.getAttribute("data-theme");if(e==="dark"||e==="light")return e;let r=st().theme;return r==="dark"||r==="light"?r:Wf()}function yf(e){let r=Wf(e),t=document.documentElement;t.setAttribute("data-theme",r),t.setAttribute("data-theme-type",r),t.style.colorScheme=r,Ln(),Yt()}function _b(){try{localStorage.removeItem(vf)}catch{Vo.delete(vf)}yf(Ob()),st().onThemeChange?.(e=>{let r=e?.type==="dark"?"dark":"light";yf(r)})}async function Ub(){Xn+=1,Tn=[],pa=!1,Uo=!1,(V.generation?.active||V.generation?.steps?.some(r=>r.status==="running"))&&(Ff($("generationStopped")),Ko()),wi(!1);let e=st();e.backend?.cancelStaleRuns&&e.backend.cancelStaleRuns().catch(r=>{st().log?.warn?.("Failed to cancel stale PPT Live backend runs",{error:String(r)})})}function Gf(){let e=Pe("stylePresetSelect");if(!e)return;let r=e.value||V.style?.stylePreset||an;e.textContent="",rc(Nr()).forEach(({key:t,displayName:n,description:i})=>{let a=document.createElement("option");a.value=t,a.textContent=n,i&&(a.title=i),e.append(a)}),e.value=r,e.selectedIndex<0&&(e.value=an),Ci(e)}function qo(){V.generation=ki(V.generation),Pc(),Gf();let e=Pe("aiStatusPill");e&&(e.textContent=Fn?$("statusPillBusy"):$("statusPillReady")),Yt()}async function Wb(){qo();try{await py(),await Ub(),qo(),Di(V),await Pt(!0)}catch(e){st().log?.error?.("PPT Live init failed",{error:String(e)}),Zt($("ready")),qo()}finally{Ln()}}Fb();za();st().onLocaleChange?.(()=>qo());Wb(); + `).join(""),e.querySelectorAll(".format-card").forEach(t=>{let n=()=>{e.querySelectorAll(".format-card").forEach(i=>i.classList.remove("is-selected")),t.classList.add("is-selected"),Ko()};t.addEventListener("click",n),t.addEventListener("keydown",i=>{(i.key==="Enter"||i.key===" ")&&(i.preventDefault(),n())})})}function Ib(e,r){if(!e||!r)return;e.innerHTML="";let t=document.createElement("div");t.className="export-preview__viewport";let n=document.createElement("div");if(n.className="export-preview__scale",r.html)n.appendChild(Lc(r.html));else{let i=document.createElement("div");i.className="export-preview__element-stage",i.innerHTML=un(r),n.append(i)}t.append(n),e.append(t),requestAnimationFrame(()=>{bi(),requestAnimationFrame(()=>bi())})}function Ko(){let e=Pe("exportPreviewInfo"),r=Pe("exportPreviewCounter"),t=Pe("exportPreviewFrame"),n=X.slides||[],i=_f().toUpperCase(),a=Math.max(1,n.length);if(tn=mt(tn,0,Math.max(0,n.length-1)),e&&(e.textContent=`${i} \xB7 ${n.length} slides`),r&&(r.textContent=`${tn+1} / ${a}`),!t)return;let o=n[tn];if(!o){t.innerHTML=`
          ${Le($("slidesEmptyHint"))}
          `;return}Ib(t,o)}async function zb(){if(jo||!xb())return;let e=_f(),r=Mf(e);if(!r){yi($("exportFormatUnavailable"));return}jo=!0,Pe("exportOverlay")?.classList.add("is-exporting"),jl(!0),qo("loading",r.working);let t=Pe("exportPreviewFrame"),n=t?.innerHTML||"";try{let{filename:i}=await Sb(e),a=$("exportSavedTo",{path:i});Pe("exportOverlay")?.classList.remove("is-exporting"),qo("success",a),yi(a),await new Promise(o=>setTimeout(o,1600)),Vo()}catch(i){let a=i instanceof Error?i.message:String(i);st().log?.error?.(`PPT Live ${e} export failed`,{error:a}),Pe("exportOverlay")?.classList.remove("is-exporting"),qo("error",`${r.failed} ${a}`),yi(`${r.failed} ${a}`)}finally{t&&n&&(t.innerHTML=n),jl(!1),jo=!1}}function Mb(){if(Pe("exportPptx")?.addEventListener("click",()=>Lb()),Pe("closeExport")?.addEventListener("click",Vo),Pe("exportCancel")?.addEventListener("click",Vo),Pe("exportConfirm")?.addEventListener("click",()=>{zb()}),Pe("exportOverlay")?.addEventListener("click",e=>{e.target===Pe("exportOverlay")&&!jo&&Vo()}),Pe("exportPreviewPrev")?.addEventListener("click",()=>{tn=Math.max(0,tn-1),Ko(),requestAnimationFrame(()=>bi())}),Pe("exportPreviewNext")?.addEventListener("click",()=>{let e=(X.slides||[]).length-1;tn=Math.min(e,tn+1),Ko(),requestAnimationFrame(()=>bi())}),typeof ResizeObserver<"u"){let e=Pe("exportPreviewFrame");e&&new ResizeObserver(()=>{Pe("exportOverlay")?.classList.contains("is-visible")&&bi()}).observe(e)}}var vf="pptLiveTheme";function Wf(e){return e==="dark"||e==="light"?e:window.matchMedia?.("(prefers-color-scheme: dark)")?.matches?"dark":"light"}function Ob(){let e=document.documentElement.getAttribute("data-theme-type")||document.documentElement.getAttribute("data-theme");if(e==="dark"||e==="light")return e;let r=st().theme;return r==="dark"||r==="light"?r:Wf()}function yf(e){let r=Wf(e),t=document.documentElement;t.setAttribute("data-theme",r),t.setAttribute("data-theme-type",r),t.style.colorScheme=r,Ln(),Yt()}function _b(){try{localStorage.removeItem(vf)}catch{Ho.delete(vf)}yf(Ob()),st().onThemeChange?.(e=>{let r=e?.type==="dark"?"dark":"light";yf(r)})}async function Ub(){Xn+=1,Tn=[],Aa=!1,Go=!1,(X.generation?.active||X.generation?.steps?.some(r=>r.status==="running"))&&(Ff($("generationStopped")),Zo()),wi(!1);let e=st();e.backend?.cancelStaleRuns&&e.backend.cancelStaleRuns().catch(r=>{st().log?.warn?.("Failed to cancel stale PPT Live backend runs",{error:String(r)})})}function Gf(){let e=Pe("stylePresetSelect");if(!e)return;let r=e.value||X.style?.stylePreset||an;e.textContent="",nc(Lr()).forEach(({key:t,displayName:n,description:i})=>{let a=document.createElement("option");a.value=t,a.textContent=n,i&&(a.title=i),e.append(a)}),e.value=r,e.selectedIndex<0&&(e.value=an),Ci(e)}function Xo(){X.generation=ki(X.generation),Fc(),Gf();let e=Pe("aiStatusPill");e&&(e.textContent=Fn?$("statusPillBusy"):$("statusPillReady")),Yt()}async function Wb(){Xo();try{await yy(),await Ub(),Xo(),Ei(X),await Pt(!0)}catch(e){st().log?.error?.("PPT Live init failed",{error:String(e)}),Zt($("ready")),Xo()}finally{Ln()}}Fb();Oa();st().onLocaleChange?.(()=>Xo());Wb(); diff --git a/src/crates/contracts/product-domains/src/miniapp/builtin/assets/ppt-live/index.html b/src/crates/contracts/product-domains/src/miniapp/builtin/assets/ppt-live/index.html index b44f4cd2ec..54c109f42a 100644 --- a/src/crates/contracts/product-domains/src/miniapp/builtin/assets/ppt-live/index.html +++ b/src/crates/contracts/product-domains/src/miniapp/builtin/assets/ppt-live/index.html @@ -165,8 +165,6 @@

          Create or edit by prompt

)}
setIsHoveringPet(true)} onPointerLeave={() => setIsHoveringPet(false)} onPointerDown={onPetPointerDown} diff --git a/src/web-ui/src/app/components/GalleryLayout/GalleryLayout.scss b/src/web-ui/src/app/components/GalleryLayout/GalleryLayout.scss index 5fafa6b5d2..6771f0d6b5 100644 --- a/src/web-ui/src/app/components/GalleryLayout/GalleryLayout.scss +++ b/src/web-ui/src/app/components/GalleryLayout/GalleryLayout.scss @@ -153,26 +153,26 @@ $content-max: 1480px; .gallery-grid { display: grid; - grid-template-columns: repeat(auto-fill, minmax(var(--gallery-grid-min, 320px), 1fr)); + grid-template-columns: repeat(auto-fill, minmax(var(--gallery-grid-min), 1fr)); gap: $size-gap-3; align-content: start; &--skeleton { --skeleton-shimmer-0: rgba(var(--color-static-white-rgb), 0); - --skeleton-shimmer-peak: var(--color-overlay-white-10); + --skeleton-shimmer-peak: var(--color-overlay-white-12); pointer-events: none; } } .gallery-skeleton-card { - height: var(--gallery-skeleton-height, 140px); + height: var(--gallery-skeleton-height); border-radius: $size-radius-lg; background: var(--element-bg-subtle); border: 1px solid var(--border-subtle); position: relative; overflow: hidden; animation: gallery-item-in 0.28s $easing-decelerate both; - animation-delay: calc(var(--surface-stagger-index, 0) * 60ms); + animation-delay: calc(var(--surface-stagger-index) * 60ms); &::after { content: ''; @@ -185,7 +185,7 @@ $content-max: 1480px; var(--skeleton-shimmer-peak) 50%, var(--skeleton-shimmer-0) 100% ); - animation: gallery-shimmer 1.6s ease-in-out calc(var(--surface-stagger-index, 0) * 0.12s) infinite; + animation: gallery-shimmer 1.6s ease-in-out calc(var(--surface-stagger-index) * 0.12s) infinite; } } @@ -341,7 +341,7 @@ $content-max: 1480px; padding: 0 $size-gap-3; height: 28px; border: 1px solid var(--border-subtle); - background: var(--color-overlay-white-03); + background: var(--color-overlay-white-04); } .gallery-filter-chip.is-active, diff --git a/src/web-ui/src/app/components/MCPInteractionDialog/MCPInteractionDialog.scss b/src/web-ui/src/app/components/MCPInteractionDialog/MCPInteractionDialog.scss index a73af71dae..012fee1a45 100644 --- a/src/web-ui/src/app/components/MCPInteractionDialog/MCPInteractionDialog.scss +++ b/src/web-ui/src/app/components/MCPInteractionDialog/MCPInteractionDialog.scss @@ -11,7 +11,7 @@ align-items: center; gap: 8px; font-size: 12px; - color: var(--text-secondary); + color: var(--color-text-secondary); } .mcp-interaction-dialog__server { @@ -21,7 +21,7 @@ } .mcp-interaction-dialog__queue { - color: var(--text-tertiary); + color: var(--color-text-muted); } .mcp-interaction-dialog__section { @@ -32,16 +32,16 @@ .mcp-interaction-dialog__label { font-size: 12px; - color: var(--text-secondary); + color: var(--color-text-secondary); } .mcp-interaction-dialog__params { margin: 0; padding: 10px; border-radius: 8px; - background: var(--bg-tertiary); - border: 1px solid var(--border-primary); - color: var(--text-primary); + background: var(--color-bg-tertiary); + border: 1px solid var(--border-base); + color: var(--color-text-primary); font-size: 12px; line-height: 1.5; white-space: pre-wrap; @@ -55,9 +55,9 @@ min-height: 190px; padding: 10px; border-radius: 8px; - background: var(--bg-secondary); - border: 1px solid var(--border-primary); - color: var(--text-primary); + background: var(--color-bg-secondary); + border: 1px solid var(--border-base); + color: var(--color-text-primary); font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; font-size: 12px; line-height: 1.5; diff --git a/src/web-ui/src/app/components/NavPanel/NavPanel.scss b/src/web-ui/src/app/components/NavPanel/NavPanel.scss index 9f29149759..0c6b05855a 100644 --- a/src/web-ui/src/app/components/NavPanel/NavPanel.scss +++ b/src/web-ui/src/app/components/NavPanel/NavPanel.scss @@ -153,19 +153,19 @@ $_section-header-height: 24px; } &__mode-pill { - --bitfun-mode-pill-liquid-pro-bg: color-mix(in srgb, var(--color-primary) 22%, var(--element-bg-medium)); + --bitfun-mode-pill-liquid-pro-bg: color-mix(in srgb, var(--color-accent-500) 22%, var(--element-bg-medium)); --bitfun-mode-pill-liquid-assistant-bg: color-mix(in srgb, var(--color-accent-500) 28%, var(--element-bg-medium)); --bitfun-mode-pill-liquid-mid-bg: color-mix( in srgb, var(--color-accent-500) 16%, - color-mix(in srgb, var(--color-primary) 14%, var(--element-bg-medium)) + color-mix(in srgb, var(--color-accent-500) 14%, var(--element-bg-medium)) ); position: relative; width: 100%; min-width: 0; padding: 5px; - border: 1px solid color-mix(in srgb, var(--color-primary) 14%, transparent); + border: 1px solid color-mix(in srgb, var(--color-accent-500) 14%, transparent); border-radius: 999px; display: flex; align-items: stretch; @@ -181,7 +181,7 @@ $_section-header-height: 24px; &:hover { transform: translateY(-1px); - border-color: color-mix(in srgb, var(--color-primary) 28%, transparent); + border-color: color-mix(in srgb, var(--color-accent-500) 28%, transparent); box-shadow: inset 0 1px 0 color-mix(in srgb, var(--color-static-white) 5%, transparent), 0 10px 22px var(--color-overlay-black-12); } @@ -223,7 +223,7 @@ $_section-header-height: 24px; background: var(--bitfun-mode-pill-liquid-pro-bg); box-shadow: inset 0 1px 0 color-mix(in srgb, var(--color-static-white) 12%, transparent), - 0 2px 10px color-mix(in srgb, var(--color-primary) 12%, transparent); + 0 2px 10px color-mix(in srgb, var(--color-accent-500) 12%, transparent); transform: translate3d(0, 0, 0) scale(1, 1); transform-origin: 50% 50%; will-change: transform; @@ -464,7 +464,7 @@ $_section-header-height: 24px; background: var(--color-bg-elevated); border: 1px solid var(--border-subtle); border-radius: $size-radius-base; - box-shadow: 0 4px 12px var(--color-overlay-black-25); + box-shadow: 0 4px 12px var(--color-overlay-black-30); z-index: 9999; transform-origin: top left; animation: bitfun-footer-menu-in $motion-fast $easing-decelerate forwards; @@ -648,7 +648,7 @@ $_section-header-height: 24px; &:hover { color: var(--color-text-primary); background: color-mix(in srgb, var(--element-bg-soft) 94%, transparent); - box-shadow: 0 2px 8px var(--color-overlay-black-06); + box-shadow: 0 2px 8px var(--color-overlay-black-08); } &:active { @@ -663,7 +663,7 @@ $_section-header-height: 24px; &.is-active { color: var(--color-text-primary); background: color-mix(in srgb, var(--element-bg-soft) 94%, transparent); - box-shadow: 0 2px 8px var(--color-overlay-black-06); + box-shadow: 0 2px 8px var(--color-overlay-black-08); } } @@ -703,7 +703,7 @@ $_section-header-height: 24px; &:hover { color: var(--color-text-primary); background: color-mix(in srgb, var(--element-bg-soft) 94%, transparent); - box-shadow: 0 2px 8px var(--color-overlay-black-06); + box-shadow: 0 2px 8px var(--color-overlay-black-08); } &:active { @@ -720,7 +720,7 @@ $_section-header-height: 24px; color: var(--color-text-primary); background: color-mix(in srgb, var(--element-bg-soft) 94%, transparent); border: 1px solid var(--border-subtle); - box-shadow: 0 2px 8px var(--color-overlay-black-06); + box-shadow: 0 2px 8px var(--color-overlay-black-08); } &.has-customizing-apps { @@ -822,7 +822,7 @@ $_section-header-height: 24px; filter: saturate(1); opacity: 1; box-shadow: 0 8px 18px rgba(var(--color-static-black-rgb), 0.22), - 0 0 0 3px color-mix(in srgb, var(--color-primary) 28%, transparent); + 0 0 0 3px color-mix(in srgb, var(--color-accent-500) 28%, transparent); } &.is-customizing { @@ -832,7 +832,7 @@ $_section-header-height: 24px; &.is-active.is-customizing { box-shadow: 0 8px 18px rgba(var(--color-static-black-rgb), 0.22), - 0 0 0 3px color-mix(in srgb, var(--color-primary) 28%, transparent), + 0 0 0 3px color-mix(in srgb, var(--color-accent-500) 28%, transparent), 0 0 0 5px color-mix(in srgb, var(--color-accent-500) 28%, transparent); } @@ -943,7 +943,7 @@ $_section-header-height: 24px; background: var(--color-bg-elevated); border: 1px solid var(--border-subtle); border-radius: $size-radius-base; - box-shadow: 0 4px 12px var(--color-overlay-black-25); + box-shadow: 0 4px 12px var(--color-overlay-black-30); z-index: 9999; transform-origin: top left; animation: bitfun-footer-menu-in $motion-fast $easing-decelerate forwards; @@ -1399,7 +1399,7 @@ $_section-header-height: 24px; 72% { transform: translate3d(16%, 0, 0) scale(1.05, 0.96); border-radius: 17px; - background: color-mix(in srgb, var(--color-primary) 20%, var(--element-bg-medium)); + background: color-mix(in srgb, var(--color-accent-500) 20%, var(--element-bg-medium)); animation-timing-function: cubic-bezier(0.16, 1, 0.3, 1); } 100% { @@ -1700,7 +1700,7 @@ $_section-header-height: 24px; background: var(--color-bg-elevated); border: 1px solid var(--border-subtle); border-radius: $size-radius-base; - box-shadow: 0 4px 12px var(--color-overlay-black-25); + box-shadow: 0 4px 12px var(--color-overlay-black-30); z-index: 9999; transform-origin: bottom left; animation: bitfun-footer-menu-in $motion-fast $easing-decelerate forwards; diff --git a/src/web-ui/src/app/components/NavPanel/sections/sessions/SessionsSection.scss b/src/web-ui/src/app/components/NavPanel/sections/sessions/SessionsSection.scss index b70ed64cc7..b97f52ed3c 100644 --- a/src/web-ui/src/app/components/NavPanel/sections/sessions/SessionsSection.scss +++ b/src/web-ui/src/app/components/NavPanel/sections/sessions/SessionsSection.scss @@ -463,7 +463,7 @@ border-radius: $size-radius-base; background: var(--color-bg-elevated); border: 1px solid var(--border-subtle); - box-shadow: 0 4px 12px var(--color-overlay-black-25); + box-shadow: 0 4px 12px var(--color-overlay-black-30); z-index: 10000; transform-origin: top left; animation: bitfun-footer-menu-in $motion-fast $easing-decelerate forwards; diff --git a/src/web-ui/src/app/components/NavPanel/sections/sessions/SessionsSection.tsx b/src/web-ui/src/app/components/NavPanel/sections/sessions/SessionsSection.tsx index 0cfa183d1c..561b6e4ac4 100644 --- a/src/web-ui/src/app/components/NavPanel/sections/sessions/SessionsSection.tsx +++ b/src/web-ui/src/app/components/NavPanel/sections/sessions/SessionsSection.tsx @@ -7,7 +7,7 @@ import React, { lazy, Suspense, useCallback, useEffect, useMemo, useRef, useState } from 'react'; import { createPortal } from 'react-dom'; -import { Pencil, Trash2, Check, X, Bot, Code2, ClipboardList, Panda, MoreHorizontal, Loader2, Archive, Clock3 } from 'lucide-react'; +import { Pencil, Trash2, Check, X, Bot, Code2, ClipboardList, Panda, MoreHorizontal, Loader2, Archive, Clock3, Copy } from 'lucide-react'; import { IconButton, Input, Tooltip } from '@/component-library'; import { useI18n } from '@/infrastructure/i18n'; import { flowChatStore } from '../../../../../flow_chat/store/FlowChatStore'; @@ -48,6 +48,8 @@ import type { } from '@/flow_chat/utils/backgroundSubagentActivity'; import { computeFixedPopoverPosition } from '@/shared/utils/fixedPopoverViewport'; import { confirmWarning } from '@/component-library/components/ConfirmDialog/confirmService'; +import { notificationService } from '@/shared/notification-system'; +import { copyTextToClipboard } from '@/shared/utils/textSelection'; import { scheduleAfterStartupPaint, scheduleAfterStartupSignal } from '@/shared/utils/startupTaskScheduling'; import { SESSION_METADATA_DEFERRED_FALLBACK_MS, @@ -774,7 +776,7 @@ const SessionsSection: React.FC = ({ } const btn = e.currentTarget as HTMLElement; const rect = btn.getBoundingClientRect(); - const { top, left } = computeFixedPopoverPosition(rect, 160, 96, 4, 8); + const { top, left } = computeFixedPopoverPosition(rect, 160, 120, 4, 8); setSessionMenuPosition({ top, left }); setOpenMenuSessionId(sessionId); }, @@ -811,6 +813,19 @@ const SessionsSection: React.FC = ({ [t] ); + const handleCopySessionId = useCallback( + async (e: React.MouseEvent, sessionId: string) => { + e.stopPropagation(); + const copied = await copyTextToClipboard(sessionId); + if (copied) { + notificationService.success(t('nav.sessions.copySessionIdSuccess'), { duration: 2000 }); + } else { + notificationService.error(t('nav.sessions.copySessionIdFailed'), { duration: 3000 }); + } + }, + [t] + ); + const handleStartEdit = useCallback( (e: React.MouseEvent, session: Session) => { e.stopPropagation(); @@ -1186,10 +1201,21 @@ const SessionsSection: React.FC = ({ className="bitfun-nav-panel__inline-item-menu-item" onClick={e => { setOpenMenuSessionId(null); handleStartEdit(e, session); }} data-testid="nav-session-menu-rename" + data-session-id={session.sessionId} > {t('nav.sessions.rename')} +
@@ -708,23 +689,11 @@ export const SSHConnectionDialog: React.FC = ({ {credentialsPrompt && ( * { @@ -435,7 +435,7 @@ gap: 0.125rem; padding-bottom: 7px; margin-bottom: 0.06rem; - border-bottom: 1px solid var(--color-overlay-white-05); + border-bottom: 1px solid var(--color-overlay-white-08); animation: bitfun-chat-input-fade-in 0.1s ease-out; } @@ -521,7 +521,7 @@ border-radius: 6px; overflow: visible; background: var(--element-bg-medium); - border: 1px solid var(--color-border-subtle); + border: 1px solid var(--border-subtle); flex-shrink: 0; &:hover .bitfun-chat-input__image-chip-remove { @@ -566,12 +566,12 @@ transition: opacity 0.15s ease, transform 0.15s ease, background 0.15s ease; &:hover { - background: var(--color-bg-elevated-hover); + background: var(--element-bg-hover); } &:focus-visible { opacity: 1; - outline: 2px solid var(--color-accent); + outline: 2px solid var(--color-accent-500); outline-offset: 1px; } } @@ -694,7 +694,7 @@ &__agent-boost-empty { padding: var(--flowchat-card-expanded-pad-x) var(--flowchat-card-expanded-pad-x); font-size: var(--flowchat-font-size-2xs); - color: var(--color-text-tertiary); + color: var(--color-text-muted); text-align: center; line-height: var(--flowchat-support-line-height); @@ -837,7 +837,7 @@ 0 8px 32px rgba(var(--color-static-black-rgb), 0.35), 0 4px 16px var(--color-overlay-black-20), 0 0 1px var(--color-accent-300), - inset 0 1px 0 var(--color-overlay-white-05); + inset 0 1px 0 var(--color-overlay-white-08); backdrop-filter: blur(20px) saturate(1.2); -webkit-backdrop-filter: blur(20px) saturate(1.2); overflow: hidden; @@ -858,7 +858,7 @@ &__boost-submenu-spinner { animation: spin 0.8s linear infinite; - color: var(--color-accent-primary); + color: var(--color-accent-500); } &__boost-submenu-list { @@ -961,15 +961,15 @@ left: 0; min-width: 200px; background: var(--color-bg-elevated); - border: 1px solid var(--color-overlay-white-10); + border: 1px solid var(--color-overlay-white-12); border-radius: 6px; overflow: hidden; backdrop-filter: blur(20px) saturate(1.2); -webkit-backdrop-filter: blur(20px) saturate(1.2); box-shadow: 0 8px 32px var(--color-overlay-black-50), - 0 0 1px var(--color-overlay-white-10), - inset 0 1px 0 var(--color-overlay-white-06); + 0 0 1px var(--color-overlay-white-12), + inset 0 1px 0 var(--color-overlay-white-08); z-index: $z-dropdown; animation: mode-dropdown-fade-in 0.15s ease-out; @@ -1046,7 +1046,7 @@ &__mode-divider-line { flex: 1; height: 1px; - background: var(--color-overlay-white-06); + background: var(--color-overlay-white-08); } &__mode-divider-text { @@ -1109,7 +1109,7 @@ &__mode-submenu-dots { font-size: var(--flowchat-font-size-xxs); - color: var(--color-text-tertiary); + color: var(--color-text-muted); margin-left: auto; letter-spacing: 1px; } @@ -1154,7 +1154,7 @@ border: none; border-radius: 50%; background: transparent; - color: var(--color-text-tertiary); + color: var(--color-text-muted); cursor: pointer; opacity: 0; pointer-events: none; @@ -1229,8 +1229,8 @@ -webkit-backdrop-filter: blur(20px) saturate(1.2); box-shadow: 0 8px 32px var(--color-overlay-black-50), - 0 0 1px var(--color-overlay-white-10), - inset 0 1px 0 var(--color-overlay-white-06); + 0 0 1px var(--color-overlay-white-12), + inset 0 1px 0 var(--color-overlay-white-08); z-index: $z-dropdown; animation: mode-dropdown-fade-in 0.15s ease-out; } @@ -1250,7 +1250,7 @@ &__slash-command-hint { font-size: var(--flowchat-font-size-xxs); - color: var(--color-text-tertiary); + color: var(--color-text-muted); font-weight: 400; text-transform: none; letter-spacing: 0; @@ -1330,7 +1330,7 @@ font-size: var(--flowchat-font-size-2xs); font-weight: 500; color: color-mix(in srgb, var(--color-accent-500) 90%, var(--color-text-primary)); - font-family: var(--font-mono); + font-family: var(--font-family-mono); flex: 0 0 112px; min-width: 0; overflow: hidden; @@ -1371,7 +1371,7 @@ padding: var(--size-gap-4) var(--flowchat-card-expanded-pad-x); text-align: center; font-size: var(--flowchat-font-size-2xs); - color: var(--color-text-tertiary); + color: var(--color-text-muted); } // Popovers: heavy blur + dark shadows read as gray/black on light theme @@ -1386,7 +1386,7 @@ -webkit-backdrop-filter: none; box-shadow: 0 4px 18px var(--color-overlay-slate-04), - 0 1px 3px var(--color-overlay-slate-03), + 0 1px 3px var(--color-overlay-slate-04), inset 0 1px 0 var(--color-overlay-white-95); } @@ -1395,7 +1395,7 @@ } &__mode-submenu-content { - background: var(--color-overlay-slate-03); + background: var(--color-overlay-slate-04); .bitfun-chat-input__mode-option:hover:not(.bitfun-chat-input__mode-option--disabled) { background: var(--element-bg-soft); @@ -1410,7 +1410,7 @@ background: var(--color-overlay-slate-08); &:hover { - background: var(--color-overlay-slate-14); + background: var(--color-overlay-slate-12); } } } @@ -1462,7 +1462,7 @@ height: 20px; border-radius: 50%; border: none; - background: var(--color-overlay-white-06); + background: var(--color-overlay-white-08); color: rgba(var(--chat-input-send-dark-icon-rgb), 0.55); display: inline-flex; align-items: center; diff --git a/src/web-ui/src/flow_chat/components/ChatInput.tsx b/src/web-ui/src/flow_chat/components/ChatInput.tsx index a3ced11ea0..f7aebdcaa6 100644 --- a/src/web-ui/src/flow_chat/components/ChatInput.tsx +++ b/src/web-ui/src/flow_chat/components/ChatInput.tsx @@ -27,10 +27,9 @@ import { AcpPlanPanel } from './AcpPlanPanel'; import type { FlowChatState } from '../types/flow-chat'; import type { FileContext, DirectoryContext, ImageContext } from '@/types/context.ts'; import { SmartRecommendations } from './smart-recommendations'; -import { useCurrentWorkspace } from '@/infrastructure/contexts/WorkspaceContext'; -import { WorkspaceKind } from '@/shared/types'; +import { useCurrentWorkspace, useWorkspaceContext } from '@/infrastructure/contexts/WorkspaceContext'; import { createImageContextFromFile, createImageContextFromClipboard } from '../utils/imageUtils'; -import { isSlashCommand, stripSlashCommand } from '../utils/slashCommand'; +import { getSlashCommandPickerQuery, isSlashCommand, stripSlashCommand } from '../utils/slashCommand'; import { notificationService } from '@/shared/notification-system'; import { inputReducer, initialInputState } from '../reducers/inputReducer'; import { modeReducer, initialModeState } from '../reducers/modeReducer'; @@ -40,6 +39,7 @@ import { useChatInputState } from '../store/chatInputStateStore'; import { useInputHistoryStore } from '../store/inputHistoryStore'; import { startBtwThread } from '../services/BtwThreadService'; import { runUsageReportCommand } from '../services/usageReportService'; +import { buildImagePayload } from '../utils/imagePayload'; import { isGoalSlashCommand, parseGoalCommand } from '../services/goalService'; import { getHistorySessionOpenTransitionSnapshot, @@ -66,11 +66,15 @@ import { DEFAULT_CHAT_INPUT_MODE_CONFIG_PATH, normalizeUserDefaultChatInputModeId, resolveAvailableChatInputMode, + resolveChatInputModePolicy, + resolveSessionAssistantWorkspace, + resolveSwitchableChatInputModes, } from '../utils/chatInputMode'; import { useSceneStore } from '@/app/stores/sceneStore'; import type { SceneTabId } from '@/app/components/SceneBar/types'; import { useAgentsStore } from '@/app/scenes/agents/agentsStore'; import { configAPI } from '@/infrastructure/api/service-api/ConfigAPI'; +import { configManager } from '@/infrastructure/config'; import type { ModeSkillInfo } from '@/infrastructure/config/types'; import MCPAPI, { type MCPPrompt, type MCPPromptMessage, type MCPServerInfo } from '@/infrastructure/api/service-api/MCPAPI'; import { ChatInputWorkspaceStrip } from './ChatInputWorkspaceStrip'; @@ -619,6 +623,7 @@ export const ChatInput: React.FC = ({ const { transition, setQueuedInput } = useSessionStateMachineActions(effectiveTargetSessionId); const { workspace, workspacePath, workspaceName } = useCurrentWorkspace(); + const { openedWorkspaces } = useWorkspaceContext(); const chatStripRepositoryPath = useMemo(() => { const fromContext = (workspacePath || '').trim(); @@ -636,7 +641,17 @@ export const ChatInput: React.FC = ({ const [tokenUsage, setTokenUsage] = React.useState( getSessionContextUsageDisplay() ); - const isAssistantWorkspace = workspace?.workspaceKind === WorkspaceKind.Assistant; + const [isModelSwitching, setIsModelSwitching] = useState(false); + const isAssistantWorkspace = useMemo( + () => resolveSessionAssistantWorkspace({ + currentWorkspace: workspace, + sessionWorkspaceId: effectiveTargetSession?.workspaceId, + sessionWorkspacePath: effectiveTargetSession?.workspacePath, + sessionRemoteConnectionId: effectiveTargetSession?.remoteConnectionId, + openedWorkspaces: openedWorkspaces.values(), + }), + [effectiveTargetSession, openedWorkspaces, workspace], + ); const currentMode = modeState.current; const isModeDropdownOpen = modeState.dropdownOpen; const acpTargetAgentType = useMemo( @@ -647,16 +662,21 @@ export const ChatInput: React.FC = ({ const activeSessionMode = effectiveTargetSessionId ? acpTargetAgentType || flowChatState.sessions.get(effectiveTargetSessionId)?.mode : undefined; - const canSwitchModes = !isAssistantWorkspace && currentMode !== 'Cowork' && !isAcpTargetSession; + const chatInputModePolicy = useMemo( + () => resolveChatInputModePolicy({ + currentMode, + isAssistantWorkspace, + sessionMode: activeSessionMode, + isAcpTargetSession, + }), + [activeSessionMode, currentMode, isAcpTargetSession, isAssistantWorkspace], + ); + const canSwitchModes = chatInputModePolicy.canSwitchModes; - // Session-level mode policy: Cowork sessions are fixed; code sessions should not switch into Cowork. + // Session-level mode policy: fixed collaboration modes are not selectable boosts. const switchableModes = useMemo( - () => - modeState.available.filter(mode => - mode.id !== 'Cowork' && - (isAssistantWorkspace || mode.id !== 'Claw') - ), - [isAssistantWorkspace, modeState.available] + () => resolveSwitchableChatInputModes(modeState.available), + [modeState.available] ); // Stable refs for Shift+Tab mode cycling (avoids adding deps to handleKeyDown) @@ -682,6 +702,24 @@ export const ChatInput: React.FC = ({ const [resolvedModeSkillsLoading, setResolvedModeSkillsLoading] = useState(false); const [userDefaultModeId, setUserDefaultModeId] = useState(null); const [defaultModeSavingId, setDefaultModeSavingId] = useState(null); + const [computerUseEnabled, setComputerUseEnabled] = useState(true); + + useEffect(() => { + let cancelled = false; + const loadComputerUseEnabled = () => { + void configManager.getConfig('ai.computer_use_enabled').then((enabled) => { + if (!cancelled) setComputerUseEnabled(enabled ?? false); + }); + }; + loadComputerUseEnabled(); + const unsubscribe = configManager.onConfigChange((path) => { + if (path === 'ai.computer_use_enabled' || path === 'ai') loadComputerUseEnabled(); + }); + return () => { + cancelled = true; + unsubscribe(); + }; + }, []); const [skillsFlyoutOpen, setSkillsFlyoutOpen] = useState(false); const [skillsFlyoutLeft, setSkillsFlyoutLeft] = useState(false); @@ -1818,7 +1856,8 @@ export const ChatInput: React.FC = ({ if (text.startsWith('/')) { const afterSlash = text.slice(1); const hasWhitespace = /\s/.test(afterSlash); - const query = afterSlash.trimStart().split(/\s+/, 1)[0]?.toLowerCase?.() ?? ''; + const pickerQuery = getSlashCommandPickerQuery(text); + const query = pickerQuery ?? afterSlash.trimStart().split(/\s+/, 1)[0]?.toLowerCase?.() ?? ''; const matchedMcpPrompt = localSlashCommandsEnabled ? resolveTypedMcpPromptCommand(text) : null; @@ -1842,7 +1881,7 @@ export const ChatInput: React.FC = ({ // Only show the picker for "/..." patterns that are plausibly a command (/ or /b... /d...). // Once the user types a space (starts composing the real question), stop showing the picker // so Enter can submit "/btw ..." or "/DeepReview ..." instead of selecting from the picker. - if (!hasWhitespace && (query === '' || query.startsWith('b') || query.startsWith('d') || query.startsWith('g') || query.startsWith('u'))) { + if (pickerQuery !== null && (query === '' || query.startsWith('b') || query.startsWith('d') || query.startsWith('g') || query.startsWith('u'))) { setSlashCommandState({ isActive: true, kind: 'actions', @@ -1856,7 +1895,7 @@ export const ChatInput: React.FC = ({ } // When idle, keep the picker for mode switching, but don't interfere with executable slash commands. - if (!isBtwCommand && !isGoalCommand && !isCompactCommand && !isUsageCommand && !isDeepReviewCommand && !matchedMcpPrompt) { + if (pickerQuery !== null && !isBtwCommand && !isGoalCommand && !isCompactCommand && !isUsageCommand && !isDeepReviewCommand && !matchedMcpPrompt) { setSlashCommandState({ isActive: true, kind: 'all', @@ -1896,6 +1935,7 @@ export const ChatInput: React.FC = ({ const message = expandComposerSpecialTokens(originalMessage); const messageCharCount = getCharacterCount(message); const question = stripSlashCommand(message, '/btw').trim(); + const imagesForBtw = [...imageContexts]; // Clear input without adding to main history. dispatchInput({ type: 'CLEAR_VALUE' }); @@ -1923,12 +1963,26 @@ export const ChatInput: React.FC = ({ } try { + let imagePayload: Awaited>; + try { + imagePayload = await buildImagePayload(imagesForBtw); + } catch (error) { + log.error('Failed to upload images for /btw thread', { + imageCount: imagesForBtw.length, + error, + }); + notificationService.error('Image upload failed. Please try again.', { duration: 3000 }); + throw error; + } + const { childSessionId } = await startBtwThread({ parentSessionId: currentSessionId, workspacePath, question, modelId: 'fast', + imagePayload, }); + imagesForBtw.forEach(image => removeContext(image.id)); openBtwSessionInAuxPane({ childSessionId, parentSessionId: currentSessionId, @@ -1943,7 +1997,7 @@ export const ChatInput: React.FC = ({ pendingLargePastesRef.current = originalPendingLargePastes; dispatchInput({ type: 'SET_VALUE', payload: originalMessage }); } - }, [clearPendingLargePastes, currentSessionId, derivedState, expandComposerSpecialTokens, inputState.value, isBtwSession, setQueuedInput, t, workspacePath]); + }, [clearPendingLargePastes, currentSessionId, derivedState, expandComposerSpecialTokens, imageContexts, inputState.value, isBtwSession, removeContext, setQueuedInput, t, workspacePath]); const submitCompactFromInput = useCallback(async () => { if (!effectiveTargetSessionId || !effectiveTargetSession) { @@ -2433,6 +2487,10 @@ export const ChatInput: React.FC = ({ const handleCancelCurrentTask = useCallback(async () => { await FlowChatManager.getInstance().cancelCurrentTask(); }, []); + + const handleModelLoadingChange = useCallback((loading: boolean) => { + setIsModelSwitching(loading); + }, []); const handleSendOrCancel = useCallback(async () => { if (!derivedState) return; @@ -2446,6 +2504,10 @@ export const ChatInput: React.FC = ({ await handleCancelCurrentTask(); return; } + + // Block sending while model switch IPC is in-flight — the backend session may + // not yet reflect the newly selected model. + if (isModelSwitching) return; if (sendButtonMode === 'retry') { await transition(SessionExecutionEvent.RESET); @@ -2574,6 +2636,7 @@ export const ChatInput: React.FC = ({ } } }, [ + isModelSwitching, inputState.value, derivedState, handleCancelCurrentTask, @@ -2818,7 +2881,23 @@ export const ChatInput: React.FC = ({ }, [currentSessionId, isBtwSession, selectSlashCommandAction, t] ); - + + const handleBoostNewSession = useCallback( + async (e: React.SyntheticEvent) => { + e.stopPropagation(); + dispatchMode({ type: 'CLOSE_DROPDOWN' }); + try { + const sessionMode = currentSessionId + ? FlowChatStore.getInstance().getState().sessions.get(currentSessionId)?.mode + : undefined; + await FlowChatManager.getInstance().createChatSession({}, sessionMode); + } catch (error) { + log.error('Failed to create new session from boost menu', { error }); + } + }, + [currentSessionId] + ); + const handleKeyDown = useCallback((e: React.KeyboardEvent) => { // Local /btw shortcut (Ctrl/Cmd+Alt+B) should work even when ChatInput is focused. if ((e.ctrlKey || e.metaKey) && e.altKey && !e.shiftKey && e.key.toLowerCase() === 'b') { @@ -2966,11 +3045,6 @@ export const ChatInput: React.FC = ({ getRichTextInlineTriggerController()?.closeInlineTrigger?.(); } setSlashCommandState({ isActive: false, kind: 'modes', query: '', selectedIndex: 0 }); - - // For mode switching picker, "/" is just a trigger and should be cleared on cancel. - if (kind !== 'actions' && kind !== 'skills') { - dispatchInput({ type: 'CLEAR_VALUE' }); - } return; } @@ -3285,6 +3359,7 @@ export const ChatInput: React.FC = ({ @@ -3310,7 +3385,7 @@ export const ChatInput: React.FC = ({ = ({ = ({
{incrementalCodeModes.length > 0 && ( incrementalCodeModes.map(modeOption => { + const modeDisabled = modeOption.id === 'ComputerUse' && !computerUseEnabled; const modeDescription = - t(`chatInput.modeDescriptions.${modeOption.id}`, { defaultValue: '' }) || - modeOption.description || - modeOption.name; + modeDisabled + ? t('chatInput.computerUseDisabled') + : t(`chatInput.modeDescriptions.${modeOption.id}`, { defaultValue: '' }) || + modeOption.description || + modeOption.name; const modeName = t(`chatInput.modeNames.${modeOption.id}`, { defaultValue: '' }) || modeOption.name; const isDefaultMode = userDefaultModeId === modeOption.id; @@ -3775,9 +3853,10 @@ export const ChatInput: React.FC = ({ return (
{ e.stopPropagation(); + if (modeDisabled) return; requestModeChange(modeOption.id); }} > @@ -3933,6 +4012,21 @@ export const ChatInput: React.FC = ({
)} + + {(!currentSessionId || isBtwSession) && ( +
+ )} +
e.key === 'Enter' && handleBoostNewSession(e)} + > + + {t('chatInput.boostNewSession')} +
)} @@ -3946,6 +4040,7 @@ export const ChatInput: React.FC = ({ currentTokens={tokenUsage.current} maxTokens={tokenUsage.max} contextUsageSource={tokenUsage.source} + onLoadingChange={handleModelLoadingChange} />
diff --git a/src/web-ui/src/flow_chat/components/ChatInputPixelPet.scss b/src/web-ui/src/flow_chat/components/ChatInputPixelPet.scss index 38b88bfdd7..e0b2a89395 100644 --- a/src/web-ui/src/flow_chat/components/ChatInputPixelPet.scss +++ b/src/web-ui/src/flow_chat/components/ChatInputPixelPet.scss @@ -765,38 +765,129 @@ } } -/* ---------- Reduced motion ---------- */ +/* ---------- Reduced motion: pet always animates ---------- + The companion pet is a functional notification surface, not decoration. + It must always animate so users never miss attention-required states + (e.g. AskUserQuestion waiting for input). These !important longhand + overrides counteract the global prefers-reduced-motion * rule (which + clamps animation-duration to 0.01ms and iteration-count to 1). + Longhand properties are used instead of the animation shorthand to + preserve animation-delay set by nth-child rules. */ @media (prefers-reduced-motion: reduce) { - .bitfun-chat-input-pixel-pet__stage, - .bitfun-chat-input-pixel-pet__stage--transition, - .bitfun-chat-input-pixel-pet__stage[class*='--idle-'], - .bitfun-chat-input-pixel-pet__petdex { - animation: none !important; - transition: none; - } - + .bitfun-chat-input-pixel-pet__petdex, + .bitfun-chat-input-pixel-pet__petdex--rest, + .bitfun-chat-input-pixel-pet__petdex--working, + .bitfun-chat-input-pixel-pet__petdex--hover, + .bitfun-chat-input-pixel-pet__petdex--dragging, .bitfun-panda-head, - .bitfun-panda-head--rest, .bitfun-panda-head--analyzing, .bitfun-panda-head--waiting, .bitfun-panda-head--working, .bitfun-panda-head--hover, .bitfun-panda-head--dragging, - .bitfun-panda-head__zzz-glyph, + .bitfun-panda-head__zzz-glyph--a, + .bitfun-panda-head__zzz-glyph--b, + .bitfun-panda-head__zzz-glyph--c, .bitfun-panda-head--analyzing .bitfun-panda-head__ear--left, .bitfun-panda-head--analyzing .bitfun-panda-head__ear--right, + .bitfun-panda-head__think-pip, + .bitfun-panda-head--waiting .bitfun-panda-head__paw--front, + .bitfun-panda-head__wait-pip, + .bitfun-panda-head__sweat-drop, .bitfun-panda-head--working .bitfun-panda-head__ear--right, .bitfun-panda-head--hover .bitfun-panda-head__ear--left, .bitfun-panda-head--hover .bitfun-panda-head__ear--right, + .bitfun-panda-head__sparkle, .bitfun-panda-head--dragging .bitfun-panda-head__paw--front, .bitfun-panda-head--dragging .bitfun-panda-head__paw--rear, - .bitfun-panda-head--waiting .bitfun-panda-head__paw--front, + .bitfun-panda-head__drag-line { + animation-iteration-count: infinite !important; + } + + /* Single-animation elements grouped by duration */ + .bitfun-panda-head, + .bitfun-panda-head__zzz-glyph--a, + .bitfun-panda-head__zzz-glyph--b, + .bitfun-panda-head__zzz-glyph--c { + animation-duration: 6.8s !important; + } + .bitfun-panda-head__think-pip, - .bitfun-panda-head__wait-pip, .bitfun-panda-head__sweat-drop, - .bitfun-panda-head__sparkle, + .bitfun-panda-head--working .bitfun-panda-head__ear--right { + animation-duration: 3.2s !important; + } + + .bitfun-panda-head--analyzing .bitfun-panda-head__ear--left, + .bitfun-panda-head--analyzing .bitfun-panda-head__ear--right { + animation-duration: 2.8s !important; + } + + .bitfun-panda-head__sparkle { + animation-duration: 2.4s !important; + } + + .bitfun-panda-head__wait-pip { + animation-duration: 2.2s !important; + } + + .bitfun-panda-head--hover .bitfun-panda-head__ear--left, + .bitfun-panda-head--hover .bitfun-panda-head__ear--right { + animation-duration: 1.9s !important; + } + + .bitfun-panda-head--waiting .bitfun-panda-head__paw--front { + animation-duration: 1.8s !important; + } + .bitfun-panda-head__drag-line { - animation: none !important; + animation-duration: 1.44s !important; + } + + .bitfun-panda-head--dragging .bitfun-panda-head__paw--front, + .bitfun-panda-head--dragging .bitfun-panda-head__paw--rear { + animation-duration: 0.96s !important; + } + + /* Two-animation elements (comma-separated durations) */ + .bitfun-chat-input-pixel-pet__petdex { + animation-duration: 1.8s, 6s !important; + } + + .bitfun-chat-input-pixel-pet__petdex--rest { + animation-duration: 2.4s, 6.4s !important; + } + + .bitfun-chat-input-pixel-pet__petdex--working { + animation-duration: 1.16s, 0.56s !important; + } + + .bitfun-chat-input-pixel-pet__petdex--hover { + animation-duration: 1.44s, 1.9s !important; + } + + .bitfun-chat-input-pixel-pet__petdex--dragging { + animation-duration: 0.96s, 0.48s !important; + } + + .bitfun-panda-head--analyzing { + animation-duration: 5.2s, 4.8s !important; + } + + .bitfun-panda-head--waiting { + animation-duration: 5.2s, 6s !important; + } + + .bitfun-panda-head--working { + animation-duration: 5.2s, 0.36s !important; + } + + .bitfun-panda-head--hover { + animation-duration: 4.2s, 1.9s !important; + } + + .bitfun-panda-head--dragging { + animation-duration: 4.8s, 0.48s !important; } } diff --git a/src/web-ui/src/flow_chat/components/ChatInputWorkspaceStrip.scss b/src/web-ui/src/flow_chat/components/ChatInputWorkspaceStrip.scss index daaa74b310..0950749389 100644 --- a/src/web-ui/src/flow_chat/components/ChatInputWorkspaceStrip.scss +++ b/src/web-ui/src/flow_chat/components/ChatInputWorkspaceStrip.scss @@ -27,7 +27,7 @@ /* Room for descenders (g, y, p) */ /* Collapsed capsule uses 18px horizontal padding on `.bitfun-chat-input__box`; match so strip text lines up with pill content */ padding: 3px 18px 4px; - font-family: var(--font-sans); + font-family: var(--font-family-sans); font-size: var(--flowchat-font-size-xxs); line-height: var(--flowchat-support-line-height); font-weight: 400; @@ -92,25 +92,25 @@ } &__goal-btn--active.icon-btn { - color: var(--color-warning-500); + color: var(--color-warning); &:hover:not(:disabled) { transform: none; border-radius: 50%; - background: color-mix(in srgb, var(--color-warning-500) 12%, var(--element-bg-medium)); - color: var(--color-warning-500); + background: color-mix(in srgb, var(--color-warning) 12%, var(--element-bg-medium)); + color: var(--color-warning); opacity: 1; } } &__goal-btn--complete.icon-btn { - color: var(--color-success-500); + color: var(--color-success); &:hover:not(:disabled) { transform: none; border-radius: 50%; - background: color-mix(in srgb, var(--color-success-500) 12%, var(--element-bg-medium)); - color: var(--color-success-500); + background: color-mix(in srgb, var(--color-success) 12%, var(--element-bg-medium)); + color: var(--color-success); opacity: 1; } } diff --git a/src/web-ui/src/flow_chat/components/CodePreview.scss b/src/web-ui/src/flow_chat/components/CodePreview.scss index a394b7938d..10cff3ded7 100644 --- a/src/web-ui/src/flow_chat/components/CodePreview.scss +++ b/src/web-ui/src/flow_chat/components/CodePreview.scss @@ -6,8 +6,6 @@ @use '../../component-library/styles/_extended-mixins' as mixins; .code-preview { - --markdown-font-mono: var(--font-family-mono); - position: relative; width: 100%; background: transparent; @@ -31,7 +29,7 @@ white-space: pre-wrap !important; word-break: break-word !important; cursor: text; - font-family: var(--markdown-font-mono) !important; + font-family: var(--font-family-mono) !important; font-weight: 400 !important; // Prism themes mark many tokens bold; match markdown body / code blocks (regular weight). @@ -41,7 +39,7 @@ > span[style] { &:hover { - background-color: var(--color-overlay-white-05) !important; + background-color: var(--color-overlay-white-08) !important; } } @@ -75,7 +73,7 @@ overflow: visible; white-space: pre-wrap; word-break: break-word; - font-family: var(--markdown-font-mono); + font-family: var(--font-family-mono); font-size: var(--flowchat-font-size-sm); line-height: var(--flowchat-code-line-height); @@ -96,7 +94,7 @@ transition: background-color 0.15s ease, border-color 0.15s ease; &:hover { - background-color: var(--color-overlay-white-05); + background-color: var(--color-overlay-white-08); } &--highlighted { @@ -181,7 +179,7 @@ } pre code > span[style]:hover { - background-color: var(--color-overlay-black-06) !important; + background-color: var(--color-overlay-black-08) !important; } } @@ -190,6 +188,6 @@ } .code-preview__plain-line:hover { - background-color: var(--color-overlay-black-06); + background-color: var(--color-overlay-black-08); } } diff --git a/src/web-ui/src/flow_chat/components/CopyOutputButton.css b/src/web-ui/src/flow_chat/components/CopyOutputButton.css index c98f5a6a05..3f39985dad 100644 --- a/src/web-ui/src/flow_chat/components/CopyOutputButton.css +++ b/src/web-ui/src/flow_chat/components/CopyOutputButton.css @@ -54,18 +54,18 @@ } .copy-output-button.copied { - background: color-mix(in srgb, var(--flowchat-copy-success-color) 90%, transparent); - border-color: color-mix(in srgb, var(--flowchat-copy-success-color) 70%, transparent); + background: color-mix(in srgb, var(--color-success) 90%, transparent); + border-color: color-mix(in srgb, var(--color-success) 70%, transparent); color: var(--color-static-white); opacity: 1; pointer-events: auto; } .copy-output-button.copied:hover { - background: color-mix(in srgb, var(--flowchat-copy-success-color) 90%, transparent); - border-color: color-mix(in srgb, var(--flowchat-copy-success-color) 70%, transparent); + background: color-mix(in srgb, var(--color-success) 90%, transparent); + border-color: color-mix(in srgb, var(--color-success) 70%, transparent); transform: translateY(0); - box-shadow: 0 2px 8px color-mix(in srgb, var(--flowchat-copy-success-color) 40%, transparent); + box-shadow: 0 2px 8px color-mix(in srgb, var(--color-success) 40%, transparent); } .button-icon { diff --git a/src/web-ui/src/flow_chat/components/CoworkExampleCards.scss b/src/web-ui/src/flow_chat/components/CoworkExampleCards.scss index 63f9e2863a..8c1ff24d3f 100644 --- a/src/web-ui/src/flow_chat/components/CoworkExampleCards.scss +++ b/src/web-ui/src/flow_chat/components/CoworkExampleCards.scss @@ -3,7 +3,7 @@ */ .bitfun-cowork-example-cards { - border: 1px solid var(--color-border); + border: 1px solid var(--border-base); border-radius: calc(var(--flowchat-card-radius) + 2px); padding: var(--flowchat-card-expanded-pad-x) var(--flowchat-card-expanded-pad-x) var(--flowchat-card-expanded-pad-x) 0; margin-bottom: var(--flowchat-card-gap); @@ -55,7 +55,7 @@ height: 28px; border-radius: 8px; background: var(--color-bg-primary); - border: 1px solid var(--color-border); + border: 1px solid var(--border-base); display: flex; align-items: center; justify-content: center; diff --git a/src/web-ui/src/flow_chat/components/CurrentSessionTitle.scss b/src/web-ui/src/flow_chat/components/CurrentSessionTitle.scss index 744e0b84a6..2f447562b7 100644 --- a/src/web-ui/src/flow_chat/components/CurrentSessionTitle.scss +++ b/src/web-ui/src/flow_chat/components/CurrentSessionTitle.scss @@ -47,7 +47,7 @@ &:hover { background: var(--element-bg-soft); - color: var(--color-primary); + color: var(--color-accent-500); transform: translateY(-1px); } diff --git a/src/web-ui/src/flow_chat/components/FileMentionPicker.scss b/src/web-ui/src/flow_chat/components/FileMentionPicker.scss index db87d4e528..e3950c3651 100644 --- a/src/web-ui/src/flow_chat/components/FileMentionPicker.scss +++ b/src/web-ui/src/flow_chat/components/FileMentionPicker.scss @@ -23,7 +23,7 @@ 0 8px 32px rgba(var(--color-static-black-rgb), 0.35), 0 4px 16px var(--color-overlay-black-20), 0 0 1px color-mix(in srgb, var(--color-accent-500) 30%, transparent), - inset 0 1px 0 var(--color-overlay-white-05); + inset 0 1px 0 var(--color-overlay-white-08); overflow: hidden; z-index: 1100; display: flex; @@ -50,7 +50,7 @@ align-items: center; gap: var(--flowchat-inline-gap); padding: var(--flowchat-inline-gap) var(--flowchat-card-expanded-pad-x); - border-bottom: 1px solid var(--color-border-subtle); + border-bottom: 1px solid var(--border-subtle); color: var(--color-text-muted); font-size: var(--flowchat-font-size-xs); font-weight: 500; @@ -69,7 +69,7 @@ height: 18px; padding: 0; background: var(--color-overlay-white-08); - border: 1px solid var(--color-overlay-white-10); + border: 1px solid var(--color-overlay-white-12); border-radius: 4px; cursor: pointer; transition: all 0.15s ease; @@ -115,7 +115,7 @@ &__spinner { animation: spin 1s linear infinite; - color: var(--color-accent-primary); + color: var(--color-accent-500); @keyframes spin { from { transform: rotate(0deg); } @@ -137,7 +137,7 @@ transition: all 0.12s ease; &:hover { - background: var(--color-overlay-white-06); + background: var(--color-overlay-white-08); .file-mention-picker__expand-icon { opacity: 1; @@ -193,7 +193,7 @@ justify-content: center; gap: var(--flowchat-control-gap); padding: var(--flowchat-inline-gap) var(--flowchat-card-expanded-pad-x); - border-top: 1px solid var(--color-border-subtle); + border-top: 1px solid var(--border-subtle); font-size: var(--flowchat-font-size-xxs); color: var(--color-text-muted); @@ -230,7 +230,7 @@ border-color: var(--border-subtle); box-shadow: 0 4px 18px var(--color-overlay-slate-04), - 0 1px 3px var(--color-overlay-slate-03), + 0 1px 3px var(--color-overlay-slate-04), inset 0 1px 0 var(--color-overlay-white-95); &__back-btn { @@ -238,7 +238,7 @@ border-color: var(--color-overlay-slate-08); &:hover { - background: var(--color-overlay-slate-06); + background: var(--color-overlay-slate-08); } } @@ -251,14 +251,14 @@ background: var(--element-bg-medium); background: color-mix( in srgb, - var(--color-accent-primary) 22%, + var(--color-accent-500) 22%, var(--color-bg-elevated) 78% ); .file-mention-picker__item-name { color: color-mix( in srgb, - var(--color-accent-primary) 72%, + var(--color-accent-500) 72%, var(--color-text-primary) ); } @@ -267,7 +267,7 @@ .file-mention-picker__expand-icon { color: color-mix( in srgb, - var(--color-accent-primary) 65%, + var(--color-accent-500) 65%, var(--color-text-primary) ); opacity: 0.92; diff --git a/src/web-ui/src/flow_chat/components/FlowItemRenderer.tsx b/src/web-ui/src/flow_chat/components/FlowItemRenderer.tsx index d7d4929f5e..553730d908 100644 --- a/src/web-ui/src/flow_chat/components/FlowItemRenderer.tsx +++ b/src/web-ui/src/flow_chat/components/FlowItemRenderer.tsx @@ -5,7 +5,7 @@ */ import React from 'react'; -import { FlowItem, FlowTextItem, FlowToolItem, FlowThinkingItem, FlowUserSteeringItem } from '../types/flow-chat'; +import { FlowItem, FlowTextItem, FlowToolItem, FlowThinkingItem, FlowUserSteeringItem, type ToolRejectOptions } from '../types/flow-chat'; import { FlowTextBlock } from './FlowTextBlock'; import { FlowToolCard } from './FlowToolCard'; import { ModelThinkingDisplay } from '../tool-cards/ModelThinkingDisplay'; @@ -16,7 +16,7 @@ interface FlowItemRendererProps { onFileViewRequest?: (filePath: string) => void; onTabOpen?: (tabInfo: any) => void; onConfirm?: (toolId: string, updatedInput?: any, permissionOptionId?: string, approve?: boolean) => void; - onReject?: (toolId: string, permissionOptionId?: string) => void; + onReject?: (toolId: string, options?: ToolRejectOptions) => void; sessionId?: string; } diff --git a/src/web-ui/src/flow_chat/components/FlowTextBlock.scss b/src/web-ui/src/flow_chat/components/FlowTextBlock.scss index d75f73e645..dfc1c3388d 100644 --- a/src/web-ui/src/flow_chat/components/FlowTextBlock.scss +++ b/src/web-ui/src/flow_chat/components/FlowTextBlock.scss @@ -108,7 +108,7 @@ } .markdown-renderer .code-block-wrapper .copy-button.copy-success:hover { - color: var(--markdown-copy-success-color); + color: var(--color-success); transform: scale(1.08); } @@ -162,7 +162,7 @@ :root[data-theme-type='light'] .flow-text-block .markdown-renderer .code-block-wrapper .copy-button.copy-success:hover, [data-theme-type='light'] .flow-text-block .markdown-renderer .code-block-wrapper .copy-button.copy-success:hover, .light .flow-text-block .markdown-renderer .code-block-wrapper .copy-button.copy-success:hover { - color: var(--markdown-copy-success-color-light); + color: var(--color-success); } :root[data-theme='light'] .flow-text-block .markdown-renderer .inline-code, diff --git a/src/web-ui/src/flow_chat/components/FlowToolCard.tsx b/src/web-ui/src/flow_chat/components/FlowToolCard.tsx index 02959c4f82..0ee6927137 100644 --- a/src/web-ui/src/flow_chat/components/FlowToolCard.tsx +++ b/src/web-ui/src/flow_chat/components/FlowToolCard.tsx @@ -6,18 +6,19 @@ import React from 'react'; import { getToolCardComponent } from '../tool-cards'; import { getToolCardConfig } from '../tool-cards/toolCardMetadata'; -import type { FlowToolItem, ToolCardDisplayContext } from '../types/flow-chat'; +import type { FlowToolItem, ToolCardDisplayContext, ToolRejectOptions } from '../types/flow-chat'; import { createLogger } from '@/shared/utils/logger'; import { FlowToolCardErrorBoundary } from './FlowToolCardErrorBoundary'; import { useTranslation } from 'react-i18next'; import { getToolInterruptionNote } from '../utils/toolInterruption'; +import { ToolApprovalBar } from './ToolApprovalBar'; const log = createLogger('FlowToolCard'); interface FlowToolCardProps { toolItem: FlowToolItem; onConfirm?: (toolId: string, updatedInput?: any, permissionOptionId?: string, approve?: boolean) => void; - onReject?: (toolId: string, permissionOptionId?: string) => void; + onReject?: (toolId: string, options?: ToolRejectOptions) => void; onOpenInEditor?: (filePath: string) => void; onOpenInPanel?: (panelType: string, data: any) => void; onExpand?: (toolId: string) => void; @@ -62,8 +63,8 @@ export const FlowToolCard: React.FC = React.memo(({ onConfirm?.(toolItem.id, updatedInput, permissionOptionId, approve); }, [toolItem.id, toolItem.toolName, onConfirm]); - const handleReject = React.useCallback((permissionOptionId?: string) => { - onReject?.(toolItem.id, permissionOptionId); + const handleReject = React.useCallback((options?: ToolRejectOptions) => { + onReject?.(toolItem.id, options); }, [toolItem.id, onReject]); const handleExpand = React.useCallback(() => { @@ -86,8 +87,6 @@ export const FlowToolCard: React.FC = React.memo(({ toolItem={toolItem} config={config} interruptionNote={interruptionNote} - onConfirm={handleConfirm} - onReject={handleReject} onOpenInEditor={onOpenInEditor} onOpenInPanel={onOpenInPanel} onExpand={handleExpand} @@ -95,6 +94,11 @@ export const FlowToolCard: React.FC = React.memo(({ displayContext={displayContext} /> + {interruptionNote && !cardHandlesInterruptionNote && (
{interruptionNote} diff --git a/src/web-ui/src/flow_chat/components/FlowToolCardErrorBoundary.tsx b/src/web-ui/src/flow_chat/components/FlowToolCardErrorBoundary.tsx index dd5c08c1db..a06b0f35fd 100644 --- a/src/web-ui/src/flow_chat/components/FlowToolCardErrorBoundary.tsx +++ b/src/web-ui/src/flow_chat/components/FlowToolCardErrorBoundary.tsx @@ -86,7 +86,7 @@ function RenderFallback({ >
= memo(({ }); // Re-measure all rows when the container width changes (wrapping may differ). - const [containerWidth, setContainerWidth] = useState(0); - useEffect(() => { + // We use a generation counter to force re-renders WITHOUT calling + // virtualizer.measure() — measure() resets the measurements cache and + // would discard heights already captured by measureElement refs. + const [measureGeneration, setMeasureGeneration] = useState(0); + useLayoutEffect(() => { const el = containerRef.current; if (!el) return; - const ro = new ResizeObserver((entries) => { - const entry = entries[0]; - if (entry) { - const w = Math.round(entry.contentRect.width); - setContainerWidth((prev) => (prev === w ? prev : w)); - } + + // On mount, after measureElement refs have stored actual heights, + // force a synchronous re-render so getVirtualItems() uses those + // measurements instead of the 22 px estimates. Without this, + // wrapped long lines (common in .md files) cause rows to overlap + // because their translateY positions are based on wrong estimates. + setMeasureGeneration((g) => g + 1); + + const ro = new ResizeObserver(() => { + // Container width changed — wrapping may differ; re-render so the + // virtualizer picks up the new measureElement heights. + setMeasureGeneration((g) => g + 1); }); ro.observe(el); return () => ro.disconnect(); }, []); - // When width changes, invalidate cached measurements so rows re-measure. - useEffect(() => { - virtualizer.measure(); - }, [containerWidth]); // eslint-disable-line react-hooks/exhaustive-deps const handleLineClick = useCallback( (index: number, line: DiffLine) => { @@ -469,6 +474,13 @@ export const InlineDiffPreview: React.FC = memo(({ const totalHeight = virtualizer.getTotalSize(); const virtualItems = virtualizer.getVirtualItems(); + // measureGeneration is a render-trigger counter (bumped by useLayoutEffect + // and ResizeObserver). Referencing it here proves to TypeScript that the + // state is consumed; the virtualizer's getTotalSize / getVirtualItems are + // called unconditionally above and naturally pick up the measurements that + // measureElement refs store before the first paint. + void measureGeneration; + return (
{truncated.truncated && ( diff --git a/src/web-ui/src/flow_chat/components/ModelSelector.scss b/src/web-ui/src/flow_chat/components/ModelSelector.scss index 8809d82bc0..bc0117510b 100644 --- a/src/web-ui/src/flow_chat/components/ModelSelector.scss +++ b/src/web-ui/src/flow_chat/components/ModelSelector.scss @@ -68,7 +68,7 @@ } &__thinking-icon { - color: var(--color-text-tertiary); + color: var(--color-text-muted); flex-shrink: 0; } @@ -123,7 +123,7 @@ } &__chevron { - color: var(--color-text-tertiary); + color: var(--color-text-muted); flex-shrink: 0; transition: transform 0.2s ease; @@ -141,15 +141,15 @@ min-width: 220px; max-width: 280px; background: var(--color-bg-elevated); - border: 1px solid var(--color-overlay-white-10); + border: 1px solid var(--color-overlay-white-12); border-radius: 6px; overflow: hidden; backdrop-filter: blur(20px) saturate(1.2); -webkit-backdrop-filter: blur(20px) saturate(1.2); box-shadow: 0 8px 32px var(--color-overlay-black-50), - 0 0 1px var(--color-overlay-white-10), - inset 0 1px 0 var(--color-overlay-white-06); + 0 0 1px var(--color-overlay-white-12), + inset 0 1px 0 var(--color-overlay-white-08); z-index: $z-dropdown; animation: bitfun-model-dropdown-fade-in 0.15s ease-out; } @@ -159,7 +159,7 @@ align-items: center; justify-content: space-between; padding: 8px 12px; - border-bottom: 1px solid var(--color-overlay-white-06); + border-bottom: 1px solid var(--color-overlay-white-08); font-size: var(--flowchat-font-size-xxs); font-weight: 500; color: var(--color-text-muted); @@ -169,7 +169,7 @@ &__dropdown-hint { font-size: var(--flowchat-font-size-2xs); - color: var(--color-text-tertiary); + color: var(--color-text-muted); font-weight: 400; text-transform: none; letter-spacing: 0; @@ -265,7 +265,7 @@ &__option-desc { font-size: var(--flowchat-font-size-2xs); - color: var(--color-text-tertiary); + color: var(--color-text-muted); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; @@ -291,7 +291,7 @@ } &__option-thinking { - color: var(--color-text-tertiary); + color: var(--color-text-muted); flex-shrink: 0; } @@ -310,7 +310,7 @@ -webkit-backdrop-filter: none; box-shadow: 0 4px 18px var(--color-overlay-slate-04), - 0 1px 3px var(--color-overlay-slate-03), + 0 1px 3px var(--color-overlay-slate-04), inset 0 1px 0 var(--color-overlay-white-95); } @@ -322,7 +322,7 @@ background: var(--color-overlay-slate-08); &:hover { - background: var(--color-overlay-slate-14); + background: var(--color-overlay-slate-12); } } } diff --git a/src/web-ui/src/flow_chat/components/ModelSelector.tsx b/src/web-ui/src/flow_chat/components/ModelSelector.tsx index 1d390ab894..d3f0a09ec9 100644 --- a/src/web-ui/src/flow_chat/components/ModelSelector.tsx +++ b/src/web-ui/src/flow_chat/components/ModelSelector.tsx @@ -3,7 +3,9 @@ * Shows the active model and allows quick switching. * * Config linkage: - * - Unified logic: all modes use ai.agent_models[mode_id] + * - Model selection is shared across all agent types via ai.agent_models + * — switching in one session refreshes every existing agent mapping plus + * the current session's agent type, so new sessions of any type inherit it. * - Supports 'auto' | 'primary' | 'fast' | specific model IDs */ @@ -47,6 +49,8 @@ interface ModelSelectorProps { maxTokens?: number; /** Semantic source for the context usage number. */ contextUsageSource?: ContextUsageSource; + /** Called when model switching starts or completes, so the parent can gate sending. */ + onLoadingChange?: (loading: boolean) => void; } interface ModelInfo { @@ -174,6 +178,7 @@ export const ModelSelector: React.FC = ({ currentTokens = 0, maxTokens = 0, contextUsageSource, + onLoadingChange, }) => { const { t } = useTranslation('flow-chat'); const [allModels, setAllModels] = useState([]); @@ -187,6 +192,11 @@ export const ModelSelector: React.FC = ({ const dropdownRef = useRef(null); const portalDropdownRef = useRef(null); + + useEffect(() => { + onLoadingChange?.(loading); + }, [loading, onLoadingChange]); + const [dropdownStyle, setDropdownStyle] = useState({ position: 'fixed', visibility: 'hidden', @@ -506,16 +516,24 @@ export const ModelSelector: React.FC = ({ const currentAgentModels = await configManager.getConfig>('ai.agent_models') || {}; - const updatedAgentModels = { - ...currentAgentModels, - [currentMode]: modelId, - }; + // Refresh **every** agent mapping already present in the config so + // all configured agents pick up the new model. Also ensure the + // current session's agent type is included. No hardcoded list — + // any agent type added to the config in the future is covered + // automatically. + const updatedAgentModels = { ...currentAgentModels }; + for (const agentType of Object.keys(updatedAgentModels)) { + updatedAgentModels[agentType] = modelId; + } + updatedAgentModels[currentMode] = modelId; await configManager.setConfig('ai.agent_models', updatedAgentModels); setAgentModels(updatedAgentModels); if (sessionId) { const store = FlowChatStore.getInstance(); + // Update the frontend session model immediately so the UI reflects the + // switch without waiting for the backend IPC round-trip. store.updateSessionModelName(sessionId, modelId); const maxContextTokens = await getModelMaxTokens(modelId, currentMode); store.updateSessionMaxContextTokens(sessionId, maxContextTokens); diff --git a/src/web-ui/src/flow_chat/components/PlannerButton.scss b/src/web-ui/src/flow_chat/components/PlannerButton.scss index 76cb5a55f6..db6bc00814 100644 --- a/src/web-ui/src/flow_chat/components/PlannerButton.scss +++ b/src/web-ui/src/flow_chat/components/PlannerButton.scss @@ -44,9 +44,9 @@ height: 100%; background: linear-gradient(90deg, transparent, - var(--color-overlay-white-05), var(--color-overlay-white-08), - var(--color-overlay-white-05), + var(--color-overlay-white-08), + var(--color-overlay-white-08), transparent ); transition: left 0.6s cubic-bezier(0.4, 0, 0.2, 1); diff --git a/src/web-ui/src/flow_chat/components/RichTextInput.scss b/src/web-ui/src/flow_chat/components/RichTextInput.scss index 15a4891b42..8f008b2d0a 100644 --- a/src/web-ui/src/flow_chat/components/RichTextInput.scss +++ b/src/web-ui/src/flow_chat/components/RichTextInput.scss @@ -25,6 +25,14 @@ cursor: text; box-shadow: none !important; + // Hide scrollbars while keeping scroll functionality + scrollbar-width: none; // Firefox + -ms-overflow-style: none; // IE/Edge legacy + + &::-webkit-scrollbar { + display: none; // Chrome, Safari, Edge + } + // Fallback placeholder via CSS :empty (may not work after browser inserts a
) &:empty::before { content: attr(data-placeholder); diff --git a/src/web-ui/src/flow_chat/components/RichTextInput.test.tsx b/src/web-ui/src/flow_chat/components/RichTextInput.test.tsx index 632d52739c..bae4b5eb78 100644 --- a/src/web-ui/src/flow_chat/components/RichTextInput.test.tsx +++ b/src/web-ui/src/flow_chat/components/RichTextInput.test.tsx @@ -168,6 +168,28 @@ describeWithJsdom('RichTextInput external sync', () => { expect(editor.firstChild).toBe(originalTextNode); }); + it('preserves trailing spaces emitted by user input', async () => { + const onChange = vi.fn(); + + await act(async () => { + root.render( + {}} + /> + ); + }); + + const editor = container.querySelector('.rich-text-input'); + expect(editor).toBeInstanceOf(HTMLDivElement); + + await updateEditorText(editor as HTMLDivElement, '/b '); + + expect(onChange).toHaveBeenLastCalledWith('/b ', emptyContexts); + }); + it('replaces the DOM node when value changes externally', async () => { const harnessRef = createRef(); const editor = await renderHarness(harnessRef); diff --git a/src/web-ui/src/flow_chat/components/RichTextInput.tsx b/src/web-ui/src/flow_chat/components/RichTextInput.tsx index 82deea447f..fcd570fee8 100644 --- a/src/web-ui/src/flow_chat/components/RichTextInput.tsx +++ b/src/web-ui/src/flow_chat/components/RichTextInput.tsx @@ -64,6 +64,10 @@ function isWhitespaceCharacter(char: string | undefined): boolean { return !char || /\s/.test(char); } +function trimEdgeLineBreaks(text: string): string { + return text.replace(/^[\r\n]+/, '').replace(/[\r\n]+$/, ''); +} + function getContextDisplayName(context: ContextItem): string { switch (context.type) { case 'file': return context.fileName; @@ -448,7 +452,11 @@ export const RichTextInput = React.forwardRef = { + 'toolCards.approval.waiting': 'Waiting for confirmation', + 'toolCards.approval.confirm': 'Allow', + 'toolCards.approval.reject': 'Reject', + 'toolCards.approval.rejectWithInstruction': 'Reject with instruction', + 'toolCards.approval.enableAutoExecute': 'Enable tool auto execute', + 'toolCards.approval.confirmTooltip': 'Allow this tool run', + 'toolCards.approval.enableAutoExecuteTooltip': '开启工具自动执行', + 'toolCards.approval.autoExecuteEnabled': 'Tool auto execute enabled', + 'toolCards.approval.autoExecuteEnableFailed': 'Failed to enable tool auto execute', + 'toolCards.approval.rejectTooltip': 'Reject this tool run', + 'toolCards.approval.rejectWithInstructionTooltip': 'Reject and tell the assistant what to do next', + 'toolCards.approval.rejectInstructionLabel': 'Rejection instruction', + 'toolCards.approval.rejectInstructionPlaceholder': 'Tell the assistant what to do instead...', + 'toolCards.approval.rejectWithInstructionSubmit': 'Reject', + 'toolCards.approval.emptyInputTooltip': 'This tool has no executable input', + 'toolCards.approval.ariaLabel': 'Tool approval', + 'toolCards.approval.remaining': 'remaining {{time}}', +}; + +vi.mock('react-i18next', async () => { + const actual = await vi.importActual('react-i18next'); + return { + ...actual, + useTranslation: () => ({ + t: (key: string, options?: Record) => { + const template = messages[key] ?? (typeof options?.defaultValue === 'string' ? options.defaultValue : key); + return template.replace(/\{\{(\w+)\}\}/g, (_match, token) => String(options?.[token] ?? `{{${token}}}`)); + }, + }), + }; +}); + +vi.mock('@/component-library', () => ({ + IconButton: ({ + children, + tooltip, + ...props + }: React.ButtonHTMLAttributes & { tooltip?: React.ReactNode }) => ( + + ), +})); + +const setConfigMock = vi.hoisted(() => vi.fn()); +const notificationSuccessMock = vi.hoisted(() => vi.fn()); +const notificationErrorMock = vi.hoisted(() => vi.fn()); +const eventBusEmitMock = vi.hoisted(() => vi.fn()); + +vi.mock('@/infrastructure/config', () => ({ + configManager: { + setConfig: setConfigMock, + }, +})); + +vi.mock('@/shared/notification-system', () => ({ + notificationService: { + success: notificationSuccessMock, + error: notificationErrorMock, + }, +})); + +vi.mock('@/shared/utils/logger', () => ({ + createLogger: () => ({ + error: vi.fn(), + }), +})); + +vi.mock('@/infrastructure/event-bus', () => ({ + globalEventBus: { + emit: eventBusEmitMock, + }, +})); + +function execCommandItem(cmd: string, status: FlowToolItem['status'] = 'pending_confirmation'): FlowToolItem { + return { + id: 'tool-exec-1', + type: 'tool', + toolName: 'ExecCommand', + status, + timestamp: Date.now(), + toolCall: { + id: 'call-exec-1', + input: { cmd }, + }, + }; +} + +describe('ToolApprovalBar', () => { + let dom: JSDOM; + let container: HTMLDivElement; + let root: Root; + + beforeEach(() => { + setConfigMock.mockResolvedValue(undefined); + notificationSuccessMock.mockClear(); + notificationErrorMock.mockClear(); + eventBusEmitMock.mockClear(); + + dom = new JSDOM('
', { + pretendToBeVisual: true, + }); + vi.stubGlobal('window', dom.window); + vi.stubGlobal('document', dom.window.document); + vi.stubGlobal('HTMLElement', dom.window.HTMLElement); + (dom.window.HTMLElement.prototype as any).attachEvent = vi.fn(); + (dom.window.HTMLElement.prototype as any).detachEvent = vi.fn(); + + container = dom.window.document.getElementById('root') as HTMLDivElement; + root = createRoot(container); + }); + + afterEach(() => { + act(() => { + root.unmount(); + }); + vi.unstubAllGlobals(); + }); + + it('renders shared approval actions for pending ExecCommand tools', () => { + const onConfirm = vi.fn(); + const onReject = vi.fn(); + const input = { cmd: 'npm test' }; + + act(() => { + root.render( + , + ); + }); + + expect(container.textContent).toContain('Waiting for confirmation'); + + const allowButton = container.querySelector('button[aria-label="Allow"]') as HTMLButtonElement; + const rejectButton = container.querySelector('button[aria-label="Reject"]') as HTMLButtonElement; + + act(() => { + allowButton.dispatchEvent(new dom.window.MouseEvent('click', { bubbles: true })); + rejectButton.dispatchEvent(new dom.window.MouseEvent('click', { bubbles: true })); + }); + + expect(onConfirm).toHaveBeenCalledWith(input); + expect(onReject).toHaveBeenCalledWith(); + }); + + it('submits a rejection instruction from the shared approval bar', () => { + const onReject = vi.fn(); + + act(() => { + root.render( + , + ); + }); + + const rejectWithInstructionButton = container.querySelector( + 'button[aria-label="Reject with instruction"]', + ) as HTMLButtonElement; + act(() => { + rejectWithInstructionButton.dispatchEvent(new dom.window.MouseEvent('click', { bubbles: true })); + }); + + const input = container.querySelector('input[aria-label="Rejection instruction"]') as HTMLInputElement; + act(() => { + input.value = 'Use the status panel instead'; + input.dispatchEvent(new dom.window.Event('change', { bubbles: true })); + }); + + const submitButton = Array.from(container.querySelectorAll('button')) + .find((button) => button.textContent === 'Reject') as HTMLButtonElement; + act(() => { + submitButton.dispatchEvent(new dom.window.MouseEvent('click', { bubbles: true })); + }); + + expect(onReject).toHaveBeenCalledWith({ instruction: 'Use the status panel instead' }); + }); + + it('disables approval for an empty ExecCommand input', () => { + act(() => { + root.render(); + }); + + const allowButton = container.querySelector('button[aria-label="Allow"]') as HTMLButtonElement; + expect(allowButton.disabled).toBe(true); + expect(allowButton.title).toBe('This tool has no executable input'); + }); + + it('enables tool auto execute from the shared approval bar', async () => { + await act(async () => { + root.render(); + }); + + const autoExecuteButton = container.querySelector( + 'button[aria-label="Enable tool auto execute"]', + ) as HTMLButtonElement; + + expect(autoExecuteButton.title).toBe('开启工具自动执行'); + + await act(async () => { + autoExecuteButton.dispatchEvent(new dom.window.MouseEvent('click', { bubbles: true })); + }); + + expect(setConfigMock).toHaveBeenCalledWith('ai.skip_tool_confirmation', true); + expect(notificationSuccessMock).toHaveBeenCalledWith('Tool auto execute enabled', { duration: 2000 }); + expect(eventBusEmitMock).toHaveBeenCalledWith('mode:config:updated'); + }); + + it('does not render for non-confirmation statuses', () => { + act(() => { + root.render(); + }); + + expect(container.textContent).toBe(''); + }); + + it('shows remaining confirmation time when confirmation timeout is set', () => { + vi.useFakeTimers(); + vi.setSystemTime(new Date('2026-06-27T12:00:00.000Z')); + + try { + act(() => { + root.render( + , + ); + }); + + expect(container.textContent).toContain('Waiting for confirmation'); + expect(container.textContent).toContain('1m 5s'); + } finally { + vi.useRealTimers(); + } + }); + + it('hides remaining confirmation time when timeout is longer than ten minutes', () => { + vi.useFakeTimers(); + vi.setSystemTime(new Date('2026-06-27T12:00:00.000Z')); + + try { + act(() => { + root.render( + , + ); + }); + + expect(container.textContent).toContain('Waiting for confirmation'); + expect(container.textContent).not.toContain('remaining'); + } finally { + vi.useRealTimers(); + } + }); +}); diff --git a/src/web-ui/src/flow_chat/components/ToolApprovalBar.tsx b/src/web-ui/src/flow_chat/components/ToolApprovalBar.tsx new file mode 100644 index 0000000000..0039efb001 --- /dev/null +++ b/src/web-ui/src/flow_chat/components/ToolApprovalBar.tsx @@ -0,0 +1,260 @@ +import React, { useEffect, useMemo, useRef, useState } from 'react'; +import { Check, MessageSquareX, ShieldCheck, X } from 'lucide-react'; +import { useTranslation } from 'react-i18next'; +import { IconButton } from '@/component-library'; +import { configManager } from '@/infrastructure/config'; +import { notificationService } from '@/shared/notification-system'; +import { createLogger } from '@/shared/utils/logger'; +import { AcpPermissionActions } from '../tool-cards/AcpPermissionActions'; +import { hasAcpPermissionOptions } from '../tool-cards/AcpPermissionActions.utils'; +import type { FlowToolItem, ToolRejectOptions } from '../types/flow-chat'; +import './ToolApprovalBar.scss'; + +const log = createLogger('ToolApprovalBar'); + +interface ToolApprovalBarProps { + toolItem: FlowToolItem; + onConfirm?: (updatedInput?: any, permissionOptionId?: string, approve?: boolean) => void; + onReject?: (options?: ToolRejectOptions) => void; +} + +function hasPendingToolConfirmation(toolItem: FlowToolItem): boolean { + return toolItem.status === 'pending_confirmation'; +} + +function formatRemainingConfirmationTime(remainingMs: number): string { + if (remainingMs < 1000) return '1s'; + const totalSeconds = Math.ceil(remainingMs / 1000); + if (totalSeconds < 60) return `${totalSeconds}s`; + const minutes = Math.floor(totalSeconds / 60); + const seconds = totalSeconds % 60; + return seconds > 0 ? `${minutes}m ${seconds}s` : `${minutes}m`; +} + +export const ToolApprovalBar: React.FC = ({ + toolItem, + onConfirm, + onReject, +}) => { + const { t } = useTranslation('flow-chat'); + const [nowMs, setNowMs] = useState(() => Date.now()); + const [showInstructionInput, setShowInstructionInput] = useState(false); + const [instruction, setInstruction] = useState(''); + const [isEnablingAutoExecute, setIsEnablingAutoExecute] = useState(false); + const instructionInputRef = useRef(null); + const input = toolItem.toolCall?.input; + const hasPermissionOptions = hasAcpPermissionOptions(toolItem); + const canConfirm = useMemo(() => { + if (toolItem.toolName === 'Bash') { + const command = typeof input?.command === 'string' ? input.command : ''; + return Boolean(command.trim()); + } + + if (toolItem.toolName === 'ExecCommand') { + const command = typeof input?.cmd === 'string' ? input.cmd : ''; + return Boolean(command.trim()); + } + + return true; + }, [input, toolItem.toolName]); + const confirmationTimeoutAt = toolItem.confirmationTimeoutAt; + const remainingConfirmationMs = useMemo(() => { + if (typeof confirmationTimeoutAt !== 'number') { + return null; + } + return Math.max(0, confirmationTimeoutAt - nowMs); + }, [confirmationTimeoutAt, nowMs]); + const confirmationCountdownLabel = useMemo(() => { + if (remainingConfirmationMs == null) { + return null; + } + if (remainingConfirmationMs > 10 * 60 * 1000) { + return null; + } + return formatRemainingConfirmationTime(remainingConfirmationMs); + }, [remainingConfirmationMs]); + + useEffect(() => { + if (typeof confirmationTimeoutAt !== 'number') { + return undefined; + } + + const tick = () => setNowMs(Date.now()); + tick(); + const handle = window.setInterval(tick, 1000); + return () => window.clearInterval(handle); + }, [confirmationTimeoutAt]); + + if (!hasPendingToolConfirmation(toolItem)) { + return null; + } + + const handleConfirm = (event: React.MouseEvent) => { + event.preventDefault(); + event.stopPropagation(); + + if (!canConfirm) { + return; + } + + onConfirm?.(input); + }; + + const handleReject = (event: React.MouseEvent) => { + event.preventDefault(); + event.stopPropagation(); + onReject?.(); + }; + + const handleEnableAutoExecute = async (event: React.MouseEvent) => { + event.preventDefault(); + event.stopPropagation(); + + if (isEnablingAutoExecute) { + return; + } + + setIsEnablingAutoExecute(true); + try { + await configManager.setConfig('ai.skip_tool_confirmation', true); + notificationService.success(t('toolCards.approval.autoExecuteEnabled'), { duration: 2000 }); + const { globalEventBus } = await import('@/infrastructure/event-bus'); + globalEventBus.emit('mode:config:updated'); + } catch (error) { + log.error('Failed to enable tool auto execute', { error }); + notificationService.error(t('toolCards.approval.autoExecuteEnableFailed')); + } finally { + setIsEnablingAutoExecute(false); + } + }; + + const handleRejectWithInstruction = (event: React.MouseEvent) => { + event.preventDefault(); + event.stopPropagation(); + + const trimmedInstruction = (instructionInputRef.current?.value ?? instruction).trim(); + onReject?.(trimmedInstruction ? { instruction: trimmedInstruction } : undefined); + }; + + const handleToggleInstructionInput = (event: React.MouseEvent) => { + event.preventDefault(); + event.stopPropagation(); + setShowInstructionInput((value) => !value); + }; + + const handleInstructionKeyDown = (event: React.KeyboardEvent) => { + event.stopPropagation(); + + if (event.key === 'Enter') { + event.preventDefault(); + const trimmedInstruction = event.currentTarget.value.trim(); + onReject?.(trimmedInstruction ? { instruction: trimmedInstruction } : undefined); + } else if (event.key === 'Escape') { + event.preventDefault(); + setShowInstructionInput(false); + setInstruction(''); + } + }; + + return ( +
+
+ + {t('toolCards.approval.waiting')} + {confirmationCountdownLabel ? ( + + {' '} + · {t('toolCards.approval.remaining', { time: confirmationCountdownLabel })} + + ) : null} + + + {hasPermissionOptions ? ( + + ) : ( + <> + + + + + + + + + + +
+ {showInstructionInput && !hasPermissionOptions && ( +
+ setInstruction(event.target.value)} + onClick={(event) => event.stopPropagation()} + onKeyDown={handleInstructionKeyDown} + placeholder={t('toolCards.approval.rejectInstructionPlaceholder')} + aria-label={t('toolCards.approval.rejectInstructionLabel')} + /> + +
+ )} +
+ ); +}; + +export default ToolApprovalBar; diff --git a/src/web-ui/src/flow_chat/components/UserMessage.css b/src/web-ui/src/flow_chat/components/UserMessage.css index c90c676b54..332cc4cc60 100644 --- a/src/web-ui/src/flow_chat/components/UserMessage.css +++ b/src/web-ui/src/flow_chat/components/UserMessage.css @@ -74,12 +74,12 @@ gap: 12px; margin-top: 8px; padding-top: 8px; - border-top: 1px solid var(--color-overlay-black-06); + border-top: 1px solid var(--color-overlay-black-08); } .message-timestamp { font-size: 0.75em; - color: var(--color-text-tertiary); + color: var(--color-text-muted); } .message-snapshot-action { diff --git a/src/web-ui/src/flow_chat/components/UserMessage.scss b/src/web-ui/src/flow_chat/components/UserMessage.scss index 0bc71dd341..fcc008814a 100644 --- a/src/web-ui/src/flow_chat/components/UserMessage.scss +++ b/src/web-ui/src/flow_chat/components/UserMessage.scss @@ -109,7 +109,7 @@ &:hover { transform: translateY(-1px); - box-shadow: 0 2px 4px var(--color-overlay-black-10); + box-shadow: 0 2px 4px var(--color-overlay-black-12); } } diff --git a/src/web-ui/src/flow_chat/components/background-command/BackgroundCommandInputDialog.scss b/src/web-ui/src/flow_chat/components/background-command/BackgroundCommandInputDialog.scss index 52d87d7b93..fe70796f7b 100644 --- a/src/web-ui/src/flow_chat/components/background-command/BackgroundCommandInputDialog.scss +++ b/src/web-ui/src/flow_chat/components/background-command/BackgroundCommandInputDialog.scss @@ -37,7 +37,7 @@ .bitfun-textarea__field { min-height: 96px; resize: vertical; - font-family: var(--font-mono); + font-family: var(--font-family-mono); font-size: 12px; line-height: 1.5; } diff --git a/src/web-ui/src/flow_chat/components/background-command/BackgroundCommandOutputPanel.scss b/src/web-ui/src/flow_chat/components/background-command/BackgroundCommandOutputPanel.scss index ec196ee7ca..96fecf4330 100644 --- a/src/web-ui/src/flow_chat/components/background-command/BackgroundCommandOutputPanel.scss +++ b/src/web-ui/src/flow_chat/components/background-command/BackgroundCommandOutputPanel.scss @@ -6,8 +6,8 @@ height: 100%; min-height: 0; background: - radial-gradient(circle at 8% 0%, color-mix(in srgb, var(--accent-primary) 10%, transparent) 0, transparent 34%), - linear-gradient(180deg, color-mix(in srgb, var(--color-bg-elevated) 96%, transparent), var(--color-bg-base)); + radial-gradient(circle at 8% 0%, color-mix(in srgb, var(--color-accent-500) 10%, transparent) 0, transparent 34%), + linear-gradient(180deg, color-mix(in srgb, var(--color-bg-elevated) 96%, transparent), var(--color-bg-primary)); color: var(--color-text-primary); &__header { @@ -95,8 +95,8 @@ width: 28px; height: 28px; border-radius: 10px; - background: color-mix(in srgb, var(--accent-primary) 14%, transparent); - color: var(--accent-primary); + background: color-mix(in srgb, var(--color-accent-500) 14%, transparent); + color: var(--color-accent-500); flex: 0 0 auto; } @@ -150,9 +150,9 @@ } &__error { - border: 1px solid color-mix(in srgb, var(--color-danger) 30%, transparent); - background: color-mix(in srgb, var(--color-danger) 10%, transparent); - color: var(--color-danger); + border: 1px solid color-mix(in srgb, var(--color-error) 30%, transparent); + background: color-mix(in srgb, var(--color-error) 10%, transparent); + color: var(--color-error); } &__output { diff --git a/src/web-ui/src/flow_chat/components/btw/BtwSessionPanel.scss b/src/web-ui/src/flow_chat/components/btw/BtwSessionPanel.scss index 0d9e466d27..d35589f65e 100644 --- a/src/web-ui/src/flow_chat/components/btw/BtwSessionPanel.scss +++ b/src/web-ui/src/flow_chat/components/btw/BtwSessionPanel.scss @@ -8,7 +8,7 @@ display: flex; flex-direction: column; height: 100%; - background: var(--color-bg-flowchat); + background: var(--color-bg-scene); color: var(--color-text-primary); &--empty { @@ -207,7 +207,7 @@ font-size: var(--tool-card-action-font-size); font-weight: 500; cursor: pointer; - box-shadow: 0 2px 12px var(--color-overlay-black-10); + box-shadow: 0 2px 12px var(--color-overlay-black-12); transition: transform 0.2s ease, box-shadow 0.2s ease; &:hover { diff --git a/src/web-ui/src/flow_chat/components/btw/DeepReviewActionBar.scss b/src/web-ui/src/flow_chat/components/btw/DeepReviewActionBar.scss index ced87e74e4..3e63701522 100644 --- a/src/web-ui/src/flow_chat/components/btw/DeepReviewActionBar.scss +++ b/src/web-ui/src/flow_chat/components/btw/DeepReviewActionBar.scss @@ -16,7 +16,7 @@ line-height: var(--flowchat-support-line-height); backdrop-filter: blur(10px); -webkit-backdrop-filter: blur(10px); - box-shadow: 0 4px 24px var(--color-overlay-black-15), 0 0 0 1px var(--color-overlay-white-06); + box-shadow: 0 4px 24px var(--color-overlay-black-15), 0 0 0 1px var(--color-overlay-white-08); overflow-x: hidden; overflow-y: auto; overscroll-behavior: contain; @@ -1014,8 +1014,8 @@ height: 6px; margin-top: 6px; border-radius: 999px; - background: var(--color-text-tertiary); - box-shadow: 0 0 0 3px color-mix(in srgb, var(--color-text-tertiary) 10%, transparent); + background: var(--color-text-muted); + box-shadow: 0 0 0 3px color-mix(in srgb, var(--color-text-muted) 10%, transparent); } &--warning { diff --git a/src/web-ui/src/flow_chat/components/codePreviewPrismTheme.ts b/src/web-ui/src/flow_chat/components/codePreviewPrismTheme.ts index 1150f55e5d..a7b8472d4b 100644 --- a/src/web-ui/src/flow_chat/components/codePreviewPrismTheme.ts +++ b/src/web-ui/src/flow_chat/components/codePreviewPrismTheme.ts @@ -4,9 +4,9 @@ import type { CSSProperties } from 'react'; import { buildSharedPrismStyle } from '@/shared/prism/prismTheme'; -/** Match `.markdown-renderer` code blocks (`Markdown.scss` --markdown-font-mono). */ +/** Match `.markdown-renderer` code blocks. */ export const CODE_PREVIEW_FONT_FAMILY = - 'var(--markdown-font-mono)'; + 'var(--font-family-mono)'; export function buildCodePreviewPrismStyle(isLight: boolean): Record { return buildSharedPrismStyle(isLight, { diff --git a/src/web-ui/src/flow_chat/components/modern/ExploreGroupRenderer.tsx b/src/web-ui/src/flow_chat/components/modern/ExploreGroupRenderer.tsx index 1e8c6d20dc..81fc228c48 100644 --- a/src/web-ui/src/flow_chat/components/modern/ExploreGroupRenderer.tsx +++ b/src/web-ui/src/flow_chat/components/modern/ExploreGroupRenderer.tsx @@ -7,7 +7,7 @@ import React, { useRef, useMemo, useCallback, useEffect, useState } from 'react'; import { ChevronRight } from 'lucide-react'; import { useTranslation } from 'react-i18next'; -import type { FlowItem, FlowToolItem, FlowTextItem, FlowThinkingItem } from '../../types/flow-chat'; +import type { FlowItem, FlowToolItem, FlowTextItem, FlowThinkingItem, ToolRejectOptions } from '../../types/flow-chat'; import type { ExploreGroupData } from '../../store/modernFlowChatStore'; import { createLogger } from '@/shared/utils/logger'; @@ -313,9 +313,9 @@ const ExploreItemRenderer = React.memo(({ item, turnId } }, [onToolConfirm]); - const handleReject = useCallback(async (toolId: string, permissionOptionId?: string) => { + const handleReject = useCallback(async (toolId: string, options?: ToolRejectOptions) => { if (onToolReject) { - await onToolReject(toolId, permissionOptionId); + await onToolReject(toolId, options); } }, [onToolReject]); diff --git a/src/web-ui/src/flow_chat/components/modern/ExploreRegion.scss b/src/web-ui/src/flow_chat/components/modern/ExploreRegion.scss index 74a066479c..c3cc715f55 100644 --- a/src/web-ui/src/flow_chat/components/modern/ExploreRegion.scss +++ b/src/web-ui/src/flow_chat/components/modern/ExploreRegion.scss @@ -22,17 +22,17 @@ align-items: center; gap: 4px; padding: 0; - color: var(--text-tertiary); + color: var(--color-text-muted); font-size: var(--flowchat-font-size-sm); line-height: var(--flowchat-support-line-height); cursor: pointer; user-select: none; - border-radius: var(--radius-sm); + border-radius: var(--size-radius-sm); transition: color 0.15s ease, background 0.15s ease; &:hover { - background: var(--bg-hover); - color: var(--text-secondary); + background: var(--element-bg-hover); + color: var(--color-text-secondary); } } @@ -84,7 +84,7 @@ border-radius: 2px; &:hover { - background: var(--border-hover); + background: var(--border-medium); } } } @@ -118,8 +118,8 @@ z-index: 3; background: linear-gradient( to bottom, - var(--color-bg-flowchat) 0%, - color-mix(in srgb, var(--color-bg-flowchat) 60%, transparent) 50%, + var(--color-bg-scene) 0%, + color-mix(in srgb, var(--color-bg-scene) 60%, transparent) 50%, transparent 100% ); } @@ -137,8 +137,8 @@ z-index: 3; background: linear-gradient( to top, - var(--color-bg-flowchat) 0%, - color-mix(in srgb, var(--color-bg-flowchat) 60%, transparent) 50%, + var(--color-bg-scene) 0%, + color-mix(in srgb, var(--color-bg-scene) 60%, transparent) 50%, transparent 100% ); } diff --git a/src/web-ui/src/flow_chat/components/modern/ExportImageButton.scss b/src/web-ui/src/flow_chat/components/modern/ExportImageButton.scss index 8d5d0a950e..e0cdc2ae25 100644 --- a/src/web-ui/src/flow_chat/components/modern/ExportImageButton.scss +++ b/src/web-ui/src/flow_chat/components/modern/ExportImageButton.scss @@ -11,7 +11,7 @@ Padding matches ModelRoundItem (3rem ≈ 48px each side) so the effective content width is identical to the live chat. */ padding: 24px 48px; - background: var(--color-bg-flowchat); + background: var(--color-bg-scene); box-sizing: border-box; font-family: var(--font-family-sans); /* Use a fixed 800px layout context so text wraps at exactly the same diff --git a/src/web-ui/src/flow_chat/components/modern/ExportImageButton.tsx b/src/web-ui/src/flow_chat/components/modern/ExportImageButton.tsx index 45fa62e8ff..46ae59c835 100644 --- a/src/web-ui/src/flow_chat/components/modern/ExportImageButton.tsx +++ b/src/web-ui/src/flow_chat/components/modern/ExportImageButton.tsx @@ -220,7 +220,7 @@ export const ExportImageButton: React.FC = ({ // Get theme background color. const computedStyle = getComputedStyle(document.documentElement); - const bgColor = computedStyle.getPropertyValue('--color-bg-flowchat').trim() || FLOWCHAT_CAPTURE_FALLBACK_COLOR.background; + const bgColor = computedStyle.getPropertyValue('--color-bg-scene').trim() || FLOWCHAT_CAPTURE_FALLBACK_COLOR.background; // Pre-load the logo as an HTMLImageElement. We do NOT try to embed it // inside the captured DOM (unreliable with /data URLs inside an diff --git a/src/web-ui/src/flow_chat/components/modern/FlowChatContext.tsx b/src/web-ui/src/flow_chat/components/modern/FlowChatContext.tsx index f714baa2b8..e19f4fbe5f 100644 --- a/src/web-ui/src/flow_chat/components/modern/FlowChatContext.tsx +++ b/src/web-ui/src/flow_chat/components/modern/FlowChatContext.tsx @@ -5,7 +5,7 @@ import { createContext, useContext } from 'react'; import type React from 'react'; -import type { FlowChatConfig, Session } from '../../types/flow-chat'; +import type { FlowChatConfig, Session, ToolRejectOptions } from '../../types/flow-chat'; import type { LineRange } from '@/component-library'; export interface FlowChatContextValue { @@ -18,7 +18,7 @@ export interface FlowChatContextValue { // Tool actions onToolConfirm?: (toolId: string, updatedInput?: any, permissionOptionId?: string, approve?: boolean) => Promise; - onToolReject?: (toolId: string, permissionOptionId?: string) => Promise; + onToolReject?: (toolId: string, options?: ToolRejectOptions) => Promise; // Session info sessionId?: string; diff --git a/src/web-ui/src/flow_chat/components/modern/FlowChatHeader.scss b/src/web-ui/src/flow_chat/components/modern/FlowChatHeader.scss index 94d8ac9132..b44d636dd9 100644 --- a/src/web-ui/src/flow_chat/components/modern/FlowChatHeader.scss +++ b/src/web-ui/src/flow_chat/components/modern/FlowChatHeader.scss @@ -191,7 +191,7 @@ gap: 0; padding-left: 2px; margin-left: 2px; - border-left: 1px solid var(--color-border-subtle); + border-left: 1px solid var(--border-subtle); } &__search-count { @@ -334,7 +334,7 @@ justify-content: space-between; gap: $size-gap-1; padding: 2px 2px 2px $size-gap-2; - color: var(--color-text-tertiary); + color: var(--color-text-muted); font-size: var(--flowchat-font-size-xs); } @@ -360,11 +360,11 @@ } &--danger:not(:disabled):hover { - color: var(--color-danger); - background: color-mix(in srgb, var(--color-danger) 12%, transparent); + color: var(--color-error); + background: color-mix(in srgb, var(--color-error) 12%, transparent); svg { - color: var(--color-danger); + color: var(--color-error); } } } @@ -389,7 +389,7 @@ } &--active { - background: color-mix(in srgb, var(--accent-primary) 12%, var(--element-bg-soft)); + background: color-mix(in srgb, var(--color-accent-500) 12%, var(--element-bg-soft)); } } @@ -537,11 +537,11 @@ } &--danger:hover:not(:disabled) { - color: var(--color-danger); - background: color-mix(in srgb, var(--color-danger) 12%, transparent); + color: var(--color-error); + background: color-mix(in srgb, var(--color-error) 12%, transparent); svg { - color: var(--color-danger); + color: var(--color-error); } } } diff --git a/src/web-ui/src/flow_chat/components/modern/FlowChatHeader.test.tsx b/src/web-ui/src/flow_chat/components/modern/FlowChatHeader.test.tsx new file mode 100644 index 0000000000..b819142e6b --- /dev/null +++ b/src/web-ui/src/flow_chat/components/modern/FlowChatHeader.test.tsx @@ -0,0 +1,149 @@ +// @vitest-environment jsdom + +import React, { act } from 'react'; +import { createRoot, type Root } from 'react-dom/client'; +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; +import { FlowChatHeader, type FlowChatHeaderProps } from './FlowChatHeader'; + +globalThis.IS_REACT_ACT_ENVIRONMENT = true; + +vi.mock('react-i18next', () => ({ + useTranslation: () => ({ + t: (key: string, values?: Record) => { + if (key === 'flowChatHeader.turnBadge') { + return `Turn ${values?.current ?? ''}`; + } + return key; + }, + }), +})); + +vi.mock('@/component-library', async () => { + const ReactModule = await import('react'); + + return { + Tooltip: ({ children }: { children: React.ReactNode }) => ( + {children} + ), + IconButton: ({ + children, + size, + tooltip, + variant, + ...props + }: React.ButtonHTMLAttributes & { + size?: string; + tooltip?: string; + variant?: string; + }) => ( + + ), + Input: ReactModule.forwardRef>((props, ref) => ( + + )), + }; +}); + +vi.mock('@/infrastructure/contexts/WorkspaceContext', () => ({ + useWorkspaceContext: () => ({ + currentWorkspace: { rootPath: '/workspace' }, + }), +})); + +vi.mock('@/shared/utils/tabUtils', () => ({ + createReviewPlatformTab: vi.fn(), +})); + +vi.mock('./SessionFilesBadge', () => ({ + SessionFilesBadge: () =>
, +})); + +function createProps(overrides: Partial = {}): FlowChatHeaderProps { + return { + currentTurn: 1, + totalTurns: 2, + currentUserMessage: 'First prompt', + visible: true, + turns: [ + { turnId: 'turn-1', turnIndex: 1, title: 'First prompt' }, + { turnId: 'turn-2', turnIndex: 2, title: 'Second prompt' }, + ], + onJumpToTurn: vi.fn(), + ...overrides, + }; +} + +describe('FlowChatHeader', () => { + let container: HTMLDivElement; + let root: Root; + + beforeEach(() => { + container = document.createElement('div'); + document.body.appendChild(container); + root = createRoot(container); + }); + + afterEach(() => { + act(() => { + root.unmount(); + }); + container.remove(); + }); + + it('keeps the turn list open until the selected turn becomes current', () => { + const onJumpToTurn = vi.fn(); + const initialProps = createProps({ onJumpToTurn }); + + act(() => { + root.render(); + }); + + const turnListButton = container.querySelector('[data-testid="flowchat-header-turn-list"]'); + expect(turnListButton).not.toBeNull(); + + act(() => { + turnListButton?.click(); + }); + + expect(container.querySelector('[role="dialog"]')).not.toBeNull(); + + const turnItems = Array.from(container.querySelectorAll('.flowchat-header__turn-list-item')); + expect(turnItems).toHaveLength(2); + + act(() => { + turnItems[1]?.click(); + }); + + expect(onJumpToTurn).toHaveBeenCalledWith('turn-2'); + expect(container.querySelector('[role="dialog"]')).not.toBeNull(); + + act(() => { + root.render(); + }); + + expect(container.querySelector('[role="dialog"]')).toBeNull(); + }); + + it('closes the turn list and notifies the container when selecting the current turn', () => { + const onJumpToTurn = vi.fn(); + + act(() => { + root.render(); + }); + + const turnListButton = container.querySelector('[data-testid="flowchat-header-turn-list"]'); + act(() => { + turnListButton?.click(); + }); + + const currentTurnItem = container.querySelector('.flowchat-header__turn-list-item'); + act(() => { + currentTurnItem?.click(); + }); + + expect(onJumpToTurn).toHaveBeenCalledWith('turn-1'); + expect(container.querySelector('[role="dialog"]')).toBeNull(); + }); +}); diff --git a/src/web-ui/src/flow_chat/components/modern/FlowChatHeader.tsx b/src/web-ui/src/flow_chat/components/modern/FlowChatHeader.tsx index b6d94c6071..261a9d40f5 100644 --- a/src/web-ui/src/flow_chat/components/modern/FlowChatHeader.tsx +++ b/src/web-ui/src/flow_chat/components/modern/FlowChatHeader.tsx @@ -276,8 +276,14 @@ export const FlowChatHeader: React.FC = ({ const handleTurnSelect = (turnId: string) => { if (!onJumpToTurn) return; + const selectedTurn = displayTurns.find(turn => turn.turnId === turnId); + if (selectedTurn?.turnIndex === currentTurn) { + onJumpToTurn(turnId); + setIsTurnListOpen(false); + return; + } + onJumpToTurn(turnId); - setIsTurnListOpen(false); }; const handleSubagentSelect = (sessionId: string) => { @@ -795,4 +801,3 @@ export const FlowChatHeader: React.FC = ({ }; FlowChatHeader.displayName = 'FlowChatHeader'; - diff --git a/src/web-ui/src/flow_chat/components/modern/ModelRoundItem.scss b/src/web-ui/src/flow_chat/components/modern/ModelRoundItem.scss index fe1c909eb3..44f0ff4e44 100644 --- a/src/web-ui/src/flow_chat/components/modern/ModelRoundItem.scss +++ b/src/web-ui/src/flow_chat/components/modern/ModelRoundItem.scss @@ -141,7 +141,7 @@ .model-round-item__retry-attempt { padding: 0.35rem 0 0.15rem; - border-top: 1px solid color-mix(in srgb, var(--color-border) 62%, transparent); + border-top: 1px solid color-mix(in srgb, var(--border-base) 62%, transparent); } .model-round-item__retry-attempt-label { @@ -192,7 +192,7 @@ // Copy button .model-round-item__copy-btn { &.copied { - color: var(--flowchat-copy-success-color); + color: var(--color-success); background: var(--color-success-bg); } } diff --git a/src/web-ui/src/flow_chat/components/modern/ModelRoundItem.tsx b/src/web-ui/src/flow_chat/components/modern/ModelRoundItem.tsx index a87d513ee1..3d29000dda 100644 --- a/src/web-ui/src/flow_chat/components/modern/ModelRoundItem.tsx +++ b/src/web-ui/src/flow_chat/components/modern/ModelRoundItem.tsx @@ -10,7 +10,7 @@ import React, { useMemo, useState, useCallback, useEffect, useLayoutEffect, useRef } from 'react'; import { useTranslation } from 'react-i18next'; import { Copy, Check } from 'lucide-react'; -import type { ModelRound, ModelRoundAttempt, FlowItem, FlowTextItem, FlowToolItem, FlowThinkingItem, TokenUsage } from '../../types/flow-chat'; +import type { ModelRound, ModelRoundAttempt, FlowItem, FlowTextItem, FlowToolItem, FlowThinkingItem, TokenUsage, ToolRejectOptions } from '../../types/flow-chat'; import { useI18n } from '@/infrastructure/i18n'; import { FlowTextBlock } from '../FlowTextBlock'; import { FlowToolCard } from '../FlowToolCard'; @@ -178,6 +178,7 @@ interface TaskWithSubagentWrapperProps { turnId: string; roundId?: string; completedToolExitNowMs: number; + allowCompletedToolExit?: boolean; } const TaskWithSubagentWrapper: React.FC = React.memo(({ @@ -188,6 +189,7 @@ const TaskWithSubagentWrapper: React.FC = React.me turnId, roundId, completedToolExitNowMs, + allowCompletedToolExit = false, }) => { const isCollapsed = useTaskCollapsed(parentTaskToolId); const isTaskRunning = @@ -210,6 +212,7 @@ const TaskWithSubagentWrapper: React.FC = React.me roundId={roundId} isLastItem={false} completedToolExitNowMs={completedToolExitNowMs} + allowCompletedToolExit={allowCompletedToolExit} /> ( roundId={options.roundId} isLastItem={isLast && itemIdx === group.items.length - 1} completedToolExitNowMs={transientNowMs} + allowCompletedToolExit /> )); @@ -455,6 +459,7 @@ export const ModelRoundItem = React.memo( turnId={turnId} roundId={options.roundId} completedToolExitNowMs={transientNowMs} + allowCompletedToolExit={false} /> ); } @@ -466,6 +471,7 @@ export const ModelRoundItem = React.memo( roundId={options.roundId} isLastItem={isLast} completedToolExitNowMs={transientNowMs} + allowCompletedToolExit={false} /> ); } @@ -816,6 +822,7 @@ interface FlowItemRendererProps { roundId?: string; isLastItem?: boolean; completedToolExitNowMs: number; + allowCompletedToolExit?: boolean; } // Do not memoize: streaming content updates frequently. @@ -825,6 +832,7 @@ const FlowItemRenderer: React.FC = ({ roundId, isLastItem, completedToolExitNowMs, + allowCompletedToolExit = false, }) => { const { onToolConfirm, @@ -863,10 +871,12 @@ const FlowItemRenderer: React.FC = ({ const isCompletedTool = toolItem.status === 'completed'; const isCollapsible = isCollapsibleTool(toolItem.toolName); const shouldAnimateCompletedExit = + allowCompletedToolExit && isCollapsible && isCompletedTool && isCompletedToolInTransientWindow(toolItem, completedToolExitNowMs); - const isSettledCompletedTool = isCollapsible && isCompletedTool && !shouldAnimateCompletedExit; + const isSettledCompletedTool = + allowCompletedToolExit && isCollapsible && isCompletedTool && !shouldAnimateCompletedExit; const toolClassName = [ 'flowchat-flow-item', isCollapsible && isCompletedTool ? 'flowchat-flow-item--tool-transition' : null, @@ -884,9 +894,9 @@ const FlowItemRenderer: React.FC = ({ await onToolConfirm(toolId, updatedInput, permissionOptionId, approve); } }} - onReject={async (_toolId: string, permissionOptionId?: string) => { + onReject={async (_toolId: string, options?: ToolRejectOptions) => { if (onToolReject) { - await onToolReject(item.id, permissionOptionId); + await onToolReject(item.id, options); } }} onOpenInEditor={(filePath: string) => { diff --git a/src/web-ui/src/flow_chat/components/modern/ModernFlowChatContainer.history-state.test.tsx b/src/web-ui/src/flow_chat/components/modern/ModernFlowChatContainer.history-state.test.tsx index 6e4557845c..4efc23b169 100644 --- a/src/web-ui/src/flow_chat/components/modern/ModernFlowChatContainer.history-state.test.tsx +++ b/src/web-ui/src/flow_chat/components/modern/ModernFlowChatContainer.history-state.test.tsx @@ -54,6 +54,9 @@ const searchStateMock = vi.hoisted(() => ({ const headerPropsMock = vi.hoisted(() => ({ latest: null as Record | null, })); +const virtualListPropsMock = vi.hoisted(() => ({ + latest: null as Record | null, +})); const agentApiMock = vi.hoisted(() => ({ listBackgroundCommandActivities: vi.fn(() => Promise.resolve({ activities: [] })), })); @@ -120,7 +123,8 @@ vi.mock('../../store/modernFlowChatStore', () => ({ })); vi.mock('./VirtualMessageList', () => ({ - VirtualMessageList: React.forwardRef((_, ref) => { + VirtualMessageList: React.forwardRef((props: Record, ref) => { + virtualListPropsMock.latest = props; React.useImperativeHandle(ref, () => virtualListMock); return (
@@ -278,6 +282,7 @@ describe('ModernFlowChatContainer historical empty state', () => { searchStateMock.goToPrev.mockReset(); searchStateMock.clearSearch.mockReset(); headerPropsMock.latest = null; + virtualListPropsMock.latest = null; clearHistorySessionOpenTransition(); }); @@ -775,7 +780,7 @@ describe('ModernFlowChatContainer historical empty state', () => { releaseSpy.mockRestore(); }); - it('requests full history projection when searching a partially loaded session', async () => { + it('keeps partial-session search scoped to the loaded window', async () => { const pendingSpy = vi .spyOn(flowChatStore, 'hasPendingSessionHistoryCompletion') .mockReturnValue(true); @@ -801,13 +806,10 @@ describe('ModernFlowChatContainer historical empty state', () => { root.render(); }); - expect(projectionSpy).toHaveBeenCalledWith('session-1', 'search'); - expect(startupTraceMock.markPhase).toHaveBeenCalledWith( + expect(projectionSpy).not.toHaveBeenCalled(); + expect(startupTraceMock.markPhase).not.toHaveBeenCalledWith( 'historical_session_full_hydrate_released_for_search', - expect.objectContaining({ - queryLength: 'older prompt'.length, - turnCount: 1, - }), + expect.anything(), ); projectionSpy.mockRestore(); @@ -894,6 +896,201 @@ describe('ModernFlowChatContainer historical empty state', () => { }); }); + it('retries header turn selection without advancing header state until the virtual list accepts it', async () => { + stateMocks.activeSession = createSession({ + isHistorical: false, + historyState: 'ready', + dialogTurns: [ + createTurn('turn-1', 'Older prompt'), + createTurn('turn-2', 'Latest prompt'), + ], + } as Partial); + stateMocks.virtualItems = [ + { type: 'user-message', turnId: 'turn-1', data: { id: 'user-turn-1', content: 'Older prompt' } }, + { type: 'user-message', turnId: 'turn-2', data: { id: 'user-turn-2', content: 'Latest prompt' } }, + ]; + stateMocks.visibleTurnInfo = { + turnId: 'turn-2', + turnIndex: 2, + totalTurns: 2, + userMessage: 'Latest prompt', + }; + virtualListMock.pinTurnToTop.mockReturnValue(false); + + await act(async () => { + root.render(); + }); + + expect(headerPropsMock.latest).toMatchObject({ + currentTurn: 2, + totalTurns: 2, + }); + + await act(async () => { + (headerPropsMock.latest?.onJumpToTurn as ((turnId: string) => void) | undefined)?.('turn-1'); + }); + + expect(virtualListMock.pinTurnToTop).toHaveBeenLastCalledWith('turn-1', { + behavior: 'smooth', + pinMode: 'transient', + }); + expect(headerPropsMock.latest).toMatchObject({ + currentTurn: 2, + totalTurns: 2, + }); + + virtualListMock.pinTurnToTop.mockReturnValue(true); + stateMocks.virtualItems = [ + ...stateMocks.virtualItems, + ]; + + await act(async () => { + root.render(); + }); + flushAnimationFrame(); + + expect(virtualListMock.pinTurnToTop).toHaveBeenLastCalledWith('turn-1', { + behavior: 'auto', + pinMode: 'transient', + }); + expect(headerPropsMock.latest).toMatchObject({ + currentTurn: 2, + totalTurns: 2, + }); + + stateMocks.visibleTurnInfo = { + turnId: 'turn-1', + turnIndex: 1, + totalTurns: 2, + userMessage: 'Older prompt', + }; + + await act(async () => { + root.render(); + }); + + expect(headerPropsMock.latest).toMatchObject({ + currentTurn: 1, + totalTurns: 2, + }); + }); + + it('continues retrying an accepted header turn selection until the viewport reports the target turn', async () => { + stateMocks.activeSession = createSession({ + isHistorical: false, + historyState: 'ready', + dialogTurns: [ + createTurn('turn-1', 'Older prompt'), + createTurn('turn-2', 'Latest prompt'), + ], + } as Partial); + stateMocks.virtualItems = [ + { type: 'user-message', turnId: 'turn-1', data: { id: 'user-turn-1', content: 'Older prompt' } }, + { type: 'user-message', turnId: 'turn-2', data: { id: 'user-turn-2', content: 'Latest prompt' } }, + ]; + stateMocks.visibleTurnInfo = { + turnId: 'turn-2', + turnIndex: 2, + totalTurns: 2, + userMessage: 'Latest prompt', + }; + virtualListMock.pinTurnToTop.mockReturnValue(true); + + await act(async () => { + root.render(); + }); + + await act(async () => { + (headerPropsMock.latest?.onJumpToTurn as ((turnId: string) => void) | undefined)?.('turn-1'); + }); + + expect(virtualListMock.pinTurnToTop).toHaveBeenLastCalledWith('turn-1', { + behavior: 'smooth', + pinMode: 'transient', + }); + expect(headerPropsMock.latest).toMatchObject({ + currentTurn: 2, + totalTurns: 2, + }); + + const acceptedCallCount = virtualListMock.pinTurnToTop.mock.calls.length; + flushAnimationFrame(); + expect(virtualListMock.pinTurnToTop.mock.calls.length).toBeGreaterThan(acceptedCallCount); + expect(virtualListMock.pinTurnToTop).toHaveBeenLastCalledWith('turn-1', { + behavior: 'auto', + pinMode: 'transient', + }); + + stateMocks.visibleTurnInfo = { + turnId: 'turn-1', + turnIndex: 1, + totalTurns: 2, + userMessage: 'Older prompt', + }; + + await act(async () => { + root.render(); + }); + flushAnimationFrame(); + expect(headerPropsMock.latest).toMatchObject({ + currentTurn: 1, + totalTurns: 2, + }); + const settledCallCount = virtualListMock.pinTurnToTop.mock.calls.length; + flushAnimationFrame(); + expect(virtualListMock.pinTurnToTop.mock.calls.length).toBe(settledCallCount); + }); + + it('cancels pending header turn retry when the user scrolls manually', async () => { + stateMocks.activeSession = createSession({ + isHistorical: false, + historyState: 'ready', + dialogTurns: [ + createTurn('turn-1', 'Older prompt'), + createTurn('turn-2', 'Latest prompt'), + ], + } as Partial); + stateMocks.virtualItems = [ + { type: 'user-message', turnId: 'turn-1', data: { id: 'user-turn-1', content: 'Older prompt' } }, + { type: 'user-message', turnId: 'turn-2', data: { id: 'user-turn-2', content: 'Latest prompt' } }, + ]; + stateMocks.visibleTurnInfo = { + turnId: 'turn-2', + turnIndex: 2, + totalTurns: 2, + userMessage: 'Latest prompt', + }; + virtualListMock.pinTurnToTop.mockReturnValue(true); + + await act(async () => { + root.render(); + }); + + await act(async () => { + (headerPropsMock.latest?.onJumpToTurn as ((turnId: string) => void) | undefined)?.('turn-1'); + }); + + expect(headerPropsMock.latest).toMatchObject({ + currentTurn: 2, + totalTurns: 2, + }); + + flushAnimationFrame(); + const retryCallCount = virtualListMock.pinTurnToTop.mock.calls.length; + expect(retryCallCount).toBeGreaterThan(1); + + await act(async () => { + (virtualListPropsMock.latest?.onUserScrollIntent as (() => void) | undefined)?.(); + }); + expect(headerPropsMock.latest).toMatchObject({ + currentTurn: 2, + totalTurns: 2, + }); + + flushAnimationFrame(); + expect(virtualListMock.pinTurnToTop.mock.calls.length).toBe(retryCallCount); + }); + it('does not expose previous navigation before the loaded tail range in partial history', async () => { stateMocks.activeSession = createSession({ isHistorical: false, @@ -956,7 +1153,7 @@ describe('ModernFlowChatContainer historical empty state', () => { expect(virtualListMock.pinTurnToTop).not.toHaveBeenCalled(); expect(startupTraceMock.markPhase).toHaveBeenCalledWith( 'historical_session_latest_anchor_skipped', - expect.objectContaining({ reason: 'streaming_follow_output', mode: 'sticky-latest' }), + expect.objectContaining({ reason: 'streaming_follow_output', mode: 'follow-output' }), ); }); diff --git a/src/web-ui/src/flow_chat/components/modern/ModernFlowChatContainer.scss b/src/web-ui/src/flow_chat/components/modern/ModernFlowChatContainer.scss index c4ddf81704..2f830cf349 100644 --- a/src/web-ui/src/flow_chat/components/modern/ModernFlowChatContainer.scss +++ b/src/web-ui/src/flow_chat/components/modern/ModernFlowChatContainer.scss @@ -3,7 +3,7 @@ */ .modern-flowchat-container.flow-chat-typography { - font-family: var(--font-sans); + font-family: var(--font-family-sans); font-size: var(--flowchat-font-size-base); line-height: var(--flowchat-text-line-height); } @@ -49,7 +49,7 @@ flex-shrink: 0; padding: var(--flowchat-card-expanded-pad-x); background: var(--modern-flowchat-input-bg); - border-top: 1px solid var(--color-border); + border-top: 1px solid var(--border-base); } } @@ -73,7 +73,7 @@ display: inline-flex; align-items: center; justify-content: center; - color: var(--color-accent); + color: var(--color-accent-500); svg { animation: history-placeholder-spin 1.1s linear infinite; @@ -115,17 +115,17 @@ justify-content: center; gap: var(--flowchat-inline-gap); padding: 0 var(--size-gap-3); - border: 1px solid var(--color-border); + border: 1px solid var(--border-base); border-radius: 6px; color: var(--color-text-primary); - background: var(--color-surface-hover); + background: var(--element-bg-hover); font: inherit; font-size: var(--flowchat-font-size-sm); cursor: pointer; } .history-session-placeholder__retry:hover { - background: var(--color-overlay-white-10); + background: var(--color-overlay-white-12); } .modern-flowchat-container__history-overlay { @@ -161,10 +161,10 @@ border-radius: 8px; opacity: 0.72; background: - linear-gradient(90deg, transparent 0 4%, var(--color-surface-hover) 4% 62%, transparent 62% 100%) 0 0 / 100% 24px no-repeat, - linear-gradient(90deg, transparent 0 9%, var(--color-surface-hover) 9% 86%, transparent 86% 100%) 0 48px / 100% 24px no-repeat, - linear-gradient(90deg, transparent 0 6%, var(--color-surface-hover) 6% 74%, transparent 74% 100%) 0 96px / 100% 24px no-repeat, - linear-gradient(90deg, transparent 0 12%, var(--color-surface-hover) 12% 68%, transparent 68% 100%) 0 144px / 100% 24px no-repeat; + linear-gradient(90deg, transparent 0 4%, var(--element-bg-hover) 4% 62%, transparent 62% 100%) 0 0 / 100% 24px no-repeat, + linear-gradient(90deg, transparent 0 9%, var(--element-bg-hover) 9% 86%, transparent 86% 100%) 0 48px / 100% 24px no-repeat, + linear-gradient(90deg, transparent 0 6%, var(--element-bg-hover) 6% 74%, transparent 74% 100%) 0 96px / 100% 24px no-repeat, + linear-gradient(90deg, transparent 0 12%, var(--element-bg-hover) 12% 68%, transparent 68% 100%) 0 144px / 100% 24px no-repeat; } .modern-flowchat-container__history-open-intent-spinner { @@ -172,8 +172,8 @@ z-index: 1; width: 18px; height: 18px; - border: 2px solid var(--color-border); - border-top-color: var(--color-accent); + border: 2px solid var(--border-base); + border-top-color: var(--color-accent-500); border-radius: 50%; background: var(--color-bg-scene); box-shadow: 0 0 0 6px var(--color-bg-scene); diff --git a/src/web-ui/src/flow_chat/components/modern/ModernFlowChatContainer.tsx b/src/web-ui/src/flow_chat/components/modern/ModernFlowChatContainer.tsx index d77772e1db..f637a428d5 100644 --- a/src/web-ui/src/flow_chat/components/modern/ModernFlowChatContainer.tsx +++ b/src/web-ui/src/flow_chat/components/modern/ModernFlowChatContainer.tsx @@ -49,6 +49,7 @@ import { resolveThreadGoalHeaderTitle } from '../../utils/threadGoalDisplay'; import { findDialogTurn, shouldUseStickyLatestPin, + shouldUseLatestTurnFollowOutput, } from '../../utils/flowChatTurnScrollPolicy'; import { isRemoteTraceContext, startupTrace } from '@/shared/utils/startupTrace'; import { scheduleAfterStartupPaint } from '@/shared/utils/startupTaskScheduling'; @@ -100,6 +101,7 @@ type BackgroundCommandSummary = { const LATEST_TURN_AUTO_PIN_MAX_ATTEMPTS = 8; const HISTORY_INITIAL_CONTENT_PAINT_MAX_ATTEMPTS = 30; const HISTORY_LOADING_LAYER_STALL_WARN_MS = 800; +const HEADER_TURN_PIN_RETRY_MAX_ATTEMPTS = 120; const MOCK_BACKGROUND_ACTIVITIES_STORAGE_KEY = 'bitfun.flowChat.mockBackgroundActivities'; const MOCK_BACKGROUND_SUBAGENTS: BackgroundSubagentSummary[] = [ @@ -220,6 +222,7 @@ export const ModernFlowChatContainer: React.FC = ( const activeSession = useActiveSession(); const visibleTurnInfo = useVisibleTurnInfo(); const [pendingHeaderTurnId, setPendingHeaderTurnId] = useState(null); + const [queuedHeaderTurnPinId, setQueuedHeaderTurnPinId] = useState(null); const [pendingHistoryOpenSession, setPendingHistoryOpenSession] = useState(null); const [searchOpenRequest, setSearchOpenRequest] = useState(0); // Track whether a slash-command or @-mention popup is open in ChatInput. @@ -240,6 +243,9 @@ export const ModernFlowChatContainer: React.FC = ( const [isSendingBackgroundCommandInput, setIsSendingBackgroundCommandInput] = useState(false); const autoPinnedTurnKeyRef = useRef(null); const releasedHistoryCompletionKeyRef = useRef(null); + const visibleTurnInfoRef = useRef(visibleTurnInfo); + const turnSummariesRef = useRef([]); + const requestHeaderTurnPinRef = useRef<((turnId: string, behavior?: ScrollBehavior) => boolean) | null>(null); const virtualListRef = useRef(null); const chatScopeRef = useRef(null); const [historyInitialContentReadyKey, setHistoryInitialContentReadyKey] = useState(null); @@ -587,38 +593,25 @@ export const ModernFlowChatContainer: React.FC = ( () => findDialogTurn(activeSession?.dialogTurns, latestTurnId), [activeSession?.dialogTurns, latestTurnId], ); + const latestTurnUsesFollowOutput = shouldUseLatestTurnFollowOutput(latestTurn); const latestTurnUsesStickyPin = shouldUseStickyLatestPin(latestTurn); const navigationVisibleTurnInfo = useMemo(() => { - if (!pendingHeaderTurnId) { - if (!visibleTurnInfo) { - return null; - } - - const localTurn = turnSummaries.find(turn => turn.turnId === visibleTurnInfo.turnId); - if (!localTurn) { - return visibleTurnInfo; - } - - return { - ...visibleTurnInfo, - turnIndex: localTurn.turnIndex, - totalTurns: turnSummaries.length, - }; + if (!visibleTurnInfo) { + return null; } - const targetTurn = turnSummaries.find(turn => turn.turnId === pendingHeaderTurnId); - if (!targetTurn) { + const localTurn = turnSummaries.find(turn => turn.turnId === visibleTurnInfo.turnId); + if (!localTurn) { return visibleTurnInfo; } return { - turnId: targetTurn.turnId, - turnIndex: targetTurn.turnIndex, + ...visibleTurnInfo, + turnIndex: localTurn.turnIndex, totalTurns: turnSummaries.length, - userMessage: targetTurn.title, }; - }, [pendingHeaderTurnId, turnSummaries, visibleTurnInfo]); + }, [turnSummaries, visibleTurnInfo]); const effectiveVisibleTurnInfo = useMemo(() => { if (!navigationVisibleTurnInfo) { return null; @@ -636,6 +629,14 @@ export const ModernFlowChatContainer: React.FC = ( navigationVisibleTurnInfo.turnIndex > 0 && navigationVisibleTurnInfo.turnIndex < turnSummaries.length; + useEffect(() => { + visibleTurnInfoRef.current = visibleTurnInfo; + }, [visibleTurnInfo]); + + useEffect(() => { + turnSummariesRef.current = turnSummaries; + }, [turnSummaries]); + const currentHeaderMessage = useMemo(() => { const turnId = effectiveVisibleTurnInfo?.turnId; if (!turnId) { @@ -663,6 +664,76 @@ export const ModernFlowChatContainer: React.FC = ( } }, [pendingHeaderTurnId, turnSummaries, visibleTurnInfo?.turnId]); + const requestHeaderTurnPin = useCallback((turnId: string, behavior: ScrollBehavior = 'smooth') => { + const isLatestTurn = turnSummaries[turnSummaries.length - 1]?.turnId === turnId; + const targetTurn = findDialogTurn(activeSession?.dialogTurns, turnId); + const pinMode = isLatestTurn && shouldUseStickyLatestPin(targetTurn) + ? 'sticky-latest' + : 'transient'; + + return virtualListRef.current?.pinTurnToTop(turnId, { + behavior, + pinMode, + }) ?? false; + }, [activeSession?.dialogTurns, turnSummaries]); + useEffect(() => { + requestHeaderTurnPinRef.current = requestHeaderTurnPin; + }, [requestHeaderTurnPin]); + const handleVirtualListUserScrollIntent = useCallback(() => { + setQueuedHeaderTurnPinId(null); + setPendingHeaderTurnId(null); + }, []); + + useEffect(() => { + if (!queuedHeaderTurnPinId) return; + + let cancelled = false; + let frameId: number | null = null; + let attempts = 0; + + const retry = () => { + if (cancelled) return; + + if (visibleTurnInfoRef.current?.turnId === queuedHeaderTurnPinId) { + setQueuedHeaderTurnPinId(null); + setPendingHeaderTurnId(null); + return; + } + + const targetStillExists = turnSummariesRef.current.some(turn => turn.turnId === queuedHeaderTurnPinId); + if (!targetStillExists) { + setQueuedHeaderTurnPinId(null); + setPendingHeaderTurnId(null); + return; + } + + const accepted = requestHeaderTurnPinRef.current?.(queuedHeaderTurnPinId, 'auto') ?? false; + if (accepted) { + setPendingHeaderTurnId(queuedHeaderTurnPinId); + } + + attempts += 1; + if (attempts >= HEADER_TURN_PIN_RETRY_MAX_ATTEMPTS) { + setQueuedHeaderTurnPinId(null); + setPendingHeaderTurnId(null); + return; + } + + frameId = requestAnimationFrame(retry); + }; + + frameId = requestAnimationFrame(retry); + + return () => { + cancelled = true; + if (frameId !== null) { + cancelAnimationFrame(frameId); + } + }; + }, [ + queuedHeaderTurnPinId, + ]); + useLayoutEffect(() => { autoPinnedTurnKeyRef.current = null; releasedHistoryCompletionKeyRef.current = null; @@ -672,6 +743,7 @@ export const ModernFlowChatContainer: React.FC = ( setHistoryInitialContentReadyKey(null); setHistoryInitialContentPostPaintKey(null); setPendingHeaderTurnId(null); + setQueuedHeaderTurnPinId(null); }, [activeSession?.sessionId]); useLayoutEffect(() => { @@ -688,14 +760,14 @@ export const ModernFlowChatContainer: React.FC = ( const pinMode = latestTurnUsesStickyPin ? 'sticky-latest' : null; - if (latestTurnUsesStickyPin) { + if (latestTurnUsesFollowOutput) { autoPinnedTurnKeyRef.current = resolvedLatestTurnKey; setPendingHeaderTurnId(null); startupTrace.markPhase('historical_session_latest_anchor_skipped', { sessionId, latestTurnId, reason: 'streaming_follow_output', - mode: pinMode, + mode: pinMode ?? 'follow-output', turnCount: turnSummaries.length, }); return; @@ -828,6 +900,7 @@ export const ModernFlowChatContainer: React.FC = ( activeSession?.remoteSshHost, hasPendingHistoryCompletion, latestTurnId, + latestTurnUsesFollowOutput, latestTurnUsesStickyPin, turnSummaries.length, visibleTurnInfo?.turnId, @@ -929,59 +1002,26 @@ export const ModernFlowChatContainer: React.FC = ( }; }, [searchCurrentMatchVirtualIndex]); - useEffect(() => { - const sessionId = activeSession?.sessionId; - const trimmedSearchQuery = searchQuery.trim(); - if ( - !sessionId || - activeSession.historyState !== 'ready' || - ( - !hasPendingHistoryCompletion && - !hasDeferredHistoryProjection - ) || - trimmedSearchQuery.length === 0 - ) { - return; - } - - if (latestTurnId) { - releasedHistoryCompletionKeyRef.current = `${sessionId}:${latestTurnId}`; - } - - const requested = flowChatStore.requestSessionFullHistoryProjection(sessionId, 'search'); - if (requested) { - startupTrace.markPhase('historical_session_full_hydrate_released_for_search', { - sessionId, - queryLength: trimmedSearchQuery.length, - turnCount: turnSummaries.length, - }); - } - }, [ - activeSession?.historyState, - activeSession?.sessionId, - hasDeferredHistoryProjection, - hasPendingHistoryCompletion, - latestTurnId, - searchQuery, - turnSummaries.length, - ]); - const handleJumpToTurn = useCallback((turnId: string) => { if (!turnId) return; - const isLatestTurn = turnSummaries[turnSummaries.length - 1]?.turnId === turnId; - const targetTurn = findDialogTurn(activeSession?.dialogTurns, turnId); - const pinMode = isLatestTurn && shouldUseStickyLatestPin(targetTurn) - ? 'sticky-latest' - : 'transient'; + const targetStillExists = turnSummaries.some(turn => turn.turnId === turnId); + if (!targetStillExists) { + setQueuedHeaderTurnPinId(null); + setPendingHeaderTurnId(null); + return; + } - const accepted = virtualListRef.current?.pinTurnToTop(turnId, { - behavior: 'smooth', - pinMode, - }) ?? false; + const accepted = requestHeaderTurnPin(turnId); + if (accepted) { + setQueuedHeaderTurnPinId(turnId); + setPendingHeaderTurnId(turnId); + return; + } - setPendingHeaderTurnId(accepted ? turnId : null); - }, [activeSession?.dialogTurns, turnSummaries]); + setQueuedHeaderTurnPinId(turnId); + setPendingHeaderTurnId(null); + }, [requestHeaderTurnPin, turnSummaries]); const handleJumpToPreviousTurn = useCallback(() => { if (!navigationVisibleTurnInfo || navigationVisibleTurnInfo.turnIndex <= 1) return; @@ -1473,6 +1513,7 @@ export const ModernFlowChatContainer: React.FC = ( <> )} diff --git a/src/web-ui/src/flow_chat/components/modern/ProcessingIndicator.scss b/src/web-ui/src/flow_chat/components/modern/ProcessingIndicator.scss index 27f0673f63..09d384bdb3 100644 --- a/src/web-ui/src/flow_chat/components/modern/ProcessingIndicator.scss +++ b/src/web-ui/src/flow_chat/components/modern/ProcessingIndicator.scss @@ -38,7 +38,7 @@ // Hint text — same breathe rhythm as tool cards (tool-card-text-fade: 1.6s 0%→25%→0%) .processing-indicator__hint { font-size: 0.8125rem; - color: var(--text-secondary); + color: var(--color-text-secondary); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; diff --git a/src/web-ui/src/flow_chat/components/modern/ScrollAnchor.scss b/src/web-ui/src/flow_chat/components/modern/ScrollAnchor.scss index 8737bdeaa7..3694df5687 100644 --- a/src/web-ui/src/flow_chat/components/modern/ScrollAnchor.scss +++ b/src/web-ui/src/flow_chat/components/modern/ScrollAnchor.scss @@ -3,7 +3,7 @@ */ .scroll-anchor { - --delay: 0s; + --flowchat-scroll-anchor-delay: 0s; position: absolute; right: 0; @@ -56,7 +56,7 @@ transition: all 0.15s ease; animation: anchorFade 0.3s ease backwards; - animation-delay: var(--delay); + animation-delay: var(--flowchat-scroll-anchor-delay); &::before { content: ''; @@ -96,9 +96,9 @@ width: 280px; background: var(--color-bg-elevated); backdrop-filter: blur(16px); - border: 1px solid var(--color-border); + border: 1px solid var(--border-base); border-radius: 8px; - box-shadow: 0 4px 20px var(--color-overlay-black-25); + box-shadow: 0 4px 20px var(--color-overlay-black-30); pointer-events: none; z-index: 10000; transform: translateX(-100%) translateY(-50%); @@ -114,15 +114,15 @@ width: 8px; height: 8px; background: var(--color-bg-elevated); - border-right: 1px solid var(--color-border); - border-top: 1px solid var(--color-border); + border-right: 1px solid var(--border-base); + border-top: 1px solid var(--border-base); } } &__preview-indicator { padding: var(--flowchat-card-expanded-pad-y) var(--flowchat-card-expanded-pad-x); background: var(--color-accent-50); - border-bottom: 1px solid var(--color-border); + border-bottom: 1px solid var(--border-base); text-align: center; } diff --git a/src/web-ui/src/flow_chat/components/modern/ScrollAnchor.tsx b/src/web-ui/src/flow_chat/components/modern/ScrollAnchor.tsx index c7d2df4cc5..7eaa014cef 100644 --- a/src/web-ui/src/flow_chat/components/modern/ScrollAnchor.tsx +++ b/src/web-ui/src/flow_chat/components/modern/ScrollAnchor.tsx @@ -167,7 +167,7 @@ export const ScrollAnchor: React.FC = ({ className={`scroll-anchor__point ${hoveredAnchor?.id === anchor.id ? 'active' : ''}`} style={{ top: `${anchor.position}%`, - '--delay': `${idx * 0.03}s` + '--flowchat-scroll-anchor-delay': `${idx * 0.03}s` } as React.CSSProperties} onClick={(e) => { e.stopPropagation(); diff --git a/src/web-ui/src/flow_chat/components/modern/SessionFileModificationsBar.scss b/src/web-ui/src/flow_chat/components/modern/SessionFileModificationsBar.scss index a9f6920e6f..28e0e2697b 100644 --- a/src/web-ui/src/flow_chat/components/modern/SessionFileModificationsBar.scss +++ b/src/web-ui/src/flow_chat/components/modern/SessionFileModificationsBar.scss @@ -178,7 +178,7 @@ } .icon-edit { - color: var(--color-primary); + color: var(--color-accent-500); } .icon-delete { diff --git a/src/web-ui/src/flow_chat/components/modern/SubagentItems.scss b/src/web-ui/src/flow_chat/components/modern/SubagentItems.scss index c8a870d541..71e152be67 100644 --- a/src/web-ui/src/flow_chat/components/modern/SubagentItems.scss +++ b/src/web-ui/src/flow_chat/components/modern/SubagentItems.scss @@ -32,7 +32,7 @@ padding: 10px 10px 10px var(--tool-card-header-icon-slot); // Continue the task card border on left/right/bottom; top border is hidden. - background: var(--color-bg-flowchat); + background: var(--color-bg-scene); border: 1px solid var(--border-base); border-top: none; // Top corners are square to merge with the header card above. diff --git a/src/web-ui/src/flow_chat/components/modern/TurnCompletionNoticeItem.scss b/src/web-ui/src/flow_chat/components/modern/TurnCompletionNoticeItem.scss index 046172d0a3..9c6db4a6e6 100644 --- a/src/web-ui/src/flow_chat/components/modern/TurnCompletionNoticeItem.scss +++ b/src/web-ui/src/flow_chat/components/modern/TurnCompletionNoticeItem.scss @@ -11,24 +11,24 @@ padding: 6px 10px; border: 1px solid transparent; border-radius: 12px; - background: color-mix(in srgb, var(--color-bg-surface) 92%, transparent); + background: color-mix(in srgb, var(--color-bg-secondary) 92%, transparent); color: var(--color-text-primary); font-size: 12px; line-height: 1.45; &--warning { border-color: color-mix(in srgb, var(--color-warning) 45%, transparent); - background: color-mix(in srgb, var(--color-warning) 10%, var(--color-bg-surface)); + background: color-mix(in srgb, var(--color-warning) 10%, var(--color-bg-secondary)); } &--error { border-color: color-mix(in srgb, var(--color-error) 45%, transparent); - background: color-mix(in srgb, var(--color-error) 10%, var(--color-bg-surface)); + background: color-mix(in srgb, var(--color-error) 10%, var(--color-bg-secondary)); } &--info { - border-color: color-mix(in srgb, var(--accent-primary) 45%, transparent); - background: color-mix(in srgb, var(--accent-primary) 10%, var(--color-bg-surface)); + border-color: color-mix(in srgb, var(--color-accent-500) 45%, transparent); + background: color-mix(in srgb, var(--color-accent-500) 10%, var(--color-bg-secondary)); } } diff --git a/src/web-ui/src/flow_chat/components/modern/UserMessageItem.scss b/src/web-ui/src/flow_chat/components/modern/UserMessageItem.scss index c271ad6d87..386dcc08d1 100644 --- a/src/web-ui/src/flow_chat/components/modern/UserMessageItem.scss +++ b/src/web-ui/src/flow_chat/components/modern/UserMessageItem.scss @@ -6,8 +6,8 @@ .user-message-item { padding: var(--flowchat-user-message-pad-y) var(--flowchat-user-message-pad-x); - background: color-mix(in srgb, var(--element-bg-medium) 86%, var(--color-bg-flowchat)); - border: 1px solid color-mix(in srgb, var(--border-muted) 38%, transparent); + background: color-mix(in srgb, var(--element-bg-medium) 86%, var(--color-bg-scene)); + border: 1px solid color-mix(in srgb, var(--border-subtle) 38%, transparent); border-radius: var(--flowchat-card-radius); margin: 0.06rem @@ -20,8 +20,8 @@ position: relative; &:hover { - background: color-mix(in srgb, var(--element-bg-medium) 92%, var(--color-bg-flowchat)); - border-color: color-mix(in srgb, var(--border-muted) 56%, transparent); + background: color-mix(in srgb, var(--element-bg-medium) 92%, var(--color-bg-scene)); + border-color: color-mix(in srgb, var(--border-subtle) 56%, transparent); .user-message-item__copy-btn { opacity: 1; @@ -123,9 +123,11 @@ // Failed state: no bubble. Icons/actions align to the first text line (not flex baseline: // `-webkit-box` line-clamp does not expose a reliable baseline for `align-items: baseline`). .user-message-item--failed { - --um-failed-fs: calc(var(--flowchat-font-size-base) * 0.86); - --um-failed-lh: 1.55; - --um-failed-line-box: calc(var(--um-failed-fs) * var(--um-failed-lh)); + --user-message-failed-font-size: calc(var(--flowchat-font-size-base) * 0.86); + --user-message-failed-line-height: 1.55; + --user-message-failed-line-box: calc( + var(--user-message-failed-font-size) * var(--user-message-failed-line-height) + ); background: transparent; border: none; @@ -164,8 +166,8 @@ align-items: center; justify-content: center; box-sizing: border-box; - height: var(--um-failed-line-box); - min-height: var(--um-failed-line-box); + height: var(--user-message-failed-line-box); + min-height: var(--user-message-failed-line-box); color: var(--color-text-muted); opacity: 0.9; @@ -181,10 +183,10 @@ width: max-content; max-width: 100%; color: var(--color-text-muted); - font-size: var(--um-failed-fs); + font-size: var(--user-message-failed-font-size); font-weight: 400; font-style: italic; - line-height: var(--um-failed-lh); + line-height: var(--user-message-failed-line-height); text-decoration: line-through; text-decoration-color: color-mix(in srgb, var(--color-text-muted) 72%, transparent); } @@ -193,7 +195,7 @@ flex: 0 0 auto; align-items: center; /* Match `.user-message-item__copy-btn` height (28px) to first-line box. */ - margin-top: calc((var(--um-failed-line-box) - 28px) / 2); + margin-top: calc((var(--user-message-failed-line-box) - 28px) / 2); } .user-message-item__failed-body .user-message-item__steering-tag { @@ -413,7 +415,7 @@ &:hover { transform: scale(1.03); - box-shadow: 0 4px 12px var(--color-overlay-black-25); + box-shadow: 0 4px 12px var(--color-overlay-black-30); } img { @@ -428,8 +430,8 @@ .user-message-item__lightbox { position: fixed; inset: 0; - z-index: 9999; - background: tokens.$modal-backdrop; + z-index: tokens.$z-fullscreen; + background: var(--color-overlay-black-80); display: flex; align-items: center; justify-content: center; diff --git a/src/web-ui/src/flow_chat/components/modern/UserMessageItem.tsx b/src/web-ui/src/flow_chat/components/modern/UserMessageItem.tsx index 89a2f2dcc4..4a36de474f 100644 --- a/src/web-ui/src/flow_chat/components/modern/UserMessageItem.tsx +++ b/src/web-ui/src/flow_chat/components/modern/UserMessageItem.tsx @@ -4,6 +4,7 @@ */ import React, { useState, useCallback, useRef, useEffect, useMemo } from 'react'; +import { createPortal } from 'react-dom'; import { Copy, Check, RotateCcw, Loader2, ArrowDownToLine, X, CircleUser, Pencil } from 'lucide-react'; import type { DialogTurn, FlowUserSteeringItem } from '../../types/flow-chat'; import { flowChatManager } from '../../services/FlowChatManager'; @@ -540,13 +541,14 @@ export const UserMessageItem = React.memo(
)} - {lightboxImage && ( + {lightboxImage && createPortal(
setLightboxImage(null)}> Preview e.stopPropagation()} /> -
+
, + document.body, )}
); diff --git a/src/web-ui/src/flow_chat/components/modern/VirtualItemRenderer.scss b/src/web-ui/src/flow_chat/components/modern/VirtualItemRenderer.scss index 30c6e2725f..f9d76ff006 100644 --- a/src/web-ui/src/flow_chat/components/modern/VirtualItemRenderer.scss +++ b/src/web-ui/src/flow_chat/components/modern/VirtualItemRenderer.scss @@ -24,15 +24,15 @@ } &--search-match { - outline: 1px solid color-mix(in srgb, var(--accent-primary) 35%, transparent); + outline: 1px solid color-mix(in srgb, var(--color-accent-500) 35%, transparent); outline-offset: -1px; border-radius: 6px; } &--search-current { - outline: 2px solid color-mix(in srgb, var(--accent-primary) 75%, transparent); + outline: 2px solid color-mix(in srgb, var(--color-accent-500) 75%, transparent); outline-offset: -1px; border-radius: 6px; - background: color-mix(in srgb, var(--accent-primary) 8%, transparent); + background: color-mix(in srgb, var(--color-accent-500) 8%, transparent); } } diff --git a/src/web-ui/src/flow_chat/components/modern/VirtualMessageList.scss b/src/web-ui/src/flow_chat/components/modern/VirtualMessageList.scss index 903c0f70b7..dfeb443981 100644 --- a/src/web-ui/src/flow_chat/components/modern/VirtualMessageList.scss +++ b/src/web-ui/src/flow_chat/components/modern/VirtualMessageList.scss @@ -44,7 +44,7 @@ display: flex; align-items: center; justify-content: center; - background: var(--color-bg-flowchat); /* FlowChat background. */ + background: var(--color-bg-scene); /* FlowChat background. */ } .empty-state { @@ -60,6 +60,22 @@ min-height: 57px; flex-shrink: 0; } + + &__history-boundary-status { + width: fit-content; + max-width: calc(100% - 32px); + margin: 0 auto 8px; + padding: 4px 10px; + border: 1px solid var(--border-subtle); + border-radius: 6px; + background: var(--element-bg-subtle); + color: var(--color-text-muted); + font-size: 12px; + line-height: 18px; + text-align: center; + overflow-wrap: anywhere; + pointer-events: none; + } .message-list-footer { /* Inline height from VirtualMessageList (measured drop-zone + bottom inset + tail clearance). */ @@ -96,7 +112,7 @@ z-index: 2; overflow: hidden; pointer-events: none; - background: var(--color-bg-flowchat); + background: var(--color-bg-scene); contain: paint; } diff --git a/src/web-ui/src/flow_chat/components/modern/VirtualMessageList.session-boundary.test.tsx b/src/web-ui/src/flow_chat/components/modern/VirtualMessageList.session-boundary.test.tsx index b5f383dc3f..5a85e6e6ab 100644 --- a/src/web-ui/src/flow_chat/components/modern/VirtualMessageList.session-boundary.test.tsx +++ b/src/web-ui/src/flow_chat/components/modern/VirtualMessageList.session-boundary.test.tsx @@ -4,7 +4,7 @@ import React from 'react'; import { act } from 'react'; import { createRoot, type Root } from 'react-dom/client'; import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; -import { VirtualMessageList } from './VirtualMessageList'; +import { VirtualMessageList, type VirtualMessageListRef } from './VirtualMessageList'; import { activeSessionHistoryProjectionHandoff } from './historyProjectionHandoff'; import type { Session } from '../../types/flow-chat'; import type { VirtualItem } from '../../store/modernFlowChatStore'; @@ -17,14 +17,50 @@ const stateMocks = vi.hoisted(() => ({ visibleTurnInfo: null as unknown, setVisibleTurnInfo: vi.fn(), })); +const flowStoreMocks = vi.hoisted(() => ({ + hasPendingSessionHistoryCompletion: vi.fn(() => false), + hasDeferredSessionHistoryProjection: vi.fn(() => false), + requestSessionFullHistoryProjection: vi.fn(), + revealPreviousSessionHistoryWindow: vi.fn(() => false), + releaseSessionHistoryCompletionAfterInitialPaint: vi.fn(() => false), +})); +const inputStateMocks = vi.hoisted(() => ({ + isActive: false, + isExpanded: false, + inputHeight: 0, +})); + +vi.mock('react-i18next', () => ({ + useTranslation: () => ({ + t: (key: string) => { + const translations: Record = { + 'historyState.preparingOlderHistory': 'Preparing older history...', + 'historyState.olderHistoryNotReady': 'Older history is not ready yet.', + }; + return translations[key] ?? key; + }, + }), +})); vi.mock('react-virtuoso', () => ({ Virtuoso: React.forwardRef((props: any, ref) => { + const scrollerRef = React.useRef(null); React.useImperativeHandle(ref, () => ({ scrollTo: vi.fn(), scrollToIndex: vi.fn(), })); + React.useLayoutEffect(() => { + if (!scrollerRef.current) { + return; + } + + props.scrollerRef?.(scrollerRef.current); + return () => { + props.scrollerRef?.(null); + }; + }, [props]); + React.useEffect(() => { if (props.data?.[0]?.turnId === 'turn-a') { props.atBottomStateChange?.(false); @@ -32,7 +68,14 @@ vi.mock('react-virtuoso', () => ({ }, [props]); return ( -
+
+ {props.components?.Header ? : null} {props.data?.map((item: VirtualItem, index: number) => (
{item.turnId} @@ -68,18 +111,16 @@ vi.mock('../../hooks/useActiveSessionState', () => ({ })); vi.mock('../../store/chatInputStateStore', () => ({ - useChatInputState: (selector: (state: any) => unknown) => selector({ - isActive: false, - isExpanded: false, - inputHeight: 0, - }), + useChatInputState: (selector: (state: any) => unknown) => selector(inputStateMocks), })); vi.mock('../../store/FlowChatStore', () => ({ flowChatStore: { - hasPendingSessionHistoryCompletion: () => false, - hasDeferredSessionHistoryProjection: () => false, - requestSessionFullHistoryProjection: vi.fn(), + hasPendingSessionHistoryCompletion: flowStoreMocks.hasPendingSessionHistoryCompletion, + hasDeferredSessionHistoryProjection: flowStoreMocks.hasDeferredSessionHistoryProjection, + requestSessionFullHistoryProjection: flowStoreMocks.requestSessionFullHistoryProjection, + revealPreviousSessionHistoryWindow: flowStoreMocks.revealPreviousSessionHistoryWindow, + releaseSessionHistoryCompletionAfterInitialPaint: flowStoreMocks.releaseSessionHistoryCompletionAfterInitialPaint, }, })); @@ -96,8 +137,8 @@ vi.mock('./VirtualItemRenderer', () => ({ })); vi.mock('../ScrollToLatestBar', () => ({ - ScrollToLatestBar: ({ visible }: { visible: boolean }) => ( -
+ ScrollToLatestBar: ({ visible, onClick }: { visible: boolean; onClick?: () => void }) => ( + +
+ ) : ( +
{errorMessage}
+ )} +
+ ); + } + + return ( +
+
+ {t('toolCards.computerUse.actionLabel')} + {parsed.action} +
+ {parsed.appName && ( +
+ {t('toolCards.computerUse.appLabel')} + {parsed.appName} +
+ )} + {parsed.target && ( +
+ {t('toolCards.computerUse.targetLabel')} + {parsed.target} +
+ )} + {parsed.loopWarningSuggestion && ( +
+ + {parsed.loopWarningSuggestion} +
+ )} +
+ ); + }; + + return ( +
+ + )} + action={{parsed.action}} + content={renderContent()} + /> + )} + expandedContent={isExpandable ? renderExpandedContent() : undefined} + /> +
+ ); +}; diff --git a/src/web-ui/src/flow_chat/tool-cards/ContextCompressionDisplay.scss b/src/web-ui/src/flow_chat/tool-cards/ContextCompressionDisplay.scss index 9270f22b58..493c21fa4c 100644 --- a/src/web-ui/src/flow_chat/tool-cards/ContextCompressionDisplay.scss +++ b/src/web-ui/src/flow_chat/tool-cards/ContextCompressionDisplay.scss @@ -18,7 +18,7 @@ .token-stat { font-weight: 600; - color: var(--tool-card-text-primary); + color: var(--color-text-primary); white-space: nowrap; } @@ -31,19 +31,19 @@ font-weight: 600; flex-shrink: 0; white-space: nowrap; - color: color-mix(in srgb, var(--tool-card-positive-color) 75%, transparent); - background: color-mix(in srgb, var(--tool-card-positive-color) 8%, transparent); + color: color-mix(in srgb, var(--color-success) 75%, transparent); + background: color-mix(in srgb, var(--color-success) 8%, transparent); } .processing-text { - color: var(--tool-card-text-secondary); + color: var(--color-text-secondary); font-style: italic; } } .compression-meta { font-size: var(--flowchat-font-size-sm); - color: var(--tool-card-text-secondary); + color: var(--color-text-secondary); white-space: nowrap; } @@ -59,14 +59,14 @@ } .icon-completed { - color: var(--tool-card-positive-icon); + color: color-mix(in srgb, var(--color-success) 90%, transparent); } .compression-detail-note { padding: var(--flowchat-card-expanded-pad-y) var(--size-gap-4); font-size: var(--flowchat-font-size-sm); line-height: var(--flowchat-support-line-height); - color: var(--tool-card-text-secondary); + color: var(--color-text-secondary); background: rgba(var(--context-compression-note-rgb), 0.08); border-top: 1px solid rgba(var(--context-compression-note-rgb), 0.18); } diff --git a/src/web-ui/src/flow_chat/tool-cards/CreatePlanDisplay.scss b/src/web-ui/src/flow_chat/tool-cards/CreatePlanDisplay.scss index 446665c8c7..cbfe998e1e 100644 --- a/src/web-ui/src/flow_chat/tool-cards/CreatePlanDisplay.scss +++ b/src/web-ui/src/flow_chat/tool-cards/CreatePlanDisplay.scss @@ -7,7 +7,7 @@ .create-plan-display { display: flex; flex-direction: column; - background: var(--color-bg-flowchat); + background: var(--color-bg-scene); border: 1px solid var(--border-base); border-radius: 8px; margin: var(--flowchat-card-gap) 0; @@ -29,13 +29,13 @@ /* Same chrome as the filled card when params are not ready yet (streaming start). */ border: 1px solid var(--border-base); border-radius: 8px; - background: var(--color-bg-flowchat); + background: var(--color-bg-scene); box-sizing: border-box; .create-plan-header { justify-content: flex-start; gap: var(--flowchat-inline-gap); - color: var(--tool-card-text-muted); + color: var(--color-text-muted); font-size: var(--tool-card-action-font-size); border-bottom: none; } @@ -55,7 +55,7 @@ 90deg, color-mix(in srgb, var(--color-warning) 7%, transparent) 0%, color-mix(in srgb, var(--color-warning) 4%, transparent) 50%, - var(--color-overlay-white-02) 100% + var(--color-overlay-white-04) 100% ); border-bottom: 1px solid var(--border-base); @@ -85,7 +85,7 @@ &:hover { .file-name { - color: var(--tool-card-text-primary); + color: var(--color-text-primary); } } } @@ -116,7 +116,7 @@ .file-name { font-size: var(--flowchat-font-size-xs); font-family: var(--tool-card-font-mono); - color: var(--tool-card-text-secondary); + color: var(--color-text-secondary); min-width: 0; overflow: hidden; text-overflow: ellipsis; @@ -139,7 +139,7 @@ border: 1px solid transparent; border-radius: 6px; background: transparent; - color: var(--tool-card-text-muted); + color: var(--color-text-muted); cursor: pointer; transition: all 0.15s ease; @@ -177,14 +177,14 @@ margin: 0 0 var(--flowchat-inline-gap) 0; font-size: var(--flowchat-font-size-base); font-weight: 600; - color: var(--tool-card-text-primary); + color: var(--color-text-primary); line-height: var(--flowchat-support-line-height); } .plan-overview { margin: 0; font-size: var(--tool-card-action-font-size); - color: var(--tool-card-text-secondary); + color: var(--color-text-secondary); line-height: var(--flowchat-support-line-height); } @@ -200,7 +200,7 @@ border: 1px solid transparent; border-radius: 6px; background: transparent; - color: var(--tool-card-text-muted); + color: var(--color-text-muted); cursor: pointer; transition: all 0.15s ease; @@ -249,7 +249,7 @@ height: 12px; &--pending { - color: var(--tool-card-text-muted); + color: var(--color-text-muted); opacity: 0.5; } @@ -270,12 +270,12 @@ .todo-content { font-size: var(--tool-card-action-font-size); line-height: var(--flowchat-support-line-height); - color: var(--tool-card-text-secondary); + color: var(--color-text-secondary); } &.status-pending { .todo-content { - color: var(--tool-card-text-secondary); + color: var(--color-text-secondary); } } @@ -283,7 +283,7 @@ background: var(--glass-blue-subtle); .todo-content { - color: var(--tool-card-text-primary); + color: var(--color-text-primary); font-weight: 500; } } @@ -291,7 +291,7 @@ &.status-completed { .todo-content { text-decoration: line-through; - color: var(--tool-card-text-muted); + color: var(--color-text-muted); opacity: 0.6; } } @@ -301,7 +301,7 @@ .todo-content { text-decoration: line-through; - color: var(--tool-card-text-muted); + color: var(--color-text-muted); } } } @@ -310,12 +310,12 @@ /* Plan params still streaming: footer actions read as disabled / grayed. */ &--plan-generating { .create-plan-footer { - background: color-mix(in srgb, var(--color-bg-flowchat) 92%, var(--tool-card-text-muted)); + background: color-mix(in srgb, var(--color-bg-scene) 92%, var(--color-text-muted)); .build-btn:disabled { opacity: 1; cursor: not-allowed; - color: var(--tool-card-text-muted); + color: var(--color-text-muted); background: color-mix(in srgb, var(--element-bg-soft) 80%, transparent); border: 1px solid var(--border-base); box-shadow: none; @@ -326,7 +326,7 @@ } svg { - color: var(--tool-card-text-muted); + color: var(--color-text-muted); } } } @@ -338,7 +338,7 @@ justify-content: space-between; padding: var(--flowchat-card-pad-y) var(--flowchat-card-pad-x); border-top: 1px solid var(--border-base); - background: var(--color-overlay-white-02); + background: var(--color-overlay-white-04); &--generating-only { justify-content: flex-end; @@ -351,7 +351,7 @@ padding: var(--flowchat-control-pad-y) var(--flowchat-control-pad-x); font-size: var(--tool-card-action-font-size); font-weight: 500; - color: var(--tool-card-text-secondary); + color: var(--color-text-secondary); background: transparent; border: none; border-radius: 4px; @@ -359,7 +359,7 @@ transition: all 0.15s ease; &:hover:not(:disabled) { - color: var(--tool-card-text-primary); + color: var(--color-text-primary); background: var(--color-overlay-white-08); } @@ -386,7 +386,7 @@ border-radius: 4px; cursor: pointer; transition: all 0.15s ease; - box-shadow: 0 1px 2px var(--color-overlay-black-10); + box-shadow: 0 1px 2px var(--color-overlay-black-12); &:hover:not(:disabled) { background: linear-gradient( @@ -434,7 +434,7 @@ var(--color-success) 0%, color-mix(in srgb, var(--color-success) 82%, var(--color-text-primary)) 100% ); - box-shadow: 0 1px 2px var(--color-overlay-black-10); + box-shadow: 0 1px 2px var(--color-overlay-black-12); } } } diff --git a/src/web-ui/src/flow_chat/tool-cards/DefaultToolCard.scss b/src/web-ui/src/flow_chat/tool-cards/DefaultToolCard.scss index 51f37c82fa..b7b11ec913 100644 --- a/src/web-ui/src/flow_chat/tool-cards/DefaultToolCard.scss +++ b/src/web-ui/src/flow_chat/tool-cards/DefaultToolCard.scss @@ -166,23 +166,23 @@ } .default-tool-card__button--confirm { - background: color-mix(in srgb, var(--tool-card-positive-color) 12%, transparent); - border-color: color-mix(in srgb, var(--tool-card-positive-color) 26%, transparent); + background: color-mix(in srgb, var(--color-success) 12%, transparent); + border-color: color-mix(in srgb, var(--color-success) 26%, transparent); color: rgb(var(--default-tool-card-confirm-text-rgb)); } .default-tool-card__button--confirm:hover:not(:disabled) { - background: color-mix(in srgb, var(--tool-card-positive-color) 18%, transparent); - border-color: color-mix(in srgb, var(--tool-card-positive-color) 40%, transparent); + background: color-mix(in srgb, var(--color-success) 18%, transparent); + border-color: color-mix(in srgb, var(--color-success) 40%, transparent); } .default-tool-card__button--reject { - background: var(--color-overlay-white-03); + background: var(--color-overlay-white-04); color: var(--color-text-secondary); } .default-tool-card__button--reject:hover:not(:disabled) { - background: var(--color-overlay-white-06); + background: var(--color-overlay-white-08); color: var(--color-text-primary); } diff --git a/src/web-ui/src/flow_chat/tool-cards/DefaultToolCard.tsx b/src/web-ui/src/flow_chat/tool-cards/DefaultToolCard.tsx index 656660d85f..1661f26c9f 100644 --- a/src/web-ui/src/flow_chat/tool-cards/DefaultToolCard.tsx +++ b/src/web-ui/src/flow_chat/tool-cards/DefaultToolCard.tsx @@ -10,8 +10,6 @@ import type { ToolCardProps } from '../types/flow-chat'; import { CompactToolCard, CompactToolCardHeader } from './CompactToolCard'; import { ToolCardStatusSlot } from './ToolCardStatusSlot'; import { useToolCardHeightContract } from './useToolCardHeightContract'; -import { hasAcpPermissionOptions } from './AcpPermissionActions.utils'; -import { AcpPermissionActions } from './AcpPermissionActions'; import { formatSessionViewPreviewText, isOnlySessionViewPreviewText, @@ -98,14 +96,11 @@ function getInlinePreview(value: any): string | null { export const DefaultToolCard: React.FC = ({ toolItem, config, - onConfirm, - onReject, - onExpand + onExpand, }) => { const { t } = useTranslation('flow-chat'); const { toolCall, toolResult, status, requiresConfirmation, userConfirmed } = toolItem; const [isExpanded, setIsExpanded] = useState(false); - const [shouldExpand, setShouldExpand] = useState(true); const toolId = toolItem.id ?? toolCall?.id; const { cardRootRef, applyExpandedState } = useToolCardHeightContract({ toolId, @@ -117,11 +112,12 @@ export const DefaultToolCard: React.FC = ({ const hasResult = toolResult !== undefined && toolResult !== null && config.resultDisplayType !== 'hidden'; const errorMessage = toolResult?.success === false ? toolResult.error || t('toolCards.default.failed') : null; const hasError = Boolean(errorMessage); - const showConfirmationActions = requiresConfirmation && !userConfirmed && + const needsConfirmation = requiresConfirmation && !userConfirmed && status !== 'completed' && status !== 'cancelled' && + status !== 'rejected' && status !== 'error'; - const canExpand = hasInput || hasResult || hasError || showConfirmationActions; + const canExpand = hasInput || hasResult || hasError; const inputPreview = useMemo(() => { if (!isExpanded || !hasInput) return null; @@ -133,36 +129,17 @@ export const DefaultToolCard: React.FC = ({ return truncatePreview(stringifyValue(toolResult?.result)); }, [hasResult, isExpanded, toolResult?.result]); - const handleConfirm = () => { - onConfirm?.(toolCall?.input); - }; - - const handleReject = () => { - onReject?.(); - }; - - const handleMouseDown = useCallback(() => { - setShouldExpand(true); - }, [applyExpandedState, canExpand, isExpanded, onExpand,shouldExpand, setShouldExpand]); - - const handleMouseMove = useCallback(() => { - setShouldExpand(false); - }, [applyExpandedState, canExpand, isExpanded, onExpand,shouldExpand, setShouldExpand]); - const handleToggleExpand = useCallback(() => { if (!canExpand) return; const nextExpanded = !isExpanded; - if (shouldExpand) { - applyExpandedState(isExpanded, nextExpanded, setIsExpanded, { - onExpand, + applyExpandedState(isExpanded, nextExpanded, setIsExpanded, { + onExpand, }); - } - setShouldExpand(true); - }, [applyExpandedState, canExpand, isExpanded, onExpand, shouldExpand, setShouldExpand]); + }, [applyExpandedState, canExpand, isExpanded, onExpand]); const getStatusText = () => { - if (requiresConfirmation && !userConfirmed) { + if (needsConfirmation) { return t('toolCards.default.waitingConfirm'); } @@ -183,6 +160,8 @@ export const DefaultToolCard: React.FC = ({ return t('toolCards.default.completed'); case 'cancelled': return t('toolCards.default.cancelled'); + case 'rejected': + return t('toolCards.default.rejected'); case 'error': return t('toolCards.default.failed'); default: @@ -191,7 +170,7 @@ export const DefaultToolCard: React.FC = ({ }; const getSummaryText = () => { - if (requiresConfirmation && !userConfirmed) { + if (needsConfirmation) { const preview = getInlinePreview(filteredInput); return preview ? `${t('toolCards.default.waitingConfirm')} - ${preview}` @@ -228,6 +207,10 @@ export const DefaultToolCard: React.FC = ({ return errorMessage || t('toolCards.default.failed'); } + if (status === 'rejected') { + return errorMessage || t('toolCards.default.rejected'); + } + if (status === 'running' || status === 'streaming') { const preview = getInlinePreview(filteredInput); return preview @@ -245,19 +228,14 @@ export const DefaultToolCard: React.FC = ({ return getStatusText(); }; - const showConfirmationHighlight = requiresConfirmation && !userConfirmed && - status !== 'completed' && - status !== 'cancelled' && - status !== 'error'; + const showConfirmationHighlight = needsConfirmation; return (
= ({
)} - {showConfirmationActions && ( -
- {hasAcpPermissionOptions(toolItem) ? ( - - ) : ( - <> - - - - )} -
- )} - {hasResult && (
{t('toolCards.common.executionResult')}
diff --git a/src/web-ui/src/flow_chat/tool-cards/ExecProcessToolCardView.test.tsx b/src/web-ui/src/flow_chat/tool-cards/ExecProcessToolCardView.test.tsx new file mode 100644 index 0000000000..7098c3d570 --- /dev/null +++ b/src/web-ui/src/flow_chat/tool-cards/ExecProcessToolCardView.test.tsx @@ -0,0 +1,168 @@ +import React from 'react'; +import { act } from 'react'; +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; +import { createRoot, type Root } from 'react-dom/client'; +import { JSDOM } from 'jsdom'; + +import { ExecProcessToolCardView, type ExecProcessCardModel } from './ExecProcessToolCardView'; +import type { FlowToolItem } from '../types/flow-chat'; + +globalThis.IS_REACT_ACT_ENVIRONMENT = true; + +const messages: Record = { + 'toolCards.terminal.cancelled': 'Cancelled', + 'toolCards.terminal.rejected': 'Rejected', + 'toolCards.terminal.receivingParams': 'Receiving parameters...', + 'toolCards.terminal.exitCode': 'Exit code: {{code}}', + 'toolCards.approval.waiting': 'Waiting for confirmation', + 'toolCards.execProcess.copyPrimary': 'Copy', + 'toolCards.execProcess.primaryCopied': 'Copied', + 'toolCards.execProcess.copyPrimaryFailed': 'Failed to copy', +}; + +vi.mock('react-i18next', async () => { + const actual = await vi.importActual('react-i18next'); + return { + ...actual, + useTranslation: () => ({ + t: (key: string, options?: Record) => { + const template = messages[key] ?? key; + return template.replace(/{{(\w+)}}/g, (_, name) => String(options?.[name] ?? '')); + }, + }), + }; +}); + +vi.mock('../../component-library', () => ({ + DotMatrixLoader: () => , + ToolProcessingDots: () => , + IconButton: ({ + children, + tooltip, + ...props + }: React.ButtonHTMLAttributes & { tooltip?: React.ReactNode }) => ( + + ), +})); + +vi.mock('@/tools/terminal/components/LazyTerminalOutputRenderer', () => ({ + LazyTerminalOutputRenderer: React.forwardRef< + { getVisibleText: () => string }, + { content: string; className?: string } + >(({ content, className }, ref) => { + React.useImperativeHandle(ref, () => ({ getVisibleText: () => content }), [content]); + return
{content}
; + }), +})); + +const model: ExecProcessCardModel = { + kind: 'command', + actionLabel: 'Run command:', + primaryText: 'npm test', + emptyText: '[No command]', + copyText: 'npm test', + waitingText: 'Running command...', + noOutputText: 'No output', + resultOutput: '', +}; + +function toolItem(status: FlowToolItem['status'], isParamsStreaming = false): FlowToolItem { + return { + id: 'tool-exec-1', + type: 'tool', + toolName: 'ExecCommand', + status, + timestamp: Date.now(), + isParamsStreaming, + toolCall: { + id: 'call-exec-1', + input: { cmd: 'npm test' }, + }, + }; +} + +describe('ExecProcessToolCardView', () => { + let dom: JSDOM; + let container: HTMLDivElement; + let root: Root; + + beforeEach(() => { + dom = new JSDOM('
', { + pretendToBeVisual: true, + }); + vi.stubGlobal('window', dom.window); + vi.stubGlobal('document', dom.window.document); + vi.stubGlobal('HTMLElement', dom.window.HTMLElement); + vi.stubGlobal('CustomEvent', dom.window.CustomEvent); + vi.stubGlobal('ResizeObserver', class { + observe = vi.fn(); + disconnect = vi.fn(); + }); + + container = dom.window.document.getElementById('root') as HTMLDivElement; + root = createRoot(container); + }); + + afterEach(() => { + act(() => { + root.unmount(); + }); + vi.unstubAllGlobals(); + }); + + it('shows cancelled state instead of receiving params when a stale streaming flag remains', () => { + act(() => { + root.render(); + }); + + act(() => { + root.render(); + }); + + expect(container.textContent).toContain('Cancelled'); + expect(container.textContent).not.toContain('Receiving parameters...'); + }); + + it('shows rejected state for user-rejected command confirmation', () => { + act(() => { + root.render(); + }); + + expect(container.textContent).toContain('Rejected'); + expect(container.textContent).not.toContain('Receiving parameters...'); + }); + + it('keeps legacy cancelled rejection state labeled as rejected', () => { + act(() => { + root.render( + , + ); + }); + + expect(container.textContent).toContain('Rejected'); + expect(container.textContent).not.toContain('Receiving parameters...'); + }); + + it('shows waiting confirmation instead of receiving params while confirmation is pending', () => { + act(() => { + root.render(); + }); + + expect(container.querySelector('.base-tool-card')).not.toBeNull(); + expect(container.querySelector('.compact-tool-card')).toBeNull(); + expect(container.textContent).toContain('Waiting for confirmation'); + expect(container.textContent).not.toContain('Receiving parameters...'); + }); +}); diff --git a/src/web-ui/src/flow_chat/tool-cards/ExecProcessToolCardView.tsx b/src/web-ui/src/flow_chat/tool-cards/ExecProcessToolCardView.tsx index b8ad0f7982..a54880dec7 100644 --- a/src/web-ui/src/flow_chat/tool-cards/ExecProcessToolCardView.tsx +++ b/src/web-ui/src/flow_chat/tool-cards/ExecProcessToolCardView.tsx @@ -66,6 +66,31 @@ function getAutoExpandedStateForStatus(status: string): boolean | null { return null; } +function isCancelledStatus(status: string): boolean { + return status === 'cancelled'; +} + +function isUserRejectedTool(toolItem: FlowToolItem): boolean { + if (toolItem.status === 'rejected') { + return true; + } + + if (toolItem.status === 'cancelled') { + if (toolItem.userConfirmed === false) { + return true; + } + + const error = toolItem.toolResult?.error; + return typeof error === 'string' && /\buser rejected\b/i.test(error); + } + + return false; +} + +function isRejectedOrCancelledStatus(toolItem: FlowToolItem): boolean { + return isCancelledStatus(toolItem.status) || isUserRejectedTool(toolItem); +} + function readProgressLogs(toolItem: FlowToolItem): string[] { const logs = (toolItem as any)._progressLogs; return Array.isArray(logs) ? logs.filter((entry): entry is string => typeof entry === 'string') : []; @@ -149,6 +174,13 @@ export const ExecProcessToolCardView: React.FC = ( return typeof progressMessage === 'string' ? progressMessage : ''; }, [progressLogs, toolItem]); const isRunning = status === 'preparing' || status === 'streaming' || status === 'running' || status === 'receiving'; + const rejectedOrCancelled = isRejectedOrCancelledStatus(toolItem); + const cancelledStatusLabelKey = isUserRejectedTool(toolItem) + ? 'toolCards.terminal.rejected' + : 'toolCards.terminal.cancelled'; + const cancelledStatusClassName = isUserRejectedTool(toolItem) + ? 'status-rejected' + : 'status-cancelled'; const maxRows = isRunning ? EXEC_OUTPUT_STREAMING_MAX_ROWS : EXEC_OUTPUT_EXPANDED_MAX_ROWS; const toolId = toolItem.id ?? toolItem.toolCall?.id; const icon = ; @@ -333,7 +365,7 @@ export const ExecProcessToolCardView: React.FC = ( completedStatus={ status === 'completed' ? model.exitCode === 0 || model.exitCode == null ? 'success' : 'error' - : status === 'error' ? 'error' : status === 'cancelled' ? 'cancelled' : undefined + : status === 'error' ? 'error' : rejectedOrCancelled ? 'cancelled' : undefined } />
@@ -342,6 +374,11 @@ export const ExecProcessToolCardView: React.FC = ( const renderHeaderExtra = () => ( {renderTimeoutIndicator()} + {rejectedOrCancelled && ( + + {t(cancelledStatusLabelKey)} + + )} {renderCopyButton()} @@ -378,19 +415,31 @@ export const ExecProcessToolCardView: React.FC = ( ); } - if (status === 'cancelled' && liveOutput) { + if (rejectedOrCancelled) { return (
-
- {renderOutputWithCopyAction(liveOutput)} -
+ {liveOutput && ( +
+ {renderOutputWithCopyAction(liveOutput)} +
+ )}
- {t('toolCards.terminal.commandInterrupted')} + + {t(cancelledStatusLabelKey)} +
); } + if (status === 'pending_confirmation') { + return ( +
+ {t('toolCards.approval.waiting')} +
+ ); + } + if (liveOutput && isRunning) { return (
@@ -443,6 +492,11 @@ export const ExecProcessToolCardView: React.FC = ( {renderPrimaryText('compact')} {renderTimeoutIndicator()} + {rejectedOrCancelled && ( + + {t(cancelledStatusLabelKey)} + + )} {renderCopyButton()} @@ -464,6 +518,7 @@ export const ExecProcessToolCardView: React.FC = ( expandedContent={renderExpandedContent()} errorContent={renderErrorContent()} isFailed={status === 'error'} + requiresConfirmation={status === 'pending_confirmation'} /> ) : ( = ({ config, sessionId, onOpenInEditor, - onConfirm, - onReject, displayContext, }) => { const { t } = useTranslation('flow-chat'); @@ -261,6 +256,7 @@ export const FileOperationToolCard: React.FC = ({ !userConfirmed && status !== 'completed' && status !== 'cancelled' && + status !== 'rejected' && status !== 'error' ); @@ -639,18 +635,6 @@ export const FileOperationToolCard: React.FC = ({ toolItem.toolName, ]); - const handleConfirmClick = useCallback((e: React.MouseEvent) => { - e.preventDefault(); - e.stopPropagation(); - onConfirm?.(toolCall?.input); - }, [onConfirm, toolCall?.input]); - - const handleRejectClick = useCallback((e: React.MouseEvent) => { - e.preventDefault(); - e.stopPropagation(); - onReject?.(); - }, [onReject]); - const handleOpenFullCodeClick = useCallback((e: React.MouseEvent) => { e.preventDefault(); e.stopPropagation(); @@ -1100,38 +1084,6 @@ export const FileOperationToolCard: React.FC = ({ {currentFilePath ? t('toolCards.file.receivingParams') : t('toolCards.file.analyzing')} )} - {showConfirmationActions && ( - hasAcpPermissionOptions(toolItem) ? ( - - ) : ( - <> - - - - - - - - ) - )} {canOpenFullCode && (
); diff --git a/src/web-ui/src/flow_chat/tool-cards/MiniAppToolDisplay.scss b/src/web-ui/src/flow_chat/tool-cards/MiniAppToolDisplay.scss index 43ebee9764..b0aa5cf50c 100644 --- a/src/web-ui/src/flow_chat/tool-cards/MiniAppToolDisplay.scss +++ b/src/web-ui/src/flow_chat/tool-cards/MiniAppToolDisplay.scss @@ -51,7 +51,7 @@ font-family: var(--tool-card-font-mono); font-size: var(--tool-card-action-font-size); font-weight: 500; - color: var(--tool-card-text-primary); + color: var(--color-text-primary); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; @@ -61,7 +61,7 @@ .output-summary { font-size: var(--flowchat-font-size-xs); - color: var(--tool-card-text-secondary); + color: var(--color-text-secondary); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; @@ -143,7 +143,7 @@ border-radius: 4px; border: 1px solid var(--border-subtle); background: transparent; - color: var(--tool-card-text-primary); + color: var(--color-text-primary); font-size: var(--flowchat-font-size-sm); font-weight: 500; cursor: pointer; @@ -152,7 +152,7 @@ &:hover { background: var(--element-bg-hover); - border-color: var(--border-primary); + border-color: var(--border-base); } } @@ -175,7 +175,7 @@ align-items: center; gap: var(--flowchat-control-gap); font-size: var(--flowchat-font-size-sm); - color: var(--tool-card-text-secondary); + color: var(--color-text-secondary); .error-operation { color: color-mix(in srgb, var(--color-error) 70%, transparent); diff --git a/src/web-ui/src/flow_chat/tool-cards/ModelThinkingDisplay.scss b/src/web-ui/src/flow_chat/tool-cards/ModelThinkingDisplay.scss index 37d1d353a7..2d122266f0 100644 --- a/src/web-ui/src/flow_chat/tool-cards/ModelThinkingDisplay.scss +++ b/src/web-ui/src/flow_chat/tool-cards/ModelThinkingDisplay.scss @@ -43,7 +43,7 @@ &:hover { .thinking-label { - color: var(--text-secondary); + color: var(--color-text-secondary); opacity: 1; } @@ -54,7 +54,7 @@ .thinking-chevron { flex-shrink: 0; - color: var(--text-tertiary); + color: var(--color-text-muted); opacity: 0.6; display: block; transition: transform 0.28s cubic-bezier(0.4, 0, 0.2, 1), opacity 0.15s ease; @@ -64,7 +64,7 @@ .thinking-label { font-size: var(--flowchat-font-size-sm); font-weight: normal; - color: var(--text-tertiary); + color: var(--color-text-muted); opacity: 0.8; line-height: var(--flowchat-support-line-height); } @@ -96,7 +96,7 @@ /* Match FlowChat body text (sans); mono stack + generic monospace yields Songti-like CJK on Windows. */ font-family: var(--font-family-sans); word-break: break-word; - color: var(--tool-card-text-muted); + color: var(--color-text-muted); padding: var(--flowchat-card-expanded-pad-y) var(--flowchat-card-expanded-pad-x); background: transparent; border: none; @@ -121,11 +121,11 @@ position: relative; min-height: 0; /* Required for the 0fr grid trick */ /* - * Match FlowChat list surface (--color-bg-flowchat === scene), not app chrome primary. + * Match FlowChat list surface, not app chrome primary. * Use a solid fill + mask fade so we never interpolate theme colors with `transparent` in sRGB * (that pulls toward black and looks wrong on near-black themes). */ - --thinking-scroll-fade-base: var(--color-bg-flowchat); + --thinking-scroll-fade-base: var(--color-bg-scene); /* Top fade */ &::before { @@ -190,9 +190,9 @@ } } -/* Markdown body: same sans as rest of chat; code stays mono via --markdown-font-mono */ +/* Markdown body: same sans as rest of chat; code stays mono via the tool-card font token. */ .thinking-content .markdown-renderer.thinking-markdown { - --markdown-font-mono: var(--tool-card-font-mono); + --font-family-mono: var(--tool-card-font-mono); --markdown-font-heading: var(--font-family-sans); --markdown-font-size: var(--flowchat-font-size-base); --markdown-font-size-sm: var(--flowchat-font-size-sm); @@ -204,12 +204,12 @@ font-family: var(--font-family-sans); font-size: var(--flowchat-font-size-base); line-height: var(--flowchat-text-line-height); - color: var(--tool-card-text-muted); + color: var(--color-text-muted); animation: none; h1, h2, h3, h4, h5, h6, strong { - color: var(--tool-card-text-secondary); + color: var(--color-text-secondary); } p, @@ -218,20 +218,20 @@ li, blockquote, .custom-blockquote { - color: var(--tool-card-text-muted); + color: var(--color-text-muted); } em { - color: var(--tool-card-text-muted); + color: var(--color-text-muted); } hr { - background: color-mix(in srgb, var(--tool-card-text-muted) 25%, transparent); + background: color-mix(in srgb, var(--color-text-muted) 25%, transparent); } blockquote, .custom-blockquote { - border-left: 2px solid color-mix(in srgb, var(--tool-card-text-muted) 35%, transparent); + border-left: 2px solid color-mix(in srgb, var(--color-text-muted) 35%, transparent); background: transparent; } @@ -240,13 +240,13 @@ border-radius: 0; background: transparent; border: none; - color: var(--tool-card-text-secondary); + color: var(--color-text-secondary); } .code-block-wrapper { border-radius: 6px; - border: 1px solid color-mix(in srgb, var(--tool-card-text-muted) 18%, transparent); - background: color-mix(in srgb, var(--tool-card-text-muted) 8%, transparent); + border: 1px solid color-mix(in srgb, var(--color-text-muted) 18%, transparent); + background: color-mix(in srgb, var(--color-text-muted) 8%, transparent); } .code-block-toolbar { @@ -286,7 +286,7 @@ } .code-block-wrapper .copy-button.copy-success:hover { - color: var(--markdown-copy-success-color); + color: var(--color-success); transform: scale(0.93); } @@ -296,16 +296,16 @@ .table-wrapper th, .table-wrapper td { - color: var(--tool-card-text-muted); - border-color: color-mix(in srgb, var(--tool-card-text-muted) 22%, transparent); + color: var(--color-text-muted); + border-color: color-mix(in srgb, var(--color-text-muted) 22%, transparent); } .table-wrapper th { - color: var(--tool-card-text-secondary); - background: color-mix(in srgb, var(--tool-card-text-muted) 6%, transparent); + color: var(--color-text-secondary); + background: color-mix(in srgb, var(--color-text-muted) 6%, transparent); } .table-wrapper tbody tr:nth-child(2n) { - background: color-mix(in srgb, var(--tool-card-text-muted) 5%, transparent); + background: color-mix(in srgb, var(--color-text-muted) 5%, transparent); } } diff --git a/src/web-ui/src/flow_chat/tool-cards/ReadFileDisplay.test.tsx b/src/web-ui/src/flow_chat/tool-cards/ReadFileDisplay.test.tsx index c3fcab8d1c..6b28d7978a 100644 --- a/src/web-ui/src/flow_chat/tool-cards/ReadFileDisplay.test.tsx +++ b/src/web-ui/src/flow_chat/tool-cards/ReadFileDisplay.test.tsx @@ -25,23 +25,6 @@ vi.mock('react-i18next', async () => { vi.mock('../../component-library', () => ({ ToolProcessingDots: () => , - IconButton: ({ - children, - tooltip, - ...props - }: React.ButtonHTMLAttributes & { tooltip?: React.ReactNode }) => ( - - ), -})); - -vi.mock('./ToolCardHeaderActions', () => ({ - ToolCardHeaderActions: ({ children }: { children: React.ReactNode }) => {children}, })); describe('ReadFileDisplay', () => { @@ -69,10 +52,7 @@ describe('ReadFileDisplay', () => { vi.unstubAllGlobals(); }); - it('renders ACP permission actions for pending read confirmation', () => { - const onConfirm = vi.fn(); - const onReject = vi.fn(); - + it('renders pending read confirmation copy without inline approval actions', () => { const toolItem: FlowToolItem = { id: 'tool-read-1', type: 'tool', @@ -120,27 +100,13 @@ describe('ReadFileDisplay', () => { ); }); expect(container.textContent).toContain('Requesting read permission:'); expect(container.textContent).toContain('/'); - - const actionButtons = container.querySelectorAll('button'); - expect(actionButtons).toHaveLength(2); - - act(() => { - actionButtons[0]?.dispatchEvent(new dom.window.MouseEvent('click', { bubbles: true })); - }); - expect(onConfirm).toHaveBeenCalledWith(toolItem.toolCall.input, 'once', true); - - act(() => { - actionButtons[1]?.dispatchEvent(new dom.window.MouseEvent('click', { bubbles: true })); - }); - expect(onReject).toHaveBeenCalledWith('reject'); + expect(container.querySelectorAll('button')).toHaveLength(0); }); it('does not report a file size for session preview truncation markers', () => { diff --git a/src/web-ui/src/flow_chat/tool-cards/ReadFileDisplay.tsx b/src/web-ui/src/flow_chat/tool-cards/ReadFileDisplay.tsx index 2af7f57981..caebd3e9f9 100644 --- a/src/web-ui/src/flow_chat/tool-cards/ReadFileDisplay.tsx +++ b/src/web-ui/src/flow_chat/tool-cards/ReadFileDisplay.tsx @@ -2,23 +2,17 @@ * Compact display for the read_file tool. */ -import React, { useMemo, useState } from 'react'; -import { Check, FileText, X } from 'lucide-react'; +import React, { useMemo } from 'react'; +import { FileText } from 'lucide-react'; import { useTranslation } from 'react-i18next'; -import { IconButton } from '../../component-library'; import type { ToolCardProps } from '../types/flow-chat'; -import { AcpPermissionActions } from './AcpPermissionActions'; -import { hasAcpPermissionOptions } from './AcpPermissionActions.utils'; import { CompactToolCard, CompactToolCardHeader } from './CompactToolCard'; -import { ToolCardHeaderActions } from './ToolCardHeaderActions'; import { ToolCardStatusSlot } from './ToolCardStatusSlot'; import { isSessionViewPreviewText } from '../utils/sessionViewPreview'; export const ReadFileDisplay: React.FC = React.memo(({ toolItem, - onConfirm, - onReject, - onOpenInEditor + onOpenInEditor, }) => { const { t } = useTranslation('flow-chat'); const { toolCall, toolResult, status, requiresConfirmation, userConfirmed } = toolItem; @@ -72,11 +66,17 @@ export const ReadFileDisplay: React.FC = React.memo(({ }, [filePath, toolItem.acpPermission?.toolCall?.rawInput]); const lineRange = useMemo(() => { - const start_line = toolCall?.input?.start_line; + // Keep legacy `start_line` so older persisted tool calls still render. + const offset = toolCall?.input?.offset ?? toolCall?.input?.start_line; + const tail = toolCall?.input?.tail === true; const limit = toolCall?.input?.limit; - if (start_line !== undefined || limit !== undefined) { - const startLine = start_line || 1; + if (tail && limit !== undefined) { + return `tail ${limit} lines`; + } + + if (offset !== undefined || limit !== undefined) { + const startLine = offset || 1; const endLine = limit ? startLine + limit - 1 : undefined; if (endLine) { @@ -87,7 +87,7 @@ export const ReadFileDisplay: React.FC = React.memo(({ } return null; - }, [toolCall?.input?.start_line, toolCall?.input?.limit]); + }, [toolCall?.input?.offset, toolCall?.input?.start_line, toolCall?.input?.tail, toolCall?.input?.limit]); const fileSize = useMemo(() => { if (!toolResult?.result) return null; @@ -109,11 +109,10 @@ export const ReadFileDisplay: React.FC = React.memo(({ !userConfirmed && status !== 'completed' && status !== 'cancelled' && + status !== 'rejected' && status !== 'error' ); - const [shouldExpand, setShouldExpand] = useState(true); - if (status === 'error') { return null; } @@ -156,82 +155,17 @@ export const ReadFileDisplay: React.FC = React.memo(({ return null; }; - const handleMouseDown = () => { - setShouldExpand(true); - } - - const handleMouseMove = () => { - setShouldExpand(false); - } - - const handleMouseUp = () => { - if (shouldExpand && canOpenFile) { - handleOpenInEditor(); - } - setShouldExpand(true); - } - - const renderActions = () => { - if (!showConfirmationActions) { - return undefined; - } - - return ( - - {hasAcpPermissionOptions(toolItem) ? ( - - ) : ( - <> - { - event.stopPropagation(); - onConfirm?.(toolCall?.input); - }} - tooltip={t('toolCards.default.waitingConfirm')} - > - - - { - event.stopPropagation(); - onReject?.(); - }} - tooltip={t('toolCards.acpPermission.reject')} - > - - - - )} - - ); - }; - return ( canOpenFile && handleOpenInEditor()} className="read-file-card" clickable={canOpenFile} header={ } />} content={renderContent()} - extra={renderActions()} /> } /> diff --git a/src/web-ui/src/flow_chat/tool-cards/SessionControlToolCard.tsx b/src/web-ui/src/flow_chat/tool-cards/SessionControlToolCard.tsx index 0ecc53896e..c52a7a2e6b 100644 --- a/src/web-ui/src/flow_chat/tool-cards/SessionControlToolCard.tsx +++ b/src/web-ui/src/flow_chat/tool-cards/SessionControlToolCard.tsx @@ -246,7 +246,7 @@ export const SessionControlToolCard: React.FC = React.memo(({ gap: 2, padding: '8px 10px', borderRadius: 8, - background: 'var(--color-bg-subtle)' + background: 'var(--element-bg-subtle)' }} > @@ -266,7 +266,7 @@ export const SessionControlToolCard: React.FC = React.memo(({ )} {toolResult?.error && ( -
+
{toolResult.error}
)} diff --git a/src/web-ui/src/flow_chat/tool-cards/SessionMessageToolCard.tsx b/src/web-ui/src/flow_chat/tool-cards/SessionMessageToolCard.tsx index 2bb7b43d88..e7beaba14d 100644 --- a/src/web-ui/src/flow_chat/tool-cards/SessionMessageToolCard.tsx +++ b/src/web-ui/src/flow_chat/tool-cards/SessionMessageToolCard.tsx @@ -126,7 +126,7 @@ export const SessionMessageToolCard: React.FC = React.memo(({ borderRadius: 8, whiteSpace: 'pre-wrap', wordBreak: 'break-word', - background: 'var(--color-bg-subtle)', + background: 'var(--element-bg-subtle)', fontFamily: 'var(--tool-card-font-mono)' }} > @@ -136,7 +136,7 @@ export const SessionMessageToolCard: React.FC = React.memo(({ )} {toolResult?.error && ( -
+
{toolResult.error}
)} diff --git a/src/web-ui/src/flow_chat/tool-cards/SnapshotFullscreenDiffViewer.css b/src/web-ui/src/flow_chat/tool-cards/SnapshotFullscreenDiffViewer.css index a0e1147b4b..e45075ee5e 100644 --- a/src/web-ui/src/flow_chat/tool-cards/SnapshotFullscreenDiffViewer.css +++ b/src/web-ui/src/flow_chat/tool-cards/SnapshotFullscreenDiffViewer.css @@ -24,7 +24,7 @@ display: flex; flex-direction: column; overflow: hidden; - border: 1px solid var(--color-overlay-white-10); + border: 1px solid var(--color-overlay-white-12); box-shadow: 0 20px 60px var(--color-overlay-black-80); } @@ -35,7 +35,7 @@ justify-content: space-between; padding: 16px 20px; background: var(--color-bg-primary); - border-bottom: 1px solid var(--color-overlay-white-10); + border-bottom: 1px solid var(--color-overlay-white-12); flex-shrink: 0; } @@ -133,20 +133,20 @@ } .header-btn.close-btn { - background: var(--color-overlay-white-05); + background: var(--color-overlay-white-08); color: var(--color-overlay-white-70); - border: 1px solid var(--color-overlay-white-10); + border: 1px solid var(--color-overlay-white-12); } .header-btn.close-btn:hover { - background: var(--color-overlay-white-10); + background: var(--color-overlay-white-12); color: var(--color-overlay-white-90); } .header-divider { width: 1px; height: 24px; - background: var(--color-overlay-white-10); + background: var(--color-overlay-white-12); margin: 0 4px; } @@ -157,7 +157,7 @@ gap: 8px; padding: 12px 20px; background: var(--diff-fullscreen-nav-bg); - border-bottom: 1px solid var(--color-overlay-white-05); + border-bottom: 1px solid var(--color-overlay-white-08); flex-shrink: 0; overflow: hidden; } @@ -170,7 +170,7 @@ height: 28px; border: none; border-radius: 4px; - background: var(--color-overlay-white-05); + background: var(--color-overlay-white-08); color: var(--color-overlay-white-70); cursor: pointer; transition: all 0.2s ease; @@ -178,7 +178,7 @@ } .nav-btn:hover:not(:disabled) { - background: var(--color-overlay-white-10); + background: var(--color-overlay-white-12); color: var(--color-overlay-white-90); } @@ -219,7 +219,7 @@ } .file-tab:hover { - background: var(--color-overlay-white-05); + background: var(--color-overlay-white-08); color: var(--color-overlay-white-80); } @@ -261,7 +261,7 @@ justify-content: space-between; padding: 12px 20px; background: var(--diff-fullscreen-surface-bg); - border-bottom: 1px solid var(--color-overlay-white-05); + border-bottom: 1px solid var(--color-overlay-white-08); flex-shrink: 0; } diff --git a/src/web-ui/src/flow_chat/tool-cards/TaskToolDisplay.scss b/src/web-ui/src/flow_chat/tool-cards/TaskToolDisplay.scss index 2dc095ab1a..2ac818c486 100644 --- a/src/web-ui/src/flow_chat/tool-cards/TaskToolDisplay.scss +++ b/src/web-ui/src/flow_chat/tool-cards/TaskToolDisplay.scss @@ -12,7 +12,7 @@ color: var(--color-text-secondary); &.is-running { - color: var(--color-primary); + color: var(--color-accent-500); } } @@ -87,7 +87,7 @@ } &:hover::before { - background-color: var(--tool-card-bg-hover); + background-color: var(--element-bg-hover); } &:hover .task-header-rail__visual { @@ -248,7 +248,7 @@ font-weight: 500; flex-shrink: 0; color: var(--color-text-secondary); - background: var(--color-overlay-white-10); + background: var(--color-overlay-white-12); letter-spacing: 0.02em; } @@ -331,7 +331,7 @@ display: flex; align-items: center; gap: var(--flowchat-inline-gap); - color: var(--tool-card-text-secondary); + color: var(--color-text-secondary); font-size: var(--flowchat-font-size-xs); font-weight: 600; letter-spacing: 0.01em; @@ -359,7 +359,7 @@ margin-bottom: var(--flowchat-card-gap); font-size: var(--flowchat-font-size-xs); font-weight: 600; - color: var(--tool-card-text-secondary); + color: var(--color-text-secondary); text-transform: uppercase; letter-spacing: 0.5px; opacity: 0.8; @@ -398,7 +398,7 @@ align-items: center; gap: var(--flowchat-inline-gap); font-size: var(--flowchat-font-size-xs); - color: var(--tool-card-text-muted); + color: var(--color-text-muted); } .error-task, @@ -407,7 +407,7 @@ } .error-separator { - color: var(--tool-card-border); + color: var(--border-base); } .tool-actions { @@ -433,21 +433,21 @@ } .confirm-button { - background: var(--color-primary); + background: var(--color-accent-500); color: var(--color-static-white); &:hover:not(:disabled) { - background: var(--color-primary-hover); + background: var(--color-accent-600); } } .reject-button { - background: var(--tool-card-bg-secondary); - color: var(--tool-card-text-primary); - border: 1px solid var(--tool-card-border); + background: var(--element-bg-soft); + color: var(--color-text-primary); + border: 1px solid var(--border-base); &:hover:not(:disabled) { - background: var(--tool-card-bg-hover); + background: var(--element-bg-hover); border-color: var(--color-error); color: var(--color-error); } @@ -501,13 +501,13 @@ // Expanded state: the wrapper becomes the unified visual container. &.task-with-subagent-wrapper--expanded { // The wrapper itself provides the unified card appearance. - background: var(--color-bg-flowchat); + background: var(--color-bg-scene); border: 1px solid var(--border-base); backdrop-filter: blur(10px); overflow: hidden; box-shadow: 0 2px 6px var(--color-overlay-black-15), - 0 1px 3px var(--color-overlay-black-10); + 0 1px 3px var(--color-overlay-black-12); // Strip the BaseToolCard's own border/background/shadow so it merges // into the wrapper; the wrapper provides the unified card chrome. diff --git a/src/web-ui/src/flow_chat/tool-cards/TaskToolDisplay.tsx b/src/web-ui/src/flow_chat/tool-cards/TaskToolDisplay.tsx index 6e7e7ac20b..a4625f1f54 100644 --- a/src/web-ui/src/flow_chat/tool-cards/TaskToolDisplay.tsx +++ b/src/web-ui/src/flow_chat/tool-cards/TaskToolDisplay.tsx @@ -10,7 +10,7 @@ import { } from 'lucide-react'; import { useTranslation } from 'react-i18next'; -import { CubeLoading, Button } from '../../component-library'; +import { CubeLoading } from '../../component-library'; import { Markdown } from '@/component-library/components/Markdown/Markdown'; import type { FlowToolItem, ToolCardProps } from '../types/flow-chat'; import { BaseToolCard } from './BaseToolCard'; @@ -21,8 +21,6 @@ import { useToolCardHeightContract } from './useToolCardHeightContract'; import { ToolTimeoutIndicator } from './ToolTimeoutIndicator'; import { getReviewerContextBySubagentId } from '@/shared/services/reviewTeamService'; import type { ReviewerContext } from '@/shared/services/reviewTeamService'; -import { hasAcpPermissionOptions } from './AcpPermissionActions.utils'; -import { AcpPermissionActions } from './AcpPermissionActions'; import { openBtwSessionInAuxPane } from '../services/btwSessionPane'; import { flowChatStore } from '../store/FlowChatStore'; import { useSessionGoalModeActive } from '../hooks/useSessionGoalModeActive'; @@ -95,10 +93,8 @@ function isDeepReviewReviewerTask(toolItem: FlowToolItem): boolean { export const TaskToolDisplay: React.FC = ({ toolItem, interruptionNote, - onConfirm, - onReject, onOpenInPanel, - sessionId + sessionId, }) => { const { t } = useTranslation('flow-chat'); const defaultTimeoutDisabled = useSessionGoalModeActive(sessionId); @@ -223,7 +219,7 @@ export const TaskToolDisplay: React.FC = ({ ); const hasInterruptionNote = Boolean(interruptionNote); const needsConfirmation = - requiresConfirmation && !userConfirmed && status !== 'completed'; + requiresConfirmation && !userConfirmed && status !== 'completed' && status !== 'cancelled' && status !== 'rejected' && status !== 'error'; /* Prompt body: same scroll + Markdown shell as ModelThinkingDisplay. */ const promptContentRef = useRef(null); @@ -258,7 +254,7 @@ export const TaskToolDisplay: React.FC = ({ const taskErrorMessage = readTaskErrorMessage(toolResult); const completedDurationStatus = isFailed ? 'error' - : status === 'cancelled' + : status === 'cancelled' || status === 'rejected' ? 'cancelled' : status === 'completed' && taskDurationMs != null ? 'success' @@ -509,41 +505,6 @@ export const TaskToolDisplay: React.FC = ({
) )} - {needsConfirmation && ( -
- {hasAcpPermissionOptions(toolItem) ? ( - - ) : ( - <> - - - - )} -
- )}
); }; @@ -559,7 +520,7 @@ export const TaskToolDisplay: React.FC = ({ expandedContent={renderExpandedContent()} headerExpandAffordance={showHeaderExpandHint} isFailed={isFailed} - requiresConfirmation={requiresConfirmation && !userConfirmed} + requiresConfirmation={needsConfirmation} />
); diff --git a/src/web-ui/src/flow_chat/tool-cards/TerminalToolCard.scss b/src/web-ui/src/flow_chat/tool-cards/TerminalToolCard.scss index d6b217eda9..0a49328ad7 100644 --- a/src/web-ui/src/flow_chat/tool-cards/TerminalToolCard.scss +++ b/src/web-ui/src/flow_chat/tool-cards/TerminalToolCard.scss @@ -22,7 +22,7 @@ */ .compact-card-action, .compact-card-content .tool-command-preview.tool-command-preview--compact { - font-family: var(--tool-compact-summary-font); + font-family: var(--font-family-sans); font-size: var(--flowchat-font-size-sm); line-height: var(--flowchat-support-line-height); font-weight: normal; @@ -35,7 +35,7 @@ .terminal-header-duration .duration-text { gap: 0.18rem; - font-family: var(--tool-compact-summary-font); + font-family: var(--font-family-sans); font-size: var(--flowchat-font-size-sm); line-height: var(--flowchat-support-line-height); font-weight: normal; @@ -100,7 +100,7 @@ font-family: var(--tool-card-font-mono); font-size: var(--flowchat-font-size-sm); font-weight: 500; - color: var(--tool-card-text-muted); + color: var(--color-text-muted); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; @@ -128,7 +128,7 @@ border: none !important; outline: none !important; box-shadow: none !important; - color: var(--text-secondary); + color: var(--color-text-secondary); } } @@ -142,7 +142,7 @@ } .command-empty { - color: #e06c75; + color: rgb(var(--tool-command-empty-rgb)); font-style: italic; } } @@ -286,16 +286,6 @@ flex-shrink: 0; } -/* ========== Confirmation action button group ========== */ -.terminal-confirm-actions { - display: inline-flex; - align-items: center; - justify-content: flex-start; - gap: 0.125rem; - flex-shrink: 0; - width: auto; -} - /* ========== Common action button styles ========== */ .terminal-action-btn { display: flex; @@ -329,14 +319,14 @@ left: 0; right: 0; bottom: 0; - background: linear-gradient(135deg, var(--color-overlay-white-10), transparent); + background: linear-gradient(135deg, var(--color-overlay-white-12), transparent); opacity: 0; transition: opacity 0.2s ease; z-index: 0; } &:hover:not(:disabled) { - background: var(--color-overlay-white-10); + background: var(--color-overlay-white-12); transform: scale(1.05); } @@ -389,7 +379,7 @@ color: var(--color-text-muted); &:hover:not(:disabled) { - background: var(--color-overlay-white-10); + background: var(--color-overlay-white-12); color: var(--color-text-secondary); box-shadow: none; } @@ -615,12 +605,6 @@ height: 16px; } - .terminal-confirm-actions { - gap: 1px; - width: auto !important; - justify-content: flex-start !important; - } - .terminal-command, .terminal-command-input { font-size: var(--flowchat-font-size-xs); diff --git a/src/web-ui/src/flow_chat/tool-cards/TerminalToolCard.tsx b/src/web-ui/src/flow_chat/tool-cards/TerminalToolCard.tsx index 199bb63eb5..11510ea4ca 100644 --- a/src/web-ui/src/flow_chat/tool-cards/TerminalToolCard.tsx +++ b/src/web-ui/src/flow_chat/tool-cards/TerminalToolCard.tsx @@ -16,7 +16,7 @@ import React, { useState, useRef, useCallback, useEffect, useLayoutEffect, useMemo } from 'react'; import { useTranslation } from 'react-i18next'; import type { ToolCardProps } from '../types/flow-chat'; -import { Terminal, Play, X, ExternalLink, Square } from 'lucide-react'; +import { Terminal, ExternalLink, Square } from 'lucide-react'; import { ToolCardStatusSlot } from './ToolCardStatusSlot'; import { createTerminalTab } from '@/shared/utils/tabUtils'; import { BaseToolCard, ToolCardHeader } from './BaseToolCard'; @@ -29,8 +29,6 @@ import { getTerminalViewState, type TerminalViewState } from './terminalToolCard import { ToolTimeoutIndicator } from './ToolTimeoutIndicator'; import { ToolCardCopyAction, ToolCardHeaderActions } from './ToolCardHeaderActions'; import { ToolCommandPreview } from './ToolCommandPreview'; -import { hasAcpPermissionOptions } from './AcpPermissionActions.utils'; -import { AcpPermissionActions } from './AcpPermissionActions'; import { formatSessionViewPreviewText } from '../utils/sessionViewPreview'; import './TerminalToolCard.scss'; @@ -224,8 +222,6 @@ function parseTerminalResult(raw: unknown, durationMs?: number): ParsedTerminalR export const TerminalToolCard: React.FC = ({ toolItem, - onConfirm, - onReject, onExpand, terminalSessionId: propTerminalSessionId, }) => { @@ -385,22 +381,6 @@ export const TerminalToolCard: React.FC = ({ ]); const waitingMessage = viewState.waitingMessageKey ? t(viewState.waitingMessageKey) : null; - const handleExecute = useCallback((e: React.MouseEvent) => { - e.stopPropagation(); - - if (!canExecuteCommand) { - return; - } - - applyTerminalExpandedState(true, { reason: 'manual' }); - onConfirm?.(toolCall?.input); - }, [applyTerminalExpandedState, canExecuteCommand, onConfirm, toolCall?.input]); - - const handleReject = useCallback((e: React.MouseEvent) => { - e.stopPropagation(); - onReject?.(); - }, [onReject]); - const handleInterrupt = useCallback(async (e: React.MouseEvent) => { e.stopPropagation(); @@ -447,7 +427,7 @@ export const TerminalToolCard: React.FC = ({ const handleCardClick = useCallback((e: React.MouseEvent) => { const target = e.target as HTMLElement; - if (target.closest('.tool-card-header-actions, .terminal-action-btn, .terminal-confirm-actions')) { + if (target.closest('.tool-card-header-actions, .terminal-action-btn')) { return; } if (shouldExpand) { @@ -531,60 +511,18 @@ export const TerminalToolCard: React.FC = ({ const renderHeaderExtra = (includeInterrupt: boolean) => ( - {/* Always visible: confirmation actions + interrupt */} - {(showConfirmButtons || (includeInterrupt && viewState.showInterruptButton)) && ( + {/* Always visible while running: interrupt */} + {includeInterrupt && viewState.showInterruptButton && ( - {showConfirmButtons && ( - e.stopPropagation()}> - {hasAcpPermissionOptions(toolItem) ? ( - - ) : ( - <> - - - - - - - - )} - - )} - {includeInterrupt && viewState.showInterruptButton && ( - - - - )} + + + )} diff --git a/src/web-ui/src/flow_chat/tool-cards/ToolCardStatusSlot.tsx b/src/web-ui/src/flow_chat/tool-cards/ToolCardStatusSlot.tsx index 1860b0f7b9..2813a9eec6 100644 --- a/src/web-ui/src/flow_chat/tool-cards/ToolCardStatusSlot.tsx +++ b/src/web-ui/src/flow_chat/tool-cards/ToolCardStatusSlot.tsx @@ -42,6 +42,7 @@ function StatusIcon({ status, size }: { status: ToolCardStatusSlotStatus; size: case 'error': return ; case 'cancelled': + case 'rejected': return ; case 'queued': case 'waiting': diff --git a/src/web-ui/src/flow_chat/tool-cards/ToolCommandPreview.scss b/src/web-ui/src/flow_chat/tool-cards/ToolCommandPreview.scss index 5e664873be..d70262f545 100644 --- a/src/web-ui/src/flow_chat/tool-cards/ToolCommandPreview.scss +++ b/src/web-ui/src/flow_chat/tool-cards/ToolCommandPreview.scss @@ -15,9 +15,7 @@ } .tool-command-preview__empty { - --tool-command-preview-empty-rgb: 224, 108, 117; - - color: rgb(var(--tool-command-preview-empty-rgb)); + color: rgb(var(--tool-command-empty-rgb)); font-style: italic; } diff --git a/src/web-ui/src/flow_chat/tool-cards/ToolTimeoutIndicator.scss b/src/web-ui/src/flow_chat/tool-cards/ToolTimeoutIndicator.scss index 8a92bc6e21..b719da4797 100644 --- a/src/web-ui/src/flow_chat/tool-cards/ToolTimeoutIndicator.scss +++ b/src/web-ui/src/flow_chat/tool-cards/ToolTimeoutIndicator.scss @@ -8,7 +8,7 @@ display: inline-flex; align-items: center; gap: var(--flowchat-inline-gap); - color: var(--tool-card-text-secondary); + color: var(--color-text-secondary); font-size: var(--flowchat-font-size-xs); font-weight: 600; letter-spacing: 0.01em; diff --git a/src/web-ui/src/flow_chat/tool-cards/_tool-card-common.scss b/src/web-ui/src/flow_chat/tool-cards/_tool-card-common.scss index 1f8153ff14..59389d693e 100644 --- a/src/web-ui/src/flow_chat/tool-cards/_tool-card-common.scss +++ b/src/web-ui/src/flow_chat/tool-cards/_tool-card-common.scss @@ -197,7 +197,7 @@ width: 16px !important; height: 16px !important; border-width: 2px !important; - box-shadow: 0 0 8px var(--color-primary-alpha) !important; + box-shadow: 0 0 8px var(--color-accent-100) !important; } .icon-completed { diff --git a/src/web-ui/src/flow_chat/tool-cards/index.ts b/src/web-ui/src/flow_chat/tool-cards/index.ts index 20173bf053..0d6c2910b4 100644 --- a/src/web-ui/src/flow_chat/tool-cards/index.ts +++ b/src/web-ui/src/flow_chat/tool-cards/index.ts @@ -50,6 +50,7 @@ import { GenerativeWidgetToolCard } from './GenerativeWidgetToolCard'; import { ReviewSessionSummaryCard } from './ReviewSessionSummaryCard'; import { SessionControlToolCard } from './SessionControlToolCard'; import { SessionMessageToolCard } from './SessionMessageToolCard'; +import { ComputerUseToolCard } from './ComputerUseToolCard'; // Tool card component map - uses backend tool names export const TOOL_CARD_COMPONENTS = { @@ -115,6 +116,9 @@ export const TOOL_CARD_COMPONENTS = { // Generative widget tool 'GenerativeUI': GenerativeWidgetToolCard, + + // Computer use (desktop automation) + 'ComputerUse': ComputerUseToolCard, }; /** diff --git a/src/web-ui/src/flow_chat/tool-cards/toolCardMetadata.ts b/src/web-ui/src/flow_chat/tool-cards/toolCardMetadata.ts index 2ab437f36f..8aab6a7418 100644 --- a/src/web-ui/src/flow_chat/tool-cards/toolCardMetadata.ts +++ b/src/web-ui/src/flow_chat/tool-cards/toolCardMetadata.ts @@ -30,7 +30,7 @@ export const TOOL_CARD_CONFIGS: Record = { resultDisplayType: 'summary', description: 'Write or create a file', displayMode: 'standard', - primaryColor: 'var(--tool-card-write-color)' + primaryColor: 'var(--color-success)' }, 'Edit': { toolName: 'Edit', @@ -330,6 +330,18 @@ export const TOOL_CARD_CONFIGS: Record = { displayMode: 'detailed', primaryColor: UI_EXCEPTION_ACCENTS.generativeUi }, + + // Computer use (desktop automation) + 'ComputerUse': { + toolName: 'ComputerUse', + displayName: 'Computer Use', + icon: 'CU', + requiresConfirmation: false, + resultDisplayType: 'summary', + description: 'Screen capture, mouse/keyboard, and accessibility control of the desktop', + displayMode: 'compact', + primaryColor: 'var(--color-accent-600)' + }, }; /** diff --git a/src/web-ui/src/flow_chat/types/flow-chat.ts b/src/web-ui/src/flow_chat/types/flow-chat.ts index a2d3e18cc6..43fb12408b 100644 --- a/src/web-ui/src/flow_chat/types/flow-chat.ts +++ b/src/web-ui/src/flow_chat/types/flow-chat.ts @@ -15,7 +15,7 @@ export interface FlowItem { id: string; type: 'text' | 'tool' | 'image-analysis' | 'thinking' | 'user-steering'; timestamp: number; - status: 'pending' | 'queued' | 'waiting' | 'preparing' | 'running' | 'streaming' | 'receiving' | 'completed' | 'cancelled' | 'error' | 'analyzing' | 'pending_confirmation' | 'confirmed'; // Includes error, analyzing, and confirmation states. + status: 'pending' | 'queued' | 'waiting' | 'preparing' | 'running' | 'streaming' | 'receiving' | 'completed' | 'cancelled' | 'rejected' | 'error' | 'analyzing' | 'pending_confirmation' | 'confirmed'; // Includes error, analyzing, and confirmation states. attemptId?: string; attemptIndex?: number; @@ -87,6 +87,7 @@ export interface FlowToolItem extends FlowItem { }; aiIntent?: string; // AI rationale for calling the tool. startTime?: number; // Tool start time. + confirmationTimeoutAt?: number; endTime?: number; // Tool end time. durationMs?: number; queueWaitMs?: number; @@ -104,6 +105,11 @@ export interface FlowToolItem extends FlowItem { _paramsBuffer?: string; // Internal buffer for accumulated params. } +export interface ToolRejectOptions { + permissionOptionId?: string; + instruction?: string; +} + export interface FlowImageAnalysisItem extends FlowItem { type: 'image-analysis'; imageContext: import('@/shared/types/context').ImageContext; @@ -165,7 +171,7 @@ export interface ModelRound { historyRounds?: ModelRound[]; isStreaming: boolean; isComplete: boolean; - status: 'pending' | 'streaming' | 'completed' | 'cancelled' | 'error' | 'pending_confirmation'; + status: 'pending' | 'streaming' | 'completed' | 'cancelled' | 'rejected' | 'error' | 'pending_confirmation'; startTime: number; endTime?: number; durationMs?: number; @@ -535,8 +541,6 @@ export interface ToolCardProps { toolItem: FlowToolItem; config: ToolCardConfig; interruptionNote?: string | null; - onConfirm?: (updatedInput?: any, permissionOptionId?: string, approve?: boolean) => void; // toolId is known within the card. - onReject?: (permissionOptionId?: string) => void; onOpenInEditor?: (filePath: string) => void; onOpenInPanel?: (panelType: string, data: any) => void; onExpand?: () => void; diff --git a/src/web-ui/src/flow_chat/utils/captureElementToDownloadsPng.tsx b/src/web-ui/src/flow_chat/utils/captureElementToDownloadsPng.tsx index 5029a38537..85b5b27ac4 100644 --- a/src/web-ui/src/flow_chat/utils/captureElementToDownloadsPng.tsx +++ b/src/web-ui/src/flow_chat/utils/captureElementToDownloadsPng.tsx @@ -19,7 +19,7 @@ export async function captureElementToDownloadsPng( fileNamePrefix: string, ): Promise { const computedStyle = getComputedStyle(document.documentElement); - const bgColor = computedStyle.getPropertyValue('--color-bg-flowchat').trim() || FLOWCHAT_CAPTURE_FALLBACK_COLOR.background; + const bgColor = computedStyle.getPropertyValue('--color-bg-scene').trim() || FLOWCHAT_CAPTURE_FALLBACK_COLOR.background; await new Promise((resolve) => setTimeout(resolve, 0)); diff --git a/src/web-ui/src/flow_chat/utils/chatInputMode.test.ts b/src/web-ui/src/flow_chat/utils/chatInputMode.test.ts index 0dc80e6189..6247757baa 100644 --- a/src/web-ui/src/flow_chat/utils/chatInputMode.test.ts +++ b/src/web-ui/src/flow_chat/utils/chatInputMode.test.ts @@ -3,8 +3,27 @@ import { describe, expect, it } from 'vitest'; import { normalizeUserDefaultChatInputModeId, resolveAvailableChatInputMode, + resolveChatInputModePolicy, + resolveSessionAssistantWorkspace, + resolveSwitchableChatInputModes, resolveWorkspaceChatInputMode, } from './chatInputMode'; +import { WorkspaceKind, type WorkspaceInfo, WorkspaceType } from '@/shared/types'; + +function createWorkspace(overrides: Partial): WorkspaceInfo { + return { + id: overrides.id ?? 'workspace-1', + name: overrides.name ?? 'Workspace', + rootPath: overrides.rootPath ?? 'D:/workspace/project', + workspaceType: overrides.workspaceType ?? WorkspaceType.SingleProject, + workspaceKind: overrides.workspaceKind ?? WorkspaceKind.Normal, + languages: overrides.languages ?? [], + openedAt: overrides.openedAt ?? new Date(0).toISOString(), + lastAccessed: overrides.lastAccessed ?? new Date(0).toISOString(), + tags: overrides.tags ?? [], + ...overrides, + }; +} describe('normalizeUserDefaultChatInputModeId', () => { it('normalizes non-empty strings and rejects blank values', () => { @@ -74,6 +93,178 @@ describe('resolveWorkspaceChatInputMode', () => { }) ).toBe('agentic'); }); + + it('keeps Claw sessions synchronized even before workspace state identifies the assistant workspace', () => { + expect( + resolveWorkspaceChatInputMode({ + currentMode: 'agentic', + isAssistantWorkspace: false, + sessionMode: 'Claw', + }) + ).toBe('Claw'); + }); +}); + +describe('resolveChatInputModePolicy', () => { + it('allows mode switching for normal code sessions', () => { + expect( + resolveChatInputModePolicy({ + currentMode: 'agentic', + isAssistantWorkspace: false, + sessionMode: 'agentic', + }), + ).toEqual({ + canSwitchModes: true, + fixedModeId: null, + fixedReason: null, + }); + }); + + it('fixes assistant workspaces to Claw', () => { + expect( + resolveChatInputModePolicy({ + currentMode: 'agentic', + isAssistantWorkspace: true, + sessionMode: 'agentic', + }), + ).toEqual({ + canSwitchModes: false, + fixedModeId: 'Claw', + fixedReason: 'assistant-workspace', + }); + }); + + it('fixes Claw sessions even when workspace resolution is temporarily stale', () => { + expect( + resolveChatInputModePolicy({ + currentMode: 'agentic', + isAssistantWorkspace: false, + sessionMode: 'claw', + }), + ).toEqual({ + canSwitchModes: false, + fixedModeId: 'Claw', + fixedReason: 'session-mode', + }); + }); + + it('fixes Cowork sessions from current or session mode', () => { + expect( + resolveChatInputModePolicy({ + currentMode: 'Cowork', + isAssistantWorkspace: false, + sessionMode: 'agentic', + }), + ).toMatchObject({ + canSwitchModes: false, + fixedModeId: 'Cowork', + fixedReason: 'current-mode', + }); + + expect( + resolveChatInputModePolicy({ + currentMode: 'agentic', + isAssistantWorkspace: false, + sessionMode: 'cowork', + }), + ).toMatchObject({ + canSwitchModes: false, + fixedModeId: 'Cowork', + fixedReason: 'session-mode', + }); + }); + + it('fixes ACP sessions without treating them as a product mode', () => { + expect( + resolveChatInputModePolicy({ + currentMode: 'agentic', + isAssistantWorkspace: false, + sessionMode: 'acp:example', + isAcpTargetSession: true, + }), + ).toEqual({ + canSwitchModes: false, + fixedModeId: null, + fixedReason: 'acp-session', + }); + }); +}); + +describe('resolveSwitchableChatInputModes', () => { + it('removes fixed collaboration modes from boost selection', () => { + expect( + resolveSwitchableChatInputModes([ + { id: 'agentic' }, + { id: 'Cowork' }, + { id: 'Claw' }, + { id: 'PlannerPlus' }, + ]), + ).toEqual([ + { id: 'agentic' }, + { id: 'PlannerPlus' }, + ]); + }); +}); + +describe('resolveSessionAssistantWorkspace', () => { + it('does not treat a project session as assistant during workspace scene transitions', () => { + const projectWorkspace = createWorkspace({ + id: 'project-1', + rootPath: 'E:/Projects/repos/claude-code', + workspaceKind: WorkspaceKind.Normal, + }); + const assistantWorkspace = createWorkspace({ + id: 'assistant-1', + rootPath: 'C:/Users/wsp/.bitfun/personal_assistant/workspace', + workspaceKind: WorkspaceKind.Assistant, + }); + + expect( + resolveSessionAssistantWorkspace({ + currentWorkspace: assistantWorkspace, + sessionWorkspaceId: projectWorkspace.id, + sessionWorkspacePath: projectWorkspace.rootPath, + openedWorkspaces: [projectWorkspace, assistantWorkspace], + }), + ).toBe(false); + }); + + it('recognizes assistant sessions from their own workspace scope even before current workspace catches up', () => { + const projectWorkspace = createWorkspace({ + id: 'project-1', + rootPath: 'E:/Projects/repos/claude-code', + workspaceKind: WorkspaceKind.Normal, + }); + const assistantWorkspace = createWorkspace({ + id: 'assistant-1', + rootPath: 'C:/Users/wsp/.bitfun/personal_assistant/workspace', + workspaceKind: WorkspaceKind.Assistant, + }); + + expect( + resolveSessionAssistantWorkspace({ + currentWorkspace: projectWorkspace, + sessionWorkspaceId: assistantWorkspace.id, + sessionWorkspacePath: assistantWorkspace.rootPath, + openedWorkspaces: [projectWorkspace, assistantWorkspace], + }), + ).toBe(true); + }); + + it('falls back to the current workspace kind when the session has no explicit workspace scope yet', () => { + const assistantWorkspace = createWorkspace({ + id: 'assistant-1', + rootPath: 'C:/Users/wsp/.bitfun/personal_assistant/workspace', + workspaceKind: WorkspaceKind.Assistant, + }); + + expect( + resolveSessionAssistantWorkspace({ + currentWorkspace: assistantWorkspace, + openedWorkspaces: [assistantWorkspace], + }), + ).toBe(true); + }); }); describe('resolveAvailableChatInputMode', () => { @@ -121,6 +312,17 @@ describe('resolveAvailableChatInputMode', () => { ).toBe('Claw'); }); + it('keeps Claw sessions pinned even before assistant workspace resolution catches up', () => { + expect( + resolveAvailableChatInputMode({ + currentMode: 'agentic', + isAssistantWorkspace: false, + sessionMode: 'Claw', + availableModeIds: ['agentic', 'Claw', 'PlannerPlus'], + }), + ).toBe('Claw'); + }); + it('falls back to the first available mode when agentic is unavailable', () => { expect( resolveAvailableChatInputMode({ diff --git a/src/web-ui/src/flow_chat/utils/chatInputMode.ts b/src/web-ui/src/flow_chat/utils/chatInputMode.ts index 8891fb3a18..e5cd476b07 100644 --- a/src/web-ui/src/flow_chat/utils/chatInputMode.ts +++ b/src/web-ui/src/flow_chat/utils/chatInputMode.ts @@ -1,5 +1,127 @@ +import { WorkspaceKind, type WorkspaceInfo } from '@/shared/types'; + export const DEFAULT_CHAT_INPUT_MODE_CONFIG_PATH = 'app.flow_chat.default_mode_id'; +const FIXED_CHAT_INPUT_MODE_IDS = new Set(['cowork', 'claw']); + +type WorkspaceResolutionInfo = Pick< + WorkspaceInfo, + 'id' | 'rootPath' | 'workspaceKind' | 'connectionId' +>; + +export type ChatInputFixedModeReason = + | 'assistant-workspace' + | 'acp-session' + | 'current-mode' + | 'session-mode'; + +export interface ChatInputModePolicy { + canSwitchModes: boolean; + fixedModeId: string | null; + fixedReason: ChatInputFixedModeReason | null; +} + +function normalizeOptionalString(value: string | null | undefined): string | null { + if (typeof value !== 'string') { + return null; + } + + const trimmed = value.trim(); + return trimmed ? trimmed : null; +} + +function normalizeWorkspacePath(value: string | null | undefined): string | null { + const trimmed = normalizeOptionalString(value); + if (!trimmed) { + return null; + } + + return trimmed.replace(/[\\/]+$/, ''); +} + +function isWorkspaceConnectionCompatible( + workspaceConnectionId: string | null | undefined, + sessionRemoteConnectionId: string | null | undefined, +): boolean { + const normalizedWorkspaceConnectionId = normalizeOptionalString(workspaceConnectionId); + const normalizedSessionRemoteConnectionId = normalizeOptionalString(sessionRemoteConnectionId); + + if (normalizedSessionRemoteConnectionId && normalizedWorkspaceConnectionId) { + return normalizedWorkspaceConnectionId === normalizedSessionRemoteConnectionId; + } + + if (normalizedSessionRemoteConnectionId && !normalizedWorkspaceConnectionId) { + return false; + } + + return true; +} + +function resolveSessionWorkspaceMatch(params: { + currentWorkspace?: WorkspaceResolutionInfo | null; + sessionWorkspaceId?: string | null; + sessionWorkspacePath?: string | null; + sessionRemoteConnectionId?: string | null; + openedWorkspaces?: Iterable; +}): WorkspaceResolutionInfo | null { + const normalizedSessionWorkspaceId = normalizeOptionalString(params.sessionWorkspaceId); + const normalizedSessionWorkspacePath = normalizeWorkspacePath(params.sessionWorkspacePath); + const normalizedSessionRemoteConnectionId = normalizeOptionalString(params.sessionRemoteConnectionId); + const currentWorkspace = params.currentWorkspace ?? null; + const openedWorkspaces = params.openedWorkspaces ?? []; + + if (normalizedSessionWorkspaceId) { + if (currentWorkspace?.id === normalizedSessionWorkspaceId) { + return currentWorkspace; + } + + for (const workspace of openedWorkspaces) { + if (workspace.id === normalizedSessionWorkspaceId) { + return workspace; + } + } + } + + if (!normalizedSessionWorkspacePath) { + return null; + } + + const matchingWorkspaces: WorkspaceResolutionInfo[] = []; + const pushIfMatching = (workspace: WorkspaceResolutionInfo | null | undefined) => { + if (!workspace) { + return; + } + + if (normalizeWorkspacePath(workspace.rootPath) !== normalizedSessionWorkspacePath) { + return; + } + + if (!isWorkspaceConnectionCompatible(workspace.connectionId, normalizedSessionRemoteConnectionId)) { + return; + } + + if (!matchingWorkspaces.some(candidate => candidate.id === workspace.id)) { + matchingWorkspaces.push(workspace); + } + }; + + pushIfMatching(currentWorkspace); + for (const workspace of openedWorkspaces) { + pushIfMatching(workspace); + } + + if (normalizedSessionRemoteConnectionId) { + const exactConnectionMatch = matchingWorkspaces.find( + (workspace) => normalizeOptionalString(workspace.connectionId) === normalizedSessionRemoteConnectionId, + ); + if (exactConnectionMatch) { + return exactConnectionMatch; + } + } + + return matchingWorkspaces[0] ?? null; +} + export function normalizeUserDefaultChatInputModeId(value: unknown): string | null { if (typeof value !== 'string') { return null; @@ -9,6 +131,98 @@ export function normalizeUserDefaultChatInputModeId(value: unknown): string | nu return trimmed ? trimmed : null; } +export function resolveSessionAssistantWorkspace(params: { + currentWorkspace?: WorkspaceResolutionInfo | null; + sessionWorkspaceId?: string | null; + sessionWorkspacePath?: string | null; + sessionRemoteConnectionId?: string | null; + openedWorkspaces?: Iterable; +}): boolean { + const matchedWorkspace = resolveSessionWorkspaceMatch(params); + if (matchedWorkspace) { + return matchedWorkspace.workspaceKind === WorkspaceKind.Assistant; + } + + const hasExplicitSessionWorkspace = + normalizeOptionalString(params.sessionWorkspaceId) !== null + || normalizeWorkspacePath(params.sessionWorkspacePath) !== null; + if (hasExplicitSessionWorkspace) { + return false; + } + + return params.currentWorkspace?.workspaceKind === WorkspaceKind.Assistant; +} + +function normalizeModeLookupId(value: string | null | undefined): string | null { + return normalizeOptionalString(value)?.toLowerCase() ?? null; +} + +function canonicalFixedModeId(value: string | null | undefined): string | null { + switch (normalizeModeLookupId(value)) { + case 'cowork': + return 'Cowork'; + case 'claw': + return 'Claw'; + default: + return null; + } +} + +export function resolveChatInputModePolicy(params: { + currentMode: string; + isAssistantWorkspace: boolean; + sessionMode?: string | null; + isAcpTargetSession?: boolean; +}): ChatInputModePolicy { + if (params.isAcpTargetSession) { + return { + canSwitchModes: false, + fixedModeId: null, + fixedReason: 'acp-session', + }; + } + + if (params.isAssistantWorkspace) { + return { + canSwitchModes: false, + fixedModeId: 'Claw', + fixedReason: 'assistant-workspace', + }; + } + + const fixedSessionModeId = canonicalFixedModeId(params.sessionMode); + if (fixedSessionModeId) { + return { + canSwitchModes: false, + fixedModeId: fixedSessionModeId, + fixedReason: 'session-mode', + }; + } + + const fixedCurrentModeId = canonicalFixedModeId(params.currentMode); + if (fixedCurrentModeId) { + return { + canSwitchModes: false, + fixedModeId: fixedCurrentModeId, + fixedReason: 'current-mode', + }; + } + + return { + canSwitchModes: true, + fixedModeId: null, + fixedReason: null, + }; +} + +export function resolveSwitchableChatInputModes( + availableModes: Iterable, +): TMode[] { + return Array.from(availableModes).filter( + mode => !FIXED_CHAT_INPUT_MODE_IDS.has(normalizeModeLookupId(mode.id) ?? ''), + ); +} + export function resolveWorkspaceChatInputMode(params: { currentMode: string; isAssistantWorkspace: boolean; @@ -21,7 +235,7 @@ export function resolveWorkspaceChatInputMode(params: { } if (normalizedSessionMode?.toLowerCase() === 'claw') { - return null; + return params.currentMode === 'Claw' ? null : 'Claw'; } if (normalizedSessionMode && normalizedSessionMode !== params.currentMode) { diff --git a/src/web-ui/src/flow_chat/utils/dialogTurnStability.test.ts b/src/web-ui/src/flow_chat/utils/dialogTurnStability.test.ts index 7bc80e1683..79e950f388 100644 --- a/src/web-ui/src/flow_chat/utils/dialogTurnStability.test.ts +++ b/src/web-ui/src/flow_chat/utils/dialogTurnStability.test.ts @@ -1,6 +1,7 @@ import { describe, expect, it } from 'vitest'; import type { DialogTurn } from '../types/flow-chat'; import { + normalizeRecoveredRoundStatus, normalizeRecoveredToolStatus, normalizeRecoveredTurnStatus, settleInterruptedDialogTurn, @@ -61,15 +62,10 @@ describe('dialogTurnStability', () => { expect(normalizeRecoveredTurnStatus('processing', { error: null })).toBe('cancelled'); }); - it('keeps pending confirmation tools actionable after recovery', () => { - expect( - normalizeRecoveredToolStatus( - 'pending_confirmation', - 'cancelled', - null, - { preservePendingConfirmation: true }, - ), - ).toBe('pending_confirmation'); + it('cancels recovered confirmation states because their runtime approval channel is gone', () => { + expect(normalizeRecoveredToolStatus('pending_confirmation', 'cancelled', null)).toBe('cancelled'); + expect(normalizeRecoveredToolStatus('confirmed', 'cancelled', null)).toBe('cancelled'); + expect(normalizeRecoveredRoundStatus('pending_confirmation', 'cancelled')).toBe('cancelled'); }); it('cancels pending confirmation tools during an explicit cancellation settle', () => { @@ -101,9 +97,48 @@ describe('dialogTurnStability', () => { }); const settled = settleInterruptedDialogTurn(turn, 42); + expect(settled.modelRounds[0].status).toBe('cancelled'); expect(settled.modelRounds[0].items[0].status).toBe('cancelled'); }); + it('cancels stale pending confirmation tools even when the persisted turn is already cancelled', () => { + const turn = createDialogTurn({ + status: 'cancelled', + endTime: 40, + modelRounds: [ + { + id: 'round-1', + index: 0, + items: [ + { + id: 'tool-1', + type: 'tool', + toolName: 'Terminal', + toolCall: { + input: { command: 'echo ok' }, + id: 'tool-1', + }, + timestamp: 2, + status: 'pending_confirmation', + startTime: 2, + }, + ], + isStreaming: false, + isComplete: false, + status: 'pending_confirmation', + startTime: 2, + }, + ], + }); + + const settled = settleInterruptedDialogTurn(turn, 42); + const tool = settled.modelRounds[0].items[0]; + + expect(settled.status).toBe('cancelled'); + expect(settled.modelRounds[0].status).toBe('cancelled'); + expect(tool.status).toBe('cancelled'); + }); + it('cancels transient nested states when settling an interrupted turn', () => { const settledAt = 99; const settled = settleInterruptedDialogTurn(createDialogTurn(), settledAt, { diff --git a/src/web-ui/src/flow_chat/utils/dialogTurnStability.ts b/src/web-ui/src/flow_chat/utils/dialogTurnStability.ts index db68210e83..f55a6f355e 100644 --- a/src/web-ui/src/flow_chat/utils/dialogTurnStability.ts +++ b/src/web-ui/src/flow_chat/utils/dialogTurnStability.ts @@ -8,15 +8,24 @@ import type { } from '../types/flow-chat'; const TRANSIENT_TURN_STATUSES = new Set(['pending', 'processing', 'finishing', 'image_analyzing', 'cancelling', 'inprogress']); -const TRANSIENT_ROUND_STATUSES = new Set(['pending', 'streaming']); -const TERMINAL_ROUND_STATUSES = new Set(['completed', 'cancelled', 'error', 'pending_confirmation']); -const TRANSIENT_TOOL_STATUSES = new Set(['pending', 'preparing', 'streaming', 'running', 'receiving', 'starting', 'analyzing']); -const TERMINAL_TOOL_STATUSES = new Set(['completed', 'cancelled', 'error', 'pending_confirmation', 'confirmed']); -const TERMINAL_ITEM_STATUSES = new Set(['completed', 'cancelled', 'error']); -const STABLE_ITEM_STATUSES = new Set(['completed', 'cancelled', 'error', 'pending_confirmation', 'confirmed']); +const TRANSIENT_ROUND_STATUSES = new Set(['pending', 'streaming', 'pending_confirmation']); +const TERMINAL_ROUND_STATUSES = new Set(['completed', 'cancelled', 'rejected', 'error']); +const TRANSIENT_TOOL_STATUSES = new Set([ + 'pending', + 'preparing', + 'streaming', + 'running', + 'receiving', + 'starting', + 'analyzing', + 'pending_confirmation', + 'confirmed', +]); +const TERMINAL_TOOL_STATUSES = new Set(['completed', 'cancelled', 'rejected', 'error']); +const TERMINAL_ITEM_STATUSES = new Set(['completed', 'cancelled', 'rejected', 'error']); +const STABLE_ITEM_STATUSES = new Set(['completed', 'cancelled', 'rejected', 'error']); type SettleInterruptedDialogTurnOptions = { - preservePendingConfirmation?: boolean; interruptionReason?: FlowToolItem['interruptionReason']; }; @@ -51,11 +60,7 @@ export function normalizeRecoveredRoundStatus( status: unknown, parentTurnStatus: DialogTurn['status'], ): ModelRound['status'] { - if (status === 'pending_confirmation') { - return status; - } - - if (status === 'completed' || status === 'cancelled' || status === 'error') { + if (status === 'completed' || status === 'cancelled' || status === 'rejected' || status === 'error') { return status; } @@ -72,7 +77,7 @@ export function normalizeRecoveredTextStatus( status: unknown, parentTurnStatus: DialogTurn['status'], ): FlowTextItem['status'] { - if (status === 'completed' || status === 'cancelled' || status === 'error') { + if (status === 'completed' || status === 'cancelled' || status === 'rejected' || status === 'error') { return status; } @@ -91,7 +96,7 @@ export function normalizeRecoveredThinkingStatus( status: unknown, parentTurnStatus: DialogTurn['status'], ): FlowThinkingItem['status'] { - if (status === 'completed' || status === 'cancelled' || status === 'error') { + if (status === 'completed' || status === 'cancelled' || status === 'rejected' || status === 'error') { return status; } @@ -110,13 +115,8 @@ export function normalizeRecoveredToolStatus( status: unknown, parentTurnStatus: DialogTurn['status'], toolResult?: Pick, 'success' | 'error'> | null, - options?: { preservePendingConfirmation?: boolean }, ): FlowToolItem['status'] { - if ((status === 'pending_confirmation' || status === 'confirmed') && options?.preservePendingConfirmation) { - return status; - } - - if (status === 'completed' || status === 'cancelled' || status === 'error') { + if (status === 'completed' || status === 'cancelled' || status === 'rejected' || status === 'error') { return status; } @@ -181,7 +181,6 @@ function settleInterruptedItem( item.status, finalTurnStatus, item.toolResult, - options, ); return { ...item, diff --git a/src/web-ui/src/flow_chat/utils/flowChatTurnScrollPolicy.test.ts b/src/web-ui/src/flow_chat/utils/flowChatTurnScrollPolicy.test.ts index b40017a28c..6777fd6239 100644 --- a/src/web-ui/src/flow_chat/utils/flowChatTurnScrollPolicy.test.ts +++ b/src/web-ui/src/flow_chat/utils/flowChatTurnScrollPolicy.test.ts @@ -3,6 +3,7 @@ import type { DialogTurn } from '../types/flow-chat'; import { isDialogTurnInFlight, isThreadGoalContinuationTurn, + shouldUseLatestTurnFollowOutput, shouldUseStickyLatestPin, } from './flowChatTurnScrollPolicy'; @@ -39,11 +40,29 @@ describe('flowChatTurnScrollPolicy', () => { }, }); expect(isThreadGoalContinuationTurn(turn)).toBe(true); + expect(shouldUseLatestTurnFollowOutput(turn)).toBe(false); expect(shouldUseStickyLatestPin(turn)).toBe(false); }); it('allows sticky pin for in-flight user turns', () => { - const turn = makeTurn({ status: 'processing' }); + const turn = makeTurn({ + status: 'processing', + modelRounds: [{ + id: 'round-1', + turnId: 'turn-1', + items: [], + isStreaming: true, + createdAt: 1, + }], + } as Partial); + expect(shouldUseLatestTurnFollowOutput(turn)).toBe(true); expect(shouldUseStickyLatestPin(turn)).toBe(true); }); + + it('lets follow-output own a newly submitted user-only turn before sticky pin starts', () => { + const turn = makeTurn({ status: 'processing', modelRounds: [] }); + expect(isDialogTurnInFlight(turn)).toBe(true); + expect(shouldUseLatestTurnFollowOutput(turn)).toBe(true); + expect(shouldUseStickyLatestPin(turn)).toBe(false); + }); }); diff --git a/src/web-ui/src/flow_chat/utils/flowChatTurnScrollPolicy.ts b/src/web-ui/src/flow_chat/utils/flowChatTurnScrollPolicy.ts index 783488b3a8..1d1ceafd00 100644 --- a/src/web-ui/src/flow_chat/utils/flowChatTurnScrollPolicy.ts +++ b/src/web-ui/src/flow_chat/utils/flowChatTurnScrollPolicy.ts @@ -20,14 +20,22 @@ export function isThreadGoalContinuationTurn(turn: DialogTurn | undefined): bool return Boolean(turn?.userMessage?.metadata?.threadGoalContinuation); } -/** Sticky-latest pin is for active user turns; auto goal checks stay at the natural tail. */ -export function shouldUseStickyLatestPin(turn: DialogTurn | undefined): boolean { +/** Follow-output owns active user turns; auto goal checks stay at the natural tail. */ +export function shouldUseLatestTurnFollowOutput(turn: DialogTurn | undefined): boolean { if (!turn || isThreadGoalContinuationTurn(turn)) { return false; } return isDialogTurnInFlight(turn); } +/** Sticky-latest pin starts only after model output exists. */ +export function shouldUseStickyLatestPin(turn: DialogTurn | undefined): boolean { + if (!turn || !shouldUseLatestTurnFollowOutput(turn)) { + return false; + } + return turn.modelRounds.length > 0; +} + export function findDialogTurn( dialogTurns: DialogTurn[] | undefined, turnId: string | null | undefined, diff --git a/src/web-ui/src/flow_chat/utils/imagePayload.ts b/src/web-ui/src/flow_chat/utils/imagePayload.ts new file mode 100644 index 0000000000..af272affa1 --- /dev/null +++ b/src/web-ui/src/flow_chat/utils/imagePayload.ts @@ -0,0 +1,75 @@ +import type { ImageContext } from '@/shared/types/context'; +import type { ImageContextData as ImageInputContextData } from '@/infrastructure/api/service-api/ImageContextTypes'; +import { api } from '@/infrastructure/api/service-api/ApiClient'; + +export interface ImageDisplayData { + id: string; + name: string; + dataUrl?: string; + imagePath?: string; + mimeType?: string; +} + +export interface ImagePayload { + imageContexts: ImageInputContextData[]; + imageDisplayData: ImageDisplayData[]; +} + +export async function buildImagePayload(imageContexts: ImageContext[]): Promise { + if (imageContexts.length === 0) { + return undefined; + } + + const clipboardImages = imageContexts.filter(ctx => !ctx.isLocal && ctx.dataUrl); + const uploadedImagePaths = new Map(); + + if (clipboardImages.length > 0) { + const uploadResults = await api.invoke>( + 'upload_image_contexts', + { + request: { + images: clipboardImages.map(ctx => ({ + id: ctx.id, + image_path: ctx.imagePath || null, + data_url: ctx.dataUrl || null, + mime_type: ctx.mimeType, + image_name: ctx.imageName, + file_size: ctx.fileSize, + width: ctx.width || null, + height: ctx.height || null, + source: ctx.source, + })), + }, + } + ); + + for (const result of uploadResults) { + if (result.image_path) { + uploadedImagePaths.set(result.id, result.image_path); + } + } + } + + return { + imageContexts: imageContexts.map(ctx => ({ + id: ctx.id, + image_path: ctx.isLocal ? ctx.imagePath : uploadedImagePaths.get(ctx.id), + data_url: undefined, + mime_type: ctx.mimeType, + metadata: { + name: ctx.imageName, + width: ctx.width, + height: ctx.height, + file_size: ctx.fileSize, + source: ctx.source, + }, + })), + imageDisplayData: imageContexts.map(ctx => ({ + id: ctx.id, + name: ctx.imageName || 'Image', + dataUrl: ctx.dataUrl, + imagePath: ctx.isLocal ? ctx.imagePath : uploadedImagePaths.get(ctx.id), + mimeType: ctx.mimeType, + })), + }; +} diff --git a/src/web-ui/src/flow_chat/utils/sessionOrdering.test.ts b/src/web-ui/src/flow_chat/utils/sessionOrdering.test.ts index 77384b0d28..8c2df084de 100644 --- a/src/web-ui/src/flow_chat/utils/sessionOrdering.test.ts +++ b/src/web-ui/src/flow_chat/utils/sessionOrdering.test.ts @@ -131,4 +131,22 @@ describe('sessionOrdering', () => { ) ).toBe(true); }); + + it('remote SSH: matches persisted metadata that only has workspaceHostname', () => { + const session = { + workspacePath: '/home/u/project-a', + remoteConnectionId: undefined, + remoteSshHost: undefined, + workspaceHostname: 'myserver.example.com', + }; + + expect( + sessionBelongsToWorkspaceNavRow( + session, + '/home/u/project-a', + 'ssh-user@myserver.example.com:22', + undefined + ) + ).toBe(true); + }); }); diff --git a/src/web-ui/src/flow_chat/utils/sessionOrdering.ts b/src/web-ui/src/flow_chat/utils/sessionOrdering.ts index 3646ac9450..e8da82d774 100644 --- a/src/web-ui/src/flow_chat/utils/sessionOrdering.ts +++ b/src/web-ui/src/flow_chat/utils/sessionOrdering.ts @@ -25,7 +25,9 @@ function effectiveWorkspaceSshHost( * We must never treat "same host" as sufficient: two tabs to the same server at `/a` vs `/b` are distinct. */ export function sessionBelongsToWorkspaceNavRow( - session: Pick, + session: Pick & { + workspaceHostname?: string | null; + }, workspacePath: string, remoteConnectionId?: string | null, remoteSshHost?: string | null @@ -38,7 +40,10 @@ export function sessionBelongsToWorkspaceNavRow( const wsConn = remoteConnectionId?.trim() ?? ''; const sessConn = session.remoteConnectionId?.trim() ?? ''; const wsHostEff = effectiveWorkspaceSshHost(remoteSshHost, remoteConnectionId); - const sessHost = session.remoteSshHost?.trim().toLowerCase() ?? ''; + const sessHost = + session.remoteSshHost?.trim().toLowerCase() || + session.workspaceHostname?.trim().toLowerCase() || + ''; const sessConnHost = hostFromSshConnectionId(sessConn); const wsConnHost = hostFromSshConnectionId(wsConn); diff --git a/src/web-ui/src/flow_chat/utils/slashCommand.test.ts b/src/web-ui/src/flow_chat/utils/slashCommand.test.ts index 59a64dd324..adca898be2 100644 --- a/src/web-ui/src/flow_chat/utils/slashCommand.test.ts +++ b/src/web-ui/src/flow_chat/utils/slashCommand.test.ts @@ -1,6 +1,8 @@ import { describe, expect, it } from 'vitest'; import { + getSlashCommandPickerQuery, + isSlashCommandPickerQuery, isSlashCommand, matchesSlashCommand, stripSlashCommand, @@ -48,6 +50,34 @@ describe('isSlashCommand', () => { }); }); +describe('isSlashCommandPickerQuery', () => { + it('allows single-token command picker queries', () => { + expect(isSlashCommandPickerQuery('')).toBe(true); + expect(isSlashCommandPickerQuery('goal')).toBe(true); + expect(isSlashCommandPickerQuery('mcp:foo-bar')).toBe(true); + }); + + it('rejects path-like queries with another slash', () => { + expect(isSlashCommandPickerQuery('users/alice')).toBe(false); + expect(isSlashCommandPickerQuery('foo/bar/baz')).toBe(false); + }); +}); + +describe('getSlashCommandPickerQuery', () => { + it('returns a lowercase query for active picker text', () => { + expect(getSlashCommandPickerQuery('/')).toBe(''); + expect(getSlashCommandPickerQuery('/Goal')).toBe('goal'); + expect(getSlashCommandPickerQuery('/mcp:foo-bar')).toBe('mcp:foo-bar'); + }); + + it('closes the picker once the slash token is no longer active', () => { + expect(getSlashCommandPickerQuery('/goal ')).toBeNull(); + expect(getSlashCommandPickerQuery('/goal focus')).toBeNull(); + expect(getSlashCommandPickerQuery('/users/alice')).toBeNull(); + expect(getSlashCommandPickerQuery('hello /goal')).toBeNull(); + }); +}); + describe('stripSlashCommand', () => { it('removes the command token and immediate whitespace', () => { expect(stripSlashCommand('/btw question?', '/btw')).toBe('question?'); diff --git a/src/web-ui/src/flow_chat/utils/slashCommand.ts b/src/web-ui/src/flow_chat/utils/slashCommand.ts index e485c03ed2..4d90d67371 100644 --- a/src/web-ui/src/flow_chat/utils/slashCommand.ts +++ b/src/web-ui/src/flow_chat/utils/slashCommand.ts @@ -1,5 +1,22 @@ const COMMAND_BOUNDARY_RE = /^(\/[A-Za-z][\w:-]*)(?=\s|$)/; +export function isSlashCommandPickerQuery(query: string): boolean { + return typeof query === 'string' && !query.includes('/'); +} + +export function getSlashCommandPickerQuery(text: string): string | null { + if (typeof text !== 'string' || !text.startsWith('/')) { + return null; + } + + const query = text.slice(1); + if (/\s/.test(query) || !isSlashCommandPickerQuery(query)) { + return null; + } + + return query.toLowerCase(); +} + export function matchesSlashCommand(text: string): string | null { if (typeof text !== 'string' || text.length === 0 || !text.startsWith('/')) { return null; diff --git a/src/web-ui/src/infrastructure/api/adapters/base.ts b/src/web-ui/src/infrastructure/api/adapters/base.ts index a29b65fb35..2af431c7dd 100644 --- a/src/web-ui/src/infrastructure/api/adapters/base.ts +++ b/src/web-ui/src/infrastructure/api/adapters/base.ts @@ -10,6 +10,12 @@ export interface ITransportAdapter { listen(event: string, callback: (data: T) => void): () => void; + + /** + * Resolves after listener registration requests already issued to the + * transport have settled. Synchronous transports can keep the default no-op. + */ + waitForListenerRegistrations?(): Promise; disconnect(): Promise; diff --git a/src/web-ui/src/infrastructure/api/adapters/tauri-adapter.ts b/src/web-ui/src/infrastructure/api/adapters/tauri-adapter.ts index bf559cf971..317b5dd4b9 100644 --- a/src/web-ui/src/infrastructure/api/adapters/tauri-adapter.ts +++ b/src/web-ui/src/infrastructure/api/adapters/tauri-adapter.ts @@ -32,6 +32,7 @@ export class TauriTransportAdapter implements ITransportAdapter { private connected: boolean = false; private invokeFn: ((action: string, params?: any) => Promise) | null = null; private initPromise: Promise | null = null; + private listenerRegistrationPromises = new Set>(); // Lazy initialize Tauri API private async ensureInitialized() { @@ -120,7 +121,7 @@ export class TauriTransportAdapter implements ITransportAdapter { let unlistenFn: UnlistenFn | null = null; let isUnlistened = false; - listen(event, (e) => { + const registration = listen(event, (e) => { if (!isUnlistened) { try { callback(e.payload); @@ -137,7 +138,10 @@ export class TauriTransportAdapter implements ITransportAdapter { } }).catch(error => { log.error('Failed to listen event', { event, error: sanitizeErrorForLog(error) }); + }).finally(() => { + this.listenerRegistrationPromises.delete(registration); }); + this.listenerRegistrationPromises.add(registration); return () => { isUnlistened = true; @@ -166,6 +170,12 @@ export class TauriTransportAdapter implements ITransportAdapter { isConnected(): boolean { return this.connected; } -} - + async waitForListenerRegistrations(): Promise { + while (this.listenerRegistrationPromises.size > 0) { + // Listener registration is async, and settling one registration can reveal + // another registration issued in the same initialization wave. + await Promise.allSettled(Array.from(this.listenerRegistrationPromises)); + } + } +} diff --git a/src/web-ui/src/infrastructure/api/service-api/ApiClient.ts b/src/web-ui/src/infrastructure/api/service-api/ApiClient.ts index 7f38bd04db..9f99aae68c 100644 --- a/src/web-ui/src/infrastructure/api/service-api/ApiClient.ts +++ b/src/web-ui/src/infrastructure/api/service-api/ApiClient.ts @@ -197,6 +197,10 @@ export class ApiClient implements IApiClient { return this.adapter; } + async waitForListenerRegistrations(): Promise { + await this.adapter.waitForListenerRegistrations?.(); + } + private createRequest(type: 'tauri' | 'http', config: TauriCommandConfig | HttpRequestConfig): ApiRequest { return { id: `${type}-${Date.now()}-${Math.random()}`, @@ -576,6 +580,9 @@ export const api = { getStats: (): ApiStats => apiClient.getStats(), + + waitForListenerRegistrations: (): Promise => + apiClient.waitForListenerRegistrations(), getAdapter: (): ITransportAdapter => apiClient.getAdapter() diff --git a/src/web-ui/src/infrastructure/api/service-api/BtwAPI.ts b/src/web-ui/src/infrastructure/api/service-api/BtwAPI.ts index 7b0c1715eb..2d178d0b21 100644 --- a/src/web-ui/src/infrastructure/api/service-api/BtwAPI.ts +++ b/src/web-ui/src/infrastructure/api/service-api/BtwAPI.ts @@ -1,5 +1,6 @@ import { api } from './ApiClient'; import { createTauriCommandError } from '../errors/TauriCommandError'; +import type { ImageContextData as ImageInputContextData } from './ImageContextTypes'; export interface BtwAskStreamRequest { requestId: string; @@ -8,6 +9,7 @@ export interface BtwAskStreamRequest { modelId?: string; childSessionId: string; childSessionName?: string; + imageContexts?: ImageInputContextData[]; } export interface BtwAskStreamResponse { diff --git a/src/web-ui/src/infrastructure/api/service-api/CronAPI.ts b/src/web-ui/src/infrastructure/api/service-api/CronAPI.ts index 3adc8f1de8..ecfb4eb228 100644 --- a/src/web-ui/src/infrastructure/api/service-api/CronAPI.ts +++ b/src/web-ui/src/infrastructure/api/service-api/CronAPI.ts @@ -1,5 +1,6 @@ import { api } from './ApiClient'; import { createTauriCommandError } from '../errors/TauriCommandError'; +import { isTauriRuntime } from '@/infrastructure/runtime'; export type CronJobRunStatus = 'queued' | 'running' | 'ok' | 'error' | 'cancelled'; export type CronJobTargetKind = 'session' | 'workspace'; @@ -102,6 +103,21 @@ export interface UpdateCronJobRequest { } export class CronAPI { + async notifyHostReady(): Promise { + if (!isTauriRuntime()) { + return; + } + + try { + // Tauri listener registration is async. Wait for listeners already issued + // by FlowChat before allowing cron to emit startup events. + await api.waitForListenerRegistrations(); + await api.invoke('notify_cron_host_ready'); + } catch (error) { + throw createTauriCommandError('notify_cron_host_ready', error); + } + } + async listJobs(request: ListCronJobsRequest = {}): Promise { try { return await api.invoke('list_cron_jobs', { request }); diff --git a/src/web-ui/src/infrastructure/api/service-api/RemoteConnectAPI.ts b/src/web-ui/src/infrastructure/api/service-api/RemoteConnectAPI.ts index 5bda6708b5..964cca42ad 100644 --- a/src/web-ui/src/infrastructure/api/service-api/RemoteConnectAPI.ts +++ b/src/web-ui/src/infrastructure/api/service-api/RemoteConnectAPI.ts @@ -40,9 +40,16 @@ export interface RemoteConnectStatus { bot_verbose_mode: boolean; } +export interface LanNetworkInterface { + interface_name: string; + ip: string; + gateway_ip: string | null; +} + export interface LanNetworkInfo { local_ip: string; gateway_ip: string | null; + available_ips: LanNetworkInterface[]; } export interface RemoteConnectFormState { @@ -116,10 +123,10 @@ class RemoteConnectAPIService { } } - async startConnection(method: string, customServerUrl?: string): Promise { + async startConnection(method: string, customServerUrl?: string, lanIp?: string): Promise { try { return await this.adapter.request('remote_connect_start', { - request: { method, custom_server_url: customServerUrl ?? null }, + request: { method, custom_server_url: customServerUrl ?? null, lan_ip: lanIp ?? null }, }); } catch (e) { log.error('startConnection failed', e); diff --git a/src/web-ui/src/infrastructure/api/service-api/ToolAPI.ts b/src/web-ui/src/infrastructure/api/service-api/ToolAPI.ts index 83bbfe3023..5c4f4f7850 100644 --- a/src/web-ui/src/infrastructure/api/service-api/ToolAPI.ts +++ b/src/web-ui/src/infrastructure/api/service-api/ToolAPI.ts @@ -80,7 +80,7 @@ export class ToolAPI { const rejectRequest = { sessionId: request.sessionId, toolId: request.toolId, - reason: 'User rejected' + reason: request.rejectReason || 'User rejected' }; const result = await api.invoke('reject_tool_execution', { request: rejectRequest }); diff --git a/src/web-ui/src/infrastructure/config/components/AIFeaturesConfig.scss b/src/web-ui/src/infrastructure/config/components/AIFeaturesConfig.scss index 58d1ac1faa..c2af710016 100644 --- a/src/web-ui/src/infrastructure/config/components/AIFeaturesConfig.scss +++ b/src/web-ui/src/infrastructure/config/components/AIFeaturesConfig.scss @@ -34,6 +34,26 @@ color: var(--color-text-secondary); } + &__inline-label { + display: inline-flex; + align-items: center; + gap: $size-gap-1; + min-width: 0; + } + + &__inline-info { + display: inline-flex; + align-items: center; + justify-content: center; + color: var(--color-text-muted); + cursor: help; + flex-shrink: 0; + } + + &__inline-info:hover { + color: var(--color-text-secondary); + } + &__policy-tooltip { max-width: min(320px, calc(100vw - 32px)); display: grid; @@ -176,7 +196,7 @@ } &__pet-expand-button:focus-visible { - outline: 2px solid var(--color-primary); + outline: 2px solid var(--color-accent-500); outline-offset: 2px; } @@ -247,12 +267,12 @@ } &__pet-select-option:focus-visible { - box-shadow: 0 0 0 2px var(--color-primary); + box-shadow: 0 0 0 2px var(--color-accent-500); } &__pet-select-option--selected { - border-color: color-mix(in srgb, var(--color-primary) 58%, var(--border-subtle)); - background: color-mix(in srgb, var(--color-primary) 10%, var(--element-bg-base)); + border-color: color-mix(in srgb, var(--color-accent-500) 58%, var(--border-subtle)); + background: color-mix(in srgb, var(--color-accent-500) 10%, var(--element-bg-base)); } &__pet-select-option-main { @@ -278,7 +298,7 @@ &__pet-select-check { flex-shrink: 0; - color: var(--color-primary); + color: var(--color-accent-500); transition: opacity 0.14s ease; } diff --git a/src/web-ui/src/infrastructure/config/components/AIModelConfig.scss b/src/web-ui/src/infrastructure/config/components/AIModelConfig.scss index 0b3f60e57d..3fa46a8471 100644 --- a/src/web-ui/src/infrastructure/config/components/AIModelConfig.scss +++ b/src/web-ui/src/infrastructure/config/components/AIModelConfig.scss @@ -672,7 +672,7 @@ padding: $size-gap-4 !important; margin: 0 $size-gap-3 $size-gap-3; - background: var(--color-overlay-white-02); + background: var(--color-overlay-white-04); border-radius: var(--size-radius-sm); animation: slideDown 0.2s ease; } diff --git a/src/web-ui/src/infrastructure/config/components/AIModelConfig.tsx b/src/web-ui/src/infrastructure/config/components/AIModelConfig.tsx index 8d1d4bb66b..aa2bd1491a 100644 --- a/src/web-ui/src/infrastructure/config/components/AIModelConfig.tsx +++ b/src/web-ui/src/infrastructure/config/components/AIModelConfig.tsx @@ -6,10 +6,10 @@ import { AIModelConfig as AIModelConfigType, ProxyConfig, ModelCategory, - ModelCapability, ReasoningMode } from '../types'; import { configManager } from '../services/ConfigManager'; +import { getCapabilitiesByCategory, resolveModelCategory } from '../services/modelCategory'; import { PROVIDER_TEMPLATES, getModelDisplayName, getProviderDisplayName, getProviderTemplateId } from '../services/modelConfigs'; import { DEFAULT_REASONING_MODE, getEffectiveReasoningMode, supportsAnthropicAdaptive, supportsAnthropicReasoning, supportsAnthropicThinkingBudget, supportsDeepSeekReasoningEffort, supportsResponsesReasoning } from '../utils/reasoning'; import { aiApi, systemAPI } from '@/infrastructure/api'; @@ -233,15 +233,6 @@ function dedupeSelectedModelDraftsByModelName(drafts: SelectedModelDraft[]): Sel return out; } -function getCapabilitiesByCategory(category: ModelCategory): ModelCapability[] { - switch (category) { - case 'general_chat': - case 'multimodal': - default: - return ['text_chat', 'function_calling']; - } -} - /** * Compute the stored request URL from a base URL and provider format. * For gemini, stores the bare base (no /v1beta/models/... suffix) — @@ -286,6 +277,90 @@ function previewRequestUrl(baseUrl: string, provider: string): string { return resolveRequestUrl(baseUrl, provider); } +function hasHttpUrlScheme(value: string): boolean { + return /^https?:\/\//i.test(value.trim()); +} + +function stableJson(value: unknown): string { + if (Array.isArray(value)) { + return `[${value.map(stableJson).join(',')}]`; + } + if (value && typeof value === 'object') { + return `{${Object.entries(value as Record) + .sort(([a], [b]) => a.localeCompare(b)) + .map(([key, entryValue]) => `${JSON.stringify(key)}:${stableJson(entryValue)}`) + .join(',')}}`; + } + return JSON.stringify(value); +} + +function normalizeComparableString(value: string | undefined): string { + return (value || '').trim(); +} + +function providerConnectionChanged( + previous: AIModelConfigType | undefined, + next: AIModelConfigType +): boolean { + if (!previous) return true; + + return ( + normalizeComparableString(previous.provider) !== normalizeComparableString(next.provider) || + normalizeComparableString(previous.base_url) !== normalizeComparableString(next.base_url) || + normalizeComparableString(previous.api_key) !== normalizeComparableString(next.api_key) || + stableJson(previous.auth || { type: 'api_key' }) !== stableJson(next.auth || { type: 'api_key' }) || + stableJson(previous.custom_headers || {}) !== stableJson(next.custom_headers || {}) || + normalizeComparableString(previous.custom_headers_mode) !== normalizeComparableString(next.custom_headers_mode) || + normalizeComparableString(previous.custom_request_body) !== normalizeComparableString(next.custom_request_body) || + normalizeComparableString(previous.custom_request_body_mode) !== normalizeComparableString(next.custom_request_body_mode) || + (previous.skip_ssl_verify ?? false) !== (next.skip_ssl_verify ?? false) + ); +} + +function modelRequestBehaviorChanged( + previous: AIModelConfigType | undefined, + next: AIModelConfigType +): boolean { + if (!previous) return true; + + return ( + normalizeComparableString(previous.model_name) !== normalizeComparableString(next.model_name) || + normalizeComparableString(previous.request_url) !== normalizeComparableString(next.request_url) || + previous.context_window !== next.context_window || + previous.max_tokens !== next.max_tokens || + previous.category !== next.category || + stableJson(previous.capabilities || []) !== stableJson(next.capabilities || []) || + normalizeComparableString(previous.reasoning_mode) !== normalizeComparableString(next.reasoning_mode) || + normalizeComparableString(previous.reasoning_effort) !== normalizeComparableString(next.reasoning_effort) || + previous.thinking_budget_tokens !== next.thinking_budget_tokens || + (previous.inline_think_in_text ?? true) !== (next.inline_think_in_text ?? true) + ); +} + +function configsNeedingAutoTest( + previousModels: AIModelConfigType[], + nextConfigs: AIModelConfigType[], + isProviderGroupEdit: boolean +): AIModelConfigType[] { + const previousById = new Map(previousModels.map(model => [model.id, model])); + const providerConnectionWasChanged = isProviderGroupEdit && nextConfigs.some(config => + providerConnectionChanged(previousById.get(config.id), config) + ); + + if (providerConnectionWasChanged) { + return nextConfigs; + } + + return nextConfigs.filter(config => { + const previous = previousById.get(config.id); + return ( + !previous || + providerConnectionChanged(previous, config) || + modelRequestBehaviorChanged(previous, config) + ); + }); +} + const AIModelConfig: React.FC = () => { const { t } = useTranslation('settings/ai-model'); const { t: tDefault } = useTranslation('settings/default-model'); @@ -1019,11 +1094,15 @@ const AIModelConfig: React.FC = () => { try { const providerName = editingConfig.name.trim(); - const baseUrl = editingConfig.base_url; + const baseUrl = editingConfig.base_url.trim(); if (!providerName || !baseUrl) { notification.warning(t('messages.fillRequired')); return; } + if (!hasHttpUrlScheme(baseUrl)) { + notification.warning(t('messages.invalidBaseUrlScheme')); + return; + } const draftsToSave = dedupeSelectedModelDraftsByModelName(selectedModelDrafts); const existingProviderInstanceId = getProviderInstanceId(editingConfig); const isProviderGroupEdit = !editingConfig.id && editingProviderModelIds.size > 0; @@ -1047,8 +1126,18 @@ const AIModelConfig: React.FC = () => { enabled: editingConfig.enabled ?? true, context_window: draft.contextWindow, max_tokens: draft.maxTokens, - category: draft.category, - capabilities: getCapabilitiesByCategory(draft.category), + category: resolveModelCategory( + draft.modelName, + draft.category, + editingConfig.provider || 'openai' + ), + capabilities: getCapabilitiesByCategory( + resolveModelCategory( + draft.modelName, + draft.category, + editingConfig.provider || 'openai' + ) + ), recommended_for: editingConfig.recommended_for || [], metadata: { ...(editingConfig.metadata || {}), @@ -1066,6 +1155,11 @@ const AIModelConfig: React.FC = () => { auth: editingConfig.auth || { type: 'api_key' }, }; }); + const configsToAutoTest = configsNeedingAutoTest( + aiModels, + configsToSave, + isProviderGroupEdit + ); let updatedModels: AIModelConfigType[]; if (editingConfig.id) { @@ -1105,17 +1199,6 @@ const AIModelConfig: React.FC = () => { } - const createdConfigIds = configsToSave.map(config => config.id).filter((id): id is string => !!id); - if (createdConfigIds.length === 0) { - - setIsEditing(false); - setEditingConfig(null); - setCreationMode(null); - setSelectedProviderId(null); - setEditingProviderModelIds(new Set()); - return; - } - setIsEditing(false); setEditingConfig(null); setCreationMode(null); @@ -1123,11 +1206,14 @@ const AIModelConfig: React.FC = () => { setEditingProviderModelIds(new Set()); - setExpandedIds(prev => new Set([...prev, ...createdConfigIds])); + const autoTestConfigIds = configsToAutoTest.map(config => config.id).filter((id): id is string => !!id); + if (autoTestConfigIds.length > 0) { + setExpandedIds(prev => new Set([...prev, ...autoTestConfigIds])); + } - configsToSave.forEach(config => { + configsToAutoTest.forEach(config => { const configId = config.id; if (!configId) return; @@ -1186,7 +1272,7 @@ const AIModelConfig: React.FC = () => { const nextDefaultModels = { ...currentDefaultModels }; let defaultModelsChanged = false; - for (const key of ['primary', 'fast']) { + for (const key of ['primary', 'fast', 'image_understanding']) { if (nextDefaultModels[key] === id) { nextDefaultModels[key] = null; defaultModelsChanged = true; @@ -2485,6 +2571,38 @@ const AIModelConfig: React.FC = () => { ); }; + const streamTtftTimeoutLabel = ( + + {t('streamTtftTimeout.label')} + + + + + + + ); + + const streamIdleTimeoutLabel = ( + + {t('streamIdleTimeout.label')} + + + + + + + ); + return ( @@ -2626,7 +2744,7 @@ const AIModelConfig: React.FC = () => { { )} > { /> { const { t } = useTranslation('settings/default-model'); const renderOptionalLabel = (text: string) => ( @@ -41,7 +43,11 @@ export const DefaultModelConfig: React.FC = () => { const [loading, setLoading] = useState(true); const [models, setModels] = useState([]); - const [defaultModels, setDefaultModels] = useState({ primary: null, fast: null }); + const [defaultModels, setDefaultModels] = useState({ + primary: null, + fast: null, + image_understanding: null, + }); const loadData = useCallback(async () => { try { @@ -57,6 +63,7 @@ export const DefaultModelConfig: React.FC = () => { setDefaultModels({ primary: defaultModelsConfig?.primary || null, fast: defaultModelsConfig?.fast || null, + image_understanding: defaultModelsConfig?.image_understanding || null, }); } catch (error) { log.error('Failed to load data', error); @@ -157,7 +164,22 @@ export const DefaultModelConfig: React.FC = () => { }, []); - const handleDefaultModelChange = async (slot: 'primary' | 'fast', modelId: string | number) => { + const slotLabel = useCallback((slot: DefaultModelSlot): string => { + switch (slot) { + case 'primary': + return t('core.primary.label'); + case 'fast': + return t('core.fast.label'); + case 'image_understanding': + return t('optional.capabilities.image_understanding.label'); + default: { + const exhaustive: never = slot; + return exhaustive; + } + } + }, [t]); + + const handleDefaultModelChange = async (slot: DefaultModelSlot, modelId: string | number) => { const modelIdStr = modelId ? String(modelId) : null; try { const currentConfig = await configManager.getConfig('ai.default_models') || {}; @@ -176,7 +198,7 @@ export const DefaultModelConfig: React.FC = () => { const modelName = getModelName(modelIdStr); notificationService.success( t('messages.modelUpdated', { - slot: slot === 'primary' ? t('core.primary.label') : t('core.fast.label'), + slot: slotLabel(slot), name: modelName || modelIdStr, }), { duration: 2000 } @@ -189,6 +211,10 @@ export const DefaultModelConfig: React.FC = () => { const enabledModels = models.filter(m => m.enabled); + const imageUnderstandingModels = enabledModels.filter(model => { + const capabilities = Array.isArray(model.capabilities) ? model.capabilities : []; + return model.category === 'multimodal' || capabilities.includes('image_understanding'); + }); if (loading) { return ( @@ -247,6 +273,26 @@ export const DefaultModelConfig: React.FC = () => { size="small" /> + + + { + const selectedValue = String(Array.isArray(value) ? value[0] : value); + void updateSetting( + 'agent_companion_display_mode', + selectedValue === 'desktop' ? 'desktop' : 'input', + ); + }} + /> + + + + + {t('features.agentCompanion.petLabel')} + + + {t('features.agentCompanion.petDescription')} + + + + void handleRefreshCompanionPets()} + disabled={companionPetsLoading} + aria-label={t('features.agentCompanion.refresh')} + tooltip={t('features.agentCompanion.refresh')} + > + + + + + + )} + align="start" + multiline + className="bitfun-func-agent-config__pet-row" + > +
+
+ + {companionPetListExpanded && ( +
+ {companionPetOptions.map((option, index) => { + const pet = companionPets.find(item => item.packagePath === option.value); + const isUserPet = pet?.source === 'user'; + const isDeleting = !!pet && companionPetDeletingPath === pet.packagePath; + const isSelected = option.value === selectedCompanionPetValue; + const showGroup = option.group && option.group !== companionPetOptions[index - 1]?.group; + return ( + + {showGroup && ( +
+ {option.group} +
+ )} +
void handleCompanionPetChange(option.value)} + onKeyDown={(event) => { + if (event.key === 'Enter' || event.key === ' ') { + event.preventDefault(); + void handleCompanionPetChange(option.value); + } + }} + > +
+ + {pet ? ( + + ) : ( + + )} + + + {option.label} + {option.description && ( + {option.description} + )} + +
+
+ {isSelected && ( + + )} + {isUserPet && IS_TAURI_DESKTOP && pet && ( + void handleDeleteCompanionPet(e, pet)} + > + + + )} +
+
+
+ ); + })} +
+ )} +
+
+ + + ) : null} @@ -659,18 +1090,25 @@ const SessionSettingsPanels: React.FC = ({ variant } />
- -
- +
+
+ + {tTools('config.subagentMaxConcurrency')} + + )} + description={tTools('config.subagentMaxConcurrencyDesc')} + align="center" + > +
+ void handleSubagentMaxConcurrencyChange(val)} + min={1} + max={100} + step={1} + size="small" + variant="compact" + /> +
+
+ + + {/* ── Computer use (desktop) ─────────────────────────────── */} + + {IS_TAURI_DESKTOP ? ( + <> + +
+ handleComputerUseEnabledChange(e.target.checked)} + disabled={computerUseBusy || computerUseStatusLoading} + size="small" + /> +
+
+ +
+ + + {computerUseAccessLabel} + + void refreshComputerUseStatus()} + > + + + + {platform === 'macos' && ( + + )} +
+
+ +
+ + + {computerUseScreenLabel} + + void refreshComputerUseStatus()} + > + + + + {platform === 'macos' && ( + + )} +
+
+ {computerUsePlatformNote && ( +
+ +

+ {t('computerUse.platformNote')}: + {computerUsePlatformNote} +

+
+ )} + + ) : null}
{/* ── Browser control (CDP) ──────────────────────────────── */} diff --git a/src/web-ui/src/infrastructure/config/components/SkillsConfig.scss b/src/web-ui/src/infrastructure/config/components/SkillsConfig.scss index 4ef2317b2f..2895e541cc 100644 --- a/src/web-ui/src/infrastructure/config/components/SkillsConfig.scss +++ b/src/web-ui/src/infrastructure/config/components/SkillsConfig.scss @@ -29,14 +29,14 @@ font-size: var(--font-size-sm); &.is-valid { - background: rgba($color-success, 0.08); - border: 1px solid rgba($color-success, 0.3); + background: color-mix(in srgb, var(--color-success) 8%, transparent); + border: 1px solid color-mix(in srgb, var(--color-success) 30%, transparent); color: var(--color-success); } &.is-invalid { - background: rgba($color-error, 0.08); - border: 1px solid rgba($color-error, 0.3); + background: color-mix(in srgb, var(--color-error) 8%, transparent); + border: 1px solid color-mix(in srgb, var(--color-error) 30%, transparent); color: var(--color-error); } } @@ -73,8 +73,8 @@ .search__wrapper { background: linear-gradient( 180deg, - var(--color-overlay-white-06) 0%, - var(--color-overlay-white-03) 100% + var(--color-overlay-white-08) 0%, + var(--color-overlay-white-04) 100% ); border: 1px solid var(--border-subtle); border-radius: $size-radius-lg; @@ -88,7 +88,7 @@ background: linear-gradient( 180deg, var(--color-overlay-white-04) 0%, - var(--color-overlay-white-02) 100% + var(--color-overlay-white-04) 100% ); color: var(--color-text-secondary); font-size: var(--font-size-sm); @@ -96,7 +96,7 @@ } &__market-state--error { - border-color: rgba($color-error, 0.4); + border-color: color-mix(in srgb, var(--color-error) 40%, transparent); color: var(--color-error); } @@ -113,8 +113,8 @@ border-radius: $size-radius-lg; background: linear-gradient( 180deg, - var(--color-overlay-white-05) 0%, - var(--color-overlay-white-03) 100% + var(--color-overlay-white-08) 0%, + var(--color-overlay-white-04) 100% ); box-shadow: $shadow-xs; transition: @@ -129,7 +129,7 @@ top: 0; bottom: 0; width: 3px; - background: rgba($color-accent-500, 0.6); + background: color-mix(in srgb, var(--color-accent-500) 60%, transparent); opacity: 0.75; } @@ -140,10 +140,10 @@ } &.is-installed { - border-color: rgba($color-success, 0.35); + border-color: color-mix(in srgb, var(--color-success) 35%, transparent); &::before { - background: rgba($color-success, 0.78); + background: color-mix(in srgb, var(--color-success) 78%, transparent); } } @@ -208,8 +208,8 @@ } &__market-item-badge--installed { - border-color: rgba($color-success, 0.35); - background: rgba($color-success, 0.14); + border-color: color-mix(in srgb, var(--color-success) 35%, transparent); + background: color-mix(in srgb, var(--color-success) 14%, transparent); color: var(--color-success); } @@ -250,8 +250,8 @@ &__market-item-source { color: var(--color-accent-500); - background: rgba($color-accent-500, 0.1); - border-color: rgba($color-accent-500, 0.25); + background: color-mix(in srgb, var(--color-accent-500) 10%, transparent); + border-color: color-mix(in srgb, var(--color-accent-500) 25%, transparent); } &__market-item-installs { @@ -265,7 +265,7 @@ font-weight: $font-weight-medium; color: var(--color-text-muted); border: 1px solid var(--border-subtle); - background: var(--color-overlay-white-06); + background: var(--color-overlay-white-08); } &__market-item-action { diff --git a/src/web-ui/src/infrastructure/config/components/common/ConfigCollectionItem.scss b/src/web-ui/src/infrastructure/config/components/common/ConfigCollectionItem.scss index 70293249f9..b759694196 100644 --- a/src/web-ui/src/infrastructure/config/components/common/ConfigCollectionItem.scss +++ b/src/web-ui/src/infrastructure/config/components/common/ConfigCollectionItem.scss @@ -143,7 +143,7 @@ } &--danger:hover { - background: rgba($color-error, 0.08); + background: color-mix(in srgb, var(--color-error) 8%, transparent); color: var(--color-error); } } diff --git a/src/web-ui/src/infrastructure/config/services/AIExperienceConfigService.test.ts b/src/web-ui/src/infrastructure/config/services/AIExperienceConfigService.test.ts index 81dac9ccbb..b7e0f33f95 100644 --- a/src/web-ui/src/infrastructure/config/services/AIExperienceConfigService.test.ts +++ b/src/web-ui/src/infrastructure/config/services/AIExperienceConfigService.test.ts @@ -6,10 +6,18 @@ const configManagerMock = vi.hoisted(() => ({ watch: vi.fn(), })); +const configApiMock = vi.hoisted(() => ({ + getConfig: vi.fn(), +})); + vi.mock('./ConfigManager', () => ({ configManager: configManagerMock, })); +vi.mock('@/infrastructure/api/service-api/ConfigAPI', () => ({ + configAPI: configApiMock, +})); + vi.mock('./AgentCompanionPetService', () => ({ DEFAULT_AGENT_COMPANION_PET: { id: 'default', @@ -55,4 +63,17 @@ describe('AIExperienceConfigService startup behavior', () => { expect(configManagerMock.getConfig).toHaveBeenCalledTimes(1); expect(configManagerMock.getConfig).toHaveBeenCalledWith('app.ai_experience'); }); + + it('can force refresh settings for cross-window lifecycle synchronization', async () => { + configApiMock.getConfig.mockResolvedValueOnce({ + enable_agent_companion: true, + agent_companion_display_mode: 'desktop', + }); + const { aiExperienceConfigService } = await import('./AIExperienceConfigService'); + + await aiExperienceConfigService.getSettingsAsync({ forceRefresh: true }); + + expect(configApiMock.getConfig).toHaveBeenCalledWith('app.ai_experience'); + expect(configManagerMock.getConfig).not.toHaveBeenCalled(); + }); }); diff --git a/src/web-ui/src/infrastructure/config/services/AIExperienceConfigService.ts b/src/web-ui/src/infrastructure/config/services/AIExperienceConfigService.ts index 801adb9e6c..73eaaaf144 100644 --- a/src/web-ui/src/infrastructure/config/services/AIExperienceConfigService.ts +++ b/src/web-ui/src/infrastructure/config/services/AIExperienceConfigService.ts @@ -2,6 +2,7 @@ import { configManager } from './ConfigManager'; import { DEFAULT_AGENT_COMPANION_PET } from './AgentCompanionPetService'; +import { configAPI } from '@/infrastructure/api/service-api/ConfigAPI'; import { createLogger } from '@/shared/utils/logger'; const log = createLogger('AIExperienceConfig'); @@ -130,10 +131,12 @@ export class AIExperienceConfigService { } - async getSettingsAsync(): Promise { + async getSettingsAsync(options?: { forceRefresh?: boolean }): Promise { this.ensureConfigWatcher(); try { - const settings = await configManager.getConfig(CONFIG_PATH); + const settings = options?.forceRefresh + ? await configAPI.getConfig(CONFIG_PATH) as AIExperienceSettings + : await configManager.getConfig(CONFIG_PATH); this.cachedSettings = normalizeSettings(settings); return this.cachedSettings; } catch (error) { diff --git a/src/web-ui/src/infrastructure/config/services/AgentCompanionWindowService.test.ts b/src/web-ui/src/infrastructure/config/services/AgentCompanionWindowService.test.ts new file mode 100644 index 0000000000..537ea511e5 --- /dev/null +++ b/src/web-ui/src/infrastructure/config/services/AgentCompanionWindowService.test.ts @@ -0,0 +1,131 @@ +import { beforeEach, describe, expect, it, vi } from 'vitest'; +import type { AIExperienceSettings } from './AIExperienceConfigService'; + +const tauriCore = vi.hoisted(() => ({ + invoke: vi.fn(), +})); + +const tauriEvent = vi.hoisted(() => ({ + emit: vi.fn(), +})); + +vi.mock('@tauri-apps/api/core', () => ({ + invoke: tauriCore.invoke, +})); + +vi.mock('@tauri-apps/api/event', () => ({ + emit: tauriEvent.emit, +})); + +vi.mock('@/infrastructure/runtime', () => ({ + isTauriRuntime: () => true, +})); + +vi.mock('@/shared/utils/logger', () => ({ + createLogger: () => ({ + debug: vi.fn(), + error: vi.fn(), + }), +})); + +function deferred() { + let resolve!: (value: T | PromiseLike) => void; + let reject!: (reason?: unknown) => void; + const promise = new Promise((res, rej) => { + resolve = res; + reject = rej; + }); + return { promise, resolve, reject }; +} + +function settings( + enableAgentCompanion: boolean, + displayMode: AIExperienceSettings['agent_companion_display_mode'] = 'desktop', +): AIExperienceSettings { + return { + enable_session_title_generation: true, + enable_visual_mode: false, + enable_agent_companion: enableAgentCompanion, + agent_companion_display_mode: displayMode, + enable_workspace_search: false, + quick_actions: [], + }; +} + +describe('syncAgentCompanionDesktopWindow', () => { + beforeEach(() => { + vi.resetModules(); + vi.clearAllMocks(); + }); + + it('skips stale queued show or hide requests so the latest config wins', async () => { + const firstInvoke = deferred(); + tauriCore.invoke + .mockReturnValueOnce(firstInvoke.promise) + .mockResolvedValue(undefined); + + const { syncAgentCompanionDesktopWindow } = await import('./AgentCompanionWindowService'); + + const first = syncAgentCompanionDesktopWindow(settings(true)); + await vi.waitFor(() => { + expect(tauriCore.invoke).toHaveBeenCalledTimes(1); + }); + + const staleHide = syncAgentCompanionDesktopWindow(settings(false)); + const latestShow = syncAgentCompanionDesktopWindow(settings(true)); + + firstInvoke.resolve(); + await Promise.all([first, staleHide, latestShow]); + + expect(tauriCore.invoke.mock.calls.map(call => call[0])).toEqual([ + 'show_agent_companion_desktop_pet', + 'show_agent_companion_desktop_pet', + ]); + expect(tauriEvent.emit).toHaveBeenCalledTimes(1); + expect(tauriEvent.emit).toHaveBeenCalledWith( + 'agent-companion://settings-updated', + expect.objectContaining({ + enable_agent_companion: true, + agent_companion_display_mode: 'desktop', + }), + ); + }); + + it('hides the desktop pet when the companion is disabled or moved to input mode', async () => { + const { syncAgentCompanionDesktopWindow } = await import('./AgentCompanionWindowService'); + + await syncAgentCompanionDesktopWindow(settings(false)); + await syncAgentCompanionDesktopWindow(settings(true, 'input')); + + expect(tauriCore.invoke.mock.calls.map(call => call[0])).toEqual([ + 'hide_agent_companion_desktop_pet', + 'hide_agent_companion_desktop_pet', + ]); + expect(tauriEvent.emit).not.toHaveBeenCalled(); + }); + + it('runs a latest hide after an in-flight stale show without emitting stale settings', async () => { + const firstInvoke = deferred(); + tauriCore.invoke + .mockReturnValueOnce(firstInvoke.promise) + .mockResolvedValue(undefined); + + const { syncAgentCompanionDesktopWindow } = await import('./AgentCompanionWindowService'); + + const staleShow = syncAgentCompanionDesktopWindow(settings(true)); + await vi.waitFor(() => { + expect(tauriCore.invoke).toHaveBeenCalledTimes(1); + }); + + const latestHide = syncAgentCompanionDesktopWindow(settings(false)); + + firstInvoke.resolve(); + await Promise.all([staleShow, latestHide]); + + expect(tauriCore.invoke.mock.calls.map(call => call[0])).toEqual([ + 'show_agent_companion_desktop_pet', + 'hide_agent_companion_desktop_pet', + ]); + expect(tauriEvent.emit).not.toHaveBeenCalled(); + }); +}); diff --git a/src/web-ui/src/infrastructure/config/services/AgentCompanionWindowService.ts b/src/web-ui/src/infrastructure/config/services/AgentCompanionWindowService.ts index 942d9892f4..a5ec3657c9 100644 --- a/src/web-ui/src/infrastructure/config/services/AgentCompanionWindowService.ts +++ b/src/web-ui/src/infrastructure/config/services/AgentCompanionWindowService.ts @@ -8,11 +8,13 @@ const log = createLogger('AgentCompanionWindowService'); * Serialized `invoke`/`emit` so rapid settings toggles cannot interleave show/hide on the backend. */ let companionDesktopWindowSyncChain: Promise = Promise.resolve(); +let companionDesktopWindowSyncRequestId = 0; export async function syncAgentCompanionDesktopWindow( settings: AIExperienceSettings, ): Promise { if (!isTauriRuntime()) return; + const requestId = companionDesktopWindowSyncRequestId += 1; const run = async (): Promise => { const startedAt = performance.now(); @@ -21,6 +23,14 @@ export async function syncAgentCompanionDesktopWindow( ? 'show_agent_companion_desktop_pet' : 'hide_agent_companion_desktop_pet'; + if (requestId !== companionDesktopWindowSyncRequestId) { + log.debug('Skipped stale Agent companion desktop window sync', { + command, + displayMode: settings.agent_companion_display_mode, + }); + return; + } + try { log.debug('Agent companion desktop window sync started', { command, @@ -28,6 +38,9 @@ export async function syncAgentCompanionDesktopWindow( }); const { invoke } = await import('@tauri-apps/api/core'); await invoke(command); + if (requestId !== companionDesktopWindowSyncRequestId) { + return; + } if (command === 'show_agent_companion_desktop_pet') { const { emit } = await import('@tauri-apps/api/event'); await emit('agent-companion://settings-updated', settings); diff --git a/src/web-ui/src/infrastructure/config/services/modelCategory.ts b/src/web-ui/src/infrastructure/config/services/modelCategory.ts new file mode 100644 index 0000000000..1711bb29d3 --- /dev/null +++ b/src/web-ui/src/infrastructure/config/services/modelCategory.ts @@ -0,0 +1,50 @@ +import type { ModelCapability, ModelCategory } from '../types'; + +const MULTIMODAL_MODEL_HINTS = [ + 'vision', + 'gpt-4o', + 'gpt-4-turbo', + 'claude-3', + 'gemini-pro-vision', + 'gemini-1.5', + 'kimi', +]; + +export function inferModelCategory( + modelName: string, + _provider?: string +): ModelCategory { + const normalized = modelName.trim().toLowerCase(); + if (MULTIMODAL_MODEL_HINTS.some(hint => normalized.includes(hint))) { + return 'multimodal'; + } + return 'general_chat'; +} + +export function resolveModelCategory( + modelName: string, + category?: ModelCategory, + provider?: string +): ModelCategory { + const inferred = inferModelCategory(modelName, provider); + + if (category === 'multimodal') { + return 'multimodal'; + } + + if (category === 'general_chat' && inferred === 'multimodal') { + return 'multimodal'; + } + + return category ?? inferred; +} + +export function getCapabilitiesByCategory(category: ModelCategory): ModelCapability[] { + switch (category) { + case 'multimodal': + return ['text_chat', 'image_understanding', 'function_calling']; + case 'general_chat': + default: + return ['text_chat', 'function_calling']; + } +} diff --git a/src/web-ui/src/infrastructure/config/services/modelConfigs.test.ts b/src/web-ui/src/infrastructure/config/services/modelConfigs.test.ts index 641f9d87dc..ecad597132 100644 --- a/src/web-ui/src/infrastructure/config/services/modelConfigs.test.ts +++ b/src/web-ui/src/infrastructure/config/services/modelConfigs.test.ts @@ -74,4 +74,52 @@ describe('modelConfigs', () => { }), ]); }); + + it('preserves multimodal model metadata when loading and saving ai.models', async () => { + configManagerMock.getConfig.mockResolvedValueOnce([ + { + id: 'kimi-1', + name: 'Moonshot', + base_url: 'https://api.moonshot.cn/v1', + api_key: '', + model_name: 'kimi-k2.7', + provider: 'openai', + category: 'multimodal', + capabilities: ['text_chat', 'image_understanding', 'function_calling'], + }, + ]); + + const { modelConfigManager } = await import('./modelConfigs'); + const listener = vi.fn(); + + modelConfigManager.addListener(listener); + await Promise.resolve(); + await Promise.resolve(); + + expect(listener).toHaveBeenCalledWith([ + expect.objectContaining({ + id: 'kimi-1', + category: 'multimodal', + capabilities: expect.arrayContaining(['image_understanding']), + }), + ]); + + modelConfigManager.updateConfig('kimi-1', { + category: 'multimodal', + capabilities: ['text_chat', 'image_understanding', 'function_calling'], + }); + + await Promise.resolve(); + + expect(configManagerMock.setConfig).toHaveBeenCalledWith( + 'ai.models', + expect.arrayContaining([ + expect.objectContaining({ + id: 'kimi-1', + category: 'multimodal', + capabilities: expect.arrayContaining(['image_understanding']), + }), + ]) + ); + }); }); diff --git a/src/web-ui/src/infrastructure/config/services/modelConfigs.ts b/src/web-ui/src/infrastructure/config/services/modelConfigs.ts index 5f9d9de6cd..e9457b35ac 100644 --- a/src/web-ui/src/infrastructure/config/services/modelConfigs.ts +++ b/src/web-ui/src/infrastructure/config/services/modelConfigs.ts @@ -1,5 +1,6 @@ import { ModelConfig, ProviderTemplate, ApiFormat } from '../../../shared/types'; import { configManager } from './ConfigManager'; +import { getCapabilitiesByCategory, resolveModelCategory } from './modelCategory'; import { i18nService } from '@/infrastructure/i18n'; import { createLogger } from '@/shared/utils/logger'; import { extractProviderSegmentFromBaseUrl, matchProviderCatalogItemByBaseUrl } from './providerCatalog'; @@ -281,7 +282,21 @@ class ModelConfigManager { description: model.description || t('settings/ai-model:messages.defaultDescription', { name: model.name }), isBuiltIn: false, contextWindow: model.context_window, - maxTokens: model.max_tokens + maxTokens: model.max_tokens, + category: resolveModelCategory( + model.model_name || '', + model.category, + model.provider + ), + capabilities: Array.isArray(model.capabilities) && model.capabilities.length > 0 + ? model.capabilities + : getCapabilitiesByCategory( + resolveModelCategory( + model.model_name || '', + model.category, + model.provider + ) + ), })); } else { // No config available from backend. @@ -310,7 +325,21 @@ class ModelConfigManager { enabled: true, description: config.description, context_window: config.contextWindow, - max_tokens: config.maxTokens + max_tokens: config.maxTokens, + category: resolveModelCategory( + config.modelName, + config.category, + config.format + ), + capabilities: config.capabilities && config.capabilities.length > 0 + ? config.capabilities + : getCapabilitiesByCategory( + resolveModelCategory( + config.modelName, + config.category, + config.format + ) + ), })); // Save to the unified config system. @@ -402,13 +431,17 @@ class ModelConfigManager { const template = PROVIDER_TEMPLATES[providerId]; if (!template) return null; + const category = resolveModelCategory(modelName, undefined, template.format); + return this.addConfig({ name: template.name, baseUrl: template.baseUrl, modelName, format: template.format, description: t('settings/ai-model:messages.templateDescription', { description: template.description, modelName }), - isBuiltIn: false + isBuiltIn: false, + category, + capabilities: getCapabilitiesByCategory(category), }); } diff --git a/src/web-ui/src/infrastructure/config/types/index.ts b/src/web-ui/src/infrastructure/config/types/index.ts index 7b76027a14..c9598819d8 100644 --- a/src/web-ui/src/infrastructure/config/types/index.ts +++ b/src/web-ui/src/infrastructure/config/types/index.ts @@ -95,7 +95,8 @@ export interface AIExperienceConfig { export type ModelCapability = | 'text_chat' - | 'function_calling'; + | 'function_calling' + | 'image_understanding'; export type ModelCategory = | 'general_chat' @@ -179,6 +180,7 @@ export interface ProxyConfig { export interface DefaultModelsConfig { primary?: string | null; fast?: string | null; + image_understanding?: string | null; } export interface AIConfig { @@ -592,6 +594,7 @@ export interface DiagnosticsBundleInfo { export interface DefaultModels { primary: string | null; fast: string | null; + image_understanding?: string | null; } export type OptionalCapabilityModels = Record; diff --git a/src/web-ui/src/infrastructure/font-preference/components/FontPreferencePanel.scss b/src/web-ui/src/infrastructure/font-preference/components/FontPreferencePanel.scss index 6e235c844d..2fea7eeb8c 100644 --- a/src/web-ui/src/infrastructure/font-preference/components/FontPreferencePanel.scss +++ b/src/web-ui/src/infrastructure/font-preference/components/FontPreferencePanel.scss @@ -97,11 +97,11 @@ min-width: 0; padding: $size-gap-1 $size-gap-3; font-size: var(--font-size-sm); - font-family: var(--font-sans); + font-family: var(--font-family-sans); background: transparent; border: 1px solid var(--border-base); border-radius: $size-radius-sm; - color: var(--text-secondary); + color: var(--color-text-secondary); cursor: pointer; transition: background $motion-fast $easing-standard, border-color $motion-fast $easing-standard, @@ -112,7 +112,7 @@ &:hover:not(:disabled):not(&--active) { background: var(--element-bg-subtle); border-color: var(--border-strong); - color: var(--text-primary); + color: var(--color-text-primary); } &--active { @@ -154,7 +154,7 @@ &__custom-unit { font-size: var(--font-size-sm); - color: var(--text-secondary); + color: var(--color-text-secondary); flex-shrink: 0; } @@ -166,7 +166,7 @@ height: 28px; background: transparent; border: none; - color: var(--text-secondary); + color: var(--color-text-secondary); cursor: pointer; font-size: var(--font-size-base); transition: background $motion-fast $easing-standard; @@ -174,7 +174,7 @@ &:hover:not(:disabled) { background: var(--element-bg-subtle); - color: var(--text-primary); + color: var(--color-text-primary); } &:disabled { @@ -190,8 +190,8 @@ border: none; border-left: 1px solid var(--border-base); border-right: 1px solid var(--border-base); - background: var(--bg-primary); - color: var(--text-primary); + background: var(--color-bg-primary); + color: var(--color-text-primary); font-size: var(--font-size-sm); text-align: center; outline: none !important; @@ -217,22 +217,22 @@ } &--error { - border-left-color: var(--color-semantic-error); - border-right-color: var(--color-semantic-error); - color: var(--color-semantic-error); + border-left-color: var(--color-error); + border-right-color: var(--color-error); + color: var(--color-error); } } &__error { font-size: var(--font-size-xs); - color: var(--color-semantic-error); + color: var(--color-error); width: 100%; } &__custom-hint { width: 100%; font-size: var(--font-size-xs); - color: var(--text-muted); + color: var(--color-text-muted); line-height: 1.4; } @@ -241,10 +241,10 @@ box-sizing: border-box; margin-top: $size-gap-2; padding: $size-gap-3 $size-gap-4; - background: var(--bg-secondary); + background: var(--color-bg-secondary); border: 1px solid var(--border-subtle); border-radius: $size-radius-sm; - color: var(--text-secondary); + color: var(--color-text-secondary); line-height: 1.6; word-wrap: break-word; transition: font-size $motion-base $easing-standard; @@ -254,18 +254,18 @@ margin-left: auto; padding: $size-gap-2 $size-gap-4; font-size: var(--font-size-sm); - font-family: var(--font-sans); + font-family: var(--font-family-sans); background: transparent; border: 1px solid var(--border-base); border-radius: $size-radius-sm; - color: var(--text-secondary); + color: var(--color-text-secondary); cursor: pointer; transition: all $motion-fast $easing-standard; &:hover { background: var(--element-bg-subtle); border-color: var(--border-strong); - color: var(--text-primary); + color: var(--color-text-primary); } } } diff --git a/src/web-ui/src/infrastructure/font-preference/types/fontPreference.test.ts b/src/web-ui/src/infrastructure/font-preference/types/fontPreference.test.ts index 6c059b0b97..5b3760e637 100644 --- a/src/web-ui/src/infrastructure/font-preference/types/fontPreference.test.ts +++ b/src/web-ui/src/infrastructure/font-preference/types/fontPreference.test.ts @@ -16,6 +16,7 @@ describe('deriveFontSizeTokens', () => { expect(tokens.lg).toBe('15px'); expect(tokens.xl).toBe('16px'); expect(tokens['2xl']).toBe('18px'); + expect('5xl' in tokens).toBe(false); }); it('clamps below minimum (12px)', () => { diff --git a/src/web-ui/src/infrastructure/font-preference/types/index.ts b/src/web-ui/src/infrastructure/font-preference/types/index.ts index 440f8b76b0..f563531f4d 100644 --- a/src/web-ui/src/infrastructure/font-preference/types/index.ts +++ b/src/web-ui/src/infrastructure/font-preference/types/index.ts @@ -36,7 +36,6 @@ export interface FontSizeTokens { '2xl': string; '3xl': string; '4xl': string; - '5xl': string; } export type FontSizeLevelPresets = Record, FontSizeTokens>; @@ -65,7 +64,6 @@ export function deriveFontSizeTokens(basePx: number): FontSizeTokens { '2xl': `${b + 4}px`, '3xl': `${b + 8}px`, '4xl': `${b + 12}px`, - '5xl': `${b + 18}px`, }; } diff --git a/src/web-ui/src/infrastructure/i18n/components/LanguageSelector.scss b/src/web-ui/src/infrastructure/i18n/components/LanguageSelector.scss index 83ab1109dc..431e9462d9 100644 --- a/src/web-ui/src/infrastructure/i18n/components/LanguageSelector.scss +++ b/src/web-ui/src/infrastructure/i18n/components/LanguageSelector.scss @@ -11,7 +11,7 @@ appearance: none; background: var(--input-bg); border: 1px solid var(--input-border); - border-radius: var(--radius-sm); + border-radius: var(--size-radius-sm); padding: 6px 28px 6px 10px; font-size: 13px; color: var(--input-text); @@ -54,7 +54,7 @@ .language-selector__inline-button { padding: 4px 10px; border: 1px solid transparent; - border-radius: var(--radius-sm); + border-radius: var(--size-radius-sm); background: transparent; color: var(--color-text-secondary); font-size: 13px; @@ -91,7 +91,7 @@ gap: 4px; padding: 6px 10px; border: 1px solid transparent; - border-radius: var(--radius-sm); + border-radius: var(--size-radius-sm); background: transparent; color: var(--color-text-secondary); font-size: 13px; @@ -127,7 +127,7 @@ padding: 4px; background: var(--color-bg-elevated); border: 1px solid var(--border-base); - border-radius: var(--radius-md); + border-radius: var(--size-radius-md); box-shadow: var(--shadow-lg); opacity: 0; visibility: hidden; @@ -150,7 +150,7 @@ width: 100%; padding: 8px 12px; border: none; - border-radius: var(--radius-sm); + border-radius: var(--size-radius-sm); background: transparent; color: var(--color-text-secondary); font-size: 13px; diff --git a/src/web-ui/src/infrastructure/i18n/store/i18nStore.ts b/src/web-ui/src/infrastructure/i18n/store/i18nStore.ts index e62f4421e0..9f9c98fe68 100644 --- a/src/web-ui/src/infrastructure/i18n/store/i18nStore.ts +++ b/src/web-ui/src/infrastructure/i18n/store/i18nStore.ts @@ -27,31 +27,51 @@ export const useI18nStore = create()( // Actions setCurrentLanguage: (locale: LocaleId) => { - set({ currentLanguage: locale }); + set((state) => ( + state.currentLanguage === locale + ? state + : { currentLanguage: locale } + )); }, setFallbackLanguage: (locale: LocaleId) => { - set({ fallbackLanguage: locale }); + set((state) => ( + state.fallbackLanguage === locale + ? state + : { fallbackLanguage: locale } + )); }, addLoadedNamespace: (namespace: I18nNamespace) => { - set((state) => ({ - loadedNamespaces: state.loadedNamespaces.includes(namespace) - ? state.loadedNamespaces - : [...state.loadedNamespaces, namespace], - })); + set((state) => ( + state.loadedNamespaces.includes(namespace) + ? state + : { loadedNamespaces: [...state.loadedNamespaces, namespace] } + )); }, setInitialized: (initialized: boolean) => { - set({ isInitialized: initialized }); + set((state) => ( + state.isInitialized === initialized + ? state + : { isInitialized: initialized } + )); }, setChanging: (changing: boolean) => { - set({ isChanging: changing }); + set((state) => ( + state.isChanging === changing + ? state + : { isChanging: changing } + )); }, setAutoDetect: (autoDetect: boolean) => { - set({ autoDetect }); + set((state) => ( + state.autoDetect === autoDetect + ? state + : { autoDetect } + )); }, reset: () => { diff --git a/src/web-ui/src/infrastructure/providers/CoreProvider.tsx b/src/web-ui/src/infrastructure/providers/CoreProvider.tsx index 9917def2b9..4b11b59270 100644 --- a/src/web-ui/src/infrastructure/providers/CoreProvider.tsx +++ b/src/web-ui/src/infrastructure/providers/CoreProvider.tsx @@ -92,7 +92,7 @@ export const CoreProvider: React.FC = ({ children }) => { onClick={() => window.location.reload()} style={{ padding: '8px 16px', - background: 'var(--color-primary)', + background: 'var(--color-accent-500)', color: 'var(--color-static-white)', border: 'none', borderRadius: '4px', diff --git a/src/web-ui/src/infrastructure/theme/core/ThemeService.test.ts b/src/web-ui/src/infrastructure/theme/core/ThemeService.test.ts index 8a10e3f44d..30e9ad49bb 100644 --- a/src/web-ui/src/infrastructure/theme/core/ThemeService.test.ts +++ b/src/web-ui/src/infrastructure/theme/core/ThemeService.test.ts @@ -2,10 +2,22 @@ import { JSDOM } from 'jsdom'; import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; import { configAPI } from '@/infrastructure/api'; -import { bitfunLightTheme } from '../presets'; -import type { ThemeConfig } from '../types'; +import { bitfunDarkTheme, bitfunLightTheme } from '../presets'; +import { + PLUGIN_THEME_COLOR_KEYS, + createPluginThemeColorProjection, +} from '../pluginThemeProjection'; +import { SYSTEM_THEME_ID, type ThemeConfig } from '../types'; import { ThemeService } from './ThemeService'; +function expectThemeError( + result: ReturnType, + path: string, + code: string, +) { + expect(result.errors).toEqual(expect.arrayContaining([expect.objectContaining({ path, code })])); +} + vi.mock('@/infrastructure/api', () => ({ configAPI: { getConfig: vi.fn(), @@ -28,7 +40,7 @@ vi.mock('@/shared/utils/logger', () => ({ }), })); -describe('ThemeService flow chat link tokens', () => { +describe('ThemeService runtime theme tokens', () => { let dom: JSDOM; const bootstrapGlobals = globalThis as typeof globalThis & { __BITFUN_BOOTSTRAP_THEME_ID__?: string; @@ -80,6 +92,93 @@ describe('ThemeService flow chat link tokens', () => { expect(rootStyle.getPropertyValue('--flowchat-link-hover-color')).toBe('#93c5fd'); }); + it('uses canonical light overlay stops for scrollbar fallback hover', async () => { + const service = new ThemeService(); + + await service.applyTheme('bitfun-light'); + + expect(document.documentElement.style.getPropertyValue('--scrollbar-thumb-hover')).toBe('rgba(0, 0, 0, 0.3)'); + }); + + it('keeps card surfaces on the compact canonical overlay ramp', async () => { + const service = new ThemeService(); + + await service.applyTheme('bitfun-dark'); + + expect(document.documentElement.style.getPropertyValue('--card-bg-default')).toBe('rgba(255, 255, 255, 0.04)'); + expect(document.documentElement.style.getPropertyValue('--card-bg-subtle')).toBe('transparent'); + expect(document.documentElement.style.getPropertyValue('--card-bg-elevated')).toBe('rgba(255, 255, 255, 0.08)'); + expect(document.documentElement.style.getPropertyValue('--card-bg-hover')).toBe('rgba(255, 255, 255, 0.08)'); + expect(document.documentElement.style.getPropertyValue('--card-bg-active')).toBe('rgba(255, 255, 255, 0.12)'); + + await service.applyTheme('bitfun-light'); + + expect(document.documentElement.style.getPropertyValue('--card-bg-default')).toBe('rgba(0, 0, 0, 0.08)'); + expect(document.documentElement.style.getPropertyValue('--card-bg-elevated')).toBe('rgba(0, 0, 0, 0.12)'); + expect(document.documentElement.style.getPropertyValue('--card-bg-subtle')).toBe('transparent'); + expect(document.documentElement.style.getPropertyValue('--card-bg-hover')).toBe('rgba(0, 0, 0, 0.12)'); + expect(document.documentElement.style.getPropertyValue('--card-bg-active')).toBe('rgba(0, 0, 0, 0.15)'); + }); + + it('keeps dark info border aligned with the canonical medium overlay stop', async () => { + const service = new ThemeService(); + + await service.applyTheme('bitfun-dark'); + + expect(document.documentElement.style.getPropertyValue('--color-info-border')).toBe('rgba(255, 255, 255, 0.24)'); + }); + + it('exports the consumed git runtime token family from the resolved theme', async () => { + const service = new ThemeService(); + + await service.applyTheme('bitfun-dark'); + + const rootStyle = document.documentElement.style; + expect(rootStyle.getPropertyValue('--git-color-branch')).toBe('#a1a1aa'); + expect(rootStyle.getPropertyValue('--git-color-branch-bg')).toBe('rgba(255, 255, 255, 0.06)'); + expect(rootStyle.getPropertyValue('--git-color-branch-bg-hover')).toBe('rgba(255, 255, 255, 0.12)'); + expect(rootStyle.getPropertyValue('--git-color-added')).toBe('rgb(34, 197, 94)'); + expect(rootStyle.getPropertyValue('--git-color-added-bg')).toBe('rgba(34, 197, 94, 0.1)'); + expect(rootStyle.getPropertyValue('--git-color-added-bg-hover')).toBe('rgba(34, 197, 94, 0.15)'); + expect(rootStyle.getPropertyValue('--git-color-changes-bg')).toBe('rgba(245, 158, 11, 0.1)'); + expect(rootStyle.getPropertyValue('--git-color-deleted-bg-hover')).toBe('rgba(239, 68, 68, 0.15)'); + expect(rootStyle.getPropertyValue('--git-color-staged-bg-hover')).toBe('rgba(34, 197, 94, 0.15)'); + expect(rootStyle.getPropertyValue('--git-color-staged-border')).toBe('rgba(34, 197, 94, 0.3)'); + expect(rootStyle.getPropertyValue('--git-color-pull')).toBe(''); + expect(rootStyle.getPropertyValue('--git-color-push')).toBe(''); + }); + + it('uses canonical dark overlay stops when a theme omits scrollbar values', () => { + const service = new ThemeService(); + const fallbackTheme: ThemeConfig = { + ...bitfunDarkTheme, + id: 'fallback-dark', + colors: { + ...bitfunDarkTheme.colors, + scrollbar: undefined, + }, + } as unknown as ThemeConfig; + + (service as unknown as { injectCSSVariables(theme: ThemeConfig): void }).injectCSSVariables(fallbackTheme); + + expect(document.documentElement.style.getPropertyValue('--scrollbar-thumb-hover')).toBe('rgba(255, 255, 255, 0.24)'); + }); + + it('exports only the compact low-risk shadow overlay stops', async () => { + const service = new ThemeService(); + + await service.applyTheme('bitfun-dark'); + + const rootStyle = document.documentElement.style; + for (const [tone, stop] of [['white', '06'], ['white', '10'], ['black', '10']] as const) { + expect(rootStyle.getPropertyValue(`--color-overlay-${tone}-${stop}`)).toBe(''); + } + expect(rootStyle.getPropertyValue('--color-overlay-white-08')).toBe('rgba(255, 255, 255, 0.08)'); + expect(rootStyle.getPropertyValue('--color-overlay-white-12')).toBe('rgba(255, 255, 255, 0.12)'); + expect(rootStyle.getPropertyValue('--color-overlay-black-12')).toBe('rgba(0, 0, 0, 0.12)'); + expect(rootStyle.getPropertyValue('--color-overlay-black-30')).toBe('rgba(0, 0, 0, 0.3)'); + }); + it('initializes from bootstrap theme selection without reading or writing themes.current', async () => { bootstrapGlobals.__BITFUN_BOOTSTRAP_THEME_ID__ = 'bitfun-slate'; bootstrapGlobals.__BITFUN_BOOTSTRAP_THEME_SELECTION__ = 'bitfun-slate'; @@ -190,4 +289,325 @@ describe('ThemeService flow chat link tokens', () => { expect.anything(), ); }); + + it('validates the core theme schema instead of only root fields', () => { + const service = new ThemeService(); + const invalidTheme: ThemeConfig = { + ...bitfunLightTheme, + id: 'custom-invalid-semantic', + name: 'Invalid Semantic', + colors: { + ...bitfunLightTheme.colors, + semantic: { + ...bitfunLightTheme.colors.semantic, + success: 'not-a-color', + }, + }, + }; + + const result = service.validateTheme(invalidTheme); + + expect(result.valid).toBe(false); + expectThemeError(result, 'colors.semantic.success', 'INVALID_COLOR_FORMAT'); + + const incompleteTheme = { + ...bitfunLightTheme, + id: 'custom-incomplete', + name: 'Incomplete Custom', + effects: undefined, + motion: undefined, + typography: undefined, + } as unknown as ThemeConfig; + const incompleteResult = service.validateTheme(incompleteTheme); + + expect(incompleteResult.valid).toBe(false); + expectThemeError(incompleteResult, 'effects', 'MISSING_THEME_FIELD_GROUP'); + expectThemeError(incompleteResult, 'motion', 'MISSING_THEME_FIELD_GROUP'); + expectThemeError(incompleteResult, 'typography', 'MISSING_THEME_FIELD_GROUP'); + + const invalidOptionalTheme = { + ...bitfunLightTheme, + id: 'custom-invalid-optional-scrollbar', + name: 'Invalid Optional Scrollbar', + colors: { + ...bitfunLightTheme.colors, + scrollbar: { + thumb: 'invalid', + thumbHover: '#ffffff', + }, + }, + } as unknown as ThemeConfig; + const invalidOptionalResult = service.validateTheme(invalidOptionalTheme); + + expect(invalidOptionalResult.valid).toBe(false); + expectThemeError(invalidOptionalResult, 'colors.scrollbar.thumb', 'INVALID_COLOR_FORMAT'); + }); + + it('normalizes older partial custom themes before applying them', async () => { + const partialCustomTheme = { + id: 'custom-partial', + name: 'Partial Custom', + type: 'light', + colors: { + background: { + primary: '#101820', + }, + text: { + primary: '#f8fafc', + }, + accent: { + 500: '#2f80ed', + }, + }, + } as unknown as ThemeConfig; + vi.mocked(configAPI.getConfig).mockImplementation(async (key: string) => { + if (key === 'themes.current') { + return 'custom-partial'; + } + if (key === 'themes') { + return { custom: [partialCustomTheme] }; + } + return undefined; + }); + const service = new ThemeService(); + + await service.initialize(); + + const normalized = service.getTheme('custom-partial'); + expect(service.getCurrentThemeId()).toBe('custom-partial'); + expect(service.getResolvedThemeId()).toBe('custom-partial'); + expect(normalized?.colors.background.primary).toBe('#101820'); + expect(normalized?.colors.background.secondary).toBe(bitfunLightTheme.colors.background.secondary); + expect(normalized?.colors.text.primary).toBe('#f8fafc'); + expect(normalized?.colors.text.secondary).toBe(bitfunLightTheme.colors.text.secondary); + expect(normalized?.effects.spacing[4]).toBe(bitfunLightTheme.effects.spacing[4]); + expect(document.documentElement.style.getPropertyValue('--color-bg-primary')).toBe('#101820'); + expect(document.documentElement.style.getPropertyValue('--color-bg-secondary')).toBe( + bitfunLightTheme.colors.background.secondary, + ); + expect(configAPI.setConfig).not.toHaveBeenCalledWith('themes.custom', expect.anything()); + }); + + it('does not export non-contract dynamic keys from custom themes', () => { + const service = new ThemeService(); + const customTheme = { + ...bitfunLightTheme, + id: 'custom-extra-keys', + colors: { + ...bitfunLightTheme.colors, + accent: { + ...bitfunLightTheme.colors.accent, + 950: '#111111', + }, + purple: { + ...bitfunLightTheme.colors.purple, + 300: '#222222', + 700: '#333333', + }, + }, + effects: { + ...bitfunLightTheme.effects, + shadow: { + ...bitfunLightTheme.effects.shadow, + '2xl': '0 0 0 #111111', + }, + blur: { + ...bitfunLightTheme.effects.blur, + intense: 'blur(99px)', + }, + radius: { + ...bitfunLightTheme.effects.radius, + huge: '99px', + }, + spacing: { + ...bitfunLightTheme.effects.spacing, + 99: '99px', + }, + }, + motion: { + ...bitfunLightTheme.motion, + duration: { + ...bitfunLightTheme.motion.duration, + lazy: '99s', + }, + easing: { + ...bitfunLightTheme.motion.easing, + bounce: 'cubic-bezier(0.68, -0.55, 0.265, 1.55)', + }, + }, + typography: { + ...bitfunLightTheme.typography, + weight: { + ...bitfunLightTheme.typography.weight, + black: 900, + }, + size: { + ...bitfunLightTheme.typography.size, + '5xl': '99px', + }, + lineHeight: { + ...bitfunLightTheme.typography.lineHeight, + loose: 2, + }, + }, + } as unknown as ThemeConfig; + + (service as unknown as { injectCSSVariables(theme: ThemeConfig): void }).injectCSSVariables(customTheme); + + const rootStyle = document.documentElement.style; + expect(rootStyle.getPropertyValue('--color-accent-950')).toBe(''); + expect(rootStyle.getPropertyValue('--color-purple-300')).toBe(''); + expect(rootStyle.getPropertyValue('--color-purple-700')).toBe(''); + expect(rootStyle.getPropertyValue('--shadow-2xl')).toBe(''); + expect(rootStyle.getPropertyValue('--blur-intense')).toBe(''); + expect(rootStyle.getPropertyValue('--size-radius-huge')).toBe(''); + expect(rootStyle.getPropertyValue('--size-gap-99')).toBe(''); + expect(rootStyle.getPropertyValue('--motion-lazy')).toBe(''); + expect(rootStyle.getPropertyValue('--easing-bounce')).toBe(''); + expect(rootStyle.getPropertyValue('--font-weight-black')).toBe(''); + expect(rootStyle.getPropertyValue('--font-size-5xl')).toBe(''); + expect(rootStyle.getPropertyValue('--line-height-loose')).toBe(''); + }); + + it('skips invalid persisted custom themes before they reach preview or runtime injection', async () => { + const invalidCustomTheme = { + ...bitfunLightTheme, + id: 'custom-broken', + name: 'Broken Custom', + colors: { + ...bitfunLightTheme.colors, + background: { + ...bitfunLightTheme.colors.background, + primary: 'definitely-not-a-color', + }, + }, + }; + vi.mocked(configAPI.getConfig).mockImplementation(async (key: string) => { + if (key === 'themes.current') { + return 'custom-broken'; + } + if (key === 'themes') { + return { custom: [invalidCustomTheme] }; + } + return undefined; + }); + const service = new ThemeService(); + + await service.initialize(); + + expect(service.getTheme('custom-broken')).toBeUndefined(); + expect(service.getCurrentThemeId()).toBe(SYSTEM_THEME_ID); + expect(document.documentElement.getAttribute('data-theme')).not.toBe('custom-broken'); + expect(configAPI.setConfig).not.toHaveBeenCalledWith('themes.custom', expect.anything()); + }); + + it('persists registered custom themes only after schema normalization succeeds', async () => { + const service = new ThemeService(); + const partialCustomTheme = { + id: 'custom-registered', + name: 'Registered Custom', + type: 'dark', + colors: { + background: { + primary: '#04080f', + }, + text: { + primary: '#f8fafc', + }, + accent: { + 500: '#7c3aed', + }, + }, + } as unknown as ThemeConfig; + + await service.registerTheme(partialCustomTheme); + + const normalized = service.getTheme('custom-registered'); + expect(normalized?.colors.background.primary).toBe('#04080f'); + expect(normalized?.colors.background.secondary).toBe(bitfunDarkTheme.colors.background.secondary); + expect(normalized?.effects.radius.base).toBe(bitfunDarkTheme.effects.radius.base); + expect(configAPI.setConfig).toHaveBeenCalledWith( + 'themes.custom', + expect.arrayContaining([ + expect.objectContaining({ + id: 'custom-registered', + colors: expect.objectContaining({ + background: expect.objectContaining({ + primary: '#04080f', + secondary: bitfunDarkTheme.colors.background.secondary, + }), + }), + }), + ]), + ); + + await expect( + service.registerTheme({ + ...bitfunLightTheme, + id: 'custom-invalid-register', + colors: { + ...bitfunLightTheme.colors, + text: { + ...bitfunLightTheme.colors.text, + primary: 'invalid', + }, + }, + }), + ).rejects.toThrow(/Invalid theme/); + expect(service.getTheme('custom-invalid-register')).toBeUndefined(); + + await expect( + service.registerTheme({ + ...bitfunLightTheme, + id: '', + name: '', + }), + ).rejects.toThrow(/Theme id cannot be empty/); + + await expect( + service.registerTheme({ + ...bitfunLightTheme, + name: 'Builtin Override', + }), + ).rejects.toThrow(/reserved for a built-in theme/); + }); + + it('projects normalized custom themes through the compact plugin color boundary', async () => { + const service = new ThemeService(); + const partialCustomTheme = { + id: 'custom-plugin-projection', + name: 'Plugin Projection', + type: 'dark', + colors: { + accent: { + 500: '#14b8a6', + 600: '#0f766e', + }, + purple: { + 500: '#a855f7', + }, + semantic: { + success: '#22c55e', + warning: '#f59e0b', + error: '#ef4444', + info: '#38bdf8', + }, + }, + } as unknown as ThemeConfig; + + await service.registerTheme(partialCustomTheme); + + const normalized = service.getTheme('custom-plugin-projection'); + expect(normalized).toBeDefined(); + const projection = createPluginThemeColorProjection(normalized!); + + expect(Object.keys(projection).sort()).toEqual([...PLUGIN_THEME_COLOR_KEYS].sort()); + expect(projection.primary).toBe('#14b8a6'); + expect(projection.secondary).toBe('#a855f7'); + expect(projection.accent).toBe('#0f766e'); + expect(projection.success).toBe('#22c55e'); + expect(projection.warning).toBe('#f59e0b'); + expect(projection.error).toBe('#ef4444'); + expect(projection.info).toBe('#38bdf8'); + }); }); diff --git a/src/web-ui/src/infrastructure/theme/core/ThemeService.ts b/src/web-ui/src/infrastructure/theme/core/ThemeService.ts index 2a4ad58b20..a04022ad45 100644 --- a/src/web-ui/src/infrastructure/theme/core/ThemeService.ts +++ b/src/web-ui/src/infrastructure/theme/core/ThemeService.ts @@ -15,6 +15,7 @@ import { } from '../types'; import { builtinThemes, getSystemPreferredDefaultThemeId } from '../presets'; import { configAPI, workspaceAPI } from '@/infrastructure/api'; +import { themeValidator } from '../utils/ThemeValidator'; import { monacoThemeSync } from '../integrations/MonacoThemeSync'; import { createLogger } from '@/shared/utils/logger'; @@ -31,6 +32,58 @@ const FLOW_CHAT_LINK_COLORS = { }, } as const; +const THEME_STATIC_COLORS = { + white: '#ffffff', + black: '#000000', +} as const; + +const ACCENT_STOPS = [50, 100, 200, 300, 400, 500, 600, 700, 800] as const; +const SECONDARY_ACCENT_STOPS = [50, 100, 200, 400, 500, 600, 800] as const; +const SHADOW_TOKENS = ['xs', 'sm', 'base', 'lg', 'xl'] as const; +const BLUR_TOKENS = ['subtle', 'base'] as const; +const RADIUS_TOKENS = ['sm', 'base', 'lg', 'xl', '2xl', 'full'] as const; +const SPACING_TOKENS = [1, 2, 3, 4, 5, 6, 8, 10, 12, 16] as const; +const MOTION_DURATION_TOKENS = ['instant', 'fast', 'base', 'slow'] as const; +const EASING_TOKENS = ['standard', 'decelerate', 'smooth'] as const; +const FONT_WEIGHT_TOKENS = ['normal', 'medium', 'semibold', 'bold'] as const; +const FONT_SIZE_TOKENS = ['xs', 'sm', 'base', 'lg', 'xl', '2xl', '3xl', '4xl'] as const; +const LINE_HEIGHT_TOKENS = ['tight', 'base', 'relaxed'] as const; + +const THEME_OVERLAYS = { + white04: 'rgba(255, 255, 255, 0.04)', + white08: 'rgba(255, 255, 255, 0.08)', + white12: 'rgba(255, 255, 255, 0.12)', + white15: 'rgba(255, 255, 255, 0.15)', + white20: 'rgba(255, 255, 255, 0.2)', + white24: 'rgba(255, 255, 255, 0.24)', + white60: 'rgba(255, 255, 255, 0.6)', + black08: 'rgba(0, 0, 0, 0.08)', + black12: 'rgba(0, 0, 0, 0.12)', + black15: 'rgba(0, 0, 0, 0.15)', + black20: 'rgba(0, 0, 0, 0.2)', + black30: 'rgba(0, 0, 0, 0.3)', + black40: 'rgba(0, 0, 0, 0.4)', + black50: 'rgba(0, 0, 0, 0.5)', + black80: 'rgba(0, 0, 0, 0.8)', +} as const; + +const THEME_OVERLAY_TOKEN_VALUES = [ + ['--color-overlay-white-04', THEME_OVERLAYS.white04], + ['--color-overlay-white-08', THEME_OVERLAYS.white08], + ['--color-overlay-white-12', THEME_OVERLAYS.white12], + ['--color-overlay-white-15', THEME_OVERLAYS.white15], + ['--color-overlay-white-20', THEME_OVERLAYS.white20], + ['--color-overlay-white-60', THEME_OVERLAYS.white60], + ['--color-overlay-black-08', THEME_OVERLAYS.black08], + ['--color-overlay-black-12', THEME_OVERLAYS.black12], + ['--color-overlay-black-15', THEME_OVERLAYS.black15], + ['--color-overlay-black-20', THEME_OVERLAYS.black20], + ['--color-overlay-black-30', THEME_OVERLAYS.black30], + ['--color-overlay-black-40', THEME_OVERLAYS.black40], + ['--color-overlay-black-50', THEME_OVERLAYS.black50], + ['--color-overlay-black-80', THEME_OVERLAYS.black80], +] as const; + declare global { // Injected by the desktop webview initialization script. These values let the // first renderer pass apply the persisted built-in theme without waiting on a @@ -39,7 +92,7 @@ declare global { var __BITFUN_BOOTSTRAP_THEME_SELECTION__: string | undefined; } -/** Space-separated R G B for `rgba(var(--color-primary-rgb) / alpha)` in component styles. */ +/** Space-separated R G B channels for accent alpha composition in component styles. */ function accentColorToRgbChannels(accent: string): string | null { const trimmed = accent.trim(); const hex6 = /^#([0-9a-f]{6})$/i.exec(trimmed); @@ -54,6 +107,43 @@ function accentColorToRgbChannels(accent: string): string | null { return null; } +function colorWithAlpha(color: string, alpha: number): string { + const channels = accentColorToRgbChannels(color); + if (channels) { + return `rgba(${channels.replace(/\s+/g, ', ')}, ${alpha})`; + } + const percent = `${Math.round(alpha * 1000) / 10}%`; + return `color-mix(in srgb, ${color} ${percent}, transparent)`; +} + +function cloneThemeConfig(theme: ThemeConfig): ThemeConfig { + return JSON.parse(JSON.stringify(theme)) as ThemeConfig; +} + +function mergeThemeConfig(base: ThemeConfig, override: Partial): ThemeConfig { + const mergeValue = (baseValue: unknown, overrideValue: unknown): unknown => { + if (overrideValue === undefined || overrideValue === null) { + return baseValue; + } + if (Array.isArray(baseValue) || Array.isArray(overrideValue)) { + return overrideValue; + } + if ( + typeof baseValue === 'object' && baseValue !== null && + typeof overrideValue === 'object' && overrideValue !== null + ) { + const merged: Record = { ...(baseValue as Record) }; + Object.entries(overrideValue as Record).forEach(([key, value]) => { + merged[key] = mergeValue(merged[key], value); + }); + return merged; + } + return overrideValue; + }; + + return mergeValue(cloneThemeConfig(base), override) as ThemeConfig; +} + export class ThemeService { private themes: Map = new Map(); @@ -187,10 +277,20 @@ export class ThemeService { const themes = themesConfig?.custom; if (Array.isArray(themes) && themes.length > 0) { + let loadedCount = 0; themes.forEach(theme => { - this.themes.set(theme.id, theme); + try { + const normalizedTheme = this.normalizeCustomTheme(theme); + this.themes.set(normalizedTheme.id, normalizedTheme); + loadedCount += 1; + } catch (error) { + log.warn('Skipped invalid user theme', { + id: theme?.id, + error: error instanceof Error ? error.message : String(error), + }); + } }); - log.info('Loaded user themes', { count: themes.length }); + log.info('Loaded user themes', { count: loadedCount, skipped: themes.length - loadedCount }); } } catch (_error) { @@ -217,18 +317,56 @@ export class ThemeService { - registerTheme(theme: ThemeConfig): void { + private normalizeCustomTheme(theme: ThemeConfig): ThemeConfig { + if (!theme || typeof theme !== 'object') { + throw new Error('Invalid theme: expected object'); + } + if (!theme.id || theme.id.trim() === '') { + throw new Error('Theme id cannot be empty'); + } + if (!theme.name || theme.name.trim() === '') { + throw new Error(`Invalid theme ${theme.id}: theme name cannot be empty`); + } if (theme.id === SYSTEM_THEME_ID) { log.error('Reserved theme id', { id: theme.id }); throw new Error(`Theme id "${SYSTEM_THEME_ID}" is reserved`); } + if (builtinThemes.some(item => item.id === theme.id)) { + log.error('Reserved builtin theme id', { id: theme.id }); + throw new Error(`Theme id "${theme.id}" is reserved for a built-in theme`); + } + if (!theme.type || !['dark', 'light'].includes(theme.type)) { + throw new Error(`Invalid theme ${theme.id || ''}: theme type must be "dark" or "light"`); + } + + const baseTheme = theme.type === 'light' + ? builtinThemes.find(item => item.type === 'light') || builtinThemes[0] + : builtinThemes.find(item => item.id === 'bitfun-dark') || builtinThemes.find(item => item.type === 'dark') || builtinThemes[0]; + const normalized = mergeThemeConfig(baseTheme, theme); + const validation = this.validateTheme(normalized); + + if (!validation.valid) { + const detail = validation.errors + .slice(0, 3) + .map(error => `${error.path}: ${error.message}`) + .join('; '); + throw new Error(`Invalid theme ${theme.id || ''}: ${detail}`); + } + + return normalized; + } + + + async registerTheme(theme: ThemeConfig): Promise { + const normalizedTheme = this.normalizeCustomTheme(theme); if (this.themes.has(theme.id)) { log.warn('Theme already exists, will override', { id: theme.id }); } - this.themes.set(theme.id, theme); - this.emitEvent('theme:register', theme.id, theme); - log.info('Theme registered', { id: theme.id, name: theme.name }); + this.themes.set(normalizedTheme.id, normalizedTheme); + this.emitEvent('theme:register', normalizedTheme.id, normalizedTheme); + log.info('Theme registered', { id: normalizedTheme.id, name: normalizedTheme.name }); + await this.saveUserThemes(); } @@ -409,34 +547,11 @@ export class ThemeService { root.style.setProperty('--color-bg-primary', colors.background.primary); - root.style.setProperty('--color-static-white', '#ffffff'); - root.style.setProperty('--color-static-black', '#000000'); + root.style.setProperty('--color-static-white', THEME_STATIC_COLORS.white); + root.style.setProperty('--color-static-black', THEME_STATIC_COLORS.black); root.style.setProperty('--color-static-white-rgb', '255, 255, 255'); root.style.setProperty('--color-static-black-rgb', '0, 0, 0'); - [ - ['--color-overlay-white-02', 'rgba(255, 255, 255, 0.02)'], - ['--color-overlay-white-03', 'rgba(255, 255, 255, 0.03)'], - ['--color-overlay-white-04', 'rgba(255, 255, 255, 0.04)'], - ['--color-overlay-white-05', 'rgba(255, 255, 255, 0.05)'], - ['--color-overlay-white-06', 'rgba(255, 255, 255, 0.06)'], - ['--color-overlay-white-08', 'rgba(255, 255, 255, 0.08)'], - ['--color-overlay-white-10', 'rgba(255, 255, 255, 0.1)'], - ['--color-overlay-white-12', 'rgba(255, 255, 255, 0.12)'], - ['--color-overlay-white-15', 'rgba(255, 255, 255, 0.15)'], - ['--color-overlay-white-20', 'rgba(255, 255, 255, 0.2)'], - ['--color-overlay-white-60', 'rgba(255, 255, 255, 0.6)'], - ['--color-overlay-black-06', 'rgba(0, 0, 0, 0.06)'], - ['--color-overlay-black-08', 'rgba(0, 0, 0, 0.08)'], - ['--color-overlay-black-10', 'rgba(0, 0, 0, 0.1)'], - ['--color-overlay-black-12', 'rgba(0, 0, 0, 0.12)'], - ['--color-overlay-black-15', 'rgba(0, 0, 0, 0.15)'], - ['--color-overlay-black-20', 'rgba(0, 0, 0, 0.2)'], - ['--color-overlay-black-25', 'rgba(0, 0, 0, 0.25)'], - ['--color-overlay-black-30', 'rgba(0, 0, 0, 0.3)'], - ['--color-overlay-black-40', 'rgba(0, 0, 0, 0.4)'], - ['--color-overlay-black-50', 'rgba(0, 0, 0, 0.5)'], - ['--color-overlay-black-80', 'rgba(0, 0, 0, 0.8)'], - ].forEach(([name, value]) => { + THEME_OVERLAY_TOKEN_VALUES.forEach(([name, value]) => { root.style.setProperty(name, value); }); root.style.setProperty('--color-bg-secondary', colors.background.secondary); @@ -445,74 +560,37 @@ export class ThemeService { root.style.setProperty('--color-bg-elevated', colors.background.elevated); root.style.setProperty('--color-bg-workbench', colors.background.workbench); root.style.setProperty('--color-bg-scene', colors.background.scene); - root.style.setProperty('--color-bg-flowchat', colors.background.scene); - root.style.setProperty('--color-bg-surface', colors.background.secondary); - root.style.setProperty('--color-bg-base', colors.background.primary); - root.style.setProperty('--color-bg-elevated-hover', colors.element.medium); - root.style.setProperty('--color-surface-elevated', colors.element.elevated); - root.style.setProperty('--color-surface-hover', colors.element.medium); - root.style.setProperty('--color-hover', colors.element.medium); - root.style.setProperty('--bg-primary', colors.background.primary); - root.style.setProperty('--bg-secondary', colors.background.secondary); - root.style.setProperty('--bg-tertiary', colors.background.tertiary); - root.style.setProperty('--bg-elevated', colors.background.elevated); - root.style.setProperty('--bg-hover', colors.element.medium); - root.style.setProperty('--secondary-bg', colors.background.secondary); - root.style.setProperty('--background-primary', colors.background.primary); - root.style.setProperty('--background-secondary', colors.background.secondary); - root.style.setProperty('--background-tertiary', colors.background.tertiary); - root.style.setProperty('--color-background-secondary', colors.background.secondary); - root.style.setProperty('--color-background-tertiary', colors.background.tertiary); if (colors.background.tooltip) { root.style.setProperty('--color-bg-tooltip', colors.background.tooltip); } - root.style.setProperty('--color-overlay', theme.type === 'dark' ? 'rgba(0, 0, 0, 0.5)' : 'rgba(0, 0, 0, 0.3)'); - - root.style.setProperty('--color-text-primary', colors.text.primary); root.style.setProperty('--color-text-secondary', colors.text.secondary); - root.style.setProperty('--color-text-tertiary', colors.text.muted); root.style.setProperty('--color-text-muted', colors.text.muted); root.style.setProperty('--color-text-disabled', colors.text.disabled); - root.style.setProperty('--text-primary', colors.text.primary); - root.style.setProperty('--text-secondary', colors.text.secondary); - root.style.setProperty('--text-tertiary', colors.text.muted); - root.style.setProperty('--text-muted', colors.text.muted); - root.style.setProperty('--text-disabled', colors.text.disabled); - Object.entries(colors.accent).forEach(([key, value]) => { + ACCENT_STOPS.forEach((key) => { + const value = colors.accent[key]; root.style.setProperty(`--color-accent-${key}`, value); }); const primaryAccent = colors.accent[500]; - const primaryHover = colors.accent[600]; - root.style.setProperty('--color-primary', primaryAccent); - root.style.setProperty('--color-primary-hover', primaryHover); - root.style.setProperty('--color-accent', primaryAccent); - root.style.setProperty('--color-accent-primary', primaryAccent); - root.style.setProperty('--accent-primary', primaryAccent); - root.style.setProperty('--accent-primary-hover', primaryHover); - root.style.setProperty('--color-primary-400', colors.accent[400]); - root.style.setProperty('--color-primary-500', primaryAccent); - root.style.setProperty('--color-primary-alpha', colors.accent[100]); - root.style.setProperty('--color-primary-bg', colors.accent[100]); - root.style.setProperty('--color-primary-bg-subtle', colors.accent[50]); - root.style.setProperty('--color-accent-alpha', colors.accent[100]); const flowChatLinkColors = theme.type === 'light' ? FLOW_CHAT_LINK_COLORS.light : FLOW_CHAT_LINK_COLORS.dark; root.style.setProperty('--flowchat-link-color', flowChatLinkColors.default); root.style.setProperty('--flowchat-link-hover-color', flowChatLinkColors.hover); - const primaryRgb = accentColorToRgbChannels(primaryAccent); - if (primaryRgb) { - root.style.setProperty('--color-primary-rgb', primaryRgb); + const accentRgb = accentColorToRgbChannels(primaryAccent); + if (accentRgb) { + root.style.setProperty('--color-accent-500-rgb', accentRgb); } - if (colors.purple) { - Object.entries(colors.purple).forEach(([key, value]) => { + const secondaryAccent = colors.purple; + if (secondaryAccent) { + SECONDARY_ACCENT_STOPS.forEach((key) => { + const value = secondaryAccent[key]; root.style.setProperty(`--color-purple-${key}`, value); }); } @@ -521,43 +599,22 @@ export class ThemeService { root.style.setProperty('--color-success', colors.semantic.success); root.style.setProperty('--color-success-bg', colors.semantic.successBg); root.style.setProperty('--color-success-border', colors.semantic.successBorder); - root.style.setProperty('--color-success-100', colors.semantic.successBg); - root.style.setProperty('--color-success-500', colors.semantic.success); root.style.setProperty('--color-warning', colors.semantic.warning); root.style.setProperty('--color-warning-bg', colors.semantic.warningBg); root.style.setProperty('--color-warning-border', colors.semantic.warningBorder); - root.style.setProperty('--color-warning-100', colors.semantic.warningBg); - root.style.setProperty('--color-warning-500', colors.semantic.warning); - root.style.setProperty('--color-warning-700', colors.semantic.warning); root.style.setProperty('--color-error', colors.semantic.error); root.style.setProperty('--color-error-bg', colors.semantic.errorBg); root.style.setProperty('--color-error-border', colors.semantic.errorBorder); - root.style.setProperty('--color-semantic-error', colors.semantic.error); - root.style.setProperty('--color-danger', colors.semantic.error); - root.style.setProperty('--color-danger-500', colors.semantic.error); - root.style.setProperty('--color-danger-text', colors.semantic.error); - root.style.setProperty('--color-danger-bg', colors.semantic.errorBg); - root.style.setProperty('--color-danger-border', colors.semantic.errorBorder); - root.style.setProperty('--color-danger-hover', colors.semantic.error); root.style.setProperty('--color-info', colors.semantic.info); root.style.setProperty('--color-info-bg', colors.semantic.infoBg); root.style.setProperty('--color-info-border', colors.semantic.infoBorder); - root.style.setProperty('--color-highlight', colors.semantic.highlight); - root.style.setProperty('--color-highlight-bg', colors.semantic.highlightBg); root.style.setProperty('--border-subtle', colors.border.subtle); - root.style.setProperty('--border-color', colors.border.subtle); root.style.setProperty('--border-base', colors.border.base); root.style.setProperty('--border-medium', colors.border.medium); - root.style.setProperty('--border-hover', colors.border.medium); root.style.setProperty('--border-strong', colors.border.strong); root.style.setProperty('--border-prominent', colors.border.prominent); - root.style.setProperty('--border-muted', colors.border.subtle); - root.style.setProperty('--border-primary', colors.border.base); - root.style.setProperty('--color-border', colors.border.base); - root.style.setProperty('--color-border-primary', colors.border.base); - root.style.setProperty('--color-border-subtle', colors.border.subtle); const sceneViewportBorder = theme.layout?.sceneViewportBorder ?? true; root.style.setProperty( @@ -571,83 +628,83 @@ export class ThemeService { root.style.setProperty('--element-bg-medium', colors.element.medium); root.style.setProperty('--element-bg-strong', colors.element.strong); root.style.setProperty('--element-bg-elevated', colors.element.elevated); - root.style.setProperty('--element-bg', colors.element.base); root.style.setProperty('--element-bg-hover', colors.element.medium); - root.style.setProperty('--color-bg-hover', colors.element.medium); - root.style.setProperty('--color-bg-subtle', colors.element.subtle); root.style.setProperty('--git-color-branch', colors.git.branch); root.style.setProperty('--git-color-branch-bg', colors.git.branchBg); + root.style.setProperty('--git-color-branch-bg-hover', colors.element.medium); root.style.setProperty('--git-color-changes', colors.git.changes); root.style.setProperty('--git-color-changes-bg', colors.git.changesBg); root.style.setProperty('--git-color-added', colors.git.added); root.style.setProperty('--git-color-added-bg', colors.git.addedBg); + root.style.setProperty('--git-color-added-bg-hover', colorWithAlpha(colors.git.added, 0.15)); root.style.setProperty('--git-color-deleted', colors.git.deleted); root.style.setProperty('--git-color-deleted-bg', colors.git.deletedBg); + root.style.setProperty('--git-color-deleted-bg-hover', colorWithAlpha(colors.git.deleted, 0.15)); root.style.setProperty('--git-color-staged', colors.git.staged); root.style.setProperty('--git-color-staged-bg', colors.git.stagedBg); + root.style.setProperty('--git-color-staged-bg-hover', colorWithAlpha(colors.git.staged, 0.15)); + root.style.setProperty('--git-color-staged-border', colorWithAlpha(colors.git.staged, 0.3)); const scrollbarThumb = colors.scrollbar?.thumb ?? ( theme.type === 'dark' - ? 'rgba(255, 255, 255, 0.12)' - : 'rgba(0, 0, 0, 0.15)' + ? THEME_OVERLAYS.white12 + : THEME_OVERLAYS.black15 ); const scrollbarThumbHover = colors.scrollbar?.thumbHover ?? ( theme.type === 'dark' - ? 'rgba(255, 255, 255, 0.22)' - : 'rgba(0, 0, 0, 0.28)' + ? THEME_OVERLAYS.white24 + : THEME_OVERLAYS.black30 ); root.style.setProperty('--scrollbar-thumb', scrollbarThumb); root.style.setProperty('--scrollbar-thumb-hover', scrollbarThumbHover); root.style.setProperty('--color-scrollbar', scrollbarThumb); - if (effects?.shadow) { - Object.entries(effects.shadow).forEach(([key, value]) => { + const shadows = effects?.shadow; + if (shadows) { + SHADOW_TOKENS.forEach((key) => { + const value = shadows[key]; root.style.setProperty(`--shadow-${key}`, value); }); - root.style.setProperty('--glass-shadow-sm', effects.shadow.sm); - root.style.setProperty('--glass-shadow-base', effects.shadow.base); - root.style.setProperty('--glass-shadow-lg', effects.shadow.lg); - root.style.setProperty('--glass-shadow-xl', effects.shadow.xl); + root.style.setProperty('--glass-shadow-sm', shadows.sm); + root.style.setProperty('--glass-shadow-base', shadows.base); + root.style.setProperty('--glass-shadow-lg', shadows.lg); + root.style.setProperty('--glass-shadow-xl', shadows.xl); } - if (effects?.glow) { - root.style.setProperty('--glow-blue', effects.glow.blue); - root.style.setProperty('--glow-purple', effects.glow.purple); - root.style.setProperty('--glow-mixed', effects.glow.mixed); - } - - - if (effects?.blur) { - Object.entries(effects.blur).forEach(([key, value]) => { + const blurs = effects?.blur; + if (blurs) { + BLUR_TOKENS.forEach((key) => { + const value = blurs[key]; root.style.setProperty(`--blur-${key}`, value); }); - root.style.setProperty('--glass-blur-sm', effects.blur.subtle); - root.style.setProperty('--glass-blur-base', effects.blur.base); + root.style.setProperty('--glass-blur-sm', blurs.subtle); + root.style.setProperty('--glass-blur-base', blurs.base); } - if (effects?.radius) { - Object.entries(effects.radius).forEach(([key, value]) => { - root.style.setProperty(`--radius-${key}`, value); + const radii = effects?.radius; + if (radii) { + RADIUS_TOKENS.forEach((key) => { + const value = radii[key]; root.style.setProperty(`--size-radius-${key}`, value); }); - if (effects.radius.base) { - root.style.setProperty('--radius-md', effects.radius.base); - root.style.setProperty('--size-radius-md', effects.radius.base); + if (radii.base) { + root.style.setProperty('--size-radius-md', radii.base); } } - if (effects?.spacing) { - Object.entries(effects.spacing).forEach(([key, value]) => { - root.style.setProperty(`--spacing-${key}`, value); + const spacing = effects?.spacing; + if (spacing) { + SPACING_TOKENS.forEach((key) => { + const value = spacing[key]; root.style.setProperty(`--size-gap-${key}`, value); }); } @@ -657,48 +714,55 @@ export class ThemeService { root.style.setProperty('--opacity-disabled', String(effects.opacity.disabled)); root.style.setProperty('--opacity-hover', String(effects.opacity.hover)); root.style.setProperty('--opacity-focus', String(effects.opacity.focus)); - root.style.setProperty('--opacity-overlay', String(effects.opacity.overlay)); } - if (motion?.duration) { - Object.entries(motion.duration).forEach(([key, value]) => { + const motionDuration = motion?.duration; + if (motionDuration) { + MOTION_DURATION_TOKENS.forEach((key) => { + const value = motionDuration[key]; root.style.setProperty(`--motion-${key}`, value); }); - root.style.setProperty('--motion-normal', motion.duration.base); } - if (motion?.easing) { - Object.entries(motion.easing).forEach(([key, value]) => { + const motionEasing = motion?.easing; + if (motionEasing) { + EASING_TOKENS.forEach((key) => { + const value = motionEasing[key]; root.style.setProperty(`--easing-${key}`, value); }); } if (typography?.font) { - root.style.setProperty('--font-sans', typography.font.sans); - root.style.setProperty('--font-mono', typography.font.mono); - root.style.setProperty('--markdown-font-mono', typography.font.mono); + root.style.setProperty('--font-family-sans', typography.font.sans); + root.style.setProperty('--font-family-mono', typography.font.mono); } - if (typography?.weight) { - Object.entries(typography.weight).forEach(([key, value]) => { + const fontWeights = typography?.weight; + if (fontWeights) { + FONT_WEIGHT_TOKENS.forEach((key) => { + const value = fontWeights[key]; root.style.setProperty(`--font-weight-${key}`, String(value)); }); } - if (typography?.size) { - Object.entries(typography.size).forEach(([key, value]) => { + const typographySize = typography?.size; + if (typographySize) { + FONT_SIZE_TOKENS.forEach((key) => { + const value = typographySize[key]; root.style.setProperty(`--font-size-${key}`, value); }); } - if (typography?.lineHeight) { - Object.entries(typography.lineHeight).forEach(([key, value]) => { + const lineHeights = typography?.lineHeight; + if (lineHeights) { + LINE_HEIGHT_TOKENS.forEach((key) => { + const value = lineHeights[key]; root.style.setProperty(`--line-height-${key}`, String(value)); }); } @@ -710,24 +774,6 @@ export class ThemeService { const buttonConfig = theme.components?.button; if (buttonConfig) { - root.style.setProperty('--btn-default-bg', buttonConfig.default.background); - root.style.setProperty('--btn-default-color', buttonConfig.default.color); - root.style.setProperty('--btn-default-border', buttonConfig.default.border); - root.style.setProperty('--btn-default-shadow', buttonConfig.default.shadow || 'none'); - - root.style.setProperty('--btn-default-hover-bg', buttonConfig.hover.background); - root.style.setProperty('--btn-default-hover-color', buttonConfig.hover.color); - root.style.setProperty('--btn-default-hover-border', buttonConfig.hover.border); - root.style.setProperty('--btn-default-hover-shadow', buttonConfig.hover.shadow || 'none'); - root.style.setProperty('--btn-default-hover-transform', buttonConfig.hover.transform || 'none'); - - root.style.setProperty('--btn-default-active-bg', buttonConfig.active.background); - root.style.setProperty('--btn-default-active-color', buttonConfig.active.color); - root.style.setProperty('--btn-default-active-border', buttonConfig.active.border); - root.style.setProperty('--btn-default-active-shadow', buttonConfig.active.shadow || 'none'); - root.style.setProperty('--btn-default-active-transform', buttonConfig.active.transform || 'none'); - - root.style.setProperty('--btn-primary-bg', buttonConfig.primary.default.background); root.style.setProperty('--btn-primary-color', buttonConfig.primary.default.color); root.style.setProperty('--btn-primary-border', buttonConfig.primary.default.border); @@ -746,34 +792,13 @@ export class ThemeService { root.style.setProperty('--btn-primary-active-transform', buttonConfig.primary.active.transform || 'none'); - root.style.setProperty('--btn-ghost-bg', buttonConfig.ghost.default.background); root.style.setProperty('--btn-ghost-color', buttonConfig.ghost.default.color); - root.style.setProperty('--btn-ghost-border', buttonConfig.ghost.default.border); - root.style.setProperty('--btn-ghost-shadow', buttonConfig.ghost.default.shadow || 'none'); root.style.setProperty('--btn-ghost-hover-bg', buttonConfig.ghost.hover.background); root.style.setProperty('--btn-ghost-hover-color', buttonConfig.ghost.hover.color); root.style.setProperty('--btn-ghost-hover-border', buttonConfig.ghost.hover.border); - root.style.setProperty('--btn-ghost-hover-shadow', buttonConfig.ghost.hover.shadow || 'none'); - root.style.setProperty('--btn-ghost-hover-transform', buttonConfig.ghost.hover.transform || 'none'); - - root.style.setProperty('--btn-ghost-active-bg', buttonConfig.ghost.active.background); - root.style.setProperty('--btn-ghost-active-color', buttonConfig.ghost.active.color); - root.style.setProperty('--btn-ghost-active-border', buttonConfig.ghost.active.border); - root.style.setProperty('--btn-ghost-active-shadow', buttonConfig.ghost.active.shadow || 'none'); - root.style.setProperty('--btn-ghost-active-transform', buttonConfig.ghost.active.transform || 'none'); } else { - root.style.setProperty('--btn-default-bg', colors.element.base); - root.style.setProperty('--btn-default-color', colors.text.secondary); - root.style.setProperty('--btn-default-border', colors.border.base); - root.style.setProperty('--btn-default-shadow', 'none'); - root.style.setProperty('--btn-default-hover-bg', colors.element.medium); - root.style.setProperty('--btn-default-hover-color', colors.text.primary); - root.style.setProperty('--btn-default-hover-border', colors.border.medium); - root.style.setProperty('--btn-default-hover-shadow', 'none'); - root.style.setProperty('--btn-default-hover-transform', 'none'); - const a = colors.accent; root.style.setProperty('--btn-primary-bg', a[200]); root.style.setProperty('--btn-primary-color', a[600]); @@ -789,163 +814,53 @@ export class ThemeService { root.style.setProperty('--btn-primary-active-border', 'transparent'); root.style.setProperty('--btn-primary-active-shadow', 'none'); root.style.setProperty('--btn-primary-active-transform', 'none'); - root.style.setProperty('--btn-ghost-bg', 'transparent'); root.style.setProperty('--btn-ghost-color', colors.text.muted); - root.style.setProperty('--btn-ghost-border', 'transparent'); - root.style.setProperty('--btn-ghost-shadow', 'none'); root.style.setProperty('--btn-ghost-hover-bg', colors.element.subtle); root.style.setProperty('--btn-ghost-hover-color', colors.text.primary); root.style.setProperty('--btn-ghost-hover-border', 'transparent'); - root.style.setProperty('--btn-ghost-hover-shadow', 'none'); - root.style.setProperty('--btn-ghost-hover-transform', 'none'); - root.style.setProperty('--btn-ghost-active-bg', colors.element.medium); - root.style.setProperty('--btn-ghost-active-color', colors.text.primary); - root.style.setProperty('--btn-ghost-active-border', 'transparent'); - root.style.setProperty('--btn-ghost-active-shadow', 'none'); - root.style.setProperty('--btn-ghost-active-transform', 'none'); } - const windowControlsConfig = theme.components?.windowControls; - if (windowControlsConfig) { - - root.style.setProperty('--window-control-minimize-dot', windowControlsConfig.minimize.dot); - root.style.setProperty('--window-control-minimize-dot-shadow', windowControlsConfig.minimize.dotShadow || 'none'); - root.style.setProperty('--window-control-minimize-hover-bg', windowControlsConfig.minimize.hoverBg); - root.style.setProperty('--window-control-minimize-hover-color', windowControlsConfig.minimize.hoverColor); - root.style.setProperty('--window-control-minimize-hover-border', windowControlsConfig.minimize.hoverBorder); - root.style.setProperty('--window-control-minimize-hover-shadow', windowControlsConfig.minimize.hoverShadow || 'none'); - - - root.style.setProperty('--window-control-maximize-dot', windowControlsConfig.maximize.dot); - root.style.setProperty('--window-control-maximize-dot-shadow', windowControlsConfig.maximize.dotShadow || 'none'); - root.style.setProperty('--window-control-maximize-hover-bg', windowControlsConfig.maximize.hoverBg); - root.style.setProperty('--window-control-maximize-hover-color', windowControlsConfig.maximize.hoverColor); - root.style.setProperty('--window-control-maximize-hover-border', windowControlsConfig.maximize.hoverBorder); - root.style.setProperty('--window-control-maximize-hover-shadow', windowControlsConfig.maximize.hoverShadow || 'none'); - - - root.style.setProperty('--window-control-close-dot', windowControlsConfig.close.dot); - root.style.setProperty('--window-control-close-dot-shadow', windowControlsConfig.close.dotShadow || 'none'); - root.style.setProperty('--window-control-close-hover-bg', windowControlsConfig.close.hoverBg); - root.style.setProperty('--window-control-close-hover-color', windowControlsConfig.close.hoverColor); - root.style.setProperty('--window-control-close-hover-border', windowControlsConfig.close.hoverBorder); - root.style.setProperty('--window-control-close-hover-shadow', windowControlsConfig.close.hoverShadow || 'none'); - - - root.style.setProperty('--window-control-default-color', windowControlsConfig.common.defaultColor); - root.style.setProperty('--window-control-default-dot', windowControlsConfig.common.defaultDot); - root.style.setProperty('--window-control-disabled-dot', windowControlsConfig.common.disabledDot); - root.style.setProperty('--window-control-flow-gradient', windowControlsConfig.common.flowGradient || 'none'); - } else { - - root.style.setProperty('--window-control-minimize-dot', colors.accent[400]); - root.style.setProperty('--window-control-minimize-dot-shadow', 'none'); - root.style.setProperty('--window-control-minimize-hover-bg', colors.accent[100]); - root.style.setProperty('--window-control-minimize-hover-color', colors.accent[500]); - root.style.setProperty('--window-control-minimize-hover-border', colors.accent[200]); - root.style.setProperty('--window-control-minimize-hover-shadow', 'none'); - - root.style.setProperty('--window-control-maximize-dot', colors.accent[400]); - root.style.setProperty('--window-control-maximize-dot-shadow', 'none'); - root.style.setProperty('--window-control-maximize-hover-bg', colors.accent[100]); - root.style.setProperty('--window-control-maximize-hover-color', colors.accent[500]); - root.style.setProperty('--window-control-maximize-hover-border', colors.accent[200]); - root.style.setProperty('--window-control-maximize-hover-shadow', 'none'); - - root.style.setProperty('--window-control-close-dot', colors.semantic.error); - root.style.setProperty('--window-control-close-dot-shadow', 'none'); - root.style.setProperty('--window-control-close-hover-bg', colors.semantic.errorBg); - root.style.setProperty('--window-control-close-hover-color', colors.semantic.error); - root.style.setProperty('--window-control-close-hover-border', colors.semantic.errorBorder); - root.style.setProperty('--window-control-close-hover-shadow', 'none'); - - root.style.setProperty('--window-control-default-color', colors.text.primary); - root.style.setProperty('--window-control-default-dot', colors.text.muted); - root.style.setProperty('--window-control-disabled-dot', colors.text.disabled); - root.style.setProperty('--window-control-flow-gradient', 'none'); - } + root.style.setProperty( + '--window-control-close-hover-color', + theme.components?.windowControls?.close.hoverColor ?? colors.semantic.error, + ); root.style.setProperty('--input-bg', colors.element.base); root.style.setProperty('--input-bg-hover', colors.element.medium); - root.style.setProperty('--input-bg-focus', colors.element.soft); - root.style.setProperty('--input-bg-disabled', colors.element.subtle); root.style.setProperty('--input-border', colors.border.base); root.style.setProperty('--input-border-hover', colors.border.medium); root.style.setProperty('--input-border-focus', colors.accent[400]); - root.style.setProperty('--input-border-error', colors.semantic.error); root.style.setProperty('--input-text', colors.text.primary); - root.style.setProperty( - '--input-placeholder', - 'color-mix(in srgb, var(--color-text-muted) 40%, var(--color-bg-primary))' - ); - - - root.style.setProperty('--card-bg', colors.element.base); - root.style.setProperty('--card-bg-hover', colors.element.medium); - root.style.setProperty('--card-bg-active', colors.element.elevated); - root.style.setProperty('--card-border', colors.border.base); - root.style.setProperty('--card-border-hover', colors.border.medium); - root.style.setProperty('--card-border-active', colors.accent[300]); if (theme.type === 'dark') { - root.style.setProperty('--card-bg-default', 'rgba(255, 255, 255, 0.025)'); - root.style.setProperty('--card-bg-elevated', 'rgba(255, 255, 255, 0.035)'); - root.style.setProperty('--card-bg-subtle', 'rgba(255, 255, 255, 0.015)'); - root.style.setProperty('--card-bg-hover', 'rgba(255, 255, 255, 0.04)'); - root.style.setProperty('--card-bg-active', 'rgba(255, 255, 255, 0.05)'); - root.style.setProperty('--card-bg-accent', 'rgba(255, 255, 255, 0.09)'); - root.style.setProperty('--card-bg-accent-hover', 'rgba(255, 255, 255, 0.13)'); + root.style.setProperty('--card-bg-default', THEME_OVERLAYS.white04); + root.style.setProperty('--card-bg-elevated', THEME_OVERLAYS.white08); + root.style.setProperty('--card-bg-subtle', 'transparent'); + root.style.setProperty('--card-bg-hover', THEME_OVERLAYS.white08); + root.style.setProperty('--card-bg-active', THEME_OVERLAYS.white12); + root.style.setProperty('--card-bg-accent', THEME_OVERLAYS.white08); + root.style.setProperty('--card-bg-accent-hover', THEME_OVERLAYS.white12); root.style.setProperty('--card-bg-purple', 'rgba(139, 92, 246, 0.08)'); - root.style.setProperty('--card-bg-purple-hover', 'rgba(139, 92, 246, 0.12)'); + root.style.setProperty('--card-bg-purple-hover', 'rgba(139, 92, 246, 0.15)'); } else { - root.style.setProperty('--card-bg-default', 'rgba(0, 0, 0, 0.06)'); - root.style.setProperty('--card-bg-elevated', 'rgba(0, 0, 0, 0.08)'); - root.style.setProperty('--card-bg-subtle', 'rgba(0, 0, 0, 0.04)'); - root.style.setProperty('--card-bg-hover', 'rgba(0, 0, 0, 0.065)'); - root.style.setProperty('--card-bg-active', 'rgba(0, 0, 0, 0.09)'); + root.style.setProperty('--card-bg-default', THEME_OVERLAYS.black08); + root.style.setProperty('--card-bg-elevated', THEME_OVERLAYS.black12); + root.style.setProperty('--card-bg-subtle', 'transparent'); + root.style.setProperty('--card-bg-hover', THEME_OVERLAYS.black12); + root.style.setProperty('--card-bg-active', THEME_OVERLAYS.black15); root.style.setProperty('--card-bg-accent', 'rgba(15, 23, 42, 0.08)'); root.style.setProperty('--card-bg-accent-hover', 'rgba(15, 23, 42, 0.12)'); root.style.setProperty('--card-bg-purple', 'rgba(124, 58, 237, 0.12)'); - root.style.setProperty('--card-bg-purple-hover', 'rgba(124, 58, 237, 0.18)'); + root.style.setProperty('--card-bg-purple-hover', 'rgba(139, 92, 246, 0.15)'); } - root.style.setProperty('--modal-bg', colors.background.elevated); - root.style.setProperty('--modal-border', colors.border.base); - root.style.setProperty('--modal-overlay', theme.type === 'dark' ? 'rgba(0, 0, 0, 0.7)' : 'rgba(0, 0, 0, 0.5)'); - - - root.style.setProperty('--nav-bg', colors.background.secondary); - root.style.setProperty('--nav-item-bg-hover', colors.element.base); - root.style.setProperty('--nav-item-bg-active', colors.element.medium); - root.style.setProperty('--nav-item-text', colors.text.secondary); - root.style.setProperty('--nav-item-text-active', colors.text.primary); - - root.style.setProperty('--panel-bg', colors.background.primary); - root.style.setProperty('--panel-header-bg', colors.background.secondary); - root.style.setProperty('--panel-border', colors.border.base); - - - root.style.setProperty('--tooltip-bg', colors.background.elevated); - root.style.setProperty('--tooltip-border', colors.border.medium); - root.style.setProperty('--tooltip-text', colors.text.primary); - - - root.style.setProperty('--tool-card-bg-primary', colors.element.base); - root.style.setProperty('--tool-card-bg-secondary', colors.element.soft); - root.style.setProperty('--tool-card-bg-hover', colors.element.medium); - root.style.setProperty('--tool-card-bg-elevated', colors.element.elevated); - root.style.setProperty('--tool-card-border', colors.border.base); - root.style.setProperty('--tool-card-border-subtle', colors.border.subtle); - root.style.setProperty('--tool-card-text-primary', colors.text.primary); - root.style.setProperty('--tool-card-text-secondary', colors.text.secondary); - root.style.setProperty('--tool-card-text-muted', colors.text.muted); root.setAttribute('data-theme', theme.id); @@ -995,6 +910,15 @@ export class ThemeService { return null; } + const validation = this.validateTheme(theme); + if (!validation.valid) { + log.error('Cannot export invalid theme', { + id: themeId, + errors: validation.errors.slice(0, 3), + }); + return null; + } + const metadata: ThemeMetadata = { id: theme.id, name: theme.name, @@ -1007,7 +931,7 @@ export class ThemeService { return { schema: '2.0.0', - theme, + theme: cloneThemeConfig(theme), metadata, exportedAt: new Date().toISOString(), }; @@ -1017,30 +941,7 @@ export class ThemeService { validateTheme(theme: ThemeConfig): ThemeValidationResult { - const errors: ThemeValidationResult['errors'] = []; - const warnings: ThemeValidationResult['warnings'] = []; - - - if (!theme.id) { - errors.push({ path: 'id', message: 'Missing theme id', code: 'MISSING_ID' }); - } - if (!theme.name) { - errors.push({ path: 'name', message: 'Missing theme name', code: 'MISSING_NAME' }); - } - if (!theme.type || !['dark', 'light'].includes(theme.type)) { - errors.push({ path: 'type', message: 'Invalid theme type', code: 'INVALID_TYPE' }); - } - - - if (!theme.colors) { - errors.push({ path: 'colors', message: 'Missing color configuration', code: 'MISSING_COLORS' }); - } - - return { - valid: errors.length === 0, - errors, - warnings, - }; + return themeValidator.validate(theme); } diff --git a/src/web-ui/src/infrastructure/theme/index.ts b/src/web-ui/src/infrastructure/theme/index.ts index 67c0d41cd6..eebf129ac7 100644 --- a/src/web-ui/src/infrastructure/theme/index.ts +++ b/src/web-ui/src/infrastructure/theme/index.ts @@ -11,6 +11,16 @@ export * from './presets'; // Core service export { ThemeService, themeService } from './core/ThemeService'; +// Extension boundary +export { + PLUGIN_THEME_COLOR_KEYS, + createPluginThemeColorProjection, +} from './pluginThemeProjection'; +export type { + PluginThemeColorKey, + PluginThemeColorProjection, +} from './pluginThemeProjection'; + // Integrations export { monacoThemeSync } from './integrations/MonacoThemeSync'; diff --git a/src/web-ui/src/infrastructure/theme/integrations/MonacoThemeSync.ts b/src/web-ui/src/infrastructure/theme/integrations/MonacoThemeSync.ts index 5afda92ade..c324c16740 100644 --- a/src/web-ui/src/infrastructure/theme/integrations/MonacoThemeSync.ts +++ b/src/web-ui/src/infrastructure/theme/integrations/MonacoThemeSync.ts @@ -10,6 +10,7 @@ const log = createLogger('MonacoThemeSync'); const SEMANTIC_HIGHLIGHTING_RULES = BitFunDarkTheme.rules; const TRANSPARENT_MONACO_BORDER = '#00000000'; +const LIGHT_MONACO_PASSIVE_SELECTION = 'rgba(15, 23, 42, 0.1)'; const TRANSPARENT_MONACO_BORDER_COLORS = { 'focusBorder': TRANSPARENT_MONACO_BORDER, 'contrastBorder': TRANSPARENT_MONACO_BORDER, @@ -27,11 +28,11 @@ function getBitfunLightMonacoTheme(): Monaco.editor.IStandaloneThemeData { 'editor.selectionBackground': 'rgba(15, 23, 42, 0.14)', 'editor.selectionForeground': '#1e293b', - 'editor.inactiveSelectionBackground': 'rgba(15, 23, 42, 0.09)', - 'editor.selectionHighlightBackground': 'rgba(15, 23, 42, 0.1)', + 'editor.inactiveSelectionBackground': LIGHT_MONACO_PASSIVE_SELECTION, + 'editor.selectionHighlightBackground': LIGHT_MONACO_PASSIVE_SELECTION, 'editor.selectionHighlightBorder': 'rgba(15, 23, 42, 0.22)', - 'editor.wordHighlightBackground': 'rgba(15, 23, 42, 0.07)', - 'editor.wordHighlightStrongBackground': 'rgba(15, 23, 42, 0.11)', + 'editor.wordHighlightBackground': LIGHT_MONACO_PASSIVE_SELECTION, + 'editor.wordHighlightStrongBackground': LIGHT_MONACO_PASSIVE_SELECTION, }), }; } diff --git a/src/web-ui/src/infrastructure/theme/pluginThemeProjection.ts b/src/web-ui/src/infrastructure/theme/pluginThemeProjection.ts new file mode 100644 index 0000000000..e481bb019c --- /dev/null +++ b/src/web-ui/src/infrastructure/theme/pluginThemeProjection.ts @@ -0,0 +1,30 @@ +import type { ColorValue, ThemeConfig } from './types'; + +export const PLUGIN_THEME_COLOR_KEYS = [ + 'primary', + 'secondary', + 'accent', + 'success', + 'warning', + 'error', + 'info', +] as const; + +export type PluginThemeColorKey = typeof PLUGIN_THEME_COLOR_KEYS[number]; +export type PluginThemeColorProjection = Record; + +export function createPluginThemeColorProjection( + theme: Pick, +): PluginThemeColorProjection { + const { colors } = theme; + + return { + primary: colors.accent[500], + secondary: colors.purple?.[500] ?? colors.accent[600], + accent: colors.accent[600], + success: colors.semantic.success, + warning: colors.semantic.warning, + error: colors.semantic.error, + info: colors.semantic.info, + }; +} diff --git a/src/web-ui/src/infrastructure/theme/presets/china-night-theme.ts b/src/web-ui/src/infrastructure/theme/presets/china-night-theme.ts index e75676893a..0cdf07d885 100644 --- a/src/web-ui/src/infrastructure/theme/presets/china-night-theme.ts +++ b/src/web-ui/src/infrastructure/theme/presets/china-night-theme.ts @@ -1,302 +1,224 @@ - + import { ThemeConfig } from '../types'; import { + createAccentScale, createChinaTypography, createCompactRadius, + createGitColors, + createSemanticColors, + createSecondaryAccentScale, createStandardEasing, createStandardSpacing, createWindowControls, + overlayBlack, + rgbFromHex, + rgbaFromHex, } from './shared'; +const CHINA_NIGHT_BACKGROUND = '#1c1c1f'; +const CHINA_NIGHT_BACKGROUND_SECONDARY = '#212019'; +const CHINA_NIGHT_TEXT_PRIMARY = '#e8e8e8'; +const CHINA_NIGHT_BUTTON_TEXT = '#c5c3be'; +const CHINA_NIGHT_ACCENT = '#73a5cc'; +const CHINA_NIGHT_ACCENT_HOVER = '#5a8bb3'; +const CHINA_NIGHT_GREEN = '#96c6b4'; +const CHINA_NIGHT_GREEN_HOVER = '#7eb09b'; +const CHINA_NIGHT_SUCCESS = '#6bc072'; +const CHINA_NIGHT_WARNING = '#f5b555'; +const CHINA_NIGHT_ERROR = '#e85555'; + +const chinaNightBackground = (alpha: number | string) => rgbaFromHex(CHINA_NIGHT_BACKGROUND, alpha); +const chinaNightText = (alpha: number | string) => rgbaFromHex(CHINA_NIGHT_TEXT_PRIMARY, alpha); +const chinaNightAccent = (alpha: number | string) => rgbaFromHex(CHINA_NIGHT_ACCENT, alpha); +const chinaNightSuccess = (alpha: number | string) => rgbaFromHex(CHINA_NIGHT_SUCCESS, alpha); +const chinaNightWarning = (alpha: number | string) => rgbaFromHex(CHINA_NIGHT_WARNING, alpha); +const chinaNightError = (alpha: number | string) => rgbaFromHex(CHINA_NIGHT_ERROR, alpha); + export const bitfunChinaNightTheme: ThemeConfig = { - + id: 'bitfun-china-night', name: 'Ink Night', type: 'dark', description: 'Chinese dark theme - Starlit ink night, moonlight like water, serene and elegant', author: 'BitFun Team', version: '1.0.0', - - + + colors: { background: { - primary: '#1a1814', - secondary: '#212019', - tertiary: '#262420', - quaternary: '#2d2926', - elevated: '#2d2926', - workbench: '#1a1814', - scene: '#1e1c17', - tooltip: 'rgba(26, 24, 20, 0.95)', + primary: CHINA_NIGHT_BACKGROUND, + secondary: CHINA_NIGHT_BACKGROUND_SECONDARY, + tertiary: '#262626', + quaternary: '#262626', + elevated: '#262626', + workbench: CHINA_NIGHT_BACKGROUND, + scene: CHINA_NIGHT_BACKGROUND, + tooltip: chinaNightBackground(0.95), }, - + text: { - primary: '#e8e6e1', - secondary: '#c5c3be', - muted: '#928f89', - disabled: '#5f5d59', - }, - - accent: { - 50: 'rgba(115, 165, 204, 0.04)', - 100: 'rgba(115, 165, 204, 0.08)', - 200: 'rgba(115, 165, 204, 0.15)', - 300: 'rgba(115, 165, 204, 0.25)', - 400: 'rgba(115, 165, 204, 0.4)', - 500: '#73a5cc', - 600: '#5a8bb3', - 700: 'rgba(90, 139, 179, 0.8)', - 800: 'rgba(90, 139, 179, 0.9)', + primary: CHINA_NIGHT_TEXT_PRIMARY, + secondary: '#c5c3be', + muted: '#928f89', + disabled: '#555555', }, - - purple: { - 50: 'rgba(150, 198, 180, 0.04)', - 100: 'rgba(150, 198, 180, 0.08)', - 200: 'rgba(150, 198, 180, 0.15)', - 300: 'rgba(150, 198, 180, 0.25)', - 400: 'rgba(150, 198, 180, 0.4)', - 500: '#96c6b4', - 600: '#7aab98', - 700: 'rgba(122, 171, 152, 0.8)', - 800: 'rgba(122, 171, 152, 0.9)', - }, - - semantic: { - success: '#6bc072', - successBg: 'rgba(107, 192, 114, 0.12)', - successBorder: 'rgba(107, 192, 114, 0.3)', - - warning: '#f5b555', - warningBg: 'rgba(245, 181, 85, 0.12)', - warningBorder: 'rgba(245, 181, 85, 0.3)', - - error: '#e85555', - errorBg: 'rgba(232, 85, 85, 0.12)', - errorBorder: 'rgba(232, 85, 85, 0.3)', - - info: '#73a5cc', - infoBg: 'rgba(115, 165, 204, 0.12)', - infoBorder: 'rgba(115, 165, 204, 0.3)', - - - highlight: '#e6a84a', - highlightBg: 'rgba(230, 168, 74, 0.15)', - }, - + + accent: createAccentScale({ base: CHINA_NIGHT_ACCENT, hover: CHINA_NIGHT_ACCENT_HOVER }), + + purple: createSecondaryAccentScale({ base: CHINA_NIGHT_GREEN, hover: CHINA_NIGHT_GREEN_HOVER }), + + semantic: createSemanticColors({ + success: CHINA_NIGHT_SUCCESS, + warning: CHINA_NIGHT_WARNING, + error: CHINA_NIGHT_ERROR, + info: CHINA_NIGHT_ACCENT, + bgAlpha: 0.12, + }), + border: { - subtle: 'rgba(232, 230, 225, 0.1)', - base: 'rgba(232, 230, 225, 0.16)', - medium: 'rgba(232, 230, 225, 0.22)', - strong: 'rgba(232, 230, 225, 0.28)', - prominent: 'rgba(232, 230, 225, 0.38)', + subtle: chinaNightText(0.1), + base: chinaNightText(0.16), + medium: chinaNightText(0.22), + strong: chinaNightText(0.28), + prominent: chinaNightText(0.38), }, - + element: { - subtle: 'rgba(115, 165, 204, 0.06)', - soft: 'rgba(115, 165, 204, 0.09)', - base: 'rgba(115, 165, 204, 0.12)', - medium: 'rgba(115, 165, 204, 0.16)', - strong: 'rgba(115, 165, 204, 0.2)', - elevated: 'rgba(45, 41, 38, 0.95)', - }, - - git: { - branch: 'rgb(115, 165, 204)', - branchBg: 'rgba(115, 165, 204, 0.12)', - changes: 'rgb(245, 181, 85)', - changesBg: 'rgba(245, 181, 85, 0.12)', - added: 'rgb(107, 192, 114)', - addedBg: 'rgba(107, 192, 114, 0.12)', - deleted: 'rgb(232, 85, 85)', - deletedBg: 'rgba(232, 85, 85, 0.12)', - staged: 'rgb(107, 192, 114)', - stagedBg: 'rgba(107, 192, 114, 0.12)', + subtle: chinaNightAccent(0.06), + soft: chinaNightAccent(0.09), + base: chinaNightAccent(0.12), + medium: chinaNightAccent(0.16), + strong: chinaNightAccent(0.2), + elevated: rgbaFromHex('#262626', 0.95), }, + + git: createGitColors({ + branch: rgbFromHex(CHINA_NIGHT_ACCENT), + branchBg: chinaNightAccent(0.12), + changes: rgbFromHex(CHINA_NIGHT_WARNING), + changesBg: chinaNightWarning(0.12), + added: rgbFromHex(CHINA_NIGHT_SUCCESS), + addedBg: chinaNightSuccess(0.12), + deleted: rgbFromHex(CHINA_NIGHT_ERROR), + deletedBg: chinaNightError(0.12), + }), }, - - + + effects: { shadow: { - xs: '0 1px 2px rgba(0, 0, 0, 0.5)', - sm: '0 2px 4px rgba(0, 0, 0, 0.6)', - base: '0 4px 8px rgba(0, 0, 0, 0.65)', - lg: '0 8px 16px rgba(0, 0, 0, 0.7)', - xl: '0 12px 24px rgba(0, 0, 0, 0.75)', - '2xl': '0 16px 32px rgba(0, 0, 0, 0.8)', + xs: `0 1px 2px ${overlayBlack(0.5)}`, + sm: `0 2px 4px ${overlayBlack(0.6)}`, + base: `0 4px 8px ${overlayBlack(0.65)}`, + lg: `0 8px 16px ${overlayBlack(0.7)}`, + xl: `0 12px 24px ${overlayBlack(0.75)}`, }, - - glow: { - blue: '0 8px 24px rgba(115, 165, 204, 0.25), 0 4px 12px rgba(115, 165, 204, 0.18), 0 2px 6px rgba(0, 0, 0, 0.3)', - purple: '0 8px 24px rgba(150, 198, 180, 0.25), 0 4px 12px rgba(150, 198, 180, 0.18), 0 2px 6px rgba(0, 0, 0, 0.3)', - mixed: '0 8px 24px rgba(115, 165, 204, 0.2), 0 4px 12px rgba(150, 198, 180, 0.18), 0 2px 6px rgba(0, 0, 0, 0.3)', - }, - + blur: { subtle: 'blur(4px) saturate(1.1)', base: 'blur(8px) saturate(1.15)', - medium: 'blur(12px) saturate(1.2)', - strong: 'blur(16px) saturate(1.25) brightness(1.05)', - intense: 'blur(20px) saturate(1.3) brightness(1.08)', }, - + radius: createCompactRadius(), - + spacing: createStandardSpacing(), - + opacity: { disabled: 0.45, hover: 0.75, focus: 0.9, - overlay: 0.5, }, }, - - + + motion: { duration: { instant: '0.1s', fast: '0.2s', base: '0.35s', slow: '0.7s', - lazy: '1.2s', }, - + easing: createStandardEasing('cubic-bezier(0.25, 0.1, 0.25, 1)'), }, - - + + typography: createChinaTypography(), - - + + components: { - - windowControls: createWindowControls({ - standard: { - dot: 'rgba(115, 165, 204, 0.45)', - dotShadow: '0 0 4px rgba(115, 165, 204, 0.2)', - hoverBg: 'rgba(115, 165, 204, 0.12)', - hoverColor: '#73a5cc', - hoverBorder: 'rgba(115, 165, 204, 0.2)', - hoverShadow: '0 2px 8px rgba(115, 165, 204, 0.15), inset 0 1px 0 rgba(255, 255, 255, 0.1)', - }, - close: { - dot: 'rgba(232, 85, 85, 0.45)', - dotShadow: '0 0 4px rgba(232, 85, 85, 0.2)', - hoverBg: 'rgba(232, 85, 85, 0.12)', - hoverColor: '#e85555', - hoverBorder: 'rgba(232, 85, 85, 0.2)', - hoverShadow: '0 2px 8px rgba(232, 85, 85, 0.15), inset 0 1px 0 rgba(255, 255, 255, 0.1)', - }, - common: { - defaultColor: 'rgba(255, 255, 255, 0.9)', - defaultDot: 'rgba(255, 255, 255, 0.2)', - disabledDot: 'rgba(255, 255, 255, 0.1)', - flowGradient: 'linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.05), rgba(255, 255, 255, 0.08), rgba(255, 255, 255, 0.05), transparent)', - }, - }), - + + windowControls: createWindowControls(CHINA_NIGHT_ERROR), + button: { - - default: { - background: 'rgba(115, 165, 204, 0.11)', - color: '#a29d96', - border: 'transparent', - shadow: 'none', - }, - hover: { - background: 'rgba(115, 165, 204, 0.19)', - color: '#ccc9c4', - border: 'transparent', - shadow: 'none', - transform: 'none', - }, - active: { - background: 'rgba(115, 165, 204, 0.15)', - color: '#ccc9c4', - border: 'transparent', - shadow: 'none', - transform: 'none', - }, - - + + + primary: { default: { - background: 'rgba(115, 165, 204, 0.24)', + background: chinaNightAccent(0.24), color: '#88b8d8', border: 'transparent', shadow: 'none', }, hover: { - background: 'rgba(115, 165, 204, 0.34)', + background: chinaNightAccent(0.34), color: '#b0d5ea', border: 'transparent', shadow: 'none', transform: 'none', }, active: { - background: 'rgba(115, 165, 204, 0.28)', + background: chinaNightAccent(0.28), color: '#b0d5ea', border: 'transparent', shadow: 'none', transform: 'none', }, }, - - + + ghost: { default: { - background: 'transparent', - color: '#a29d96', - border: 'transparent', - shadow: 'none', + color: '#9a9a9a', }, hover: { - background: 'rgba(115, 165, 204, 0.13)', - color: '#ccc9c4', - border: 'transparent', - shadow: 'none', - transform: 'none', - }, - active: { - background: 'rgba(115, 165, 204, 0.11)', - color: '#ccc9c4', + background: chinaNightAccent(0.13), + color: CHINA_NIGHT_BUTTON_TEXT, border: 'transparent', - shadow: 'none', - transform: 'none', }, }, }, }, - - + + monaco: { base: 'vs-dark', inherit: true, rules: [ - { token: 'comment', foreground: '928f89', fontStyle: 'italic' }, - { token: 'keyword', foreground: 'e85555' }, - { token: 'string', foreground: '6bc072' }, - { token: 'number', foreground: 'f5b555' }, - { token: 'type', foreground: '73a5cc' }, - { token: 'class', foreground: '73a5cc' }, - { token: 'function', foreground: '96c6b4' }, - { token: 'variable', foreground: 'c5c3be' }, - { token: 'constant', foreground: 'd4a574' }, - { token: 'operator', foreground: 'e85555' }, - { token: 'tag', foreground: '73a5cc' }, - { token: 'attribute.name', foreground: '96c6b4' }, - { token: 'attribute.value', foreground: '6bc072' }, + { token: 'comment', foreground: '928f89', fontStyle: 'italic' }, + { token: 'keyword', foreground: 'e85555' }, + { token: 'string', foreground: '6bc072' }, + { token: 'number', foreground: 'f5b555' }, + { token: 'type', foreground: '73a5cc' }, + { token: 'class', foreground: '73a5cc' }, + { token: 'function', foreground: '96c6b4' }, + { token: 'variable', foreground: 'c5c3be' }, + { token: 'constant', foreground: 'd4a574' }, + { token: 'operator', foreground: 'e85555' }, + { token: 'tag', foreground: '73a5cc' }, + { token: 'attribute.name', foreground: '96c6b4' }, + { token: 'attribute.value', foreground: '6bc072' }, ], colors: { - background: '#1a1814', - foreground: '#e8e6e1', - lineHighlight: '#212019', - selection: 'rgba(115, 165, 204, 0.25)', - cursor: '#73a5cc', - 'editor.selectionBackground': 'rgba(115, 165, 204, 0.25)', - 'editorCursor.foreground': '#73a5cc', + background: CHINA_NIGHT_BACKGROUND, + foreground: CHINA_NIGHT_TEXT_PRIMARY, + lineHighlight: CHINA_NIGHT_BACKGROUND_SECONDARY, + selection: chinaNightAccent(0.25), + cursor: CHINA_NIGHT_ACCENT, + 'editor.selectionBackground': chinaNightAccent(0.25), + 'editorCursor.foreground': CHINA_NIGHT_ACCENT, }, }, }; - diff --git a/src/web-ui/src/infrastructure/theme/presets/china-style-theme.ts b/src/web-ui/src/infrastructure/theme/presets/china-style-theme.ts index 6a94e739a2..24a8088db1 100644 --- a/src/web-ui/src/infrastructure/theme/presets/china-style-theme.ts +++ b/src/web-ui/src/infrastructure/theme/presets/china-style-theme.ts @@ -1,306 +1,233 @@ - + import { ThemeConfig } from '../types'; import { createChinaTypography, + createAccentScale, createCompactRadius, + createGitColors, + createSemanticColors, + createSecondaryAccentScale, createStandardEasing, createStandardSpacing, createWindowControls, + rgbFromHex, + rgbaFromHex, + STATIC_BLACK, + STATIC_WHITE, } from './shared'; +const CHINA_STYLE_PAPER = '#faf8f0'; +const CHINA_STYLE_INK = '#1c1c1f'; +const CHINA_STYLE_BUTTON_TEXT = '#3d3d3d'; +const CHINA_STYLE_BLUE = '#2e5e8a'; +const CHINA_STYLE_BLUE_HOVER = '#234a6d'; +const CHINA_STYLE_GREEN = '#7eb09b'; +const CHINA_STYLE_GREEN_HOVER = '#5a9078'; +const CHINA_STYLE_SUCCESS = '#52ad5a'; +const CHINA_STYLE_WARNING = '#f0a020'; +const CHINA_STYLE_ERROR = '#c8102e'; +const CHINA_STYLE_BORDER = '#6a5c46'; + +const chinaStylePaper = (alpha: number | string) => rgbaFromHex(CHINA_STYLE_PAPER, alpha); +const chinaStyleBlue = (alpha: number | string) => rgbaFromHex(CHINA_STYLE_BLUE, alpha); +const chinaStyleSuccess = (alpha: number | string) => rgbaFromHex(CHINA_STYLE_SUCCESS, alpha); +const chinaStyleWarning = (alpha: number | string) => rgbaFromHex(CHINA_STYLE_WARNING, alpha); +const chinaStyleError = (alpha: number | string) => rgbaFromHex(CHINA_STYLE_ERROR, alpha); +const chinaStyleBorder = (alpha: number | string) => rgbaFromHex(CHINA_STYLE_BORDER, alpha); + export const bitfunChinaStyleTheme: ThemeConfig = { - + id: 'bitfun-china-style', name: 'Ink Charm', type: 'light', description: 'Chinese style theme - Rice paper and ink, blue and vermilion, warm and elegant', author: 'BitFun Team', version: '1.0.0', - - + + colors: { background: { - primary: '#faf8f0', - secondary: '#f5f3e8', - tertiary: '#f0ede0', - quaternary: '#ebe8d8', - elevated: '#ebe9e3', - workbench: '#faf8f0', - scene: '#fdfcf6', - tooltip: 'rgba(250, 248, 240, 0.96)', + primary: CHINA_STYLE_PAPER, + secondary: '#f5f3e8', + tertiary: '#f0ede0', + quaternary: '#ebe8d8', + elevated: '#f0ede0', + workbench: CHINA_STYLE_PAPER, + scene: CHINA_STYLE_PAPER, + tooltip: chinaStylePaper(0.96), }, - + text: { - primary: '#1a1a1a', - secondary: '#3d3d3d', - muted: '#6a6a6a', - disabled: '#9a9a9a', - }, - - accent: { - 50: 'rgba(46, 94, 138, 0.04)', - 100: 'rgba(46, 94, 138, 0.08)', - 200: 'rgba(46, 94, 138, 0.15)', - 300: 'rgba(46, 94, 138, 0.25)', - 400: 'rgba(46, 94, 138, 0.4)', - 500: '#2e5e8a', - 600: '#234a6d', - 700: 'rgba(35, 74, 109, 0.8)', - 800: 'rgba(35, 74, 109, 0.9)', + primary: CHINA_STYLE_INK, + secondary: '#3d3d3d', + muted: '#6a6a6a', + disabled: '#9a9a9a', }, - - purple: { - 50: 'rgba(126, 176, 155, 0.04)', - 100: 'rgba(126, 176, 155, 0.08)', - 200: 'rgba(126, 176, 155, 0.15)', - 300: 'rgba(126, 176, 155, 0.25)', - 400: 'rgba(126, 176, 155, 0.4)', - 500: '#7eb09b', - 600: '#5a9078', - 700: 'rgba(90, 144, 120, 0.8)', - 800: 'rgba(90, 144, 120, 0.9)', - }, - - semantic: { - success: '#52ad5a', - successBg: 'rgba(82, 173, 90, 0.08)', - successBorder: 'rgba(82, 173, 90, 0.25)', - - warning: '#f0a020', - warningBg: 'rgba(240, 160, 32, 0.08)', - warningBorder: 'rgba(240, 160, 32, 0.25)', - - error: '#c8102e', - errorBg: 'rgba(200, 16, 46, 0.08)', - errorBorder: 'rgba(200, 16, 46, 0.25)', - - info: '#2e5e8a', - infoBg: 'rgba(46, 94, 138, 0.08)', - infoBorder: 'rgba(46, 94, 138, 0.25)', - highlight: '#2e5e8a', - highlightBg: 'rgba(46, 94, 138, 0.12)', - }, - + + accent: createAccentScale({ base: CHINA_STYLE_BLUE, hover: CHINA_STYLE_BLUE_HOVER }), + + purple: createSecondaryAccentScale({ base: CHINA_STYLE_GREEN, hover: CHINA_STYLE_GREEN_HOVER }), + + semantic: createSemanticColors({ + success: CHINA_STYLE_SUCCESS, + warning: CHINA_STYLE_WARNING, + error: CHINA_STYLE_ERROR, + info: CHINA_STYLE_BLUE, + bgAlpha: 0.08, + borderAlpha: 0.25, + }), + border: { - subtle: 'rgba(106, 92, 70, 0.12)', - base: 'rgba(106, 92, 70, 0.2)', - medium: 'rgba(106, 92, 70, 0.28)', - strong: 'rgba(106, 92, 70, 0.36)', - prominent: 'rgba(106, 92, 70, 0.48)', + subtle: chinaStyleBorder(0.12), + base: chinaStyleBorder(0.2), + medium: chinaStyleBorder(0.28), + strong: chinaStyleBorder(0.36), + prominent: chinaStyleBorder(0.48), }, - + element: { - subtle: 'rgba(46, 94, 138, 0.03)', - soft: 'rgba(46, 94, 138, 0.06)', - base: 'rgba(46, 94, 138, 0.1)', - medium: 'rgba(46, 94, 138, 0.14)', - strong: 'rgba(46, 94, 138, 0.18)', - elevated: 'rgba(255, 255, 255, 0.85)', - }, - - git: { - branch: 'rgb(46, 94, 138)', - branchBg: 'rgba(46, 94, 138, 0.08)', - changes: 'rgb(240, 160, 32)', - changesBg: 'rgba(240, 160, 32, 0.08)', - added: 'rgb(82, 173, 90)', - addedBg: 'rgba(82, 173, 90, 0.08)', - deleted: 'rgb(200, 16, 46)', - deletedBg: 'rgba(200, 16, 46, 0.08)', - staged: 'rgb(82, 173, 90)', - stagedBg: 'rgba(82, 173, 90, 0.08)', + subtle: chinaStyleBlue(0.03), + soft: chinaStyleBlue(0.06), + base: chinaStyleBlue(0.1), + medium: chinaStyleBlue(0.14), + strong: chinaStyleBlue(0.18), + elevated: rgbaFromHex(STATIC_WHITE, 0.85), }, + + git: createGitColors({ + branch: rgbFromHex(CHINA_STYLE_BLUE), + branchBg: chinaStyleBlue(0.08), + changes: rgbFromHex(CHINA_STYLE_WARNING), + changesBg: chinaStyleWarning(0.08), + added: rgbFromHex(CHINA_STYLE_SUCCESS), + addedBg: chinaStyleSuccess(0.08), + deleted: rgbFromHex(CHINA_STYLE_ERROR), + deletedBg: chinaStyleError(0.08), + }), }, - - + + effects: { shadow: { - xs: '0 1px 2px rgba(106, 92, 70, 0.06)', - sm: '0 2px 4px rgba(106, 92, 70, 0.08)', - base: '0 4px 8px rgba(106, 92, 70, 0.1)', - lg: '0 8px 16px rgba(106, 92, 70, 0.12)', - xl: '0 12px 24px rgba(106, 92, 70, 0.15)', - '2xl': '0 16px 32px rgba(106, 92, 70, 0.18)', + xs: `0 1px 2px ${chinaStyleBorder(0.06)}`, + sm: `0 2px 4px ${chinaStyleBorder(0.08)}`, + base: `0 4px 8px ${chinaStyleBorder(0.1)}`, + lg: `0 8px 16px ${chinaStyleBorder(0.12)}`, + xl: `0 12px 24px ${chinaStyleBorder(0.15)}`, }, - - glow: { - blue: '0 8px 24px rgba(46, 94, 138, 0.18), 0 4px 12px rgba(46, 94, 138, 0.12), 0 2px 6px rgba(106, 92, 70, 0.05)', - purple: '0 8px 24px rgba(126, 176, 155, 0.18), 0 4px 12px rgba(126, 176, 155, 0.12), 0 2px 6px rgba(106, 92, 70, 0.05)', - mixed: '0 8px 24px rgba(46, 94, 138, 0.12), 0 4px 12px rgba(126, 176, 155, 0.1), 0 2px 6px rgba(106, 92, 70, 0.05)', - }, - + blur: { subtle: 'blur(4px) saturate(1.03)', base: 'blur(8px) saturate(1.05)', - medium: 'blur(12px) saturate(1.08)', - strong: 'blur(16px) saturate(1.1) brightness(1.02)', - intense: 'blur(20px) saturate(1.12) brightness(1.03)', }, - + radius: createCompactRadius(), - + spacing: createStandardSpacing(), - + opacity: { disabled: 0.5, hover: 0.75, focus: 0.9, - overlay: 0.35, }, }, - - + + motion: { duration: { instant: '0.1s', - fast: '0.2s', - base: '0.35s', + fast: '0.2s', + base: '0.35s', slow: '0.7s', - lazy: '1.2s', }, - + easing: createStandardEasing('cubic-bezier(0.25, 0.1, 0.25, 1)'), }, - - + + typography: createChinaTypography(), - - + + components: { - - windowControls: createWindowControls({ - standard: { - dot: 'rgba(46, 94, 138, 0.45)', - dotShadow: '0 0 4px rgba(46, 94, 138, 0.2)', - hoverBg: 'rgba(46, 94, 138, 0.12)', - hoverColor: '#2e5e8a', - hoverBorder: 'rgba(46, 94, 138, 0.2)', - hoverShadow: '0 2px 8px rgba(46, 94, 138, 0.15), inset 0 1px 0 rgba(255, 255, 255, 0.5)', - }, - close: { - dot: 'rgba(200, 16, 46, 0.45)', - dotShadow: '0 0 4px rgba(200, 16, 46, 0.2)', - hoverBg: 'rgba(200, 16, 46, 0.12)', - hoverColor: '#c8102e', - hoverBorder: 'rgba(200, 16, 46, 0.2)', - hoverShadow: '0 2px 8px rgba(200, 16, 46, 0.15), inset 0 1px 0 rgba(255, 255, 255, 0.5)', - }, - common: { - defaultColor: 'rgba(26, 26, 26, 0.9)', - defaultDot: 'rgba(106, 92, 70, 0.2)', - disabledDot: 'rgba(106, 92, 70, 0.1)', - flowGradient: 'linear-gradient(90deg, transparent, rgba(106, 92, 70, 0.05), rgba(106, 92, 70, 0.08), rgba(106, 92, 70, 0.05), transparent)', - }, - }), - + + windowControls: createWindowControls(CHINA_STYLE_ERROR), + button: { - - default: { - background: 'rgba(46, 94, 138, 0.09)', - color: '#5a5a5a', - border: 'transparent', - shadow: 'none', - }, - hover: { - background: 'rgba(46, 94, 138, 0.16)', - color: '#3a3a3a', - border: 'transparent', - shadow: 'none', - transform: 'none', - }, - active: { - background: 'rgba(46, 94, 138, 0.12)', - color: '#3a3a3a', - border: 'transparent', - shadow: 'none', - transform: 'none', - }, - - + + + primary: { default: { - background: '#000000', - color: '#ffffff', + background: STATIC_BLACK, + color: STATIC_WHITE, border: 'transparent', shadow: 'none', }, hover: { background: '#262626', - color: '#ffffff', + color: STATIC_WHITE, border: 'transparent', shadow: 'none', transform: 'none', }, active: { - background: '#1a1a1a', - color: '#ffffff', + background: CHINA_STYLE_INK, + color: STATIC_WHITE, border: 'transparent', shadow: 'none', transform: 'none', }, }, - - + + ghost: { default: { - background: 'transparent', - color: '#5a5a5a', - border: 'transparent', - shadow: 'none', + color: '#555555', }, hover: { - background: 'rgba(46, 94, 138, 0.11)', - color: '#3a3a3a', - border: 'transparent', - shadow: 'none', - transform: 'none', - }, - active: { - background: 'rgba(46, 94, 138, 0.08)', - color: '#3a3a3a', + background: chinaStyleBlue(0.11), + color: CHINA_STYLE_BUTTON_TEXT, border: 'transparent', - shadow: 'none', - transform: 'none', }, }, }, }, - - + + monaco: { base: 'vs', inherit: true, rules: [ - { token: 'comment', foreground: '6a6a6a', fontStyle: 'italic' }, - { token: 'keyword', foreground: 'c8102e' }, - { token: 'string', foreground: '52ad5a' }, - { token: 'number', foreground: 'f0a020' }, - { token: 'type', foreground: '2e5e8a' }, - { token: 'class', foreground: '2e5e8a' }, - { token: 'function', foreground: '7eb09b' }, - { token: 'variable', foreground: '3d3d3d' }, - { token: 'constant', foreground: 'a0522d' }, - { token: 'operator', foreground: 'c8102e' }, - { token: 'tag', foreground: '2e5e8a' }, - { token: 'attribute.name', foreground: '7eb09b' }, - { token: 'attribute.value', foreground: '52ad5a' }, + { token: 'comment', foreground: '6a6a6a', fontStyle: 'italic' }, + { token: 'keyword', foreground: 'c8102e' }, + { token: 'string', foreground: '52ad5a' }, + { token: 'number', foreground: 'f0a020' }, + { token: 'type', foreground: '2e5e8a' }, + { token: 'class', foreground: '2e5e8a' }, + { token: 'function', foreground: '7eb09b' }, + { token: 'variable', foreground: '3d3d3d' }, + { token: 'constant', foreground: 'a0522d' }, + { token: 'operator', foreground: 'c8102e' }, + { token: 'tag', foreground: '2e5e8a' }, + { token: 'attribute.name', foreground: '7eb09b' }, + { token: 'attribute.value', foreground: '52ad5a' }, ], colors: { - background: '#faf8f0', - foreground: '#1a1a1a', - lineHighlight: '#f5f3e8', - selection: 'rgba(46, 94, 138, 0.28)', - cursor: '#2e5e8a', - - 'editor.selectionBackground': 'rgba(46, 94, 138, 0.28)', - 'editor.selectionForeground': '#1a1a1a', - 'editor.inactiveSelectionBackground': 'rgba(46, 94, 138, 0.18)', - 'editor.selectionHighlightBackground': 'rgba(46, 94, 138, 0.2)', - 'editor.selectionHighlightBorder': 'rgba(46, 94, 138, 0.35)', - 'editorCursor.foreground': '#2e5e8a', - 'editor.wordHighlightBackground': 'rgba(46, 94, 138, 0.12)', - 'editor.wordHighlightStrongBackground': 'rgba(46, 94, 138, 0.22)', + background: CHINA_STYLE_PAPER, + foreground: CHINA_STYLE_INK, + lineHighlight: '#f5f3e8', + selection: chinaStyleBlue(0.28), + cursor: CHINA_STYLE_BLUE, + + 'editor.selectionBackground': chinaStyleBlue(0.28), + 'editor.selectionForeground': CHINA_STYLE_INK, + 'editor.inactiveSelectionBackground': chinaStyleBlue(0.18), + 'editor.selectionHighlightBackground': chinaStyleBlue(0.2), + 'editor.selectionHighlightBorder': chinaStyleBlue(0.35), + 'editorCursor.foreground': CHINA_STYLE_BLUE, + 'editor.wordHighlightBackground': chinaStyleBlue(0.12), + 'editor.wordHighlightStrongBackground': chinaStyleBlue(0.22), }, }, }; diff --git a/src/web-ui/src/infrastructure/theme/presets/cyber-theme.ts b/src/web-ui/src/infrastructure/theme/presets/cyber-theme.ts index d14ba916a2..290857dc9d 100644 --- a/src/web-ui/src/infrastructure/theme/presets/cyber-theme.ts +++ b/src/web-ui/src/infrastructure/theme/presets/cyber-theme.ts @@ -1,121 +1,120 @@ - + import { ThemeConfig } from '../types'; import { + createAccentScale, createCompactRadius, createExpressiveTypography, + createGitColors, + createSemanticColors, + createSecondaryAccentScale, createStandardEasing, createStandardSpacing, createWindowControls, + overlayBlack, + rgbFromHex, + rgbaFromHex, + STATIC_WHITE, } from './shared'; +const CYBER_BACKGROUND = '#0e0e10'; +const CYBER_TEXT_PRIMARY = '#e0f2ff'; +const CYBER_TEXT_SECONDARY = '#c7e7ff'; +const CYBER_TEXT_MUTED = '#7fadcc'; +const CYBER_ACCENT = '#00e6ff'; +const CYBER_ACCENT_HOVER = '#00ccff'; +const CYBER_PURPLE = '#8a2be2'; +const CYBER_PURPLE_HOVER = '#7928ca'; +const CYBER_SUCCESS = '#00ff9f'; +const CYBER_WARNING = '#ffcc00'; +const CYBER_ERROR = '#ff0055'; +const CYBER_SURFACE_SECONDARY = '#1c1c1f'; + +const cyberAccent = (alpha: number | string) => rgbaFromHex(CYBER_ACCENT, alpha); +const cyberSuccess = (alpha: number | string) => rgbaFromHex(CYBER_SUCCESS, alpha); +const cyberWarning = (alpha: number | string) => rgbaFromHex(CYBER_WARNING, alpha); +const cyberError = (alpha: number | string) => rgbaFromHex(CYBER_ERROR, alpha); + export const bitfunCyberTheme: ThemeConfig = { - + id: 'bitfun-cyber', name: 'Cyber', type: 'dark', description: 'Tech-style theme - Deep black hole, neon future, ultimate tech aesthetics', author: 'BitFun Team', version: '1.0.0', - - + + colors: { background: { - primary: '#101010', - secondary: '#151515', - tertiary: '#1a1a1a', - quaternary: '#1f1f1f', - elevated: '#0d0d0d', - workbench: '#101010', - scene: '#141414', - tooltip: 'rgba(16, 16, 16, 0.95)', + primary: CYBER_BACKGROUND, + secondary: CYBER_SURFACE_SECONDARY, + tertiary: CYBER_SURFACE_SECONDARY, + quaternary: CYBER_SURFACE_SECONDARY, + elevated: CYBER_BACKGROUND, + workbench: CYBER_BACKGROUND, + scene: CYBER_SURFACE_SECONDARY, + tooltip: rgbaFromHex(CYBER_BACKGROUND, 0.95), }, - + text: { - primary: '#e0f2ff', - secondary: '#c7e7ff', - muted: '#7fadcc', - disabled: '#4a5a66', + primary: CYBER_TEXT_PRIMARY, + secondary: CYBER_TEXT_SECONDARY, + muted: CYBER_TEXT_MUTED, + disabled: '#475569', }, - - accent: { - 50: 'rgba(0, 230, 255, 0.05)', - 100: 'rgba(0, 230, 255, 0.1)', - 200: 'rgba(0, 230, 255, 0.18)', - 300: 'rgba(0, 230, 255, 0.3)', - 400: 'rgba(0, 230, 255, 0.45)', - 500: '#00e6ff', - 600: '#00ccff', - 700: 'rgba(0, 204, 255, 0.85)', - 800: 'rgba(0, 204, 255, 0.95)', - }, - - purple: { - 50: 'rgba(138, 43, 226, 0.05)', - 100: 'rgba(138, 43, 226, 0.1)', - 200: 'rgba(138, 43, 226, 0.18)', - 300: 'rgba(138, 43, 226, 0.3)', - 400: 'rgba(138, 43, 226, 0.45)', - 500: '#8a2be2', - 600: '#7928ca', - 700: 'rgba(121, 40, 202, 0.85)', - 800: 'rgba(121, 40, 202, 0.95)', - }, - - semantic: { - success: '#00ff9f', - successBg: 'rgba(0, 255, 159, 0.12)', - successBorder: 'rgba(0, 255, 159, 0.35)', - - warning: '#ffcc00', - warningBg: 'rgba(255, 204, 0, 0.12)', - warningBorder: 'rgba(255, 204, 0, 0.35)', - - error: '#ff0055', - errorBg: 'rgba(255, 0, 85, 0.12)', - errorBorder: 'rgba(255, 0, 85, 0.35)', - - info: '#00e6ff', - infoBg: 'rgba(0, 230, 255, 0.12)', - infoBorder: 'rgba(0, 230, 255, 0.35)', - - - highlight: '#ffdd44', - highlightBg: 'rgba(255, 221, 68, 0.15)', - }, - + + accent: createAccentScale({ + base: CYBER_ACCENT, + hover: CYBER_ACCENT_HOVER, + alpha: { 50: 0.05, 100: 0.1, 200: 0.18, 300: 0.3, 400: 0.45, 700: 0.85, 800: 0.95 }, + }), + + purple: createSecondaryAccentScale({ + base: CYBER_PURPLE, + hover: CYBER_PURPLE_HOVER, + alpha: { 50: 0.05, 100: 0.1, 200: 0.18, 400: 0.45, 800: 0.95 }, + }), + + semantic: createSemanticColors({ + success: CYBER_SUCCESS, + warning: CYBER_WARNING, + error: CYBER_ERROR, + info: CYBER_ACCENT, + bgAlpha: 0.12, + borderAlpha: 0.35, + }), + border: { - subtle: 'rgba(0, 230, 255, 0.14)', - base: 'rgba(0, 230, 255, 0.2)', - medium: 'rgba(0, 230, 255, 0.28)', - strong: 'rgba(0, 230, 255, 0.36)', - prominent: 'rgba(0, 230, 255, 0.5)', + subtle: cyberAccent(0.14), + base: cyberAccent(0.2), + medium: cyberAccent(0.28), + strong: cyberAccent(0.36), + prominent: cyberAccent(0.5), }, - + element: { - subtle: 'rgba(0, 230, 255, 0.06)', - soft: 'rgba(0, 230, 255, 0.09)', - base: 'rgba(0, 230, 255, 0.13)', - medium: 'rgba(0, 230, 255, 0.17)', - strong: 'rgba(0, 230, 255, 0.22)', - elevated: 'rgba(0, 230, 255, 0.27)', - }, - - git: { - branch: 'rgb(0, 230, 255)', - branchBg: 'rgba(0, 230, 255, 0.12)', - changes: 'rgb(255, 204, 0)', - changesBg: 'rgba(255, 204, 0, 0.12)', - added: 'rgb(0, 255, 159)', - addedBg: 'rgba(0, 255, 159, 0.12)', - deleted: 'rgb(255, 0, 85)', - deletedBg: 'rgba(255, 0, 85, 0.12)', - staged: 'rgb(0, 255, 159)', - stagedBg: 'rgba(0, 255, 159, 0.12)', + subtle: cyberAccent(0.06), + soft: cyberAccent(0.09), + base: cyberAccent(0.13), + medium: cyberAccent(0.17), + strong: cyberAccent(0.22), + elevated: cyberAccent(0.27), }, + + git: createGitColors({ + branch: rgbFromHex(CYBER_ACCENT), + branchBg: cyberAccent(0.12), + changes: rgbFromHex(CYBER_WARNING), + changesBg: cyberWarning(0.12), + added: rgbFromHex(CYBER_SUCCESS), + addedBg: cyberSuccess(0.12), + deleted: rgbFromHex(CYBER_ERROR), + deletedBg: cyberError(0.12), + }), }, - - + + effects: { shadow: { xs: '0 1px 3px rgba(0, 0, 0, 0.9)', @@ -123,166 +122,96 @@ export const bitfunCyberTheme: ThemeConfig = { base: '0 4px 12px rgba(0, 0, 0, 0.8)', lg: '0 8px 20px rgba(0, 0, 0, 0.75)', xl: '0 12px 28px rgba(0, 0, 0, 0.7)', - '2xl': '0 16px 36px rgba(0, 0, 0, 0.65)', - }, - - glow: { - - blue: '0 0 12px rgba(0, 230, 255, 0.4), 0 0 24px rgba(0, 230, 255, 0.25), 0 0 36px rgba(0, 230, 255, 0.15), 0 4px 16px rgba(0, 0, 0, 0.3)', - - purple: '0 0 12px rgba(138, 43, 226, 0.4), 0 0 24px rgba(138, 43, 226, 0.25), 0 0 36px rgba(138, 43, 226, 0.15), 0 4px 16px rgba(0, 0, 0, 0.3)', - - mixed: '0 0 16px rgba(0, 230, 255, 0.35), 0 0 28px rgba(138, 43, 226, 0.25), 0 0 40px rgba(0, 230, 255, 0.12), 0 4px 20px rgba(0, 0, 0, 0.35)', }, - + blur: { subtle: 'blur(4px) saturate(1.2)', base: 'blur(8px) saturate(1.3)', - medium: 'blur(12px) saturate(1.4)', - strong: 'blur(16px) saturate(1.5) brightness(1.15)', - intense: 'blur(20px) saturate(1.6) brightness(1.2)', }, - + radius: createCompactRadius(), - + spacing: createStandardSpacing(), - + opacity: { disabled: 0.5, hover: 0.85, focus: 0.95, - overlay: 0.5, }, }, - - + + motion: { duration: { instant: '0.08s', fast: '0.12s', base: '0.25s', slow: '0.5s', - lazy: '0.8s', }, - + easing: createStandardEasing('cubic-bezier(0.25, 0.46, 0.45, 0.94)'), }, - - + + typography: createExpressiveTypography(), - - + + components: { - - windowControls: createWindowControls({ - standard: { - dot: 'rgba(0, 230, 255, 0.5)', - dotShadow: '0 0 6px rgba(0, 230, 255, 0.35)', - hoverBg: 'rgba(0, 230, 255, 0.15)', - hoverColor: '#00e6ff', - hoverBorder: 'rgba(0, 230, 255, 0.3)', - hoverShadow: '0 0 12px rgba(0, 230, 255, 0.3), 0 2px 8px rgba(0, 230, 255, 0.2), inset 0 1px 0 rgba(0, 230, 255, 0.2)', - }, - close: { - dot: 'rgba(255, 0, 85, 0.5)', - dotShadow: '0 0 6px rgba(255, 0, 85, 0.35)', - hoverBg: 'rgba(255, 0, 85, 0.15)', - hoverColor: '#ff0055', - hoverBorder: 'rgba(255, 0, 85, 0.3)', - hoverShadow: '0 0 12px rgba(255, 0, 85, 0.3), 0 2px 8px rgba(255, 0, 85, 0.2), inset 0 1px 0 rgba(255, 0, 85, 0.2)', - }, - common: { - defaultColor: 'rgba(224, 242, 255, 0.9)', - defaultDot: 'rgba(0, 230, 255, 0.2)', - disabledDot: 'rgba(0, 230, 255, 0.1)', - flowGradient: 'linear-gradient(90deg, transparent, rgba(0, 230, 255, 0.08), rgba(0, 230, 255, 0.12), rgba(0, 230, 255, 0.08), transparent)', - }, - }), - + + windowControls: createWindowControls(CYBER_ERROR), + button: { - - default: { - background: 'rgba(0, 230, 255, 0.08)', - color: '#7fadcc', - border: 'rgba(0, 230, 255, 0.15)', - shadow: '0 0 8px rgba(0, 230, 255, 0.1)', - }, - hover: { - background: 'rgba(0, 230, 255, 0.14)', - color: '#c7e7ff', - border: 'rgba(0, 230, 255, 0.3)', - shadow: '0 0 16px rgba(0, 230, 255, 0.2), 0 2px 8px rgba(0, 0, 0, 0.3)', - transform: 'translateY(-1px)', - }, - active: { - background: 'rgba(0, 230, 255, 0.12)', - color: '#c7e7ff', - border: 'rgba(0, 230, 255, 0.35)', - shadow: '0 0 12px rgba(0, 230, 255, 0.15)', - transform: 'translateY(0)', - }, - - + + + primary: { default: { - background: 'rgba(0, 230, 255, 0.18)', - color: '#e0f2ff', - border: 'rgba(0, 230, 255, 0.4)', - shadow: '0 0 16px rgba(0, 230, 255, 0.25)', + background: cyberAccent(0.18), + color: CYBER_TEXT_PRIMARY, + border: cyberAccent(0.4), + shadow: `0 0 16px ${cyberAccent(0.25)}`, }, hover: { - background: 'rgba(0, 230, 255, 0.25)', - color: '#ffffff', - border: 'rgba(0, 230, 255, 0.6)', - shadow: '0 0 24px rgba(0, 230, 255, 0.4), 0 0 36px rgba(0, 230, 255, 0.2), 0 4px 12px rgba(0, 0, 0, 0.3)', + background: cyberAccent(0.25), + color: STATIC_WHITE, + border: cyberAccent(0.6), + shadow: `0 0 24px ${cyberAccent(0.4)}, 0 0 36px ${cyberAccent(0.2)}, 0 4px 12px ${overlayBlack(0.3)}`, transform: 'translateY(-2px)', }, active: { - background: 'rgba(0, 230, 255, 0.22)', - color: '#ffffff', - border: 'rgba(0, 230, 255, 0.5)', - shadow: '0 0 20px rgba(0, 230, 255, 0.3)', + background: cyberAccent(0.22), + color: STATIC_WHITE, + border: cyberAccent(0.5), + shadow: `0 0 20px ${cyberAccent(0.3)}`, transform: 'translateY(-1px)', }, }, - - + + ghost: { default: { - background: 'transparent', - color: '#7fadcc', - border: 'rgba(0, 230, 255, 0.2)', - shadow: 'none', + color: CYBER_TEXT_MUTED, }, hover: { - background: 'rgba(0, 230, 255, 0.1)', - color: '#c7e7ff', - border: 'rgba(0, 230, 255, 0.35)', - shadow: '0 0 12px rgba(0, 230, 255, 0.15)', - transform: 'translateY(-1px)', - }, - active: { - background: 'rgba(0, 230, 255, 0.08)', - color: '#c7e7ff', - border: 'rgba(0, 230, 255, 0.3)', - shadow: '0 0 8px rgba(0, 230, 255, 0.1)', - transform: 'translateY(0)', + background: cyberAccent(0.1), + color: CYBER_TEXT_SECONDARY, + border: cyberAccent(0.35), }, }, }, }, - - + + monaco: { base: 'vs-dark', inherit: true, rules: [], colors: { - background: '#101010', - foreground: '#c7e7ff', - lineHighlight: '#151515', + background: CYBER_BACKGROUND, + foreground: CYBER_TEXT_SECONDARY, + lineHighlight: CYBER_SURFACE_SECONDARY, selection: '#1a4d66', - cursor: '#00e6ff', + cursor: CYBER_ACCENT, }, }, }; diff --git a/src/web-ui/src/infrastructure/theme/presets/dark-theme.ts b/src/web-ui/src/infrastructure/theme/presets/dark-theme.ts index 8d5841b230..6f378f802b 100644 --- a/src/web-ui/src/infrastructure/theme/presets/dark-theme.ts +++ b/src/web-ui/src/infrastructure/theme/presets/dark-theme.ts @@ -1,279 +1,203 @@ - + import { ThemeConfig } from '../types'; import { + createAccentScale, createDarkNeutralBorder, createDarkNeutralElement, createDarkNeutralScrollbar, + createGitColors, + createSemanticColors, + createSecondaryAccentScale, createStandardEasing, createStandardRadius, createStandardSpacing, createStandardTypography, createWindowControls, + overlayBlack, + overlayWhite, + rgbFromHex, + rgbaFromHex, + STATIC_WHITE, } from './shared'; +const DARK_BACKGROUND_PRIMARY = '#0e0e10'; +const DARK_BACKGROUND_SECONDARY = '#1c1c1f'; +const DARK_TEXT_PRIMARY = '#e8e8e8'; +const DARK_BUTTON_TEXT = '#c8c8c8'; +const DARK_ACCENT = '#60a5fa'; +const DARK_ACCENT_HOVER = '#3b82f6'; +const DARK_PURPLE = '#8b5cf6'; +const DARK_PURPLE_HOVER = '#7c3aed'; +const DARK_SUCCESS = '#34d399'; +const DARK_WARNING = '#f59e0b'; +const DARK_ERROR = '#ef4444'; + +const darkWarning = (alpha: number | string) => rgbaFromHex(DARK_WARNING, alpha); +const darkError = (alpha: number | string) => rgbaFromHex(DARK_ERROR, alpha); + export const bitfunDarkTheme: ThemeConfig = { - + id: 'bitfun-dark', name: 'Dark', type: 'dark', description: 'Default dark theme', author: 'BitFun Team', version: '2.1.0', - - + + colors: { background: { - primary: '#0e0e10', - secondary: '#1c1c1f', - tertiary: '#0e0e10', - quaternary: '#252528', - elevated: '#1c1c1f', - workbench: '#0e0e10', - scene: '#1c1c1f', - tooltip: 'rgba(28, 28, 31, 0.96)', + primary: DARK_BACKGROUND_PRIMARY, + secondary: DARK_BACKGROUND_SECONDARY, + tertiary: DARK_BACKGROUND_PRIMARY, + quaternary: '#262626', + elevated: DARK_BACKGROUND_SECONDARY, + workbench: DARK_BACKGROUND_PRIMARY, + scene: DARK_BACKGROUND_SECONDARY, + tooltip: rgbaFromHex(DARK_BACKGROUND_SECONDARY, 0.96), }, - + text: { - primary: '#e8e8e8', + primary: DARK_TEXT_PRIMARY, secondary: '#b0b0b0', muted: '#858585', disabled: '#555555', }, - - accent: { - 50: 'rgba(96, 165, 250, 0.04)', - 100: 'rgba(96, 165, 250, 0.08)', - 200: 'rgba(96, 165, 250, 0.15)', - 300: 'rgba(96, 165, 250, 0.25)', - 400: 'rgba(96, 165, 250, 0.4)', - 500: '#60a5fa', - 600: '#3b82f6', - 700: 'rgba(59, 130, 246, 0.8)', - 800: 'rgba(59, 130, 246, 0.9)', - }, - - purple: { - 50: 'rgba(139, 92, 246, 0.04)', - 100: 'rgba(139, 92, 246, 0.08)', - 200: 'rgba(139, 92, 246, 0.15)', - 300: 'rgba(139, 92, 246, 0.25)', - 400: 'rgba(139, 92, 246, 0.4)', - 500: '#8b5cf6', - 600: '#7c3aed', - 700: 'rgba(124, 58, 237, 0.8)', - 800: 'rgba(124, 58, 237, 0.9)', - }, - - semantic: { - success: '#34d399', - successBg: 'rgba(52, 211, 153, 0.1)', - successBorder: 'rgba(52, 211, 153, 0.3)', - - warning: '#f59e0b', - warningBg: 'rgba(245, 158, 11, 0.1)', - warningBorder: 'rgba(245, 158, 11, 0.3)', - - error: '#ef4444', - errorBg: 'rgba(239, 68, 68, 0.1)', - errorBorder: 'rgba(239, 68, 68, 0.3)', - + + accent: createAccentScale({ base: DARK_ACCENT, hover: DARK_ACCENT_HOVER }), + + purple: createSecondaryAccentScale({ base: DARK_PURPLE, hover: DARK_PURPLE_HOVER }), + + semantic: createSemanticColors({ + success: DARK_SUCCESS, + warning: DARK_WARNING, + error: DARK_ERROR, info: '#a1a1aa', - infoBg: 'rgba(255, 255, 255, 0.08)', - infoBorder: 'rgba(255, 255, 255, 0.22)', - - - highlight: '#a8a8a8', - highlightBg: 'rgba(255, 255, 255, 0.1)', - }, - + overrides: { + infoBg: overlayWhite(0.08), + infoBorder: overlayWhite(0.24), + }, + }), + border: createDarkNeutralBorder(), - + element: createDarkNeutralElement(), - - git: { + + git: createGitColors({ branch: '#a1a1aa', - branchBg: 'rgba(255, 255, 255, 0.06)', - changes: 'rgb(245, 158, 11)', - changesBg: 'rgba(245, 158, 11, 0.1)', + branchBg: overlayWhite(0.06), + changes: rgbFromHex(DARK_WARNING), + changesBg: darkWarning(0.1), added: 'rgb(34, 197, 94)', addedBg: 'rgba(34, 197, 94, 0.1)', - deleted: 'rgb(239, 68, 68)', - deletedBg: 'rgba(239, 68, 68, 0.1)', - staged: 'rgb(34, 197, 94)', - stagedBg: 'rgba(34, 197, 94, 0.1)', - }, - + deleted: rgbFromHex(DARK_ERROR), + deletedBg: darkError(0.1), + }), + scrollbar: createDarkNeutralScrollbar(), }, - - + + effects: { shadow: { - xs: '0 1px 2px rgba(0, 0, 0, 0.9)', - sm: '0 2px 4px rgba(0, 0, 0, 0.8)', - base: '0 4px 8px rgba(0, 0, 0, 0.7)', - lg: '0 8px 16px rgba(0, 0, 0, 0.6)', - xl: '0 12px 24px rgba(0, 0, 0, 0.5)', - '2xl': '0 16px 32px rgba(0, 0, 0, 0.4)', - }, - - glow: { - blue: '0 12px 32px rgba(96, 165, 250, 0.2), 0 6px 16px rgba(96, 165, 250, 0.12), 0 3px 8px rgba(0, 0, 0, 0.12)', - purple: '0 12px 32px rgba(139, 92, 246, 0.22), 0 6px 16px rgba(124, 58, 237, 0.14), 0 3px 8px rgba(0, 0, 0, 0.12)', - mixed: '0 12px 32px rgba(255, 255, 255, 0.06), 0 6px 16px rgba(139, 92, 246, 0.12), 0 3px 8px rgba(0, 0, 0, 0.12)', + xs: `0 1px 2px ${overlayBlack(0.9)}`, + sm: `0 2px 4px ${overlayBlack(0.8)}`, + base: `0 4px 8px ${overlayBlack(0.7)}`, + lg: `0 8px 16px ${overlayBlack(0.6)}`, + xl: `0 12px 24px ${overlayBlack(0.5)}`, }, - + blur: { subtle: 'blur(4px) saturate(1.05)', base: 'blur(8px) saturate(1.1)', - medium: 'blur(12px) saturate(1.2)', - strong: 'blur(16px) saturate(1.3) brightness(1.1)', - intense: 'blur(20px) saturate(1.4) brightness(1.15)', }, - + radius: createStandardRadius(), - + spacing: createStandardSpacing(), - + opacity: { disabled: 0.6, hover: 0.8, focus: 0.9, - overlay: 0.4, }, }, - - + + motion: { duration: { instant: '0.1s', fast: '0.15s', base: '0.3s', slow: '0.6s', - lazy: '1s', }, - + easing: createStandardEasing(), }, - - + + typography: createStandardTypography(), - - + + components: { - - windowControls: createWindowControls({ - standard: { - dot: 'rgba(255, 255, 255, 0.38)', - dotShadow: '0 0 4px rgba(0, 0, 0, 0.35)', - hoverBg: 'rgba(255, 255, 255, 0.1)', - hoverColor: '#e4e4e4', - hoverBorder: 'rgba(255, 255, 255, 0.16)', - hoverShadow: '0 2px 8px rgba(0, 0, 0, 0.25), inset 0 1px 0 rgba(255, 255, 255, 0.08)', - }, - close: { - dot: 'rgba(239, 68, 68, 0.45)', - dotShadow: '0 0 4px rgba(239, 68, 68, 0.2)', - hoverBg: 'rgba(239, 68, 68, 0.12)', - hoverColor: '#ef4444', - hoverBorder: 'rgba(239, 68, 68, 0.2)', - hoverShadow: '0 2px 8px rgba(239, 68, 68, 0.15), inset 0 1px 0 rgba(255, 255, 255, 0.1)', - }, - common: { - defaultColor: 'rgba(232, 232, 232, 0.9)', - defaultDot: 'rgba(255, 255, 255, 0.2)', - disabledDot: 'rgba(255, 255, 255, 0.1)', - flowGradient: 'linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.05), rgba(255, 255, 255, 0.08), rgba(255, 255, 255, 0.05), transparent)', - }, - }), - + + windowControls: createWindowControls(DARK_ERROR), + button: { - - default: { - background: 'rgba(255, 255, 255, 0.08)', - color: '#9a9a9a', - border: 'transparent', - shadow: 'none', - }, - hover: { - background: 'rgba(255, 255, 255, 0.14)', - color: '#c8c8c8', - border: 'transparent', - shadow: 'none', - transform: 'none', - }, - active: { - background: 'rgba(255, 255, 255, 0.1)', - color: '#c8c8c8', - border: 'transparent', - shadow: 'none', - transform: 'none', - }, - - + + + primary: { default: { - background: 'rgba(255, 255, 255, 0.16)', - color: '#f4f4f5', + background: overlayWhite(0.16), + color: '#f3f3f5', border: 'transparent', shadow: 'none', }, hover: { - background: 'rgba(255, 255, 255, 0.24)', - color: '#fafafa', + background: overlayWhite(0.24), + color: STATIC_WHITE, border: 'transparent', shadow: 'none', transform: 'none', }, active: { - background: 'rgba(255, 255, 255, 0.2)', - color: '#fafafa', + background: overlayWhite(0.2), + color: STATIC_WHITE, border: 'transparent', shadow: 'none', transform: 'none', }, }, - - + + ghost: { default: { - background: 'transparent', color: '#9a9a9a', - border: 'transparent', - shadow: 'none', }, hover: { - background: 'rgba(255, 255, 255, 0.1)', - color: '#c8c8c8', + background: overlayWhite(0.1), + color: DARK_BUTTON_TEXT, border: 'transparent', - shadow: 'none', - transform: 'none', - }, - active: { - background: 'rgba(255, 255, 255, 0.07)', - color: '#c8c8c8', - border: 'transparent', - shadow: 'none', - transform: 'none', }, }, }, }, - - - - + + + + monaco: { base: 'vs-dark', inherit: true, - rules: [], + rules: [], colors: { - background: '#121214', - foreground: '#e8e8e8', - lineHighlight: '#18181a', - selection: 'rgba(255, 255, 255, 0.12)', - cursor: '#c4c4c4', + background: DARK_BACKGROUND_PRIMARY, + foreground: DARK_TEXT_PRIMARY, + lineHighlight: DARK_BACKGROUND_SECONDARY, + selection: overlayWhite(0.12), + cursor: DARK_BUTTON_TEXT, }, }, }; diff --git a/src/web-ui/src/infrastructure/theme/presets/light-theme.ts b/src/web-ui/src/infrastructure/theme/presets/light-theme.ts index 349c6b5c36..87edadbb4e 100644 --- a/src/web-ui/src/infrastructure/theme/presets/light-theme.ts +++ b/src/web-ui/src/infrastructure/theme/presets/light-theme.ts @@ -1,16 +1,42 @@ - + import { ThemeConfig } from '../types'; import { + createAccentScale, + createGitColors, + createSemanticColors, + createSecondaryAccentScale, createStandardEasing, createStandardRadius, createStandardSpacing, createStandardTypography, createWindowControls, + rgbFromHex, + rgbaFromHex, + STATIC_BLACK, + STATIC_WHITE, } from './shared'; +const LIGHT_INK = '#0f172a'; +const LIGHT_TEXT_PRIMARY = '#1e293b'; +const LIGHT_TEXT_STRONG = '#334155'; +const LIGHT_ACCENT = '#64748b'; +const LIGHT_ACCENT_HOVER = '#475569'; +const LIGHT_PURPLE = '#7c6b99'; +const LIGHT_PURPLE_HOVER = '#655680'; +const LIGHT_SUCCESS = '#5b9a6f'; +const LIGHT_WARNING = '#c08c42'; +const LIGHT_ERROR = '#c26565'; + +const lightInk = (alpha: number | string) => rgbaFromHex(LIGHT_INK, alpha); +const lightAccent = (alpha: number | string) => rgbaFromHex(LIGHT_ACCENT, alpha); +const lightAccentHover = (alpha: number | string) => rgbaFromHex(LIGHT_ACCENT_HOVER, alpha); +const lightSuccess = (alpha: number | string) => rgbaFromHex(LIGHT_SUCCESS, alpha); +const lightWarning = (alpha: number | string) => rgbaFromHex(LIGHT_WARNING, alpha); +const lightError = (alpha: number | string) => rgbaFromHex(LIGHT_ERROR, alpha); + export const bitfunLightTheme: ThemeConfig = { - + id: 'bitfun-light', name: 'Light', type: 'light', @@ -21,301 +47,222 @@ export const bitfunLightTheme: ThemeConfig = { layout: { sceneViewportBorder: false, }, - - + + colors: { background: { primary: '#f3f3f5', - secondary: '#ffffff', - tertiary: '#e8eaee', - quaternary: '#e0e3e8', - elevated: '#ffffff', - workbench: '#eceef1', - scene: '#ffffff', - tooltip: 'rgba(255, 255, 255, 0.98)', + secondary: STATIC_WHITE, + tertiary: '#e8e8e8', + quaternary: '#e8e8e8', + elevated: STATIC_WHITE, + workbench: '#e8e8e8', + scene: STATIC_WHITE, + tooltip: rgbaFromHex(STATIC_WHITE, 0.98), }, - + text: { - primary: '#1e293b', - secondary: '#3d4f66', - muted: '#64748b', - disabled: '#94a3b8', - }, - - - accent: { - 50: 'rgba(15, 23, 42, 0.04)', - 100: 'rgba(15, 23, 42, 0.07)', - 200: 'rgba(15, 23, 42, 0.1)', - 300: 'rgba(15, 23, 42, 0.16)', - 400: 'rgba(15, 23, 42, 0.26)', - 500: '#64748b', - 600: '#475569', - 700: 'rgba(71, 85, 105, 0.88)', - 800: 'rgba(51, 65, 85, 0.94)', - }, - - - purple: { - 50: 'rgba(107, 90, 137, 0.04)', - 100: 'rgba(107, 90, 137, 0.08)', - 200: 'rgba(107, 90, 137, 0.14)', - 300: 'rgba(107, 90, 137, 0.22)', - 400: 'rgba(107, 90, 137, 0.36)', - 500: '#7c6b99', - 600: '#655680', - 700: 'rgba(101, 86, 128, 0.8)', - 800: 'rgba(101, 86, 128, 0.9)', - }, - - - semantic: { - success: '#5b9a6f', - successBg: 'rgba(91, 154, 111, 0.08)', - successBorder: 'rgba(91, 154, 111, 0.25)', - - warning: '#c08c42', - warningBg: 'rgba(192, 140, 66, 0.08)', - warningBorder: 'rgba(192, 140, 66, 0.25)', - - error: '#c26565', - errorBg: 'rgba(194, 101, 101, 0.08)', - errorBorder: 'rgba(194, 101, 101, 0.25)', - - info: '#64748b', - infoBg: 'rgba(100, 116, 139, 0.1)', - infoBorder: 'rgba(100, 116, 139, 0.28)', - - - highlight: '#b8863a', - highlightBg: 'rgba(184, 134, 58, 0.12)', + primary: LIGHT_TEXT_PRIMARY, + secondary: '#3d4f66', + muted: LIGHT_ACCENT, + disabled: '#94a3b8', }, - - + + + accent: createAccentScale({ + base: LIGHT_ACCENT, + hover: LIGHT_ACCENT_HOVER, + alpha: { 700: 0.88 }, + stops: { + 50: lightInk(0.04), + 100: lightInk(0.07), + 200: lightInk(0.1), + 300: lightInk(0.16), + 400: lightInk(0.26), + 800: rgbaFromHex(LIGHT_TEXT_STRONG, 0.94), + }, + }), + + + purple: createSecondaryAccentScale({ + base: '#6b5a89', + hover: LIGHT_PURPLE_HOVER, + alpha: { 200: 0.14, 400: 0.36 }, + stops: { + 500: LIGHT_PURPLE, + }, + }), + + + semantic: createSemanticColors({ + success: LIGHT_SUCCESS, + warning: LIGHT_WARNING, + error: LIGHT_ERROR, + info: LIGHT_ACCENT, + bgAlpha: 0.08, + borderAlpha: 0.25, + overrides: { + infoBg: lightAccent(0.1), + infoBorder: lightAccent(0.28), + }, + }), + + border: { - subtle: 'rgba(100, 116, 139, 0.15)', - base: 'rgba(100, 116, 139, 0.22)', - medium: 'rgba(100, 116, 139, 0.32)', - strong: 'rgba(100, 116, 139, 0.42)', - prominent: 'rgba(100, 116, 139, 0.52)', + subtle: lightAccent(0.15), + base: lightAccent(0.22), + medium: lightAccent(0.32), + strong: lightAccent(0.42), + prominent: lightAccent(0.52), }, - - + + element: { - subtle: 'rgba(15, 23, 42, 0.045)', - soft: 'rgba(15, 23, 42, 0.065)', - base: 'rgba(15, 23, 42, 0.09)', - medium: 'rgba(15, 23, 42, 0.12)', - strong: 'rgba(15, 23, 42, 0.16)', - elevated: 'rgba(255, 255, 255, 0.92)', - }, - - - git: { - branch: 'rgb(71, 85, 105)', - branchBg: 'rgba(71, 85, 105, 0.1)', - changes: 'rgb(192, 140, 66)', - changesBg: 'rgba(192, 140, 66, 0.08)', - added: 'rgb(91, 154, 111)', - addedBg: 'rgba(91, 154, 111, 0.08)', - deleted: 'rgb(194, 101, 101)', - deletedBg: 'rgba(194, 101, 101, 0.08)', - staged: 'rgb(91, 154, 111)', - stagedBg: 'rgba(91, 154, 111, 0.08)', + subtle: lightInk(0.045), + soft: lightInk(0.065), + base: lightInk(0.09), + medium: lightInk(0.12), + strong: lightInk(0.16), + elevated: rgbaFromHex(STATIC_WHITE, 0.92), }, + + + git: createGitColors({ + branch: rgbFromHex(LIGHT_ACCENT_HOVER), + branchBg: lightAccentHover(0.1), + changes: rgbFromHex(LIGHT_WARNING), + changesBg: lightWarning(0.08), + added: rgbFromHex(LIGHT_SUCCESS), + addedBg: lightSuccess(0.08), + deleted: rgbFromHex(LIGHT_ERROR), + deletedBg: lightError(0.08), + }), }, - - + + effects: { shadow: { - - xs: '0 1px 2px rgba(71, 85, 105, 0.06)', - sm: '0 2px 4px rgba(71, 85, 105, 0.08)', - base: '0 4px 8px rgba(71, 85, 105, 0.1)', - lg: '0 8px 16px rgba(71, 85, 105, 0.12)', - xl: '0 12px 24px rgba(71, 85, 105, 0.14)', - '2xl': '0 16px 32px rgba(71, 85, 105, 0.16)', - }, - - - glow: { - blue: '0 8px 24px rgba(15, 23, 42, 0.08), 0 4px 12px rgba(15, 23, 42, 0.05), 0 2px 6px rgba(71, 85, 105, 0.04)', - purple: '0 8px 24px rgba(15, 23, 42, 0.07), 0 4px 12px rgba(100, 116, 139, 0.06), 0 2px 6px rgba(71, 85, 105, 0.04)', - mixed: '0 8px 24px rgba(15, 23, 42, 0.07), 0 4px 12px rgba(15, 23, 42, 0.05), 0 2px 6px rgba(71, 85, 105, 0.04)', + + xs: `0 1px 2px ${lightAccentHover(0.06)}`, + sm: `0 2px 4px ${lightAccentHover(0.08)}`, + base: `0 4px 8px ${lightAccentHover(0.1)}`, + lg: `0 8px 16px ${lightAccentHover(0.12)}`, + xl: `0 12px 24px ${lightAccentHover(0.14)}`, }, - + + blur: { subtle: 'blur(4px) saturate(1.02)', base: 'blur(8px) saturate(1.05)', - medium: 'blur(12px) saturate(1.08)', - strong: 'blur(16px) saturate(1.10) brightness(1.02)', - intense: 'blur(20px) saturate(1.12) brightness(1.03)', }, - + radius: createStandardRadius(), - + spacing: createStandardSpacing(), - + opacity: { disabled: 0.55, hover: 0.75, focus: 0.9, - overlay: 0.35, }, }, - - + + motion: { duration: { instant: '0.1s', fast: '0.15s', base: '0.3s', slow: '0.6s', - lazy: '1s', }, - + easing: createStandardEasing(), }, - - + + typography: createStandardTypography(), - - + + components: { - - windowControls: createWindowControls({ - standard: { - dot: 'rgba(100, 116, 139, 0.5)', - dotShadow: '0 0 4px rgba(15, 23, 42, 0.12)', - hoverBg: 'rgba(15, 23, 42, 0.08)', - hoverColor: '#475569', - hoverBorder: 'rgba(100, 116, 139, 0.28)', - hoverShadow: '0 2px 8px rgba(15, 23, 42, 0.08), inset 0 1px 0 rgba(255, 255, 255, 0.6)', - }, - close: { - dot: 'rgba(194, 101, 101, 0.55)', - dotShadow: '0 0 4px rgba(194, 101, 101, 0.2)', - hoverBg: 'rgba(194, 101, 101, 0.14)', - hoverColor: '#a85555', - hoverBorder: 'rgba(194, 101, 101, 0.25)', - hoverShadow: '0 2px 8px rgba(194, 101, 101, 0.15), inset 0 1px 0 rgba(255, 255, 255, 0.6)', - }, - common: { - defaultColor: 'rgba(30, 41, 59, 0.95)', - defaultDot: 'rgba(100, 116, 139, 0.28)', - disabledDot: 'rgba(100, 116, 139, 0.15)', - flowGradient: 'linear-gradient(90deg, transparent, rgba(100, 116, 139, 0.06), rgba(100, 116, 139, 0.1), rgba(100, 116, 139, 0.06), transparent)', - }, - }), - + + windowControls: createWindowControls('#a85555'), + button: { - - default: { - background: 'rgba(15, 23, 42, 0.07)', - color: '#475569', - border: 'transparent', - shadow: 'none', - }, - hover: { - background: 'rgba(15, 23, 42, 0.11)', - color: '#334155', - border: 'transparent', - shadow: 'none', - transform: 'none', - }, - active: { - background: 'rgba(15, 23, 42, 0.09)', - color: '#334155', - border: 'transparent', - shadow: 'none', - transform: 'none', - }, - - + + + primary: { default: { - background: '#000000', - color: '#ffffff', + background: STATIC_BLACK, + color: STATIC_WHITE, border: 'transparent', shadow: 'none', }, hover: { background: '#262626', - color: '#ffffff', + color: STATIC_WHITE, border: 'transparent', shadow: 'none', transform: 'none', }, active: { - background: '#1a1a1a', - color: '#ffffff', + background: '#1c1c1f', + color: STATIC_WHITE, border: 'transparent', shadow: 'none', transform: 'none', }, }, - - + + ghost: { default: { - background: 'transparent', - color: '#475569', - border: 'transparent', - shadow: 'none', + color: LIGHT_ACCENT_HOVER, }, hover: { - background: 'rgba(15, 23, 42, 0.08)', - color: '#334155', - border: 'transparent', - shadow: 'none', - transform: 'none', - }, - active: { - background: 'rgba(15, 23, 42, 0.055)', - color: '#334155', + background: lightInk(0.08), + color: LIGHT_TEXT_STRONG, border: 'transparent', - shadow: 'none', - transform: 'none', }, }, }, }, - - + + monaco: { base: 'vs', inherit: true, rules: [ - { token: 'comment', foreground: '94a3b8', fontStyle: 'italic' }, - { token: 'keyword', foreground: '6b5a89' }, - { token: 'string', foreground: '5b9a6f' }, - { token: 'number', foreground: 'b8863a' }, + { token: 'comment', foreground: '94a3b8', fontStyle: 'italic' }, + { token: 'keyword', foreground: '6b5a89' }, + { token: 'string', foreground: '5b9a6f' }, + { token: 'number', foreground: 'b8863a' }, { token: 'type', foreground: '475569' }, { token: 'class', foreground: '475569' }, - { token: 'function', foreground: '7c6b99' }, - { token: 'variable', foreground: '475569' }, - { token: 'constant', foreground: 'c08c42' }, - { token: 'operator', foreground: '6b5a89' }, + { token: 'function', foreground: '7c6b99' }, + { token: 'variable', foreground: '475569' }, + { token: 'constant', foreground: 'c08c42' }, + { token: 'operator', foreground: '6b5a89' }, { token: 'tag', foreground: '475569' }, - { token: 'attribute.name', foreground: '7c6b99' }, - { token: 'attribute.value', foreground: '5b9a6f' }, + { token: 'attribute.name', foreground: '7c6b99' }, + { token: 'attribute.value', foreground: '5b9a6f' }, ], colors: { - background: '#f7f8fa', - foreground: '#1e293b', - lineHighlight: '#f0f4f8', - selection: 'rgba(15, 23, 42, 0.14)', - cursor: '#1e293b', - - 'editor.selectionBackground': 'rgba(15, 23, 42, 0.14)', - 'editor.selectionForeground': '#1e293b', - 'editor.inactiveSelectionBackground': 'rgba(15, 23, 42, 0.09)', - 'editor.selectionHighlightBackground': 'rgba(15, 23, 42, 0.1)', - 'editor.selectionHighlightBorder': 'rgba(15, 23, 42, 0.22)', - 'editorCursor.foreground': '#1e293b', - - 'editor.wordHighlightBackground': 'rgba(15, 23, 42, 0.07)', - 'editor.wordHighlightStrongBackground': 'rgba(15, 23, 42, 0.11)', + background: '#f3f3f5', + foreground: LIGHT_TEXT_PRIMARY, + lineHighlight: '#f0f4f8', + selection: lightInk(0.14), + cursor: LIGHT_TEXT_PRIMARY, + + 'editor.selectionBackground': lightInk(0.14), + 'editor.selectionForeground': LIGHT_TEXT_PRIMARY, + 'editor.inactiveSelectionBackground': lightInk(0.09), + 'editor.selectionHighlightBackground': lightInk(0.1), + 'editor.selectionHighlightBorder': lightInk(0.22), + 'editorCursor.foreground': LIGHT_TEXT_PRIMARY, + + 'editor.wordHighlightBackground': lightInk(0.07), + 'editor.wordHighlightStrongBackground': lightInk(0.11), }, }, }; @@ -323,4 +270,3 @@ export const bitfunLightTheme: ThemeConfig = { - diff --git a/src/web-ui/src/infrastructure/theme/presets/midnight-theme.ts b/src/web-ui/src/infrastructure/theme/presets/midnight-theme.ts index 68e5c42310..a261f75aab 100644 --- a/src/web-ui/src/infrastructure/theme/presets/midnight-theme.ts +++ b/src/web-ui/src/infrastructure/theme/presets/midnight-theme.ts @@ -1,275 +1,199 @@ - + import { ThemeConfig } from '../types'; import { + createAccentScale, + createGitColors, + createSemanticColors, + createSecondaryAccentScale, createStandardEasing, createStandardRadius, createStandardSpacing, createStandardTypography, createWindowControls, + overlayBlack, + overlayWhite, + rgbFromHex, + rgbaFromHex, } from './shared'; +const MIDNIGHT_BACKGROUND = '#2b2d30'; +const MIDNIGHT_BACKGROUND_SECONDARY = '#1c1c1f'; +const MIDNIGHT_TEXT_PRIMARY = '#c8c8c8'; +const MIDNIGHT_BUTTON_TEXT = '#b0b0b0'; +const MIDNIGHT_ACCENT = '#60a5fa'; +const MIDNIGHT_ACCENT_HOVER = '#3b82f6'; +const MIDNIGHT_PURPLE = '#9c78ff'; +const MIDNIGHT_PURPLE_HOVER = '#8b5cf6'; +const MIDNIGHT_SUCCESS = '#6aab73'; +const MIDNIGHT_WARNING = '#e0a055'; +const MIDNIGHT_ERROR = '#cc7f7a'; +const MIDNIGHT_CONTROL_ERROR = '#ef4444'; + +const midnightBackground = (alpha: number | string) => rgbaFromHex(MIDNIGHT_BACKGROUND, alpha); +const midnightText = (alpha: number | string) => rgbaFromHex(MIDNIGHT_TEXT_PRIMARY, alpha); +const midnightAccent = (alpha: number | string) => rgbaFromHex(MIDNIGHT_ACCENT, alpha); +const midnightSuccess = (alpha: number | string) => rgbaFromHex(MIDNIGHT_SUCCESS, alpha); +const midnightWarning = (alpha: number | string) => rgbaFromHex(MIDNIGHT_WARNING, alpha); +const midnightError = (alpha: number | string) => rgbaFromHex(MIDNIGHT_ERROR, alpha); + export const bitfunMidnightTheme: ThemeConfig = { - + id: 'bitfun-midnight', name: 'Midnight', type: 'dark', description: 'Midnight gray dark theme - Professional and elegant, inspired by JetBrains IDE', author: 'BitFun Team', version: '1.0.0', - - + + colors: { background: { - primary: '#2b2d30', - secondary: '#1e1f22', - tertiary: '#313335', - quaternary: '#3c3f41', - elevated: '#2b2d30', - workbench: '#212121', - scene: '#27292c', - tooltip: 'rgba(43, 45, 48, 0.94)', + primary: MIDNIGHT_BACKGROUND, + secondary: MIDNIGHT_BACKGROUND_SECONDARY, + tertiary: '#313335', + quaternary: '#3d3d3d', + elevated: MIDNIGHT_BACKGROUND, + workbench: MIDNIGHT_BACKGROUND_SECONDARY, + scene: MIDNIGHT_BACKGROUND, + tooltip: midnightBackground(0.94), }, - + text: { - primary: '#bcbec4', - secondary: '#9da0a8', - muted: '#6f737a', - disabled: '#4e5157', - }, - - accent: { - 50: 'rgba(88, 166, 255, 0.04)', - 100: 'rgba(88, 166, 255, 0.08)', - 200: 'rgba(88, 166, 255, 0.15)', - 300: 'rgba(88, 166, 255, 0.25)', - 400: 'rgba(88, 166, 255, 0.4)', - 500: '#58a6ff', - 600: '#3b82f6', - 700: 'rgba(59, 130, 246, 0.8)', - 800: 'rgba(59, 130, 246, 0.9)', + primary: MIDNIGHT_TEXT_PRIMARY, + secondary: '#a1a1aa', + muted: '#6f737a', + disabled: '#555555', }, - - purple: { - 50: 'rgba(156, 120, 255, 0.04)', - 100: 'rgba(156, 120, 255, 0.08)', - 200: 'rgba(156, 120, 255, 0.15)', - 300: 'rgba(156, 120, 255, 0.25)', - 400: 'rgba(156, 120, 255, 0.4)', - 500: '#9c78ff', - 600: '#8b5cf6', - 700: 'rgba(139, 92, 246, 0.8)', - 800: 'rgba(139, 92, 246, 0.9)', - }, - - semantic: { - success: '#6aab73', - successBg: 'rgba(106, 171, 115, 0.1)', - successBorder: 'rgba(106, 171, 115, 0.3)', - - warning: '#e0a055', - warningBg: 'rgba(224, 160, 85, 0.1)', - warningBorder: 'rgba(224, 160, 85, 0.3)', - - error: '#cc7f7a', - errorBg: 'rgba(204, 127, 122, 0.1)', - errorBorder: 'rgba(204, 127, 122, 0.3)', - - info: '#58a6ff', - infoBg: 'rgba(88, 166, 255, 0.1)', - infoBorder: 'rgba(88, 166, 255, 0.3)', - - - highlight: '#d4a574', - highlightBg: 'rgba(212, 165, 116, 0.15)', - }, - + + accent: createAccentScale({ base: MIDNIGHT_ACCENT, hover: MIDNIGHT_ACCENT_HOVER }), + + purple: createSecondaryAccentScale({ base: MIDNIGHT_PURPLE, hover: MIDNIGHT_PURPLE_HOVER }), + + semantic: createSemanticColors({ + success: MIDNIGHT_SUCCESS, + warning: MIDNIGHT_WARNING, + error: MIDNIGHT_ERROR, + info: MIDNIGHT_ACCENT, + }), + border: { - subtle: 'rgba(255, 255, 255, 0.08)', - base: 'rgba(255, 255, 255, 0.14)', - medium: 'rgba(255, 255, 255, 0.2)', - strong: 'rgba(255, 255, 255, 0.26)', - prominent: 'rgba(255, 255, 255, 0.35)', + subtle: overlayWhite(0.08), + base: overlayWhite(0.14), + medium: overlayWhite(0.2), + strong: overlayWhite(0.26), + prominent: overlayWhite(0.35), }, - + element: { - subtle: 'rgba(255, 255, 255, 0.04)', - soft: 'rgba(255, 255, 255, 0.06)', - base: 'rgba(255, 255, 255, 0.09)', - medium: 'rgba(255, 255, 255, 0.12)', - strong: 'rgba(255, 255, 255, 0.15)', - elevated: 'rgba(255, 255, 255, 0.18)', - }, - - git: { - branch: 'rgb(88, 166, 255)', - branchBg: 'rgba(88, 166, 255, 0.1)', - changes: 'rgb(224, 160, 85)', - changesBg: 'rgba(224, 160, 85, 0.1)', - added: 'rgb(106, 171, 115)', - addedBg: 'rgba(106, 171, 115, 0.1)', - deleted: 'rgb(204, 127, 122)', - deletedBg: 'rgba(204, 127, 122, 0.1)', - staged: 'rgb(106, 171, 115)', - stagedBg: 'rgba(106, 171, 115, 0.1)', + subtle: overlayWhite(0.04), + soft: overlayWhite(0.06), + base: overlayWhite(0.09), + medium: overlayWhite(0.12), + strong: overlayWhite(0.15), + elevated: overlayWhite(0.18), }, + + git: createGitColors({ + branch: rgbFromHex(MIDNIGHT_ACCENT), + branchBg: midnightAccent(0.1), + changes: rgbFromHex(MIDNIGHT_WARNING), + changesBg: midnightWarning(0.1), + added: rgbFromHex(MIDNIGHT_SUCCESS), + addedBg: midnightSuccess(0.1), + deleted: rgbFromHex(MIDNIGHT_ERROR), + deletedBg: midnightError(0.1), + }), }, - - + + effects: { shadow: { - xs: '0 1px 2px rgba(0, 0, 0, 0.8)', - sm: '0 2px 4px rgba(0, 0, 0, 0.75)', - base: '0 4px 8px rgba(0, 0, 0, 0.7)', - lg: '0 8px 16px rgba(0, 0, 0, 0.65)', - xl: '0 12px 24px rgba(0, 0, 0, 0.8)', - '2xl': '0 16px 32px rgba(0, 0, 0, 0.85)', + xs: `0 1px 2px ${overlayBlack(0.8)}`, + sm: `0 2px 4px ${overlayBlack(0.75)}`, + base: `0 4px 8px ${overlayBlack(0.7)}`, + lg: `0 8px 16px ${overlayBlack(0.65)}`, + xl: `0 12px 24px ${overlayBlack(0.8)}`, }, - - glow: { - blue: '0 12px 32px rgba(88, 166, 255, 0.25), 0 6px 16px rgba(88, 166, 255, 0.18), 0 3px 8px rgba(0, 0, 0, 0.15)', - purple: '0 12px 32px rgba(156, 120, 255, 0.25), 0 6px 16px rgba(156, 120, 255, 0.18), 0 3px 8px rgba(0, 0, 0, 0.15)', - mixed: '0 12px 32px rgba(88, 166, 255, 0.2), 0 6px 16px rgba(156, 120, 255, 0.18), 0 3px 8px rgba(0, 0, 0, 0.15)', - }, - + blur: { subtle: 'blur(4px) saturate(1.1)', base: 'blur(8px) saturate(1.2)', - medium: 'blur(12px) saturate(1.25)', - strong: 'blur(16px) saturate(1.3) brightness(1.05)', - intense: 'blur(20px) saturate(1.4) brightness(1.1)', }, - + radius: createStandardRadius(), - + spacing: createStandardSpacing(), - + opacity: { disabled: 0.5, hover: 0.8, focus: 0.9, - overlay: 0.4, }, }, - - + + motion: { duration: { instant: '0.1s', fast: '0.15s', base: '0.3s', slow: '0.6s', - lazy: '1s', }, - + easing: createStandardEasing(), }, - - + + typography: createStandardTypography(), - - + + components: { - - windowControls: createWindowControls({ - standard: { - dot: 'rgba(88, 166, 255, 0.45)', - dotShadow: '0 0 4px rgba(88, 166, 255, 0.2)', - hoverBg: 'rgba(88, 166, 255, 0.12)', - hoverColor: '#58a6ff', - hoverBorder: 'rgba(88, 166, 255, 0.2)', - hoverShadow: '0 2px 8px rgba(88, 166, 255, 0.15), inset 0 1px 0 rgba(255, 255, 255, 0.1)', - }, - close: { - dot: 'rgba(239, 68, 68, 0.45)', - dotShadow: '0 0 4px rgba(239, 68, 68, 0.2)', - hoverBg: 'rgba(239, 68, 68, 0.12)', - hoverColor: '#ef4444', - hoverBorder: 'rgba(239, 68, 68, 0.2)', - hoverShadow: '0 2px 8px rgba(239, 68, 68, 0.15), inset 0 1px 0 rgba(255, 255, 255, 0.1)', - }, - common: { - defaultColor: 'rgba(188, 190, 196, 0.9)', - defaultDot: 'rgba(188, 190, 196, 0.2)', - disabledDot: 'rgba(188, 190, 196, 0.1)', - flowGradient: 'linear-gradient(90deg, transparent, rgba(188, 190, 196, 0.05), rgba(188, 190, 196, 0.08), rgba(188, 190, 196, 0.05), transparent)', - }, - }), - + + windowControls: createWindowControls(MIDNIGHT_CONTROL_ERROR), + button: { - - default: { - background: 'rgba(188, 190, 196, 0.11)', - color: '#949699', - border: 'transparent', - shadow: 'none', - }, - hover: { - background: 'rgba(188, 190, 196, 0.17)', - color: '#afb1b5', - border: 'transparent', - shadow: 'none', - transform: 'none', - }, - active: { - background: 'rgba(188, 190, 196, 0.14)', - color: '#afb1b5', - border: 'transparent', - shadow: 'none', - transform: 'none', - }, - - + + + primary: { default: { - background: 'rgba(88, 166, 255, 0.2)', + background: midnightAccent(0.2), color: '#6aa8e8', border: 'transparent', shadow: 'none', }, hover: { - background: 'rgba(88, 166, 255, 0.3)', + background: midnightAccent(0.3), color: '#8fc0f0', border: 'transparent', shadow: 'none', transform: 'none', }, active: { - background: 'rgba(88, 166, 255, 0.24)', + background: midnightAccent(0.24), color: '#8fc0f0', border: 'transparent', shadow: 'none', transform: 'none', }, }, - - + + ghost: { default: { - background: 'transparent', - color: '#949699', - border: 'transparent', - shadow: 'none', + color: '#9a9a9a', }, hover: { - background: 'rgba(188, 190, 196, 0.13)', - color: '#afb1b5', - border: 'transparent', - shadow: 'none', - transform: 'none', - }, - active: { - background: 'rgba(188, 190, 196, 0.11)', - color: '#afb1b5', + background: midnightText(0.13), + color: MIDNIGHT_BUTTON_TEXT, border: 'transparent', - shadow: 'none', - transform: 'none', }, }, }, }, - - + + monaco: { base: 'vs-dark', inherit: true, @@ -289,12 +213,11 @@ export const bitfunMidnightTheme: ThemeConfig = { { token: 'attribute.value', foreground: 'a5c261' }, ], colors: { - background: '#2b2d30', - foreground: '#bcbec4', + background: MIDNIGHT_BACKGROUND, + foreground: MIDNIGHT_TEXT_PRIMARY, lineHighlight: '#313335', selection: '#3d4752', - cursor: '#58a6ff', + cursor: MIDNIGHT_ACCENT, }, }, }; - diff --git a/src/web-ui/src/infrastructure/theme/presets/shared.ts b/src/web-ui/src/infrastructure/theme/presets/shared.ts index bbb57b497e..4349733865 100644 --- a/src/web-ui/src/infrastructure/theme/presets/shared.ts +++ b/src/web-ui/src/infrastructure/theme/presets/shared.ts @@ -1,24 +1,151 @@ import type { + AccentColors, BorderColors, ElementBackgrounds, + GitColors, RadiusConfig, ScrollbarColors, + SecondaryAccentColors, + SemanticColors, ThemeConfig, WindowControlsConfig, } from '../types'; -type WindowControlState = WindowControlsConfig['minimize']; +export const STATIC_BLACK = '#000000'; +export const STATIC_WHITE = '#ffffff'; + +function hexToRgbChannels(hex: string): [number, number, number] { + const raw = hex.trim().replace(/^#/, ''); + const expanded = raw.length === 3 + ? raw.split('').map(channel => channel + channel).join('') + : raw; + if (!/^[0-9a-f]{6}$/i.test(expanded)) { + throw new Error(`Invalid hex color: ${hex}`); + } + const value = Number.parseInt(expanded, 16); + return [ + (value >> 16) & 255, + (value >> 8) & 255, + value & 255, + ]; +} + +export function rgbFromHex(hex: string): string { + const [r, g, b] = hexToRgbChannels(hex); + return `rgb(${r}, ${g}, ${b})`; +} + +export function rgbaFromHex(hex: string, alpha: number | string): string { + const [r, g, b] = hexToRgbChannels(hex); + return `rgba(${r}, ${g}, ${b}, ${alpha})`; +} + +export function overlayBlack(alpha: number | string): string { + return rgbaFromHex(STATIC_BLACK, alpha); +} + +export function overlayWhite(alpha: number | string): string { + return rgbaFromHex(STATIC_WHITE, alpha); +} + +export interface AccentScaleInput { + base: string; + hover?: string; + stops?: Partial; + alpha?: Partial>; +} + +export interface SecondaryAccentScaleInput { + base: string; + hover?: string; + stops?: Partial; + alpha?: Partial>; +} + +export interface SemanticColorsInput { + success: string; + warning: string; + error: string; + info: string; + bgAlpha?: number | string; + borderAlpha?: number | string; + overrides?: Partial; +} + +export function createAccentScale(input: AccentScaleInput): AccentColors { + const hover = input.hover ?? input.base; + const alpha = { + 50: 0.04, + 100: 0.08, + 200: 0.15, + 300: 0.25, + 400: 0.4, + 700: 0.8, + 800: 0.9, + ...input.alpha, + }; -export function createWindowControls(config: { - standard: WindowControlState; - close: WindowControlState; - common: WindowControlsConfig['common']; -}): WindowControlsConfig { return { - minimize: { ...config.standard }, - maximize: { ...config.standard }, - close: { ...config.close }, - common: { ...config.common }, + 50: rgbaFromHex(input.base, alpha[50]), + 100: rgbaFromHex(input.base, alpha[100]), + 200: rgbaFromHex(input.base, alpha[200]), + 300: rgbaFromHex(input.base, alpha[300]), + 400: rgbaFromHex(input.base, alpha[400]), + 500: input.base, + 600: hover, + 700: rgbaFromHex(hover, alpha[700]), + 800: rgbaFromHex(hover, alpha[800]), + ...input.stops, + }; +} + +export function createSecondaryAccentScale(input: SecondaryAccentScaleInput): SecondaryAccentColors { + const hover = input.hover ?? input.base; + const alpha = { + 50: 0.04, + 100: 0.08, + 200: 0.15, + 400: 0.4, + 800: 0.9, + ...input.alpha, + }; + + return { + 50: rgbaFromHex(input.base, alpha[50]), + 100: rgbaFromHex(input.base, alpha[100]), + 200: rgbaFromHex(input.base, alpha[200]), + 400: rgbaFromHex(input.base, alpha[400]), + 500: input.base, + 600: hover, + 800: rgbaFromHex(hover, alpha[800]), + ...input.stops, + }; +} + +export function createSemanticColors(input: SemanticColorsInput): SemanticColors { + const bgAlpha = input.bgAlpha ?? 0.1; + const borderAlpha = input.borderAlpha ?? 0.3; + + return { + success: input.success, + successBg: rgbaFromHex(input.success, bgAlpha), + successBorder: rgbaFromHex(input.success, borderAlpha), + warning: input.warning, + warningBg: rgbaFromHex(input.warning, bgAlpha), + warningBorder: rgbaFromHex(input.warning, borderAlpha), + error: input.error, + errorBg: rgbaFromHex(input.error, bgAlpha), + errorBorder: rgbaFromHex(input.error, borderAlpha), + info: input.info, + infoBg: rgbaFromHex(input.info, bgAlpha), + infoBorder: rgbaFromHex(input.info, borderAlpha), + ...input.overrides, + }; +} + +export function createWindowControls(closeHoverColor: WindowControlsConfig['close']['hoverColor']): WindowControlsConfig { + return { + close: { hoverColor: closeHoverColor }, }; } @@ -43,7 +170,6 @@ export function createStandardTypography(): ThemeConfig['typography'] { '2xl': '18px', '3xl': '22px', '4xl': '26px', - '5xl': '32px', }, lineHeight: { tight: 1.2, @@ -85,7 +211,6 @@ export function createChinaTypography(): ThemeConfig['typography'] { '2xl': '18px', '3xl': '22px', '4xl': '26px', - '5xl': '32px', }, lineHeight: { tight: 1.3, @@ -147,36 +272,44 @@ export function createStandardEasing(smooth = 'cubic-bezier(0.4, 0, 0.2, 1)'): T return { standard: 'cubic-bezier(0.4, 0, 0.2, 1)', decelerate: 'cubic-bezier(0, 0, 0.2, 1)', - accelerate: 'cubic-bezier(0.4, 0, 1, 1)', - bounce: 'cubic-bezier(0.68, -0.55, 0.265, 1.55)', smooth, }; } export function createDarkNeutralBorder(): BorderColors { return { - subtle: 'rgba(255, 255, 255, 0.12)', - base: 'rgba(255, 255, 255, 0.18)', - medium: 'rgba(255, 255, 255, 0.24)', - strong: 'rgba(255, 255, 255, 0.32)', - prominent: 'rgba(255, 255, 255, 0.4)', + subtle: overlayWhite(0.12), + base: overlayWhite(0.18), + medium: overlayWhite(0.24), + strong: overlayWhite(0.3), + prominent: overlayWhite(0.4), }; } export function createDarkNeutralElement(): ElementBackgrounds { return { - subtle: 'rgba(255, 255, 255, 0.05)', - soft: 'rgba(255, 255, 255, 0.07)', - base: 'rgba(255, 255, 255, 0.095)', - medium: 'rgba(255, 255, 255, 0.125)', - strong: 'rgba(255, 255, 255, 0.155)', - elevated: 'rgba(255, 255, 255, 0.19)', + subtle: overlayWhite(0.06), + soft: overlayWhite(0.06), + base: overlayWhite(0.1), + medium: overlayWhite(0.12), + strong: overlayWhite(0.15), + elevated: overlayWhite(0.2), + }; +} + +export function createGitColors( + config: Omit & Partial>, +): GitColors { + return { + ...config, + staged: config.staged ?? config.added, + stagedBg: config.stagedBg ?? config.addedBg, }; } export function createDarkNeutralScrollbar(): ScrollbarColors { return { - thumb: 'rgba(255, 255, 255, 0.15)', - thumbHover: 'rgba(255, 255, 255, 0.28)', + thumb: overlayWhite(0.15), + thumbHover: overlayWhite(0.3), }; } diff --git a/src/web-ui/src/infrastructure/theme/presets/slate-theme.ts b/src/web-ui/src/infrastructure/theme/presets/slate-theme.ts index bdde4dfd0b..cbb475d2be 100644 --- a/src/web-ui/src/infrastructure/theme/presets/slate-theme.ts +++ b/src/web-ui/src/infrastructure/theme/presets/slate-theme.ts @@ -1,19 +1,46 @@ - + import { ThemeConfig } from '../types'; import { + createAccentScale, createDarkNeutralBorder, createDarkNeutralElement, createDarkNeutralScrollbar, + createGitColors, + createSemanticColors, + createSecondaryAccentScale, createSlateRadius, createStandardEasing, createStandardSpacing, createStandardTypography, createWindowControls, + overlayBlack, + overlayWhite, + rgbFromHex, + rgbaFromHex, + STATIC_WHITE, } from './shared'; +const SLATE_BACKGROUND_PRIMARY = '#1c1c1f'; +const SLATE_BACKGROUND_SECONDARY = '#262626'; +const SLATE_TEXT_PRIMARY = '#e8e8e8'; +const SLATE_TEXT_MUTED = '#a8b0bd'; +const SLATE_BUTTON_TEXT = SLATE_TEXT_PRIMARY; +const SLATE_ACCENT_SOFT = '#e8e8e8'; +const SLATE_ACCENT = '#94a3b8'; +const SLATE_ACCENT_HOVER = '#64748b'; +const SLATE_PURPLE = '#b8c4ff'; +const SLATE_PURPLE_HOVER = '#9dacf5'; +const SLATE_SUCCESS = '#7eb09b'; +const SLATE_WARNING = '#f59e0b'; +const SLATE_ERROR = '#c9878d'; + +const slateSuccess = (alpha: number | string) => rgbaFromHex(SLATE_SUCCESS, alpha); +const slateWarning = (alpha: number | string) => rgbaFromHex(SLATE_WARNING, alpha); +const slateError = (alpha: number | string) => rgbaFromHex(SLATE_ERROR, alpha); + export const bitfunSlateTheme: ThemeConfig = { - + id: 'bitfun-slate', name: 'Slate', type: 'dark', @@ -24,250 +51,162 @@ export const bitfunSlateTheme: ThemeConfig = { layout: { sceneViewportBorder: false, }, - + colors: { background: { - primary: '#14161a', - secondary: '#22262c', - tertiary: '#14161a', - quaternary: '#2c3038', - elevated: '#22262c', - workbench: '#14161a', - scene: '#22262c', - tooltip: 'rgba(34, 38, 44, 0.96)', + primary: SLATE_BACKGROUND_PRIMARY, + secondary: SLATE_BACKGROUND_SECONDARY, + tertiary: SLATE_BACKGROUND_PRIMARY, + quaternary: '#313335', + elevated: SLATE_BACKGROUND_SECONDARY, + workbench: SLATE_BACKGROUND_PRIMARY, + scene: SLATE_BACKGROUND_SECONDARY, + tooltip: rgbaFromHex(SLATE_BACKGROUND_SECONDARY, 0.96), }, - + text: { - primary: '#eef0f3', + primary: SLATE_TEXT_PRIMARY, secondary: '#c8ccd2', - muted: '#9ea4ab', - disabled: '#65696f', + muted: '#a1a1aa', + disabled: '#6a6a6a', }, - - + + // Cool gray accent — neutral chrome for slate surfaces (links, focus, nav tints). - accent: { - 50: 'rgba(226, 232, 240, 0.05)', - 100: 'rgba(226, 232, 240, 0.09)', - 200: 'rgba(203, 213, 225, 0.14)', - 300: 'rgba(203, 213, 225, 0.24)', - 400: 'rgba(148, 163, 184, 0.45)', - 500: '#94a3b8', - 600: '#64748b', - 700: 'rgba(100, 116, 139, 0.85)', - 800: 'rgba(71, 85, 105, 0.92)', - }, - - - purple: { - 50: 'rgba(184, 198, 255, 0.04)', - 100: 'rgba(184, 198, 255, 0.08)', - 200: 'rgba(184, 198, 255, 0.15)', - 300: 'rgba(184, 198, 255, 0.25)', - 400: 'rgba(184, 198, 255, 0.4)', - 500: '#b8c4ff', - 600: '#9dacf5', - 700: 'rgba(157, 172, 245, 0.8)', - 800: 'rgba(157, 172, 245, 0.9)', - }, - - semantic: { - success: '#7fb899', - successBg: 'rgba(127, 184, 153, 0.1)', - successBorder: 'rgba(127, 184, 153, 0.3)', - - warning: '#f59e0b', - warningBg: 'rgba(245, 158, 11, 0.1)', - warningBorder: 'rgba(245, 158, 11, 0.3)', - - error: '#c9878d', - errorBg: 'rgba(201, 135, 141, 0.1)', - errorBorder: 'rgba(201, 135, 141, 0.3)', - - info: '#a8b0bd', - infoBg: 'rgba(255, 255, 255, 0.07)', - infoBorder: 'rgba(255, 255, 255, 0.2)', - - - highlight: '#c8cdd4', - highlightBg: 'rgba(255, 255, 255, 0.1)', - }, - + accent: createAccentScale({ + base: SLATE_ACCENT, + hover: SLATE_ACCENT_HOVER, + alpha: { 700: 0.85 }, + stops: { + 50: rgbaFromHex(SLATE_ACCENT_SOFT, 0.05), + 100: rgbaFromHex(SLATE_ACCENT_SOFT, 0.09), + 200: 'rgba(203, 213, 225, 0.14)', + 300: 'rgba(203, 213, 225, 0.24)', + 400: 'rgba(148, 163, 184, 0.45)', + 800: 'rgba(71, 85, 105, 0.92)', + }, + }), + + + purple: createSecondaryAccentScale({ base: SLATE_PURPLE, hover: SLATE_PURPLE_HOVER }), + + semantic: createSemanticColors({ + success: SLATE_SUCCESS, + warning: SLATE_WARNING, + error: SLATE_ERROR, + info: SLATE_TEXT_MUTED, + overrides: { + infoBg: overlayWhite(0.07), + infoBorder: overlayWhite(0.2), + }, + }), + border: createDarkNeutralBorder(), - + element: createDarkNeutralElement(), - - git: { - branch: '#9ca6b8', - branchBg: 'rgba(255, 255, 255, 0.06)', - changes: 'rgb(245, 158, 11)', - changesBg: 'rgba(245, 158, 11, 0.1)', - added: 'rgb(127, 184, 153)', - addedBg: 'rgba(127, 184, 153, 0.1)', - deleted: 'rgb(201, 135, 141)', - deletedBg: 'rgba(201, 135, 141, 0.1)', - staged: 'rgb(127, 184, 153)', - stagedBg: 'rgba(127, 184, 153, 0.1)', - }, - + + git: createGitColors({ + branch: SLATE_ACCENT, + branchBg: overlayWhite(0.06), + changes: rgbFromHex(SLATE_WARNING), + changesBg: slateWarning(0.1), + added: rgbFromHex(SLATE_SUCCESS), + addedBg: slateSuccess(0.1), + deleted: rgbFromHex(SLATE_ERROR), + deletedBg: slateError(0.1), + }), + scrollbar: createDarkNeutralScrollbar(), }, - - + + effects: { shadow: { - xs: '0 1px 2px rgba(0, 0, 0, 0.85)', - sm: '0 2px 4px rgba(0, 0, 0, 0.8)', - base: '0 4px 8px rgba(0, 0, 0, 0.75)', - lg: '0 8px 16px rgba(0, 0, 0, 0.7)', - xl: '0 12px 24px rgba(0, 0, 0, 0.85)', - '2xl': '0 16px 32px rgba(0, 0, 0, 0.9)', + xs: `0 1px 2px ${overlayBlack(0.85)}`, + sm: `0 2px 4px ${overlayBlack(0.8)}`, + base: `0 4px 8px ${overlayBlack(0.75)}`, + lg: `0 8px 16px ${overlayBlack(0.7)}`, + xl: `0 12px 24px ${overlayBlack(0.85)}`, }, - - glow: { - blue: '0 12px 32px rgba(148, 163, 184, 0.14), 0 6px 16px rgba(148, 163, 184, 0.1), 0 3px 8px rgba(0, 0, 0, 0.2)', - purple: '0 12px 32px rgba(184, 198, 255, 0.2), 0 6px 16px rgba(184, 198, 255, 0.12), 0 3px 8px rgba(0, 0, 0, 0.2)', - mixed: '0 12px 32px rgba(255, 255, 255, 0.05), 0 6px 16px rgba(184, 198, 255, 0.1), 0 3px 8px rgba(0, 0, 0, 0.18)', - }, - + blur: { subtle: 'blur(4px) saturate(1.05) brightness(0.98)', base: 'blur(8px) saturate(1.08) brightness(0.98)', - medium: 'blur(12px) saturate(1.12) brightness(0.97)', - strong: 'blur(16px) saturate(1.15) brightness(0.97)', - intense: 'blur(20px) saturate(1.18) brightness(0.96)', }, - + radius: createSlateRadius(), - + spacing: createStandardSpacing(), - + opacity: { disabled: 0.5, hover: 0.75, focus: 0.85, - overlay: 0.5, }, }, - - + + motion: { duration: { instant: '0.08s', fast: '0.12s', base: '0.25s', slow: '0.5s', - lazy: '0.8s', }, - + easing: createStandardEasing(), }, - - + + typography: createStandardTypography(), - - + + components: { - - windowControls: createWindowControls({ - standard: { - dot: 'rgba(203, 213, 225, 0.42)', - dotShadow: '0 0 4px rgba(0, 0, 0, 0.35)', - hoverBg: 'rgba(255, 255, 255, 0.09)', - hoverColor: '#e2e6eb', - hoverBorder: 'rgba(255, 255, 255, 0.14)', - hoverShadow: '0 2px 8px rgba(0, 0, 0, 0.22), inset 0 1px 0 rgba(255, 255, 255, 0.06)', - }, - close: { - dot: 'rgba(201, 135, 141, 0.5)', - dotShadow: '0 0 4px rgba(201, 135, 141, 0.25)', - hoverBg: 'rgba(201, 135, 141, 0.15)', - hoverColor: '#c9878d', - hoverBorder: 'rgba(201, 135, 141, 0.25)', - hoverShadow: '0 2px 8px rgba(201, 135, 141, 0.18), inset 0 1px 0 rgba(255, 255, 255, 0.08)', - }, - common: { - defaultColor: 'rgba(232, 234, 236, 0.92)', - defaultDot: 'rgba(198, 202, 208, 0.48)', - disabledDot: 'rgba(168, 171, 176, 0.2)', - flowGradient: 'linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.05), rgba(255, 255, 255, 0.08), rgba(255, 255, 255, 0.05), transparent)', - }, - }), - + + windowControls: createWindowControls(SLATE_ERROR), + button: { - - default: { - background: 'rgba(255, 255, 255, 0.08)', - color: '#a8b0bd', - border: 'transparent', - shadow: 'none', - }, - hover: { - background: 'rgba(255, 255, 255, 0.12)', - color: '#dce0e6', - border: 'transparent', - shadow: 'none', - transform: 'none', - }, - active: { - background: 'rgba(255, 255, 255, 0.1)', - color: '#dce0e6', - border: 'transparent', - shadow: 'none', - transform: 'none', - }, - - + + + primary: { default: { - background: 'rgba(255, 255, 255, 0.14)', - color: '#f0f2f5', + background: overlayWhite(0.14), + color: SLATE_TEXT_PRIMARY, border: 'transparent', shadow: 'none', }, hover: { - background: 'rgba(255, 255, 255, 0.2)', - color: '#ffffff', + background: overlayWhite(0.2), + color: STATIC_WHITE, border: 'transparent', shadow: 'none', transform: 'none', }, active: { - background: 'rgba(255, 255, 255, 0.17)', - color: '#ffffff', + background: overlayWhite(0.17), + color: STATIC_WHITE, border: 'transparent', shadow: 'none', transform: 'none', }, }, - - + + ghost: { default: { - background: 'transparent', - color: '#a8b0bd', - border: 'transparent', - shadow: 'none', + color: SLATE_TEXT_MUTED, }, hover: { - background: 'rgba(255, 255, 255, 0.08)', - color: '#dce0e6', + background: overlayWhite(0.08), + color: SLATE_BUTTON_TEXT, border: 'transparent', - shadow: 'none', - transform: 'none', - }, - active: { - background: 'rgba(255, 255, 255, 0.06)', - color: '#dce0e6', - border: 'transparent', - shadow: 'none', - transform: 'none', }, }, }, }, - - + + monaco: { base: 'vs-dark', inherit: true, @@ -287,10 +226,10 @@ export const bitfunSlateTheme: ThemeConfig = { { token: 'attribute.value', foreground: '8fc8a9' }, ], colors: { - background: '#1a1c1e', - foreground: '#eef0f3', - lineHighlight: '#22252a', - selection: 'rgba(255, 255, 255, 0.12)', + background: SLATE_BACKGROUND_PRIMARY, + foreground: SLATE_TEXT_PRIMARY, + lineHighlight: SLATE_BACKGROUND_SECONDARY, + selection: overlayWhite(0.12), cursor: '#aeb6c3', }, }, diff --git a/src/web-ui/src/infrastructure/theme/presets/startupThemeBootstrap.test.ts b/src/web-ui/src/infrastructure/theme/presets/startupThemeBootstrap.test.ts new file mode 100644 index 0000000000..eb3023a155 --- /dev/null +++ b/src/web-ui/src/infrastructure/theme/presets/startupThemeBootstrap.test.ts @@ -0,0 +1,97 @@ +import fs from 'node:fs'; +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; + +import { describe, expect, it } from 'vitest'; + +import { builtinThemes, DEFAULT_DARK_THEME_ID, DEFAULT_LIGHT_THEME_ID } from './index'; +import { createStartupThemeBootstrapManifest } from './startupThemeBootstrap'; +import { createThemePromptSnapshotManifest } from './themePromptSnapshots'; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); +const generatedManifestPath = path.resolve( + __dirname, + '../../../../../apps/desktop/src/generated/startup_theme_bootstrap.json', +); +const generatedThemePromptSnapshotPath = path.resolve( + __dirname, + '../../../../../crates/assembly/core/src/agentic/tools/implementations/generated/theme_prompt_snapshots.json', +); + +describe('startup theme bootstrap manifest', () => { + it('projects only the desktop first-paint theme fields from TS builtin themes', () => { + const manifest = createStartupThemeBootstrapManifest(builtinThemes); + + expect(manifest).toMatchObject({ + version: 1, + defaultLightThemeId: DEFAULT_LIGHT_THEME_ID, + defaultDarkThemeId: DEFAULT_DARK_THEME_ID, + }); + expect(manifest.themes).toHaveLength(builtinThemes.length); + expect(new Set(manifest.themes.map(theme => theme.id)).size).toBe(builtinThemes.length); + expect(manifest.themes.map(theme => theme.id)).toEqual( + expect.arrayContaining([DEFAULT_LIGHT_THEME_ID, DEFAULT_DARK_THEME_ID]) + ); + + const light = manifest.themes.find(theme => theme.id === 'bitfun-light'); + const sourceLight = builtinThemes.find(theme => theme.id === 'bitfun-light'); + + expect(light).toEqual({ + id: sourceLight?.id, + bgPrimary: sourceLight?.colors.background.primary, + bgSecondary: sourceLight?.colors.background.secondary, + bgScene: sourceLight?.colors.background.scene, + isLight: true, + textPrimary: sourceLight?.colors.text.primary, + textMuted: sourceLight?.colors.text.muted, + accentColor: sourceLight?.colors.accent[500], + }); + }); + + it('keeps the committed desktop manifest synchronized with TS builtin themes', () => { + const generated = JSON.parse(fs.readFileSync(generatedManifestPath, 'utf8')); + + expect(generated).toEqual(createStartupThemeBootstrapManifest(builtinThemes)); + }); +}); + +describe('theme prompt snapshot manifest', () => { + it('projects generative UI prompt fields from TS builtin themes', () => { + const manifest = createThemePromptSnapshotManifest(builtinThemes); + + expect(manifest).toMatchObject({ + version: 1, + defaultLightThemeId: DEFAULT_LIGHT_THEME_ID, + defaultDarkThemeId: DEFAULT_DARK_THEME_ID, + }); + expect(manifest.themes).toHaveLength(builtinThemes.length); + + const dark = manifest.themes.find(theme => theme.id === 'bitfun-dark'); + const sourceDark = builtinThemes.find(theme => theme.id === 'bitfun-dark'); + + expect(dark).toEqual({ + id: sourceDark?.id, + themeType: sourceDark?.type, + bgPrimary: sourceDark?.colors.background.primary, + bgSecondary: sourceDark?.colors.background.secondary, + bgScene: sourceDark?.colors.background.scene, + textPrimary: sourceDark?.colors.text.primary, + textMuted: sourceDark?.colors.text.muted, + accent500: sourceDark?.colors.accent[500], + accent600: sourceDark?.colors.accent[600], + borderBase: sourceDark?.colors.border.base, + elementBase: sourceDark?.colors.element.base, + radiusBase: sourceDark?.effects.radius.base, + spacing4: sourceDark?.effects.spacing[4], + shadowBase: sourceDark?.effects.shadow.base, + styleNotes: sourceDark?.description, + }); + }); + + it('keeps the committed generative UI manifest synchronized with TS builtin themes', () => { + const generated = JSON.parse(fs.readFileSync(generatedThemePromptSnapshotPath, 'utf8')); + + expect(generated).toEqual(createThemePromptSnapshotManifest(builtinThemes)); + }); +}); diff --git a/src/web-ui/src/infrastructure/theme/presets/startupThemeBootstrap.ts b/src/web-ui/src/infrastructure/theme/presets/startupThemeBootstrap.ts new file mode 100644 index 0000000000..8713fccf0e --- /dev/null +++ b/src/web-ui/src/infrastructure/theme/presets/startupThemeBootstrap.ts @@ -0,0 +1,46 @@ +import type { ThemeConfig, ThemeId } from '../types'; +import { DEFAULT_DARK_THEME_ID, DEFAULT_LIGHT_THEME_ID } from './index'; + +export const STARTUP_THEME_BOOTSTRAP_VERSION = 1; + +export interface StartupThemeBootstrapEntry { + id: ThemeId; + bgPrimary: string; + bgSecondary: string; + bgScene: string; + isLight: boolean; + textPrimary: string; + textMuted: string; + accentColor: string; +} + +export interface StartupThemeBootstrapManifest { + version: typeof STARTUP_THEME_BOOTSTRAP_VERSION; + defaultLightThemeId: ThemeId; + defaultDarkThemeId: ThemeId; + themes: StartupThemeBootstrapEntry[]; +} + +export function createStartupThemeBootstrapEntry(theme: ThemeConfig): StartupThemeBootstrapEntry { + return { + id: theme.id, + bgPrimary: theme.colors.background.primary, + bgSecondary: theme.colors.background.secondary, + bgScene: theme.colors.background.scene, + isLight: theme.type === 'light', + textPrimary: theme.colors.text.primary, + textMuted: theme.colors.text.muted, + accentColor: theme.colors.accent[500], + }; +} + +export function createStartupThemeBootstrapManifest( + themes: readonly ThemeConfig[], +): StartupThemeBootstrapManifest { + return { + version: STARTUP_THEME_BOOTSTRAP_VERSION, + defaultLightThemeId: DEFAULT_LIGHT_THEME_ID, + defaultDarkThemeId: DEFAULT_DARK_THEME_ID, + themes: themes.map(createStartupThemeBootstrapEntry), + }; +} diff --git a/src/web-ui/src/infrastructure/theme/presets/themePresetOutput.test.ts b/src/web-ui/src/infrastructure/theme/presets/themePresetOutput.test.ts index d3b34546af..c37c83096e 100644 --- a/src/web-ui/src/infrastructure/theme/presets/themePresetOutput.test.ts +++ b/src/web-ui/src/infrastructure/theme/presets/themePresetOutput.test.ts @@ -2,6 +2,20 @@ import { createHash } from 'node:crypto'; import { describe, expect, it } from 'vitest'; import { builtinThemes } from './index'; +import { + PLUGIN_THEME_COLOR_KEYS, + createPluginThemeColorProjection, +} from '../pluginThemeProjection'; +import { + createAccentScale, + createGitColors, + createSemanticColors, + createSecondaryAccentScale, + overlayBlack, + overlayWhite, + rgbFromHex, + rgbaFromHex, +} from './shared'; function hashTheme(theme: unknown): string { return createHash('sha256') @@ -10,6 +24,123 @@ function hashTheme(theme: unknown): string { } describe('builtin theme preset output', () => { + it('formats hex palette references as stable rgb strings', () => { + expect(rgbFromHex('#00e6ff')).toBe('rgb(0, 230, 255)'); + expect(rgbaFromHex('#00e6ff', 0.12)).toBe('rgba(0, 230, 255, 0.12)'); + expect(rgbaFromHex('#00e6ff', '0.12')).toBe('rgba(0, 230, 255, 0.12)'); + expect(overlayBlack(0.3)).toBe('rgba(0, 0, 0, 0.3)'); + expect(overlayWhite(0.08)).toBe('rgba(255, 255, 255, 0.08)'); + }); + + it('aliases staged git colors to added colors unless a theme overrides them', () => { + expect(createGitColors({ + branch: '#64748b', + branchBg: 'rgba(100, 116, 139, 0.1)', + changes: '#f59e0b', + changesBg: 'rgba(245, 158, 11, 0.1)', + added: '#22c55e', + addedBg: 'rgba(34, 197, 94, 0.1)', + deleted: '#ef4444', + deletedBg: 'rgba(239, 68, 68, 0.1)', + })).toMatchObject({ + staged: '#22c55e', + stagedBg: 'rgba(34, 197, 94, 0.1)', + }); + + expect(createGitColors({ + branch: '#64748b', + branchBg: 'rgba(100, 116, 139, 0.1)', + changes: '#f59e0b', + changesBg: 'rgba(245, 158, 11, 0.1)', + added: '#22c55e', + addedBg: 'rgba(34, 197, 94, 0.1)', + deleted: '#ef4444', + deletedBg: 'rgba(239, 68, 68, 0.1)', + staged: '#10b981', + stagedBg: 'rgba(16, 185, 129, 0.1)', + })).toMatchObject({ + staged: '#10b981', + stagedBg: 'rgba(16, 185, 129, 0.1)', + }); + }); + + it('derives repeated palette families from compact authoring inputs', () => { + expect(createAccentScale({ + base: '#60a5fa', + hover: '#3b82f6', + })).toEqual({ + 50: 'rgba(96, 165, 250, 0.04)', + 100: 'rgba(96, 165, 250, 0.08)', + 200: 'rgba(96, 165, 250, 0.15)', + 300: 'rgba(96, 165, 250, 0.25)', + 400: 'rgba(96, 165, 250, 0.4)', + 500: '#60a5fa', + 600: '#3b82f6', + 700: 'rgba(59, 130, 246, 0.8)', + 800: 'rgba(59, 130, 246, 0.9)', + }); + + expect(createSecondaryAccentScale({ + base: '#8b5cf6', + hover: '#7c3aed', + })).toEqual({ + 50: 'rgba(139, 92, 246, 0.04)', + 100: 'rgba(139, 92, 246, 0.08)', + 200: 'rgba(139, 92, 246, 0.15)', + 400: 'rgba(139, 92, 246, 0.4)', + 500: '#8b5cf6', + 600: '#7c3aed', + 800: 'rgba(124, 58, 237, 0.9)', + }); + + expect(createSemanticColors({ + success: '#34d399', + warning: '#f59e0b', + error: '#ef4444', + info: '#a1a1aa', + })).toMatchObject({ + successBg: 'rgba(52, 211, 153, 0.1)', + successBorder: 'rgba(52, 211, 153, 0.3)', + warningBg: 'rgba(245, 158, 11, 0.1)', + errorBorder: 'rgba(239, 68, 68, 0.3)', + infoBg: 'rgba(161, 161, 170, 0.1)', + infoBorder: 'rgba(161, 161, 170, 0.3)', + }); + }); + + it('keeps near-neutral preset foregrounds on canonical stops', () => { + const serializedThemes = JSON.stringify(builtinThemes).toLowerCase(); + + expect(serializedThemes).not.toContain('#fafafa'); + expect(serializedThemes).not.toContain('#e2e6eb'); + expect(serializedThemes).not.toContain('#f0f2f5'); + }); + + it('projects builtin themes to a compact OpenCode-compatible plugin color key set', () => { + expect(PLUGIN_THEME_COLOR_KEYS).toEqual([ + 'primary', + 'secondary', + 'accent', + 'success', + 'warning', + 'error', + 'info', + ]); + + for (const theme of builtinThemes) { + const projection = createPluginThemeColorProjection(theme); + + expect(Object.keys(projection).sort()).toEqual([...PLUGIN_THEME_COLOR_KEYS].sort()); + expect(projection.primary).toBe(theme.colors.accent[500]); + expect(projection.secondary).toBe(theme.colors.purple?.[500] ?? theme.colors.accent[600]); + expect(projection.accent).toBe(theme.colors.accent[600]); + expect(projection.success).toBe(theme.colors.semantic.success); + expect(projection.warning).toBe(theme.colors.semantic.warning); + expect(projection.error).toBe(theme.colors.semantic.error); + expect(projection.info).toBe(theme.colors.semantic.info); + } + }); + it('keeps resolved preset objects stable across helper refactors', () => { expect(builtinThemes.map(theme => ({ id: theme.id, @@ -18,42 +149,42 @@ describe('builtin theme preset output', () => { }))).toMatchInlineSnapshot(` [ { - "hash": "063f8984522a0be4753e2fa36e47030f8e86cfb09057d617ffbb6c37e3821ef1", + "hash": "146da4fe962477dfef3ebcf275aec128019dee60dd1fde16b22e995b7264e377", "id": "bitfun-light", "type": "light", }, { - "hash": "466b9c64bb1625b6d191209f289802a388c29fb21d307bd139e9bfda9d4db067", + "hash": "9272e35f09967ad85bf5ad48eeabed079a0d8025f13342a1480aaa6a4d256dc3", "id": "bitfun-slate", "type": "dark", }, { - "hash": "51f8ff5912d12b0105b1b595930c4d564381fb1c8ce5c6b9436a97ba31bef80e", + "hash": "64ed9a89e66a7e0dd475ac2015eed5d45000693828c1b95c8cb06023b8053145", "id": "bitfun-dark", "type": "dark", }, { - "hash": "df6cadb36332f77286c3ac9a862dc8cd7805944ee86ed57af6e1f3b40f4f2e08", + "hash": "ab49b7424428d3308ffe878bc51553e8c1e151b234628e5ec05b635a04816b9a", "id": "bitfun-midnight", "type": "dark", }, { - "hash": "46ac5adf2b0dd0bc633f27665e1544893eb57617c123500b2d5b543690eca1f9", + "hash": "a00254950833b446f6547c3753da862c3818e6801028dab905ec6939f078f86e", "id": "bitfun-china-style", "type": "light", }, { - "hash": "5b2db0c0dfc253022fadd1d5bc07da65e7f473c27d66fe297cc695a50466699c", + "hash": "069b6629f8749c10d1ab9b65bc29d8484cad30d3ccf7f73a2a36908ae43a75ed", "id": "bitfun-china-night", "type": "dark", }, { - "hash": "e974f329a592a936fe1794f7bedc9f68d1579db1663435175eb429c96e4b37d1", + "hash": "7c569ff17c1d1852b2e2f1ba8e41a1f8350bd52a3aa048c65b853d3aedc37c52", "id": "bitfun-cyber", "type": "dark", }, { - "hash": "1c391cd9207188d5edf906dabd3f23f28e07952c10f1ee9ebf30432747fc0fa0", + "hash": "5b2b2064a339c59e93d0a3b4f6d29a1e2c0096f1828063bbb2e258e2d48f4cb3", "id": "bitfun-tokyo-night", "type": "dark", }, diff --git a/src/web-ui/src/infrastructure/theme/presets/themePromptSnapshots.ts b/src/web-ui/src/infrastructure/theme/presets/themePromptSnapshots.ts new file mode 100644 index 0000000000..79447eb9c3 --- /dev/null +++ b/src/web-ui/src/infrastructure/theme/presets/themePromptSnapshots.ts @@ -0,0 +1,60 @@ +import type { ThemeConfig, ThemeId } from '../types'; +import { DEFAULT_DARK_THEME_ID, DEFAULT_LIGHT_THEME_ID } from './index'; + +export const THEME_PROMPT_SNAPSHOT_VERSION = 1; + +export interface ThemePromptSnapshotEntry { + id: ThemeId; + themeType: ThemeConfig['type']; + bgPrimary: string; + bgSecondary: string; + bgScene: string; + textPrimary: string; + textMuted: string; + accent500: string; + accent600: string; + borderBase: string; + elementBase: string; + radiusBase: string; + spacing4: string; + shadowBase: string; + styleNotes: string; +} + +export interface ThemePromptSnapshotManifest { + version: typeof THEME_PROMPT_SNAPSHOT_VERSION; + defaultLightThemeId: ThemeId; + defaultDarkThemeId: ThemeId; + themes: ThemePromptSnapshotEntry[]; +} + +export function createThemePromptSnapshotEntry(theme: ThemeConfig): ThemePromptSnapshotEntry { + return { + id: theme.id, + themeType: theme.type, + bgPrimary: theme.colors.background.primary, + bgSecondary: theme.colors.background.secondary, + bgScene: theme.colors.background.scene, + textPrimary: theme.colors.text.primary, + textMuted: theme.colors.text.muted, + accent500: theme.colors.accent[500], + accent600: theme.colors.accent[600], + borderBase: theme.colors.border.base, + elementBase: theme.colors.element.base, + radiusBase: theme.effects.radius.base, + spacing4: theme.effects.spacing[4], + shadowBase: theme.effects.shadow.base, + styleNotes: theme.description ?? theme.name, + }; +} + +export function createThemePromptSnapshotManifest( + themes: readonly ThemeConfig[], +): ThemePromptSnapshotManifest { + return { + version: THEME_PROMPT_SNAPSHOT_VERSION, + defaultLightThemeId: DEFAULT_LIGHT_THEME_ID, + defaultDarkThemeId: DEFAULT_DARK_THEME_ID, + themes: themes.map(createThemePromptSnapshotEntry), + }; +} diff --git a/src/web-ui/src/infrastructure/theme/presets/tokyo-night-theme.ts b/src/web-ui/src/infrastructure/theme/presets/tokyo-night-theme.ts index 7eaacb76db..28fd35ecc8 100644 --- a/src/web-ui/src/infrastructure/theme/presets/tokyo-night-theme.ts +++ b/src/web-ui/src/infrastructure/theme/presets/tokyo-night-theme.ts @@ -2,13 +2,48 @@ import { ThemeConfig } from '../types'; import { + createAccentScale, createCompactRadius, createExpressiveTypography, + createGitColors, + createSemanticColors, + createSecondaryAccentScale, createStandardEasing, createStandardSpacing, createWindowControls, + overlayBlack, + rgbFromHex, + rgbaFromHex, + STATIC_WHITE, } from './shared'; +const TOKYO_BACKGROUND_PRIMARY = '#1a1b26'; +const TOKYO_BACKGROUND_SECONDARY = '#1e202e'; +const TOKYO_TEXT_PRIMARY = '#c0caf5'; +const TOKYO_TEXT_SECONDARY = '#a9b1d6'; +const TOKYO_TEXT_MUTED = '#787c99'; +const TOKYO_ACCENT = '#7aa2f7'; +const TOKYO_ACCENT_HOVER = '#6183bb'; +const TOKYO_PURPLE = '#bb9af7'; +const TOKYO_PURPLE_HOVER = '#9d7cd8'; +const TOKYO_SUCCESS = '#9ece6a'; +const TOKYO_WARNING = '#e0af68'; +const TOKYO_ERROR = '#f7768e'; +const TOKYO_INFO = '#7dcfff'; +const TOKYO_BORDER = '#334155'; +const TOKYO_SCROLLBAR = '#868bc4'; +const TOKYO_GIT_ADDED = '#41a6b5'; +const TOKYO_PRIMARY_BUTTON = '#3d59a1'; + +const tokyoAccent = (alpha: number | string) => rgbaFromHex(TOKYO_ACCENT, alpha); +const tokyoSuccess = (alpha: number | string) => rgbaFromHex(TOKYO_SUCCESS, alpha); +const tokyoWarning = (alpha: number | string) => rgbaFromHex(TOKYO_WARNING, alpha); +const tokyoError = (alpha: number | string) => rgbaFromHex(TOKYO_ERROR, alpha); +const tokyoBorder = (alpha: number | string) => rgbaFromHex(TOKYO_BORDER, alpha); +const tokyoScrollbar = (alpha: number | string) => rgbaFromHex(TOKYO_SCROLLBAR, alpha); +const tokyoGitAdded = (alpha: number | string) => rgbaFromHex(TOKYO_GIT_ADDED, alpha); +const tokyoPrimaryButton = (alpha: number | string) => rgbaFromHex(TOKYO_PRIMARY_BUTTON, alpha); + /** Colors aligned with the Tokyo Night palette (Enkia / VS Code Tokyo Night). */ export const bitfunTokyoNightTheme: ThemeConfig = { id: 'bitfun-tokyo-night', @@ -21,129 +56,92 @@ export const bitfunTokyoNightTheme: ThemeConfig = { colors: { background: { - primary: '#1a1b26', - secondary: '#16161e', - tertiary: '#14141b', - quaternary: '#1e202e', - elevated: '#20222c', - workbench: '#16161e', - scene: '#1a1b26', - tooltip: 'rgba(22, 22, 30, 0.94)', + primary: TOKYO_BACKGROUND_PRIMARY, + secondary: TOKYO_BACKGROUND_SECONDARY, + tertiary: TOKYO_BACKGROUND_SECONDARY, + quaternary: TOKYO_BACKGROUND_SECONDARY, + elevated: TOKYO_BACKGROUND_SECONDARY, + workbench: TOKYO_BACKGROUND_SECONDARY, + scene: TOKYO_BACKGROUND_PRIMARY, + tooltip: rgbaFromHex(TOKYO_BACKGROUND_SECONDARY, 0.94), }, text: { - primary: '#c0caf5', - secondary: '#a9b1d6', - muted: '#787c99', + primary: TOKYO_TEXT_PRIMARY, + secondary: TOKYO_TEXT_SECONDARY, + muted: TOKYO_TEXT_MUTED, disabled: '#545c7e', }, - accent: { - 50: 'rgba(122, 162, 247, 0.05)', - 100: 'rgba(122, 162, 247, 0.08)', - 200: 'rgba(122, 162, 247, 0.15)', - 300: 'rgba(122, 162, 247, 0.25)', - 400: 'rgba(122, 162, 247, 0.4)', - 500: '#7aa2f7', - 600: '#6183bb', - 700: 'rgba(97, 131, 187, 0.85)', - 800: 'rgba(97, 131, 187, 0.95)', - }, - - purple: { - 50: 'rgba(187, 154, 247, 0.05)', - 100: 'rgba(187, 154, 247, 0.08)', - 200: 'rgba(187, 154, 247, 0.15)', - 300: 'rgba(187, 154, 247, 0.25)', - 400: 'rgba(187, 154, 247, 0.4)', - 500: '#bb9af7', - 600: '#9d7cd8', - 700: 'rgba(157, 124, 216, 0.85)', - 800: 'rgba(157, 124, 216, 0.95)', - }, - - semantic: { - success: '#9ece6a', - successBg: 'rgba(158, 206, 106, 0.12)', - successBorder: 'rgba(158, 206, 106, 0.35)', - - warning: '#e0af68', - warningBg: 'rgba(224, 175, 104, 0.12)', - warningBorder: 'rgba(224, 175, 104, 0.35)', - - error: '#f7768e', - errorBg: 'rgba(247, 118, 142, 0.12)', - errorBorder: 'rgba(247, 118, 142, 0.35)', + accent: createAccentScale({ + base: TOKYO_ACCENT, + hover: TOKYO_ACCENT_HOVER, + alpha: { 50: 0.05, 700: 0.85, 800: 0.95 }, + }), - info: '#7dcfff', - infoBg: 'rgba(125, 207, 255, 0.12)', - infoBorder: 'rgba(125, 207, 255, 0.35)', + purple: createSecondaryAccentScale({ + base: TOKYO_PURPLE, + hover: TOKYO_PURPLE_HOVER, + alpha: { 50: 0.05, 800: 0.95 }, + }), - highlight: '#e0af68', - highlightBg: 'rgba(224, 175, 104, 0.15)', - }, + semantic: createSemanticColors({ + success: TOKYO_SUCCESS, + warning: TOKYO_WARNING, + error: TOKYO_ERROR, + info: TOKYO_INFO, + bgAlpha: 0.12, + borderAlpha: 0.35, + }), border: { - subtle: 'rgba(54, 59, 84, 0.45)', - base: 'rgba(54, 59, 84, 0.6)', - medium: 'rgba(54, 59, 84, 0.72)', - strong: 'rgba(54, 59, 84, 0.85)', - prominent: 'rgba(122, 162, 247, 0.45)', + subtle: tokyoBorder(0.45), + base: tokyoBorder(0.6), + medium: tokyoBorder(0.72), + strong: tokyoBorder(0.85), + prominent: tokyoAccent(0.45), }, element: { - subtle: 'rgba(122, 162, 247, 0.06)', - soft: 'rgba(122, 162, 247, 0.08)', - base: 'rgba(122, 162, 247, 0.11)', - medium: 'rgba(122, 162, 247, 0.14)', - strong: 'rgba(122, 162, 247, 0.18)', - elevated: 'rgba(122, 162, 247, 0.22)', - }, - - git: { - branch: 'rgb(122, 162, 247)', - branchBg: 'rgba(122, 162, 247, 0.12)', - changes: 'rgb(224, 175, 104)', - changesBg: 'rgba(224, 175, 104, 0.12)', - added: 'rgb(65, 166, 181)', - addedBg: 'rgba(65, 166, 181, 0.12)', - deleted: 'rgb(247, 118, 142)', - deletedBg: 'rgba(247, 118, 142, 0.12)', - staged: 'rgb(158, 206, 106)', - stagedBg: 'rgba(158, 206, 106, 0.12)', - }, + subtle: tokyoAccent(0.06), + soft: tokyoAccent(0.08), + base: tokyoAccent(0.11), + medium: tokyoAccent(0.14), + strong: tokyoAccent(0.18), + elevated: tokyoAccent(0.22), + }, + + git: createGitColors({ + branch: rgbFromHex(TOKYO_ACCENT), + branchBg: tokyoAccent(0.12), + changes: rgbFromHex(TOKYO_WARNING), + changesBg: tokyoWarning(0.12), + added: rgbFromHex(TOKYO_GIT_ADDED), + addedBg: tokyoGitAdded(0.12), + deleted: rgbFromHex(TOKYO_ERROR), + deletedBg: tokyoError(0.12), + staged: rgbFromHex(TOKYO_SUCCESS), + stagedBg: tokyoSuccess(0.12), + }), scrollbar: { - thumb: 'rgba(134, 139, 196, 0.15)', - thumbHover: 'rgba(134, 139, 196, 0.28)', + thumb: tokyoScrollbar(0.15), + thumbHover: tokyoScrollbar(0.28), }, }, effects: { shadow: { - xs: '0 1px 3px rgba(0, 0, 0, 0.55)', - sm: '0 2px 6px rgba(0, 0, 0, 0.5)', - base: '0 4px 12px rgba(0, 0, 0, 0.48)', - lg: '0 8px 20px rgba(0, 0, 0, 0.45)', - xl: '0 12px 28px rgba(0, 0, 0, 0.42)', - '2xl': '0 16px 36px rgba(0, 0, 0, 0.38)', - }, - - glow: { - blue: - '0 0 12px rgba(122, 162, 247, 0.35), 0 0 24px rgba(122, 162, 247, 0.2), 0 4px 16px rgba(0, 0, 0, 0.35)', - purple: - '0 0 12px rgba(187, 154, 247, 0.32), 0 0 24px rgba(187, 154, 247, 0.18), 0 4px 16px rgba(0, 0, 0, 0.35)', - mixed: - '0 0 16px rgba(122, 162, 247, 0.3), 0 0 28px rgba(187, 154, 247, 0.2), 0 4px 20px rgba(0, 0, 0, 0.35)', + xs: `0 1px 3px ${overlayBlack(0.55)}`, + sm: `0 2px 6px ${overlayBlack(0.5)}`, + base: `0 4px 12px ${overlayBlack(0.48)}`, + lg: `0 8px 20px ${overlayBlack(0.45)}`, + xl: `0 12px 28px ${overlayBlack(0.42)}`, }, blur: { subtle: 'blur(4px) saturate(1.15)', base: 'blur(8px) saturate(1.2)', - medium: 'blur(12px) saturate(1.25)', - strong: 'blur(16px) saturate(1.3) brightness(1.08)', - intense: 'blur(20px) saturate(1.35) brightness(1.1)', }, radius: createCompactRadius(), @@ -154,7 +152,6 @@ export const bitfunTokyoNightTheme: ThemeConfig = { disabled: 0.5, hover: 0.88, focus: 0.96, - overlay: 0.52, }, }, @@ -164,7 +161,6 @@ export const bitfunTokyoNightTheme: ThemeConfig = { fast: '0.12s', base: '0.25s', slow: '0.5s', - lazy: '0.8s', }, easing: createStandardEasing('cubic-bezier(0.25, 0.46, 0.45, 0.94)'), @@ -173,100 +169,42 @@ export const bitfunTokyoNightTheme: ThemeConfig = { typography: createExpressiveTypography(), components: { - windowControls: createWindowControls({ - standard: { - dot: 'rgba(122, 162, 247, 0.55)', - dotShadow: '0 0 6px rgba(122, 162, 247, 0.35)', - hoverBg: 'rgba(122, 162, 247, 0.14)', - hoverColor: '#7aa2f7', - hoverBorder: 'rgba(122, 162, 247, 0.35)', - hoverShadow: - '0 0 12px rgba(122, 162, 247, 0.28), 0 2px 8px rgba(122, 162, 247, 0.15), inset 0 1px 0 rgba(122, 162, 247, 0.18)', - }, - close: { - dot: 'rgba(247, 118, 142, 0.55)', - dotShadow: '0 0 6px rgba(247, 118, 142, 0.35)', - hoverBg: 'rgba(247, 118, 142, 0.14)', - hoverColor: '#f7768e', - hoverBorder: 'rgba(247, 118, 142, 0.35)', - hoverShadow: - '0 0 12px rgba(247, 118, 142, 0.28), 0 2px 8px rgba(247, 118, 142, 0.15), inset 0 1px 0 rgba(247, 118, 142, 0.18)', - }, - common: { - defaultColor: 'rgba(192, 202, 245, 0.92)', - defaultDot: 'rgba(122, 162, 247, 0.22)', - disabledDot: 'rgba(122, 162, 247, 0.12)', - flowGradient: - 'linear-gradient(90deg, transparent, rgba(122, 162, 247, 0.08), rgba(187, 154, 247, 0.1), rgba(122, 162, 247, 0.08), transparent)', - }, - }), + windowControls: createWindowControls(TOKYO_ERROR), button: { - default: { - background: 'rgba(122, 162, 247, 0.1)', - color: '#a9b1d6', - border: 'rgba(122, 162, 247, 0.22)', - shadow: '0 0 8px rgba(122, 162, 247, 0.12)', - }, - hover: { - background: 'rgba(122, 162, 247, 0.16)', - color: '#c0caf5', - border: 'rgba(122, 162, 247, 0.38)', - shadow: '0 0 16px rgba(122, 162, 247, 0.22), 0 2px 8px rgba(0, 0, 0, 0.35)', - transform: 'translateY(-1px)', - }, - active: { - background: 'rgba(122, 162, 247, 0.13)', - color: '#c0caf5', - border: 'rgba(122, 162, 247, 0.42)', - shadow: '0 0 12px rgba(122, 162, 247, 0.18)', - transform: 'translateY(0)', - }, primary: { default: { - background: 'rgba(61, 89, 161, 0.55)', - color: '#c0caf5', - border: 'rgba(122, 162, 247, 0.45)', - shadow: '0 0 14px rgba(61, 89, 161, 0.35)', + background: tokyoPrimaryButton(0.55), + color: TOKYO_TEXT_PRIMARY, + border: tokyoAccent(0.45), + shadow: `0 0 14px ${tokyoPrimaryButton(0.35)}`, }, hover: { - background: 'rgba(61, 89, 161, 0.72)', - color: '#ffffff', - border: 'rgba(122, 162, 247, 0.55)', + background: tokyoPrimaryButton(0.72), + color: STATIC_WHITE, + border: tokyoAccent(0.55), shadow: - '0 0 22px rgba(122, 162, 247, 0.35), 0 4px 12px rgba(0, 0, 0, 0.35)', + `0 0 22px ${tokyoAccent(0.35)}, 0 4px 12px ${overlayBlack(0.35)}`, transform: 'translateY(-2px)', }, active: { - background: 'rgba(61, 89, 161, 0.62)', - color: '#ffffff', - border: 'rgba(122, 162, 247, 0.48)', - shadow: '0 0 18px rgba(122, 162, 247, 0.28)', + background: tokyoPrimaryButton(0.62), + color: STATIC_WHITE, + border: tokyoAccent(0.48), + shadow: `0 0 18px ${tokyoAccent(0.28)}`, transform: 'translateY(-1px)', }, }, ghost: { default: { - background: 'transparent', - color: '#787c99', - border: 'rgba(54, 59, 84, 0.55)', - shadow: 'none', + color: TOKYO_TEXT_MUTED, }, hover: { - background: 'rgba(122, 162, 247, 0.1)', - color: '#c0caf5', - border: 'rgba(122, 162, 247, 0.35)', - shadow: '0 0 12px rgba(122, 162, 247, 0.15)', - transform: 'translateY(-1px)', - }, - active: { - background: 'rgba(122, 162, 247, 0.08)', - color: '#a9b1d6', - border: 'rgba(122, 162, 247, 0.3)', - shadow: '0 0 8px rgba(122, 162, 247, 0.12)', - transform: 'translateY(0)', + background: tokyoAccent(0.1), + color: TOKYO_TEXT_PRIMARY, + border: tokyoAccent(0.35), }, }, }, @@ -277,11 +215,11 @@ export const bitfunTokyoNightTheme: ThemeConfig = { inherit: true, rules: [], colors: { - background: '#1a1b26', - foreground: '#a9b1d6', - lineHighlight: '#1e202e', + background: TOKYO_BACKGROUND_PRIMARY, + foreground: TOKYO_TEXT_SECONDARY, + lineHighlight: TOKYO_BACKGROUND_SECONDARY, selection: 'rgba(81, 92, 126, 0.35)', - cursor: '#c0caf5', + cursor: TOKYO_TEXT_PRIMARY, }, }, }; diff --git a/src/web-ui/src/infrastructure/theme/store/themeStore.ts b/src/web-ui/src/infrastructure/theme/store/themeStore.ts index 9609363d6a..6cd19e829d 100644 --- a/src/web-ui/src/infrastructure/theme/store/themeStore.ts +++ b/src/web-ui/src/infrastructure/theme/store/themeStore.ts @@ -109,7 +109,7 @@ export const useThemeStore = create((set) => ({ set({ loading: true, error: null }); try { - themeService.registerTheme(theme); + await themeService.registerTheme(theme); const themes = themeService.getThemeList(); set({ diff --git a/src/web-ui/src/infrastructure/theme/types/index.ts b/src/web-ui/src/infrastructure/theme/types/index.ts index 1d4d0f6a51..be1750a8ca 100644 --- a/src/web-ui/src/infrastructure/theme/types/index.ts +++ b/src/web-ui/src/infrastructure/theme/types/index.ts @@ -51,7 +51,10 @@ export interface AccentColors { 800: ColorValue; } - +export type SecondaryAccentStop = 50 | 100 | 200 | 400 | 500 | 600 | 800; +export type SecondaryAccentColors = Pick; + + export interface SemanticColors { success: ColorValue; successBg: ColorValue; @@ -68,10 +71,6 @@ export interface SemanticColors { info: ColorValue; infoBg: ColorValue; infoBorder: ColorValue; - - - highlight: ColorValue; - highlightBg: ColorValue; } @@ -122,23 +121,12 @@ export interface ShadowConfig { base: string; lg: string; xl: string; - '2xl': string; -} - - -export interface GlowConfig { - blue: string; - purple: string; - mixed: string; } export interface BlurConfig { subtle: string; base: string; - medium: string; - strong: string; - intense: string; } @@ -170,40 +158,12 @@ export interface OpacityConfig { disabled: number; hover: number; focus: number; - overlay: number; } export interface ButtonConfig { - - default: { - background: ColorValue; - color: ColorValue; - border: ColorValue; - shadow?: string; - }; - - - hover: { - background: ColorValue; - color: ColorValue; - border: ColorValue; - shadow?: string; - transform?: string; - }; - - - active: { - background: ColorValue; - color: ColorValue; - border: ColorValue; - shadow?: string; - transform?: string; - }; - - primary: { default: { background: ColorValue; @@ -230,66 +190,20 @@ export interface ButtonConfig { ghost: { default: { - background: ColorValue; color: ColorValue; - border: ColorValue; - shadow?: string; }; hover: { background: ColorValue; color: ColorValue; border: ColorValue; - shadow?: string; - transform?: string; - }; - active: { - background: ColorValue; - color: ColorValue; - border: ColorValue; - shadow?: string; - transform?: string; }; }; } export interface WindowControlsConfig { - - minimize: { - dot: ColorValue; - dotShadow?: string; - hoverBg: ColorValue; - hoverColor: ColorValue; - hoverBorder: ColorValue; - hoverShadow?: string; - }; - - - maximize: { - dot: ColorValue; - dotShadow?: string; - hoverBg: ColorValue; - hoverColor: ColorValue; - hoverBorder: ColorValue; - hoverShadow?: string; - }; - - close: { - dot: ColorValue; - dotShadow?: string; - hoverBg: ColorValue; - hoverColor: ColorValue; - hoverBorder: ColorValue; - hoverShadow?: string; - }; - - - common: { - defaultColor: ColorValue; - defaultDot: ColorValue; - disabledDot: ColorValue; - flowGradient?: string; + hoverColor: ColorValue; }; } @@ -301,15 +215,12 @@ export interface MotionConfig { fast: string; base: string; slow: string; - lazy: string; } export interface EasingConfig { standard: string; decelerate: string; - accelerate: string; - bounce: string; smooth: string; } @@ -339,7 +250,6 @@ export interface FontSizeConfig { '2xl': string; '3xl': string; '4xl': string; - '5xl': string; } @@ -394,7 +304,7 @@ export interface ThemeConfig { background: BackgroundColors; text: TextColors; accent: AccentColors; - purple?: AccentColors; + purple?: SecondaryAccentColors; semantic: SemanticColors; border: BorderColors; element: ElementBackgrounds; @@ -405,7 +315,6 @@ export interface ThemeConfig { effects: { shadow: ShadowConfig; - glow: GlowConfig; blur: BlurConfig; radius: RadiusConfig; spacing: SpacingConfig; diff --git a/src/web-ui/src/infrastructure/theme/utils/ThemeValidator.ts b/src/web-ui/src/infrastructure/theme/utils/ThemeValidator.ts index 1007629bae..0b92f15055 100644 --- a/src/web-ui/src/infrastructure/theme/utils/ThemeValidator.ts +++ b/src/web-ui/src/infrastructure/theme/utils/ThemeValidator.ts @@ -1,76 +1,81 @@ - +import { bitfunLightTheme } from '../presets/light-theme'; +import type { ThemeConfig, ThemeValidationResult } from '../types'; -import { ThemeConfig, ThemeValidationResult, ColorValue } from '../types'; +const REQUIRED_SCHEMA_ROOTS = ['colors', 'effects', 'motion', 'typography'] as const; +const OPTIONAL_SCHEMA_PATHS = new Set([ + 'colors.background.tooltip', + 'colors.purple', + 'colors.scrollbar', +]); +const OPTIONAL_SCHEMA_REFERENCES = { + 'colors.scrollbar': { + thumb: 'transparent', + thumbHover: 'transparent', + }, +} as const; - -function isValidColor(color: ColorValue): boolean { - - const hexPattern = /^#([0-9A-Fa-f]{3}){1,2}$/; - if (hexPattern.test(color)) { - return true; +function isRecord(value: unknown): value is Record { + return typeof value === 'object' && value !== null && !Array.isArray(value); +} + +function isOptionalSchemaPath(path: string): boolean { + return OPTIONAL_SCHEMA_PATHS.has(path); +} + +function isColorPath(path: string): boolean { + return path.startsWith('colors.'); +} + +function readPath(source: unknown, path: string): unknown { + return path.split('.').reduce((current, key) => ( + isRecord(current) ? current[key] : undefined + ), source); +} + +function isValidColor(color: unknown): boolean { + if (typeof color !== 'string') { + return false; } - - - const rgbPattern = /^rgba?\(\s*\d+\s*,\s*\d+\s*,\s*\d+\s*(,\s*[\d.]+\s*)?\)$/; - if (rgbPattern.test(color)) { + + if (/^#([0-9A-Fa-f]{3}){1,2}$/.test(color)) { return true; } - - - const hslPattern = /^hsla?\(\s*\d+\s*,\s*\d+%\s*,\s*\d+%\s*(,\s*[\d.]+\s*)?\)$/; - if (hslPattern.test(color)) { + + if (/^rgba?\(\s*\d+\s*,\s*\d+\s*,\s*\d+\s*(,\s*[\d.]+\s*)?\)$/.test(color)) { return true; } - - - const namedColors = ['transparent', 'currentColor']; - if (namedColors.includes(color)) { + + if (/^hsla?\(\s*\d+\s*,\s*\d+%\s*,\s*\d+%\s*(,\s*[\d.]+\s*)?\)$/.test(color)) { return true; } - - return false; + + return color === 'transparent' || color === 'currentColor'; } - function calculateContrast(_color1: string, _color2: string): number { - - return 4.5; } - export class ThemeValidator { - validate(theme: ThemeConfig): ThemeValidationResult { const errors: ThemeValidationResult['errors'] = []; const warnings: ThemeValidationResult['warnings'] = []; - - + this.validateBasicFields(theme, errors); - - - this.validateColors(theme, errors, warnings); - - - this.validateEffects(theme, errors, warnings); - - + this.validateRequiredSchema(theme, errors); this.validateContrast(theme, warnings); - - this.validateCompleteness(theme, warnings); - + return { valid: errors.length === 0, errors, warnings, }; } - - + private validateBasicFields( theme: ThemeConfig, - errors: ThemeValidationResult['errors'] + errors: ThemeValidationResult['errors'], ): void { if (!theme.id || theme.id.trim() === '') { errors.push({ @@ -79,7 +84,7 @@ export class ThemeValidator { code: 'MISSING_ID', }); } - + if (!theme.name || theme.name.trim() === '') { errors.push({ path: 'name', @@ -87,7 +92,7 @@ export class ThemeValidator { code: 'MISSING_NAME', }); } - + if (!theme.type || !['dark', 'light'].includes(theme.type)) { errors.push({ path: 'type', @@ -96,103 +101,97 @@ export class ThemeValidator { }); } } - - - private validateColors( + + private validateRequiredSchema( theme: ThemeConfig, errors: ThemeValidationResult['errors'], - _warnings: ThemeValidationResult['warnings'] ): void { - if (!theme.colors) { - errors.push({ - path: 'colors', - message: 'Missing color configuration', - code: 'MISSING_COLORS', - }); + REQUIRED_SCHEMA_ROOTS.forEach((root) => { + this.validateSchemaValue( + root, + (theme as unknown as Record)[root], + (bitfunLightTheme as unknown as Record)[root], + errors, + ); + }); + + Object.entries(OPTIONAL_SCHEMA_REFERENCES).forEach(([path, reference]) => { + const value = readPath(theme, path); + if (value !== undefined) { + this.validateSchemaValue(path, value, reference, errors); + } + }); + } + + private validateSchemaValue( + path: string, + value: unknown, + reference: unknown, + errors: ThemeValidationResult['errors'], + ): void { + if (isOptionalSchemaPath(path) && value === undefined) { return; } - - - if (!theme.colors.background) { - errors.push({ - path: 'colors.background', - message: 'Missing background color configuration', - code: 'MISSING_BACKGROUND_COLORS', - }); - } else { - this.validateColorGroup('colors.background', theme.colors.background as unknown as Record, errors); - } - - - if (!theme.colors.text) { - errors.push({ - path: 'colors.text', - message: 'Missing text color configuration', - code: 'MISSING_TEXT_COLORS', - }); - } else { - this.validateColorGroup('colors.text', theme.colors.text as unknown as Record, errors); - } - - - if (!theme.colors.accent) { - errors.push({ - path: 'colors.accent', - message: 'Missing accent color configuration', - code: 'MISSING_ACCENT_COLORS', + + if (isRecord(reference)) { + if (!isRecord(value)) { + errors.push({ + path, + message: `Missing ${path} configuration`, + code: path === 'colors' ? 'MISSING_COLORS' : 'MISSING_THEME_FIELD_GROUP', + }); + return; + } + + Object.entries(reference).forEach(([key, childReference]) => { + this.validateSchemaValue(`${path}.${key}`, value[key], childReference, errors); }); - } else { - this.validateColorGroup('colors.accent', theme.colors.accent as unknown as Record, errors); + return; } - } - - - private validateColorGroup( - path: string, - colorGroup: Record, - errors: ThemeValidationResult['errors'] - ): void { - Object.entries(colorGroup).forEach(([key, value]) => { + + if (isColorPath(path)) { if (!isValidColor(value)) { errors.push({ - path: `${path}.${key}`, - message: `Invalid color value: ${value}`, + path, + message: `Invalid color value: ${String(value)}`, code: 'INVALID_COLOR_FORMAT', }); } - }); - } - - - private validateEffects( - theme: ThemeConfig, - _errors: ThemeValidationResult['errors'], - warnings: ThemeValidationResult['warnings'] - ): void { - if (!theme.effects) { - warnings.push({ - path: 'effects', - message: 'Missing effects configuration; default values will be used', - code: 'MISSING_EFFECTS', + return; + } + + if (typeof reference === 'number') { + if (typeof value !== 'number' || !Number.isFinite(value)) { + errors.push({ + path, + message: `Missing numeric value for ${path}`, + code: 'INVALID_THEME_NUMBER', + }); + } + return; + } + + if (typeof reference === 'string' && (typeof value !== 'string' || value.trim() === '')) { + errors.push({ + path, + message: `Missing string value for ${path}`, + code: 'INVALID_THEME_STRING', }); } } - - + private validateContrast( theme: ThemeConfig, - warnings: ThemeValidationResult['warnings'] + warnings: ThemeValidationResult['warnings'], ): void { - if (!theme.colors) { + const textPrimary = theme.colors?.text?.primary; + const bgPrimary = theme.colors?.background?.primary; + if (!textPrimary || !bgPrimary) { return; } - - - const textPrimary = theme.colors.text.primary; - const bgPrimary = theme.colors.background.primary; - + const contrast = calculateContrast(textPrimary, bgPrimary); - + if (contrast < 4.5) { warnings.push({ path: 'colors', @@ -201,28 +200,11 @@ export class ThemeValidator { }); } } - - + private validateCompleteness( theme: ThemeConfig, - warnings: ThemeValidationResult['warnings'] + warnings: ThemeValidationResult['warnings'], ): void { - if (!theme.motion) { - warnings.push({ - path: 'motion', - message: 'Missing motion configuration; default values will be used', - code: 'MISSING_MOTION', - }); - } - - if (!theme.typography) { - warnings.push({ - path: 'typography', - message: 'Missing typography configuration; default values will be used', - code: 'MISSING_TYPOGRAPHY', - }); - } - if (!theme.monaco) { warnings.push({ path: 'monaco', @@ -233,5 +215,4 @@ export class ThemeValidator { } } - export const themeValidator = new ThemeValidator(); diff --git a/src/web-ui/src/infrastructure/update/UpdateAvailableDialog.scss b/src/web-ui/src/infrastructure/update/UpdateAvailableDialog.scss index a7be662d1f..082dcbc41d 100644 --- a/src/web-ui/src/infrastructure/update/UpdateAvailableDialog.scss +++ b/src/web-ui/src/infrastructure/update/UpdateAvailableDialog.scss @@ -114,7 +114,7 @@ word-break: break-word; border-radius: 8px; border: 1px solid var(--border-subtle); - background: var(--color-bg-subtle); + background: var(--element-bg-subtle); color: var(--color-text-secondary); } diff --git a/src/web-ui/src/infrastructure/update/UpdateInstallProgressModal.scss b/src/web-ui/src/infrastructure/update/UpdateInstallProgressModal.scss index 1718cb2b15..261e4f45b7 100644 --- a/src/web-ui/src/infrastructure/update/UpdateInstallProgressModal.scss +++ b/src/web-ui/src/infrastructure/update/UpdateInstallProgressModal.scss @@ -8,7 +8,7 @@ .bitfun-update-progress__bar { height: 8px; border-radius: 4px; - background: var(--color-bg-subtle); + background: var(--element-bg-subtle); overflow: hidden; position: relative; } @@ -16,7 +16,7 @@ .bitfun-update-progress__fill { height: 100%; border-radius: 4px; - background: var(--color-accent); + background: var(--color-accent-500); transition: width 0.15s ease-out; } diff --git a/src/web-ui/src/locales/en-US/common.json b/src/web-ui/src/locales/en-US/common.json index f5ed79edac..a9e29e52c3 100644 --- a/src/web-ui/src/locales/en-US/common.json +++ b/src/web-ui/src/locales/en-US/common.json @@ -166,6 +166,9 @@ "modeCowork": "Cowork", "noSessions": "No sessions", "rename": "Rename", + "copySessionId": "Copy ID", + "copySessionIdSuccess": "Session ID copied", + "copySessionIdFailed": "Failed to copy session ID", "delete": "Delete", "confirmEdit": "Confirm", "cancelEdit": "Cancel", diff --git a/src/web-ui/src/locales/en-US/flow-chat.json b/src/web-ui/src/locales/en-US/flow-chat.json index e4c7eb3c0a..518421d659 100644 --- a/src/web-ui/src/locales/en-US/flow-chat.json +++ b/src/web-ui/src/locales/en-US/flow-chat.json @@ -44,6 +44,8 @@ "loadingDescription": "Preparing the conversation history.", "failedTitle": "Session history did not load", "failedDescription": "Retry loading the saved conversation.", + "preparingOlderHistory": "Preparing older history...", + "olderHistoryNotReady": "Older history is not ready yet.", "remoteSessionMissing": "The backend session data was not found. If you just reconnected an SSH remote workspace, close and reopen the remote project, or create a new session and try again.", "retry": "Retry" }, @@ -553,6 +555,7 @@ "boostSectionAgent": "Agent", "boostSectionContext": "Context", "boostStartBtw": "Start side question", + "boostNewSession": "New session", "boostAddContext": "Reference file or folder", "boostSkills": "Skills", "modeSection": "Modes", @@ -624,6 +627,7 @@ "noMatchingCommand": "No matching command", "selectHint": "↑↓ Select · Enter Confirm · Esc Cancel", "current": "Current", + "computerUseDisabled": "Computer Use is disabled in Settings → Session permissions", "professionalMode": "Expert Mode", "designMode": "Design Mode", "wip": "WIP", @@ -1357,6 +1361,25 @@ "commandInterrupted": "Command interrupted", "executionFailed": "Command execution failed" }, + "approval": { + "waiting": "Waiting for confirmation", + "confirm": "Allow", + "reject": "Reject", + "rejectWithInstruction": "Reject with instruction", + "enableAutoExecute": "Enable tool auto execute", + "confirmTooltip": "Allow this tool run", + "enableAutoExecuteTooltip": "Enable tool auto execute", + "autoExecuteEnabled": "Tool auto execute enabled", + "autoExecuteEnableFailed": "Failed to enable tool auto execute", + "rejectTooltip": "Reject this tool run", + "rejectWithInstructionTooltip": "Reject and tell the assistant what to do next", + "rejectInstructionLabel": "Rejection instruction", + "rejectInstructionPlaceholder": "Tell the assistant what to do instead...", + "rejectWithInstructionSubmit": "Reject", + "emptyInputTooltip": "This tool has no executable input", + "remaining": "remaining {{time}}", + "ariaLabel": "Tool approval" + }, "execProcess": { "executeCommand": "Run command:", "writeStdin": "Write stdin:", @@ -1450,6 +1473,20 @@ "noTitle": "No title", "summaryAvailable": "summary available" }, + "computerUse": { + "running": "Controlling the desktop...", + "runningOnApp": "Controlling {{app}}...", + "controllingApp": "Controlling {{app}}", + "done": "Action completed", + "actionFailed": "Action failed", + "actionLabel": "Action", + "appLabel": "Application", + "targetLabel": "Target", + "permissionDenied": "Permission required for desktop control", + "permissionDeniedHint": "BitFun needs macOS Accessibility and Screen Recording permissions before it can control other apps. Open System Settings, grant access to BitFun, then retry.", + "openSettings": "Open System Settings", + "openSettingsFailed": "Could not open System Settings" + }, "webFetch": { "parsingUrl": "Parsing URL...", "readLabel": "Read Webpage:", @@ -1580,6 +1617,7 @@ "executing": "Executing...", "completed": "Completed", "cancelled": "Cancelled", + "rejected": "Rejected", "failed": "Execution failed", "preparing": "Preparing" }, diff --git a/src/web-ui/src/locales/en-US/scenes/agents.json b/src/web-ui/src/locales/en-US/scenes/agents.json index c009a6853a..746c0f9114 100644 --- a/src/web-ui/src/locales/en-US/scenes/agents.json +++ b/src/web-ui/src/locales/en-US/scenes/agents.json @@ -37,6 +37,7 @@ "subtitle": "Built-in core agents covering mainstream AI workflows, ready to use out of the box.", "empty": "No core agents detected", "roleLabel": "Primary Use · ", + "computerUseDisabledBadge": "Disabled in Settings", "modes": { "agentic": { "role": "Coding specialist agent" diff --git a/src/web-ui/src/locales/en-US/settings/agentic-tools.json b/src/web-ui/src/locales/en-US/settings/agentic-tools.json index 1a1ce62658..97872be0f3 100644 --- a/src/web-ui/src/locales/en-US/settings/agentic-tools.json +++ b/src/web-ui/src/locales/en-US/settings/agentic-tools.json @@ -14,10 +14,14 @@ "config": { "autoExecute": "Auto Execute", "autoExecuteDesc": "Skip user confirmation before tool execution.", + "subagentMaxConcurrency": "Subagent Concurrency Limit", + "subagentMaxConcurrencyDesc": "How many subagents may run in parallel at the same time.", "confirmTimeout": "Confirm Timeout", "confirmTimeoutDesc": "Maximum time (seconds) to wait for user confirmation of tool calls.", + "confirmTimeoutHint": "Set 0 to disable confirmation timeout.", "executionTimeout": "Execution Timeout", - "executionTimeoutDesc": "Maximum time (seconds) for tool execution, including Task subagent runs.", + "executionTimeoutDesc": "Maximum time (seconds) for tool execution.", + "executionTimeoutHint": "Set 0 to disable execution timeout. Subagents and command execution tools manage their own execution limits and are not capped by this setting.", "subagentBatchPolicy": { "label": "Subagent Batch Scheduling", "desc": "Choose how multiple subagent launches from the same model response are scheduled.", diff --git a/src/web-ui/src/locales/en-US/settings/ai-model.json b/src/web-ui/src/locales/en-US/settings/ai-model.json index 5382063f63..efe84ba007 100644 --- a/src/web-ui/src/locales/en-US/settings/ai-model.json +++ b/src/web-ui/src/locales/en-US/settings/ai-model.json @@ -219,6 +219,7 @@ }, "streamIdleTimeout": { "title": "Streaming Timeout", + "effectiveNextRound": "Changes take effect next turn.", "label": "Idle Timeout (seconds)", "hint": "Global idle timeout between streamed chunks after the stream starts. Leave empty to wait indefinitely.", "placeholder": "Leave empty for no timeout", @@ -229,7 +230,7 @@ "streamTtftTimeout": { "title": "First Token Timeout", "label": "TTFT Timeout (seconds)", - "hint": "Maximum wait time for the model to start streaming after a request is sent. Leave empty to wait indefinitely.", + "hint": "Maximum wait time after a request is sent for the first effective streamed output. Leave empty to wait indefinitely.", "placeholder": "Leave empty for no timeout", "invalid": "Enter a positive integer in seconds, or leave this field empty.", "save": "Save First Token Timeout", @@ -270,6 +271,7 @@ "fillRequired": "Please fill in required fields", "fillModelName": "Please fill in model name", "duplicateModelNameUnderProvider": "This provider already has a model with this name. Use a different name or edit the existing entry.", + "invalidBaseUrlScheme": "API URL must start with http:// or https://", "saveFailed": "Save failed", "deleteFailed": "Failed to delete configuration", "loadFailed": "Failed to load AI configuration", @@ -278,7 +280,10 @@ "errorDetails": "Error details", "connectionTestMessages": { "toolCallsNotDetected": "This test did not detect any tool calls, so tool calling could not be verified this time.", - "imageInputCheckFailed": "The connection succeeded, but the image input check failed." + "imageInputCheckFailed": "The connection succeeded, but the image input check failed.", + "tlsOrCertificateIssue": "The connection failure may be related to SSL/TLS certificate verification. Check corporate proxies, self-signed certificates, system root certificates, or network interception. If you trust this network environment, you can enable \"Skip SSL Certificate Verification\" in this model's Advanced Settings, but it reduces HTTPS security.", + "proxyIssue": "The connection failure may be related to proxy or tunnel configuration. Check global proxy settings, proxy authentication, HTTPS CONNECT support, and whether the current network can reach this model API endpoint.", + "networkIssue": "The request failed while being sent, before any HTTP response was received. Check network connectivity, DNS, proxy, firewall, and certificate settings. In corporate proxy or self-signed certificate environments, you may also need to review \"Skip SSL Certificate Verification\" in Advanced Settings." }, "autoSetPrimary": "Automatically set as primary model", "defaultDescription": "{{name}} model configuration", diff --git a/src/web-ui/src/locales/zh-CN/common.json b/src/web-ui/src/locales/zh-CN/common.json index 2fabcea561..4ce01c2aeb 100644 --- a/src/web-ui/src/locales/zh-CN/common.json +++ b/src/web-ui/src/locales/zh-CN/common.json @@ -166,6 +166,9 @@ "modeCowork": "Cowork", "noSessions": "暂无会话", "rename": "重命名", + "copySessionId": "复制 ID", + "copySessionIdSuccess": "已复制会话 ID", + "copySessionIdFailed": "复制会话 ID 失败", "delete": "删除", "confirmEdit": "确认", "cancelEdit": "取消", diff --git a/src/web-ui/src/locales/zh-CN/flow-chat.json b/src/web-ui/src/locales/zh-CN/flow-chat.json index 912ea26842..b549e79061 100644 --- a/src/web-ui/src/locales/zh-CN/flow-chat.json +++ b/src/web-ui/src/locales/zh-CN/flow-chat.json @@ -44,6 +44,8 @@ "loadingDescription": "正在准备该会话的历史内容。", "failedTitle": "历史会话加载失败", "failedDescription": "可以重新加载已保存的会话内容。", + "preparingOlderHistory": "正在准备更早的历史记录...", + "olderHistoryNotReady": "更早的历史记录暂未准备好。", "remoteSessionMissing": "在后端找不到该会话数据。若刚重新连接过 SSH 远程工作区,请关闭并重新打开该远程项目,或新建会话后再试。", "retry": "重试" }, @@ -553,6 +555,7 @@ "boostSectionAgent": "智能体", "boostSectionContext": "上下文", "boostStartBtw": "发起侧问", + "boostNewSession": "新建会话", "boostAddContext": "引用文件或文件夹", "boostSkills": "Skills", "modeSection": "模式", @@ -618,6 +621,7 @@ "noMatchingCommand": "没有匹配的命令", "selectHint": "↑↓ 选择 · Enter 确认 · Esc 取消", "current": "当前", + "computerUseDisabled": "已在「设置 → 会话权限」中禁用 Computer Use", "professionalMode": "专业模式", "designMode": "设计模式", "wip": "开发中", @@ -1357,6 +1361,25 @@ "commandInterrupted": "命令已被中断", "executionFailed": "命令执行失败" }, + "approval": { + "waiting": "等待确认", + "confirm": "允许", + "reject": "拒绝", + "rejectWithInstruction": "说明后拒绝", + "enableAutoExecute": "开启工具自动执行", + "confirmTooltip": "允许执行这个工具", + "enableAutoExecuteTooltip": "开启工具自动执行", + "autoExecuteEnabled": "已开启工具自动执行", + "autoExecuteEnableFailed": "开启工具自动执行失败", + "rejectTooltip": "拒绝执行这个工具", + "rejectWithInstructionTooltip": "拒绝并告诉助手下一步怎么做", + "rejectInstructionLabel": "拒绝说明", + "rejectInstructionPlaceholder": "告诉助手改怎么做...", + "rejectWithInstructionSubmit": "拒绝", + "emptyInputTooltip": "这个工具没有可执行输入", + "remaining": "剩余 {{time}}", + "ariaLabel": "工具执行确认" + }, "execProcess": { "executeCommand": "运行命令:", "writeStdin": "写入标准输入:", @@ -1450,6 +1473,20 @@ "noTitle": "无标题", "summaryAvailable": "有搜索摘要" }, + "computerUse": { + "running": "正在控制桌面...", + "runningOnApp": "正在控制 {{app}}...", + "controllingApp": "正在控制 {{app}}", + "done": "操作已完成", + "actionFailed": "操作失败", + "actionLabel": "动作", + "appLabel": "应用", + "targetLabel": "目标", + "permissionDenied": "需要桌面控制权限", + "permissionDeniedHint": "BitFun 需要 macOS「辅助功能」和「屏幕录制」权限才能控制其他应用。请打开系统设置,为 BitFun 授权后重试。", + "openSettings": "打开系统设置", + "openSettingsFailed": "无法打开系统设置" + }, "webFetch": { "parsingUrl": "解析链接中...", "readLabel": "读取网页:", @@ -1580,6 +1617,7 @@ "executing": "执行中...", "completed": "已完成", "cancelled": "已取消", + "rejected": "已拒绝", "failed": "执行失败", "preparing": "准备中" }, diff --git a/src/web-ui/src/locales/zh-CN/scenes/agents.json b/src/web-ui/src/locales/zh-CN/scenes/agents.json index 800313d77c..cd5280ca7e 100644 --- a/src/web-ui/src/locales/zh-CN/scenes/agents.json +++ b/src/web-ui/src/locales/zh-CN/scenes/agents.json @@ -37,6 +37,7 @@ "subtitle": "平台内置的核心 Agent,覆盖主流 AI 工作流,开箱即用。", "empty": "暂未检测到核心智能体", "roleLabel": "主要应用 · ", + "computerUseDisabledBadge": "已在设置中禁用", "modes": { "agentic": { "role": "编码专业智能体" diff --git a/src/web-ui/src/locales/zh-CN/settings/agentic-tools.json b/src/web-ui/src/locales/zh-CN/settings/agentic-tools.json index 266bb8d7da..92d67bebb6 100644 --- a/src/web-ui/src/locales/zh-CN/settings/agentic-tools.json +++ b/src/web-ui/src/locales/zh-CN/settings/agentic-tools.json @@ -14,10 +14,14 @@ "config": { "autoExecute": "自动执行", "autoExecuteDesc": "跳过工具执行前的用户确认步骤。", + "subagentMaxConcurrency": "子智能体并发上限", + "subagentMaxConcurrencyDesc": "同一时间允许并行运行的子智能体数量。", "confirmTimeout": "确认超时", "confirmTimeoutDesc": "等待用户确认工具调用的最长时间(秒)。", + "confirmTimeoutHint": "设置为 0 可关闭确认超时。", "executionTimeout": "执行超时", - "executionTimeoutDesc": "工具执行的最长时间(秒),包含 Task 子智能体运行时长。", + "executionTimeoutDesc": "工具执行的最长时间(秒)。", + "executionTimeoutHint": "设置为 0 可关闭执行超时。子智能体和命令执行工具有各自的执行限制,不受此项约束。", "subagentBatchPolicy": { "label": "子智能体批量调度", "desc": "选择同一模型回复中多个子智能体启动请求的调度方式。", diff --git a/src/web-ui/src/locales/zh-CN/settings/ai-model.json b/src/web-ui/src/locales/zh-CN/settings/ai-model.json index 454e993745..da84c42568 100644 --- a/src/web-ui/src/locales/zh-CN/settings/ai-model.json +++ b/src/web-ui/src/locales/zh-CN/settings/ai-model.json @@ -219,6 +219,7 @@ }, "streamIdleTimeout": { "title": "流式超时", + "effectiveNextRound": "修改后下一次对话生效", "label": "Chunk 空闲超时(秒)", "hint": "流式响应开始后,两次 chunk 之间的最大等待时间。留空表示无限等待。", "placeholder": "留空表示不设超时", @@ -229,7 +230,7 @@ "streamTtftTimeout": { "title": "首 Token 超时", "label": "TTFT 超时(秒)", - "hint": "请求发出后,等待模型开始流式输出的最长时间。留空表示无限等待。", + "hint": "请求发出后,等待首个有效流式输出的最长时间。留空表示无限等待。", "placeholder": "留空表示不设超时", "invalid": "请输入正整数秒数,或留空。", "save": "保存首 Token 超时", @@ -270,6 +271,7 @@ "fillRequired": "请填写必要字段", "fillModelName": "请填写模型名称", "duplicateModelNameUnderProvider": "该服务商下已有同名模型,请改用其他名称或编辑已有条目", + "invalidBaseUrlScheme": "API地址必须以 http:// 或 https:// 开头", "saveFailed": "保存失败", "deleteFailed": "删除配置失败", "loadFailed": "加载AI配置失败", @@ -278,7 +280,10 @@ "errorDetails": "详细错误", "connectionTestMessages": { "toolCallsNotDetected": "本次测试未检测到工具调用,因此暂时无法验证工具调用能力。", - "imageInputCheckFailed": "连接已成功,但图片输入测试未通过。" + "imageInputCheckFailed": "连接已成功,但图片输入测试未通过。", + "tlsOrCertificateIssue": "连接失败可能与 SSL/TLS 证书校验有关。请检查公司代理、自签证书、系统根证书或网络拦截设置;如果确认网络环境可信,可在该模型的高级设置中开启“跳过SSL证书验证”,但这会降低 HTTPS 安全性。", + "proxyIssue": "连接失败可能与代理或隧道配置有关。请检查全局代理设置、代理认证、代理是否支持 HTTPS CONNECT,以及当前网络是否允许访问该模型 API 地址。", + "networkIssue": "连接失败发生在发送请求阶段,尚未收到服务端 HTTP 响应。请检查网络连通性、DNS、代理、防火墙和证书环境;如果处在公司代理或自签证书环境,也可能需要检查高级设置中的“跳过SSL证书验证”。" }, "autoSetPrimary": "已自动设为主力模型", "defaultDescription": "{{name}} 模型配置", diff --git a/src/web-ui/src/locales/zh-TW/common.json b/src/web-ui/src/locales/zh-TW/common.json index 1bef045176..6ab926d3cf 100644 --- a/src/web-ui/src/locales/zh-TW/common.json +++ b/src/web-ui/src/locales/zh-TW/common.json @@ -166,6 +166,9 @@ "modeCowork": "Cowork", "noSessions": "暫無會話", "rename": "重新命名", + "copySessionId": "複製 ID", + "copySessionIdSuccess": "已複製會話 ID", + "copySessionIdFailed": "複製會話 ID 失敗", "delete": "刪除", "confirmEdit": "確認", "cancelEdit": "取消", diff --git a/src/web-ui/src/locales/zh-TW/flow-chat.json b/src/web-ui/src/locales/zh-TW/flow-chat.json index 15f9d450c9..ac92de1736 100644 --- a/src/web-ui/src/locales/zh-TW/flow-chat.json +++ b/src/web-ui/src/locales/zh-TW/flow-chat.json @@ -44,6 +44,8 @@ "loadingDescription": "正在準備該會話的歷史內容。", "failedTitle": "歷史會話載入失敗", "failedDescription": "可以重新載入已儲存的會話內容。", + "preparingOlderHistory": "正在準備更早的歷史記錄...", + "olderHistoryNotReady": "更早的歷史記錄暫未準備好。", "remoteSessionMissing": "在後端找不到該會話資料。若剛重新連接過 SSH 遠端工作區,請關閉並重新開啟該遠端項目,或新增會話後再試。", "retry": "重試" }, @@ -553,6 +555,7 @@ "boostSectionAgent": "智能體", "boostSectionContext": "上下文", "boostStartBtw": "發起側問", + "boostNewSession": "新建會話", "boostAddContext": "引用檔案或資料夾", "boostSkills": "Skills", "modeSection": "模式", @@ -618,6 +621,7 @@ "noMatchingCommand": "沒有匹配的命令", "selectHint": "↑↓ 選擇 · Enter 確認 · Esc 取消", "current": "目前", + "computerUseDisabled": "已在「設定 → 工作階段權限」中停用 Computer Use", "professionalMode": "專業模式", "designMode": "設計模式", "wip": "開發中", @@ -1357,6 +1361,25 @@ "commandInterrupted": "命令已被中斷", "executionFailed": "命令執行失敗" }, + "approval": { + "waiting": "等待確認", + "confirm": "允許", + "reject": "拒絕", + "rejectWithInstruction": "說明後拒絕", + "enableAutoExecute": "開啟工具自動執行", + "confirmTooltip": "允許執行這個工具", + "enableAutoExecuteTooltip": "開啟工具自動執行", + "autoExecuteEnabled": "已開啟工具自動執行", + "autoExecuteEnableFailed": "開啟工具自動執行失敗", + "rejectTooltip": "拒絕執行這個工具", + "rejectWithInstructionTooltip": "拒絕並告訴助手下一步怎麼做", + "rejectInstructionLabel": "拒絕說明", + "rejectInstructionPlaceholder": "告訴助手改怎麼做...", + "rejectWithInstructionSubmit": "拒絕", + "emptyInputTooltip": "這個工具沒有可執行輸入", + "remaining": "剩餘 {{time}}", + "ariaLabel": "工具執行確認" + }, "execProcess": { "executeCommand": "執行命令:", "writeStdin": "寫入標準輸入:", @@ -1450,6 +1473,20 @@ "noTitle": "無標題", "summaryAvailable": "有搜尋摘要" }, + "computerUse": { + "running": "正在控制桌面...", + "runningOnApp": "正在控制 {{app}}...", + "controllingApp": "正在控制 {{app}}", + "done": "操作已完成", + "actionFailed": "操作失敗", + "actionLabel": "動作", + "appLabel": "應用", + "targetLabel": "目標", + "permissionDenied": "需要桌面控制權限", + "permissionDeniedHint": "BitFun 需要 macOS「輔助使用」和「螢幕錄製」權限才能控制其他應用。請打開系統設定,為 BitFun 授權後重試。", + "openSettings": "打開系統設定", + "openSettingsFailed": "無法打開系統設定" + }, "webFetch": { "parsingUrl": "解析連結中...", "readLabel": "讀取網頁:", @@ -1580,6 +1617,7 @@ "executing": "執行中...", "completed": "已完成", "cancelled": "已取消", + "rejected": "已拒絕", "failed": "執行失敗", "preparing": "準備中" }, diff --git a/src/web-ui/src/locales/zh-TW/scenes/agents.json b/src/web-ui/src/locales/zh-TW/scenes/agents.json index f0ac4535ed..f5b07bd5ba 100644 --- a/src/web-ui/src/locales/zh-TW/scenes/agents.json +++ b/src/web-ui/src/locales/zh-TW/scenes/agents.json @@ -32,6 +32,7 @@ "subtitle": "平臺內置的核心 Agent,覆蓋主流 AI 工作流,開箱即用。", "empty": "暫未檢測到核心智能體", "roleLabel": "主要應用 · ", + "computerUseDisabledBadge": "已在設定中停用", "modes": { "agentic": { "role": "編碼專業智能體" diff --git a/src/web-ui/src/locales/zh-TW/settings/agentic-tools.json b/src/web-ui/src/locales/zh-TW/settings/agentic-tools.json index f5f5fa766c..a0d37abf2d 100644 --- a/src/web-ui/src/locales/zh-TW/settings/agentic-tools.json +++ b/src/web-ui/src/locales/zh-TW/settings/agentic-tools.json @@ -14,10 +14,14 @@ "config": { "autoExecute": "自動執行", "autoExecuteDesc": "跳過工具執行前的用戶確認步驟。", + "subagentMaxConcurrency": "子智能體並發上限", + "subagentMaxConcurrencyDesc": "同一時間允許並行執行的子智能體數量。", "confirmTimeout": "確認超時", "confirmTimeoutDesc": "等待用戶確認工具調用的最長時間(秒)。", + "confirmTimeoutHint": "設為 0 可關閉確認超時。", "executionTimeout": "執行超時", - "executionTimeoutDesc": "工具執行的最長時間(秒),包含 Task 子智能體執行時長。", + "executionTimeoutDesc": "工具執行的最長時間(秒)。", + "executionTimeoutHint": "設為 0 可關閉執行超時。子智能體和命令執行工具有各自的執行限制,不受此項約束。", "subagentBatchPolicy": { "label": "子智能體批量調度", "desc": "選擇同一模型回覆中多個子智能體啟動請求的調度方式。", diff --git a/src/web-ui/src/locales/zh-TW/settings/ai-model.json b/src/web-ui/src/locales/zh-TW/settings/ai-model.json index 57272be4fb..3a00b03256 100644 --- a/src/web-ui/src/locales/zh-TW/settings/ai-model.json +++ b/src/web-ui/src/locales/zh-TW/settings/ai-model.json @@ -219,6 +219,7 @@ }, "streamIdleTimeout": { "title": "流式超時", + "effectiveNextRound": "修改後下一次對話生效", "label": "Chunk 空閒超時(秒)", "hint": "流式響應開始後,兩次 chunk 之間的最大等待時間。留空表示無限等待。", "placeholder": "留空表示不設超時", @@ -229,7 +230,7 @@ "streamTtftTimeout": { "title": "首 Token 超時", "label": "TTFT 超時(秒)", - "hint": "請求發出後,等待模型開始流式輸出的最長時間。留空表示無限等待。", + "hint": "請求發出後,等待首個有效流式輸出的最長時間。留空表示無限等待。", "placeholder": "留空表示不設超時", "invalid": "請輸入正整數秒數,或留空。", "save": "儲存首 Token 超時", @@ -270,6 +271,7 @@ "fillRequired": "請填寫必要字段", "fillModelName": "請填寫模型名稱", "duplicateModelNameUnderProvider": "該服務商下已有同名模型,請改用其他名稱或編輯已有條目", + "invalidBaseUrlScheme": "API地址必須以 http:// 或 https:// 開頭", "saveFailed": "儲存失敗", "deleteFailed": "刪除設定失敗", "loadFailed": "載入AI設定失敗", @@ -278,7 +280,10 @@ "errorDetails": "詳細錯誤", "connectionTestMessages": { "toolCallsNotDetected": "本次測試未檢測到工具調用,因此暫時無法驗證工具調用能力。", - "imageInputCheckFailed": "連接已成功,但圖片輸入測試未通過。" + "imageInputCheckFailed": "連接已成功,但圖片輸入測試未通過。", + "tlsOrCertificateIssue": "連接失敗可能與 SSL/TLS 證書校驗有關。請檢查公司代理、自簽證書、系統根證書或網路攔截設定;如果確認網路環境可信,可在該模型的高級設置中開啟「跳過SSL證書驗證」,但這會降低 HTTPS 安全性。", + "proxyIssue": "連接失敗可能與代理或隧道設定有關。請檢查全局代理設定、代理認證、代理是否支援 HTTPS CONNECT,以及目前網路是否允許訪問該模型 API 地址。", + "networkIssue": "連接失敗發生在發送請求階段,尚未收到服務端 HTTP 回應。請檢查網路連通性、DNS、代理、防火牆和證書環境;如果處在公司代理或自簽證書環境,也可能需要檢查高級設置中的「跳過SSL證書驗證」。" }, "autoSetPrimary": "已自動設為主力模型", "defaultDescription": "{{name}} 模型設定", diff --git a/src/web-ui/src/shared/announcement-system/styles/AnnouncementToast.scss b/src/web-ui/src/shared/announcement-system/styles/AnnouncementToast.scss index f3c9de20df..284fecd967 100644 --- a/src/web-ui/src/shared/announcement-system/styles/AnnouncementToast.scss +++ b/src/web-ui/src/shared/announcement-system/styles/AnnouncementToast.scss @@ -158,7 +158,7 @@ $toast-width: 320px; &__ring-fill { fill: none; - stroke: var(--color-primary); + stroke: var(--color-accent-500); stroke-width: 1.5; stroke-linecap: round; stroke-dasharray: $ring-circumference; @@ -229,7 +229,7 @@ $toast-width: 320px; white-space: nowrap; &--primary { - background: var(--color-primary); + background: var(--color-accent-500); color: var(--color-static-white); &:hover { opacity: 0.9; } diff --git a/src/web-ui/src/shared/announcement-system/styles/FeatureModal.scss b/src/web-ui/src/shared/announcement-system/styles/FeatureModal.scss index 97ed36a834..06904e1970 100644 --- a/src/web-ui/src/shared/announcement-system/styles/FeatureModal.scss +++ b/src/web-ui/src/shared/announcement-system/styles/FeatureModal.scss @@ -113,8 +113,8 @@ &--active { width: 18px; border-radius: 3px; - background: var(--color-primary); - border-color: var(--color-primary); + background: var(--color-accent-500); + border-color: var(--color-accent-500); } &:hover:not(&--active) { @@ -150,7 +150,7 @@ &--next, &--done { - background: var(--color-primary); + background: var(--color-accent-500); color: var(--color-static-white); padding: 6px 22px; @@ -238,7 +238,7 @@ width: 28px; height: 3px; border-radius: 2px; - background: var(--color-primary); + background: var(--color-accent-500); flex-shrink: 0; } } diff --git a/src/web-ui/src/shared/context-menu-system/components/ui/ContextMenu.scss b/src/web-ui/src/shared/context-menu-system/components/ui/ContextMenu.scss index 6b2946c7d7..8f58fc205a 100644 --- a/src/web-ui/src/shared/context-menu-system/components/ui/ContextMenu.scss +++ b/src/web-ui/src/shared/context-menu-system/components/ui/ContextMenu.scss @@ -14,7 +14,7 @@ background: var(--color-bg-elevated); border: 1px solid var(--border-subtle); border-radius: $size-radius-base; - box-shadow: 0 4px 12px var(--color-overlay-black-25); + box-shadow: 0 4px 12px var(--color-overlay-black-30); padding: $size-gap-1 0; box-sizing: border-box; @@ -33,7 +33,7 @@ :root[data-theme-type='light'] & { box-shadow: - 0 1px 2px var(--color-overlay-slate-06), + 0 1px 2px var(--color-overlay-slate-08), 0 10px 28px -8px var(--color-overlay-slate-12); } diff --git a/src/web-ui/src/shared/context-system/components/ContextCard/ContextCard.scss b/src/web-ui/src/shared/context-system/components/ContextCard/ContextCard.scss index f38b858ce4..0c06ba729b 100644 --- a/src/web-ui/src/shared/context-system/components/ContextCard/ContextCard.scss +++ b/src/web-ui/src/shared/context-system/components/ContextCard/ContextCard.scss @@ -13,10 +13,10 @@ background: linear-gradient(135deg, var(--color-overlay-white-08) 0%, - var(--color-overlay-white-06) 50%, - var(--color-overlay-white-10) 100% + var(--color-overlay-white-08) 50%, + var(--color-overlay-white-12) 100% ); - border: 1px solid $border-base; + border: 1px solid var(--border-base); border-radius: $size-radius-base; padding: $size-gap-3; @@ -73,7 +73,7 @@ width: 32px; height: 32px; border-radius: $size-radius-sm; - background: $element-bg-soft; + background: var(--element-bg-soft); color: var(--color-text-secondary); svg { @@ -142,9 +142,9 @@ font-size: 11px; font-weight: $font-weight-medium; color: var(--color-text-secondary); - background: $element-bg-medium; + background: var(--element-bg-medium); border-radius: $size-radius-sm; - border: 1px solid $border-subtle; + border: 1px solid var(--border-subtle); } @@ -153,7 +153,7 @@ margin-top: $size-gap-2; padding: $size-gap-2; background: var(--color-bg-tertiary); - border: 1px solid $border-subtle; + border: 1px solid var(--border-subtle); border-radius: $size-radius-sm; max-height: 100px; overflow: auto; @@ -193,7 +193,7 @@ transition: all $motion-fast $easing-standard; &:hover { - background: $element-bg-medium; + background: var(--element-bg-medium); color: var(--color-text-secondary); } @@ -235,7 +235,7 @@ &__error { background: var(--color-error-bg); - border: 1px solid $color-error-border; + border: 1px solid var(--color-error-border); color: var(--color-error); } @@ -247,7 +247,7 @@ &__warning { background: var(--color-warning-bg); - border: 1px solid $color-warning-border; + border: 1px solid var(--color-warning-border); color: var(--color-warning); } @@ -305,7 +305,7 @@ transform: translateY(-2px); box-shadow: 0 8px 24px var(--color-accent-200), - 0 4px 12px var(--color-overlay-black-10); + 0 4px 12px var(--color-overlay-black-12); } &:active { diff --git a/src/web-ui/src/shared/context-system/components/ContextList/ContextList.scss b/src/web-ui/src/shared/context-system/components/ContextList/ContextList.scss index ce3123ea29..4744def107 100644 --- a/src/web-ui/src/shared/context-system/components/ContextList/ContextList.scss +++ b/src/web-ui/src/shared/context-system/components/ContextList/ContextList.scss @@ -38,8 +38,8 @@ font-size: 11px; font-weight: $font-weight-semibold; color: var(--color-text-primary); - background: $element-bg-medium; - border: 1px solid $border-subtle; + background: var(--element-bg-medium); + border: 1px solid var(--border-subtle); border-radius: $size-radius-full; } @@ -52,7 +52,7 @@ font-weight: $font-weight-medium; color: var(--color-text-muted); background: transparent; - border: 1px solid $border-subtle; + border: 1px solid var(--border-subtle); border-radius: $size-radius-sm; cursor: pointer; transition: all $motion-fast $easing-standard; diff --git a/src/web-ui/src/shared/context-system/core/types/CodeSnippetContextImpl.test.ts b/src/web-ui/src/shared/context-system/core/types/CodeSnippetContextImpl.test.ts index 11ed7b2778..02e142b162 100644 --- a/src/web-ui/src/shared/context-system/core/types/CodeSnippetContextImpl.test.ts +++ b/src/web-ui/src/shared/context-system/core/types/CodeSnippetContextImpl.test.ts @@ -14,10 +14,10 @@ describe('CodeSnippetContextImpl language metadata', () => { it('keeps code snippet language accents centralized', () => { expect(getLanguageColor('typescript')).toBe('#3178c6'); - expect(getLanguageColor('rust')).toBe('var(--color-bg-primary)'); - expect(getLanguageColor('java')).toBe('#007396'); - expect(getLanguageColor('unknown-lang')).toBe('#858585'); - expect(getLanguageColor()).toBe('#858585'); + expect(getLanguageColor('rust')).toBe('#e38c00'); + expect(getLanguageColor('java')).toBe('#e38c00'); + expect(getLanguageColor('unknown-lang')).toBe('#64748b'); + expect(getLanguageColor()).toBe('#64748b'); }); it('keeps mermaid diagram context color in the UI exception registry', () => { diff --git a/src/web-ui/src/shared/context-system/drag-drop/ContextDropZone.scss b/src/web-ui/src/shared/context-system/drag-drop/ContextDropZone.scss index 3d52d5ab65..474b4ca620 100644 --- a/src/web-ui/src/shared/context-system/drag-drop/ContextDropZone.scss +++ b/src/web-ui/src/shared/context-system/drag-drop/ContextDropZone.scss @@ -29,11 +29,11 @@ align-items: center; justify-content: center; - background: $modal-backdrop; + background: var(--color-overlay-black-80); backdrop-filter: blur(12px); -webkit-backdrop-filter: blur(12px); - border: 3px dashed $border-accent; + border: 3px dashed var(--border-accent); border-radius: $size-radius-lg; pointer-events: none; diff --git a/src/web-ui/src/shared/notification-system/components/LoadingNotification.scss b/src/web-ui/src/shared/notification-system/components/LoadingNotification.scss index 27a4be14f9..180d319fa9 100644 --- a/src/web-ui/src/shared/notification-system/components/LoadingNotification.scss +++ b/src/web-ui/src/shared/notification-system/components/LoadingNotification.scss @@ -8,7 +8,7 @@ background: var(--color-bg-primary); border-radius: $size-radius-base; box-shadow: 0 2px 6px var(--color-overlay-black-08); - border: 1px solid $border-base; + border: 1px solid var(--border-base); padding: $size-gap-2 $size-gap-3; display: flex; align-items: flex-start; @@ -104,7 +104,7 @@ transition: all $motion-base $easing-standard; &:hover { - background: $element-bg-base; + background: var(--element-bg-base); color: var(--color-text-primary); } diff --git a/src/web-ui/src/shared/notification-system/components/NotificationCenter.scss b/src/web-ui/src/shared/notification-system/components/NotificationCenter.scss index bcde11acf7..6680cd11b7 100644 --- a/src/web-ui/src/shared/notification-system/components/NotificationCenter.scss +++ b/src/web-ui/src/shared/notification-system/components/NotificationCenter.scss @@ -157,7 +157,7 @@ color: var(--color-text-muted); text-transform: uppercase; letter-spacing: 0.5px; - background: $element-bg-subtle; + background: var(--element-bg-subtle); user-select: none; } @@ -350,7 +350,7 @@ &__item-progress-bar { width: 100%; height: 3px; - background: $element-bg-base; + background: var(--element-bg-base); border-radius: 1px; overflow: hidden; margin: 4px 0; @@ -358,7 +358,7 @@ &__item-progress-fill { height: 100%; - background: linear-gradient(90deg, $color-accent-600, $color-purple-500); + background: linear-gradient(90deg, var(--color-accent-600), var(--color-purple-500)); border-radius: 2px; transition: width 0.3s cubic-bezier(0.4, 0, 0.2, 1); @@ -371,7 +371,7 @@ } &.is-cancelled { - background: $color-text-muted; + background: var(--color-text-muted); } } @@ -557,7 +557,7 @@ &__active-task-progress-bar { width: 100%; height: 3px; - background: $element-bg-base; + background: var(--element-bg-base); border-radius: 1px; overflow: hidden; margin-top: 4px; @@ -565,7 +565,7 @@ &__active-task-progress-fill { height: 100%; - background: linear-gradient(90deg, $color-accent-600, $color-purple-500); + background: linear-gradient(90deg, var(--color-accent-600), var(--color-purple-500)); border-radius: 1px; transition: width 0.3s cubic-bezier(0.4, 0, 0.2, 1); } diff --git a/src/web-ui/src/shared/notification-system/components/NotificationItem.scss b/src/web-ui/src/shared/notification-system/components/NotificationItem.scss index c4d586f5d7..4551572a7a 100644 --- a/src/web-ui/src/shared/notification-system/components/NotificationItem.scss +++ b/src/web-ui/src/shared/notification-system/components/NotificationItem.scss @@ -89,14 +89,14 @@ border: none; background: none; font: inherit; - color: var(--color-primary); + color: var(--color-accent-500); text-decoration: underline; text-align: left; cursor: pointer; word-break: break-all; &:hover { - color: var(--color-accent); + color: var(--color-accent-500); } } @@ -130,11 +130,11 @@ } &--secondary { - background: $element-bg-base; + background: var(--element-bg-base); color: var(--color-text-secondary); &:hover { - background: $element-bg-medium; + background: var(--element-bg-medium); color: var(--color-text-primary); } @@ -148,7 +148,7 @@ color: var(--color-text-primary); &:hover { - background: $git-color-deleted; + background: var(--git-color-deleted); } &:active { @@ -173,7 +173,7 @@ transition: all $motion-base $easing-standard; &:hover { - background: $element-bg-base; + background: var(--element-bg-base); color: var(--color-text-primary); } diff --git a/src/web-ui/src/shared/notification-system/components/ProgressNotification.scss b/src/web-ui/src/shared/notification-system/components/ProgressNotification.scss index b2c6940d8d..07f9aa15fc 100644 --- a/src/web-ui/src/shared/notification-system/components/ProgressNotification.scss +++ b/src/web-ui/src/shared/notification-system/components/ProgressNotification.scss @@ -116,14 +116,14 @@ &__progress-bar { width: 100%; height: 3px; - background: $element-bg-base; + background: var(--element-bg-base); border-radius: 2px; overflow: hidden; } &__progress-fill { height: 100%; - background: linear-gradient(90deg, $color-accent-600, $color-purple-500); + background: linear-gradient(90deg, var(--color-accent-600), var(--color-purple-500)); border-radius: 2px; transition: width 0.3s cubic-bezier(0.4, 0, 0.2, 1); } @@ -154,7 +154,7 @@ transition: all $motion-base $easing-standard; &:hover { - background: $element-bg-base; + background: var(--element-bg-base); color: var(--color-text-primary); } diff --git a/src/web-ui/src/shared/services/agent-service.ts b/src/web-ui/src/shared/services/agent-service.ts index 2750fa4312..6dc49e1db7 100644 --- a/src/web-ui/src/shared/services/agent-service.ts +++ b/src/web-ui/src/shared/services/agent-service.ts @@ -630,13 +630,15 @@ export class AgentService { sessionId: string, toolUseId: string, action: 'confirm' | 'reject', - updatedInput?: any + updatedInput?: any, + rejectReason?: string ): Promise { const requestPayload = { sessionId, toolId: toolUseId, action, - updatedInput: updatedInput || null + updatedInput: updatedInput || null, + rejectReason }; return toolAPI.confirmToolExecution(requestPayload); diff --git a/src/web-ui/src/shared/theme/languageIdentityAccents.ts b/src/web-ui/src/shared/theme/languageIdentityAccents.ts index 49ed621a27..9d4c9a9593 100644 --- a/src/web-ui/src/shared/theme/languageIdentityAccents.ts +++ b/src/web-ui/src/shared/theme/languageIdentityAccents.ts @@ -1,68 +1,79 @@ // Language identity colors are data semantics, not app theme surface colors. // Keep this registry narrow and only add values that are consumed outside the // full language registry. +const LANGUAGE_IDENTITY_PALETTE = { + blue: '#3178c6', + cyan: '#00add8', + yellow: '#f7df1e', + orange: '#e38c00', + red: '#ef4444', + green: '#22c55e', + purple: '#8b5cf6', + slate: '#64748b', +} as const; + export const BUILTIN_LANGUAGE_ACCENTS = { - typescript: '#3178c6', - typescriptReact: '#61dafb', - javascript: '#f7df1e', - javascriptReact: '#61dafb', - python: '#3776ab', - rust: '#ce422b', - go: '#00add8', - java: '#b07219', - kotlin: '#a97bff', - cpp: '#f34b7d', - c: '#555555', - csharp: '#178600', - swift: '#f05138', - php: '#4f5d95', - ruby: '#cc342d', - scala: '#c22d40', - dart: '#00b4ab', - lua: '#000080', - r: '#198ce7', - html: '#e34c26', - xml: '#0060ac', - vue: '#41b883', - svelte: '#ff3e00', - css: '#563d7c', - scss: '#c6538c', - sass: '#c6538c', - less: '#1d365d', - json: '#cbcb41', - yaml: '#cb171e', - toml: '#9c4121', - sql: '#e38c00', - graphql: '#e10098', - dockerfile: '#2496ed', - makefile: '#427819', - ini: '#d1dbe0', - env: '#ecd53f', - shell: '#89e051', - powershell: '#012456', - batch: '#c1f12e', - markdown: '#083fa1', - restructuredtext: '#141414', - image: '#a855f7', - audio: '#22c55e', - video: '#ef4444', - font: '#f59e0b', - archive: '#8b5cf6', - binary: '#64748b', - plaintext: '#6e7781', + typescript: LANGUAGE_IDENTITY_PALETTE.blue, + typescriptReact: LANGUAGE_IDENTITY_PALETTE.cyan, + javascript: LANGUAGE_IDENTITY_PALETTE.yellow, + javascriptReact: LANGUAGE_IDENTITY_PALETTE.cyan, + python: LANGUAGE_IDENTITY_PALETTE.blue, + rust: LANGUAGE_IDENTITY_PALETTE.orange, + go: LANGUAGE_IDENTITY_PALETTE.cyan, + java: LANGUAGE_IDENTITY_PALETTE.orange, + kotlin: LANGUAGE_IDENTITY_PALETTE.purple, + cpp: LANGUAGE_IDENTITY_PALETTE.red, + c: LANGUAGE_IDENTITY_PALETTE.slate, + csharp: LANGUAGE_IDENTITY_PALETTE.green, + swift: LANGUAGE_IDENTITY_PALETTE.red, + php: LANGUAGE_IDENTITY_PALETTE.purple, + ruby: LANGUAGE_IDENTITY_PALETTE.red, + scala: LANGUAGE_IDENTITY_PALETTE.red, + dart: LANGUAGE_IDENTITY_PALETTE.cyan, + lua: LANGUAGE_IDENTITY_PALETTE.blue, + r: LANGUAGE_IDENTITY_PALETTE.blue, + html: LANGUAGE_IDENTITY_PALETTE.red, + xml: LANGUAGE_IDENTITY_PALETTE.blue, + vue: LANGUAGE_IDENTITY_PALETTE.green, + svelte: LANGUAGE_IDENTITY_PALETTE.orange, + css: LANGUAGE_IDENTITY_PALETTE.blue, + scss: LANGUAGE_IDENTITY_PALETTE.purple, + sass: LANGUAGE_IDENTITY_PALETTE.purple, + less: LANGUAGE_IDENTITY_PALETTE.blue, + json: LANGUAGE_IDENTITY_PALETTE.yellow, + yaml: LANGUAGE_IDENTITY_PALETTE.red, + toml: LANGUAGE_IDENTITY_PALETTE.orange, + sql: LANGUAGE_IDENTITY_PALETTE.orange, + graphql: LANGUAGE_IDENTITY_PALETTE.purple, + dockerfile: LANGUAGE_IDENTITY_PALETTE.blue, + makefile: LANGUAGE_IDENTITY_PALETTE.green, + ini: LANGUAGE_IDENTITY_PALETTE.slate, + env: LANGUAGE_IDENTITY_PALETTE.yellow, + shell: LANGUAGE_IDENTITY_PALETTE.green, + powershell: LANGUAGE_IDENTITY_PALETTE.blue, + batch: LANGUAGE_IDENTITY_PALETTE.green, + markdown: LANGUAGE_IDENTITY_PALETTE.blue, + restructuredtext: LANGUAGE_IDENTITY_PALETTE.slate, + image: LANGUAGE_IDENTITY_PALETTE.purple, + audio: LANGUAGE_IDENTITY_PALETTE.green, + video: LANGUAGE_IDENTITY_PALETTE.red, + font: LANGUAGE_IDENTITY_PALETTE.orange, + archive: LANGUAGE_IDENTITY_PALETTE.purple, + binary: LANGUAGE_IDENTITY_PALETTE.slate, + plaintext: LANGUAGE_IDENTITY_PALETTE.slate, } as const; export const CODE_SNIPPET_LANGUAGE_ACCENTS = { javascript: BUILTIN_LANGUAGE_ACCENTS.javascript, typescript: BUILTIN_LANGUAGE_ACCENTS.typescript, python: BUILTIN_LANGUAGE_ACCENTS.python, - rust: 'var(--color-bg-primary)', + rust: BUILTIN_LANGUAGE_ACCENTS.rust, go: BUILTIN_LANGUAGE_ACCENTS.go, - java: '#007396', + java: BUILTIN_LANGUAGE_ACCENTS.java, html: BUILTIN_LANGUAGE_ACCENTS.html, - css: '#1572b6', - scss: '#cc6699', - fallback: '#858585', + css: BUILTIN_LANGUAGE_ACCENTS.css, + scss: BUILTIN_LANGUAGE_ACCENTS.scss, + fallback: LANGUAGE_IDENTITY_PALETTE.slate, } as const; export function getCodeSnippetLanguageAccent(language?: string): string { diff --git a/src/web-ui/src/shared/theme/syntaxHighlightAccents.ts b/src/web-ui/src/shared/theme/syntaxHighlightAccents.ts index e84f20324d..ce0df6e966 100644 --- a/src/web-ui/src/shared/theme/syntaxHighlightAccents.ts +++ b/src/web-ui/src/shared/theme/syntaxHighlightAccents.ts @@ -1,26 +1,48 @@ // Syntax highlight colors are renderer palettes, not app theme surface colors. // Keep them centralized so Prism/Markdown surfaces do not carry app raw color debt. +const LIGHT_PRISM_PALETTE = { + foreground: '#24292f', + muted: '#6e7781', + keyword: '#cf222e', + literal: '#0550ae', + functionName: '#8250df', + tag: '#116329', + punctuation: '#57606a', + property: '#953800', +} as const; + +const DARK_PRISM_PALETTE = { + foreground: '#d4d4d4', + comment: '#6a9955', + keyword: '#c586c0', + string: '#ce9178', + functionName: '#dcdcaa', + number: '#b5cea8', + tag: '#569cd6', + property: '#9cdcfe', +} as const; + export const SHARED_PRISM_COLOR_SCHEME = { light: { - foreground: '#24292f', - comment: '#6e7781', - keyword: '#cf222e', - string: '#0a3069', - functionName: '#8250df', - number: '#0550ae', - tag: '#116329', - punctuation: '#57606a', - property: '#953800', + foreground: LIGHT_PRISM_PALETTE.foreground, + comment: LIGHT_PRISM_PALETTE.muted, + keyword: LIGHT_PRISM_PALETTE.keyword, + string: LIGHT_PRISM_PALETTE.literal, + functionName: LIGHT_PRISM_PALETTE.functionName, + number: LIGHT_PRISM_PALETTE.literal, + tag: LIGHT_PRISM_PALETTE.tag, + punctuation: LIGHT_PRISM_PALETTE.punctuation, + property: LIGHT_PRISM_PALETTE.property, }, dark: { - foreground: '#d4d4d4', - comment: '#6a9955', - keyword: '#c586c0', - string: '#ce9178', - functionName: '#dcdcaa', - number: '#b5cea8', - tag: '#569cd6', - punctuation: '#d4d4d4', - property: '#9cdcfe', + foreground: DARK_PRISM_PALETTE.foreground, + comment: DARK_PRISM_PALETTE.comment, + keyword: DARK_PRISM_PALETTE.keyword, + string: DARK_PRISM_PALETTE.string, + functionName: DARK_PRISM_PALETTE.functionName, + number: DARK_PRISM_PALETTE.number, + tag: DARK_PRISM_PALETTE.tag, + punctuation: DARK_PRISM_PALETTE.foreground, + property: DARK_PRISM_PALETTE.property, }, } as const; diff --git a/src/web-ui/src/shared/theme/themeBoundaryFallbacks.ts b/src/web-ui/src/shared/theme/themeBoundaryFallbacks.ts index 9a76a1f509..9d29225cd2 100644 --- a/src/web-ui/src/shared/theme/themeBoundaryFallbacks.ts +++ b/src/web-ui/src/shared/theme/themeBoundaryFallbacks.ts @@ -1,6 +1,6 @@ // Last-resort values for isolated surfaces that can render before root theme // variables are available. Keep these values exact and boundary-scoped. -export const WIDGET_IFRAME_FALLBACK_COLOR = { +const BOUNDARY_FALLBACK_COLOR = { textPrimary: '#e8e8e8', textSecondary: '#b0b0b0', textMuted: '#858585', @@ -11,26 +11,48 @@ export const WIDGET_IFRAME_FALLBACK_COLOR = { warning: '#f59e0b', error: '#ef4444', staticWhite: '#ffffff', - borderSubtle: 'rgba(255, 255, 255, 0.1)', - borderBase: 'rgba(255, 255, 255, 0.16)', - borderMedium: 'rgba(255, 255, 255, 0.24)', - elementBgSubtle: 'rgba(255, 255, 255, 0.05)', - elementBgBase: 'rgba(255, 255, 255, 0.08)', - elementBgMedium: 'rgba(255, 255, 255, 0.14)', + staticBlack: '#000000', + overlayWhite08: 'rgba(255, 255, 255, 0.08)', + overlayWhite12: 'rgba(255, 255, 255, 0.12)', + overlayWhite24: 'rgba(255, 255, 255, 0.24)', + overlayBlack15: 'rgba(0, 0, 0, 0.15)', + overlayBlack30: 'rgba(0, 0, 0, 0.3)', shadowBase: 'rgba(0, 0, 0, 0.4)', + captureBackground: '#121214', +} as const; + +export const WIDGET_IFRAME_FALLBACK_COLOR = { + textPrimary: BOUNDARY_FALLBACK_COLOR.textPrimary, + textSecondary: BOUNDARY_FALLBACK_COLOR.textSecondary, + textMuted: BOUNDARY_FALLBACK_COLOR.textMuted, + accent500: BOUNDARY_FALLBACK_COLOR.accent500, + accent600: BOUNDARY_FALLBACK_COLOR.accent600, + bgSecondary: BOUNDARY_FALLBACK_COLOR.bgSecondary, + success: BOUNDARY_FALLBACK_COLOR.success, + warning: BOUNDARY_FALLBACK_COLOR.warning, + error: BOUNDARY_FALLBACK_COLOR.error, + staticWhite: BOUNDARY_FALLBACK_COLOR.staticWhite, + staticBlack: BOUNDARY_FALLBACK_COLOR.staticBlack, + borderSubtle: BOUNDARY_FALLBACK_COLOR.overlayWhite12, + borderBase: BOUNDARY_FALLBACK_COLOR.overlayWhite12, + borderMedium: BOUNDARY_FALLBACK_COLOR.overlayWhite24, + elementBgSubtle: BOUNDARY_FALLBACK_COLOR.overlayWhite08, + elementBgBase: BOUNDARY_FALLBACK_COLOR.overlayWhite12, + elementBgMedium: BOUNDARY_FALLBACK_COLOR.overlayWhite12, + shadowBase: BOUNDARY_FALLBACK_COLOR.shadowBase, } as const; export const MINI_APP_SCROLLBAR_FALLBACKS = { dark: { - thumb: 'rgba(255, 255, 255, 0.12)', - thumbHover: 'rgba(255, 255, 255, 0.22)', + thumb: BOUNDARY_FALLBACK_COLOR.overlayWhite12, + thumbHover: BOUNDARY_FALLBACK_COLOR.overlayWhite24, }, light: { - thumb: 'rgba(0, 0, 0, 0.15)', - thumbHover: 'rgba(0, 0, 0, 0.28)', + thumb: BOUNDARY_FALLBACK_COLOR.overlayBlack15, + thumbHover: BOUNDARY_FALLBACK_COLOR.overlayBlack30, }, } as const; export const FLOWCHAT_CAPTURE_FALLBACK_COLOR = { - background: '#121214', + background: BOUNDARY_FALLBACK_COLOR.captureBackground, } as const; diff --git a/src/web-ui/src/shared/theme/uiExceptionAccents.ts b/src/web-ui/src/shared/theme/uiExceptionAccents.ts index ed66684514..372f5c0f14 100644 --- a/src/web-ui/src/shared/theme/uiExceptionAccents.ts +++ b/src/web-ui/src/shared/theme/uiExceptionAccents.ts @@ -1,56 +1,71 @@ -// Concrete exception accents shared by UI metadata that must preserve existing visuals. -// Prefer theme tokens for theme-aware surfaces; do not treat this as a token contract. +// Concrete exception accents shared by UI metadata. Keep this palette compact: +// theme-aware surfaces should use theme tokens, and metadata-only accents should +// map back to one of these reviewed semantic colors instead of adding one-off hexes. +const EXCEPTION_ACCENT = { + primary: '#3b82f6', + secondary: '#8b5cf6', + info: '#06b6d4', + success: '#22c55e', + warning: '#f59e0b', + error: '#ef4444', + neutral: '#64748b', + teal: '#14b8a6', + strokeYellow: '#eab308', + textOnDark: '#e2e8f0', + staticWhite: '#ffffff', +} as const; + export const UI_EXCEPTION_ACCENTS = { - contextCompression: '#a855f7', - generativeUi: '#38bdf8', - miniApp: '#7c8cef', - mermaidDiagram: '#22c55e', + contextCompression: EXCEPTION_ACCENT.secondary, + generativeUi: EXCEPTION_ACCENT.info, + miniApp: EXCEPTION_ACCENT.secondary, + mermaidDiagram: EXCEPTION_ACCENT.success, agentCapability: { - docs: '#6eb88c', - testing: '#c9944d', - creative: '#e879a0', - ops: '#5ea3a3', + docs: EXCEPTION_ACCENT.success, + testing: EXCEPTION_ACCENT.warning, + creative: EXCEPTION_ACCENT.secondary, + ops: EXCEPTION_ACCENT.info, }, insights: { - positive: '#6eb88c', - time: '#818cf8', - neutral: '#c9944d', - issue: '#c77070', + positive: EXCEPTION_ACCENT.success, + time: EXCEPTION_ACCENT.secondary, + neutral: EXCEPTION_ACCENT.warning, + issue: EXCEPTION_ACCENT.error, }, progress: { - compacting: '#0f766e', + compacting: EXCEPTION_ACCENT.teal, }, templateContext: { - memories: '#f472b6', + memories: EXCEPTION_ACCENT.secondary, }, reviewTeam: { - memberDefault: '#64748b', - businessLogic: '#2563eb', - performance: '#d97706', - security: '#dc2626', - architecture: '#0891b2', - frontend: '#059669', - judge: '#7c3aed', + memberDefault: EXCEPTION_ACCENT.neutral, + businessLogic: EXCEPTION_ACCENT.primary, + performance: EXCEPTION_ACCENT.warning, + security: EXCEPTION_ACCENT.error, + architecture: EXCEPTION_ACCENT.info, + frontend: EXCEPTION_ACCENT.success, + judge: EXCEPTION_ACCENT.secondary, }, - tealAction: '#14b8a6', - todo: '#0d9488', + tealAction: EXCEPTION_ACCENT.teal, + todo: EXCEPTION_ACCENT.teal, textStroke: [ - '#eab308', - '#ef4444', - '#3b82f6', - '#06b6d4', - '#8b5cf6', + EXCEPTION_ACCENT.strokeYellow, + EXCEPTION_ACCENT.error, + EXCEPTION_ACCENT.primary, + EXCEPTION_ACCENT.info, + EXCEPTION_ACCENT.secondary, ], inspectorOverlay: { - activeBorder: '#3b82f6', - activeBackground: 'rgba(59, 130, 246, 0.12)', + activeBorder: EXCEPTION_ACCENT.primary, + activeBackground: 'rgba(59, 130, 246, 0.15)', activeBorderSubtle: 'rgba(59, 130, 246, 0.4)', - selectedBorder: '#22c55e', + selectedBorder: EXCEPTION_ACCENT.success, selectedBackground: 'rgba(34, 197, 94, 0.18)', browserTooltipBackground: 'rgba(10, 10, 10, 0.92)', mainTooltipBackground: 'rgba(15, 23, 42, 0.95)', - tooltipText: '#e2e8f0', + tooltipText: EXCEPTION_ACCENT.textOnDark, tooltipShadow: 'rgba(0, 0, 0, 0.5)', - staticWhite: '#ffffff', + staticWhite: EXCEPTION_ACCENT.staticWhite, }, } as const; diff --git a/src/web-ui/src/shared/types/chat.ts b/src/web-ui/src/shared/types/chat.ts index 5b3331ba97..2aeb904875 100644 --- a/src/web-ui/src/shared/types/chat.ts +++ b/src/web-ui/src/shared/types/chat.ts @@ -47,6 +47,8 @@ export interface ModelConfig { isBuiltIn?: boolean; contextWindow?: number; maxTokens?: number; + category?: 'general_chat' | 'multimodal'; + capabilities?: Array<'text_chat' | 'function_calling' | 'image_understanding'>; } diff --git a/src/web-ui/src/shared/utils/aiConnectionTestMessages.ts b/src/web-ui/src/shared/utils/aiConnectionTestMessages.ts index 5bc243dc30..ea1fc652b9 100644 --- a/src/web-ui/src/shared/utils/aiConnectionTestMessages.ts +++ b/src/web-ui/src/shared/utils/aiConnectionTestMessages.ts @@ -2,11 +2,17 @@ type TranslateFn = (key: string) => string; export type ConnectionTestMessageCode = | 'tool_calls_not_detected' - | 'image_input_check_failed'; + | 'image_input_check_failed' + | 'tls_or_certificate_issue' + | 'proxy_issue' + | 'network_issue'; const MESSAGE_KEY_BY_CODE: Record = { tool_calls_not_detected: 'messages.connectionTestMessages.toolCallsNotDetected', image_input_check_failed: 'messages.connectionTestMessages.imageInputCheckFailed', + tls_or_certificate_issue: 'messages.connectionTestMessages.tlsOrCertificateIssue', + proxy_issue: 'messages.connectionTestMessages.proxyIssue', + network_issue: 'messages.connectionTestMessages.networkIssue', }; export function translateConnectionTestMessage( diff --git a/src/web-ui/src/shared/utils/version.ts b/src/web-ui/src/shared/utils/version.ts index 31b05ddfe8..baf3af4b2a 100644 --- a/src/web-ui/src/shared/utils/version.ts +++ b/src/web-ui/src/shared/utils/version.ts @@ -6,7 +6,7 @@ import { i18nService } from '@/infrastructure/i18n'; const DEFAULT_VERSION_INFO: VersionInfo = { name: 'BitFun', - version: '0.2.10', + version: '0.2.12', buildDate: new Date().toISOString(), buildTimestamp: Date.now(), isDev: import.meta.env.DEV, diff --git a/src/web-ui/src/tools/editor/components/CodeEditor.scss b/src/web-ui/src/tools/editor/components/CodeEditor.scss index 240e8c2d5a..dd36ef8d58 100644 --- a/src/web-ui/src/tools/editor/components/CodeEditor.scss +++ b/src/web-ui/src/tools/editor/components/CodeEditor.scss @@ -44,7 +44,7 @@ $_editor-padding-desktop: 32px; &__loading-spinner { width: $_editor-loading-spinner-size; height: $_editor-loading-spinner-size; - border: $_editor-loading-spinner-border solid $color-accent-200; + border: $_editor-loading-spinner-border solid var(--color-accent-200); border-top-color: var(--color-accent-500); border-radius: $size-radius-full; animation: code-editor-spin $motion-lazy $easing-standard infinite; diff --git a/src/web-ui/src/tools/editor/components/DiffEditor.scss b/src/web-ui/src/tools/editor/components/DiffEditor.scss index b365ca806f..77d85302bc 100644 --- a/src/web-ui/src/tools/editor/components/DiffEditor.scss +++ b/src/web-ui/src/tools/editor/components/DiffEditor.scss @@ -42,7 +42,7 @@ $_diff-error-max-width: 400px; align-items: center; padding: 0 $size-gap-3; background: var(--color-bg-primary); - border-bottom: 1px solid $border-base; + border-bottom: 1px solid var(--border-base); flex-shrink: 0; height: $_diff-toolbar-height; min-height: $_diff-toolbar-height; @@ -106,8 +106,8 @@ $_diff-error-max-width: 400px; min-width: 32px; height: 32px; padding: 0 $size-gap-3; - background: $element-bg-base; - border: 1px solid $border-base; + background: var(--element-bg-base); + border: 1px solid var(--border-base); border-radius: $size-radius-base; color: var(--color-text-secondary); font-size: var(--font-size-lg); @@ -116,7 +116,7 @@ $_diff-error-max-width: 400px; transition: all $motion-base $easing-standard; &:hover:not(:disabled) { - background: $element-bg-medium; + background: var(--element-bg-medium); border-color: var(--color-accent-500); color: var(--color-accent-500); transform: translateY(-1px); @@ -137,7 +137,7 @@ $_diff-error-max-width: 400px; } &:focus-visible { - outline: 2px solid $color-accent-500; + outline: 2px solid var(--color-accent-500); outline-offset: 2px; } } @@ -202,7 +202,7 @@ $_diff-error-max-width: 400px; gap: $size-gap-3; &__icon { - color: var(--color-text-tertiary); + color: var(--color-text-muted); opacity: 0.6; } @@ -215,7 +215,7 @@ $_diff-error-max-width: 400px; } &__path { - color: var(--color-text-tertiary); + color: var(--color-text-muted); font-size: var(--font-size-xs); font-family: $font-family-mono; margin: 0; @@ -356,13 +356,13 @@ $_diff-error-max-width: 400px; } .monaco-diff-editor .diffViewport { - background-color: var(--color-overlay-white-03) !important; + background-color: var(--color-overlay-white-04) !important; border-left: 1px dashed var(--border-medium) !important; border-right: 1px dashed var(--border-medium) !important; transition: all 0.2s ease; &:hover { - background-color: var(--color-overlay-white-05) !important; + background-color: var(--color-overlay-white-08) !important; border-left-style: solid !important; border-right-style: solid !important; } @@ -374,8 +374,8 @@ $_diff-error-max-width: 400px; 45deg, transparent, transparent 4px, - var(--color-overlay-white-02) 4px, - var(--color-overlay-white-02) 8px + var(--color-overlay-white-04) 4px, + var(--color-overlay-white-04) 8px ) !important; } @@ -529,7 +529,7 @@ $_diff-error-max-width: 400px; // High Contrast Mode @media (prefers-contrast: high) { .diff-editor-container { - border: 2px solid $border-base; + border: 2px solid var(--border-base); } .diff-editor-toolbar { diff --git a/src/web-ui/src/tools/editor/components/EditorBreadcrumb.scss b/src/web-ui/src/tools/editor/components/EditorBreadcrumb.scss index 15c1d1fd5f..333e385257 100644 --- a/src/web-ui/src/tools/editor/components/EditorBreadcrumb.scss +++ b/src/web-ui/src/tools/editor/components/EditorBreadcrumb.scss @@ -19,7 +19,7 @@ $_dropdown-width: 220px; height: $_breadcrumb-height; padding: 0 $size-gap-3; background: var(--color-bg-scene); - border-bottom: 1px solid var(--color-border-subtle); + border-bottom: 1px solid var(--border-subtle); overflow: hidden; flex-shrink: 0; gap: 2px; @@ -88,7 +88,7 @@ $_dropdown-width: 220px; width: $_dropdown-width; max-height: $_dropdown-max-height; background: var(--color-bg-elevated); - border: 1px solid var(--color-border-subtle); + border: 1px solid var(--border-subtle); border-radius: 6px; box-shadow: var(--shadow-base); z-index: 10000; @@ -100,7 +100,7 @@ $_dropdown-width: 220px; align-items: center; gap: $size-gap-2; padding: 6px 8px; - border-bottom: 1px solid var(--color-border-subtle); + border-bottom: 1px solid var(--border-subtle); background: var(--color-bg-tertiary); } diff --git a/src/web-ui/src/tools/editor/components/EditorStatusBar.scss b/src/web-ui/src/tools/editor/components/EditorStatusBar.scss index 4ad41125e0..a761bfaaf5 100644 --- a/src/web-ui/src/tools/editor/components/EditorStatusBar.scss +++ b/src/web-ui/src/tools/editor/components/EditorStatusBar.scss @@ -20,7 +20,7 @@ $_section-gap: 12px; height: $_statusbar-height; padding: $_statusbar-padding; background: var(--color-bg-scene); - border-top: 1px solid var(--color-border-subtle); + border-top: 1px solid var(--border-subtle); font-size: $_statusbar-font-size; font-family: $font-family-mono; color: var(--color-text-muted); @@ -63,7 +63,7 @@ $_section-gap: 12px; &__separator { width: 1px; height: 12px; - background: var(--color-border-subtle); + background: var(--border-subtle); flex-shrink: 0; } diff --git a/src/web-ui/src/tools/editor/components/ImageViewer.scss b/src/web-ui/src/tools/editor/components/ImageViewer.scss index 12e6924b38..72c3f9909b 100644 --- a/src/web-ui/src/tools/editor/components/ImageViewer.scss +++ b/src/web-ui/src/tools/editor/components/ImageViewer.scss @@ -31,7 +31,7 @@ gap: $size-gap-2; padding: $size-gap-2 $size-gap-3; background: var(--color-bg-primary); - border-bottom: 1px solid $border-base; + border-bottom: 1px solid var(--border-base); min-height: 32px; flex-shrink: 0; container-type: inline-size; @@ -131,7 +131,7 @@ &__divider { width: 1px; height: 20px; - background: $border-base; + background: var(--border-base); margin: 0 $size-gap-1; } diff --git a/src/web-ui/src/tools/editor/components/MarkdownEditor.scss b/src/web-ui/src/tools/editor/components/MarkdownEditor.scss index c658033c5f..c763fe571b 100644 --- a/src/web-ui/src/tools/editor/components/MarkdownEditor.scss +++ b/src/web-ui/src/tools/editor/components/MarkdownEditor.scss @@ -5,7 +5,7 @@ flex-direction: column; width: 100%; height: 100%; - background: var(--color-bg-flowchat); + background: var(--color-bg-scene); color: var(--color-text-primary); overflow: hidden; } @@ -16,7 +16,7 @@ gap: $size-gap-3; padding: $size-gap-3 $size-gap-4; border-bottom: 1px solid color-mix(in srgb, var(--color-warning) 22%, transparent); - background: color-mix(in srgb, var(--color-warning) 8%, var(--color-bg-flowchat)); + background: color-mix(in srgb, var(--color-warning) 8%, var(--color-bg-scene)); color: var(--color-text-secondary); flex-shrink: 0; } @@ -49,7 +49,7 @@ min-height: 34px; padding: 4px $size-gap-3; border-bottom: 1px solid var(--border-subtle); - background: color-mix(in srgb, var(--color-bg-flowchat) 92%, var(--color-bg-elevated)); + background: color-mix(in srgb, var(--color-bg-scene) 92%, var(--color-bg-elevated)); flex-shrink: 0; } @@ -93,7 +93,7 @@ justify-content: center; width: 100%; height: 100%; - background: var(--color-bg-flowchat); + background: var(--color-bg-scene); color: var(--color-text-primary); } @@ -103,7 +103,7 @@ justify-content: center; width: 100%; height: 100%; - background: var(--color-bg-flowchat); + background: var(--color-bg-scene); color: var(--color-text-primary); .error-content { @@ -113,9 +113,9 @@ gap: $size-gap-4; padding: $size-gap-8; text-align: center; - background: $element-bg-base; + background: var(--element-bg-base); border-radius: $size-radius-lg; - border: 1px solid $border-base; + border: 1px solid var(--border-base); backdrop-filter: blur(8px); .error-icon { @@ -132,9 +132,9 @@ .retry-button { padding: $size-gap-2 $size-gap-4; - background: $button-bg-default; + background: var(--element-bg-subtle); color: var(--color-text-secondary); - border: 1px solid $button-border-default; + border: 1px solid var(--border-subtle); border-radius: $size-radius-base; cursor: pointer; font-size: var(--font-size-sm); @@ -144,7 +144,7 @@ &:hover { background: $button-bg-hover; - border-color: $button-border-hover; + border-color: var(--border-accent); color: var(--color-text-primary); transform: translateY(-1px); box-shadow: $shadow-sm; diff --git a/src/web-ui/src/tools/editor/components/PlanViewer.scss b/src/web-ui/src/tools/editor/components/PlanViewer.scss index 5c74f28617..14dde6da07 100644 --- a/src/web-ui/src/tools/editor/components/PlanViewer.scss +++ b/src/web-ui/src/tools/editor/components/PlanViewer.scss @@ -51,7 +51,7 @@ transition: all 0.15s ease; &:hover { - background: var(--color-bg-hover); + background: var(--element-bg-hover); border-color: var(--border-medium); } } @@ -95,7 +95,7 @@ height: 18px; color: var(--color-text-muted); border-radius: 4px; - background: var(--color-overlay-white-03); + background: var(--color-overlay-white-04); transition: all 0.15s ease; svg { @@ -120,7 +120,7 @@ font-size: 14px; font-weight: 500; color: var(--color-text-primary); - font-family: var(--font-mono); + font-family: var(--font-family-mono); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; @@ -201,7 +201,7 @@ border-radius: 4px; cursor: pointer; transition: all 0.15s ease; - box-shadow: 0 1px 2px var(--color-overlay-black-10); + box-shadow: 0 1px 2px var(--color-overlay-black-12); &:hover:not(:disabled) { background: linear-gradient(135deg, var(--color-warning) 0%, var(--color-warning) 100%); @@ -233,7 +233,7 @@ &:hover { background: linear-gradient(135deg, var(--color-success) 0%, var(--color-success) 100%); - box-shadow: 0 1px 2px var(--color-overlay-black-10); + box-shadow: 0 1px 2px var(--color-overlay-black-12); } } } diff --git a/src/web-ui/src/tools/editor/components/ReadOnlyCodeBlock/ReadOnlyCodeBlock.scss b/src/web-ui/src/tools/editor/components/ReadOnlyCodeBlock/ReadOnlyCodeBlock.scss index 73243ebd1e..d90d504fe1 100644 --- a/src/web-ui/src/tools/editor/components/ReadOnlyCodeBlock/ReadOnlyCodeBlock.scss +++ b/src/web-ui/src/tools/editor/components/ReadOnlyCodeBlock/ReadOnlyCodeBlock.scss @@ -71,7 +71,7 @@ // Bordered mode .readonly-code-block--bordered { - border: 1px solid var(--color-border); + border: 1px solid var(--border-base); } // Inline mode (for inline code) diff --git a/src/web-ui/src/tools/editor/components/StatusBarPopovers/StatusBarPopovers.scss b/src/web-ui/src/tools/editor/components/StatusBarPopovers/StatusBarPopovers.scss index 5aa2adc718..104fd1fa94 100644 --- a/src/web-ui/src/tools/editor/components/StatusBarPopovers/StatusBarPopovers.scss +++ b/src/web-ui/src/tools/editor/components/StatusBarPopovers/StatusBarPopovers.scss @@ -16,7 +16,7 @@ min-width: 200px; max-width: 320px; background: var(--color-bg-elevated); - border: 1px solid var(--color-border-subtle); + border: 1px solid var(--border-subtle); border-radius: $popover-radius; box-shadow: $popover-shadow; font-size: 12px; @@ -74,7 +74,7 @@ &:focus-visible { outline: none; - background: var(--color-overlay-white-10); + background: var(--color-overlay-white-12); } &--active { @@ -103,7 +103,7 @@ padding: 6px 12px; font-size: 11px; color: var(--color-text-muted); - border-bottom: 1px solid var(--color-border-subtle); + border-bottom: 1px solid var(--border-subtle); } } diff --git a/src/web-ui/src/tools/editor/meditor/components/MEditor.scss b/src/web-ui/src/tools/editor/meditor/components/MEditor.scss index cb72d9857f..ab6a814df6 100644 --- a/src/web-ui/src/tools/editor/meditor/components/MEditor.scss +++ b/src/web-ui/src/tools/editor/meditor/components/MEditor.scss @@ -8,8 +8,8 @@ &-toolbar { padding: $size-gap-2; - border-bottom: 1px solid $border-base; - background: $element-bg-soft; + border-bottom: 1px solid var(--border-base); + background: var(--element-bg-soft); backdrop-filter: blur(8px); } @@ -31,13 +31,13 @@ &-mode-split { .m-editor-content { > .m-editor-edit-panel { - border-right: 1px solid $border-base; + border-right: 1px solid var(--border-base); } } } &-dark { - background: var(--color-bg-flowchat); + background: var(--color-bg-scene); color: var(--color-text-primary); .m-editor-textarea { @@ -45,17 +45,17 @@ } .m-editor-toolbar { - background: $element-bg-soft; - border-bottom-color: $border-base; + background: var(--element-bg-soft); + border-bottom-color: var(--border-base); } .m-editor-edit-panel { - border-right-color: $border-base !important; + border-right-color: var(--border-base) !important; } } &-light { - background: var(--color-bg-flowchat); + background: var(--color-bg-scene); color: var(--color-text-primary); border: 1px solid var(--border-base); diff --git a/src/web-ui/src/tools/editor/meditor/components/TiptapEditor.scss b/src/web-ui/src/tools/editor/meditor/components/TiptapEditor.scss index e0a8e058c4..00a165a810 100644 --- a/src/web-ui/src/tools/editor/meditor/components/TiptapEditor.scss +++ b/src/web-ui/src/tools/editor/meditor/components/TiptapEditor.scss @@ -1,9 +1,6 @@ @use '../../../../component-library/styles/tokens.scss' as *; .m-editor-tiptap { - --m-editor-highlight-rgb: 255, 213, 79; - --m-editor-highlight-border-rgb: 255, 193, 7; - position: relative; width: 100%; height: 100%; @@ -38,7 +35,7 @@ } &.m-editor-tiptap-block-highlighted { - background: rgba(var(--m-editor-highlight-rgb), 0.15); + background: rgba(var(--markdown-editor-highlight-rgb), 0.15); } } @@ -213,7 +210,7 @@ } &[data-block-id].m-editor-tiptap-block-highlighted { - border-top-color: rgba(var(--m-editor-highlight-border-rgb), 0.55); + border-top-color: rgba(var(--markdown-editor-highlight-border-rgb), 0.55); } } @@ -467,7 +464,7 @@ .m-editor-inline-ai__panel { width: min(26rem, calc(100vw - #{$size-gap-8})); padding: 10px 10px 8px; - border: 1px solid var(--color-overlay-white-10); + border: 1px solid var(--color-overlay-white-12); border-radius: 12px; background: var(--color-bg-tertiary); backdrop-filter: blur(16px) saturate(1.2); @@ -475,7 +472,7 @@ box-shadow: 0 8px 24px color-mix(in srgb, var(--color-static-black) 35%, transparent), inset 0 1px 0 var(--color-overlay-white-08), - inset 0 -1px 0 var(--color-overlay-white-02); + inset 0 -1px 0 var(--color-overlay-white-04); transition: transform 0.28s cubic-bezier(0.4, 0, 0.2, 1), box-shadow 0.28s cubic-bezier(0.4, 0, 0.2, 1); @@ -487,7 +484,7 @@ 0 16px 40px color-mix(in srgb, var(--color-static-black) 45%, transparent), 0 0 28px var(--color-accent-100), inset 0 1px 0 var(--color-overlay-white-12), - inset 0 -1px 0 var(--color-overlay-white-03); + inset 0 -1px 0 var(--color-overlay-white-04); } :root[data-theme='light'] &, @@ -532,7 +529,7 @@ .light & { .m-editor-inline-ai__composer-input .bitfun-input-container { background: var(--color-overlay-slate-04); - border-color: var(--color-overlay-slate-10); + border-color: var(--color-overlay-slate-12); } } @@ -747,7 +744,7 @@ bottom: 6px; width: 2px; border-radius: 999px; - background: $color-accent-400; + background: var(--color-accent-400); } .m-editor-inline-ai-preview--streaming, @@ -761,11 +758,11 @@ .m-editor-inline-ai-preview--error { .m-editor-inline-ai-preview__title, .m-editor-inline-ai-preview__body { - color: $color-error; + color: var(--color-error); } .m-editor-inline-ai-preview__body::before { - background: $color-error-border; + background: var(--color-error-border); } } @@ -1008,7 +1005,7 @@ resize: vertical; border: 1px solid color-mix(in srgb, var(--color-text-primary) 12%, transparent); border-radius: $size-radius-base; - background: color-mix(in srgb, var(--color-bg-flowchat) 94%, black); + background: color-mix(in srgb, var(--color-bg-scene) 94%, black); color: var(--color-text-primary); padding: $size-gap-3; line-height: 1.55; @@ -1068,7 +1065,7 @@ word-break: break-word; border: 1px solid color-mix(in srgb, var(--color-text-primary) 12%, transparent); border-radius: $size-radius-base; - background: color-mix(in srgb, var(--color-bg-flowchat) 94%, black); + background: color-mix(in srgb, var(--color-bg-scene) 94%, black); color: var(--color-text-primary); padding: $size-gap-3; line-height: 1.55; @@ -1126,7 +1123,7 @@ padding: $size-gap-2 $size-gap-3; border: 1px solid color-mix(in srgb, var(--color-text-primary) 10%, transparent); border-radius: $size-radius-base; - background: color-mix(in srgb, var(--color-bg-flowchat) 88%, white); + background: color-mix(in srgb, var(--color-bg-scene) 88%, white); } [data-type='details'] > button { @@ -1184,7 +1181,7 @@ margin: $size-gap-3 0; border: 1px solid color-mix(in srgb, var(--color-text-primary) 10%, transparent); border-radius: $size-radius-base; - background: color-mix(in srgb, var(--color-bg-flowchat) 88%, white); + background: color-mix(in srgb, var(--color-bg-scene) 88%, white); overflow: hidden; } @@ -1221,7 +1218,7 @@ } .m-editor-details-block__summary::placeholder { - color: var(--color-text-tertiary); + color: var(--color-text-muted); } .m-editor-details-block__body { diff --git a/src/web-ui/src/tools/editor/themes/bitfun-dark.theme.test.ts b/src/web-ui/src/tools/editor/themes/bitfun-dark.theme.test.ts index f9268c390f..5f2c1c7d5d 100644 --- a/src/web-ui/src/tools/editor/themes/bitfun-dark.theme.test.ts +++ b/src/web-ui/src/tools/editor/themes/bitfun-dark.theme.test.ts @@ -10,8 +10,8 @@ describe('BitFunDarkTheme color roles', () => { expect(colors['editor.lineHighlightBorder']).toBe('#202024'); expect(colors['editorWidget.background']).toBe('#18181a'); expect(colors['editorHoverWidget.statusBarBackground']).toBe('#202024'); - expect(colors['diffEditor.unchangedRegionBackground']).toBe('#0D0D0F'); - expect(colors['diffEditor.unchangedCodeBackground']).toBe('#0D0D0F'); + expect(colors['diffEditor.unchangedRegionBackground']).toBe('#121214'); + expect(colors['diffEditor.unchangedCodeBackground']).toBe('#121214'); }); it('keeps BitFun accent roles output-equivalent', () => { @@ -29,4 +29,15 @@ describe('BitFunDarkTheme color roles', () => { expect(colors['editorWarning.foreground']).toBe(colors['minimap.warningHighlight']); expect(colors['editorLink.activeForeground']).toBe('#7DCFFF'); }); + + it('keeps diff text, line, and gutter strengths visually ordered', () => { + expect(colors['diffEditor.insertedTextBackground']).toBe('#23863625'); + expect(colors['diffEditor.insertedLineBackground']).toBe('#23863630'); + expect(colors['diffEditorGutter.insertedLineBackground']).toBe('#23863638'); + expect(colors['diffEditor.removedTextBackground']).toBe('#DA363325'); + expect(colors['diffEditor.removedLineBackground']).toBe('#DA363330'); + expect(colors['diffEditorGutter.removedLineBackground']).toBe('#DA363338'); + expect(colors['diffEditor.modifiedTextBackground']).toBe('#1F6FEB20'); + expect(colors['diffEditor.modifiedLineBackground']).toBe('#1F6FEB28'); + }); }); diff --git a/src/web-ui/src/tools/editor/themes/bitfun-dark.theme.ts b/src/web-ui/src/tools/editor/themes/bitfun-dark.theme.ts index 44cfbf47fc..ec268ca2f7 100644 --- a/src/web-ui/src/tools/editor/themes/bitfun-dark.theme.ts +++ b/src/web-ui/src/tools/editor/themes/bitfun-dark.theme.ts @@ -19,7 +19,7 @@ const MONACO_DARK_SURFACE = { background: '#121214', elevated: '#18181a', borderSubtle: '#202024', - diffDeep: '#0D0D0F', + diffDeep: '#121214', } as const; const MONACO_EDITOR_TEXT = { @@ -48,6 +48,17 @@ const MONACO_STATUS_COLOR = { link: '#7DCFFF', } as const; +const MONACO_DIFF_COLOR = { + insertedTextBackground: '#23863625', + insertedLineBackground: '#23863630', + insertedGutterBackground: '#23863638', + removedTextBackground: '#DA363325', + removedLineBackground: '#DA363330', + removedGutterBackground: '#DA363338', + modifiedTextBackground: '#1F6FEB20', + modifiedLineBackground: '#1F6FEB28', +} as const; + /** * BitFun Dark Theme Configuration * Follows Monaco Editor official theme format @@ -392,21 +403,21 @@ export const BitFunDarkTheme: editor.IStandaloneThemeData = { 'editorOverviewRuler.infoForeground': MONACO_BRAND_ACCENT.base, // Diff Editor (GitHub Dark style) - 'diffEditor.insertedTextBackground': '#23863625', - 'diffEditor.insertedLineBackground': '#23863630', + 'diffEditor.insertedTextBackground': MONACO_DIFF_COLOR.insertedTextBackground, + 'diffEditor.insertedLineBackground': MONACO_DIFF_COLOR.insertedLineBackground, 'diffEditor.insertedTextBorder': TRANSPARENT_MONACO_BORDER, - 'diffEditorGutter.insertedLineBackground': '#23863638', + 'diffEditorGutter.insertedLineBackground': MONACO_DIFF_COLOR.insertedGutterBackground, - 'diffEditor.removedTextBackground': '#DA363325', - 'diffEditor.removedLineBackground': '#DA363330', + 'diffEditor.removedTextBackground': MONACO_DIFF_COLOR.removedTextBackground, + 'diffEditor.removedLineBackground': MONACO_DIFF_COLOR.removedLineBackground, 'diffEditor.removedTextBorder': TRANSPARENT_MONACO_BORDER, - 'diffEditorGutter.removedLineBackground': '#DA363338', + 'diffEditorGutter.removedLineBackground': MONACO_DIFF_COLOR.removedGutterBackground, - 'diffEditor.modifiedTextBackground': '#1F6FEB20', - 'diffEditor.modifiedLineBackground': '#1F6FEB28', + 'diffEditor.modifiedTextBackground': MONACO_DIFF_COLOR.modifiedTextBackground, + 'diffEditor.modifiedLineBackground': MONACO_DIFF_COLOR.modifiedLineBackground, 'diffEditor.border': '#2A2D35', - 'diffEditor.diagonalFill': '#16181D', + 'diffEditor.diagonalFill': MONACO_DARK_SURFACE.elevated, 'diffEditor.unchangedRegionBackground': MONACO_DARK_SURFACE.diffDeep, 'diffEditor.unchangedCodeBackground': MONACO_DARK_SURFACE.diffDeep, diff --git a/src/web-ui/src/tools/file-system/components/FileSearchResults.scss b/src/web-ui/src/tools/file-system/components/FileSearchResults.scss index 2e7444e060..9958ce4efc 100644 --- a/src/web-ui/src/tools/file-system/components/FileSearchResults.scss +++ b/src/web-ui/src/tools/file-system/components/FileSearchResults.scss @@ -39,7 +39,7 @@ $_icon-size: 16px; .bitfun-search-results__header { padding: $size-gap-2 $size-gap-3; - border-bottom: 1px solid $border-base; + border-bottom: 1px solid var(--border-base); background: var(--color-bg-primary); flex-shrink: 0; } @@ -111,8 +111,8 @@ $_icon-size: 16px; color: var(--color-text-muted); &--directory { - background: $color-warning-bg; - color: $color-warning; + background: var(--color-warning-bg); + color: var(--color-warning); } } @@ -153,7 +153,7 @@ $_icon-size: 16px; appearance: none; &:hover { - background: $git-color-branch-bg-hover; + background: var(--git-color-branch-bg-hover); } &:active { @@ -230,7 +230,7 @@ $_icon-size: 16px; } .bitfun-search-results__highlight { - background: rgba($color-accent-500, 0.25); + background: color-mix(in srgb, var(--color-accent-500) 25%, transparent); color: var(--color-accent-500); padding: 1px 2px; border-radius: 2px; @@ -247,7 +247,7 @@ $_icon-size: 16px; @include mixins.flex-layout(row, center, center, $size-gap-1); padding: $size-gap-1 $size-gap-2; flex-shrink: 0; - border-top: 1px solid $border-base; + border-top: 1px solid var(--border-base); background: var(--element-bg-subtle); color: var(--color-text-muted); font-size: var(--font-size-xs); diff --git a/src/web-ui/src/tools/file-system/styles/FileExplorer.scss b/src/web-ui/src/tools/file-system/styles/FileExplorer.scss index 23370facc2..00d33a476c 100644 --- a/src/web-ui/src/tools/file-system/styles/FileExplorer.scss +++ b/src/web-ui/src/tools/file-system/styles/FileExplorer.scss @@ -163,17 +163,17 @@ $_indent-width: 8px; } &--selected { - background: $git-color-branch-bg; - color: $git-color-branch; + background: var(--git-color-branch-bg); + color: var(--git-color-branch); .bitfun-file-explorer__node-name { - color: $git-color-branch; + color: var(--git-color-branch); font-weight: $font-weight-medium; } &:hover { - background: $git-color-branch-bg-hover; - color: $git-color-branch; + background: var(--git-color-branch-bg-hover); + color: var(--git-color-branch); } } @@ -186,11 +186,11 @@ $_indent-width: 8px; } &--highlighted { - background: $git-color-branch-bg !important; + background: var(--git-color-branch-bg) !important; animation: file-node-highlight-fade 2s ease-out forwards; .bitfun-file-explorer__node-name { - color: $git-color-branch; + color: var(--git-color-branch); font-weight: $font-weight-medium; } } @@ -198,10 +198,10 @@ $_indent-width: 8px; @keyframes file-node-highlight-fade { 0% { - background: rgba(var(--color-primary-rgb) / 0.2); + background: rgba(var(--color-accent-500-rgb) / 0.2); } 30% { - background: $git-color-branch-bg; + background: var(--git-color-branch-bg); } 100% { background: transparent; @@ -224,7 +224,7 @@ $_indent-width: 8px; min-width: $_icon-size; &--folder { - color: $git-color-branch; + color: var(--git-color-branch); } &--folder-open { diff --git a/src/web-ui/src/tools/generative-widget/GenerativeWidgetFrame.scss b/src/web-ui/src/tools/generative-widget/GenerativeWidgetFrame.scss index be1e3be5b3..41bad51524 100644 --- a/src/web-ui/src/tools/generative-widget/GenerativeWidgetFrame.scss +++ b/src/web-ui/src/tools/generative-widget/GenerativeWidgetFrame.scss @@ -4,7 +4,7 @@ overflow-x: auto; overflow-y: hidden; border-radius: 10px; - background: var(--color-overlay-white-02); + background: var(--color-overlay-white-04); border: 1px solid var(--color-overlay-white-08); } diff --git a/src/web-ui/src/tools/generative-widget/GenerativeWidgetFrame.tsx b/src/web-ui/src/tools/generative-widget/GenerativeWidgetFrame.tsx index 2cb205c261..8ea52274e3 100644 --- a/src/web-ui/src/tools/generative-widget/GenerativeWidgetFrame.tsx +++ b/src/web-ui/src/tools/generative-widget/GenerativeWidgetFrame.tsx @@ -3,9 +3,11 @@ import morphdomRuntime from 'morphdom/dist/morphdom-umd.js?raw'; import { themeService } from '@/infrastructure/theme'; import { createWidgetThemeFallbackCss, + createWidgetThemeStaticShellCss, readWidgetThemePayload, type WidgetThemePayload, } from './themePayload'; +import { createWidgetThemeCompatibilityAliasCss } from './themePayloadCompatibility'; import './GenerativeWidgetFrame.scss'; export type WidgetMessage = @@ -92,8 +94,10 @@ export const GENERATIVE_WIDGET_SHELL_HTML = ` * { box-sizing: border-box; } :root { ${createWidgetThemeFallbackCss()} - --font-sans: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; - --font-mono: "SF Mono", Consolas, monospace; + --font-family-sans: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; + --font-family-mono: "SF Mono", Consolas, monospace; +${createWidgetThemeStaticShellCss()} +${createWidgetThemeCompatibilityAliasCss()} --font-size-xs: 12px; --font-size-sm: 14px; --font-size-base: 14px; @@ -101,12 +105,6 @@ ${createWidgetThemeFallbackCss()} --font-size-2xl: 18px; --font-weight-medium: 500; --font-weight-semibold: 600; - --spacing-3: 12px; - --spacing-4: 16px; - --spacing-5: 20px; - --radius-sm: 6px; - --radius-base: 8px; - --radius-lg: 12px; --motion-fast: 0.15s; --easing-standard: ease; } @@ -117,7 +115,7 @@ ${createWidgetThemeFallbackCss()} min-height: 0; background: transparent; color: var(--color-text-primary); - font-family: var(--font-sans); + font-family: var(--font-family-sans); overflow-x: hidden; overflow-y: hidden; } @@ -149,7 +147,7 @@ ${createWidgetThemeFallbackCss()} line-height: 1.5; } body, button, input, textarea, select { - font-family: var(--font-sans); + font-family: var(--font-family-sans); } button, input, textarea, select { font: inherit; @@ -180,25 +178,25 @@ ${createWidgetThemeFallbackCss()} max-width: 100%; display: flex; flex-direction: column; - gap: var(--spacing-4); + gap: var(--size-gap-4); color: var(--color-text-primary); } .bf-stack { display: flex; flex-direction: column; - gap: var(--spacing-3); + gap: var(--size-gap-3); } .bf-row { display: flex; align-items: center; - gap: var(--spacing-3); + gap: var(--size-gap-3); min-width: 0; } .bf-row-wrap { display: flex; flex-wrap: wrap; align-items: center; - gap: var(--spacing-3); + gap: var(--size-gap-3); min-width: 0; } .bf-toolbar { @@ -206,9 +204,9 @@ ${createWidgetThemeFallbackCss()} flex-wrap: wrap; align-items: center; justify-content: space-between; - gap: var(--spacing-3); - padding: var(--spacing-3) var(--spacing-4); - border-radius: var(--radius-lg); + gap: var(--size-gap-3); + padding: var(--size-gap-3) var(--size-gap-4); + border-radius: var(--size-radius-lg); background: color-mix(in srgb, var(--color-bg-secondary) 82%, transparent); border: 1px solid var(--border-subtle); box-shadow: var(--shadow-xs); @@ -216,14 +214,14 @@ ${createWidgetThemeFallbackCss()} .bf-section { display: flex; flex-direction: column; - gap: var(--spacing-3); + gap: var(--size-gap-3); } .bf-section-header { display: flex; flex-wrap: wrap; align-items: flex-start; justify-content: space-between; - gap: var(--spacing-3); + gap: var(--size-gap-3); } .bf-title { margin: 0; @@ -252,10 +250,10 @@ ${createWidgetThemeFallbackCss()} position: relative; display: flex; flex-direction: column; - gap: var(--spacing-3); + gap: var(--size-gap-3); width: 100%; - padding: var(--spacing-4); - border-radius: var(--radius-lg); + padding: var(--size-gap-4); + border-radius: var(--size-radius-lg); background: var(--color-bg-secondary); border: 1px solid var(--border-subtle); box-shadow: var(--shadow-sm); @@ -272,7 +270,7 @@ ${createWidgetThemeFallbackCss()} box-shadow: 0 0 0 4px color-mix(in srgb, var(--color-accent-500) 18%, transparent), 0 10px 24px color-mix(in srgb, var(--color-accent-500) 14%, transparent); - border-radius: min(var(--radius-base), 12px); + border-radius: min(var(--size-radius-base), 12px); transition: outline-color 120ms ease, box-shadow 120ms ease, transform 120ms ease; transform: translateY(-1px); } @@ -283,7 +281,7 @@ ${createWidgetThemeFallbackCss()} .bf-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(min(180px, 100%), 1fr)); - gap: var(--spacing-3); + gap: var(--size-gap-3); width: 100%; min-width: 0; } @@ -292,8 +290,8 @@ ${createWidgetThemeFallbackCss()} flex-direction: column; gap: 6px; min-width: 0; - padding: var(--spacing-3); - border-radius: var(--radius-base); + padding: var(--size-gap-3); + border-radius: var(--size-radius-base); background: var(--element-bg-base); border: 1px solid var(--border-subtle); } @@ -358,7 +356,7 @@ ${createWidgetThemeFallbackCss()} max-width: 100%; padding: 0 12px; border: 1px solid var(--border-base); - border-radius: var(--radius-sm); + border-radius: var(--size-radius-sm); background: var(--element-bg-base); color: var(--color-text-secondary); text-decoration: none; @@ -388,7 +386,7 @@ ${createWidgetThemeFallbackCss()} max-width: 100%; min-width: 0; padding: 0 12px; - border-radius: var(--radius-sm); + border-radius: var(--size-radius-sm); border: 1px solid var(--border-base); background: var(--element-bg-subtle); color: var(--color-text-primary); @@ -425,9 +423,9 @@ ${createWidgetThemeFallbackCss()} display: flex; align-items: flex-start; justify-content: space-between; - gap: var(--spacing-3); - padding: var(--spacing-3); - border-radius: var(--radius-base); + gap: var(--size-gap-3); + padding: var(--size-gap-3); + border-radius: var(--size-radius-base); background: var(--element-bg-subtle); border: 1px solid transparent; } @@ -442,7 +440,7 @@ ${createWidgetThemeFallbackCss()} width: 100%; overflow-x: auto; border: 1px solid var(--border-subtle); - border-radius: var(--radius-base); + border-radius: var(--size-radius-base); background: var(--color-bg-secondary); } .bf-table { @@ -474,8 +472,8 @@ ${createWidgetThemeFallbackCss()} justify-content: center; gap: 8px; min-height: 140px; - padding: var(--spacing-5); - border-radius: var(--radius-lg); + padding: var(--size-gap-5); + border-radius: var(--size-radius-lg); border: 1px dashed var(--border-base); background: color-mix(in srgb, var(--element-bg-subtle) 80%, transparent); color: var(--color-text-muted); @@ -493,17 +491,17 @@ ${createWidgetThemeFallbackCss()} border-radius: 6px; background: var(--element-bg-base); color: var(--color-text-primary); - font-family: var(--font-mono); + font-family: var(--font-family-mono); font-size: 12px; } .bf-mono { - font-family: var(--font-mono); + font-family: var(--font-family-mono); } @media (max-width: 560px) { .bf-card, .bf-panel, .bf-toolbar { - padding: var(--spacing-3); + padding: var(--size-gap-3); } .bf-grid { grid-template-columns: 1fr; @@ -755,7 +753,10 @@ ${createWidgetThemeFallbackCss()} vars['--color-text-primary'] || getComputedStyle(root).getPropertyValue('--color-text-primary') || body.style.color; - body.style.fontFamily = vars['--font-sans'] || body.style.fontFamily; + body.style.fontFamily = + vars['--font-family-sans'] || + getComputedStyle(root).getPropertyValue('--font-family-sans') || + body.style.fontFamily; } } diff --git a/src/web-ui/src/tools/generative-widget/GenerativeWidgetPanel.scss b/src/web-ui/src/tools/generative-widget/GenerativeWidgetPanel.scss index 9c7fac3927..8d10a5c778 100644 --- a/src/web-ui/src/tools/generative-widget/GenerativeWidgetPanel.scss +++ b/src/web-ui/src/tools/generative-widget/GenerativeWidgetPanel.scss @@ -78,8 +78,8 @@ } .bitfun-generative-widget-panel__button--primary { - border-color: var(--color-primary); - background: color-mix(in srgb, var(--color-primary) 18%, transparent); + border-color: var(--color-accent-500); + background: color-mix(in srgb, var(--color-accent-500) 18%, transparent); } .bitfun-generative-widget-panel__workspace { @@ -128,7 +128,7 @@ padding: 14px 16px; background: transparent; color: var(--color-text-primary); - font: 13px/1.6 var(--font-mono); + font: 13px/1.6 var(--font-family-mono); } .bitfun-generative-widget-panel__error { diff --git a/src/web-ui/src/tools/generative-widget/themePayload.test.ts b/src/web-ui/src/tools/generative-widget/themePayload.test.ts index edf8ab9930..e890002322 100644 --- a/src/web-ui/src/tools/generative-widget/themePayload.test.ts +++ b/src/web-ui/src/tools/generative-widget/themePayload.test.ts @@ -2,12 +2,161 @@ import { createHash } from 'node:crypto'; import { afterEach, describe, expect, it, vi } from 'vitest'; import { + WIDGET_THEME_STATIC_SHELL_VAR_NAMES, WIDGET_THEME_FALLBACK_VARS, createWidgetThemeFallbackCss, + createWidgetThemeStaticShellCss, readWidgetThemePayload, } from './themePayload'; +import { createWidgetThemeCompatibilityAliasCss } from './themePayloadCompatibility'; -const WIDGET_THEME_VAR_NAMES_HASH = 'c33a807d44e85a0771a50bfc9277eb98ffbd717d9b750ff742aab06420cc46ed'; +const WIDGET_THEME_VAR_NAMES_HASH = '3c69b6767d5c5aa2752af7eab4c68596f1ab6014397dac02fa2b70343c771981'; +const WIDGET_THEME_STATIC_SHELL_VAR_NAMES_HASH = '7c53d90b3bc30c9d1c8494bb3865f42108e10e747841bae493ad26bcb30bf9b8'; +const RETIRED_WIDGET_THEME_COMPAT_KEYS = [ + '--background-primary', + '--background-secondary', + '--background-tertiary', + '--border-muted', + '--border-primary', + '--border-color', + '--border-hover', + '--bg-elevated', + '--bg-hover', + '--bg-primary', + '--bg-secondary', + '--bg-tertiary', + '--color-background-secondary', + '--color-background-tertiary', + '--color-bg-base', + '--color-bg-elevated-hover', + '--color-bg-flowchat', + '--color-bg-hover', + '--color-bg-subtle', + '--color-bg-surface', + '--color-border', + '--color-border-primary', + '--color-border-subtle', + '--color-hover', + '--color-semantic-error', + '--color-success-100', + '--color-success-500', + '--color-surface-elevated', + '--color-surface-hover', + '--color-text-tertiary', + '--color-warning-100', + '--color-warning-500', + '--color-warning-700', + '--color-overlay-white-02', + '--color-overlay-white-03', + '--color-overlay-white-05', + '--color-overlay-white-06', + '--color-overlay-white-10', + '--color-overlay-black-06', + '--color-overlay-black-10', + '--color-overlay-black-25', + '--color-accent', + '--color-accent-primary', + '--color-accent-alpha', + '--color-primary', + '--color-primary-rgb', + '--color-primary-400', + '--color-primary-hover', + '--color-primary-500', + '--color-primary-alpha', + '--color-primary-bg', + '--color-primary-bg-subtle', + '--accent-primary', + '--accent-primary-hover', + '--color-danger', + '--color-danger-500', + '--color-danger-text', + '--color-danger-bg', + '--color-danger-border', + '--color-danger-hover', + '--element-bg', + '--font-mono', + '--font-sans', + '--markdown-font-mono', + '--motion-normal', + '--radius-2xl', + '--radius-base', + '--radius-full', + '--radius-lg', + '--radius-md', + '--radius-sm', + '--radius-xl', + '--secondary-bg', + '--spacing-1', + '--spacing-10', + '--spacing-12', + '--spacing-16', + '--spacing-2', + '--spacing-3', + '--spacing-4', + '--spacing-5', + '--spacing-6', + '--spacing-8', + '--text-disabled', + '--text-muted', + '--text-primary', + '--text-secondary', + '--text-tertiary', + '--tool-card-bg-primary', + '--tool-card-bg-secondary', + '--tool-card-bg-hover', + '--tool-card-bg-elevated', + '--tool-card-border', + '--tool-card-border-subtle', + '--tool-card-text-primary', + '--tool-card-text-secondary', + '--tool-card-text-muted', + '--tool-compact-summary-font', +] as const; +const RETIRED_WIDGET_THEME_INTERNAL_KEYS = [ + '--btn-ghost-bg', + '--btn-ghost-border', + '--btn-ghost-shadow', + '--btn-ghost-hover-shadow', + '--btn-ghost-hover-transform', + '--btn-ghost-active-bg', + '--btn-ghost-active-color', + '--btn-ghost-active-border', + '--btn-ghost-active-shadow', + '--btn-ghost-active-transform', +] as const; +const DERIVED_WIDGET_THEME_PAYLOAD_KEYS = [ + '--color-accent-50', + '--color-accent-100', + '--color-accent-200', + '--color-accent-300', + '--color-accent-400', + '--color-accent-700', + '--color-accent-800', + '--color-success-border', + '--color-warning-border', + '--color-error-border', + '--border-strong', + '--border-prominent', + '--element-bg-strong', + '--size-radius-md', +] as const; +const STATIC_WIDGET_SHELL_THEME_VARS = new Set([ + ...WIDGET_THEME_STATIC_SHELL_VAR_NAMES, + '--font-family-mono', + '--font-family-sans', +]); +const LOCAL_ONLY_WIDGET_SHELL_KEYS = [ + '--color-bg-workbench', + '--color-overlay-white-08', + '--color-overlay-black-12', + '--glass-base', + '--tool-card-header-pad-y', + '--tool-card-action-line-height', +] as const; +const IFRAME_ROOT_BASE_KEYS = [ + '--font-family-sans', + '--font-family-mono', +] as const; function readPayloadWithHostValues(hostValues: Record = {}) { const requestedNames: string[] = []; @@ -45,6 +194,11 @@ function hashNames(names: string[]): string { .digest('hex'); } +function readCompatibilityAliasEntries(css: string): Array<[string, string]> { + return Array.from(css.matchAll(/^\s+(--[-\w]+): var\((--[-\w]+)\);$/gm)) + .map(([, name, canonical]) => [name, canonical]); +} + describe('generated widget theme payload contract', () => { afterEach(() => { vi.unstubAllGlobals(); @@ -60,17 +214,99 @@ describe('generated widget theme payload contract', () => { first: requestedNames[0], last: requestedNames[requestedNames.length - 1], }).toEqual({ - count: 323, + count: 80, hash: WIDGET_THEME_VAR_NAMES_HASH, first: '--color-bg-primary', - last: '--tool-card-action-font-weight', + last: '--btn-ghost-hover-border', }); }); - it('includes every static iframe fallback key in the host payload allowlist', () => { - const { payload } = readPayloadWithHostValues(); + it('uses reviewed iframe fallback values for requested host payload keys', () => { + const { payload, requestedNames } = readPayloadWithHostValues(); + const requestedFallbackVars = Object.fromEntries( + Object.entries(WIDGET_THEME_FALLBACK_VARS).filter(([name]) => requestedNames.includes(name)), + ); + + expect(payload?.vars).toEqual(requestedFallbackVars); + }); + + it('does not export retired low-risk compatibility keys', () => { + const { requestedNames } = readPayloadWithHostValues(); - expect(payload?.vars).toEqual(WIDGET_THEME_FALLBACK_VARS); + expect(requestedNames).not.toEqual(expect.arrayContaining(RETIRED_WIDGET_THEME_COMPAT_KEYS)); + expect(requestedNames).not.toEqual(expect.arrayContaining(RETIRED_WIDGET_THEME_INTERNAL_KEYS)); + expect(requestedNames).not.toEqual(expect.arrayContaining(DERIVED_WIDGET_THEME_PAYLOAD_KEYS)); + expect(requestedNames).toEqual( + expect.arrayContaining([ + '--color-accent-500', + '--color-accent-500-rgb', + '--color-accent-600', + '--color-error', + '--color-error-bg', + '--color-info', + '--color-info-bg', + '--color-info-border', + '--size-radius-xl', + '--size-gap-16', + '--btn-primary-bg', + '--btn-primary-hover-bg', + '--btn-primary-hover-transform', + '--btn-ghost-hover-bg', + ]) + ); + }); + + it('exports button component tokens from the host theme payload', () => { + const hostValues = { + '--btn-primary-bg': 'linear-gradient(test-primary)', + '--btn-primary-color': '#101010', + '--btn-primary-border': '1px solid #202020', + '--btn-primary-shadow': '0 1px 2px #303030', + '--btn-primary-hover-bg': 'linear-gradient(test-hover)', + '--btn-primary-hover-color': '#404040', + '--btn-primary-hover-border': '1px solid #505050', + '--btn-primary-hover-shadow': '0 2px 4px #606060', + '--btn-primary-hover-transform': 'translateY(-1px)', + '--btn-primary-active-bg': 'linear-gradient(test-active)', + '--btn-primary-active-color': '#707070', + '--btn-primary-active-border': '1px solid #808080', + '--btn-primary-active-shadow': 'none', + '--btn-primary-active-transform': 'none', + '--btn-ghost-color': '#909090', + '--btn-ghost-hover-bg': 'rgba(144, 144, 144, 0.1)', + '--btn-ghost-hover-color': '#a0a0a0', + '--btn-ghost-hover-border': '1px solid #b0b0b0', + }; + const { payload } = readPayloadWithHostValues(hostValues); + + expect(payload?.vars).toMatchObject(hostValues); + }); + + it('keeps retired payload keys available as iframe aliases', () => { + const { requestedNames } = readPayloadWithHostValues(); + const compatibilityAliasCss = createWidgetThemeCompatibilityAliasCss(); + const aliasEntries = readCompatibilityAliasEntries(compatibilityAliasCss); + const aliasNames = new Set(aliasEntries.map(([name]) => name)); + + expect(aliasEntries.map(([name]) => name).sort()).toEqual([...RETIRED_WIDGET_THEME_COMPAT_KEYS].sort()); + for (const [key, canonical] of aliasEntries) { + expect(RETIRED_WIDGET_THEME_COMPAT_KEYS).toContain(key); + expect(aliasNames.has(canonical)).toBe(false); + expect( + requestedNames.includes(canonical) + || canonical in WIDGET_THEME_FALLBACK_VARS + || STATIC_WIDGET_SHELL_THEME_VARS.has(canonical), + ).toBe(true); + } + }); + + it('keeps derived widget keys resolvable without host payload reads', () => { + const { requestedNames } = readPayloadWithHostValues(); + + for (const name of DERIVED_WIDGET_THEME_PAYLOAD_KEYS) { + expect(requestedNames).not.toContain(name); + expect(name in WIDGET_THEME_FALLBACK_VARS || STATIC_WIDGET_SHELL_THEME_VARS.has(name)).toBe(true); + } }); it('renders fallback CSS from the same reviewed fallback map', () => { @@ -80,4 +316,33 @@ describe('generated widget theme payload contract', () => { expect(css).toContain(` ${name}: ${value};`); } }); + + it('keeps host-internal shell vars available without exporting them from the host payload', () => { + const { requestedNames } = readPayloadWithHostValues(); + const css = createWidgetThemeStaticShellCss(); + const resolvableVars = new Set([ + ...Object.keys(WIDGET_THEME_FALLBACK_VARS), + ...requestedNames, + ...WIDGET_THEME_STATIC_SHELL_VAR_NAMES, + ...IFRAME_ROOT_BASE_KEYS, + ]); + + for (const name of WIDGET_THEME_STATIC_SHELL_VAR_NAMES) { + expect(css).toContain(` ${name}: `); + } + expect({ + count: WIDGET_THEME_STATIC_SHELL_VAR_NAMES.length, + hash: hashNames(WIDGET_THEME_STATIC_SHELL_VAR_NAMES), + first: WIDGET_THEME_STATIC_SHELL_VAR_NAMES[0], + last: WIDGET_THEME_STATIC_SHELL_VAR_NAMES[WIDGET_THEME_STATIC_SHELL_VAR_NAMES.length - 1], + }).toEqual({ + count: 83, + hash: WIDGET_THEME_STATIC_SHELL_VAR_NAMES_HASH, + first: '--color-bg-quaternary', + last: '--tool-card-action-font-weight', + }); + const referencedVars = [...css.matchAll(/var\((--[a-zA-Z0-9_-]+)/g)].map((match) => match[1]); + expect(referencedVars.filter((name) => !resolvableVars.has(name))).toEqual([]); + expect(requestedNames).not.toEqual(expect.arrayContaining(LOCAL_ONLY_WIDGET_SHELL_KEYS)); + }); }); diff --git a/src/web-ui/src/tools/generative-widget/themePayload.ts b/src/web-ui/src/tools/generative-widget/themePayload.ts index ed3bd747b7..177b0ed987 100644 --- a/src/web-ui/src/tools/generative-widget/themePayload.ts +++ b/src/web-ui/src/tools/generative-widget/themePayload.ts @@ -7,16 +7,33 @@ export type WidgetThemePayload = { }; const FALLBACK_VAR = { + bgPrimary: '--color-bg-primary', + bgElevated: '--color-bg-elevated', + bgTertiary: '--color-bg-tertiary', + bgScene: '--color-bg-scene', textPrimary: '--color-text-primary', textSecondary: '--color-text-secondary', textMuted: '--color-text-muted', + textDisabled: '--color-text-disabled', + accent50: '--color-accent-50', + accent100: '--color-accent-100', + accent400: '--color-accent-400', accent500: '--color-accent-500', + accent500Rgb: '--color-accent-500-rgb', accent600: '--color-accent-600', bgSecondary: '--color-bg-secondary', success: '--color-success', + successBg: '--color-success-bg', warning: '--color-warning', + warningBg: '--color-warning-bg', error: '--color-error', + errorBg: '--color-error-bg', + errorBorder: '--color-error-border', staticWhite: '--color-static-white', + staticBlack: '--color-static-black', + overlayWhite04: '--color-overlay-white-04', + overlayBlack08: '--color-overlay-black-08', + overlayBlack30: '--color-overlay-black-30', borderSubtle: '--border-subtle', borderBase: '--border-base', borderMedium: '--border-medium', @@ -24,25 +41,63 @@ const FALLBACK_VAR = { elementBgBase: '--element-bg-base', elementBgMedium: '--element-bg-medium', elementBgSoft: '--element-bg-soft', + elementBgElevated: '--element-bg-elevated', + elementBgHover: '--element-bg-hover', + motionBase: '--motion-base', shadowXs: '--shadow-xs', shadowSm: '--shadow-sm', + sizeRadiusSm: '--size-radius-sm', + sizeRadiusBase: '--size-radius-base', + sizeRadiusMd: '--size-radius-md', + sizeRadiusLg: '--size-radius-lg', + sizeRadiusXl: '--size-radius-xl', + sizeRadius2xl: '--size-radius-2xl', + sizeRadiusFull: '--size-radius-full', + sizeGap1: '--size-gap-1', + sizeGap2: '--size-gap-2', + sizeGap3: '--size-gap-3', + sizeGap4: '--size-gap-4', + sizeGap5: '--size-gap-5', + sizeGap6: '--size-gap-6', + sizeGap8: '--size-gap-8', + sizeGap10: '--size-gap-10', + sizeGap12: '--size-gap-12', + sizeGap16: '--size-gap-16', } as const; type WidgetThemeFallbackVarName = typeof FALLBACK_VAR[keyof typeof FALLBACK_VAR]; +const widgetVar = (name: string): string => `var(${name})`; // Keep this fallback map small and self-contained. It is the last-resort iframe // contract for static widget rendering before the host theme payload arrives. export const WIDGET_THEME_FALLBACK_VARS = { + [FALLBACK_VAR.bgPrimary]: 'transparent', + [FALLBACK_VAR.bgElevated]: WIDGET_IFRAME_FALLBACK_COLOR.bgSecondary, + [FALLBACK_VAR.bgTertiary]: WIDGET_IFRAME_FALLBACK_COLOR.bgSecondary, + [FALLBACK_VAR.bgScene]: WIDGET_IFRAME_FALLBACK_COLOR.bgSecondary, [FALLBACK_VAR.textPrimary]: WIDGET_IFRAME_FALLBACK_COLOR.textPrimary, [FALLBACK_VAR.textSecondary]: WIDGET_IFRAME_FALLBACK_COLOR.textSecondary, [FALLBACK_VAR.textMuted]: WIDGET_IFRAME_FALLBACK_COLOR.textMuted, + [FALLBACK_VAR.textDisabled]: WIDGET_IFRAME_FALLBACK_COLOR.textMuted, + [FALLBACK_VAR.accent50]: `color-mix(in srgb, ${widgetVar(FALLBACK_VAR.accent500)} 10%, transparent)`, + [FALLBACK_VAR.accent100]: `color-mix(in srgb, ${widgetVar(FALLBACK_VAR.accent500)} 16%, transparent)`, + [FALLBACK_VAR.accent400]: `color-mix(in srgb, ${widgetVar(FALLBACK_VAR.accent500)} 78%, ${widgetVar(FALLBACK_VAR.staticWhite)})`, [FALLBACK_VAR.accent500]: WIDGET_IFRAME_FALLBACK_COLOR.accent500, + [FALLBACK_VAR.accent500Rgb]: '96 165 250', [FALLBACK_VAR.accent600]: WIDGET_IFRAME_FALLBACK_COLOR.accent600, [FALLBACK_VAR.bgSecondary]: WIDGET_IFRAME_FALLBACK_COLOR.bgSecondary, [FALLBACK_VAR.success]: WIDGET_IFRAME_FALLBACK_COLOR.success, + [FALLBACK_VAR.successBg]: `color-mix(in srgb, ${widgetVar(FALLBACK_VAR.success)} 16%, transparent)`, [FALLBACK_VAR.warning]: WIDGET_IFRAME_FALLBACK_COLOR.warning, + [FALLBACK_VAR.warningBg]: `color-mix(in srgb, ${widgetVar(FALLBACK_VAR.warning)} 16%, transparent)`, [FALLBACK_VAR.error]: WIDGET_IFRAME_FALLBACK_COLOR.error, + [FALLBACK_VAR.errorBg]: `color-mix(in srgb, ${widgetVar(FALLBACK_VAR.error)} 16%, transparent)`, + [FALLBACK_VAR.errorBorder]: `color-mix(in srgb, ${widgetVar(FALLBACK_VAR.error)} 34%, transparent)`, [FALLBACK_VAR.staticWhite]: WIDGET_IFRAME_FALLBACK_COLOR.staticWhite, + [FALLBACK_VAR.staticBlack]: WIDGET_IFRAME_FALLBACK_COLOR.staticBlack, + [FALLBACK_VAR.overlayWhite04]: `color-mix(in srgb, ${widgetVar(FALLBACK_VAR.staticWhite)} 4%, transparent)`, + [FALLBACK_VAR.overlayBlack08]: `color-mix(in srgb, ${widgetVar(FALLBACK_VAR.staticBlack)} 8%, transparent)`, + [FALLBACK_VAR.overlayBlack30]: `color-mix(in srgb, ${widgetVar(FALLBACK_VAR.staticBlack)} 30%, transparent)`, [FALLBACK_VAR.borderSubtle]: WIDGET_IFRAME_FALLBACK_COLOR.borderSubtle, [FALLBACK_VAR.borderBase]: WIDGET_IFRAME_FALLBACK_COLOR.borderBase, [FALLBACK_VAR.borderMedium]: WIDGET_IFRAME_FALLBACK_COLOR.borderMedium, @@ -50,8 +105,28 @@ export const WIDGET_THEME_FALLBACK_VARS = { [FALLBACK_VAR.elementBgBase]: WIDGET_IFRAME_FALLBACK_COLOR.elementBgBase, [FALLBACK_VAR.elementBgMedium]: WIDGET_IFRAME_FALLBACK_COLOR.elementBgMedium, [FALLBACK_VAR.elementBgSoft]: WIDGET_IFRAME_FALLBACK_COLOR.elementBgBase, + [FALLBACK_VAR.elementBgElevated]: WIDGET_IFRAME_FALLBACK_COLOR.elementBgBase, + [FALLBACK_VAR.elementBgHover]: WIDGET_IFRAME_FALLBACK_COLOR.elementBgMedium, + [FALLBACK_VAR.motionBase]: '0.2s', [FALLBACK_VAR.shadowXs]: `0 1px 2px ${WIDGET_IFRAME_FALLBACK_COLOR.shadowBase}`, [FALLBACK_VAR.shadowSm]: `0 2px 4px ${WIDGET_IFRAME_FALLBACK_COLOR.shadowBase}`, + [FALLBACK_VAR.sizeRadiusSm]: '6px', + [FALLBACK_VAR.sizeRadiusBase]: '8px', + [FALLBACK_VAR.sizeRadiusMd]: widgetVar(FALLBACK_VAR.sizeRadiusBase), + [FALLBACK_VAR.sizeRadiusLg]: '12px', + [FALLBACK_VAR.sizeRadiusXl]: '16px', + [FALLBACK_VAR.sizeRadius2xl]: '20px', + [FALLBACK_VAR.sizeRadiusFull]: '9999px', + [FALLBACK_VAR.sizeGap1]: '4px', + [FALLBACK_VAR.sizeGap2]: '8px', + [FALLBACK_VAR.sizeGap3]: '12px', + [FALLBACK_VAR.sizeGap4]: '16px', + [FALLBACK_VAR.sizeGap5]: '20px', + [FALLBACK_VAR.sizeGap6]: '24px', + [FALLBACK_VAR.sizeGap8]: '32px', + [FALLBACK_VAR.sizeGap10]: '40px', + [FALLBACK_VAR.sizeGap12]: '48px', + [FALLBACK_VAR.sizeGap16]: '64px', } as const satisfies Record; export function createWidgetThemeFallbackCss(): string { @@ -60,359 +135,204 @@ export function createWidgetThemeFallbackCss(): string { .join('\n'); } +const WIDGET_THEME_STATIC_SHELL_VARS = { + '--color-bg-quaternary': widgetVar(FALLBACK_VAR.bgSecondary), + '--color-bg-workbench': widgetVar(FALLBACK_VAR.bgPrimary), + '--color-bg-tooltip': widgetVar(FALLBACK_VAR.bgElevated), + '--color-overlay-white-08': `color-mix(in srgb, ${widgetVar(FALLBACK_VAR.staticWhite)} 8%, transparent)`, + '--color-overlay-white-12': `color-mix(in srgb, ${widgetVar(FALLBACK_VAR.staticWhite)} 12%, transparent)`, + '--color-overlay-white-15': `color-mix(in srgb, ${widgetVar(FALLBACK_VAR.staticWhite)} 15%, transparent)`, + '--color-overlay-white-20': `color-mix(in srgb, ${widgetVar(FALLBACK_VAR.staticWhite)} 20%, transparent)`, + '--color-overlay-white-60': `color-mix(in srgb, ${widgetVar(FALLBACK_VAR.staticWhite)} 60%, transparent)`, + '--color-overlay-black-12': `color-mix(in srgb, ${widgetVar(FALLBACK_VAR.staticBlack)} 12%, transparent)`, + '--color-overlay-black-15': `color-mix(in srgb, ${widgetVar(FALLBACK_VAR.staticBlack)} 15%, transparent)`, + '--color-overlay-black-20': `color-mix(in srgb, ${widgetVar(FALLBACK_VAR.staticBlack)} 20%, transparent)`, + '--color-overlay-black-40': `color-mix(in srgb, ${widgetVar(FALLBACK_VAR.staticBlack)} 40%, transparent)`, + '--color-overlay-black-50': `color-mix(in srgb, ${widgetVar(FALLBACK_VAR.staticBlack)} 50%, transparent)`, + '--color-overlay-black-80': `color-mix(in srgb, ${widgetVar(FALLBACK_VAR.staticBlack)} 80%, transparent)`, + '--color-accent-200': `color-mix(in srgb, ${widgetVar(FALLBACK_VAR.accent500)} 24%, transparent)`, + '--color-accent-300': `color-mix(in srgb, ${widgetVar(FALLBACK_VAR.accent500)} 34%, transparent)`, + '--color-accent-700': `color-mix(in srgb, ${widgetVar(FALLBACK_VAR.accent600)} 86%, ${widgetVar(FALLBACK_VAR.staticBlack)})`, + '--color-accent-800': `color-mix(in srgb, ${widgetVar(FALLBACK_VAR.accent600)} 92%, ${widgetVar(FALLBACK_VAR.staticBlack)})`, + '--color-success-border': `color-mix(in srgb, ${widgetVar(FALLBACK_VAR.success)} 34%, transparent)`, + '--color-warning-border': `color-mix(in srgb, ${widgetVar(FALLBACK_VAR.warning)} 34%, transparent)`, + '--color-info': widgetVar(FALLBACK_VAR.textMuted), + '--color-info-bg': widgetVar(FALLBACK_VAR.elementBgSubtle), + '--color-info-border': widgetVar(FALLBACK_VAR.borderMedium), + '--border-strong': widgetVar(FALLBACK_VAR.borderMedium), + '--border-prominent': widgetVar(FALLBACK_VAR.borderMedium), + '--element-bg-strong': widgetVar(FALLBACK_VAR.elementBgMedium), + '--scrollbar-thumb': widgetVar(FALLBACK_VAR.elementBgMedium), + '--scrollbar-thumb-hover': widgetVar(FALLBACK_VAR.elementBgElevated), + '--color-scrollbar': widgetVar(FALLBACK_VAR.elementBgMedium), + '--glass-base': widgetVar(FALLBACK_VAR.elementBgSubtle), + '--glass-bg-base': widgetVar(FALLBACK_VAR.elementBgBase), + '--glass-bg-hover': widgetVar(FALLBACK_VAR.elementBgMedium), + '--glass-bg-active': widgetVar(FALLBACK_VAR.elementBgElevated), + '--glass-border-base': widgetVar(FALLBACK_VAR.borderBase), + '--glass-border-hover': widgetVar(FALLBACK_VAR.borderMedium), + '--glass-border-focus': widgetVar(FALLBACK_VAR.accent400), + '--glass-blur-sm': 'blur(4px)', + '--glass-blur-base': 'blur(8px)', + '--glass-shadow-sm': widgetVar(FALLBACK_VAR.shadowSm), + '--glass-shadow-base': `0 4px 8px ${widgetVar(FALLBACK_VAR.overlayBlack30)}`, + '--glass-shadow-lg': `0 8px 16px ${widgetVar(FALLBACK_VAR.overlayBlack30)}`, + '--glass-shadow-xl': `0 12px 24px ${widgetVar(FALLBACK_VAR.overlayBlack30)}`, + '--shadow-base': `0 4px 8px ${widgetVar(FALLBACK_VAR.overlayBlack30)}`, + '--shadow-lg': `0 8px 16px ${widgetVar(FALLBACK_VAR.overlayBlack30)}`, + '--shadow-xl': `0 12px 24px ${widgetVar(FALLBACK_VAR.overlayBlack30)}`, + '--font-size-xxs': '10px', + '--font-size-2xs': '11px', + '--font-size-xl': '16px', + '--opacity-disabled': '0.6', + '--opacity-hover': '0.8', + '--opacity-focus': '0.9', + '--motion-slow': '0.6s', + '--smooth-height-collapse-duration': '0.6s', + '--tool-card-font-mono': widgetVar('--font-family-mono'), + '--btn-primary-bg': widgetVar(FALLBACK_VAR.accent100), + '--btn-primary-color': widgetVar(FALLBACK_VAR.accent600), + '--btn-primary-border': 'transparent', + '--btn-primary-shadow': 'none', + '--btn-primary-hover-bg': widgetVar(FALLBACK_VAR.accent400), + '--btn-primary-hover-color': widgetVar(FALLBACK_VAR.textPrimary), + '--btn-primary-hover-border': 'transparent', + '--btn-primary-hover-shadow': 'none', + '--btn-primary-hover-transform': 'none', + '--btn-primary-active-bg': widgetVar(FALLBACK_VAR.accent100), + '--btn-primary-active-color': widgetVar(FALLBACK_VAR.textPrimary), + '--btn-primary-active-border': 'transparent', + '--btn-primary-active-shadow': 'none', + '--btn-primary-active-transform': 'none', + '--btn-ghost-color': widgetVar(FALLBACK_VAR.textMuted), + '--btn-ghost-hover-bg': widgetVar(FALLBACK_VAR.elementBgSubtle), + '--btn-ghost-hover-color': widgetVar(FALLBACK_VAR.textPrimary), + '--btn-ghost-hover-border': 'transparent', + '--tool-card-header-pad-y': '0.44rem', + '--tool-card-header-pad-x': '0', + '--tool-card-header-pad-right': '0.625rem', + '--tool-card-header-icon-rail': '24px', + '--tool-card-header-icon-slot': '34px', + '--tool-card-icon-size': '16px', + '--tool-card-expanded-pad-y': '0.5rem', + '--tool-card-expanded-pad-x': '0.625rem', + '--tool-card-action-font-size': widgetVar('--font-size-sm'), + '--tool-card-action-line-height': '1.45', + '--tool-card-action-font-weight': widgetVar('--font-weight-medium'), +} as const; + +export const WIDGET_THEME_STATIC_SHELL_VAR_NAMES = Object.keys(WIDGET_THEME_STATIC_SHELL_VARS); + +export function createWidgetThemeStaticShellCss(): string { + return Object.entries(WIDGET_THEME_STATIC_SHELL_VARS) + .map(([name, value]) => ` ${name}: ${value};`) + .join('\n'); +} + // Host -> generated-widget iframe theme contract. Keep groups explicit so // isolated widgets receive stable tokens without scraping every root variable. +// Host chrome/layout internals stay private: widgets have their own stacking +// and layout context, so FlowChat, navigation, and z-index keys are not exported. const WIDGET_THEME_VAR_GROUPS = { staticAndOverlay: [ - '--color-bg-primary', - FALLBACK_VAR.staticWhite, - '--color-static-black', - '--color-overlay-white-02', - '--color-overlay-white-03', - '--color-overlay-white-04', - '--color-overlay-white-05', - '--color-overlay-white-06', - '--color-overlay-white-08', - '--color-overlay-white-10', - '--color-overlay-white-12', - '--color-overlay-white-15', - '--color-overlay-white-20', - '--color-overlay-white-60', - '--color-overlay-black-06', - '--color-overlay-black-08', - '--color-overlay-black-10', - '--color-overlay-black-12', - '--color-overlay-black-15', - '--color-overlay-black-20', - '--color-overlay-black-25', - '--color-overlay-black-30', - '--color-overlay-black-40', - '--color-overlay-black-50', - '--color-overlay-black-80', + FALLBACK_VAR.bgPrimary, + FALLBACK_VAR.staticWhite, + FALLBACK_VAR.staticBlack, ], backgroundSurface: [ - FALLBACK_VAR.bgSecondary, - '--color-bg-tertiary', - '--color-bg-quaternary', - '--color-bg-elevated', - '--color-bg-workbench', - '--color-bg-scene', - '--color-bg-flowchat', - '--color-bg-surface', - '--color-bg-base', - '--color-bg-subtle', - '--color-bg-hover', - '--color-bg-elevated-hover', - '--color-hover', - '--color-background-secondary', - '--color-background-tertiary', - '--background-primary', - '--background-secondary', - '--background-tertiary', - '--bg-primary', - '--bg-secondary', - '--bg-tertiary', - '--bg-elevated', - '--bg-hover', - '--secondary-bg', - '--color-surface-elevated', - '--color-surface-hover', - '--color-bg-tooltip', + FALLBACK_VAR.bgSecondary, + FALLBACK_VAR.bgTertiary, + FALLBACK_VAR.bgElevated, + FALLBACK_VAR.bgScene, ], text: [ - FALLBACK_VAR.textPrimary, - FALLBACK_VAR.textSecondary, - '--color-text-tertiary', - '--text-primary', - '--text-secondary', - '--text-tertiary', - '--text-muted', - '--text-disabled', - FALLBACK_VAR.textMuted, - '--color-text-disabled', + FALLBACK_VAR.textPrimary, + FALLBACK_VAR.textSecondary, + FALLBACK_VAR.textMuted, + FALLBACK_VAR.textDisabled, ], accent: [ - '--color-accent-50', - '--color-accent-100', - '--color-accent-200', - '--color-accent-300', - '--color-accent-400', - FALLBACK_VAR.accent500, - FALLBACK_VAR.accent600, - '--color-accent-700', - '--color-accent-800', - '--color-accent', - '--color-accent-primary', - '--color-accent-alpha', - '--color-primary', - '--color-primary-rgb', - '--color-primary-400', - '--color-primary-hover', - '--color-primary-500', - '--color-primary-alpha', - '--color-primary-bg', - '--color-primary-bg-subtle', - '--accent-primary', - '--accent-primary-hover', + FALLBACK_VAR.accent500, + FALLBACK_VAR.accent500Rgb, + FALLBACK_VAR.accent600, ], semantic: [ - FALLBACK_VAR.success, - '--color-success-bg', - '--color-success-border', - '--color-success-100', - '--color-success-500', - FALLBACK_VAR.warning, - '--color-warning-bg', - '--color-warning-border', - '--color-warning-100', - '--color-warning-500', - '--color-warning-700', - FALLBACK_VAR.error, - '--color-error-bg', - '--color-error-border', - '--color-semantic-error', - '--color-danger', - '--color-danger-500', - '--color-danger-text', - '--color-danger-bg', - '--color-danger-border', - '--color-danger-hover', - '--color-info', - '--color-info-bg', - '--color-info-border', + FALLBACK_VAR.success, + FALLBACK_VAR.successBg, + FALLBACK_VAR.warning, + FALLBACK_VAR.warningBg, + FALLBACK_VAR.error, + FALLBACK_VAR.errorBg, + '--color-info', + '--color-info-bg', + '--color-info-border', ], border: [ - FALLBACK_VAR.borderSubtle, - '--border-color', - FALLBACK_VAR.borderBase, - FALLBACK_VAR.borderMedium, - '--border-hover', - '--border-strong', - '--border-prominent', - '--border-muted', - '--border-primary', - '--color-border', - '--color-border-primary', - '--color-border-subtle', - ], - zIndex: [ - '--z-base', - '--z-decoration', - '--z-content', - '--z-header', - '--z-sticky', - '--z-bottom-bar', - '--z-floating', - '--z-dropdown', - '--z-overlay', - '--z-drawer', - '--z-modal', - '--z-modal-active', - '--z-fullscreen', - '--z-toast', - '--z-tooltip', - '--z-popover', - '--z-notification', - '--z-context-menu', - '--z-extreme', + FALLBACK_VAR.borderSubtle, + FALLBACK_VAR.borderBase, + FALLBACK_VAR.borderMedium, ], elementGlassShadow: [ - FALLBACK_VAR.elementBgSubtle, - FALLBACK_VAR.elementBgSoft, - FALLBACK_VAR.elementBgBase, - FALLBACK_VAR.elementBgMedium, - '--element-bg-strong', - '--element-bg-elevated', - '--element-bg', - '--element-bg-hover', - '--scrollbar-thumb', - '--scrollbar-thumb-hover', - '--color-scrollbar', - '--glass-base', - '--glass-bg-base', - '--glass-bg-hover', - '--glass-bg-active', - '--glass-border-base', - '--glass-border-hover', - '--glass-border-focus', - '--glass-blur-sm', - '--glass-blur-base', - '--glass-shadow-sm', - '--glass-shadow-base', - '--glass-shadow-lg', - '--glass-shadow-xl', - FALLBACK_VAR.shadowXs, - FALLBACK_VAR.shadowSm, - '--shadow-base', - '--shadow-lg', - '--shadow-xl', + FALLBACK_VAR.elementBgSubtle, + FALLBACK_VAR.elementBgSoft, + FALLBACK_VAR.elementBgBase, + FALLBACK_VAR.elementBgMedium, + FALLBACK_VAR.elementBgElevated, + FALLBACK_VAR.elementBgHover, + FALLBACK_VAR.shadowXs, + FALLBACK_VAR.shadowSm, ], shapeSpacingTypography: [ - '--radius-sm', - '--radius-base', - '--radius-md', - '--radius-lg', - '--radius-xl', - '--radius-2xl', - '--radius-full', - '--size-radius-sm', - '--size-radius-base', - '--size-radius-md', - '--size-radius-lg', - '--size-radius-xl', - '--spacing-1', - '--spacing-2', - '--spacing-3', - '--spacing-4', - '--spacing-5', - '--spacing-6', - '--spacing-8', - '--spacing-10', - '--spacing-12', - '--spacing-16', - '--size-gap-1', - '--size-gap-2', - '--size-gap-3', - '--size-gap-4', - '--size-gap-5', - '--size-gap-6', - '--size-gap-8', - '--size-gap-10', - '--size-gap-12', - '--size-gap-16', - '--font-size-xxs', - '--font-size-2xs', - '--font-size-xs', - '--font-size-sm', - '--font-size-base', - '--font-size-lg', - '--font-size-xl', - '--font-size-2xl', - '--font-weight-medium', - '--font-weight-semibold', - '--opacity-disabled', - '--opacity-hover', - '--opacity-focus', - ], - flowChat: [ - '--flowchat-font-size-xxs', - '--flowchat-font-size-2xs', - '--flowchat-font-size-xs', - '--flowchat-font-size-sm', - '--flowchat-font-size-base', - '--flowchat-font-size-lg', - '--flowchat-font-size-xl', - '--flowchat-font-size-2xl', - '--flowchat-font-size-3xl', - '--flowchat-font-size-4xl', - '--flowchat-font-size-5xl', - '--flowchat-text-line-height', - '--flowchat-support-line-height', - '--flowchat-compact-line-height', - '--flowchat-flow-item-gap', - '--flowchat-inline-gap', - '--flowchat-control-gap', - '--flowchat-control-pad-y', - '--flowchat-control-pad-x', - '--flowchat-card-gap', - '--flowchat-card-radius', - '--flowchat-card-pad-y', - '--flowchat-card-pad-x', - '--flowchat-card-tight-pad-y', - '--flowchat-card-tight-pad-x', - '--flowchat-card-header-pad-y', - '--flowchat-card-header-pad-x', - '--flowchat-card-header-pad-right', - '--flowchat-card-expanded-pad-y', - '--flowchat-card-expanded-pad-x', - '--flowchat-code-font-size', - '--flowchat-code-line-height', - '--flowchat-code-block-pad-y', - '--flowchat-code-block-pad-x', - '--flowchat-markdown-block-gap', - '--flowchat-markdown-paragraph-gap', - '--flowchat-markdown-list-item-gap', - '--flowchat-markdown-list-nested-gap', - '--flowchat-markdown-heading-gap-before', - '--flowchat-markdown-heading-gap-after', - '--flowchat-markdown-code-block-gap', - '--flowchat-markdown-code-block-pad-y', - '--flowchat-markdown-code-block-pad-x', - '--flowchat-markdown-table-cell-pad-y', - '--flowchat-markdown-table-cell-pad-x', - ], - navigation: [ - '--bitfun-nav-row-action-size', - '--bitfun-nav-row-action-icon-size', - '--bitfun-nav-row-action-offset', - '--bitfun-nav-row-action-gap', + FALLBACK_VAR.sizeRadiusSm, + FALLBACK_VAR.sizeRadiusBase, + FALLBACK_VAR.sizeRadiusLg, + FALLBACK_VAR.sizeRadiusXl, + FALLBACK_VAR.sizeRadius2xl, + FALLBACK_VAR.sizeRadiusFull, + FALLBACK_VAR.sizeGap1, + FALLBACK_VAR.sizeGap2, + FALLBACK_VAR.sizeGap3, + FALLBACK_VAR.sizeGap4, + FALLBACK_VAR.sizeGap5, + FALLBACK_VAR.sizeGap6, + FALLBACK_VAR.sizeGap8, + FALLBACK_VAR.sizeGap10, + FALLBACK_VAR.sizeGap12, + FALLBACK_VAR.sizeGap16, + '--font-size-xs', + '--font-size-sm', + '--font-size-base', + '--font-size-lg', + '--font-size-2xl', + '--font-weight-medium', + '--font-weight-semibold', ], motionAndFonts: [ - '--motion-fast', - '--motion-normal', - '--motion-base', - '--motion-slow', - '--smooth-height-collapse-duration', - '--easing-standard', - '--font-family-sans', - '--font-family-mono', - '--font-sans', - '--font-mono', - '--markdown-font-mono', - '--tool-card-font-mono', - '--tool-compact-summary-font', + '--motion-fast', + FALLBACK_VAR.motionBase, + '--easing-standard', + '--font-family-sans', + '--font-family-mono', ], buttons: [ - '--btn-primary-bg', - '--btn-primary-color', - '--btn-primary-border', - '--btn-primary-shadow', - '--btn-primary-hover-bg', - '--btn-primary-hover-color', - '--btn-primary-hover-border', - '--btn-primary-hover-shadow', - '--btn-primary-hover-transform', - '--btn-primary-active-bg', - '--btn-primary-active-color', - '--btn-primary-active-border', - '--btn-primary-active-shadow', - '--btn-primary-active-transform', - '--btn-ghost-bg', - '--btn-ghost-color', - '--btn-ghost-border', - '--btn-ghost-shadow', - '--btn-ghost-hover-bg', - '--btn-ghost-hover-color', - '--btn-ghost-hover-border', - '--btn-ghost-hover-shadow', - '--btn-ghost-hover-transform', - '--btn-ghost-active-bg', - '--btn-ghost-active-color', - '--btn-ghost-active-border', - '--btn-ghost-active-shadow', - '--btn-ghost-active-transform', - ], - toolCard: [ - '--tool-card-bg-primary', - '--tool-card-bg-secondary', - '--tool-card-bg-hover', - '--tool-card-bg-elevated', - '--tool-card-border', - '--tool-card-border-subtle', - '--tool-card-text-primary', - '--tool-card-text-secondary', - '--tool-card-text-muted', - '--tool-card-header-pad-y', - '--tool-card-header-pad-x', - '--tool-card-header-pad-right', - '--tool-card-header-icon-rail', - '--tool-card-header-icon-slot', - '--tool-card-icon-size', - '--tool-card-expanded-pad-y', - '--tool-card-expanded-pad-x', - '--tool-card-action-font-size', - '--tool-card-action-line-height', - '--tool-card-action-font-weight', + '--btn-primary-bg', + '--btn-primary-color', + '--btn-primary-border', + '--btn-primary-shadow', + '--btn-primary-hover-bg', + '--btn-primary-hover-color', + '--btn-primary-hover-border', + '--btn-primary-hover-shadow', + '--btn-primary-hover-transform', + '--btn-primary-active-bg', + '--btn-primary-active-color', + '--btn-primary-active-border', + '--btn-primary-active-shadow', + '--btn-primary-active-transform', + '--btn-ghost-color', + '--btn-ghost-hover-bg', + '--btn-ghost-hover-color', + '--btn-ghost-hover-border', ], } as const; diff --git a/src/web-ui/src/tools/generative-widget/themePayloadCompatibility.ts b/src/web-ui/src/tools/generative-widget/themePayloadCompatibility.ts new file mode 100644 index 0000000000..cb0e6b9f87 --- /dev/null +++ b/src/web-ui/src/tools/generative-widget/themePayloadCompatibility.ts @@ -0,0 +1,106 @@ +const WIDGET_THEME_COMPAT_ALIAS_FALLBACKS = { + '--border-muted': '--border-subtle', + '--border-primary': '--border-base', + '--border-color': '--border-subtle', + '--border-hover': '--border-medium', + '--background-primary': '--color-bg-primary', + '--background-secondary': '--color-bg-secondary', + '--background-tertiary': '--color-bg-tertiary', + '--bg-primary': '--color-bg-primary', + '--bg-secondary': '--color-bg-secondary', + '--bg-tertiary': '--color-bg-tertiary', + '--bg-elevated': '--color-bg-elevated', + '--bg-hover': '--element-bg-hover', + '--color-background-secondary': '--color-bg-secondary', + '--color-background-tertiary': '--color-bg-tertiary', + '--color-bg-base': '--color-bg-primary', + '--color-bg-elevated-hover': '--element-bg-hover', + '--color-bg-flowchat': '--color-bg-scene', + '--color-bg-hover': '--element-bg-hover', + '--color-bg-subtle': '--element-bg-subtle', + '--color-bg-surface': '--color-bg-secondary', + '--color-border': '--border-base', + '--color-border-primary': '--border-base', + '--color-border-subtle': '--border-subtle', + '--color-hover': '--element-bg-hover', + '--color-semantic-error': '--color-error', + '--color-success-100': '--color-success-bg', + '--color-success-500': '--color-success', + '--color-surface-elevated': '--element-bg-elevated', + '--color-surface-hover': '--element-bg-hover', + '--color-text-tertiary': '--color-text-muted', + '--text-primary': '--color-text-primary', + '--text-secondary': '--color-text-secondary', + '--text-muted': '--color-text-muted', + '--text-disabled': '--color-text-disabled', + '--color-warning-100': '--color-warning-bg', + '--color-warning-500': '--color-warning', + '--color-warning-700': '--color-warning', + '--color-overlay-white-02': '--color-overlay-white-04', + '--color-overlay-white-03': '--color-overlay-white-04', + '--color-overlay-white-05': '--color-overlay-white-08', + '--color-overlay-white-06': '--color-overlay-white-08', + '--color-overlay-white-10': '--color-overlay-white-12', + '--color-overlay-black-06': '--color-overlay-black-08', + '--color-overlay-black-10': '--color-overlay-black-12', + '--color-overlay-black-25': '--color-overlay-black-30', + '--color-accent': '--color-accent-500', + '--color-accent-primary': '--color-accent-500', + '--color-accent-alpha': '--color-accent-100', + '--color-primary': '--color-accent-500', + '--color-primary-rgb': '--color-accent-500-rgb', + '--color-primary-400': '--color-accent-400', + '--color-primary-hover': '--color-accent-600', + '--color-primary-500': '--color-accent-500', + '--color-primary-alpha': '--color-accent-100', + '--color-primary-bg': '--color-accent-100', + '--color-primary-bg-subtle': '--color-accent-50', + '--accent-primary': '--color-accent-500', + '--accent-primary-hover': '--color-accent-600', + '--color-danger': '--color-error', + '--color-danger-500': '--color-error', + '--color-danger-text': '--color-error', + '--color-danger-bg': '--color-error-bg', + '--color-danger-border': '--color-error-border', + '--color-danger-hover': '--color-error', + '--element-bg': '--element-bg-base', + '--font-mono': '--font-family-mono', + '--font-sans': '--font-family-sans', + '--markdown-font-mono': '--font-family-mono', + '--motion-normal': '--motion-base', + '--radius-sm': '--size-radius-sm', + '--radius-base': '--size-radius-base', + '--radius-md': '--size-radius-md', + '--radius-lg': '--size-radius-lg', + '--radius-xl': '--size-radius-xl', + '--radius-2xl': '--size-radius-2xl', + '--radius-full': '--size-radius-full', + '--secondary-bg': '--color-bg-secondary', + '--spacing-1': '--size-gap-1', + '--spacing-2': '--size-gap-2', + '--spacing-3': '--size-gap-3', + '--spacing-4': '--size-gap-4', + '--spacing-5': '--size-gap-5', + '--spacing-6': '--size-gap-6', + '--spacing-8': '--size-gap-8', + '--spacing-10': '--size-gap-10', + '--spacing-12': '--size-gap-12', + '--spacing-16': '--size-gap-16', + '--text-tertiary': '--color-text-muted', + '--tool-card-bg-primary': '--element-bg-base', + '--tool-card-bg-secondary': '--element-bg-soft', + '--tool-card-bg-hover': '--element-bg-medium', + '--tool-card-bg-elevated': '--element-bg-elevated', + '--tool-card-border': '--border-base', + '--tool-card-border-subtle': '--border-subtle', + '--tool-card-text-primary': '--color-text-primary', + '--tool-card-text-secondary': '--color-text-secondary', + '--tool-card-text-muted': '--color-text-muted', + '--tool-compact-summary-font': '--font-family-sans', +} as const; + +export function createWidgetThemeCompatibilityAliasCss(): string { + return Object.entries(WIDGET_THEME_COMPAT_ALIAS_FALLBACKS) + .map(([name, canonical]) => ` ${name}: var(${canonical});`) + .join('\n'); +} diff --git a/src/web-ui/src/tools/git/components/CreateBranchDialog/CreateBranchDialog.scss b/src/web-ui/src/tools/git/components/CreateBranchDialog/CreateBranchDialog.scss index 7237e35267..8926d3f32c 100644 --- a/src/web-ui/src/tools/git/components/CreateBranchDialog/CreateBranchDialog.scss +++ b/src/web-ui/src/tools/git/components/CreateBranchDialog/CreateBranchDialog.scss @@ -15,8 +15,8 @@ } .bitfun-create-branch-dialog { - background: $color-bg-primary; - border: 1px solid $border-base; + background: var(--color-bg-primary); + border: 1px solid var(--border-base); border-radius: $size-radius-base; padding: $size-gap-5; min-width: 520px; @@ -33,7 +33,7 @@ margin-bottom: $size-gap-4; gap: $size-gap-3; padding-bottom: $size-gap-3; - border-bottom: 1px solid $border-base; + border-bottom: 1px solid var(--border-base); } &__title { @@ -57,7 +57,7 @@ transition: all $motion-base $easing-standard; &:hover:not(:disabled) { - background: $element-bg-base; + background: var(--element-bg-base); color: var(--color-text-primary); } @@ -73,8 +73,8 @@ justify-content: space-between; padding: $size-gap-3; margin-bottom: $size-gap-4; - background: $element-bg-subtle; - border: 1px solid $border-base; + background: var(--element-bg-subtle); + border: 1px solid var(--border-base); border-radius: $size-radius-sm; } @@ -97,8 +97,8 @@ color: var(--color-text-primary); font-weight: $font-weight-medium; padding: 2px 8px; - background: $element-bg-base; - border: 1px solid $border-base; + background: var(--element-bg-base); + border: 1px solid var(--border-base); border-radius: $size-radius-sm; } @@ -130,8 +130,8 @@ &__input { width: 100%; padding: $size-gap-2 $size-gap-3; - background: $element-bg-base; - border: 1px solid $border-base; + background: var(--element-bg-base); + border: 1px solid var(--border-base); border-radius: $size-radius-sm; color: var(--color-text-primary); font-family: $font-family-mono; @@ -141,7 +141,7 @@ &:focus { outline: none; border-color: var(--color-accent-500); - background: $element-bg-base; + background: var(--element-bg-base); box-shadow: 0 0 0 2px var(--color-accent-200); } @@ -209,8 +209,8 @@ code { display: inline-block; padding: 1px 4px; - background: $element-bg-base; - border: 1px solid $border-base; + background: var(--element-bg-base); + border: 1px solid var(--border-base); border-radius: $size-radius-sm; font-family: $font-family-mono; font-size: 10px; @@ -223,7 +223,7 @@ gap: $size-gap-2; margin-top: $size-gap-4; padding-top: $size-gap-4; - border-top: 1px solid $border-base; + border-top: 1px solid var(--border-base); justify-content: flex-end; } @@ -236,8 +236,8 @@ span { display: inline-block; padding: 2px 6px; - background: $element-bg-base; - border: 1px solid $border-base; + background: var(--element-bg-base); + border: 1px solid var(--border-base); border-radius: $size-radius-sm; font-family: $font-family-mono; margin: 0 4px; diff --git a/src/web-ui/src/tools/git/components/GitBranchHistoryView/GitBranchHistoryView.scss b/src/web-ui/src/tools/git/components/GitBranchHistoryView/GitBranchHistoryView.scss index 355d360d35..5b25c196f4 100644 --- a/src/web-ui/src/tools/git/components/GitBranchHistoryView/GitBranchHistoryView.scss +++ b/src/web-ui/src/tools/git/components/GitBranchHistoryView/GitBranchHistoryView.scss @@ -33,7 +33,7 @@ button { padding: 8px 20px; - background: var(--color-primary); + background: var(--color-accent-500); color: var(--color-static-white); border: none; border-radius: 6px; @@ -56,8 +56,8 @@ &__spinner { width: 36px; height: 36px; - border: 3px solid var(--color-border); - border-top-color: var(--color-primary); + border: 3px solid var(--border-base); + border-top-color: var(--color-accent-500); border-radius: 50%; animation: git-branch-history-spin 0.8s linear infinite; } @@ -67,8 +67,8 @@ align-items: center; justify-content: space-between; padding: 10px 12px; - border-bottom: 1px solid var(--color-border); - background: var(--color-background-secondary); + border-bottom: 1px solid var(--border-base); + background: var(--color-bg-secondary); flex-shrink: 0; gap: 12px; } @@ -99,7 +99,7 @@ flex-shrink: 0; &:hover { - background: var(--color-hover); + background: var(--element-bg-hover); } } @@ -131,7 +131,7 @@ &--expanded { .git-branch-history-view__commit-main { - background: var(--color-background-secondary); + background: var(--color-bg-secondary); } } @@ -185,17 +185,17 @@ transition: background 0.15s ease; &:hover { - background: var(--color-hover); + background: var(--element-bg-hover); } } &__checkbox { - color: var(--color-text-tertiary); + color: var(--color-text-muted); transition: color 0.15s ease; } &__checkbox--checked { - color: var(--color-primary); + color: var(--color-accent-500); } &__checkbox--partial { @@ -203,14 +203,14 @@ display: flex; align-items: center; justify-content: center; - color: var(--color-text-tertiary); + color: var(--color-text-muted); } &__checkbox-partial-mark { position: absolute; width: 8px; height: 2px; - background: var(--color-primary); + background: var(--color-accent-500); border-radius: 1px; } @@ -243,7 +243,7 @@ width: 2px; flex: 1; min-height: 16px; - background: var(--color-border); + background: var(--border-base); margin-top: 4px; } @@ -271,7 +271,7 @@ align-items: center; gap: 8px; font-size: 11px; - color: var(--color-text-tertiary); + color: var(--color-text-muted); } &__commit-author { @@ -291,14 +291,14 @@ gap: 3px; font-family: 'SF Mono', 'Monaco', 'Consolas', monospace; padding: 1px 4px; - background: var(--color-background-tertiary); + background: var(--color-bg-tertiary); border-radius: 3px; cursor: pointer; transition: all 0.15s ease; font-size: 10px; &:hover { - background: var(--color-hover); + background: var(--element-bg-hover); color: var(--color-text-secondary); } @@ -330,7 +330,7 @@ padding: 0; background: transparent; border: none; - color: var(--color-text-tertiary); + color: var(--color-text-muted); cursor: pointer; border-radius: 4px; transition: all 0.15s ease; @@ -339,14 +339,14 @@ &:hover { color: var(--color-text-primary); - background: var(--color-background-tertiary); + background: var(--color-bg-tertiary); } } &__commit-details { margin-left: 36px; padding: 10px 12px; - background: var(--color-background-secondary); + background: var(--color-bg-secondary); border-radius: 6px; margin-bottom: 4px; } @@ -364,7 +364,7 @@ } &__detail-label { - color: var(--color-text-tertiary); + color: var(--color-text-muted); min-width: 60px; flex-shrink: 0; } @@ -377,7 +377,7 @@ &--mono { font-family: 'SF Mono', 'Monaco', 'Consolas', monospace; font-size: 10px; - background: var(--color-background-tertiary); + background: var(--color-bg-tertiary); padding: 2px 6px; border-radius: 3px; } diff --git a/src/web-ui/src/tools/git/components/GitDiffEditor/GitDiffEditor.scss b/src/web-ui/src/tools/git/components/GitDiffEditor/GitDiffEditor.scss index 3ed02ba86d..6e73ac8ca4 100644 --- a/src/web-ui/src/tools/git/components/GitDiffEditor/GitDiffEditor.scss +++ b/src/web-ui/src/tools/git/components/GitDiffEditor/GitDiffEditor.scss @@ -1,4 +1,3 @@ -@use 'sass:color'; @use '../../../../component-library/styles/tokens' as *; $_diff-spinner-size: 40px; @@ -49,7 +48,7 @@ $_diff-padding-desktop: 32px; box-shadow: inset 0 1px 3px var(--color-overlay-black-20), - 0 0 0 1px var(--color-overlay-white-03); + 0 0 0 1px var(--color-overlay-white-04); background: linear-gradient( 180deg, @@ -195,7 +194,7 @@ $_diff-padding-desktop: 32px; } .monaco-diff-editor .diffViewport { - background: var(--color-overlay-white-02) !important; + background: var(--color-overlay-white-04) !important; } .monaco-diff-editor .diagonal-fill { @@ -217,7 +216,7 @@ $_diff-padding-desktop: 32px; right: $size-gap-3; padding: $size-gap-2 $size-gap-4; background: var(--color-overlay-black-80); - border: 1px solid var(--color-overlay-white-10); + border: 1px solid var(--color-overlay-white-12); border-radius: $size-radius-base; color: var(--color-text-secondary); font-size: var(--font-size-sm); @@ -234,7 +233,7 @@ $_diff-padding-desktop: 32px; left: 0; right: 0; bottom: 0; - background: $modal-backdrop; + background: var(--color-overlay-black-80); backdrop-filter: blur(8px); display: flex; flex-direction: column; @@ -256,7 +255,7 @@ $_diff-padding-desktop: 32px; &__spinner { width: $_diff-spinner-size; height: $_diff-spinner-size; - border: $_diff-spinner-border solid $color-accent-200; + border: $_diff-spinner-border solid var(--color-accent-200); border-top-color: var(--color-accent-500); border-radius: $size-radius-full; animation: git-diff-editor-spin $motion-lazy $easing-standard infinite; @@ -289,8 +288,8 @@ $_diff-padding-desktop: 32px; &--accepted { background: linear-gradient(135deg, - color.adjust($color-success, $lightness: 10%) 0%, - $color-success 100% + color-mix(in srgb, var(--color-success) 85%, var(--color-static-white)) 0%, + var(--color-success) 100% ); color: var(--color-static-white); box-shadow: @@ -300,8 +299,8 @@ $_diff-padding-desktop: 32px; &--rejected { background: linear-gradient(135deg, - color.adjust($color-error, $lightness: 10%) 0%, - $color-error 100% + color-mix(in srgb, var(--color-error) 85%, var(--color-static-white)) 0%, + var(--color-error) 100% ); color: var(--color-static-white); box-shadow: diff --git a/src/web-ui/src/tools/git/components/GitDiffView/GitDiffView.scss b/src/web-ui/src/tools/git/components/GitDiffView/GitDiffView.scss index c14de85dfd..f08610e371 100644 --- a/src/web-ui/src/tools/git/components/GitDiffView/GitDiffView.scss +++ b/src/web-ui/src/tools/git/components/GitDiffView/GitDiffView.scss @@ -19,7 +19,7 @@ $opacity-high: 0.8 !default; align-items: center; justify-content: space-between; padding: $size-gap-3 $size-gap-4; - border-bottom: 1px solid $border-base; + border-bottom: 1px solid var(--border-base); background: var(--color-bg-primary); flex-shrink: 0; } @@ -39,8 +39,8 @@ $opacity-high: 0.8 !default; &__commit-range, &__diff-type { padding: 2px $size-gap-1-5; - background: $element-bg-base; - border: 1px solid $border-base; + background: var(--element-bg-base); + border: 1px solid var(--border-base); border-radius: $size-radius-xs; font-size: 11px; font-family: $font-family-mono; @@ -49,7 +49,7 @@ $opacity-high: 0.8 !default; &__diff-type-switcher { display: flex; - border: 1px solid $border-base; + border: 1px solid var(--border-base); border-radius: $size-radius-xs; overflow: hidden; } @@ -66,18 +66,18 @@ $opacity-high: 0.8 !default; transition: all $motion-base $easing-standard; &:hover { - background: $element-bg-base; + background: var(--element-bg-base); color: var(--color-text-primary); } &--active { - background: $element-bg-base; + background: var(--element-bg-base); color: var(--color-accent-500); font-weight: $font-weight-semibold; } & + & { - border-left: 1px solid $border-base; + border-left: 1px solid var(--border-base); } } @@ -106,8 +106,8 @@ $opacity-high: 0.8 !default; align-items: center; gap: $size-gap-1; padding: $size-gap-1-5 $size-gap-2; - background: $element-bg-base; - border: 1px solid $border-base; + background: var(--element-bg-base); + border: 1px solid var(--border-base); border-radius: $size-radius-sm; color: var(--color-text-secondary); font-size: var(--font-size-xs); @@ -115,8 +115,8 @@ $opacity-high: 0.8 !default; transition: all $motion-base $easing-standard; &:hover { - background: $element-bg-medium; - border-color: $border-medium; + background: var(--element-bg-medium); + border-color: var(--border-medium); } &--active { @@ -166,7 +166,7 @@ $opacity-high: 0.8 !default; &__loading-spinner { width: 32px; height: 32px; - border: 3px solid $border-base; + border: 3px solid var(--border-base); border-top-color: var(--color-accent-500); border-radius: $size-radius-full; animation: bitfun-git-diff-view-spin 1s linear infinite; @@ -191,13 +191,13 @@ $opacity-high: 0.8 !default; align-items: center; justify-content: space-between; padding: $size-gap-2 $size-gap-3; - background: $element-bg-subtle; + background: var(--element-bg-subtle); cursor: pointer; user-select: none; transition: all $motion-base $easing-standard; &:hover { - background: $element-bg-base; + background: var(--element-bg-base); } } @@ -235,23 +235,23 @@ $opacity-high: 0.8 !default; color: var(--color-text-muted); &--added { - background: $git-color-added-bg; - color: $git-color-added; + background: var(--git-color-added-bg); + color: var(--git-color-added); } &--modified { - background: $git-color-changes-bg; - color: $git-color-changes; + background: var(--git-color-changes-bg); + color: var(--git-color-changes); } &--deleted { - background: $git-color-deleted-bg; - color: $git-color-deleted; + background: var(--git-color-deleted-bg); + color: var(--git-color-deleted); } &--renamed { - background: $git-color-branch-bg; - color: $git-color-branch; + background: var(--git-color-branch-bg); + color: var(--git-color-branch); } } @@ -266,14 +266,14 @@ $opacity-high: 0.8 !default; } &__additions { - color: $git-color-added; + color: var(--git-color-added); display: flex; align-items: center; gap: 4px; } &__deletions { - color: $git-color-deleted; + color: var(--git-color-deleted); display: flex; align-items: center; gap: 4px; @@ -294,7 +294,7 @@ $opacity-high: 0.8 !default; font-size: var(--font-size-xs); line-height: 1.6; overflow-x: auto; - border-top: 1px solid $border-base; + border-top: 1px solid var(--border-base); } @@ -304,39 +304,39 @@ $opacity-high: 0.8 !default; transition: background $motion-fast $easing-standard; &--added { - background: $git-color-added-bg; + background: var(--git-color-added-bg); &:hover { - background: $git-color-added-bg-hover; + background: var(--git-color-added-bg-hover); } .bitfun-git-diff-view__line-content { - color: $git-color-added; + color: var(--git-color-added); } } &--deleted { - background: $git-color-deleted-bg; + background: var(--git-color-deleted-bg); &:hover { - background: $git-color-deleted-bg-hover; + background: var(--git-color-deleted-bg-hover); } .bitfun-git-diff-view__line-content { - color: $git-color-deleted; + color: var(--git-color-deleted); } } &--context { &:hover { - background: $element-bg-subtle; + background: var(--element-bg-subtle); } } &--hunk-header { - background: $element-bg-base; - border-top: 1px solid $border-base; - border-bottom: 1px solid $border-base; + background: var(--element-bg-base); + border-top: 1px solid var(--border-base); + border-bottom: 1px solid var(--border-base); .bitfun-git-diff-view__line-content { color: var(--color-accent-500); @@ -352,8 +352,8 @@ $opacity-high: 0.8 !default; text-align: right; color: var(--color-text-muted); user-select: none; - border-right: 1px solid $border-base; - background: $element-bg-subtle; + border-right: 1px solid var(--border-base); + background: var(--element-bg-subtle); font-size: 11px; } @@ -382,7 +382,7 @@ $opacity-high: 0.8 !default; &__commit-dialog { background: var(--color-bg-secondary); - border: 1px solid $border-base; + border: 1px solid var(--border-base); border-radius: $size-radius-lg; padding: $size-gap-6; min-width: 500px; @@ -456,8 +456,8 @@ $opacity-high: 0.8 !default; &__form-textarea { min-height: 120px; padding: $size-gap-3; - background: $element-bg-base; - border: 1px solid $border-base; + background: var(--element-bg-base); + border: 1px solid var(--border-base); border-radius: $size-radius-base; color: var(--color-text-primary); font-family: $font-family-mono; @@ -469,7 +469,7 @@ $opacity-high: 0.8 !default; &:focus { outline: none; border-color: var(--color-accent-500); - background: $element-bg-medium; + background: var(--element-bg-medium); box-shadow: 0 0 0 3px var(--card-bg-accent); } @@ -533,14 +533,14 @@ $opacity-high: 0.8 !default; &__ai-confidence-bar { width: 60px; height: 4px; - background: var(--color-overlay-white-10); + background: var(--color-overlay-white-12); border-radius: 2px; overflow: hidden; } &__ai-confidence-fill { height: 100%; - background: linear-gradient(90deg, $color-accent-400, $color-accent-600); + background: linear-gradient(90deg, var(--color-accent-400), var(--color-accent-600)); border-radius: 2px; transition: width $motion-base $easing-standard; } @@ -595,7 +595,7 @@ $opacity-high: 0.8 !default; align-items: center; gap: $size-gap-3; padding: $size-gap-3; - background: $element-bg-base; + background: var(--element-bg-base); border-radius: $size-radius-base; margin-bottom: $size-gap-4; font-size: var(--font-size-sm); diff --git a/src/web-ui/src/tools/git/components/GitGraphView/GitGraphView.scss b/src/web-ui/src/tools/git/components/GitGraphView/GitGraphView.scss index 44a711d03e..a7d516e252 100644 --- a/src/web-ui/src/tools/git/components/GitGraphView/GitGraphView.scss +++ b/src/web-ui/src/tools/git/components/GitGraphView/GitGraphView.scss @@ -29,7 +29,7 @@ button { padding: 8px 16px; - background: var(--color-primary); + background: var(--color-accent-500); color: var(--color-static-white); border: none; border-radius: 4px; @@ -45,8 +45,8 @@ &__spinner { width: 32px; height: 32px; - border: 3px solid var(--color-overlay-white-10); - border-top-color: var(--color-primary); + border: 3px solid var(--color-overlay-white-12); + border-top-color: var(--color-accent-500); border-radius: 50%; animation: spin 0.8s linear infinite; } @@ -56,8 +56,8 @@ align-items: center; justify-content: space-between; padding: 12px 16px; - border-bottom: 1px solid var(--color-border); - background: var(--color-background-secondary); + border-bottom: 1px solid var(--border-base); + background: var(--color-bg-secondary); flex-shrink: 0; } @@ -78,7 +78,7 @@ align-items: center; gap: 4px; padding: 4px 8px; - background: var(--color-primary); + background: var(--color-accent-500); color: var(--color-static-white); border-radius: 4px; font-size: 12px; @@ -105,7 +105,7 @@ font-family: 'SF Mono', 'Monaco', 'Courier New', monospace; &--no-results { - color: var(--color-text-tertiary); + color: var(--color-text-muted); } } @@ -124,7 +124,7 @@ transition: all 0.15s ease; &:hover:not(:disabled) { - background: var(--color-hover); + background: var(--element-bg-hover); color: var(--color-text-primary); } @@ -218,13 +218,13 @@ gap: 12px; padding: 0 16px; cursor: pointer; - border-bottom: 1px solid var(--color-border); + border-bottom: 1px solid var(--border-base); transition: background 0.15s, box-shadow 0.15s; background: var(--color-bg-primary); &:hover { - background: var(--color-hover); - box-shadow: 0 1px 3px var(--color-overlay-black-10); + background: var(--element-bg-hover); + box-shadow: 0 1px 3px var(--color-overlay-black-12); z-index: 1; } @@ -326,7 +326,7 @@ &__commit-hash { font-family: 'Courier New', monospace; padding: 2px 6px; - background: var(--color-background-tertiary); + background: var(--color-bg-tertiary); border-radius: 4px; white-space: nowrap; } @@ -353,7 +353,7 @@ border-radius: 4px; &:hover { - background: var(--color-hover); + background: var(--element-bg-hover); color: var(--color-text-primary); } } diff --git a/src/web-ui/src/tools/git/components/GitSettingsView/GitSettingsView.css b/src/web-ui/src/tools/git/components/GitSettingsView/GitSettingsView.css index bb3dabd4cc..0d99d05a89 100644 --- a/src/web-ui/src/tools/git/components/GitSettingsView/GitSettingsView.css +++ b/src/web-ui/src/tools/git/components/GitSettingsView/GitSettingsView.css @@ -12,7 +12,7 @@ align-items: center; justify-content: space-between; padding: 16px; - border-bottom: 1px solid var(--color-border-primary); + border-bottom: 1px solid var(--border-base); background: var(--color-bg-secondary); } @@ -42,7 +42,7 @@ width: 32px; height: 32px; background: var(--color-bg-tertiary); - border: 1px solid var(--color-border-primary); + border: 1px solid var(--border-base); border-radius: 4px; color: var(--color-text-secondary); cursor: pointer; @@ -50,7 +50,7 @@ } .refresh-btn:hover:not(:disabled) { - background: var(--color-bg-hover); + background: var(--element-bg-hover); color: var(--color-text-primary); } @@ -117,7 +117,7 @@ .settings-tabs { display: flex; - border-bottom: 1px solid var(--color-border-primary); + border-bottom: 1px solid var(--border-base); background: var(--color-bg-secondary); } @@ -138,12 +138,12 @@ .tab-btn:hover { color: var(--color-text-primary); - background: var(--color-bg-hover); + background: var(--element-bg-hover); } .tab-btn.active { - color: var(--color-primary); - border-bottom-color: var(--color-primary); + color: var(--color-accent-500); + border-bottom-color: var(--color-accent-500); } .settings-content { @@ -159,7 +159,7 @@ .settings-section { margin-bottom: 32px; padding-bottom: 24px; - border-bottom: 1px solid var(--color-border-primary); + border-bottom: 1px solid var(--border-base); } .settings-section:last-child { @@ -207,7 +207,7 @@ width: 100%; padding: 8px 12px; background: var(--color-bg-tertiary); - border: 1px solid var(--color-border-primary); + border: 1px solid var(--border-base); border-radius: 4px; color: var(--color-text-primary); font-size: 12px; @@ -217,8 +217,8 @@ .form-group input:focus, .form-group select:focus { outline: none; - border-color: var(--color-primary); - background: var(--color-bg-hover); + border-color: var(--color-accent-500); + background: var(--element-bg-hover); } .form-group input::placeholder { @@ -228,7 +228,7 @@ .checkbox-group { padding: 12px; background: var(--color-bg-tertiary); - border: 1px solid var(--color-border-primary); + border: 1px solid var(--border-base); border-radius: 6px; } @@ -262,7 +262,7 @@ .remote-config { padding: 16px; background: var(--color-bg-tertiary); - border: 1px solid var(--color-border-primary); + border: 1px solid var(--border-base); border-radius: 6px; margin-bottom: 16px; } @@ -270,10 +270,10 @@ .remote-config h5 { margin: 0 0 12px; padding: 4px 8px; - background: var(--color-primary-bg); - border: 1px solid var(--color-primary); + background: var(--color-accent-100); + border: 1px solid var(--color-accent-500); border-radius: 3px; - color: var(--color-primary); + color: var(--color-accent-500); font-size: 11px; font-weight: 600; text-transform: uppercase; @@ -283,7 +283,7 @@ .branch-config { padding: 16px; background: var(--color-bg-tertiary); - border: 1px solid var(--color-border-primary); + border: 1px solid var(--border-base); border-radius: 6px; margin-bottom: 16px; } @@ -338,7 +338,7 @@ .retry-btn { padding: 8px 16px; - background: var(--color-primary); + background: var(--color-accent-500); border: none; border-radius: 4px; color: white; @@ -348,7 +348,7 @@ } .retry-btn:hover { - background: var(--color-primary-hover); + background: var(--color-accent-600); } @media (max-width: 768px) { @@ -373,7 +373,7 @@ } .tab-btn.active { - border-right-color: var(--color-primary); + border-right-color: var(--color-accent-500); border-bottom-color: transparent; } diff --git a/src/web-ui/src/tools/git/components/GitSettingsView/GitSettingsView.scss b/src/web-ui/src/tools/git/components/GitSettingsView/GitSettingsView.scss index a1cc705452..b05be42c24 100644 --- a/src/web-ui/src/tools/git/components/GitSettingsView/GitSettingsView.scss +++ b/src/web-ui/src/tools/git/components/GitSettingsView/GitSettingsView.scss @@ -4,7 +4,6 @@ $size-gap-1-5: 6px !default; $size-radius-xs: 4px !default; $opacity-secondary: 0.7 !default; $opacity-high: 0.8 !default; -$border-subtle: var(--color-overlay-white-06) !default; .bitfun-git-settings-view { display: flex; @@ -19,8 +18,8 @@ $border-subtle: var(--color-overlay-white-06) !default; align-items: center; justify-content: space-between; padding: $size-gap-4; - border-bottom: 1px solid $border-base; - background: $element-bg-soft; + border-bottom: 1px solid var(--border-base); + background: var(--element-bg-soft); } &__header-left { @@ -52,13 +51,13 @@ $border-subtle: var(--color-overlay-white-06) !default; &--success { background: var(--color-success-bg); - border-bottom: 1px solid $color-success; + border-bottom: 1px solid var(--color-success); color: var(--color-success); } &--error { background: var(--color-error-bg); - border-bottom: 1px solid $color-error; + border-bottom: 1px solid var(--color-error); color: var(--color-error); } } @@ -121,8 +120,8 @@ $border-subtle: var(--color-overlay-white-06) !default; &__form-input { padding: $size-gap-2 $size-gap-3; - background: $element-bg-base; - border: 1px solid $border-base; + background: var(--element-bg-base); + border: 1px solid var(--border-base); border-radius: $size-radius-base; color: var(--color-text-primary); font-size: var(--font-size-sm); @@ -132,7 +131,7 @@ $border-subtle: var(--color-overlay-white-06) !default; &:focus { outline: none; border-color: var(--color-accent-500); - background: $element-bg-medium; + background: var(--element-bg-medium); } &::placeholder { @@ -147,8 +146,8 @@ $border-subtle: var(--color-overlay-white-06) !default; &__form-select { padding: $size-gap-2 $size-gap-3; - background: $element-bg-base; - border: 1px solid $border-base; + background: var(--element-bg-base); + border: 1px solid var(--border-base); border-radius: $size-radius-base; color: var(--color-text-primary); font-size: var(--font-size-sm); @@ -158,7 +157,7 @@ $border-subtle: var(--color-overlay-white-06) !default; &:focus { outline: none; border-color: var(--color-accent-500); - background: $element-bg-medium; + background: var(--element-bg-medium); } &:disabled { @@ -202,7 +201,7 @@ $border-subtle: var(--color-overlay-white-06) !default; gap: $size-gap-2; margin-bottom: $size-gap-4; padding-bottom: $size-gap-2; - border-bottom: 1px solid $border-subtle; + border-bottom: 1px solid var(--color-overlay-white-08); } &__section-title { @@ -220,15 +219,15 @@ $border-subtle: var(--color-overlay-white-06) !default; &__config-item { padding: $size-gap-4; - background: $element-bg-soft; - border: 1px solid $border-base; + background: var(--element-bg-soft); + border: 1px solid var(--border-base); border-radius: $size-radius-base; margin-bottom: $size-gap-3; transition: all $motion-base $easing-standard; &:hover { - background: $element-bg-base; - border-color: $border-medium; + background: var(--element-bg-base); + border-color: var(--border-medium); } &:last-child { @@ -303,23 +302,23 @@ $border-subtle: var(--color-overlay-white-06) !default; align-items: center; gap: $size-gap-2; padding: $size-gap-3; - background: $element-bg-base; - border: 1px solid $border-base; + background: var(--element-bg-base); + border: 1px solid var(--border-base); border-radius: $size-radius-base; cursor: pointer; transition: all $motion-base $easing-standard; margin-top: $size-gap-4; &:hover { - background: $element-bg-medium; - border-color: $border-medium; + background: var(--element-bg-medium); + border-color: var(--border-medium); } } &__advanced-content { margin-top: $size-gap-3; padding: $size-gap-4; - background: $element-bg-subtle; + background: var(--element-bg-subtle); border-radius: $size-radius-base; } } diff --git a/src/web-ui/src/tools/git/components/PushButton/PushButton.scss b/src/web-ui/src/tools/git/components/PushButton/PushButton.scss index 0dec60b908..7825c13bb6 100644 --- a/src/web-ui/src/tools/git/components/PushButton/PushButton.scss +++ b/src/web-ui/src/tools/git/components/PushButton/PushButton.scss @@ -16,7 +16,7 @@ &__main { border-top-right-radius: 0 !important; border-bottom-right-radius: 0 !important; - border-right: 1px solid var(--color-overlay-white-10) !important; + border-right: 1px solid var(--color-overlay-white-12) !important; padding-right: 8px !important; display: flex; @@ -58,8 +58,8 @@ &__menu { min-width: 120px; - background: $element-bg-subtle; - border: 1px solid $border-base; + background: var(--element-bg-subtle); + border: 1px solid var(--border-base); border-radius: $size-radius-base; box-shadow: 0 8px 20px var(--color-overlay-black-80); z-index: $z-dropdown; @@ -76,8 +76,8 @@ white-space: nowrap; background: linear-gradient(135deg, var(--color-overlay-white-08) 0%, - var(--color-overlay-white-06) 50%, - var(--color-overlay-white-10) 100%); + var(--color-overlay-white-08) 50%, + var(--color-overlay-white-12) 100%); border: 1px solid var(--color-overlay-white-15); border-radius: 6px; cursor: pointer; @@ -93,7 +93,7 @@ box-shadow: 0 2px 8px var(--color-overlay-black-20), inset 0 1px 0 var(--color-overlay-white-15), - inset 0 -1px 0 var(--color-overlay-white-05); + inset 0 -1px 0 var(--color-overlay-white-08); &:last-child { margin-bottom: 0; @@ -182,7 +182,7 @@ &__menu-divider { height: 1px; - background: var(--color-overlay-white-10); + background: var(--color-overlay-white-12); margin: 4px 0; } } diff --git a/src/web-ui/src/tools/lsp/components/LspPluginList/LspPluginList.scss b/src/web-ui/src/tools/lsp/components/LspPluginList/LspPluginList.scss index 8ad39eb3a6..7541ed0192 100644 --- a/src/web-ui/src/tools/lsp/components/LspPluginList/LspPluginList.scss +++ b/src/web-ui/src/tools/lsp/components/LspPluginList/LspPluginList.scss @@ -264,7 +264,7 @@ &__tag { padding: 4px $size-gap-2; - background: $element-bg-soft; + background: var(--element-bg-soft); border: none; border-radius: $size-radius-sm; font-size: var(--font-size-xs); @@ -273,7 +273,7 @@ transition: all $motion-fast $easing-standard; &:hover { - background: $element-bg-medium; + background: var(--element-bg-medium); } } diff --git a/src/web-ui/src/tools/lsp/components/ReferencesPanel/ReferencesPanel.scss b/src/web-ui/src/tools/lsp/components/ReferencesPanel/ReferencesPanel.scss index d4edb8c67f..a0915d6501 100644 --- a/src/web-ui/src/tools/lsp/components/ReferencesPanel/ReferencesPanel.scss +++ b/src/web-ui/src/tools/lsp/components/ReferencesPanel/ReferencesPanel.scss @@ -25,11 +25,11 @@ min-width: 300px; background: var(--color-bg-primary); - border: 1px solid $border-prominent; + border: 1px solid var(--border-prominent); border-radius: $size-radius-base; box-shadow: - $shadow-2xl, + var(--shadow-xl), 0 0 0 1px var(--color-overlay-black-80), $inner-glow-top; @@ -65,8 +65,8 @@ align-items: center; justify-content: space-between; padding: $size-gap-3 $size-gap-4; - background: $element-bg-soft; - border-bottom: 1px solid $border-base; + background: var(--element-bg-soft); + border-bottom: 1px solid var(--border-base); } .references-panel__title { @@ -104,13 +104,13 @@ transition: all $motion-fast ease; &:hover { - background: $element-bg-base; + background: var(--element-bg-base); color: var(--color-text-secondary); } &:active { transform: scale(0.95); - background: $element-bg-medium; + background: var(--element-bg-medium); } } @@ -136,7 +136,7 @@ margin-bottom: $size-gap-2; border-radius: $size-radius-sm; overflow: hidden; - background: $element-bg-subtle; + background: var(--element-bg-subtle); &:last-child { margin-bottom: 0; @@ -148,8 +148,8 @@ align-items: center; gap: $size-gap-2; padding: $size-gap-2 $size-gap-3; - background: $element-bg-soft; - border-bottom: 1px solid $border-subtle; + background: var(--element-bg-soft); + border-bottom: 1px solid var(--border-subtle); cursor: default; svg { @@ -203,9 +203,9 @@ &:hover { background: linear-gradient(135deg, - $color-accent-200 0%, - $color-accent-100 50%, - $color-accent-200 100% + var(--color-accent-200) 0%, + var(--color-accent-100) 50%, + var(--color-accent-200) 100% ); color: var(--color-text-primary); @@ -289,7 +289,7 @@ border-width: 2px; border-color: rgba(var(--color-static-white-rgb), 0.5); box-shadow: - $shadow-2xl, + var(--shadow-xl), 0 0 0 2px rgba(var(--color-static-black-rgb), 0.9); } @@ -316,7 +316,7 @@ } .references-panel__reference-item:focus-visible { - outline: 2px solid $color-accent-500; + outline: 2px solid var(--color-accent-500); outline-offset: -2px; border-radius: $size-radius-sm; } diff --git a/src/web-ui/src/tools/mermaid-editor/theme/_tokens.scss b/src/web-ui/src/tools/mermaid-editor/theme/_tokens.scss index 58686dd824..c0a5c44036 100644 --- a/src/web-ui/src/tools/mermaid-editor/theme/_tokens.scss +++ b/src/web-ui/src/tools/mermaid-editor/theme/_tokens.scss @@ -12,11 +12,15 @@ @use '../../../component-library/styles/tokens' as *; $mermaid-light-node-fill: rgba(15, 23, 42, 0.06); +$mermaid-light-node-fill-hover: rgba(15, 23, 42, 0.1); $mermaid-light-node-stroke: rgba(100, 116, 139, 0.35); +$mermaid-light-edge-label-border: rgba(100, 116, 139, 0.25); $mermaid-light-text-primary: #1e293b; $mermaid-light-text-secondary: #475569; -$mermaid-light-surface: #f7f8fa; +$mermaid-light-surface: #f3f4f6; +$mermaid-light-section-alt-fill: #e8eaef; $mermaid-light-highlight-stroke: #334155; +$mermaid-light-note-text: #92400e; // ==================== Mermaid CSS variables ==================== // Define dark theme defaults in :root @@ -29,18 +33,16 @@ $mermaid-light-highlight-stroke: #334155; --mermaid-node-stroke-hover: #{$border-strong}; --mermaid-node-stroke-width: 1.5px; --mermaid-node-text: #{$color-text-primary}; - --mermaid-node-radius: #{$size-radius-sm}; --mermaid-node-dash-array: 4 2; - + // ==================== Edge styles ==================== --mermaid-edge-stroke: #{$border-medium}; --mermaid-edge-stroke-hover: #{$border-strong}; - --mermaid-edge-stroke-width: 1.5px; --mermaid-edge-label-bg: #{$color-bg-secondary}; --mermaid-edge-label-border: #{$border-subtle}; --mermaid-edge-label-text: #{$color-text-secondary}; --mermaid-arrow-color: #{$color-text-muted}; - + // ==================== Subgraph/cluster styles ==================== --mermaid-cluster-fill: #{$element-bg-subtle}; --mermaid-cluster-stroke: #{$border-subtle}; @@ -48,27 +50,23 @@ $mermaid-light-highlight-stroke: #334155; --mermaid-cluster-radius: #{$size-radius-base}; --mermaid-cluster-text: #{$color-text-secondary}; --mermaid-cluster-dash-array: 5 3; - + // ==================== Status colors ==================== --mermaid-success: #{$color-success}; - --mermaid-success-bg: #{$color-success-bg}; --mermaid-error: #{$color-error}; --mermaid-error-bg: #{$color-error-bg}; --mermaid-warning: #{$color-warning}; - --mermaid-warning-bg: #{$color-warning-bg}; --mermaid-info: #{$color-info}; - --mermaid-info-bg: #{$color-info-bg}; - + // ==================== Highlight styles ==================== --mermaid-highlight-stroke: #{$color-accent-500}; --mermaid-highlight-glow: #{$glow-shadow-blue}; - --mermaid-selected-stroke: #{$color-accent-600}; - + // ==================== Interaction effects ==================== --mermaid-transition-duration: #{$motion-base}; --mermaid-transition-easing: #{$easing-standard}; --mermaid-hover-filter: brightness(1.08); - + // ==================== Sequence diagram ==================== --mermaid-actor-fill: #{$element-bg-base}; --mermaid-actor-stroke: #{$border-medium}; @@ -80,7 +78,7 @@ $mermaid-light-highlight-stroke: #334155; --mermaid-note-text: #{$color-text-secondary}; --mermaid-activation-fill: rgba(74, 144, 217, 0.15); --mermaid-activation-stroke: #{$border-medium}; - + // ==================== Gantt chart ==================== --mermaid-section-fill: #{$color-bg-primary}; --mermaid-section-alt-fill: #{$color-bg-secondary}; @@ -92,7 +90,7 @@ $mermaid-light-highlight-stroke: #334155; --mermaid-active-stroke: #{$color-info}; --mermaid-crit-fill: #{$color-error-bg}; --mermaid-crit-stroke: #{$color-error}; - + // ==================== Pie chart ==================== --mermaid-pie-stroke: #{$color-bg-primary}; --mermaid-pie-stroke-width: 2px; @@ -106,20 +104,9 @@ $mermaid-light-highlight-stroke: #334155; --mermaid-pie-6: #d890b8; --mermaid-pie-7: #68c8d8; --mermaid-pie-8: #a8d868; - + // ==================== Class/ER diagram ==================== - --mermaid-class-fill: #{$element-bg-base}; - --mermaid-class-stroke: #{$border-medium}; --mermaid-class-text: #{$color-text-primary}; - --mermaid-class-divider: #{$border-subtle}; - - // ==================== Git diagram ==================== - --mermaid-commit-text: #{$color-text-primary}; - --mermaid-branch-label: #{$color-text-primary}; - - // ==================== Background and container ==================== - --mermaid-bg: var(--color-bg-scene); - --mermaid-container-border: #{$border-subtle}; } // ==================== Light theme overrides ==================== @@ -129,75 +116,63 @@ $mermaid-light-highlight-stroke: #334155; .light { // Node styles --mermaid-node-fill: #{$mermaid-light-node-fill}; - --mermaid-node-fill-hover: rgba(15, 23, 42, 0.1); + --mermaid-node-fill-hover: #{$mermaid-light-node-fill-hover}; --mermaid-node-stroke: #{$mermaid-light-node-stroke}; --mermaid-node-stroke-hover: rgba(51, 65, 85, 0.55); --mermaid-node-text: #{$mermaid-light-text-primary}; - + // Edges --mermaid-edge-stroke: #{$mermaid-light-node-stroke}; --mermaid-edge-stroke-hover: rgba(51, 65, 85, 0.5); --mermaid-edge-label-bg: #{$mermaid-light-surface}; - --mermaid-edge-label-border: rgba(100, 116, 139, 0.25); + --mermaid-edge-label-border: #{$mermaid-light-edge-label-border}; --mermaid-edge-label-text: #{$mermaid-light-text-secondary}; --mermaid-arrow-color: #64748b; - + // Subgraph/cluster - --mermaid-cluster-fill: rgba(15, 23, 42, 0.04); - --mermaid-cluster-stroke: rgba(100, 116, 139, 0.25); + --mermaid-cluster-fill: #{$mermaid-light-node-fill}; + --mermaid-cluster-stroke: #{$mermaid-light-edge-label-border}; --mermaid-cluster-text: #{$mermaid-light-text-secondary}; - + // Highlight - slate (matches light theme focus / accent) --mermaid-highlight-stroke: #{$mermaid-light-highlight-stroke}; --mermaid-highlight-glow: drop-shadow(0 0 6px rgba(15, 23, 42, 0.18)); - --mermaid-selected-stroke: #{$mermaid-light-text-primary}; - + // Interaction effects --mermaid-hover-filter: brightness(0.97); - + // Sequence diagram --mermaid-actor-fill: #{$mermaid-light-node-fill}; --mermaid-actor-stroke: #{$mermaid-light-node-stroke}; --mermaid-actor-text: #{$mermaid-light-text-primary}; --mermaid-signal-stroke: #{$mermaid-light-node-stroke}; --mermaid-signal-text: #{$mermaid-light-text-primary}; - --mermaid-note-fill: rgba(184, 134, 58, 0.12); - --mermaid-note-stroke: rgba(192, 140, 66, 0.4); - --mermaid-note-text: #92400e; - --mermaid-activation-fill: rgba(15, 23, 42, 0.1); + --mermaid-note-fill: color-mix(in srgb, var(--color-warning) 12%, transparent); + --mermaid-note-stroke: color-mix(in srgb, var(--color-warning) 40%, transparent); + --mermaid-note-text: #{$mermaid-light-note-text}; + --mermaid-activation-fill: #{$mermaid-light-node-fill-hover}; --mermaid-activation-stroke: rgba(51, 65, 85, 0.35); - + // Gantt chart --mermaid-section-fill: #{$mermaid-light-surface}; - --mermaid-section-alt-fill: #f0f1f3; - --mermaid-grid-stroke: rgba(100, 116, 139, 0.15); + --mermaid-section-alt-fill: #{$mermaid-light-section-alt-fill}; + --mermaid-grid-stroke: color-mix(in srgb, var(--color-text-muted) 15%, transparent); --mermaid-task-text: #{$mermaid-light-text-primary}; - --mermaid-done-fill: rgba(91, 154, 111, 0.15); - --mermaid-done-stroke: #5b9a6f; - --mermaid-active-fill: rgba(15, 23, 42, 0.08); + --mermaid-done-fill: var(--color-success-bg); + --mermaid-done-stroke: var(--color-success); + --mermaid-active-fill: #{$mermaid-light-node-fill-hover}; --mermaid-active-stroke: #{$mermaid-light-highlight-stroke}; - --mermaid-crit-fill: rgba(194, 101, 101, 0.15); - --mermaid-crit-stroke: #c26565; - + --mermaid-crit-fill: var(--color-error-bg); + --mermaid-crit-stroke: var(--color-error); + // Pie chart - cohesive colors. --mermaid-pie-stroke: #{$mermaid-light-surface}; --mermaid-pie-title-text: #{$mermaid-light-text-primary}; --mermaid-pie-legend-text: #{$mermaid-light-text-secondary}; --mermaid-pie-1: #{$mermaid-light-text-secondary}; - + // Class diagram - --mermaid-class-fill: #{$mermaid-light-node-fill}; - --mermaid-class-stroke: #{$mermaid-light-node-stroke}; --mermaid-class-text: #{$mermaid-light-text-primary}; - --mermaid-class-divider: rgba(100, 116, 139, 0.2); - - // Git diagram - --mermaid-commit-text: #{$mermaid-light-text-primary}; - --mermaid-branch-label: #{$mermaid-light-text-primary}; - - // Background - --mermaid-bg: var(--color-bg-scene); - --mermaid-container-border: rgba(100, 116, 139, 0.18); } // ==================== Mixins ==================== diff --git a/src/web-ui/src/tools/mermaid-editor/theme/mermaidThemeFallbacks.test.ts b/src/web-ui/src/tools/mermaid-editor/theme/mermaidThemeFallbacks.test.ts index f5e701ae22..3fd2d9224c 100644 --- a/src/web-ui/src/tools/mermaid-editor/theme/mermaidThemeFallbacks.test.ts +++ b/src/web-ui/src/tools/mermaid-editor/theme/mermaidThemeFallbacks.test.ts @@ -12,7 +12,7 @@ describe('mermaid theme fallback palette', () => { expect(getMermaidThemeFallback('dark', 'nodeFill')).toBe('#1c1e23'); expect(getMermaidThemeFallback('light', 'nodeFill')).toBe('#e8eaef'); expect(getMermaidThemeFallback('dark', 'nodeText')).toBe('#e0e2e8'); - expect(getMermaidThemeFallback('light', 'nodeText')).toBe('#1f2937'); + expect(getMermaidThemeFallback('light', 'nodeText')).toBe('#1e293b'); }); it('returns paired fallbacks for CSS variable resolution without duplicating call-site literals', () => { @@ -33,6 +33,16 @@ describe('mermaid theme fallback palette', () => { }); }); + it('merges indistinguishable Mermaid fallback colors without removing semantic roles', () => { + expect(getMermaidThemeFallback('light', 'nodeFillHover')).toBe('#e0e2e8'); + expect(getMermaidThemeFallback('dark', 'edgeLabelBorderHover')).toBe( + getMermaidThemeFallback('dark', 'nodeStroke') + ); + expect(getMermaidThemeFallback('dark', 'textMuted')).toBe( + getMermaidThemeFallback('dark', 'nodeStrokeHover') + ); + }); + it('keeps every theme mode on the same fallback key contract', () => { const fallbackKeys = Object.keys(MERMAID_THEME_FALLBACKS.dark).sort(); diff --git a/src/web-ui/src/tools/mermaid-editor/theme/mermaidThemeFallbacks.ts b/src/web-ui/src/tools/mermaid-editor/theme/mermaidThemeFallbacks.ts index cc92d503a2..bed6885ef2 100644 --- a/src/web-ui/src/tools/mermaid-editor/theme/mermaidThemeFallbacks.ts +++ b/src/web-ui/src/tools/mermaid-editor/theme/mermaidThemeFallbacks.ts @@ -2,114 +2,141 @@ export const MERMAID_THEME_MODES = ['dark', 'light'] as const; export type MermaidThemeMode = typeof MERMAID_THEME_MODES[number]; +const DARK_NODE_FILL = '#1c1e23'; +const DARK_NODE_FILL_HOVER = '#262830'; +const DARK_NODE_TEXT = '#e0e2e8'; +const DARK_NODE_STROKE = '#5a5e6a'; +const DARK_NODE_BORDER = '#4a4e58'; +const DARK_NODE_STROKE_SUBTLE = '#3a3e48'; +const DARK_NODE_STROKE_HOVER = '#6a6e7a'; +const DARK_CLUSTER_TEXT = '#9ca3af'; +const DARK_INFO = '#78a8d8'; +const DARK_ERROR = '#e87878'; +const DARK_ERROR_FILL = 'rgba(232, 120, 120, 0.15)'; + +const LIGHT_NODE_FILL = '#e8eaef'; +const LIGHT_NODE_FILL_HOVER = '#e0e2e8'; +const LIGHT_NODE_STROKE = '#9ca3af'; +const LIGHT_NODE_STROKE_MUTED = '#d1d5db'; +const LIGHT_NODE_STROKE_HOVER = '#64748b'; +const MERMAID_SOFT_LIGHT_SURFACE = '#f3f4f6'; +const LIGHT_EDGE_LABEL_BG = MERMAID_SOFT_LIGHT_SURFACE; +const LIGHT_SECTION_ALT_FILL = LIGHT_NODE_FILL_HOVER; +const LIGHT_TITLE_TEXT = '#111827'; +const LIGHT_HIGHLIGHT_STROKE = '#334155'; +const LIGHT_ERROR = '#dc2626'; +const LIGHT_WARNING = '#f59e0b'; +const LIGHT_CLUSTER_FILL = 'rgba(229, 231, 235, 0.7)'; +const LIGHT_ERROR_FILL = 'rgba(239, 68, 68, 0.15)'; + export const MERMAID_THEME_FALLBACKS = { dark: { - nodeFill: '#1c1e23', + nodeFill: DARK_NODE_FILL, nodeFillRuntime: 'rgba(28, 30, 35, 0.9)', - nodeFillHover: '#262830', + nodeFillHover: DARK_NODE_FILL_HOVER, nodeFillHoverRuntime: 'rgba(38, 40, 48, 0.95)', - nodeText: '#e0e2e8', - nodeTextStrong: '#f0f2f8', - nodeStroke: '#5a5e6a', - nodeBorder: '#4a4e58', - nodeStrokeMuted: '#4a4e58', - nodeStrokeSubtle: '#3a3e48', - nodeStrokeHover: '#6a6e7a', + nodeText: DARK_NODE_TEXT, + nodeTextStrong: MERMAID_SOFT_LIGHT_SURFACE, + nodeStroke: DARK_NODE_STROKE, + nodeBorder: DARK_NODE_BORDER, + nodeStrokeMuted: DARK_NODE_BORDER, + nodeStrokeSubtle: DARK_NODE_STROKE_SUBTLE, + nodeStrokeHover: DARK_NODE_STROKE_HOVER, nodeStrokeHoverStrong: '#8a8e9a', - edgeStroke: '#5a5e6a', - edgeLabelBorderHover: '#5a5e68', + edgeStroke: DARK_NODE_STROKE, + edgeLabelBorderHover: DARK_NODE_STROKE, clusterFill: '#16181c', clusterFillRuntime: 'rgba(24, 26, 30, 0.6)', clusterFillHover: 'rgba(34, 36, 42, 0.7)', - clusterText: '#9a9ea8', - textMuted: '#6a6e78', + clusterText: DARK_CLUSTER_TEXT, + textMuted: DARK_NODE_STROKE_HOVER, arrow: '#7a7e8a', - edgeLabelBg: '#1a1c20', - edgeLabelBgStrong: '#262830', + edgeLabelBg: DARK_NODE_FILL, + edgeLabelBgStrong: DARK_NODE_FILL_HOVER, edgeLabelBgRuntime: 'rgba(26, 28, 32, 0.95)', - edgeLabelBgHover: 'rgba(36, 38, 45, 0.98)', - noteFill: '#222428', - noteText: '#9a9ea8', - noteStroke: '#4a4e58', - activationFill: '#2a2c32', - activationStroke: '#5a5e6a', - sectionFill: '#1c1e23', - sectionAltFill: '#262830', - gridStroke: '#3a3e48', + edgeLabelBgHover: DARK_NODE_FILL_HOVER, + noteFill: DARK_NODE_FILL_HOVER, + noteText: DARK_CLUSTER_TEXT, + noteStroke: DARK_NODE_BORDER, + activationFill: DARK_NODE_FILL_HOVER, + activationStroke: DARK_NODE_STROKE, + sectionFill: DARK_NODE_FILL, + sectionAltFill: DARK_NODE_FILL_HOVER, + gridStroke: DARK_NODE_STROKE_SUBTLE, doneFill: 'rgba(109, 212, 160, 0.15)', doneStroke: '#6dd4a0', activeFill: 'rgba(120, 168, 216, 0.15)', - activeStroke: '#78a8d8', - critFill: 'rgba(232, 120, 120, 0.15)', - critStroke: '#e87878', + activeStroke: DARK_INFO, + critFill: DARK_ERROR_FILL, + critStroke: DARK_ERROR, warning: '#e8b060', - info: '#78a8d8', - taskClickableInfo: '#78a8d8', + info: DARK_INFO, + taskClickableInfo: DARK_INFO, pie5: '#a090d8', pie6: '#d890b8', pie7: '#68c8d8', pie8: '#a8d868', - pieTitleText: '#e0e2e8', - pieLegendText: '#9a9ea8', - pieStroke: '#1c1e23', - errorFill: 'rgba(232, 120, 120, 0.12)', - error: '#e87878', + pieTitleText: DARK_NODE_TEXT, + pieLegendText: DARK_CLUSTER_TEXT, + pieStroke: DARK_NODE_FILL, + errorFill: DARK_ERROR_FILL, + error: DARK_ERROR, highlightStroke: '#a8acb8', highlightGlow: 'drop-shadow(0 0 6px rgba(168, 172, 184, 0.4))', highlightGlowStrong: 'drop-shadow(0 0 10px rgba(168, 172, 184, 0.5))', }, light: { - nodeFill: '#e8eaef', - nodeFillRuntime: '#e8eaef', - nodeFillHover: '#dfe2e8', - nodeFillHoverRuntime: '#dfe2e8', - nodeText: '#1f2937', - nodeTextStrong: '#111827', - nodeStroke: '#9ca3af', - nodeBorder: '#9ca3af', - nodeStrokeMuted: '#d1d5db', - nodeStrokeSubtle: '#d1d5db', - nodeStrokeHover: '#6b7280', - nodeStrokeHoverStrong: '#6b7280', - edgeStroke: '#9ca3af', - edgeLabelBorderHover: '#9ca3af', - clusterFill: 'rgba(229, 231, 235, 0.7)', - clusterFillRuntime: 'rgba(229, 231, 235, 0.7)', + nodeFill: LIGHT_NODE_FILL, + nodeFillRuntime: LIGHT_NODE_FILL, + nodeFillHover: LIGHT_NODE_FILL_HOVER, + nodeFillHoverRuntime: LIGHT_NODE_FILL_HOVER, + nodeText: '#1e293b', + nodeTextStrong: LIGHT_TITLE_TEXT, + nodeStroke: LIGHT_NODE_STROKE, + nodeBorder: LIGHT_NODE_STROKE, + nodeStrokeMuted: LIGHT_NODE_STROKE_MUTED, + nodeStrokeSubtle: LIGHT_NODE_STROKE_MUTED, + nodeStrokeHover: LIGHT_NODE_STROKE_HOVER, + nodeStrokeHoverStrong: LIGHT_NODE_STROKE_HOVER, + edgeStroke: LIGHT_NODE_STROKE, + edgeLabelBorderHover: LIGHT_NODE_STROKE, + clusterFill: LIGHT_CLUSTER_FILL, + clusterFillRuntime: LIGHT_CLUSTER_FILL, clusterFillHover: 'rgba(209, 213, 219, 0.8)', - clusterText: '#4b5563', - textMuted: '#6b7280', - arrow: '#6b7280', - edgeLabelBg: '#f3f4f6', - edgeLabelBgStrong: '#f3f4f6', - edgeLabelBgRuntime: '#f3f4f6', - edgeLabelBgHover: '#e5e7eb', + clusterText: '#475569', + textMuted: LIGHT_NODE_STROKE_HOVER, + arrow: LIGHT_NODE_STROKE_HOVER, + edgeLabelBg: LIGHT_EDGE_LABEL_BG, + edgeLabelBgStrong: LIGHT_EDGE_LABEL_BG, + edgeLabelBgRuntime: LIGHT_EDGE_LABEL_BG, + edgeLabelBgHover: LIGHT_SECTION_ALT_FILL, noteFill: '#fef3c7', noteText: '#92400e', noteStroke: '#f59e0b', activationFill: 'rgba(147, 197, 253, 0.25)', activationStroke: '#93c5fd', - sectionFill: '#f3f4f6', - sectionAltFill: '#e5e7eb', + sectionFill: LIGHT_EDGE_LABEL_BG, + sectionAltFill: LIGHT_SECTION_ALT_FILL, gridStroke: 'rgba(156, 163, 175, 0.3)', doneFill: 'rgba(34, 197, 94, 0.2)', doneStroke: '#16a34a', - activeFill: 'rgba(15, 23, 42, 0.08)', - activeStroke: '#334155', - critFill: 'rgba(239, 68, 68, 0.2)', - critStroke: '#dc2626', - warning: '#f59e0b', + activeFill: 'rgba(15, 23, 42, 0.1)', + activeStroke: LIGHT_HIGHLIGHT_STROKE, + critFill: LIGHT_ERROR_FILL, + critStroke: LIGHT_ERROR, + warning: LIGHT_WARNING, info: '#64748b', taskClickableInfo: '#475569', pie5: '#8b5cf6', pie6: '#ec4899', pie7: '#06b6d4', pie8: '#84cc16', - pieTitleText: '#111827', - pieLegendText: '#374151', - pieStroke: '#f9fafb', - errorFill: 'rgba(239, 68, 68, 0.15)', - error: '#dc2626', - highlightStroke: '#334155', + pieTitleText: LIGHT_TITLE_TEXT, + pieLegendText: LIGHT_HIGHLIGHT_STROKE, + pieStroke: MERMAID_SOFT_LIGHT_SURFACE, + errorFill: LIGHT_ERROR_FILL, + error: LIGHT_ERROR, + highlightStroke: LIGHT_HIGHLIGHT_STROKE, highlightGlow: 'drop-shadow(0 0 6px rgba(15, 23, 42, 0.18))', highlightGlowStrong: 'drop-shadow(0 0 10px rgba(15, 23, 42, 0.22))', }, diff --git a/src/web-ui/src/tools/terminal/components/ConnectedTerminal.tsx b/src/web-ui/src/tools/terminal/components/ConnectedTerminal.tsx index 5a39850818..b53c164280 100644 --- a/src/web-ui/src/tools/terminal/components/ConnectedTerminal.tsx +++ b/src/web-ui/src/tools/terminal/components/ConnectedTerminal.tsx @@ -11,6 +11,7 @@ import { registerTerminalActions, unregisterTerminalActions } from '../services/ import { POWERSHELL_READLINE_PASTE_SEQUENCE, ResizeRepaintGuard, + TerminalInputQueue, createResizeRepaintScreenSnapshot, resolveTerminalPaste, shouldUsePowerShellReadlinePaste, @@ -281,14 +282,31 @@ const ConnectedTerminal: React.FC = memo(({ onReplay: handleReplay, }); + // Keep latest write in a ref so the input queue closure stays stable without + // recreating the queue on every render. + const writeRef = useRef(write); + writeRef.current = write; + + // Coalesce rapid keystrokes into batched, sequentially-ordered IPC writes. + // Without this, each keystroke fires a separate fire-and-forget + // `invoke('terminal_write')`, which on macOS can lose characters due to IPC + // latency and lack of ordering guarantees for concurrent async command + // handlers. The queue buffers input and flushes it as a single batched write, + // with only one flush in flight at a time. + const inputQueueRef = useRef(null); + if (!inputQueueRef.current) { + inputQueueRef.current = new TerminalInputQueue( + (data) => writeRef.current(data), + (err) => log.error('Write failed', { sessionId, error: err }), + ); + } + const handleData = useCallback((data: string) => { if (!isExited) { resizeRepaintGuardRef.current.clear(); - write(data).catch(err => { - log.error('Write failed', { sessionId, error: err }); - }); + inputQueueRef.current?.enqueue(data); } - }, [write, isExited, sessionId]); + }, [isExited]); const handleResize = useCallback((cols: number, rows: number) => { const lastSize = lastSentSizeRef.current; @@ -351,9 +369,9 @@ const ConnectedTerminal: React.FC = memo(({ return false; } - await write(POWERSHELL_READLINE_PASTE_SEQUENCE); + inputQueueRef.current?.enqueue(POWERSHELL_READLINE_PASTE_SEQUENCE); return true; - }, [initialSession?.shellType, isExited, session?.shellType, write]); + }, [initialSession?.shellType, isExited, session?.shellType]); // Handle paste with VS Code-style multi-line safety policy. const handlePaste = useCallback(async ( @@ -397,6 +415,13 @@ const ConnectedTerminal: React.FC = memo(({ } }, [session]); + // Discard pending input when the terminal exits to avoid cascading write errors. + useEffect(() => { + if (isExited) { + inputQueueRef.current?.clear(); + } + }, [isExited]); + const terminalId = `terminal-${sessionId}`; useEffect(() => { diff --git a/src/web-ui/src/tools/terminal/components/Terminal.tsx b/src/web-ui/src/tools/terminal/components/Terminal.tsx index 401c33e7fb..68008c615e 100644 --- a/src/web-ui/src/tools/terminal/components/Terminal.tsx +++ b/src/web-ui/src/tools/terminal/components/Terminal.tsx @@ -245,6 +245,12 @@ const Terminal = forwardRef(({ const onPasteRef = useRef(onPaste); const resizeSuspendedRef = useRef(resizeSuspended); const pendingFitAfterSuspendRef = useRef(false); + // Track key-pressed state to mirror xterm's internal _keyDownSeen flag. + // Used by the input-event safety net (see below). + const keyDownSeenRef = useRef(false); + // Track whether keypress already handled the character, mirroring xterm's + // _keyPressHandled, to avoid duplicates in the safety net. + const keyPressHandledRef = useRef(false); const [isReady, setIsReady] = useState(false); const currentTheme = themeService.getCurrentTheme(); const initialFontWeights = getXtermFontWeights(currentTheme.type); @@ -658,7 +664,16 @@ const Terminal = forwardRef(({ // Intercept paste (Ctrl+V / Ctrl+Shift+V) so callers can apply the same // policy as context-menu paste before xterm normalizes/sends the data. + // Also track key-pressed state and bypass keyCode 229 for the input-event + // safety net below. terminal.attachCustomKeyEventHandler((event: KeyboardEvent) => { + if (event.type === 'keydown') { + keyDownSeenRef.current = true; + } else if (event.type === 'keyup') { + keyDownSeenRef.current = false; + keyPressHandledRef.current = false; + } + if (event.type === 'keydown' && event.ctrlKey && (event.key === 'v' || event.key === 'V')) { event.preventDefault(); @@ -686,10 +701,62 @@ const Terminal = forwardRef(({ return false; } - + + // When an IME is active (even in ASCII mode), keydown events may carry + // keyCode 229. xterm.js delegates these to _handleAnyTextareaChanges + // (setTimeout(0) — racy during key rollover) and _inputEvent (skipped + // when _keyDownSeen is true). Both paths can lose the character. + // + // Bypass xterm entirely for keyCode 229: return false without + // preventDefault(), so the browser inserts the character into the + // textarea and fires an input event. The safety-net listener below + // catches that event synchronously — no setTimeout race, no duplicates. + // During active IME composition, compositionstart/compositionend events + // still fire and xterm handles them normally; this only affects the + // non-composition passthrough path. + if (event.type === 'keydown' && event.keyCode === 229) { + return false; + } + return true; }); + // Input-event safety net for key rollover with an active IME. + // + // Because we bypass keyCode 229 in the custom key handler above, xterm's + // _handleAnyTextareaChanges (setTimeout(0)) is never called for those keys. + // Instead, the browser inserts the character and fires an input event. + // + // xterm's own _inputEvent handler skips events where (composed && keyDownSeen) + // — exactly the key-rollover case. This listener catches those skipped + // insertText events and forwards them via onData, preventing character loss. + // It only fires when xterm would skip (composed + keyDownSeen) and keypress + // didn't already handle the character, so it never causes duplicates. + const textareaEl = container.querySelector('.xterm-helper-textarea'); + let textareaKeyPressHandler: (() => void) | null = null; + let textareaInputHandler: ((ev: Event) => void) | null = null; + if (textareaEl) { + textareaKeyPressHandler = () => { + keyPressHandledRef.current = true; + }; + textareaInputHandler = (ev: Event) => { + const inputEvent = ev as InputEvent; + if ( + inputEvent.data && + inputEvent.inputType === 'insertText' && + inputEvent.composed === true && + keyDownSeenRef.current && + !keyPressHandledRef.current + ) { + // xterm's _inputEvent will skip this (composed + keyDownSeen). + // Forward the character to prevent loss during key rollover. + onDataRef.current?.(inputEvent.data); + } + }; + textareaEl.addEventListener('keypress', textareaKeyPressHandler); + textareaEl.addEventListener('input', textareaInputHandler, true); + } + const resizeObserver = new ResizeObserver(() => { requestAnimationFrame(() => { fitRef.current(false); @@ -748,6 +815,12 @@ const Terminal = forwardRef(({ binaryDisposable.dispose(); titleDisposable.dispose(); container.removeEventListener('paste', handleNativePaste, true); + if (textareaEl && textareaKeyPressHandler) { + textareaEl.removeEventListener('keypress', textareaKeyPressHandler); + } + if (textareaEl && textareaInputHandler) { + textareaEl.removeEventListener('input', textareaInputHandler, true); + } resizeObserver.disconnect(); intersectionObserver.disconnect(); fontLoadCancelled = true; diff --git a/src/web-ui/src/tools/terminal/utils/TerminalInputQueue.test.ts b/src/web-ui/src/tools/terminal/utils/TerminalInputQueue.test.ts new file mode 100644 index 0000000000..013ef6581e --- /dev/null +++ b/src/web-ui/src/tools/terminal/utils/TerminalInputQueue.test.ts @@ -0,0 +1,148 @@ +import { describe, expect, it, vi } from 'vitest'; + +import { TerminalInputQueue } from './TerminalInputQueue'; + +/** Flush all pending microtasks by waiting for the next macrotask. */ +function flushMicrotasks(): Promise { + return new Promise(resolve => setTimeout(resolve, 0)); +} + +describe('TerminalInputQueue', () => { + it('batches multiple synchronous enqueue calls into a single write', async () => { + const write = vi.fn((_data: string): Promise => Promise.resolve()); + const onError = vi.fn(); + const queue = new TerminalInputQueue(write, onError); + + queue.enqueue('1'); + queue.enqueue('2'); + queue.enqueue('3'); + + // Microtask hasn't run yet — no writes should have occurred. + expect(write).not.toHaveBeenCalled(); + + await flushMicrotasks(); + + expect(write).toHaveBeenCalledTimes(1); + expect(write).toHaveBeenCalledWith('123'); + expect(onError).not.toHaveBeenCalled(); + }); + + it('does not start a second write while the first is in flight', async () => { + let resolveFirst: () => void = () => {}; + const write = vi.fn((data: string): Promise => { + if (data === 'first') { + return new Promise(resolve => { + resolveFirst = resolve; + }); + } + return Promise.resolve(); + }); + const onError = vi.fn(); + const queue = new TerminalInputQueue(write, onError); + + queue.enqueue('first'); + await flushMicrotasks(); + + expect(write).toHaveBeenCalledTimes(1); + expect(write).toHaveBeenLastCalledWith('first'); + + // Enqueue more data while the first write is still in flight. + queue.enqueue('A'); + queue.enqueue('B'); + + expect(write).toHaveBeenCalledTimes(1); // Still only one call. + + // Resolve the first write — the queued data should flush as a single batch. + resolveFirst(); + await flushMicrotasks(); + + expect(write).toHaveBeenCalledTimes(2); + expect(write).toHaveBeenLastCalledWith('AB'); + }); + + it('preserves ordering across multiple flush cycles', async () => { + const writtenData: string[] = []; + const write = vi.fn((data: string): Promise => { + writtenData.push(data); + return Promise.resolve(); + }); + const onError = vi.fn(); + const queue = new TerminalInputQueue(write, onError); + + // First batch + queue.enqueue('a'); + queue.enqueue('b'); + await flushMicrotasks(); + + // Second batch + queue.enqueue('c'); + queue.enqueue('d'); + await flushMicrotasks(); + + expect(writtenData).toEqual(['ab', 'cd']); + }); + + it('reports write errors to onError and continues processing', async () => { + const write = vi.fn((_data: string): Promise => Promise.resolve()); + write.mockRejectedValueOnce(new Error('boom')); + const onError = vi.fn(); + const queue = new TerminalInputQueue(write, onError); + + queue.enqueue('data'); + await flushMicrotasks(); + + expect(onError).toHaveBeenCalledTimes(1); + expect((onError.mock.calls[0][0] as Error).message).toBe('boom'); + + // Queue should still accept new data after an error. + queue.enqueue('more'); + await flushMicrotasks(); + + expect(write).toHaveBeenCalledTimes(2); + expect(write).toHaveBeenLastCalledWith('more'); + expect(onError).toHaveBeenCalledTimes(1); + }); + + it('clear() discards buffered data that has not been flushed', async () => { + const write = vi.fn((_data: string): Promise => Promise.resolve()); + const onError = vi.fn(); + const queue = new TerminalInputQueue(write, onError); + + queue.enqueue('pending'); + queue.clear(); + + await flushMicrotasks(); + + // The microtask still runs drain(), but the buffer was cleared, + // so data is empty and write should not be called. + expect(write).not.toHaveBeenCalled(); + }); + + it('clear() does not affect data already mid-flush', async () => { + let resolveWrite: () => void = () => {}; + const write = vi.fn((_data: string): Promise => + new Promise(resolve => { + resolveWrite = resolve; + }), + ); + const onError = vi.fn(); + const queue = new TerminalInputQueue(write, onError); + + queue.enqueue('in-flight'); + await flushMicrotasks(); + + expect(write).toHaveBeenCalledWith('in-flight'); + + // Data that arrives after the flush started but before it completes. + queue.enqueue('queued'); + // Clear should discard 'queued' but not affect 'in-flight'. + queue.clear(); + + resolveWrite(); + await flushMicrotasks(); + + // Only 'in-flight' was written; 'queued' was discarded. + expect(write).toHaveBeenCalledTimes(1); + expect(write).toHaveBeenLastCalledWith('in-flight'); + }); +}); diff --git a/src/web-ui/src/tools/terminal/utils/TerminalInputQueue.ts b/src/web-ui/src/tools/terminal/utils/TerminalInputQueue.ts new file mode 100644 index 0000000000..09de386627 --- /dev/null +++ b/src/web-ui/src/tools/terminal/utils/TerminalInputQueue.ts @@ -0,0 +1,82 @@ +/** + * Coalesces terminal input into batched, sequentially-ordered writes. + * + * Problem: xterm.js fires `onData` once per keystroke. If each call triggers a + * separate fire-and-forget `invoke('terminal_write')`, rapid typing creates a + * burst of concurrent IPC calls. On macOS (WKWebView) this can cause character + * loss because: + * - concurrent async command handlers are not guaranteed FIFO ordering after + * the `get_or_init_api` mutex releases, + * - WKWebView IPC has higher latency and bursts of concurrent invokes can + * interfere with each other. + * + * Solution: buffer incoming data and flush it as a single batched write. Only + * one flush is in flight at a time, so ordering is guaranteed and IPC overhead + * is reduced. The first keystroke is dispatched via a microtask (near-zero + * latency); subsequent keystrokes that arrive while a flush is in flight are + * automatically coalesced into the next flush. + * + * IME safety: xterm.js fires `onData` only after IME composition completes, so + * this queue never interferes with input-method candidate selection or the + * Enter-key guard in chat input components. + */ +export class TerminalInputQueue { + private buffer = ''; + private flushing = false; + private readonly write: (data: string) => Promise; + private readonly onError: (error: unknown) => void; + + constructor( + write: (data: string) => Promise, + onError: (error: unknown) => void, + ) { + this.write = write; + this.onError = onError; + } + + /** + * Append data to the buffer and schedule a flush if one is not already in + * progress. + */ + enqueue(data: string): void { + this.buffer += data; + if (!this.flushing) { + this.flushing = true; + queueMicrotask(() => { + void this.drain(); + }); + } + } + + /** + * Flush the current buffer contents to the write function. After the write + * completes, if more data arrived during the flush, drain again; otherwise + * mark the queue as idle. + */ + private async drain(): Promise { + const data = this.buffer; + this.buffer = ''; + try { + if (data) { + await this.write(data); + } + } catch (err) { + this.onError(err); + } finally { + if (this.buffer) { + // More data arrived during the flush — drain again. + void this.drain(); + } else { + this.flushing = false; + } + } + } + + /** + * Discard any buffered data that has not been flushed yet. Data that is + * already mid-flush is unaffected. + */ + clear(): void { + this.buffer = ''; + } +} diff --git a/src/web-ui/src/tools/terminal/utils/index.ts b/src/web-ui/src/tools/terminal/utils/index.ts index ee06881a96..e841e0d4f7 100644 --- a/src/web-ui/src/tools/terminal/utils/index.ts +++ b/src/web-ui/src/tools/terminal/utils/index.ts @@ -2,6 +2,7 @@ * Terminal utilities. */ +export { TerminalInputQueue } from './TerminalInputQueue'; export { TerminalResizeDebouncer } from './TerminalResizeDebouncer'; export type { ResizeCallback, ResizeDebounceOptions } from './TerminalResizeDebouncer'; export { ResizeRepaintGuard, createResizeRepaintScreenSnapshot } from './resizeRepaintGuard'; diff --git a/src/web-ui/src/tools/terminal/utils/xtermTheme.ts b/src/web-ui/src/tools/terminal/utils/xtermTheme.ts index c3a9681ccc..a04e602d26 100644 --- a/src/web-ui/src/tools/terminal/utils/xtermTheme.ts +++ b/src/web-ui/src/tools/terminal/utils/xtermTheme.ts @@ -117,7 +117,7 @@ export function buildXtermTheme( cursor: theme.colors.text.primary, cursorAccent: theme.colors.background.secondary, selectionBackground: isDark ? 'rgba(255, 255, 255, 0.3)' : 'rgba(173, 214, 255, 0.45)', - selectionInactiveBackground: isDark ? 'rgba(255, 255, 255, 0.16)' : 'rgba(173, 214, 255, 0.25)', + selectionInactiveBackground: isDark ? 'rgba(255, 255, 255, 0.15)' : 'rgba(173, 214, 255, 0.25)', ...getXtermAnsiPalette(theme.type), ...overrides, }; diff --git a/src/web-ui/src/tools/workspace/components/WorkspaceManager.css b/src/web-ui/src/tools/workspace/components/WorkspaceManager.css index 688ef95548..7278315d02 100644 --- a/src/web-ui/src/tools/workspace/components/WorkspaceManager.css +++ b/src/web-ui/src/tools/workspace/components/WorkspaceManager.css @@ -20,8 +20,8 @@ -webkit-backdrop-filter: blur(12px) saturate(1.05); box-shadow: 0 8px 32px color-mix(in srgb, var(--color-error) 12%, transparent), - 0 4px 16px var(--color-overlay-black-06), - inset 0 1px 0 var(--color-overlay-white-10); + 0 4px 16px var(--color-overlay-black-08), + inset 0 1px 0 var(--color-overlay-white-12); } /* Section headings */ @@ -49,9 +49,9 @@ -webkit-backdrop-filter: blur(12px) saturate(1.05); box-shadow: 0 8px 32px color-mix(in srgb, var(--color-accent-500) 12%, transparent), - 0 4px 16px var(--color-overlay-black-06), - inset 0 1px 0 var(--color-overlay-white-10), - inset 0 -1px 0 var(--color-overlay-white-05); + 0 4px 16px var(--color-overlay-black-08), + inset 0 1px 0 var(--color-overlay-white-12), + inset 0 -1px 0 var(--color-overlay-white-08); transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); animation: slideInUp 0.4s ease-out; } @@ -64,7 +64,7 @@ right: 0; bottom: 0; background: linear-gradient(135deg, - var(--color-overlay-white-02) 0%, + var(--color-overlay-white-04) 0%, transparent 50%, color-mix(in srgb, var(--color-static-white) 1%, transparent) 100%); pointer-events: none; @@ -257,8 +257,8 @@ .workspace-type { font-size: 12px; padding: 2px 8px; - background-color: var(--secondary-bg); - color: var(--text-secondary); + background-color: var(--color-bg-secondary); + color: var(--color-text-secondary); border-radius: 4px; text-transform: capitalize; } @@ -268,7 +268,7 @@ align-items: center; gap: 4px; font-size: 12px; - color: var(--text-tertiary); + color: var(--color-text-muted); } .workspace-actions { diff --git a/tests/e2e/scripts/perf-coverage-contract.test.mjs b/tests/e2e/scripts/perf-coverage-contract.test.mjs index 5decbb08be..7998b2a06a 100644 --- a/tests/e2e/scripts/perf-coverage-contract.test.mjs +++ b/tests/e2e/scripts/perf-coverage-contract.test.mjs @@ -28,6 +28,9 @@ test('performance scripts expose focused startup stability and interaction profi const startupRunner = readText('tests/e2e/scripts/run-startup-stability.mjs'); assert.match(startupRunner, /BITFUN_E2E_PERF_STARTUP_ITERATIONS/); + assert.match(startupRunner, /--samples/); + assert.match(startupRunner, /--iterations/); + assert.match(startupRunner, /readIntegerArgOrEnv/); assert.match(startupRunner, /BITFUN_E2E_PERF_STARTUP_MAX_INTERACTIVE_MS/); assert.match(startupRunner, /collects startup timing from the current build/); assert.match(startupRunner, /seenTraceIds/); @@ -65,3 +68,25 @@ test('long session required frame trace samples fail when trace phases are missi assert.match(startupSpec, /measurement\.traceWaitErrors\.length > 0/); assert.match(startupSpec, /measurement\.clickToPostHydrateUsableMs\)\.toBeGreaterThan\(0\)/); }); + +test('long session navigation lookup follows current session nav DOM contract', () => { + const startupSpec = readText('tests/e2e/specs/performance/startup-session-perf.spec.ts'); + + assert.match(startupSpec, /data-testid="nav-session-item"/); + assert.match(startupSpec, /data-testid="nav-session-list-toggle"/); + assert.match(startupSpec, /data-session-nav-toggle-action/); + assert.match(startupSpec, /data-session-id/); +}); + +test('long session interaction matrix isolates user data and avoids active-session preload bias', () => { + const interactionRunner = readText('tests/e2e/scripts/run-long-session-interaction-matrix.mjs'); + + assert.match(interactionRunner, /BITFUN_E2E_STORAGE_ROOT/); + assert.match(interactionRunner, /generate-long-session-fixture\.mjs/); + assert.match(interactionRunner, /BITFUN_E2E_PERF_SESSION_ID/); + assert.match(interactionRunner, /perf-long-session-001/); + assert.match(interactionRunner, /BITFUN_E2E_PERF_RAPID_SWITCH_SESSION_IDS/); + assert.match(interactionRunner, /perf-rapid-c-000/); + assert.match(interactionRunner, /pruneOldPerfRuns/); + assert.match(interactionRunner, /MAX_RETAINED_PERF_RUNS/); +}); diff --git a/tests/e2e/scripts/run-long-session-interaction-matrix.mjs b/tests/e2e/scripts/run-long-session-interaction-matrix.mjs index 9ba1ddbac2..00802a7fdf 100644 --- a/tests/e2e/scripts/run-long-session-interaction-matrix.mjs +++ b/tests/e2e/scripts/run-long-session-interaction-matrix.mjs @@ -6,6 +6,16 @@ import { fileURLToPath } from 'node:url'; const SCRIPT_DIR = path.dirname(fileURLToPath(import.meta.url)); const ROOT = path.resolve(SCRIPT_DIR, '..', '..', '..'); const REPORT_DIR = path.join(ROOT, 'tests', 'e2e', 'reports', 'performance'); +const PERF_RUN_ROOT = path.join(ROOT, 'tests', 'e2e', '.bitfun', 'perf-runs'); +const FIXTURE_SCRIPT = path.join(ROOT, 'tests', 'e2e', 'scripts', 'generate-long-session-fixture.mjs'); +const DEFAULT_LONG_SESSION_TARGET_ID = 'perf-long-session-001'; +const DEFAULT_RAPID_SWITCH_SESSION_IDS = [ + 'perf-rapid-a-000', + 'perf-rapid-b-000', + 'perf-rapid-c-000', +]; +const MAX_RETAINED_PERF_RUNS = 8; +const matrixRunId = `${new Date().toISOString().replace(/[:.]/g, '-')}-${process.pid}`; const scenarios = { 'first-open': { @@ -94,6 +104,14 @@ function runPnpm(args, options) { }); } +function runNode(args, options) { + return spawnSync(process.execPath, args, { + ...options, + shell: false, + encoding: 'utf8', + }); +} + function runnerStdioOptions() { if (process.env.BITFUN_E2E_PERF_RUNNER_STREAM_LOGS === '1') { return { stdio: 'inherit' }; @@ -132,6 +150,121 @@ function allowMissingReports() { ); } +function safePathSegment(value) { + return String(value).replace(/[^a-zA-Z0-9_.-]/g, '-'); +} + +function assertPathWithin(parent, candidate) { + const relative = path.relative(parent, candidate); + if (relative === '' || (!relative.startsWith('..') && !path.isAbsolute(relative))) { + return; + } + throw new Error(`Refusing to clean path outside performance run root: ${candidate}`); +} + +function pruneOldPerfRuns(maxRuns = MAX_RETAINED_PERF_RUNS) { + if (!fs.existsSync(PERF_RUN_ROOT)) { + return; + } + + const runs = fs + .readdirSync(PERF_RUN_ROOT, { withFileTypes: true }) + .filter(entry => entry.isDirectory()) + .map(entry => { + const fullPath = path.join(PERF_RUN_ROOT, entry.name); + const stat = fs.statSync(fullPath); + return { fullPath, mtimeMs: stat.mtimeMs }; + }) + .sort((left, right) => right.mtimeMs - left.mtimeMs); + + for (const run of runs.slice(maxRuns)) { + assertPathWithin(PERF_RUN_ROOT, run.fullPath); + fs.rmSync(run.fullPath, { recursive: true, force: true }); + } +} + +function runFixture(args, env) { + const result = runNode([FIXTURE_SCRIPT, ...args], { + cwd: ROOT, + env, + }); + if (result.status !== 0) { + throw new Error( + `Failed to generate long-session fixture.\n${outputTail(result)}`, + ); + } +} + +function prepareScenarioRuntime(name, baseEnv) { + const scenarioRoot = path.join(PERF_RUN_ROOT, matrixRunId, safePathSegment(name)); + assertPathWithin(PERF_RUN_ROOT, scenarioRoot); + fs.rmSync(scenarioRoot, { recursive: true, force: true }); + + const storageRoot = path.join(scenarioRoot, 'storage'); + const workspace = path.join(scenarioRoot, 'workspace'); + const homeRoot = path.join(storageRoot, 'home'); + const userRoot = path.join(storageRoot, 'user-root'); + const logRoot = path.join(storageRoot, 'logs'); + fs.mkdirSync(workspace, { recursive: true }); + fs.writeFileSync( + path.join(workspace, 'README.md'), + '# BitFun performance fixture workspace\n', + 'utf8', + ); + + const env = { + ...baseEnv, + BITFUN_E2E_STORAGE_ROOT: storageRoot, + BITFUN_E2E_HOME: homeRoot, + BITFUN_HOME: homeRoot, + BITFUN_E2E_USER_ROOT: userRoot, + BITFUN_USER_ROOT: userRoot, + BITFUN_E2E_LOG_DIR: logRoot, + E2E_TEST_WORKSPACE: workspace, + BITFUN_E2E_PERF_SESSION_ID: DEFAULT_LONG_SESSION_TARGET_ID, + BITFUN_E2E_PERF_RAPID_SWITCH_SESSION_IDS: DEFAULT_RAPID_SWITCH_SESSION_IDS.join(','), + }; + const timestampBase = Date.now(); + + runFixture([ + '--workspace', + workspace, + '--bitfun-home', + homeRoot, + '--bitfun-user-root', + userRoot, + '--session-prefix', + 'perf-long-session', + '--session-count', + '80', + '--long-session-index', + '1', + '--last-active-at-base', + String(timestampBase), + ], env); + + ['perf-rapid-a', 'perf-rapid-b', 'perf-rapid-c'].forEach((prefix, index) => { + runFixture([ + '--workspace', + workspace, + '--bitfun-home', + homeRoot, + '--bitfun-user-root', + userRoot, + '--session-prefix', + prefix, + '--session-count', + '1', + '--long-session-index', + '0', + '--last-active-at-base', + String(timestampBase - 10_000 - index * 1_000), + ], env); + }); + + return env; +} + function newestReport(prefix, startedAtMs) { if (!prefix || !fs.existsSync(REPORT_DIR)) { return null; @@ -203,7 +336,7 @@ function selectedScenarioNames() { function runScenario(name, baseEnv, options) { const scenario = scenarios[name]; const env = { - ...baseEnv, + ...prepareScenarioRuntime(name, baseEnv), ...(scenario.env ?? {}), }; const args = [ @@ -274,6 +407,8 @@ if (hasFlag('--dry-run')) { process.exit(0); } +pruneOldPerfRuns(Math.max(0, MAX_RETAINED_PERF_RUNS - 1)); + const results = names.map(name => runScenario(name, baseEnv, { allowMissingReports: missingReportsAllowed }), ); diff --git a/tests/e2e/scripts/run-startup-stability.mjs b/tests/e2e/scripts/run-startup-stability.mjs index 224e6e02d7..efe85f0258 100644 --- a/tests/e2e/scripts/run-startup-stability.mjs +++ b/tests/e2e/scripts/run-startup-stability.mjs @@ -12,11 +12,15 @@ function readFlag(name) { return process.argv.includes(name); } -function readNumberEnv(name, fallback) { - const raw = process.env[name]; - if (raw === undefined || raw === '') { - return fallback; +function readArgValue(name) { + const index = process.argv.indexOf(name); + if (index < 0) { + return undefined; } + return process.argv[index + 1]; +} + +function parseNonNegativeNumber(name, raw) { const value = Number(raw); if (!Number.isFinite(value) || value < 0) { throw new Error(`${name} must be a non-negative number, got ${raw}`); @@ -24,8 +28,26 @@ function readNumberEnv(name, fallback) { return value; } -function readIntegerEnv(name, fallback) { - return Math.max(1, Math.trunc(readNumberEnv(name, fallback))); +function readNumberEnv(name, fallback) { + const raw = process.env[name]; + if (raw === undefined || raw === '') { + return fallback; + } + return parseNonNegativeNumber(name, raw); +} + +function readNumberArgOrEnv(argNames, envName, fallback) { + for (const argName of argNames) { + const raw = readArgValue(argName); + if (raw !== undefined && raw !== '') { + return parseNonNegativeNumber(argName, raw); + } + } + return readNumberEnv(envName, fallback); +} + +function readIntegerArgOrEnv(argNames, envName, fallback) { + return Math.max(1, Math.trunc(readNumberArgOrEnv(argNames, envName, fallback))); } function shellQuote(value) { @@ -204,7 +226,11 @@ function runStartupIteration(index, total, env, thresholds, seenTraceIds) { }; } -const iterations = readIntegerEnv('BITFUN_E2E_PERF_STARTUP_ITERATIONS', 5); +const iterations = readIntegerArgOrEnv( + ['--samples', '--iterations'], + 'BITFUN_E2E_PERF_STARTUP_ITERATIONS', + 5, +); const thresholds = { interactiveShellReadyMs: readNumberEnv('BITFUN_E2E_PERF_STARTUP_MAX_INTERACTIVE_MS', 5000), firstScriptEvalMs: readNumberEnv('BITFUN_E2E_PERF_STARTUP_MAX_FIRST_SCRIPT_MS', 3000), diff --git a/tests/e2e/specs/l1-chat-turn-navigation-release.spec.ts b/tests/e2e/specs/l1-chat-turn-navigation-release.spec.ts new file mode 100644 index 0000000000..40bdd4a06f --- /dev/null +++ b/tests/e2e/specs/l1-chat-turn-navigation-release.spec.ts @@ -0,0 +1,486 @@ +/** + * Release-compatible regression coverage for header turn navigation. + * + * This spec intentionally avoids Vite-only /src imports so it can run against + * the bundled release-fast desktop app with persisted long-session fixtures. + */ +import { $, browser, expect } from '@wdio/globals'; +import { openWorkspace } from '../helpers/workspace-helper'; +import { saveFailureScreenshot } from '../helpers/screenshot-utils'; + +const SESSION_NAV_ITEM_SELECTOR = + '[data-testid="session-nav-item"], [data-testid="nav-session-item"]'; +const SESSION_NAV_TOGGLE_SELECTOR = + '[data-testid="session-nav-show-more"], [data-testid="nav-session-list-toggle"]'; +const DEFAULT_SESSION_ID = 'release-turn-nav-000'; +const DEFAULT_TARGET_TURN_INDEX = 2; + +type TurnViewportMetrics = { + rootExists: boolean; + scrollerExists: boolean; + targetExists: boolean; + targetVisible: boolean; + scrollTop: number; + scrollHeight: number; + clientHeight: number; + maxScrollTop: number; + targetTop: number | null; + targetBottom: number | null; + scrollerTop: number | null; + scrollerBottom: number | null; + deltaToPinnedTop: number | null; + distanceFromBottom: number | null; + visibleTurnIds: string[]; + activeTurnListText: string | null; +}; + +type SessionShellState = { + historyState: string | null; + contextRestoreState: string | null; + isPartial: boolean; + dialogTurnCount: number; + virtualItemCount: number; + hasPendingHistoryCompletion: boolean; + hasDeferredHistoryProjection: boolean; + latestTurnId: string | null; + turnListOpen: boolean; + turnListTexts: string[]; +}; + +function escapeCssAttributeValue(value: string): string { + return value.replace(/\\/g, '\\\\').replace(/"/g, '\\"'); +} + +async function findSessionItem(sessionId: string): Promise { + const targetSelector = + `[data-testid="session-nav-item"][data-session-id="${escapeCssAttributeValue(sessionId)}"], ` + + `[data-testid="nav-session-item"][data-session-id="${escapeCssAttributeValue(sessionId)}"]`; + + const readVisibleSessionIds = async (): Promise => + browser.execute((selector) => + Array.from(document.querySelectorAll(selector)) + .map(element => element.getAttribute('data-session-id') || '') + .filter(Boolean), + SESSION_NAV_ITEM_SELECTOR); + + const findTarget = async (): Promise => { + const item = await $(targetSelector); + return await item.isExisting() ? item : null; + }; + + const findExpandableToggles = async (): Promise => { + const toggles = await browser.$$(SESSION_NAV_TOGGLE_SELECTOR); + const expandable: WebdriverIO.Element[] = []; + for (const toggle of toggles) { + if ( + !(await toggle.isExisting()) || + !(await toggle.isDisplayed()) || + !(await toggle.isEnabled()) + ) { + continue; + } + + const action = await toggle.getAttribute('data-session-nav-toggle-action').catch(() => null); + if (action === 'show-less') { + continue; + } + expandable.push(toggle); + } + return expandable; + }; + + let lastVisibleSessionIds: string[] = []; + for (let attempt = 0; attempt < 12; attempt += 1) { + const existing = await findTarget(); + if (existing) { + return existing; + } + + lastVisibleSessionIds = await readVisibleSessionIds(); + const toggles = await findExpandableToggles(); + if (toggles.length === 0) { + break; + } + + let clickedAny = false; + for (let toggleIndex = 0; toggleIndex < toggles.length; toggleIndex += 1) { + const item = await findTarget(); + if (item) { + return item; + } + + const currentToggles = await findExpandableToggles(); + const toggle = currentToggles[toggleIndex]; + if (!toggle) { + break; + } + + const beforeCount = lastVisibleSessionIds.length; + clickedAny = true; + await toggle.click(); + await browser.waitUntil(async () => { + if (await findTarget()) { + return true; + } + const ids = await readVisibleSessionIds(); + const nextToggles = await findExpandableToggles(); + return ids.length !== beforeCount || nextToggles.length !== toggles.length; + }, { timeout: 3000, interval: 100 }).catch(() => undefined); + lastVisibleSessionIds = await readVisibleSessionIds(); + } + + if (!clickedAny) { + break; + } + } + + console.log('[ReleaseTurnNav] visible sessions while locating target', JSON.stringify({ + target: sessionId, + visibleSessionIds: lastVisibleSessionIds.slice(0, 40), + visibleSessionCount: lastVisibleSessionIds.length, + })); + return null; +} + +async function isSessionItemActive(item: WebdriverIO.Element): Promise { + const className = await item.getAttribute('class') ?? ''; + return className.split(/\s+/).includes('is-active'); +} + +async function readTurnViewportMetrics(targetTurnId: string): Promise { + return browser.execute((turnId) => { + const root = document.querySelector( + '.modern-flowchat-container__messages .virtual-message-list', + ); + const scroller = root?.querySelector( + '[data-virtuoso-scroller="true"], [data-virtuoso-scroller], .virtual-message-list__static-scroller', + ) ?? null; + const wrappers = Array.from(root?.querySelectorAll( + '.virtual-item-wrapper[data-turn-id]', + ) ?? []); + const target = wrappers.find(element => + element.dataset.turnId === turnId && element.dataset.itemType === 'user-message' + ) ?? null; + const scrollerRect = scroller?.getBoundingClientRect() ?? null; + const targetRect = target?.getBoundingClientRect() ?? null; + const visibleTurnIds = scrollerRect + ? wrappers + .filter(element => { + const rect = element.getBoundingClientRect(); + return rect.bottom > scrollerRect.top && rect.top < scrollerRect.bottom; + }) + .map(element => element.dataset.turnId || '') + .filter(Boolean) + : []; + const targetVisible = Boolean( + scrollerRect && + targetRect && + targetRect.bottom > scrollerRect.top && + targetRect.top < scrollerRect.bottom + ); + const activeTurnListItem = document.querySelector( + '.flowchat-header__turn-list-item--active', + ); + + return { + rootExists: Boolean(root), + scrollerExists: Boolean(scroller), + targetExists: Boolean(target), + targetVisible, + scrollTop: scroller?.scrollTop ?? 0, + scrollHeight: scroller?.scrollHeight ?? 0, + clientHeight: scroller?.clientHeight ?? 0, + maxScrollTop: scroller ? Math.max(0, scroller.scrollHeight - scroller.clientHeight) : 0, + targetTop: targetRect?.top ?? null, + targetBottom: targetRect?.bottom ?? null, + scrollerTop: scrollerRect?.top ?? null, + scrollerBottom: scrollerRect?.bottom ?? null, + deltaToPinnedTop: scrollerRect && targetRect ? targetRect.top - scrollerRect.top : null, + distanceFromBottom: scrollerRect && targetRect ? scrollerRect.bottom - targetRect.bottom : null, + visibleTurnIds: Array.from(new Set(visibleTurnIds)), + activeTurnListText: activeTurnListItem?.textContent?.trim() ?? null, + }; + }, targetTurnId); +} + +async function readSessionShellState(): Promise { + return browser.execute(() => { + const messages = document.querySelector( + '.modern-flowchat-container__messages', + ); + const turnListItems = Array.from(document.querySelectorAll( + '.flowchat-header__turn-list-item', + )); + return { + historyState: messages?.dataset.historyState ?? null, + contextRestoreState: messages?.dataset.contextRestoreState ?? null, + isPartial: messages?.dataset.isPartial === 'true', + dialogTurnCount: Number(messages?.dataset.dialogTurnCount ?? 0), + virtualItemCount: Number(messages?.dataset.virtualItemCount ?? 0), + hasPendingHistoryCompletion: messages?.dataset.hasPendingHistoryCompletion === 'true', + hasDeferredHistoryProjection: messages?.dataset.hasDeferredHistoryProjection === 'true', + latestTurnId: messages?.dataset.latestTurnId || null, + turnListOpen: turnListItems.length > 0, + turnListTexts: turnListItems.map(item => item.textContent?.trim() ?? ''), + }; + }); +} + +async function waitForSessionHydrated(): Promise { + await browser.waitUntil(async () => { + const turnListButton = await $('[data-testid="flowchat-header-turn-list"]'); + if (!(await turnListButton.isExisting()) || !(await turnListButton.isEnabled())) { + return false; + } + const state = await browser.execute(() => { + const root = document.querySelector( + '.modern-flowchat-container__messages .virtual-message-list', + ); + const scroller = root?.querySelector( + '[data-virtuoso-scroller="true"], [data-virtuoso-scroller], .virtual-message-list__static-scroller', + ) ?? null; + const items = root?.querySelectorAll('.virtual-item-wrapper[data-turn-id]') ?? []; + return { + hasRoot: Boolean(root), + hasScroller: Boolean(scroller), + itemCount: items.length, + }; + }); + return state.hasRoot && state.hasScroller && state.itemCount > 0; + }, { + timeout: 30000, + interval: 200, + timeoutMsg: '[ReleaseTurnNav] generated session did not hydrate message list', + }); +} + +async function openHeaderTurnList(): Promise { + const existingItems = await $$('.flowchat-header__turn-list-item'); + if (existingItems.length > 0) { + return; + } + + const turnListButton = await $('[data-testid="flowchat-header-turn-list"]'); + await turnListButton.waitForClickable({ timeout: 10000 }); + await turnListButton.click(); + await browser.waitUntil(async () => { + const items = await $$('.flowchat-header__turn-list-item'); + return items.length > 0; + }, { + timeout: 3000, + interval: 100, + timeoutMsg: '[ReleaseTurnNav] turn list did not open', + }); +} + +async function closeHeaderTurnList(): Promise { + const items = await $$('.flowchat-header__turn-list-item'); + if (items.length === 0) { + return; + } + + const turnListButton = await $('[data-testid="flowchat-header-turn-list"]'); + await turnListButton.click(); + await browser.waitUntil(async () => { + const currentItems = await $$('.flowchat-header__turn-list-item'); + return currentItems.length === 0; + }, { timeout: 3000, interval: 100 }).catch(() => undefined); +} + +async function readHeaderTurnListTexts(): Promise { + await openHeaderTurnList(); + return browser.execute(() => + Array.from(document.querySelectorAll('.flowchat-header__turn-list-item')) + .map(item => item.textContent?.trim() ?? ''), + ); +} + +async function scrollToPreviousHistoryBoundary(): Promise { + await browser.execute(() => { + const scroller = document.querySelector( + '.modern-flowchat-container__messages .virtual-message-list [data-virtuoso-scroller="true"], ' + + '.modern-flowchat-container__messages .virtual-message-list [data-virtuoso-scroller], ' + + '.modern-flowchat-container__messages .virtual-message-list .virtual-message-list__static-scroller', + ); + if (!scroller) { + return; + } + scroller.dispatchEvent(new WheelEvent('wheel', { + bubbles: true, + cancelable: true, + deltaY: -1200, + deltaMode: 0, + })); + scroller.scrollTop = 0; + scroller.dispatchEvent(new Event('scroll', { bubbles: true })); + }); +} + +async function revealHistoryUntilTurnListContains(targetTitle: string): Promise<{ + attempts: number; + finalState: SessionShellState; +}> { + let state = await readSessionShellState(); + for (let attempt = 0; attempt < 8; attempt += 1) { + const texts = await readHeaderTurnListTexts(); + state = { + ...await readSessionShellState(), + turnListOpen: true, + turnListTexts: texts, + }; + if (texts.some(text => text.includes(targetTitle))) { + return { attempts: attempt, finalState: state }; + } + + await closeHeaderTurnList(); + if (!state.isPartial && !state.hasPendingHistoryCompletion && !state.hasDeferredHistoryProjection) { + break; + } + + const beforeCount = state.dialogTurnCount; + await scrollToPreviousHistoryBoundary(); + await browser.waitUntil(async () => { + const next = await readSessionShellState(); + return ( + next.dialogTurnCount > beforeCount || + next.isPartial === false || + next.hasDeferredHistoryProjection !== state.hasDeferredHistoryProjection || + next.hasPendingHistoryCompletion !== state.hasPendingHistoryCompletion + ); + }, { timeout: 6000, interval: 150 }).catch(() => undefined); + await browser.pause(500); + } + + await openHeaderTurnList(); + state = await readSessionShellState(); + throw new Error(`[ReleaseTurnNav] target turn title not available in header list: ${JSON.stringify({ + targetTitle, + state, + })}`); +} + +async function scrollToLatest(targetTurnId: string): Promise { + await browser.execute(() => { + const scroller = document.querySelector( + '.modern-flowchat-container__messages .virtual-message-list [data-virtuoso-scroller="true"], ' + + '.modern-flowchat-container__messages .virtual-message-list [data-virtuoso-scroller], ' + + '.modern-flowchat-container__messages .virtual-message-list .virtual-message-list__static-scroller', + ); + if (!scroller) { + return; + } + scroller.scrollTop = Math.max(0, scroller.scrollHeight - scroller.clientHeight); + scroller.dispatchEvent(new Event('scroll', { bubbles: true })); + }); + await browser.pause(800); + return readTurnViewportMetrics(targetTurnId); +} + +async function clickHeaderTurnListItemByTitle(targetTitle: string): Promise<{ + itemCount: number; + itemTexts: string[]; +}> { + await openHeaderTurnList(); + const items = await $$('.flowchat-header__turn-list-item'); + const itemTexts: string[] = []; + let targetIndex = -1; + for (let index = 0; index < items.length; index += 1) { + const text = await items[index].getText(); + itemTexts.push(text); + if (targetIndex < 0 && text.includes(targetTitle)) { + targetIndex = index; + } + } + if (targetIndex < 0) { + throw new Error(`[ReleaseTurnNav] turn list item not found: ${JSON.stringify({ + targetTitle, + itemTexts, + })}`); + } + await items[targetIndex].click(); + return { itemCount: items.length, itemTexts }; +} + +describe('Release long-session turn navigation', () => { + let hasWorkspace = false; + + before(async function () { + const workspacePath = process.env.E2E_TEST_WORKSPACE; + if (!workspacePath) { + console.log('[ReleaseTurnNav] E2E_TEST_WORKSPACE is missing; skipping release fixture spec.'); + return; + } + hasWorkspace = await openWorkspace(workspacePath, { requireWorkspaceLabel: true }); + }); + + it('moves the real message viewport when selecting an older turn from the header list (#1281)', async function () { + if (!hasWorkspace) { + this.skip(); + return; + } + + const sessionId = process.env.BITFUN_E2E_TURN_NAV_SESSION_ID || DEFAULT_SESSION_ID; + const targetTurnIndex = Number( + process.env.BITFUN_E2E_TURN_NAV_TARGET_INDEX || DEFAULT_TARGET_TURN_INDEX, + ); + const targetTurnId = `${sessionId}-turn-${String(targetTurnIndex).padStart(4, '0')}`; + const targetTitle = `Synthetic user turn ${targetTurnIndex}`; + + const item = await findSessionItem(sessionId); + if (!item) { + throw new Error(`[ReleaseTurnNav] generated session not found: ${sessionId}`); + } + if (!(await isSessionItemActive(item))) { + await item.click(); + } + await waitForSessionHydrated(); + + const before = await scrollToLatest(targetTurnId); + expect(before.scrollerExists).toBe(true); + expect(before.maxScrollTop).toBeGreaterThan(300); + + const revealState = await revealHistoryUntilTurnListContains(targetTitle); + const clickResult = await clickHeaderTurnListItemByTitle(targetTitle); + expect(clickResult.itemCount).toBeGreaterThan(0); + + let lastMetrics = await readTurnViewportMetrics(targetTurnId); + await browser.waitUntil(async () => { + const metrics = await readTurnViewportMetrics(targetTurnId); + lastMetrics = metrics; + return ( + metrics.targetVisible && + metrics.deltaToPinnedTop !== null && + Math.abs(metrics.deltaToPinnedTop) <= 100 + ); + }, { + timeout: 8000, + interval: 150, + timeoutMsg: `[ReleaseTurnNav] selected turn did not move near the top of the message viewport: ${JSON.stringify({ + targetTurnId, + targetTitle, + revealState, + clickResult, + lastMetrics, + shellState: await readSessionShellState(), + })}`, + }); + + await browser.pause(600); + const after = await readTurnViewportMetrics(targetTurnId); + const diagnostics = { sessionId, targetTurnId, targetTitle, revealState, clickResult, before, after }; + console.log('[ReleaseTurnNav] diagnostics:', JSON.stringify(diagnostics)); + + expect(after.targetExists).toBe(true); + expect(after.targetVisible).toBe(true); + expect(after.deltaToPinnedTop).not.toBeNull(); + expect(Math.abs(after.deltaToPinnedTop!)).toBeLessThanOrEqual(100); + expect(after.distanceFromBottom).not.toBeNull(); + expect(after.distanceFromBottom!).toBeGreaterThan(200); + }); + + afterEach(async function () { + if (this.currentTest?.state === 'failed') { + await saveFailureScreenshot(`l1-chat-turn-navigation-release-${this.currentTest.title}`); + } + }); +}); diff --git a/tests/e2e/specs/performance/startup-session-perf.spec.ts b/tests/e2e/specs/performance/startup-session-perf.spec.ts index be7009492a..c3ad446081 100644 --- a/tests/e2e/specs/performance/startup-session-perf.spec.ts +++ b/tests/e2e/specs/performance/startup-session-perf.spec.ts @@ -36,6 +36,10 @@ const LONG_SESSION_RESIZE_BOTTOM_SETTLE_MAX_MS = 120; const LONG_SESSION_INPUT_MIN_TOP_RATIO = 0.65; const LONG_SESSION_INPUT_BOTTOM_TOLERANCE_PX = 96; const LONG_SESSION_MAX_LATEST_TEXT_DELAY_AFTER_VISIBLE_MS = 120; +const SESSION_NAV_ITEM_SELECTOR = + '[data-testid="session-nav-item"], [data-testid="nav-session-item"]'; +const SESSION_NAV_TOGGLE_SELECTOR = + '[data-testid="session-nav-show-more"], [data-testid="nav-session-list-toggle"]'; type LongSessionPostVisibleInteraction = | 'first-scroll' @@ -685,21 +689,28 @@ async function waitForOptionalTracePhaseForSessionSince( } } +function escapeCssAttributeValue(value: string): string { + return value.replace(/\\/g, '\\\\').replace(/"/g, '\\"'); +} + async function findSessionItem(sessionId: string): Promise | null> { + const targetSelector = + `[data-testid="session-nav-item"][data-session-id="${escapeCssAttributeValue(sessionId)}"], ` + + `[data-testid="nav-session-item"][data-session-id="${escapeCssAttributeValue(sessionId)}"]`; const readVisibleSessionIds = async (): Promise => - browser.execute(() => - Array.from(document.querySelectorAll('[data-testid="session-nav-item"]')) + browser.execute((selector) => + Array.from(document.querySelectorAll(selector)) .map(element => element.getAttribute('data-session-id') || '') - .filter(Boolean) - ); + .filter(Boolean), + SESSION_NAV_ITEM_SELECTOR); const findTarget = async (): Promise | null> => { - const item = await $(`[data-testid="session-nav-item"][data-session-id="${sessionId}"]`); + const item = await $(targetSelector); return await item.isExisting() ? item : null; }; const findExpandableToggles = async (): Promise>> => { - const toggles = await browser.$$('[data-testid="session-nav-show-more"]'); + const toggles = await browser.$$(SESSION_NAV_TOGGLE_SELECTOR); const expandable: Array> = []; for (const toggle of toggles) { if ( @@ -3611,6 +3622,42 @@ type LongSessionOpenMeasurementOptions = { postVisibleInteraction?: LongSessionPostVisibleInteraction; }; +type LongSessionSendAfterOpenMeasurement = { + appMode: string; + sessionId: string; + fixtureScenario: string | null; + previousLatestTurnId: string; + message: string; + clickedAtMs: number; + postSendObserveMs: number; + beforeState: LongSessionActiveMessageState; + afterSendState: LongSessionActiveMessageState; + finalState: LongSessionActiveMessageState; + observedLatestTurnId: string; + viewport: LongSessionViewportState; + viewportTimelineSummary: LongSessionViewportTimelineSummary; + visualStateSummary: LongSessionVisualStateSummary; + visualStateEvents: LongSessionVisualStateEvent[]; + mutationEvents: LongSessionDomMutationEvent[]; + layoutShiftEvents: LongSessionLayoutShiftEvent[]; + screenshotPath: string | null; + events: StartupTraceSnapshot['phases']['events']; + apiSegments: ReturnType; + api: StartupTraceSnapshot['api']; + native: StartupTraceSnapshot['native']; +}; + +type LongSessionActiveMessageState = { + activeSessionId: string | null; + latestTurnId: string | null; + dialogTurnCount: number; + virtualItemCount: number; + visibleTextLength: number; + showHistoryLoadingLayer: string | null; + showHistoryOpenIntentOverlay: string | null; + showHistoryTransitionOverlay: string | null; +}; + type RapidLongSessionSwitchMeasurement = { appMode: string; requestedSessionIds: string[]; @@ -3898,6 +3945,7 @@ async function readActiveSessionNavId(): Promise { return browser.execute(() => { const active = document.querySelector( '[data-testid="session-nav-item"].is-active, ' + + '[data-testid="nav-session-item"].is-active, ' + '.bitfun-nav-panel__inline-item.is-active[data-session-id]', ); return active?.getAttribute('data-session-id') ?? null; @@ -4385,6 +4433,261 @@ async function collectLongSessionOpenMeasurement( return measurement; } +async function readLongSessionActiveMessageState(): Promise { + return browser.execute(() => { + const messages = document.querySelector('.modern-flowchat-container__messages'); + const virtualItems = Array.from( + document.querySelectorAll('.modern-flowchat-container__messages .virtual-item-wrapper'), + ); + const visibleTextLength = virtualItems.reduce((total, item) => { + const rect = item.getBoundingClientRect(); + const style = window.getComputedStyle(item); + if ( + rect.width <= 0 || + rect.height <= 0 || + style.display === 'none' || + style.visibility === 'hidden' || + Number(style.opacity || '1') <= 0.01 + ) { + return total; + } + return total + (item.innerText?.length ?? 0); + }, 0); + const numeric = (value: string | undefined): number => { + const parsed = Number(value ?? 0); + return Number.isFinite(parsed) ? parsed : 0; + }; + + return { + activeSessionId: messages?.dataset.activeSessionId || null, + latestTurnId: messages?.dataset.latestTurnId || null, + dialogTurnCount: numeric(messages?.dataset.dialogTurnCount), + virtualItemCount: numeric(messages?.dataset.virtualItemCount), + visibleTextLength, + showHistoryLoadingLayer: messages?.dataset.showHistoryLoadingLayer || null, + showHistoryOpenIntentOverlay: messages?.dataset.showHistoryOpenIntentOverlay || null, + showHistoryTransitionOverlay: messages?.dataset.showHistoryTransitionOverlay || null, + }; + }); +} + +async function ensureLongSessionOpenForSend( + sessionId: string, + expectedLatestTurnId: string, +): Promise { + await switchAwayFromSession(sessionId); + + const activeSessionId = await readActiveSessionNavId(); + if (activeSessionId !== sessionId) { + const item = await findSessionItem(sessionId); + if (!item) { + return false; + } + await item.click(); + } + + const fixtureScenario = await readLongSessionFixtureScenario(sessionId); + await waitForLatestLongSessionTurnVisible(5000, expectedLatestTurnId); + await waitForLatestLongSessionViewportUsable( + 5000, + expectedLatestTurnId, + { requireLatestModelRound: requiresLatestModelRoundForFixture(fixtureScenario) }, + ); + await waitForBrowserAnimationFrames(2); + return true; +} + +async function clearLongSessionPendingQueue(sessionId: string): Promise { + await browser.execute(async (targetSessionId) => { + try { + const { pendingQueueManager } = await import( + '/src/flow_chat/services/flow-chat-manager/PendingQueueModule.ts' + ); + pendingQueueManager.clear(targetSessionId); + } catch { + // Keep the localStorage fallback below for bundled/runtime import differences. + } + try { + window.localStorage.removeItem(`flowChat.pendingQueue.${targetSessionId}`); + } catch { + // localStorage may be unavailable in unusual WebView states. + } + }, sessionId); + + await browser.waitUntil(async () => { + const pendingQueueCount = await browser.execute(() => + document.querySelectorAll('[data-testid="pending-queue-panel"] .bitfun-pending-queue-panel__item').length + ); + return pendingQueueCount === 0; + }, { + timeout: 1000, + interval: 50, + timeoutMsg: 'Pending queue did not clear before send-after-open measurement', + }).catch(() => undefined); +} + +async function setLongSessionChatInputValue(message: string): Promise { + await browser.execute((value) => { + const input = document.querySelector( + '.rich-text-input[contenteditable="true"], ' + + '[data-testid="chat-input-textarea"], ' + + '.bitfun-chat-input [contenteditable="true"]', + ); + if (!input) { + throw new Error('Chat input was not found'); + } + + input.focus(); + if (input.isContentEditable) { + input.textContent = value; + } else if ('value' in input) { + (input as HTMLInputElement | HTMLTextAreaElement).value = value; + } else { + input.textContent = value; + } + + const inputEvent = typeof InputEvent !== 'undefined' + ? new InputEvent('input', { bubbles: true, inputType: 'insertText', data: value }) + : new Event('input', { bubbles: true }); + input.dispatchEvent(inputEvent); + input.dispatchEvent(new Event('change', { bubbles: true })); + }, message); + + await browser.waitUntil(async () => { + const button = await $('[data-testid="chat-input-send-btn"], button.bitfun-chat-input__send-button'); + if (!await button.isExisting()) { + return false; + } + const disabled = await button.getAttribute('disabled'); + const ariaDisabled = await button.getAttribute('aria-disabled'); + return await button.isEnabled() && disabled === null && ariaDisabled !== 'true'; + }, { + timeout: 3000, + interval: 50, + timeoutMsg: 'Chat send button did not become enabled', + }); +} + +async function clickLongSessionSendButton(): Promise { + const button = await $('[data-testid="chat-input-send-btn"], button.bitfun-chat-input__send-button'); + await button.waitForExist({ timeout: 3000 }); + await button.click(); + await browser.execute(() => { + window.dispatchEvent(new CustomEvent('bitfun:e2e-long-session-user-interaction', { + detail: { type: 'send-message' }, + })); + }); +} + +async function collectLongSessionSendAfterOpenMeasurement( + sessionId: string, + previousLatestTurnId: string, +): Promise { + const fixtureScenario = await readLongSessionFixtureScenario(sessionId); + const message = `E2E history send blank probe ${Date.now()}`; + + await clearLongSessionPendingQueue(sessionId); + await setLongSessionChatInputValue(message); + const beforeState = await readLongSessionActiveMessageState(); + if (beforeState.activeSessionId !== sessionId) { + throw new Error( + `Expected active session ${sessionId} before send, got ${beforeState.activeSessionId ?? 'none'}`, + ); + } + + const clickedAtMs = await readPerformanceNow(); + await startLongSessionViewportTimelineRecorder( + previousLatestTurnId, + clickedAtMs, + process.env.BITFUN_E2E_RENDER_PROFILE === '1', + ); + + await clickLongSessionSendButton(); + + let afterSendState = await readLongSessionActiveMessageState(); + await browser.waitUntil(async () => { + const state = await readLongSessionActiveMessageState(); + const changed = + state.dialogTurnCount !== beforeState.dialogTurnCount || + state.latestTurnId !== beforeState.latestTurnId || + state.virtualItemCount !== beforeState.virtualItemCount; + if (changed) { + afterSendState = state; + } + return changed; + }, { + timeout: 1500, + interval: 25, + timeoutMsg: 'Send did not change the active session message state', + }).catch(() => undefined); + + const postSendObserveMs = numericEnv('BITFUN_E2E_PERF_SEND_AFTER_OPEN_OBSERVE_MS') ?? 2000; + if (postSendObserveMs > 0) { + await browser.pause(postSendObserveMs); + } + + const finalState = await readLongSessionActiveMessageState(); + const observedLatestTurnId = + finalState.latestTurnId || + afterSendState.latestTurnId || + beforeState.latestTurnId || + previousLatestTurnId; + const viewport = await readLongSessionViewportState(observedLatestTurnId); + const viewportTimeline = await stopLongSessionViewportTimelineRecorder(); + const viewportTimelineSummary = summarizeLongSessionViewportTimeline( + viewportTimeline.samples, + sessionId, + ); + const visualStateSummary = summarizeLongSessionVisualStateEvents( + viewportTimeline.visualStateEvents, + viewportTimeline.mutationEvents, + viewportTimeline.layoutShiftEvents, + viewportTimelineSummary, + sessionId, + ); + const verboseTimelineReport = process.env.BITFUN_E2E_PERF_VERBOSE_REPORT === '1'; + const finalSnapshot = await readStartupTraceSnapshot(); + const events = finalSnapshot.phases.events.filter(event => + event.atMs >= clickedAtMs && + ( + ( + event.phase.startsWith('historical_session') && + traceEventSessionId(event) === sessionId + ) || + event.phase.startsWith('flowchat') || + event.phase === 'react_render_profile' || + event.phase === 'git_status_request' || + event.phase === 'git_state_refresh' + ) + ); + const screenshotPath = await maybeSavePerfScreenshot(`long-session-send-${sessionId}`); + + return { + appMode: process.env.BITFUN_E2E_APP_MODE ?? 'auto', + sessionId, + fixtureScenario, + previousLatestTurnId, + message, + clickedAtMs, + postSendObserveMs, + beforeState, + afterSendState, + finalState, + observedLatestTurnId, + viewport, + viewportTimelineSummary, + visualStateSummary, + visualStateEvents: verboseTimelineReport ? viewportTimeline.visualStateEvents : [], + mutationEvents: verboseTimelineReport ? viewportTimeline.mutationEvents : [], + layoutShiftEvents: verboseTimelineReport ? viewportTimeline.layoutShiftEvents : [], + screenshotPath, + events, + apiSegments: summarizeApiCommandSegments(finalSnapshot), + api: finalSnapshot.api, + native: finalSnapshot.native, + }; +} + function expectLongSessionMeasurementUsable( measurement: LongSessionOpenMeasurement, maxLatestFrameMs?: number, @@ -4569,6 +4872,30 @@ function expectLongSessionMeasurementUsable( } } +function expectLongSessionSendAfterOpenStable( + measurement: LongSessionSendAfterOpenMeasurement, +): void { + expect(measurement.beforeState.activeSessionId).toBe(measurement.sessionId); + expect(measurement.afterSendState.activeSessionId).toBe(measurement.sessionId); + expect(measurement.finalState.activeSessionId).toBe(measurement.sessionId); + expect(measurement.beforeState.dialogTurnCount).toBeGreaterThan(0); + expect(measurement.afterSendState.dialogTurnCount).toBeGreaterThan(measurement.beforeState.dialogTurnCount); + expect(measurement.afterSendState.latestTurnId).not.toBe(measurement.beforeState.latestTurnId); + expect(measurement.afterSendState.virtualItemCount).toBeGreaterThan(measurement.beforeState.virtualItemCount); + expect(measurement.viewport.hasScroller).toBe(true); + expect(measurement.viewport.visibleItemCount).toBeGreaterThan(0); + expect(measurement.viewport.visibleTextLength).toBeGreaterThan(0); + expect(measurement.visualStateSummary.firstLoadingLayerAtMs).toBeNull(); + expect(measurement.visualStateSummary.loadingLayerToggleCount).toBe(0); + expect(measurement.visualStateSummary.overlayCountToggleCount).toBe(0); + expect(measurement.visualStateSummary.placeholderCountToggleCount).toBe(0); + expect(measurement.visualStateSummary.postUserInteractionBlankSurfacePointEventCount).toBe(0); + expect(measurement.visualStateSummary.postLatestTextVisibleBlankSurfacePointEventCount).toBe(0); + expect(measurement.visualStateSummary.postLatestTextVisibleLoadingSurfacePointEventCount).toBe(0); + expect(measurement.visualStateSummary.postLatestTextVisibleTransparentSurfacePointEventCount).toBe(0); + expect(measurement.visualStateSummary.loadingTransitions).toHaveLength(0); +} + describe('Performance telemetry', () => { const startupPage = new StartupPage(); @@ -4685,6 +5012,52 @@ describe('Performance telemetry', () => { }); }); + it('keeps a generated long session visible after sending a new message', async function () { + await ensurePerformanceWorkspace(startupPage); + + const sessionId = process.env.BITFUN_E2E_PERF_SESSION_ID || DEFAULT_PERF_SESSION_ID; + const expectedLatestTurnId = await readExpectedLatestTurnId(sessionId); + if (!expectedLatestTurnId) { + console.log(`[Perf] Session ${sessionId} not found; generate it before running this spec.`); + this.skip(); + return; + } + + const opened = await ensureLongSessionOpenForSend(sessionId, expectedLatestTurnId); + if (!opened) { + console.log(`[Perf] Session ${sessionId} not reachable from the session navigation.`); + this.skip(); + return; + } + + const measurement = await collectLongSessionSendAfterOpenMeasurement( + sessionId, + expectedLatestTurnId, + ); + + console.log('[Perf] long-session-send-after-open', JSON.stringify({ + appMode: measurement.appMode, + sessionId, + fixtureScenario: measurement.fixtureScenario, + beforeState: measurement.beforeState, + afterSendState: measurement.afterSendState, + finalState: measurement.finalState, + visualStateSummary: { + firstLoadingLayerAtMs: measurement.visualStateSummary.firstLoadingLayerAtMs, + loadingLayerToggleCount: measurement.visualStateSummary.loadingLayerToggleCount, + postUserInteractionBlankSurfacePointEventCount: + measurement.visualStateSummary.postUserInteractionBlankSurfacePointEventCount, + postLatestTextVisibleBlankSurfacePointEventCount: + measurement.visualStateSummary.postLatestTextVisibleBlankSurfacePointEventCount, + virtualItemElementChangeCount: + measurement.visualStateSummary.postLatestTextVisibleVirtualItemElementChangeCount, + }, + })); + + await writeReport('long-session-send-after-open', measurement); + expectLongSessionSendAfterOpenStable(measurement); + }); + it('collects rapid-switch timing across generated long sessions', async function () { await ensurePerformanceWorkspace(startupPage);