前端设计语言改成fluent2风格。增加主题色设置。

This commit is contained in:
xiaoxixi 2026-07-27 22:25:59 +08:00
parent c1a6660313
commit 1c1b96f5d4
27 changed files with 821 additions and 494 deletions

View File

@ -92,6 +92,7 @@ Scheduler → SessionManager scheduled execution → AgentLoop → Scheduler del
- **Scheduler** supports legacy direct-delivery jobs and managed `task`/`monitor` jobs; managed agents cannot call `send_message`, and `on_alert` suppresses only healthy informational results
- **AgentLoop** is stateless across turns; it receives prepared history, calls LLM providers, executes tools, and returns one result
- **WebUI management APIs** only expose allowlisted config/profile/log/storage operations; keep response limits, secret redaction, atomic config writes, and profile path allowlists intact
- **WebUI styling** uses the local Fluent 2 semantic tokens in `webui/src/styles.css`; page and component styles should consume the aliases instead of introducing independent hard-coded palettes, and must preserve selectable light/dark and brand-color themes, keyboard focus, responsive layout, and reduced-motion behavior. Browser-only appearance settings belong in `lib/theme.js`/`localStorage`, and `public/theme-init.js` must restore them before Svelte mounts
- **Gateway config reload** validates and prepares a complete next runtime generation before retiring the old one; close runtime admission before draining inbound lanes, Sessions, Scheduler jobs, and background sub-agents; MCP connects only during activation; host/port, workspace, and effective storage path remain restart-only, and runtime reload must never mutate process environment variables
- **WebUI slash completion** must consume the existing `get_slash_commands` WebSocket response; do not duplicate the backend command list in frontend source
- **WebUI chat rendering** sanitizes Markdown before inserting HTML; durable `turn_committed` deltas calibrate normal terminal Turns without a full history reload, and history must preserve structured tool-call metadata so calls and results remain independently collapsible
@ -145,4 +146,4 @@ Scheduler → SessionManager scheduled execution → AgentLoop → Scheduler del
- `resources/skills/about-picobot/references/` — runtime knowledge shipped to PicoBot; update it only when the assistant's built-in product knowledge must change
## Version Management
- 在每次功能变化、架构变化后,适当地更新整个产品的版本号
- 在每次功能变化、架构变化后,适当地更新整个产品的版本号。功能变化增加中段数字ui变化、bug修改增加末端数字。 注意版本号变更是提交时和仓库中的版本比较,不要在长时间的工程中,不断变化版本号。

View File

@ -1,6 +1,6 @@
[package]
name = "picobot"
version = "1.3.0"
version = "1.3.1"
edition = "2024"
[dependencies]

View File

@ -164,11 +164,11 @@ docker compose exec picobot picobot pair --gateway-url http://127.0.0.1:19876
不要从宿主机经发布端口直接调用签发接口;容器会把该连接识别为非回环来源并拒绝。`picobot` 已加入正式镜像的 `PATH`,可在容器 shell 中直接调用。
WebUI 随二进制嵌入,不需要 Node.js、npm 或单独部署静态文件,提供:
WebUI 随二进制嵌入,不需要 Node.js、npm 或单独部署静态文件。界面采用本地实现的 Microsoft Fluent 2 视觉系统提供语义化中性色表面、品牌蓝交互状态、统一组件层级和完整的浅色/深色主题,并提供:
- 在线聊天、会话创建/切换、历史回放、流式 Markdown、独立思考区、实时工具状态、可折叠历史工具调用卡片以及基于 Gateway 实时命令清单的 `/` 斜杠命令补全。
- 文件选择、拖放和剪贴板图片上传;消息中的附件可预览或下载。附件按服务端路径引用,原文件移动或删除后历史附件可能不可用。
- 可持久化的浅色/深色主题,首次访问时跟随系统偏好。
- “配置 → 外观”提供浅色/深色模式和六套 Fluent 品牌色;选择即时生效并保存在当前浏览器中,首次访问时明暗模式跟随系统偏好。
- Cron 定时任务、最近运行记录和后台子任务状态。
- 当前聊天 session 的可展开 Todo 侧栏;计划变化时自动展开,其他 session 的变化显示未读提示。
- Knowledge/Timeline 记忆的分类与全文检索。
@ -181,7 +181,7 @@ WebUI 默认启用设备配对鉴权,管理 API 与 `/ws` 都拒绝未配对
#### WebUI 开发
WebUI 源码位于 `webui/`,使用 Svelte 5、Vite 和无样式的 Bits UI 可访问组件原语。运行发布版 PicoBot 不需要 Node.js从源码编译或修改前端时需要 Node.js 20+
WebUI 源码位于 `webui/`,使用 Svelte 5、Vite 和无样式的 Bits UI 可访问组件原语。Fluent 2 外观由 `webui/src/styles.css` 中的本地语义令牌与 Svelte 组件实现,不加载 CDN 或外部 UI 运行库;明暗模式和品牌色分别保存在浏览器 `picobot-theme``picobot-accent` 项中,并由 `theme-init.js` 在应用挂载前恢复。运行发布版 PicoBot 不需要 Node.js从源码编译或修改前端时需要 Node.js 20+
```bash
cd webui

View File

@ -3,7 +3,7 @@ services:
build:
context: .
dockerfile: Dockerfile
image: picobot:1.3.0
image: picobot:1.3.1
container_name: picobot-test
restart: unless-stopped
ports:

View File

@ -245,7 +245,7 @@ Turn delivery 使用 `spawn_graceful`。全局取消发生时先停止读取快
### WebUI 与管理 API
Gateway 在 `/` 提供随二进制编译的 HTML/CSS/JavaScript不依赖外部 CDN 或前端运行服务。前端源码位于 `webui/`,由 Svelte 5 + Vite 构建Bits UI 提供无样式的可访问组件原语。`build.rs` 监听前端源码、锁文件和构建配置,增量地将生产资源生成到 Cargo `OUT_DIR`Rust 再从该目录编译嵌入;前端产物不进入仓库,最终用户使用发布二进制时不需要 Node.js。浏览器聊天继续使用 `/ws``cli_chat` 渠道,因此复用现有 dialog scope、每会话串行 worker、历史持久化和出站 lane会话历史帧保留工具调用 ID、名称、参数和工具结果角色WebUI 在正常完成时合并 `turn_committed` 增量并将工具信息渲染为默认折叠的工具卡片;聊天页的 Todo 侧栏默认隐藏,按 session 保存快照和未读状态,并通过结构化 `session_plan`/`plan_updated` 帧刷新,计划变化不会写入聊天历史;斜杠命令补全通过 `get_slash_commands` 获取 Gateway 的实时命令与别名清单不在前端重复定义WebUI 不直接调用 Provider 或 SessionManager。
Gateway 在 `/` 提供随二进制编译的 HTML/CSS/JavaScript不依赖外部 CDN 或前端运行服务。前端源码位于 `webui/`,由 Svelte 5 + Vite 构建Bits UI 提供无样式的可访问组件原语。视觉层通过 `webui/src/styles.css` 中的本地 Fluent 2 语义令牌实现浅色/深色表面、六套品牌色、状态色、层级和控件状态;页面组件必须复用语义别名,不能把独立硬编码调色板或外部 Fluent 运行库引入发布产物。明暗模式和品牌色只保存在浏览器 `localStorage``theme-init.js` 必须在 Svelte 挂载前恢复 `data-theme``data-accent`,防止首屏颜色闪烁;这些外观选项不属于 Gateway 配置,也不跨设备同步。`build.rs` 监听前端源码、锁文件和构建配置,增量地将生产资源生成到 Cargo `OUT_DIR`Rust 再从该目录编译嵌入;前端产物不进入仓库,最终用户使用发布二进制时不需要 Node.js。浏览器聊天继续使用 `/ws``cli_chat` 渠道,因此复用现有 dialog scope、每会话串行 worker、历史持久化和出站 lane会话历史帧保留工具调用 ID、名称、参数和工具结果角色WebUI 在正常完成时合并 `turn_committed` 增量并将工具信息渲染为默认折叠的工具卡片;聊天页的 Todo 侧栏默认隐藏,按 session 保存快照和未读状态,并通过结构化 `session_plan`/`plan_updated` 帧刷新,计划变化不会写入聊天历史;斜杠命令补全通过 `get_slash_commands` 获取 Gateway 的实时命令与别名清单不在前端重复定义WebUI 不直接调用 Provider 或 SessionManager。
WebUI/TUI 文件字节通过受鉴权的 HTTP 接口流式传输WebSocket 只携带短期 `upload_id` 和结构化附件描述。`UploadRegistry` 在内存中按 `cli_chat` chat scope 校验并消费待发送上传;消息继续以 `media_refs` 保存 Gateway 本地路径,不建立永久附件资产。下载接口必须通过 client、session、message 和附件序号反查路径,不能接受客户端路径。历史附件路径失效属于正常状态,不得影响历史文本读取。待发送但未进入消息的上传由 `TaskSupervisor` 所有的限时清理任务回收。Agent 上下文会为所有媒体注入内部路径清单,客户端响应不得暴露该路径。

View File

@ -4,9 +4,9 @@
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="color-scheme" content="light dark" />
<meta name="theme-color" content="#0b1017" />
<meta name="theme-color" content="#f5f5f5" />
<script src="/theme-init.js"></script>
<title>PicoBot Console</title>
<title>PicoBot Workspace</title>
</head>
<body>
<div id="app"></div>

View File

@ -1,12 +1,12 @@
{
"name": "picobot-webui",
"version": "1.3.0",
"version": "1.3.1",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "picobot-webui",
"version": "1.3.0",
"version": "1.3.1",
"dependencies": {
"bits-ui": "^2.0.0",
"dompurify": "^3.4.12",

View File

@ -1,7 +1,7 @@
{
"name": "picobot-webui",
"private": true,
"version": "1.3.0",
"version": "1.3.1",
"type": "module",
"engines": {
"node": ">=20"

View File

@ -1,5 +1,8 @@
try {
var t = localStorage.getItem("picobot-theme");
if (!t) t = matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light";
var a = localStorage.getItem("picobot-accent");
if (!["blue", "purple", "teal", "green", "orange", "magenta"].includes(a)) a = "blue";
document.documentElement.dataset.theme = t;
document.documentElement.dataset.accent = a;
} catch (e) {}

View File

@ -3,8 +3,9 @@
import { Tooltip } from "bits-ui";
import { api } from "./lib/api.js";
import { chat } from "./lib/chat.svelte.js";
import { applyTheme, preferredTheme } from "./lib/theme.js";
import { applyAccent, applyTheme, preferredAccent, preferredTheme } from "./lib/theme.js";
import Toast from "./lib/Toast.svelte";
import Icon from "./lib/Icon.svelte";
import ActivitySpine from "./lib/components/ActivitySpine.svelte";
import ChatPage from "./pages/ChatPage.svelte";
import TasksPage from "./pages/TasksPage.svelte";
@ -16,13 +17,13 @@
import PairingPage from "./pages/PairingPage.svelte";
const pages = [
["chat", "聊天"],
["overview", "概览"],
["tools", "工具"],
["logs", "日志"],
["memory", "记忆"],
["tasks", "任务"],
["settings", "配置"]
{ name: "chat", label: "聊天", description: "与 PicoBot 协作并管理会话" },
{ name: "overview", label: "概览", description: "查看运行状态与系统容量" },
{ name: "tools", label: "工具", description: "浏览工具、Skills 与 MCP 连接" },
{ name: "logs", label: "日志", description: "检查实时事件与运行记录" },
{ name: "memory", label: "记忆", description: "查找和维护长期记忆" },
{ name: "tasks", label: "任务", description: "跟踪定时任务与后台工作" },
{ name: "settings", label: "配置", description: "管理 Gateway 与 Agent 配置" }
];
const icons = {
chat: '<path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"/>',
@ -41,7 +42,7 @@
let authReady = $state(false);
let authenticated = $state(false);
let toast = $state();
const meta = $derived(pages.find(([name]) => name === current) || pages[0]);
const meta = $derived(pages.find((page) => page.name === current) || pages[0]);
async function health() {
try {
@ -89,15 +90,22 @@
onMount(() => {
const preferred = preferredTheme();
const accent = preferredAccent();
applyTheme(preferred);
applyAccent(accent);
theme = preferred;
checkAuth().then(() => authenticated && health());
const authRequired = () => (authenticated = false);
const appearanceChanged = (event) => {
if (event.detail?.theme === "light" || event.detail?.theme === "dark") theme = event.detail.theme;
};
window.addEventListener("picobot-auth-required", authRequired);
window.addEventListener("picobot-appearance-change", appearanceChanged);
const timer = setInterval(() => authenticated && health(), 30_000);
return () => {
clearInterval(timer);
window.removeEventListener("picobot-auth-required", authRequired);
window.removeEventListener("picobot-appearance-change", appearanceChanged);
chat.disconnect();
};
});
@ -110,29 +118,30 @@
{:else}
<Tooltip.Provider delayDuration={350}>
<div class="shell">
{#if menuOpen}<button class="nav-scrim" aria-label="关闭导航" onclick={() => (menuOpen = false)}></button>{/if}
<aside class:open={menuOpen} class="sidebar">
<div class="brand"><span class="brand-mark">P</span><div><strong>PicoBot</strong><small>Local agent console</small></div></div>
<nav>
<div class="brand"><span class="brand-mark"><Icon name="bot" size={22} /></span><div><strong>PicoBot</strong><small>Agent workspace</small></div></div>
<span class="nav-label">工作区</span>
<nav aria-label="主导航">
{#each pages as page}
<button class:active={current === page[0]} onclick={() => selectPage(page[0])}>
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">{@html icons[page[0]]}</svg>
{page[1]}
<button class:active={current === page.name} aria-current={current === page.name ? "page" : undefined} onclick={() => selectPage(page.name)}>
<span class="nav-icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">{@html icons[page.name]}</svg></span>
{page.label}
</button>
{/each}
</nav>
<div class="gateway-status">
<i class:online></i>
<div><b>{online ? "运行中" : "不可用"}</b><small>{version}</small></div>
<button class="theme-toggle" aria-label={theme === "dark" ? "切换到浅色主题" : "切换到深色主题"} onclick={toggleTheme}>
<span aria-hidden="true">{theme === "dark" ? "☀" : "☾"}</span><span>{theme === "dark" ? "浅色" : "深色"}</span>
</button>
<i class:online></i><div><b>{online ? "Gateway 已连接" : "Gateway 不可用"}</b><small>{version}</small></div>
</div>
</aside>
<main>
<ActivitySpine {version} />
<main class="app-main">
<header class="topbar">
<button class="menu" aria-label="菜单" onclick={() => (menuOpen = !menuOpen)}>☰</button>
<h1>{meta[1]}</h1>
<button class="menu icon-button" aria-label="打开导航" onclick={() => (menuOpen = !menuOpen)}><Icon name="panel" /></button>
<div class="page-heading"><h1>{meta.label}</h1><p>{meta.description}</p></div>
<ActivitySpine {version} />
<button class="theme-toggle" aria-label={theme === "dark" ? "切换到浅色主题" : "切换到深色主题"} onclick={toggleTheme}>
<Icon name={theme === "dark" ? "sun" : "moon"} /><span>{theme === "dark" ? "浅色" : "深色"}</span>
</button>
</header>
{#if current === "chat"}<ChatPage notify={(text, error) => toast.show(text, error)} />
{:else if current === "logs"}<LogsPage />

53
webui/src/lib/Icon.svelte Normal file
View File

@ -0,0 +1,53 @@
<script>
let { name, size = 20 } = $props();
</script>
<svg
class="fluent-icon"
width={size}
height={size}
viewBox="0 0 20 20"
fill="none"
xmlns="http://www.w3.org/2000/svg"
aria-hidden="true"
>
{#if name === "add"}
<path d="M10 4.25v11.5M4.25 10h11.5" />
{:else if name === "search"}
<circle cx="8.75" cy="8.75" r="5.25" /><path d="m12.65 12.65 3.6 3.6" />
{:else if name === "refresh"}
<path d="M15.8 7.5A6.25 6.25 0 1 0 16 11" /><path d="M12.5 4.25h3.5v3.5" />
{:else if name === "sun"}
<circle cx="10" cy="10" r="3.25" /><path d="M10 2.25v1.5M10 16.25v1.5M2.25 10h1.5M16.25 10h1.5M4.52 4.52l1.06 1.06M14.42 14.42l1.06 1.06M15.48 4.52l-1.06 1.06M5.58 14.42l-1.06 1.06" />
{:else if name === "moon"}
<path d="M16.4 12.2A6.5 6.5 0 0 1 7.8 3.6 6.5 6.5 0 1 0 16.4 12.2Z" />
{:else if name === "attach"}
<path d="m7.1 10.95 4.8-4.8a2.5 2.5 0 0 1 3.54 3.54l-6.5 6.5a4 4 0 0 1-5.66-5.66l6.15-6.15" />
{:else if name === "send"}
<path class="filled" d="M17.72 2.27a.75.75 0 0 0-.77-.18L2.48 7.5a.75.75 0 0 0 .06 1.42l6.14 1.84 1.84 6.14a.75.75 0 0 0 1.42.06l5.41-14.47a.75.75 0 0 0-.18-.77Zm-7.9 7.43L5.1 8.29l9.56-3.58-4.84 4.99Zm1.89 5.2-1.41-4.72 4.99-4.84-3.58 9.56Z" />
{:else if name === "dismiss"}
<path d="m5 5 10 10M15 5 5 15" />
{:else if name === "download"}
<path d="M10 2.75v9.5m-3.5-3.5 3.5 3.5 3.5-3.5M3.25 15v1.25c0 .41.34.75.75.75h12a.75.75 0 0 0 .75-.75V15" />
{:else if name === "document"}
<path d="M5.25 2.75h6l3.5 3.5v10.5a.75.75 0 0 1-.75.75H5.25a.75.75 0 0 1-.75-.75V3.5c0-.41.34-.75.75-.75Z" /><path d="M11.25 2.75v3.5h3.5M7 10h5.5M7 13h5.5" />
{:else if name === "lock"}
<rect x="4" y="8" width="12" height="9" rx="2" /><path d="M6.5 8V6a3.5 3.5 0 0 1 7 0v2M10 11v3" />
{:else if name === "check"}
<path d="m4 10.25 3.65 3.65L16 5.75" />
{:else if name === "bot"}
<rect x="3" y="5.5" width="14" height="11" rx="3" /><path d="M10 2.5v3M6.5 10h.01M13.5 10h.01M7 13.25c1.8 1.25 4.2 1.25 6 0" />
{:else if name === "more"}
<circle class="filled" cx="4" cy="10" r="1.25" /><circle class="filled" cx="10" cy="10" r="1.25" /><circle class="filled" cx="16" cy="10" r="1.25" />
{:else if name === "chevron-down"}
<path d="m5.25 7.5 4.75 4.75 4.75-4.75" />
{:else if name === "panel"}
<rect x="2.75" y="3.25" width="14.5" height="13.5" rx="2" /><path d="M12.25 3.25v13.5" />
{/if}
</svg>
<style>
.fluent-icon { display: block; flex: 0 0 auto; }
path, circle, rect { stroke: currentColor; stroke-width: 1.45; stroke-linecap: round; stroke-linejoin: round; }
.filled { fill: currentColor; stroke: none; }
</style>

View File

@ -1,5 +1,6 @@
<script>
import Markdown from "./Markdown.svelte";
import Icon from "./Icon.svelte";
let { call, result = null } = $props();
let open = $state(false);
@ -15,10 +16,10 @@
<article class:open class="tool-call">
<button type="button" class="tool-call-summary" onclick={() => open = !open} aria-expanded={open}>
<span class="tool-call-icon" aria-hidden="true"></span>
<span class="tool-call-icon"><Icon name="more" size={16} /></span>
<span class="tool-call-title"><small>工具调用</small><strong>{call?.name || result?.tool_name || "未知工具"}</strong></span>
<span class="tool-call-status">{result ? "已完成" : "已调用"}</span>
<span class="tool-call-chevron" aria-hidden="true"></span>
<span class="tool-call-chevron"><Icon name="chevron-down" size={14} /></span>
</button>
{#if open}
<div class="tool-call-details">
@ -29,4 +30,3 @@
</div>
{/if}
</article>

View File

@ -1,5 +1,6 @@
<script>
import Markdown from "./Markdown.svelte";
import Icon from "./Icon.svelte";
let { turn } = $props();
@ -17,7 +18,7 @@
</script>
<div class="message assistant active-turn">
<div class="avatar">P</div>
<div class="avatar"><Icon name="bot" size={15} /></div>
<div class="message-content">
{#each turn.blocks as block (block.id)}
{#if block.type === "reasoning"}

View File

@ -19,36 +19,34 @@
});
const running = $derived(chat.turn?.status === "running");
const turnLabel = $derived(chat.turn ? `TURN ${String(chat.turn.id ?? "").slice(0, 6).toUpperCase()}` : "");
const turnLabel = $derived(chat.turn ? `Turn ${String(chat.turn.id ?? "").slice(0, 6).toUpperCase()}` : "");
const ctx = $derived(chat.turn?.usage?.prompt_tokens != null
? `${(chat.turn.usage.prompt_tokens / 1000).toFixed(1)}k` : null);
</script>
<div class="spine mono">
<div class="spine mono" title={version}>
{#if running}
<span class="spine-turn active"><i class="pulse-dot active"></i>{turnLabel} · STREAMING</span>
<span class="spine-turn active"><i class="pulse-dot active"></i>{turnLabel} · 生成中</span>
{#if rate != null}<span class="spine-rate">{rate} tok/s</span>{/if}
{#if ctx}<span>ctx {ctx}</span>{/if}
{:else if chat.turn}
<span class="spine-turn idle"><i class="pulse-dot idle"></i>IDLE</span>
<span>最近 {turnLabel}</span>
<span class="spine-turn idle"><i class="pulse-dot idle"></i>空闲</span>
<span class="spine-context">最近 {turnLabel}</span>
{:else}
<span class="spine-turn idle"><i class="pulse-dot idle"></i>READY</span>
<span class="spine-turn idle"><i class="pulse-dot idle"></i>就绪</span>
{/if}
<span class="spine-right">
<span class:spine-ok={chat.connected} class:spine-down={!chat.connected}>{chat.connected ? "已连接" : "重连中"}</span>
{#if version}<span>{version}</span>{/if}
</span>
</div>
<style>
.spine { display: flex; align-items: center; gap: 14px; font-size: 11.5px; color: var(--spine-text);
background: var(--spine-bg); border-bottom: 1px solid var(--line); padding: 8px 16px; font-variant-numeric: tabular-nums; }
.spine { display: flex; align-items: center; gap: 10px; min-height: 32px; margin-left: auto; padding: 0 11px; border: 1px solid var(--line); border-radius: 6px; font-size: 11.5px; color: var(--text-soft); background: var(--panel-2); font-variant-numeric: tabular-nums; }
.spine-turn { display: inline-flex; align-items: center; gap: 7px; font-weight: 700; }
.spine-turn.active { color: var(--spine-accent); }
.spine-turn.idle { color: var(--spine-signal); }
.spine-rate { color: var(--spine-signal); }
.spine-right { margin-left: auto; display: inline-flex; gap: 14px; color: var(--spine-faint); }
.spine-ok { color: var(--spine-signal); }
.spine-down { color: var(--spine-accent); }
.spine-turn.active, .spine-rate { color: var(--accent); }
.spine-turn.idle, .spine-ok { color: var(--signal); }
.spine-right { display: inline-flex; gap: 10px; padding-left: 10px; border-left: 1px solid var(--line); color: var(--muted); }
.spine-down { color: var(--danger); }
@media (max-width: 1040px) { .spine-context, .spine-rate, .spine > span:not(.spine-turn):not(.spine-right) { display: none; } }
@media (max-width: 680px) { .spine { display: none; } }
</style>

View File

@ -0,0 +1,149 @@
<script>
import { onMount } from "svelte";
import Icon from "../Icon.svelte";
import { ACCENT_THEMES, applyAccent, applyTheme, preferredAccent, preferredTheme } from "../theme.js";
let theme = $state("light");
let accent = $state("blue");
function setTheme(value) {
theme = value;
applyTheme(value);
}
function setAccent(value) {
accent = value;
applyAccent(value);
}
onMount(() => {
theme = preferredTheme();
accent = preferredAccent();
const sync = (event) => {
if (event.detail?.theme) theme = event.detail.theme;
if (event.detail?.accent) accent = event.detail.accent;
};
window.addEventListener("picobot-appearance-change", sync);
return () => window.removeEventListener("picobot-appearance-change", sync);
});
</script>
<div class="appearance-layout">
<div class="appearance-sections">
<section class="panel appearance-card">
<div class="section-heading">
<div><h2>显示模式</h2><p>选择更适合当前环境的页面明暗度。</p></div>
</div>
<div class="mode-grid" role="group" aria-label="显示模式">
<button class:selected={theme === "light"} aria-pressed={theme === "light"} onclick={() => setTheme("light")}>
<span class="mode-preview light-preview"><i></i><b></b><em></em></span>
<span class="option-copy"><Icon name="sun" size={17} /><strong>浅色</strong></span>
{#if theme === "light"}<span class="selected-mark"><Icon name="check" size={13} /></span>{/if}
</button>
<button class:selected={theme === "dark"} aria-pressed={theme === "dark"} onclick={() => setTheme("dark")}>
<span class="mode-preview dark-preview"><i></i><b></b><em></em></span>
<span class="option-copy"><Icon name="moon" size={17} /><strong>深色</strong></span>
{#if theme === "dark"}<span class="selected-mark"><Icon name="check" size={13} /></span>{/if}
</button>
</div>
</section>
<section class="panel appearance-card">
<div class="section-heading">
<div><h2>主题色</h2><p>品牌色会应用到主操作、选中状态、进度和焦点提示。</p></div>
</div>
<div class="accent-grid" role="group" aria-label="主题色">
{#each ACCENT_THEMES as option}
<button
class:selected={accent === option.id}
aria-pressed={accent === option.id}
style={`--swatch:${option.swatch}`}
onclick={() => setAccent(option.id)}
>
<span class="swatch">{#if accent === option.id}<Icon name="check" size={14} />{/if}</span>
<span><strong>{option.label}</strong><small>{option.description}</small></span>
</button>
{/each}
</div>
</section>
<p class="storage-note">外观设置仅保存在当前浏览器中,不会写入 Gateway 配置或同步到其他设备。</p>
</div>
<aside class="panel preview-card" aria-label="主题实时预览">
<div class="preview-heading"><span>实时预览</span><span class="preview-status"><i></i>已应用</span></div>
<div class="preview-window">
<div class="preview-titlebar"><span class="preview-logo"><Icon name="bot" size={15} /></span><b>PicoBot</b><span class="window-dots">•••</span></div>
<div class="preview-body">
<div class="preview-nav">
<span></span><span class="active"></span><span></span><span></span>
</div>
<div class="preview-content">
<div class="preview-line wide"></div><div class="preview-line short"></div>
<div class="preview-message"><Icon name="bot" size={13} /><span></span></div>
<div class="preview-action"><span></span><button>继续</button></div>
</div>
</div>
</div>
<p>当前为 <strong>{ACCENT_THEMES.find((option) => option.id === accent)?.label}</strong> · {theme === "dark" ? "深色模式" : "浅色模式"}</p>
</aside>
</div>
<style>
.appearance-layout { display: grid; grid-template-columns: minmax(0, 1fr) 300px; gap: 18px; align-items: start; }
.appearance-sections { display: grid; gap: 14px; }
.appearance-card { padding: 20px; }
.section-heading h2 { margin: 0; font-size: 16px; line-height: 22px; }
.section-heading p { margin: 3px 0 0; color: var(--muted); font-size: 12px; line-height: 18px; }
.mode-grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 12px; margin-top: 16px; }
.mode-grid > button { position: relative; padding: 10px; border: 1px solid var(--line-strong); border-radius: 8px; color: var(--text); background: var(--panel); text-align: left; cursor: pointer; }
.mode-grid > button:hover { background: var(--panel-2); }
.mode-grid > button.selected { border-color: var(--accent); box-shadow: 0 0 0 1px var(--accent); }
.mode-preview { position: relative; height: 80px; display: block; overflow: hidden; border: 1px solid #d1d1d1; border-radius: 5px; background: #f5f5f5; }
.mode-preview::before { content: ""; position: absolute; inset: 0 auto 0 0; width: 24%; border-right: 1px solid #d1d1d1; background: #fafafa; }
.mode-preview i, .mode-preview b, .mode-preview em { position: absolute; display: block; border-radius: 2px; }
.mode-preview i { top: 12px; left: 31%; width: 46%; height: 7px; background: var(--accent); }
.mode-preview b { top: 27px; left: 31%; width: 57%; height: 30px; border: 1px solid #e0e0e0; background: #fff; }
.mode-preview em { top: 63px; left: 31%; width: 40%; height: 5px; background: #d1d1d1; }
.dark-preview { border-color: #424242; background: #1f1f1f; }
.dark-preview::before { border-color: #424242; background: #242424; }
.dark-preview b { border-color: #424242; background: #292929; }
.dark-preview em { background: #666; }
.option-copy { display: flex; align-items: center; gap: 7px; margin-top: 9px; }
.option-copy strong { font-size: 13px; }
.selected-mark { position: absolute; right: 8px; bottom: 8px; width: 20px; height: 20px; display: grid; place-items: center; border-radius: 50%; color: var(--accent-contrast); background: var(--accent); }
.accent-grid { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 8px; margin-top: 16px; }
.accent-grid button { min-width: 0; display: flex; align-items: center; gap: 10px; padding: 10px; border: 1px solid var(--line); border-radius: 6px; color: var(--text); background: var(--panel); text-align: left; cursor: pointer; }
.accent-grid button:hover { border-color: var(--line-strong); background: var(--panel-2); }
.accent-grid button.selected { border-color: var(--accent); background: var(--accent-soft); }
.swatch { width: 30px; height: 30px; flex: 0 0 30px; display: grid; place-items: center; border: 1px solid rgb(0 0 0 / 12%); border-radius: 6px; color: #fff; background: var(--swatch); }
.accent-grid strong, .accent-grid small { display: block; overflow: hidden; white-space: nowrap; text-overflow: ellipsis; }
.accent-grid strong { font-size: 12px; }
.accent-grid small { margin-top: 2px; color: var(--muted); font-size: 10px; }
.storage-note { margin: -2px 2px 0; color: var(--muted); font-size: 11px; line-height: 17px; }
.preview-card { position: sticky; top: 0; overflow: hidden; }
.preview-heading { display: flex; align-items: center; justify-content: space-between; padding: 13px 14px; border-bottom: 1px solid var(--line); font-size: 12px; font-weight: 600; }
.preview-status { display: inline-flex; align-items: center; gap: 5px; color: var(--signal); font-size: 10px; }
.preview-status i { width: 6px; height: 6px; border-radius: 50%; background: currentColor; }
.preview-window { margin: 14px; overflow: hidden; border: 1px solid var(--line-strong); border-radius: 7px; background: var(--bg); box-shadow: var(--shadow-8); }
.preview-titlebar { height: 38px; display: flex; align-items: center; gap: 7px; padding: 0 9px; border-bottom: 1px solid var(--line); background: var(--panel); font-size: 11px; }
.preview-logo { width: 23px; height: 23px; display: grid; place-items: center; border-radius: 5px; color: var(--accent-contrast); background: var(--accent); }
.window-dots { margin-left: auto; color: var(--muted); letter-spacing: 2px; }
.preview-body { height: 180px; display: grid; grid-template-columns: 48px 1fr; }
.preview-nav { display: grid; align-content: start; gap: 8px; padding: 12px 8px; border-right: 1px solid var(--line); background: var(--panel); }
.preview-nav span { height: 7px; border-radius: 3px; background: var(--line-strong); }
.preview-nav span.active { height: 20px; border-left: 2px solid var(--accent); border-radius: 3px; background: var(--accent-soft); }
.preview-content { padding: 18px 14px; }
.preview-line { height: 7px; border-radius: 3px; background: var(--line-strong); }
.preview-line.wide { width: 70%; }
.preview-line.short { width: 42%; margin-top: 7px; opacity: .65; }
.preview-message { display: flex; align-items: center; gap: 7px; margin-top: 24px; color: var(--accent); }
.preview-message span { width: 72%; height: 34px; border: 1px solid var(--line); border-radius: 6px; background: var(--panel); box-shadow: var(--shadow-2); }
.preview-action { display: flex; align-items: center; gap: 7px; margin-top: 20px; }
.preview-action span { flex: 1; height: 28px; border: 1px solid var(--line); border-radius: 4px; background: var(--panel); }
.preview-action button { height: 28px; padding: 0 9px; border: 0; border-radius: 4px; color: var(--accent-contrast); background: var(--accent); font-size: 10px; }
.preview-card > p { margin: 0; padding: 0 14px 14px; color: var(--muted); font-size: 11px; }
.preview-card > p strong { color: var(--text); }
@media (max-width: 1100px) { .appearance-layout { grid-template-columns: 1fr; } .preview-card { position: static; } .preview-window { max-width: 420px; } }
@media (max-width: 620px) { .mode-grid, .accent-grid { grid-template-columns: 1fr; } .mode-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); } .accent-grid button { min-height: 52px; } }
</style>

View File

@ -1,30 +1,16 @@
<script>
let { depth = 0, cap = 1, segments = 8, label = "capacity" } = $props();
let { depth = 0, cap = 1, label = "capacity" } = $props();
const ratio = $derived(cap > 0 ? Math.min(depth / cap, 1) : 0);
const lit = $derived(Math.round(ratio * segments));
const nearFull = $derived(ratio > 0.9);
const barColor = $derived(nearFull ? "var(--danger)" : "var(--signal)");
</script>
<div class="meter" role="meter" aria-label={label} aria-valuemin={0} aria-valuemax={cap} aria-valuenow={depth}>
{#each Array(segments) as _, i}
<span class="seg" class:lit={i < lit} style:--seg-color={barColor}></span>
{/each}
<span class="fill" style={`width:${ratio * 100}%;--meter-color:${barColor}`}></span>
</div>
<style>
.meter { display: flex; gap: 3px; align-items: stretch; height: 100%; }
.seg {
flex: 1;
min-width: 4px;
border-radius: 2px;
background: var(--panel-2);
border: 1px solid var(--line);
transition: background .2s, border-color .2s;
}
.seg.lit {
background: var(--seg-color);
border-color: var(--seg-color);
}
.meter { height: 4px; overflow: hidden; border-radius: 2px; background: var(--panel-2); }
.fill { display: block; height: 100%; border-radius: inherit; background: var(--meter-color); transition: width .2s ease, background .2s ease; }
</style>

View File

@ -16,9 +16,9 @@
</div>
<style>
.tile { display: flex; flex-direction: column; gap: 6px; padding: 15px 17px; transition: border-color .18s ease, transform .18s ease; }
.tile:hover { border-color: var(--line-strong); transform: translateY(-2px); }
.value { font-size: 30px; font-weight: 700; line-height: 1.1; letter-spacing: -0.02em; color: var(--text); font-variant-numeric: tabular-nums; }
.tile { display: flex; flex-direction: column; gap: 5px; padding: 15px 17px; transition: border-color .1s ease; }
.tile:hover { border-color: var(--line-strong); }
.value { font-size: 28px; font-weight: 600; line-height: 1.2; letter-spacing: -0.02em; color: var(--text); font-variant-numeric: tabular-nums; }
.sub { font-size: 12px; color: var(--muted); }
.spark { height: 28px; margin-top: 4px; }
</style>

View File

@ -1,4 +1,16 @@
const STORAGE_KEY = "picobot-theme";
const ACCENT_STORAGE_KEY = "picobot-accent";
export const ACCENT_THEMES = Object.freeze([
{ id: "blue", label: "海蓝", swatch: "#0f6cbd", description: "Fluent 默认蓝" },
{ id: "purple", label: "紫罗兰", swatch: "#5c2e91", description: "沉静而专注" },
{ id: "teal", label: "青绿", swatch: "#006c4c", description: "清晰且克制" },
{ id: "green", label: "森林", swatch: "#107c10", description: "稳定与活力" },
{ id: "orange", label: "暖橙", swatch: "#ca5010", description: "醒目而友好" },
{ id: "magenta", label: "莓红", swatch: "#9b0062", description: "鲜明有个性" }
]);
const ACCENT_IDS = new Set(ACCENT_THEMES.map((theme) => theme.id));
export function preferredTheme() {
const saved = localStorage.getItem(STORAGE_KEY);
@ -6,11 +18,34 @@ export function preferredTheme() {
return matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light";
}
export function preferredAccent() {
const saved = localStorage.getItem(ACCENT_STORAGE_KEY);
return ACCENT_IDS.has(saved) ? saved : "blue";
}
function notifyAppearance() {
window.dispatchEvent(new CustomEvent("picobot-appearance-change", {
detail: {
theme: document.documentElement.dataset.theme,
accent: document.documentElement.dataset.accent
}
}));
}
export function applyTheme(theme) {
document.documentElement.dataset.theme = theme;
document.documentElement.style.colorScheme = theme;
const next = theme === "dark" ? "dark" : "light";
document.documentElement.dataset.theme = next;
document.documentElement.style.colorScheme = next;
document
.querySelector('meta[name="theme-color"]')
?.setAttribute("content", theme === "dark" ? "#0b1017" : "#eef1f5");
localStorage.setItem(STORAGE_KEY, theme);
?.setAttribute("content", next === "dark" ? "#1f1f1f" : "#f5f5f5");
localStorage.setItem(STORAGE_KEY, next);
notifyAppearance();
}
export function applyAccent(accent) {
const next = ACCENT_IDS.has(accent) ? accent : "blue";
document.documentElement.dataset.accent = next;
localStorage.setItem(ACCENT_STORAGE_KEY, next);
notifyAppearance();
}

View File

@ -6,6 +6,7 @@
import Markdown from "../lib/Markdown.svelte";
import ToolCallCard from "../lib/ToolCallCard.svelte";
import TurnView from "../lib/TurnView.svelte";
import Icon from "../lib/Icon.svelte";
let { notify } = $props();
let sessions = $state([]);
@ -377,9 +378,9 @@
<section class:todo-open={todoOpen && currentPlan} class="page active chat-layout">
<aside class="sessions-panel">
<button class="primary full" onclick={() => send({ type: "create_session", title: null })}> 新建对话</button>
<label class="search"><span></span><input bind:value={search} placeholder="搜索对话" /></label>
<div class="session-list-head"><span class="label-caps">SESSIONS</span><span class="session-count">{filteredSessions.length}</span></div>
<button class="primary full" onclick={() => send({ type: "create_session", title: null })}><Icon name="add" size={16} />新建对话</button>
<label class="search"><Icon name="search" size={16} /><input bind:value={search} placeholder="搜索对话" /></label>
<div class="session-list-head"><span class="label-caps">最近对话</span><span class="session-count">{filteredSessions.length}</span></div>
<div class="session-list">
{#each filteredSessions as session (session.session_id)}
<button class:active={session.session_id === currentId} class="session-item" onclick={() => loadSession(session.session_id)}>
@ -394,23 +395,23 @@
<div class="chat-heading-actions">
{#if currentPlan}
<button class:active={todoOpen} class="todo-toggle" onclick={() => todoOpen = !todoOpen} aria-expanded={todoOpen}>
任务 {completedItems}/{currentPlan.items.length}
<Icon name="panel" size={14} />任务 {completedItems}/{currentPlan.items.length}
</button>
{/if}
<Tooltip.Root>
<Tooltip.Trigger class="icon-button" aria-label="刷新会话" onclick={() => { send({ type: "list_sessions", include_archived: false }); if (currentId) send({ type: "get_session_plan", session_id: currentId }); }}></Tooltip.Trigger>
<Tooltip.Trigger class="icon-button" aria-label="刷新会话" onclick={() => { send({ type: "list_sessions", include_archived: false }); if (currentId) send({ type: "get_session_plan", session_id: currentId }); }}><Icon name="refresh" size={18} /></Tooltip.Trigger>
<Tooltip.Portal><Tooltip.Content class="tooltip" sideOffset={7}>刷新会话<Tooltip.Arrow class="tooltip-arrow" /></Tooltip.Content></Tooltip.Portal>
</Tooltip.Root>
</div>
</div>
<div class="messages" bind:this={messageBox}>
{#if messages.length === 0 && !activeTurn}
<div class="empty"><div class="empty-logo">P</div><h2>今天想做些什么?</h2><p>消息与 CLI 客户端使用同一套会话、记忆和工具能力。</p></div>
<div class="empty"><div class="empty-logo"><Icon name="bot" size={26} /></div><h2>今天想做些什么?</h2><p>消息与 CLI 客户端使用同一套会话、记忆和工具能力。</p></div>
{/if}
{#each messages as message (message.id)}
{#if message.role !== "tool" && (message.content || message.tool_calls?.length || message.attachments?.length)}
<div class:user={message.role === "user"} class:assistant={message.role !== "user"} class:has-tools={message.tool_calls?.length} class="message">
<div class="avatar">{message.role === "user" ? "你" : "P"}</div>
<div class="avatar">{#if message.role === "user"}{:else}<Icon name="bot" size={15} />{/if}</div>
<div class="message-content">
{#if message.reasoning_content}
<details class="reasoning-block historical">
@ -430,13 +431,13 @@
<a class="image-preview-link" href={attachmentUrl(message, attachment)} target="_blank" rel="noreferrer" aria-label={`查看原图 ${attachment.name}`}>
<img class="image-preview" src={attachmentUrl(message, attachment, true)} alt={attachment.name} />
</a>
<div class="attachment-meta"><div><strong>{attachment.name}</strong><small>{attachment.mime_type || attachment.media_type}</small></div><a href={attachmentUrl(message, attachment)} download={attachment.name} aria-label={`下载 ${attachment.name}`}>↓</a></div>
<div class="attachment-meta"><div><strong>{attachment.name}</strong><small>{attachment.mime_type || attachment.media_type}</small></div><a href={attachmentUrl(message, attachment)} download={attachment.name} aria-label={`下载 ${attachment.name}`}><Icon name="download" size={16} /></a></div>
</article>
{:else}
<article class="attachment-card">
<span class="attachment-icon"></span>
<span class="attachment-icon"><Icon name="document" size={18} /></span>
<div><strong>{attachment.name}</strong><small>{attachment.mime_type || attachment.media_type}</small></div>
<a href={attachmentUrl(message, attachment)} download={attachment.name} aria-label={`下载 ${attachment.name}`}>↓</a>
<a href={attachmentUrl(message, attachment)} download={attachment.name} aria-label={`下载 ${attachment.name}`}><Icon name="download" size={16} /></a>
</article>
{/if}
{/each}
@ -454,7 +455,7 @@
{/if}
{/each}
{#if activeTurn}<TurnView turn={activeTurn} />
{:else if thinking}<div class="message assistant typing"><div class="avatar">P</div><div class="bubble"><span class="pulse"></span>正在思考…</div></div>{/if}
{:else if thinking}<div class="message assistant typing"><div class="avatar"><Icon name="bot" size={15} /></div><div class="bubble"><span class="pulse"></span>正在思考…</div></div>{/if}
</div>
<form class="composer" onsubmit={(event) => { event.preventDefault(); submit(); }} ondragover={(event) => event.preventDefault()} ondrop={dropFiles}>
{#if commandSuggestions.length}
@ -479,14 +480,14 @@
<div class="pending-uploads">
{#each pendingUploads as upload (upload.localId)}
<div class:error={upload.status === "error"} class="pending-upload">
<span></span><div><strong>{upload.name}</strong><small>{upload.status === "uploading" ? `上传中 ${upload.progress}%` : upload.status === "error" ? upload.error : formatBytes(upload.size)}</small></div>
<button type="button" aria-label={`移除 ${upload.name}`} onclick={() => removeUpload(upload.localId)}>×</button>
<Icon name="document" size={16} /><div><strong>{upload.name}</strong><small>{upload.status === "uploading" ? `上传中 ${upload.progress}%` : upload.status === "error" ? upload.error : formatBytes(upload.size)}</small></div>
<button type="button" aria-label={`移除 ${upload.name}`} onclick={() => removeUpload(upload.localId)}><Icon name="dismiss" size={14} /></button>
</div>
{/each}
</div>
{/if}
<input class="file-input" bind:this={fileInput} type="file" multiple onchange={(event) => { addFiles(event.currentTarget.files); event.currentTarget.value = ""; }} />
<button class="attach" type="button" aria-label="添加附件" onclick={() => fileInput?.click()}></button>
<button class="attach" type="button" aria-label="添加附件" onclick={() => fileInput?.click()}><Icon name="attach" size={18} /></button>
<textarea
bind:this={input}
bind:value={draft}
@ -501,7 +502,7 @@
aria-activedescendant={commandSuggestions.length ? `slash-command-${selectedCommand}` : undefined}
placeholder="输入消息,输入 / 查看命令"
></textarea>
<button class="send" type="submit" aria-label="发送" disabled={!chat.connected || (!draft.trim() && !pendingUploads.some((upload) => upload.status === "ready")) || pendingUploads.some((upload) => upload.status === "uploading")}>↑</button>
<button class="send" type="submit" aria-label="发送" disabled={!chat.connected || (!draft.trim() && !pendingUploads.some((upload) => upload.status === "ready")) || pendingUploads.some((upload) => upload.status === "uploading")}><Icon name="send" size={18} /></button>
<small><span class:online={chat.connected}>{chat.connected ? "已连接" : "已断开,正在重连"}</span><span>/ 打开命令 · Shift+Enter 换行</span></small>
</form>
</div>
@ -509,7 +510,7 @@
<aside class="todo-panel" aria-label="当前任务计划">
<div class="todo-heading">
<div><small>当前计划</small><strong>{currentPlan.objective}</strong></div>
<button class="icon-button" aria-label="关闭任务侧栏" onclick={() => todoOpen = false}>×</button>
<button class="icon-button" aria-label="关闭任务侧栏" onclick={() => todoOpen = false}><Icon name="dismiss" size={18} /></button>
</div>
<div class="todo-progress"><span style={`width:${currentPlan.items.length ? completedItems / currentPlan.items.length * 100 : 0}%`}></span></div>
<div class="todo-summary"><span>{completedItems}/{currentPlan.items.length} 已完成</span><span>v{currentPlan.version}</span></div>

View File

@ -1,6 +1,7 @@
<script>
import { onMount } from "svelte";
import { api } from "../lib/api.js";
import Icon from "../lib/Icon.svelte";
const MAX_LINES = 2000;
const LEVELS = ["全部", "INF", "WRN", "ERR"];
@ -142,11 +143,11 @@
<button class="chip" class:active={level === lv} onclick={() => (level = lv)}>{lv}</button>
{/each}
</div>
<label class="search grow"><span></span><input bind:value={search} placeholder="搜索日志" /></label>
<label class="search grow"><Icon name="search" size={16} /><input bind:value={search} placeholder="搜索日志" /></label>
<button class="secondary" class:active={paused} onclick={() => (paused = !paused)}>
{paused ? "▶ 继续" : "⏸ 暂停"}
</button>
<button class="secondary" onclick={download}>下载</button>
<button class="secondary" onclick={download}><Icon name="download" size={16} />下载</button>
</div>
<div class="status-bar" class:ok={connected} class:fail={!connected}>
@ -182,7 +183,7 @@
border: 1px solid var(--line);
background: var(--panel);
color: var(--muted);
border-radius: 99px;
border-radius: 4px;
padding: 5px 12px;
font-size: 12px;
font-weight: 500;

View File

@ -1,6 +1,7 @@
<script>
import { onMount } from "svelte";
import { api, formatTime } from "../lib/api.js";
import Icon from "../lib/Icon.svelte";
let query = $state("");
let category = $state("");
@ -98,7 +99,7 @@
<section class="page active content-page">
<form class="toolbar filters" onsubmit={(event) => { event.preventDefault(); applyFilters(); }}>
<label class="search grow"><span></span><input bind:value={query} placeholder="搜索记忆内容或键" /></label>
<label class="search grow"><Icon name="search" size={16} /><input bind:value={query} placeholder="搜索记忆内容或键" /></label>
<select bind:value={category}><option value="">全部</option><option value="knowledge">Knowledge</option><option value="timeline">Timeline</option></select>
<button class="secondary" type="submit">查询</button>
</form>
@ -161,11 +162,11 @@
.card-actions { display: flex; gap: 8px; flex-wrap: wrap; }
.card > .card-actions { margin-top: 14px; }
.danger-outline { border-color: var(--danger-border); color: var(--danger); }
.danger-solid { border-radius: 9px; padding: 9px 14px; font-weight: 650; cursor: pointer; border: 1px solid var(--danger); background: var(--danger); color: #fff; }
.danger-solid { border-radius: 4px; padding: 5px 12px; font-weight: 600; cursor: pointer; border: 1px solid var(--danger); background: var(--danger); color: #fff; }
.danger-solid:hover { filter: brightness(1.08); }
.edit-field { display: grid; gap: 6px; margin-top: 12px; }
.edit-field > span { color: var(--muted); font-size: 12px; }
.edit-field textarea { width: 100%; min-height: 90px; resize: vertical; border: 1px solid var(--line); border-radius: 8px; background: var(--panel-2); color: var(--text); padding: 10px; font: 14px/1.6 var(--font-ui); }
.edit-field textarea { width: 100%; min-height: 90px; resize: vertical; border: 1px solid var(--line-strong); border-radius: 4px; background: var(--panel); color: var(--text); padding: 10px; font: 14px/1.6 var(--font-ui); }
.edit-field input[type="range"] { width: 100%; accent-color: var(--accent); }
.action-error { margin: 12px 0 0; font-size: 13px; }
.confirm { margin-top: 12px; border-top: 1px solid var(--line); padding-top: 12px; display: grid; gap: 10px; }

View File

@ -63,14 +63,14 @@
<div class="status-bar panel">
<div class="status-left">
<span class="pulse-dot" class:active={running}></span>
<strong class="mono">{running ? "RUNNING" : "IDLE"}</strong>
<span class="label-caps">GEN {status.generation}</span>
<strong>{running ? "运行中" : "空闲"}</strong>
<span class="label-caps">运行代 {status.generation}</span>
</div>
<div class="status-meta mono">
<span>{uptime}</span>
<span>v{status.version}</span>
<span>WS {status.ws_connections}</span>
<span>BG {status.background_tasks}</span>
<span>WebSocket {status.ws_connections}</span>
<span>后台任务 {status.background_tasks}</span>
<span class="label-caps">{status.phase}</span>
</div>
</div>
@ -80,14 +80,14 @@
{/if}
<div class="tiles">
<MetricTile label="SESSIONS" value={fmt(status.sessions?.total)} sub="{status.sessions?.active_turns || 0} active" />
<MetricTile label="TOKENS" value={fmt(totalTokens)} sub="cost ${totalCost}" />
<MetricTile label="TOOL CALLS" value={fmt(status.metrics?.tool_calls)} />
<MetricTile label="TURNS" value={fmt(status.metrics?.turns)} sub="p95 {fmt(p95)}ms" />
<MetricTile label="会话" value={fmt(status.sessions?.total)} sub="{status.sessions?.active_turns || 0} 个活跃 Turn" />
<MetricTile label="Tokens" value={fmt(totalTokens)} sub="成本 ${totalCost}" />
<MetricTile label="工具调用" value={fmt(status.metrics?.tool_calls)} />
<MetricTile label="Turns" value={fmt(status.metrics?.turns)} sub="p95 {fmt(p95)}ms" />
</div>
<div class="panel section">
<span class="label-caps">PROVIDERS</span>
<span class="label-caps">模型提供商</span>
<table class="provider-table">
<thead>
<tr><th>名称</th><th>模型</th><th>状态</th><th>延迟</th><th>Tokens In</th><th>Tokens Out</th><th>Cost</th></tr>
@ -111,7 +111,7 @@
</div>
<div class="panel section">
<span class="label-caps">MESSAGE BUS</span>
<span class="label-caps">消息总线</span>
<div class="bus-grid">
<div class="bus-item">
<span class="bus-label">Inbound</span>
@ -138,7 +138,7 @@
<div class="two-col">
<div class="panel section">
<span class="label-caps">CHANNELS</span>
<span class="label-caps">渠道</span>
<div class="channel-list">
{#each status.channels || [] as ch}
<div class="channel-row">
@ -152,7 +152,7 @@
</div>
<div class="panel section">
<span class="label-caps">SCHEDULER</span>
<span class="label-caps">调度器</span>
<div class="sched-grid mono">
<span>Jobs: {status.scheduler?.jobs || 0}</span>
<span>Enabled: {status.scheduler?.enabled || 0}</span>
@ -173,8 +173,8 @@
.status-meta { display: flex; gap: 16px; font-size: 12px; color: var(--text-soft); flex-wrap: wrap; font-variant-numeric: tabular-nums; }
.warn-bar { color: var(--accent); font-size: 12px; margin-bottom: 10px; }
.tiles { display: grid; grid-template-columns: repeat(4, 1fr); gap: 10px; margin-bottom: 14px; }
.section { padding: 16px 18px; margin-bottom: 14px; display: flex; flex-direction: column; gap: 12px; }
.provider-table { width: 100%; border-collapse: collapse; font-size: 12.5px; font-variant-numeric: tabular-nums; }
.section { padding: 16px 18px; margin-bottom: 14px; display: flex; flex-direction: column; gap: 12px; overflow-x: auto; }
.provider-table { width: 100%; min-width: 720px; border-collapse: collapse; font-size: 12.5px; font-variant-numeric: tabular-nums; }
.provider-table th { text-align: left; color: var(--muted); font-weight: 500; padding: 4px 8px; border-bottom: 1px solid var(--line); font-size: 11px; }
.provider-table td { padding: 8px 8px; border-bottom: 1px solid var(--line); color: var(--text-soft); }
.provider-table tr:last-child td { border-bottom: none; }

View File

@ -1,5 +1,6 @@
<script>
import { api } from "../lib/api.js";
import Icon from "../lib/Icon.svelte";
let { onpaired } = $props();
let code = $state("");
@ -31,8 +32,8 @@
<main class="pairing-screen">
<section class="pairing-card">
<div class="pairing-brand"><span class="brand-mark">P</span><strong>PicoBot</strong></div>
<div class="pairing-icon" aria-hidden="true"></div>
<div class="pairing-brand"><span class="brand-mark"><Icon name="bot" size={22} /></span><strong>PicoBot</strong></div>
<div class="pairing-icon"><Icon name="lock" size={24} /></div>
<h1>配对此浏览器</h1>
<p>这是一个新浏览器。请在运行 Gateway 的设备上生成一次性配对码,然后在下方输入。</p>
<code class="pairing-command">picobot pair</code>

View File

@ -2,9 +2,10 @@
import { onMount } from "svelte";
import { Tabs } from "bits-ui";
import { api } from "../lib/api.js";
import AppearanceSettings from "../lib/components/AppearanceSettings.svelte";
let { notify } = $props();
let tab = $state("config");
let tab = $state("appearance");
let content = $state("");
let original = $state("");
let path = $state("");
@ -14,7 +15,8 @@
let pollTimer = null;
const isConfig = $derived(tab === "config");
const title = $derived(tab === "config" ? "运行配置" : tab === "user" ? "用户档案" : "Agent 行为准则");
const isAppearance = $derived(tab === "appearance");
const title = $derived(tab === "config" ? "运行配置" : tab === "user" ? "用户档案" : tab === "agents" ? "Agent 行为准则" : "页面外观");
const isDirty = $derived(content !== original);
const jsonParse = $derived.by(() => {
@ -32,6 +34,7 @@
async function load() {
loading = true;
if (isAppearance) { loading = false; return; }
try {
if (isConfig) {
const result = await api("/api/config");
@ -107,7 +110,7 @@
onMount(() => {
load();
startPolling();
if (tab === "config") startPolling();
return stopPolling;
});
</script>
@ -115,12 +118,14 @@
<section class="page active content-page">
<div class="settings-grid">
<Tabs.Root value={tab} onValueChange={changeTab} orientation="vertical">
<Tabs.List class="settings-nav" aria-label="配置文件">
<Tabs.Trigger value="config">config.json</Tabs.Trigger><Tabs.Trigger value="user">USER.md</Tabs.Trigger><Tabs.Trigger value="agents">AGENTS.md</Tabs.Trigger>
<Tabs.List class="settings-nav" aria-label="设置分类">
<Tabs.Trigger value="appearance">外观</Tabs.Trigger><Tabs.Trigger value="config">config.json</Tabs.Trigger><Tabs.Trigger value="user">USER.md</Tabs.Trigger><Tabs.Trigger value="agents">AGENTS.md</Tabs.Trigger>
</Tabs.List>
</Tabs.Root>
{#if isConfig}
{#if isAppearance}
<AppearanceSettings />
{:else if isConfig}
<div class="config-layout">
<div class="editor-card">
<div class="editor-head">
@ -194,7 +199,7 @@
.reload-row + .reload-row { border-top: 1px solid var(--line); }
.reload-error { margin-top: 8px; padding: 8px 10px; border-radius: 8px; background: var(--danger-soft); color: var(--danger); font-size: 12px; line-height: 1.5; }
.json-error { padding: 9px 15px; color: var(--danger); font-size: 12px; background: var(--danger-soft); border-top: 1px solid var(--danger-border); }
.dirty-badge { display: inline-flex; align-items: center; gap: 5px; font-size: 11px; font-weight: 600; color: var(--accent); background: var(--accent-soft); border: 1px solid var(--accent-border); border-radius: 99px; padding: 3px 9px; white-space: nowrap; }
.dirty-badge { display: inline-flex; align-items: center; gap: 5px; font-size: 11px; font-weight: 600; color: var(--accent); background: var(--accent-soft); border: 1px solid var(--accent-border); border-radius: 4px; padding: 3px 8px; white-space: nowrap; }
.dirty-badge::before { content: ""; width: 6px; height: 6px; border-radius: 50%; background: var(--accent); }
.editor-actions { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; justify-content: flex-end; }
.editor-head { min-height: 62px; height: auto; padding: 10px 15px; flex-wrap: wrap; gap: 8px; }

View File

@ -3,6 +3,7 @@
import { Tabs } from "bits-ui";
import { api, formatTime } from "../lib/api.js";
import StatusBadge from "../lib/StatusBadge.svelte";
import Icon from "../lib/Icon.svelte";
let tab = $state("scheduled");
let jobs = $state([]);
@ -84,7 +85,7 @@
<Tabs.Trigger value="background">后台任务</Tabs.Trigger>
</Tabs.List>
</Tabs.Root>
<button class="secondary" onclick={load}>刷新</button>
<button class="secondary" onclick={load}><Icon name="refresh" size={16} />刷新</button>
</div>
<div class="cards">
{#if loading}<div class="loading">加载中…</div>

View File

@ -2,6 +2,7 @@
import { onMount } from "svelte";
import { Tabs } from "bits-ui";
import { api } from "../lib/api.js";
import Icon from "../lib/Icon.svelte";
let tab = $state("tools");
let tools = $state([]);
@ -78,7 +79,7 @@
<Tabs.Trigger value="mcp">MCP</Tabs.Trigger>
</Tabs.List>
</Tabs.Root>
<button class="secondary" onclick={load}>刷新</button>
<button class="secondary" onclick={load}><Icon name="refresh" size={16} />刷新</button>
</div>
{#if loading}
@ -87,7 +88,7 @@
<div class="empty-card error-text">{error}</div>
{:else if tab === "tools"}
<form class="filters-bar" onsubmit={(event) => event.preventDefault()}>
<label class="search grow"><span></span><input bind:value={query} placeholder="搜索工具名称或描述" /></label>
<label class="search grow"><Icon name="search" size={16} /><input bind:value={query} placeholder="搜索工具名称或描述" /></label>
<div class="chips">
{#each filters as [value, label]}
<button type="button" class="chip" class:active={filter === value} onclick={() => (filter = value)}>{label}</button>
@ -95,10 +96,10 @@
</div>
</form>
<div class="legend">
<span class="cap signal"> 只读</span>
<span class="cap info"> 可并发</span>
<span class="cap accent"> 有副作用</span>
<span class="cap danger"> 独占</span>
<span class="cap signal"> 只读</span>
<span class="cap info"> 可并发</span>
<span class="cap accent"> 有副作用</span>
<span class="cap danger"> 独占</span>
</div>
<div class="tool-grid">
{#each filtered as tool (tool.name)}
@ -112,10 +113,10 @@
</div>
<p>{tool.description}</p>
<div class="cap-row">
{#if tool.read_only}<span class="cap signal"> 只读</span>{/if}
{#if tool.concurrency_safe}<span class="cap info"> 可并发</span>{/if}
{#if !tool.read_only}<span class="cap accent"> 有副作用</span>{/if}
{#if tool.exclusive}<span class="cap danger"> 独占</span>{/if}
{#if tool.read_only}<span class="cap signal"> 只读</span>{/if}
{#if tool.concurrency_safe}<span class="cap info"> 可并发</span>{/if}
{#if !tool.read_only}<span class="cap accent"> 有副作用</span>{/if}
{#if tool.exclusive}<span class="cap danger"> 独占</span>{/if}
</div>
<details class="schema-wrap">
<summary>参数 schema</summary>
@ -170,7 +171,7 @@
<style>
.filters-bar { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; margin-bottom: 12px; }
.chips { display: flex; gap: 7px; flex-wrap: wrap; }
.chip { border: 1px solid var(--line); background: var(--panel); color: var(--muted); border-radius: 99px; padding: 6px 12px; font-size: 12px; font-weight: 500; cursor: pointer; }
.chip { border: 1px solid var(--line-strong); background: var(--panel); color: var(--muted); border-radius: 4px; padding: 5px 10px; font-size: 12px; font-weight: 600; cursor: pointer; }
.chip.active { background: var(--accent-soft); border-color: var(--accent-border); color: var(--accent); }
.legend { display: flex; gap: 8px; flex-wrap: wrap; margin-bottom: 16px; }
.tool-grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 11px; }

View File

@ -1,434 +1,515 @@
@font-face {
font-family: "Space Grotesk";
src: url("/fonts/space-grotesk.woff2") format("woff2");
font-weight: 300 700; font-style: normal; font-display: swap;
}
@font-face {
font-family: "JetBrains Mono";
src: url("/fonts/jetbrains-mono-400.woff2") format("woff2");
font-weight: 400; font-style: normal; font-display: swap;
font-weight: 400;
font-style: normal;
font-display: swap;
}
@font-face {
font-family: "JetBrains Mono";
src: url("/fonts/jetbrains-mono-700.woff2") format("woff2");
font-weight: 700; font-style: normal; font-display: swap;
font-weight: 700;
font-style: normal;
font-display: swap;
}
:root {
--font-ui: "Space Grotesk", ui-sans-serif, system-ui, "PingFang SC", "Microsoft YaHei", "Noto Sans SC", sans-serif;
--font-mono: "JetBrains Mono", ui-monospace, SFMono-Regular, Consolas, monospace;
color-scheme: dark;
font-family: var(--font-ui);
font-weight: 400;
:root,
:root[data-theme="light"] {
color-scheme: light;
font-family: "Segoe UI Variable Text", "Segoe UI", system-ui, -apple-system, "PingFang SC", "Microsoft YaHei", sans-serif;
font-synthesis: none;
-webkit-font-smoothing: antialiased;
text-rendering: optimizeLegibility;
font-kerning: normal;
color: #e7ecf3;
background: #0b1017;
--bg: #0b1017;
--panel: #0e1520;
--panel-2: #131c29;
--sidebar: #0d131c;
--header: rgb(11 16 23 / 84%);
--line: #1d2733;
--line-strong: #2c3a4c;
--muted: #8fa3b8;
--faint: #5b6b7e;
--text: #e7ecf3;
--text-soft: #b8c4d4;
--accent: #ffb454;
--accent-hover: #ffc370;
--accent-contrast: #1a1206;
--accent-soft: rgb(255 180 84 / 12%);
--accent-border: rgb(255 180 84 / 35%);
--signal: #2dd4bf;
--signal-soft: rgb(45 212 191 / 12%);
--signal-border: rgb(45 212 191 / 35%);
--info: #6aa6ff;
--info-soft: rgb(106 166 255 / 12%);
--info-border: rgb(106 166 255 / 35%);
--danger: #ff7b86;
--danger-soft: rgb(255 123 134 / 12%);
--danger-border: rgb(255 123 134 / 35%);
--warning: #ffb454;
--warning-soft: rgb(255 180 84 / 10%);
--success-soft: rgb(45 212 191 / 12%);
--overlay: #101826;
--code-bg: #080c12;
--user-bubble: #221d38;
--spine-bg: #0e1520;
--spine-text: #b8c4d4;
--spine-signal: #2dd4bf;
--spine-accent: #ffb454;
--spine-faint: #8fa3b8;
--shadow: 0 16px 45px rgb(0 0 0 / 35%);
--shadow-sm: 0 1px 2px rgb(2 6 12 / 45%), 0 6px 20px rgb(2 6 12 / 22%);
--shadow-lift: 0 4px 10px rgb(2 6 12 / 50%), 0 18px 44px rgb(2 6 12 / 32%);
--edge: inset 0 1px 0 0 rgb(255 255 255 / 5%);
--dot: rgb(140 170 210 / 7%);
--glow: rgb(255 180 84 / 6%);
--accent-glow: rgb(255 180 84 / 26%);
--radius: 11px;
/* Fluent 2 semantic tokens */
--color-neutral-background-1: #ffffff;
--color-neutral-background-2: #f5f5f5;
--color-neutral-background-3: #f0f0f0;
--color-neutral-background-4: #e8e8e8;
--color-neutral-foreground-1: #242424;
--color-neutral-foreground-2: #424242;
--color-neutral-foreground-3: #616161;
--color-neutral-stroke-1: #d1d1d1;
--color-neutral-stroke-2: #e0e0e0;
--color-brand-background: #0f6cbd;
--color-brand-background-hover: #115ea3;
--color-brand-background-pressed: #0c3b5e;
--color-brand-foreground: #115ea3;
--color-brand-subtle: #ebf3fc;
--color-status-success: #0e700e;
--color-status-warning: #8a3707;
--color-status-danger: #b10e1c;
--color-status-info: #004e8c;
/* Compatibility aliases used by page components. */
--font-ui: "Segoe UI Variable Text", "Segoe UI", system-ui, -apple-system, "PingFang SC", "Microsoft YaHei", sans-serif;
--font-mono: "JetBrains Mono", "Cascadia Code", Consolas, monospace;
--bg: var(--color-neutral-background-2);
--panel: var(--color-neutral-background-1);
--panel-2: var(--color-neutral-background-3);
--sidebar: #fafafa;
--header: rgb(255 255 255 / 94%);
--line: var(--color-neutral-stroke-2);
--line-strong: var(--color-neutral-stroke-1);
--muted: var(--color-neutral-foreground-3);
--faint: #707070;
--text: var(--color-neutral-foreground-1);
--text-soft: var(--color-neutral-foreground-2);
--accent: var(--color-brand-background);
--accent-hover: var(--color-brand-background-hover);
--accent-pressed: var(--color-brand-background-pressed);
--accent-contrast: #ffffff;
--accent-soft: var(--color-brand-subtle);
--accent-border: #96c6fa;
--signal: var(--color-status-success);
--signal-soft: #f1faf1;
--signal-border: #9fd89f;
--info: var(--color-status-info);
--info-soft: #ebf3fc;
--info-border: #b4d6fa;
--danger: var(--color-status-danger);
--danger-soft: #fdf3f4;
--danger-border: #eeacb2;
--warning: var(--color-status-warning);
--warning-soft: #fff9f5;
--success-soft: #f1faf1;
--overlay: #ffffff;
--code-bg: #f5f5f5;
--user-bubble: #ebf3fc;
--shadow-2: 0 1px 2px rgb(0 0 0 / 14%), 0 0 2px rgb(0 0 0 / 12%);
--shadow-8: 0 4px 8px rgb(0 0 0 / 14%), 0 0 2px rgb(0 0 0 / 12%);
--shadow-16: 0 8px 16px rgb(0 0 0 / 14%), 0 0 2px rgb(0 0 0 / 12%);
--shadow: var(--shadow-16);
--shadow-sm: var(--shadow-2);
--shadow-lift: var(--shadow-8);
--edge: none;
--radius: 8px;
}
:root[data-theme="light"] {
color-scheme: light;
color: #1a2230;
background: #eef1f5;
--bg: #eef1f5;
--panel: #ffffff;
--panel-2: #f4f6f9;
--sidebar: #f7f9fc;
--header: rgb(238 241 245 / 86%);
--line: #d8dee8;
--line-strong: #c2ccd9;
--muted: #5b6b7e;
--faint: #8494a8;
--text: #1a2230;
--text-soft: #3d4b5e;
--accent: #a86300;
--accent-hover: #c47400;
--accent-contrast: #ffffff;
--accent-soft: rgb(196 116 0 / 10%);
--accent-border: rgb(196 116 0 / 35%);
--signal: #0d9488;
--signal-soft: rgb(13 148 136 / 10%);
--signal-border: rgb(13 148 136 / 35%);
--info: #2f6fd0;
--info-soft: rgb(47 111 208 / 10%);
--info-border: rgb(47 111 208 / 35%);
--danger: #d94354;
--danger-soft: rgb(217 67 84 / 10%);
--danger-border: rgb(217 67 84 / 35%);
/* --warning intentionally equals --accent: single-amber signal system */
--warning: #a86300;
--warning-soft: rgb(196 116 0 / 8%);
--success-soft: rgb(13 148 136 / 10%);
--overlay: #ffffff;
--code-bg: #f7f9fc;
--user-bubble: #ece7fb;
--spine-bg: #0e1520;
--shadow: 0 16px 45px rgb(31 41 55 / 12%);
--shadow-sm: 0 1px 2px rgb(23 32 48 / 7%), 0 6px 20px rgb(23 32 48 / 6%);
--shadow-lift: 0 4px 10px rgb(23 32 48 / 10%), 0 18px 44px rgb(23 32 48 / 13%);
--edge: inset 0 1px 0 0 rgb(255 255 255 / 70%);
--dot: rgb(26 42 70 / 6%);
--glow: rgb(196 116 0 / 5%);
--accent-glow: rgb(196 116 0 / 20%);
:root[data-theme="dark"] {
color-scheme: dark;
--color-neutral-background-1: #292929;
--color-neutral-background-2: #1f1f1f;
--color-neutral-background-3: #333333;
--color-neutral-background-4: #3d3d3d;
--color-neutral-foreground-1: #ffffff;
--color-neutral-foreground-2: #d6d6d6;
--color-neutral-foreground-3: #adadad;
--color-neutral-stroke-1: #666666;
--color-neutral-stroke-2: #424242;
--color-brand-background: #479ef5;
--color-brand-background-hover: #62abf5;
--color-brand-background-pressed: #2886de;
--color-brand-foreground: #62abf5;
--color-brand-subtle: #0f2d46;
--color-status-success: #54b054;
--color-status-warning: #fce100;
--color-status-danger: #dc626d;
--color-status-info: #62abf5;
--bg: var(--color-neutral-background-2);
--panel: var(--color-neutral-background-1);
--panel-2: var(--color-neutral-background-3);
--sidebar: #242424;
--header: rgb(41 41 41 / 94%);
--line: var(--color-neutral-stroke-2);
--line-strong: var(--color-neutral-stroke-1);
--muted: var(--color-neutral-foreground-3);
--faint: #929292;
--text: var(--color-neutral-foreground-1);
--text-soft: var(--color-neutral-foreground-2);
--accent: var(--color-brand-background);
--accent-hover: var(--color-brand-background-hover);
--accent-pressed: var(--color-brand-background-pressed);
--accent-contrast: #0f0f0f;
--accent-soft: var(--color-brand-subtle);
--accent-border: #0f6cbd;
--signal: var(--color-status-success);
--signal-soft: #183b18;
--signal-border: #107c10;
--info: var(--color-status-info);
--info-soft: #0f2d46;
--info-border: #0f6cbd;
--danger: var(--color-status-danger);
--danger-soft: #3b1219;
--danger-border: #8f1d2c;
--warning: var(--color-status-warning);
--warning-soft: #3a2d00;
--success-soft: #183b18;
--overlay: #333333;
--code-bg: #1f1f1f;
--user-bubble: #0f2d46;
--shadow-2: 0 1px 2px rgb(0 0 0 / 28%), 0 0 2px rgb(0 0 0 / 24%);
--shadow-8: 0 4px 8px rgb(0 0 0 / 28%), 0 0 2px rgb(0 0 0 / 24%);
--shadow-16: 0 8px 16px rgb(0 0 0 / 28%), 0 0 2px rgb(0 0 0 / 24%);
}
/* Curated Fluent brand ramps. Each accent has tuned light and dark values. */
:root[data-accent="purple"] { --color-brand-background: #5c2e91; --color-brand-background-hover: #4f2682; --color-brand-background-pressed: #32145f; --color-brand-foreground: #5c2e91; --color-brand-subtle: #f5eef8; --accent-border: #c6b1de; }
:root[data-accent="teal"] { --color-brand-background: #006c4c; --color-brand-background-hover: #005a3f; --color-brand-background-pressed: #003d2b; --color-brand-foreground: #006c4c; --color-brand-subtle: #e6f7f2; --accent-border: #83c7b8; }
:root[data-accent="green"] { --color-brand-background: #107c10; --color-brand-background-hover: #0e6b0e; --color-brand-background-pressed: #0b5a0b; --color-brand-foreground: #107c10; --color-brand-subtle: #edf7ed; --accent-border: #9fd89f; }
:root[data-accent="orange"] { --color-brand-background: #ca5010; --color-brand-background-hover: #a83f0b; --color-brand-background-pressed: #7a2e08; --color-brand-foreground: #a83f0b; --color-brand-subtle: #fff3e8; --accent-border: #f2bb8d; }
:root[data-accent="magenta"] { --color-brand-background: #9b0062; --color-brand-background-hover: #850053; --color-brand-background-pressed: #65003f; --color-brand-foreground: #9b0062; --color-brand-subtle: #fceef5; --accent-border: #d8a0bd; }
:root[data-theme="dark"][data-accent="purple"] { --color-brand-background: #b4a0ff; --color-brand-background-hover: #c6b7ff; --color-brand-background-pressed: #9f85f2; --color-brand-foreground: #c6b7ff; --color-brand-subtle: #2b173d; --accent-border: #744da9; --accent-contrast: #1f1f1f; }
:root[data-theme="dark"][data-accent="teal"] { --color-brand-background: #4cc2a0; --color-brand-background-hover: #65d4b4; --color-brand-background-pressed: #2ea987; --color-brand-foreground: #65d4b4; --color-brand-subtle: #0d3328; --accent-border: #008567; --accent-contrast: #0f0f0f; }
:root[data-theme="dark"][data-accent="green"] { --color-brand-background: #54b054; --color-brand-background-hover: #6bc46b; --color-brand-background-pressed: #3f963f; --color-brand-foreground: #6bc46b; --color-brand-subtle: #183b18; --accent-border: #107c10; --accent-contrast: #0f0f0f; }
:root[data-theme="dark"][data-accent="orange"] { --color-brand-background: #f4a261; --color-brand-background-hover: #ffb57a; --color-brand-background-pressed: #db8747; --color-brand-foreground: #ffb57a; --color-brand-subtle: #402313; --accent-border: #c45d1d; --accent-contrast: #1f1f1f; }
:root[data-theme="dark"][data-accent="magenta"] { --color-brand-background: #e36fbe; --color-brand-background-hover: #ef88cc; --color-brand-background-pressed: #c951a3; --color-brand-foreground: #ef88cc; --color-brand-subtle: #3d1830; --accent-border: #a4267a; --accent-contrast: #1f1f1f; }
* { box-sizing: border-box; scrollbar-width: thin; scrollbar-color: var(--line-strong) transparent; }
*::-webkit-scrollbar { width: 10px; height: 10px; }
*::-webkit-scrollbar-track { background: transparent; }
*::-webkit-scrollbar-thumb { background: var(--line-strong); border-radius: 8px; border: 3px solid transparent; background-clip: content-box; }
*::-webkit-scrollbar-thumb:hover { background-color: var(--faint); }
::selection { background: var(--accent); color: var(--accent-contrast); }
h1, h2, h3, h4 { font-weight: 600; letter-spacing: -0.01em; }
*::-webkit-scrollbar-thumb { border: 3px solid transparent; border-radius: 10px; background: var(--line-strong); background-clip: content-box; }
*::-webkit-scrollbar-thumb:hover { background-color: var(--muted); }
::selection { color: var(--text); background: var(--accent-soft); }
html, body, #app { width: 100%; min-width: 320px; height: 100%; }
body { margin: 0; color: var(--text); background: var(--bg); font-size: 14px; }
h1, h2, h3, h4 { font-family: "Segoe UI Variable Display", var(--font-ui); font-weight: 600; }
strong, b { font-weight: 600; }
body { margin: 0; min-width: 320px; color: var(--text); background: var(--bg); transition: color .2s, background .2s; }
.pairing-screen { min-height: 100vh; display: grid; place-items: center; padding: 24px; background: radial-gradient(circle at 50% 0%, var(--accent-soft), transparent 38%), var(--bg); }
.pairing-card { width: min(100%, 420px); padding: 30px; border: 1px solid var(--line); border-radius: 18px; background: var(--panel); box-shadow: var(--edge), var(--shadow); }
.pairing-brand { display: flex; align-items: center; gap: 10px; margin-bottom: 28px; }
.pairing-icon { width: 52px; height: 52px; display: grid; place-items: center; margin-bottom: 18px; border-radius: 14px; color: var(--accent); background: var(--accent-soft); font-size: 24px; }
.pairing-card h1 { margin: 0 0 9px; font-size: 26px; }
.pairing-card > p { margin: 0 0 18px; color: var(--muted); font-size: 14px; line-height: 1.7; }
.pairing-command { display: block; margin-bottom: 22px; padding: 10px 12px; border: 1px solid var(--line); border-radius: 8px; color: var(--text); background: var(--code-bg); font: 13px/1.5 var(--font-mono); }
.pairing-card form { display: grid; gap: 10px; }
.pairing-card label { color: var(--text-soft); font-size: 12px; font-weight: 600; }
.pairing-card input { width: 100%; padding: 12px 14px; border: 1px solid var(--line); border-radius: 10px; color: var(--text); background: var(--panel-2); font: 600 24px/1.2 var(--font-mono); letter-spacing: .28em; text-align: center; }
.pairing-card form .primary { margin-top: 3px; padding: 11px 14px; }
.pairing-card > small { display: block; margin-top: 17px; color: var(--muted); font-size: 11px; line-height: 1.5; }
.pairing-error { padding: 8px 10px; border-radius: 8px; color: var(--danger); background: var(--danger-soft); font-size: 12px; }
.auth-loading { color: var(--muted); font-size: 14px; }
button, input, textarea, select { font: inherit; }
button:focus-visible, input:focus-visible, textarea:focus-visible, select:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
button:disabled { cursor: not-allowed; opacity: .45; }
.shell { height: 100vh; display: grid; grid-template-columns: 232px 1fr; }
.sidebar { border-right: 1px solid var(--line); background: radial-gradient(circle at 18% -4%, var(--glow), transparent 42%), var(--sidebar); display: flex; flex-direction: column; padding: 20px 14px; }
.brand { display: flex; align-items: center; gap: 12px; padding: 2px 8px 26px; }
.brand-mark, .empty-logo { display: grid; place-items: center; background: var(--accent); color: var(--accent-contrast); font-weight: 900; border-radius: 10px; }
.brand-mark { width: 36px; height: 36px; box-shadow: var(--edge), 0 5px 18px var(--accent-glow); }
.brand strong { display: block; font-size: 15px; font-weight: 700; letter-spacing: -0.01em; }
.brand small { display: block; color: var(--muted); font-size: 12px; margin-top: 2px; }
.sidebar nav { display: grid; gap: 5px; }
.sidebar nav button, .settings-nav button { position: relative; border: 0; background: transparent; color: var(--muted); text-align: left; padding: 11px 13px; border-radius: 9px; cursor: pointer; font-size: 14px; font-weight: 500; transition: background-color .15s ease, color .15s ease; }
.sidebar nav button::before { content: ""; position: absolute; left: 0; top: 50%; width: 3px; height: 20px; border-radius: 99px; background: var(--accent); box-shadow: 0 0 10px var(--accent-glow); transform: translateY(-50%) scaleY(0); transition: transform .22s cubic-bezier(.22,.9,.36,1); }
.sidebar nav button.active::before { transform: translateY(-50%) scaleY(1); }
.sidebar nav button { display: flex; align-items: center; gap: 11px; font-size: 15px; }
.sidebar nav button svg { width: 19px; height: 19px; flex-shrink: 0; opacity: .85; }
.sidebar nav button:hover svg, .sidebar nav button.active svg { opacity: 1; }
.sidebar nav button:hover, .sidebar nav button.active, .settings-nav button:hover, .settings-nav button[data-state="active"] { background: var(--accent-soft); color: var(--text); }
.sidebar nav button.active { color: var(--accent); }
@keyframes status-breathe { 0%, 100% { box-shadow: 0 0 7px currentColor; } 50% { box-shadow: 0 0 15px currentColor; } }
.gateway-status { margin-top: auto; border-top: 1px solid var(--line); padding: 18px 8px 2px; display: flex; gap: 10px; align-items: center; }
.gateway-status i { width: 9px; height: 9px; border-radius: 50%; color: var(--warning); background: currentColor; box-shadow: 0 0 12px currentColor; }
.gateway-status i.online { color: var(--signal); background: currentColor; animation: status-breathe 2.6s ease-in-out infinite; }
.gateway-status b, .gateway-status small { display: block; font-size: 13px; }
.gateway-status b { font-weight: 600; }
.gateway-status small { color: var(--muted); margin-top: 2px; }
main { min-width: 0; height: 100vh; display: flex; flex-direction: column;
background-color: var(--bg);
background-image:
radial-gradient(circle at 82% -8%, var(--glow), transparent 46%),
radial-gradient(var(--dot) 1px, transparent 1.5px);
background-size: auto, 22px 22px;
button:focus-visible, input:focus-visible, textarea:focus-visible, select:focus-visible, a:focus-visible, summary:focus-visible {
outline: 2px solid var(--accent);
outline-offset: 2px;
}
.topbar { height: 76px; flex: 0 0 76px; border-bottom: 1px solid var(--line); display: flex; align-items: center; padding: 0 26px; background: var(--header); backdrop-filter: blur(16px); }
.topbar h1 { font-size: 20px; margin: 0; font-weight: 700; letter-spacing: -0.015em; }
.topbar p { font-size: 13px; color: var(--muted); margin: 4px 0 0; }
.topbar .menu { display: none; background: none; border: 0; color: var(--text); font-size: 20px; margin-right: 12px; }
.theme-toggle { margin-left: auto; display: flex; align-items: center; gap: 7px; height: 34px; padding: 0 11px; border: 1px solid var(--line); border-radius: 9px; color: var(--muted); background: var(--panel); cursor: pointer; font-size: 13px; font-weight: 500; transition: color .15s ease, border-color .15s ease; }
.theme-toggle:hover { color: var(--text); border-color: var(--accent-border); }
.page { min-height: 0; flex: 1; animation: page-in .32s cubic-bezier(.22,.9,.36,1) both; }
.chat-layout { display: grid; grid-template-columns: 270px minmax(0, 1fr); }
.chat-layout.todo-open { grid-template-columns: 270px minmax(0, 1fr) 320px; }
.sessions-panel { border-right: 1px solid var(--line); padding: 16px; background: color-mix(in srgb, var(--sidebar) 60%, var(--panel)); overflow: auto; }
.primary, .secondary { border-radius: 9px; padding: 9px 14px; font-size: 14px; font-weight: 600; cursor: pointer; transition: transform .12s ease, box-shadow .18s ease, border-color .15s ease, background-color .15s ease; }
.primary { border: 1px solid var(--accent); background: var(--accent); color: var(--accent-contrast); }
.primary:hover { border-color: var(--accent-hover); background: var(--accent-hover); box-shadow: 0 6px 18px var(--accent-glow); }
.primary:active, .secondary:active { transform: scale(.97); }
.secondary { border: 1px solid var(--line); background: var(--panel-2); color: var(--text); }
.secondary:hover { border-color: var(--line-strong); }
.full { width: 100%; padding: 11px; }
.search { height: 39px; border: 1px solid var(--line); border-radius: 9px; background: var(--panel); display: flex; align-items: center; padding: 0 11px; color: var(--muted); transition: border-color .15s ease, box-shadow .15s ease; }
.search:focus-within { border-color: var(--accent-border); box-shadow: 0 0 0 3px var(--accent-soft); }
.sessions-panel > .search { margin: 13px 0 0; }
.search input { border: 0; outline: 0; background: transparent; color: var(--text); width: 100%; padding: 0 7px; font-size: 14px; }
.session-list-head { display: flex; align-items: baseline; justify-content: space-between; margin: 15px 2px 9px; }
.session-count { font-family: var(--font-mono); font-size: 11px; color: var(--faint); font-variant-numeric: tabular-nums; }
.session-list { display: grid; gap: 6px; }
.session-item { position: relative; width: 100%; padding: 11px 12px; border-radius: 9px; cursor: pointer; border: 1px solid transparent; color: var(--text); background: transparent; text-align: left; transition: background-color .15s ease, border-color .15s ease; }
.session-item::before { content: ""; position: absolute; left: 0; top: 50%; width: 3px; height: 22px; border-radius: 99px; background: var(--accent); box-shadow: 0 0 10px var(--accent-glow); transform: translateY(-50%) scaleY(0); transition: transform .22s cubic-bezier(.22,.9,.36,1); }
.session-item:hover { background: var(--panel-2); border-color: var(--line); }
.session-item.active { background: var(--accent-soft); border-color: var(--accent-border); }
.session-item.active::before { transform: translateY(-50%) scaleY(1); }
.session-item strong { font-size: 14px; font-weight: 600; display: block; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.plan-unread { display: inline-block; width: 7px; height: 7px; margin-left: 7px; border-radius: 50%; background: var(--accent); box-shadow: 0 0 9px var(--accent); }
.session-item small { color: var(--muted); font-size: 12px; display: flex; justify-content: space-between; margin-top: 6px; }
.chat-panel { display: flex; flex-direction: column; min-width: 0; min-height: 0; }
.chat-heading { height: 57px; flex: 0 0 57px; border-bottom: 1px solid var(--line); display: flex; align-items: center; justify-content: space-between; padding: 0 20px; background: var(--header); backdrop-filter: blur(16px); }
.chat-heading strong, .chat-heading small { display: block; }
.chat-heading strong { font-size: 14px; font-weight: 600; }
.chat-heading-actions { display: flex; align-items: center; gap: 9px; }
.todo-toggle { border: 1px solid var(--line); border-radius: 8px; padding: 6px 9px; background: var(--panel-2); color: var(--muted); font-size: 12px; font-weight: 500; cursor: pointer; }
.todo-toggle.active { border-color: var(--accent-border); color: var(--accent); background: var(--accent-soft); }
.chat-heading small { font-size: 11px; color: var(--muted); margin-top: 3px; }
.icon-button { border: 0; background: none; color: var(--muted); font-size: 20px; cursor: pointer; border-radius: 6px; transition: color .15s ease, transform .12s ease; }
.icon-button:hover { color: var(--text); }
.icon-button:active { transform: scale(.9); }
.tooltip { z-index: 50; border: 1px solid var(--line); background: var(--overlay); color: var(--text); padding: 6px 9px; border-radius: 7px; font-size: 12px; box-shadow: var(--shadow); }
button:disabled { cursor: not-allowed; opacity: .45; }
/* Pairing */
.pairing-screen { min-height: 100vh; display: grid; place-items: center; padding: 24px; background: linear-gradient(135deg, var(--bg), var(--panel-2)); }
.pairing-card { width: min(100%, 440px); padding: 32px; border: 1px solid var(--line); border-radius: 12px; background: var(--panel); box-shadow: var(--shadow-16); }
.pairing-brand { display: flex; align-items: center; gap: 10px; margin-bottom: 32px; }
.pairing-brand strong { font-size: 16px; }
.pairing-icon { width: 48px; height: 48px; display: grid; place-items: center; margin-bottom: 20px; border-radius: 8px; color: var(--accent); background: var(--accent-soft); }
.pairing-card h1 { margin: 0 0 8px; font-size: 28px; line-height: 36px; }
.pairing-card > p { margin: 0 0 20px; color: var(--muted); font-size: 14px; line-height: 20px; }
.pairing-command { display: block; margin-bottom: 24px; padding: 10px 12px; border: 1px solid var(--line); border-radius: 4px; color: var(--text); background: var(--code-bg); font: 13px/20px var(--font-mono); }
.pairing-card form { display: grid; gap: 10px; }
.pairing-card label { color: var(--text-soft); font-size: 13px; font-weight: 600; }
.pairing-card input { width: 100%; height: 48px; padding: 0 14px; border: 1px solid var(--line-strong); border-radius: 4px; color: var(--text); background: var(--panel); font: 600 24px/1 var(--font-mono); letter-spacing: .22em; text-align: center; }
.pairing-card input:hover { border-color: var(--muted); }
.pairing-card input:focus { border-color: var(--accent); border-bottom-width: 2px; outline: none; }
.pairing-card form .primary { margin-top: 4px; }
.pairing-card > small { display: block; margin-top: 18px; color: var(--muted); font-size: 12px; line-height: 18px; }
.pairing-error, .turn-error { padding: 9px 11px; border: 1px solid var(--danger-border); border-radius: 6px; color: var(--danger); background: var(--danger-soft); font-size: 12px; }
.auth-loading { color: var(--muted); font-size: 14px; }
/* Application shell */
.shell { height: 100vh; display: grid; grid-template-columns: 248px minmax(0, 1fr); overflow: hidden; }
.sidebar { z-index: 30; display: flex; flex-direction: column; min-width: 0; padding: 8px 12px 12px; border-right: 1px solid var(--line); background: var(--sidebar); }
.brand { display: flex; align-items: center; gap: 11px; min-height: 60px; padding: 6px 8px; }
.brand-mark, .empty-logo { display: grid; place-items: center; color: var(--accent-contrast); background: linear-gradient(145deg, var(--accent), var(--accent-pressed)); }
.brand-mark { width: 36px; height: 36px; border-radius: 8px; box-shadow: var(--shadow-2); }
.brand strong { display: block; font-size: 15px; line-height: 20px; }
.brand small { display: block; margin-top: 1px; color: var(--muted); font-size: 11px; line-height: 16px; }
.nav-label { padding: 16px 12px 6px; color: var(--muted); font-size: 11px; font-weight: 600; }
.sidebar nav { display: grid; gap: 2px; }
.sidebar nav button, .settings-nav button { position: relative; min-height: 40px; padding: 0 10px; border: 0; border-radius: 6px; color: var(--text-soft); background: transparent; text-align: left; cursor: pointer; font-size: 14px; font-weight: 500; transition: background-color .1s ease, color .1s ease; }
.sidebar nav button { display: flex; align-items: center; gap: 9px; }
.sidebar nav button::before { content: ""; position: absolute; left: 0; top: 10px; bottom: 10px; width: 3px; border-radius: 2px; background: var(--accent); transform: scaleY(0); transition: transform .12s ease; }
.sidebar nav button:hover, .settings-nav button:hover { background: var(--panel-2); color: var(--text); }
.sidebar nav button.active, .settings-nav button[data-state="active"] { color: var(--text); background: var(--accent-soft); }
.sidebar nav button.active::before { transform: scaleY(1); }
.nav-icon { width: 28px; height: 28px; display: grid; place-items: center; color: var(--muted); }
.nav-icon svg { width: 18px; height: 18px; }
.sidebar nav button.active .nav-icon { color: var(--accent); }
.gateway-status { display: flex; align-items: center; gap: 10px; min-height: 54px; margin-top: auto; padding: 8px 10px; border: 1px solid var(--line); border-radius: 8px; background: var(--panel); box-shadow: var(--shadow-2); }
.gateway-status i { width: 8px; height: 8px; flex: 0 0 auto; border-radius: 50%; color: var(--warning); background: currentColor; }
.gateway-status i.online { color: var(--signal); }
.gateway-status b, .gateway-status small { display: block; }
.gateway-status b { font-size: 12px; line-height: 16px; }
.gateway-status small { margin-top: 1px; color: var(--muted); font-size: 10px; line-height: 14px; }
.app-main { min-width: 0; min-height: 0; height: 100vh; display: flex; flex-direction: column; background: var(--bg); }
.topbar { z-index: 10; min-height: 68px; flex: 0 0 68px; display: flex; align-items: center; gap: 12px; padding: 0 24px; border-bottom: 1px solid var(--line); background: var(--header); backdrop-filter: blur(16px); }
.page-heading { min-width: 180px; }
.topbar h1 { margin: 0; font-size: 20px; line-height: 26px; }
.topbar p { margin: 1px 0 0; color: var(--muted); font-size: 12px; line-height: 16px; }
.topbar .menu { display: none; }
.theme-toggle { min-width: 74px; height: 32px; display: inline-flex; align-items: center; justify-content: center; gap: 7px; padding: 0 10px; border: 1px solid var(--line-strong); border-radius: 4px; color: var(--text-soft); background: var(--panel); cursor: pointer; font-size: 12px; font-weight: 600; }
.theme-toggle:hover { color: var(--text); background: var(--panel-2); }
.nav-scrim { display: none; }
/* Fluent controls */
.primary, .secondary, .danger-solid { min-height: 32px; padding: 5px 12px; border-radius: 4px; font-size: 13px; line-height: 20px; font-weight: 600; cursor: pointer; transition: background-color .1s ease, border-color .1s ease; }
.primary, .secondary, .danger-solid { display: inline-flex; align-items: center; justify-content: center; gap: 6px; }
.primary { border: 1px solid var(--accent); color: var(--accent-contrast); background: var(--accent); }
.primary:hover { border-color: var(--accent-hover); background: var(--accent-hover); }
.primary:active { border-color: var(--accent-pressed); background: var(--accent-pressed); }
.secondary { border: 1px solid var(--line-strong); color: var(--text); background: var(--panel); }
.secondary:hover { background: var(--panel-2); }
.secondary:active { background: var(--color-neutral-background-4); }
.full { width: 100%; min-height: 36px; }
.icon-button { width: 32px; height: 32px; display: inline-grid; place-items: center; padding: 0; border: 0; border-radius: 4px; color: var(--text-soft); background: transparent; cursor: pointer; }
.icon-button:hover { color: var(--text); background: var(--panel-2); }
.search { height: 34px; display: flex; align-items: center; padding: 0 9px; border: 1px solid var(--line-strong); border-radius: 4px; color: var(--muted); background: var(--panel); }
.search:hover { border-color: var(--muted); }
.search:focus-within { border-color: var(--accent); border-bottom-width: 2px; }
.search input { width: 100%; padding: 0 7px; border: 0; outline: 0; color: var(--text); background: transparent; font-size: 13px; }
.filters select { height: 34px; padding: 0 28px 0 10px; border: 1px solid var(--line-strong); border-radius: 4px; color: var(--text); background: var(--panel); }
.grow { flex: 1; max-width: 520px; }
.tabs { display: flex; gap: 16px; min-height: 36px; border-bottom: 1px solid var(--line); }
.tabs button { position: relative; padding: 0 2px 9px; border: 0; color: var(--muted); background: none; cursor: pointer; font-size: 13px; font-weight: 600; }
.tabs button::after { content: ""; position: absolute; right: 0; bottom: -1px; left: 0; height: 2px; border-radius: 2px; background: var(--accent); transform: scaleX(0); }
.tabs button:hover { color: var(--text); }
.tabs button[data-state="active"] { color: var(--text); }
.tabs button[data-state="active"]::after { transform: scaleX(1); }
.tooltip { z-index: 50; padding: 6px 9px; border: 1px solid var(--line); border-radius: 4px; color: var(--text); background: var(--overlay); box-shadow: var(--shadow-8); font-size: 12px; }
.tooltip-arrow { fill: var(--overlay); }
.messages { flex: 1; min-height: 0; overflow-y: auto; padding: 26px max(24px, calc((100% - 850px) / 2)) 18px; }
.empty { text-align: center; color: var(--muted); padding-top: 12vh; }
.empty-logo { width: 54px; height: 54px; margin: auto; font-size: 24px; }
.empty h2 { color: var(--text); font-size: 26px; font-weight: 700; letter-spacing: -0.015em; margin: 18px 0 8px; }
.empty p { font-size: 14px; }
@keyframes msg-in { from { opacity: 0; transform: translateY(8px); } to { opacity: 1; transform: translateY(0); } }
@keyframes page-in { from { opacity: 0; transform: translateY(10px); } to { opacity: 1; transform: translateY(0); } }
.message { display: flex; gap: 12px; margin: 15px 0; align-items: flex-start; animation: msg-in .3s cubic-bezier(.22,.9,.36,1) both; }
/* Chat */
.page { min-height: 0; flex: 1; animation: page-in .16s ease both; }
.chat-layout { display: grid; grid-template-columns: 280px minmax(0, 1fr); }
.chat-layout.todo-open { grid-template-columns: 280px minmax(0, 1fr) 320px; }
.sessions-panel { overflow: auto; padding: 16px 12px; border-right: 1px solid var(--line); background: var(--panel); }
.sessions-panel > .search { margin-top: 12px; }
.session-list-head { display: flex; align-items: center; justify-content: space-between; margin: 18px 8px 7px; }
.session-count { color: var(--muted); font: 11px var(--font-mono); font-variant-numeric: tabular-nums; }
.session-list { display: grid; gap: 2px; }
.session-item { position: relative; width: 100%; padding: 10px 10px 10px 12px; border: 0; border-radius: 6px; color: var(--text); background: transparent; text-align: left; cursor: pointer; }
.session-item::before { content: ""; position: absolute; left: 0; top: 11px; bottom: 11px; width: 3px; border-radius: 2px; background: var(--accent); transform: scaleY(0); }
.session-item:hover { background: var(--panel-2); }
.session-item.active { background: var(--accent-soft); }
.session-item.active::before { transform: scaleY(1); }
.session-item strong { display: block; overflow: hidden; font-size: 13px; white-space: nowrap; text-overflow: ellipsis; }
.session-item small { display: flex; justify-content: space-between; margin-top: 4px; color: var(--muted); font-size: 11px; }
.plan-unread { display: inline-block; width: 6px; height: 6px; margin-left: 6px; border-radius: 50%; background: var(--accent); }
.chat-panel { min-width: 0; min-height: 0; display: flex; flex-direction: column; }
.chat-heading { min-height: 52px; flex: 0 0 52px; display: flex; align-items: center; justify-content: space-between; padding: 0 18px; border-bottom: 1px solid var(--line); background: var(--panel); }
.chat-heading strong, .chat-heading small { display: block; }
.chat-heading strong { font-size: 13px; }
.chat-heading small { margin-top: 2px; color: var(--muted); font-size: 11px; }
.chat-heading-actions { display: flex; align-items: center; gap: 6px; }
.todo-toggle { min-height: 28px; padding: 3px 9px; border: 1px solid var(--line-strong); border-radius: 4px; color: var(--text-soft); background: var(--panel); font-size: 11px; font-weight: 600; cursor: pointer; }
.todo-toggle { display: inline-flex; align-items: center; gap: 5px; }
.todo-toggle:hover { background: var(--panel-2); }
.todo-toggle.active { border-color: var(--accent-border); color: var(--accent); background: var(--accent-soft); }
.messages { flex: 1; min-height: 0; overflow-y: auto; padding: 24px max(24px, calc((100% - 880px) / 2)) 18px; }
.empty { padding-top: 12vh; color: var(--muted); text-align: center; }
.empty-logo { width: 52px; height: 52px; margin: auto; border-radius: 10px; font-size: 22px; box-shadow: var(--shadow-8); }
.empty h2 { margin: 18px 0 6px; color: var(--text); font-size: 24px; line-height: 32px; }
.empty p { margin: 0; font-size: 13px; }
.message { display: flex; align-items: flex-start; gap: 10px; margin: 14px 0; animation: message-in .18s ease both; }
.message.user { flex-direction: row-reverse; }
.avatar { width: 28px; height: 28px; flex: 0 0 28px; border: 1px solid var(--line); border-radius: 8px; display: grid; place-items: center; font-size: 12px; font-weight: 600; background: var(--panel-2); }
.message.assistant .avatar { background: var(--accent); color: var(--accent-contrast); border-color: var(--accent); font-weight: bold; }
.message-content { display: grid; gap: 8px; max-width: min(82%, 760px); min-width: 0; }
.avatar { width: 28px; height: 28px; flex: 0 0 28px; display: grid; place-items: center; border: 1px solid var(--line); border-radius: 6px; color: var(--text-soft); background: var(--panel); font-size: 11px; font-weight: 600; }
.message.assistant .avatar { border-color: var(--accent); color: var(--accent-contrast); background: var(--accent); }
.message-content { min-width: 0; max-width: min(84%, 760px); display: grid; gap: 7px; }
.message.user .message-content { justify-items: end; }
.bubble { max-width: 100%; padding: 12px 15px; border: 1px solid var(--line); border-radius: 12px; background: var(--panel); font-size: 15px; line-height: 1.7; overflow-wrap: anywhere; box-shadow: var(--edge); }
.message.user .bubble { background: var(--user-bubble); border-color: var(--accent-border); }
.message-attachments { display: grid; gap: 7px; width: min(100%, 520px); }
.attachment-card { display: grid; grid-template-columns: 42px minmax(0, 1fr) 30px; gap: 9px; align-items: center; padding: 8px; border: 1px solid var(--line); border-radius: 10px; background: var(--panel); }
.attachment-icon { width: 42px; height: 42px; border-radius: 7px; background: var(--panel-2); display: grid; place-items: center; color: var(--accent); }
.attachment-card strong, .attachment-card small { display: block; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.attachment-card strong { font-size: 13px; font-weight: 600; }.attachment-card small { margin-top: 3px; color: var(--muted); font-size: 10px; }
.attachment-card a { width: 28px; height: 28px; display: grid; place-items: center; border: 1px solid var(--line); border-radius: 7px; color: var(--accent); text-decoration: none; }
.image-attachment { width: min(100%, 680px); overflow: hidden; border: 1px solid var(--line); border-radius: 12px; background: var(--panel); }
.image-preview-link { display: grid; max-height: 520px; overflow: hidden; place-items: center; background: var(--panel-2); }
.image-preview { display: block; max-width: 100%; max-height: 520px; object-fit: contain; }
.attachment-meta { display: grid; grid-template-columns: minmax(0, 1fr) 30px; gap: 9px; align-items: center; padding: 8px 10px; }
.attachment-meta strong, .attachment-meta small { display: block; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.attachment-meta strong { font-size: 13px; font-weight: 600; }.attachment-meta small { margin-top: 3px; color: var(--muted); font-size: 10px; }
.attachment-meta > a { width: 28px; height: 28px; display: grid; place-items: center; border: 1px solid var(--line); border-radius: 7px; color: var(--accent); text-decoration: none; }
.bubble { max-width: 100%; padding: 10px 13px; border: 1px solid var(--line); border-radius: 8px; color: var(--text); background: var(--panel); box-shadow: var(--shadow-2); font-size: 14px; line-height: 1.65; overflow-wrap: anywhere; }
.message.user .bubble { border-color: var(--accent-border); border-top-right-radius: 2px; background: var(--user-bubble); }
.message.assistant .bubble { border-top-left-radius: 2px; }
.typing .bubble { color: var(--muted); }
.active-turn .message-content { width: min(82%, 760px); }
.active-turn .message-content { width: min(84%, 760px); }
.streaming { border-color: var(--accent-border); }
.reasoning-block, .live-tool { width: min(100%, 680px); overflow: hidden; border: 1px solid var(--line); border-radius: 10px; background: var(--panel); box-shadow: var(--edge); }
.reasoning-block summary, .live-tool summary { padding: 9px 12px; color: var(--muted); font-size: 12px; cursor: pointer; user-select: none; }
.message-attachments { width: min(100%, 520px); display: grid; gap: 7px; }
.attachment-card { display: grid; grid-template-columns: 38px minmax(0, 1fr) 30px; align-items: center; gap: 9px; padding: 8px; border: 1px solid var(--line); border-radius: 8px; background: var(--panel); box-shadow: var(--shadow-2); }
.attachment-icon { width: 38px; height: 38px; display: grid; place-items: center; border-radius: 6px; color: var(--accent); background: var(--accent-soft); }
.attachment-card strong, .attachment-card small, .attachment-meta strong, .attachment-meta small { display: block; overflow: hidden; white-space: nowrap; text-overflow: ellipsis; }
.attachment-card strong, .attachment-meta strong { font-size: 12px; }
.attachment-card small, .attachment-meta small { margin-top: 2px; color: var(--muted); font-size: 10px; }
.attachment-card a, .attachment-meta > a { width: 28px; height: 28px; display: grid; place-items: center; border: 1px solid var(--line); border-radius: 4px; color: var(--accent); text-decoration: none; }
.image-attachment { width: min(100%, 680px); overflow: hidden; border: 1px solid var(--line); border-radius: 8px; background: var(--panel); box-shadow: var(--shadow-2); }
.image-preview-link { max-height: 520px; display: grid; overflow: hidden; place-items: center; background: var(--panel-2); }
.image-preview { display: block; max-width: 100%; max-height: 520px; object-fit: contain; }
.attachment-meta { display: grid; grid-template-columns: minmax(0, 1fr) 30px; align-items: center; gap: 9px; padding: 8px 10px; }
.reasoning-block, .live-tool { width: min(100%, 680px); overflow: hidden; border: 1px solid var(--line); border-radius: 6px; background: var(--panel); }
.reasoning-block summary, .live-tool summary { padding: 8px 11px; color: var(--muted); font-size: 12px; cursor: pointer; user-select: none; }
.reasoning-block summary::marker, .live-tool summary::marker { color: var(--accent); }
.reasoning-content { padding: 10px 13px; border-top: 1px solid var(--line); color: var(--muted); font-size: 13px; line-height: 1.65; }
.reasoning-content { padding: 10px 12px; border-top: 1px solid var(--line); color: var(--text-soft); font-size: 13px; line-height: 1.6; }
.reasoning-block.historical { border-style: dashed; }
.live-tool summary { display: grid; grid-template-columns: 24px 1fr auto; align-items: center; gap: 8px; }
.live-tool summary strong { color: var(--text); font: 600 13px/1.4 var(--font-mono); }
.live-tool summary { display: grid; grid-template-columns: 22px 1fr auto; align-items: center; gap: 8px; }
.live-tool summary strong { color: var(--text); font: 600 12px/18px var(--font-mono); }
.live-tool summary small { color: var(--muted); }
.live-tool-details { display: grid; gap: 8px; padding: 10px 12px; border-top: 1px solid var(--line); }
.live-tool-details pre { max-height: 260px; margin: 0; padding: 10px; overflow: auto; border-radius: 8px; color: var(--text-soft); background: var(--code-bg); font: 12px/1.55 var(--font-mono); white-space: pre-wrap; }
.live-tool-details { display: grid; gap: 8px; padding: 10px 11px; border-top: 1px solid var(--line); }
.live-tool-details pre { max-height: 260px; margin: 0; padding: 10px; overflow: auto; border-radius: 4px; color: var(--text-soft); background: var(--code-bg); font: 12px/1.55 var(--font-mono); white-space: pre-wrap; }
.turn-status, .completion-status { color: var(--muted); font-size: 11px; }
.turn-status.failed, .turn-error { color: var(--danger); }
.turn-error { padding: 8px 10px; border-radius: 8px; background: var(--danger-soft); font-size: 12px; }
.pulse { display: inline-block; width: 6px; height: 6px; margin-right: 8px; border-radius: 50%; background: var(--accent); animation: pulse 1.1s infinite; }
@keyframes pulse { 50% { opacity: .25; transform: scale(.8); } }
.pulse { display: inline-block; width: 6px; height: 6px; margin-right: 7px; border-radius: 50%; background: var(--accent); animation: pulse 1.2s ease-in-out infinite; }
/* Markdown and tool cards */
.markdown-body { min-width: 0; }
.markdown-body > :first-child { margin-top: 0; }
.markdown-body > :last-child { margin-bottom: 0; }
.markdown-body p, .markdown-body ul, .markdown-body ol, .markdown-body blockquote, .markdown-body pre, .markdown-body table { margin: .65em 0; }
.markdown-body p, .markdown-body ul, .markdown-body ol, .markdown-body blockquote, .markdown-body pre, .markdown-body table { margin: .62em 0; }
.markdown-body ul, .markdown-body ol { padding-left: 1.4em; }
.markdown-body li + li { margin-top: .2em; }
.markdown-body h1, .markdown-body h2, .markdown-body h3, .markdown-body h4 { margin: 1em 0 .45em; line-height: 1.3; }
.markdown-body h1 { font-size: 1.45em; }
.markdown-body h2 { font-size: 1.25em; }
.markdown-body h3 { font-size: 1.1em; }
.markdown-body h1, .markdown-body h2, .markdown-body h3, .markdown-body h4 { margin: 1em 0 .42em; line-height: 1.3; }
.markdown-body h1 { font-size: 1.42em; }
.markdown-body h2 { font-size: 1.22em; }
.markdown-body h3 { font-size: 1.08em; }
.markdown-body a { color: var(--accent); text-underline-offset: 2px; }
.markdown-body blockquote { padding-left: 12px; border-left: 3px solid var(--accent-border); color: var(--muted); }
.markdown-body code { padding: .14em .35em; border: 1px solid var(--line); border-radius: 5px; color: var(--text); background: var(--code-bg); font: .88em/1.5 var(--font-mono); }
.markdown-body pre { max-width: 100%; padding: 12px 13px; overflow-x: auto; border: 1px solid var(--line); border-radius: 9px; background: var(--code-bg); }
.markdown-body blockquote { padding-left: 12px; border-left: 3px solid var(--accent); color: var(--muted); }
.markdown-body code { padding: .12em .32em; border-radius: 3px; color: var(--text); background: var(--code-bg); font: .88em/1.5 var(--font-mono); }
.markdown-body pre { max-width: 100%; padding: 12px; overflow-x: auto; border: 1px solid var(--line); border-radius: 6px; background: var(--code-bg); }
.markdown-body pre code { padding: 0; border: 0; background: transparent; }
.markdown-body table { display: block; max-width: 100%; border-collapse: collapse; overflow-x: auto; }
.markdown-body table { display: block; max-width: 100%; overflow-x: auto; border-collapse: collapse; }
.markdown-body th, .markdown-body td { padding: 7px 10px; border: 1px solid var(--line); text-align: left; }
.markdown-body th { background: var(--panel-2); }
.markdown-body hr { border: 0; border-top: 1px solid var(--line); }
.markdown-body img { max-width: 100%; border-radius: 9px; }
.tool-calls { display: grid; gap: 7px; width: min(100%, 680px); }
.tool-call { overflow: hidden; border: 1px solid var(--line); border-radius: 11px; background: var(--panel); box-shadow: var(--edge); transition: border-color .18s ease; }
.tool-call:hover { border-color: var(--line-strong); }
.markdown-body img { max-width: 100%; border-radius: 6px; }
.tool-calls { width: min(100%, 680px); display: grid; gap: 6px; }
.tool-call { overflow: hidden; border: 1px solid var(--line); border-radius: 6px; background: var(--panel); }
.tool-call.open { border-color: var(--accent-border); }
.tool-call-summary { width: 100%; min-height: 48px; display: grid; grid-template-columns: 30px 1fr auto 18px; align-items: center; gap: 9px; padding: 7px 11px; border: 0; color: var(--text); background: transparent; text-align: left; cursor: pointer; }
.tool-call-summary { width: 100%; min-height: 46px; display: grid; grid-template-columns: 28px 1fr auto 18px; align-items: center; gap: 9px; padding: 7px 10px; border: 0; color: var(--text); background: transparent; text-align: left; cursor: pointer; }
.tool-call-summary:hover { background: var(--panel-2); }
.tool-call-icon { width: 28px; height: 28px; display: grid; place-items: center; border-radius: 8px; color: var(--accent); background: var(--accent-soft); font-size: 14px; }
.tool-call-icon { width: 26px; height: 26px; display: grid; place-items: center; border-radius: 4px; color: var(--accent); background: var(--accent-soft); font-size: 13px; }
.tool-call-title small, .tool-call-title strong { display: block; }
.tool-call-title small { margin-bottom: 1px; color: var(--muted); font-size: 10px; font-weight: 500; }
.tool-call-title strong { font: 600 13px/1.4 var(--font-mono); }
.tool-call-title small { color: var(--muted); font-size: 10px; }
.tool-call-title strong { font: 600 12px/18px var(--font-mono); }
.tool-call-status { color: var(--muted); font-size: 11px; }
.tool-call-chevron { color: var(--muted); transition: transform .18s; }
.tool-call-chevron { color: var(--muted); transition: transform .12s; }
.tool-call.open .tool-call-chevron { transform: rotate(180deg); }
.tool-call-details { display: grid; gap: 12px; padding: 12px; border-top: 1px solid var(--line); background: color-mix(in srgb, var(--panel-2) 55%, transparent); }
.tool-call-details > div > span { display: block; margin-bottom: 6px; color: var(--muted); font-family: var(--font-mono); font-size: 10px; font-weight: 500; letter-spacing: .12em; text-transform: uppercase; }
.tool-call-details pre, .tool-result { max-height: 300px; margin: 0; padding: 10px 11px; overflow: auto; border: 1px solid var(--line); border-radius: 8px; color: var(--text-soft); background: var(--code-bg); font: 12px/1.6 var(--font-mono); white-space: pre-wrap; overflow-wrap: anywhere; }
.tool-result .markdown-body { font: inherit; }
.composer { position: relative; margin: 0 max(18px, calc((100% - 850px) / 2)) 18px; border: 1px solid var(--line); background: var(--panel); border-radius: 14px; padding: 10px 11px 6px; display: grid; grid-template-columns: 38px 1fr 38px; box-shadow: var(--edge), var(--shadow); transition: border-color .18s ease; }
.composer:focus-within { border-color: var(--accent-border); }
.composer textarea { resize: none; max-height: 180px; background: transparent; border: 0; outline: 0; color: var(--text); padding: 7px; font-size: 15px; line-height: 1.6; }
.tool-call-details { display: grid; gap: 12px; padding: 12px; border-top: 1px solid var(--line); background: var(--panel-2); }
.tool-call-details > div > span { display: block; margin-bottom: 6px; color: var(--muted); font-size: 11px; font-weight: 600; }
.tool-call-details pre, .tool-result { max-height: 300px; margin: 0; padding: 10px 11px; overflow: auto; border: 1px solid var(--line); border-radius: 4px; color: var(--text-soft); background: var(--code-bg); font: 12px/1.6 var(--font-mono); white-space: pre-wrap; overflow-wrap: anywhere; }
/* Composer and active plan */
.composer { position: relative; margin: 0 max(18px, calc((100% - 880px) / 2)) 18px; display: grid; grid-template-columns: 36px 1fr 36px; padding: 9px 10px 5px; border: 1px solid var(--line-strong); border-radius: 8px; background: var(--panel); box-shadow: var(--shadow-16); }
.composer:focus-within { border-color: var(--accent); border-bottom-width: 2px; }
.composer textarea { max-height: 180px; resize: none; padding: 6px 8px; border: 0; outline: 0; color: var(--text); background: transparent; font-size: 14px; line-height: 1.6; }
.file-input { display: none; }
.attach { width: 34px; height: 34px; align-self: end; border: 1px solid var(--line); border-radius: 9px; color: var(--muted); background: var(--panel-2); cursor: pointer; font-size: 18px; transition: color .15s ease, border-color .15s ease, transform .12s ease; }
.attach:hover { color: var(--accent); border-color: var(--accent-border); }
.attach:active { transform: scale(.94); }
.pending-uploads { grid-column: 1 / -1; display: flex; gap: 7px; padding: 2px 4px 8px; overflow-x: auto; }
.pending-upload { min-width: 170px; max-width: 260px; display: grid; grid-template-columns: 24px minmax(0, 1fr) 22px; gap: 6px; align-items: center; padding: 7px; border: 1px solid var(--line); border-radius: 9px; background: var(--panel-2); }
.pending-upload.error { border-color: var(--danger); }.pending-upload strong, .pending-upload small { display: block; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }.pending-upload strong { font-size: 11px; font-weight: 600; }.pending-upload small { color: var(--muted); font-size: 10px; }.pending-upload button { border: 0; color: var(--muted); background: none; cursor: pointer; }
.send { width: 36px; height: 36px; border-radius: 10px; color: var(--accent-contrast); background: var(--accent); border: 0; font-size: 19px; cursor: pointer; transition: transform .12s ease, box-shadow .18s ease, background-color .15s ease; }
.send:hover:not(:disabled) { background: var(--accent-hover); box-shadow: 0 5px 16px var(--accent-glow); }
.send:active:not(:disabled) { transform: scale(.92); }
.composer small { grid-column: 1 / -1; display: flex; justify-content: space-between; padding: 3px 7px; color: var(--muted); font-size: 11px; }
.composer small .online { color: var(--accent); }
.todo-panel { display: none; min-width: 0; border-left: 1px solid var(--line); background: var(--sidebar); overflow: hidden; }
.attach, .send { width: 32px; height: 32px; align-self: end; display: grid; place-items: center; border-radius: 4px; cursor: pointer; }
.attach { border: 0; color: var(--text-soft); background: transparent; }
.attach:hover { background: var(--panel-2); }
.send { border: 1px solid var(--accent); color: var(--accent-contrast); background: var(--accent); }
.send:hover:not(:disabled) { border-color: var(--accent-hover); background: var(--accent-hover); }
.send:active:not(:disabled) { border-color: var(--accent-pressed); background: var(--accent-pressed); }
.composer small { grid-column: 1 / -1; display: flex; justify-content: space-between; padding: 3px 7px 0; color: var(--muted); font-size: 10px; }
.composer small .online { color: var(--signal); }
.pending-uploads { grid-column: 1 / -1; display: flex; gap: 7px; padding: 2px 3px 8px; overflow-x: auto; }
.pending-upload { min-width: 170px; max-width: 260px; display: grid; grid-template-columns: 24px minmax(0, 1fr) 22px; align-items: center; gap: 6px; padding: 7px; border: 1px solid var(--line); border-radius: 6px; background: var(--panel-2); }
.pending-upload.error { border-color: var(--danger); }
.pending-upload strong, .pending-upload small { display: block; overflow: hidden; white-space: nowrap; text-overflow: ellipsis; }
.pending-upload strong { font-size: 11px; }
.pending-upload small { color: var(--muted); font-size: 10px; }
.pending-upload button { border: 0; color: var(--muted); background: none; cursor: pointer; }
.command-menu { position: absolute; z-index: 5; right: 0; bottom: calc(100% + 8px); left: 0; max-height: min(360px, 48vh); overflow-y: auto; padding: 6px; border: 1px solid var(--line); border-radius: 8px; background: var(--overlay); box-shadow: var(--shadow-16); }
.command-menu-heading { position: sticky; z-index: 1; top: -6px; display: flex; justify-content: space-between; gap: 16px; padding: 8px 9px; color: var(--muted); background: var(--overlay); font-size: 11px; }
.command-menu-heading kbd { color: var(--muted); font: inherit; }
.command-menu button { width: 100%; display: grid; grid-template-columns: minmax(100px, auto) 1fr; gap: 14px; padding: 8px 9px; border: 0; border-radius: 4px; color: var(--text); background: transparent; text-align: left; cursor: pointer; }
.command-menu button:hover, .command-menu button.selected { background: var(--accent-soft); }
.command-menu code { color: var(--accent); font: 600 12px/1.4 var(--font-mono); }
.command-menu button span { color: var(--text-soft); font-size: 12px; }
.todo-panel { display: none; min-width: 0; overflow: hidden; border-left: 1px solid var(--line); background: var(--panel); }
.todo-open .todo-panel { display: flex; flex-direction: column; }
.todo-heading { min-height: 78px; padding: 14px 14px 12px 16px; border-bottom: 1px solid var(--line); display: flex; align-items: flex-start; justify-content: space-between; gap: 10px; }
.todo-heading { min-height: 74px; display: flex; align-items: flex-start; justify-content: space-between; gap: 10px; padding: 14px 14px 11px 16px; border-bottom: 1px solid var(--line); }
.todo-heading small, .todo-heading strong { display: block; }
.todo-heading small { color: var(--muted); font-size: 11px; margin-bottom: 5px; }
.todo-heading strong { font-size: 14px; font-weight: 600; line-height: 1.4; }
.todo-heading small { margin-bottom: 4px; color: var(--muted); font-size: 11px; }
.todo-heading strong { font-size: 13px; line-height: 18px; }
.todo-progress { height: 3px; background: var(--panel-2); }
.todo-progress span { display: block; height: 100%; background: var(--accent); transition: width .2s; }
.todo-summary { display: flex; justify-content: space-between; padding: 11px 16px; color: var(--muted); font-size: 11px; }
.todo-items { min-height: 0; overflow-y: auto; padding: 4px 12px 18px; }
.todo-item { display: grid; grid-template-columns: 24px 1fr; gap: 8px; padding: 10px 8px; border-radius: 9px; color: var(--text-soft); }
.todo-item + .todo-item { margin-top: 3px; }
.todo-item.running { background: var(--accent-soft); color: var(--text); }
.todo-summary { display: flex; justify-content: space-between; padding: 10px 16px; color: var(--muted); font-size: 11px; }
.todo-items { min-height: 0; overflow-y: auto; padding: 3px 10px 18px; }
.todo-item { display: grid; grid-template-columns: 22px 1fr; gap: 8px; padding: 9px 7px; border-radius: 6px; color: var(--text-soft); }
.todo-item + .todo-item { margin-top: 2px; }
.todo-item.running { color: var(--text); background: var(--accent-soft); }
.todo-item.blocked { background: var(--danger-soft); }
.todo-item.done { color: var(--muted); }
.todo-icon { width: 21px; height: 21px; display: grid; place-items: center; border: 1px solid var(--line); border-radius: 50%; font-size: 12px; }
.todo-item.running .todo-icon { color: var(--accent); border-color: var(--accent-border); }
.todo-item.blocked .todo-icon { color: var(--danger); border-color: var(--danger); }
.todo-item.done .todo-icon { color: var(--accent); }
.todo-icon { width: 20px; height: 20px; display: grid; place-items: center; border: 1px solid var(--line-strong); border-radius: 50%; font-size: 11px; }
.todo-item.running .todo-icon { border-color: var(--accent); color: var(--accent); }
.todo-item.blocked .todo-icon { border-color: var(--danger); color: var(--danger); }
.todo-item.done .todo-icon { color: var(--signal); }
.todo-item strong, .todo-item small { display: block; }
.todo-item strong { font-size: 13px; font-weight: 600; line-height: 1.45; }
.todo-item small { color: var(--muted); font-size: 11px; margin-top: 3px; }
.todo-item p { margin: 6px 0 0; color: var(--text-soft); font-size: 11px; line-height: 1.5; white-space: pre-wrap; overflow-wrap: anywhere; }
.command-menu { position: absolute; z-index: 5; left: 0; right: 0; bottom: calc(100% + 8px); max-height: min(360px, 48vh); overflow-y: auto; padding: 6px; border: 1px solid var(--line); border-radius: 13px; background: var(--overlay); box-shadow: var(--shadow); }
.command-menu-heading { position: sticky; top: -6px; z-index: 1; display: flex; justify-content: space-between; gap: 16px; padding: 9px 10px 8px; color: var(--muted); background: var(--overlay); font-size: 11px; }
.command-menu-heading kbd { color: var(--muted); font: inherit; }
.command-menu button { display: grid; grid-template-columns: minmax(100px, auto) 1fr; gap: 14px; width: 100%; padding: 9px 10px; border: 0; border-radius: 8px; color: var(--text); background: transparent; text-align: left; cursor: pointer; }
.command-menu button:hover, .command-menu button.selected { background: var(--accent-soft); }
.command-menu code { color: var(--accent); font: 600 13px/1.4 var(--font-mono); }
.command-menu button span { color: var(--text-soft); font-size: 13px; line-height: 1.4; }
.content-page { padding: 22px 26px; overflow: auto; }
.toolbar { display: flex; justify-content: space-between; align-items: center; margin-bottom: 18px; gap: 10px; }
.tabs { display: flex; background: var(--panel); padding: 3px; border: 1px solid var(--line); border-radius: 10px; }
.tabs button { border: 0; background: none; color: var(--muted); padding: 7px 13px; border-radius: 7px; cursor: pointer; }
.tabs button[data-state="active"] { background: var(--panel-2); color: var(--text); }
.filters select { height: 39px; background: var(--panel); border: 1px solid var(--line); color: var(--text); border-radius: 9px; padding: 0 10px; }
.grow { flex: 1; max-width: 520px; }
.cards { display: grid; gap: 11px; }
.card { background: var(--panel); border: 1px solid var(--line); border-radius: var(--radius); padding: 15px 17px; box-shadow: var(--edge), var(--shadow-sm); transition: transform .18s ease, box-shadow .18s ease, border-color .18s ease; }
.card:hover { transform: translateY(-2px); border-color: var(--line-strong); box-shadow: var(--edge), var(--shadow-lift); }
.card-row { display: flex; justify-content: space-between; gap: 18px; align-items: flex-start; }
.card h3 { font-size: 15px; font-weight: 600; margin: 0 0 7px; }
.card p { color: var(--text-soft); font-size: 13px; line-height: 1.6; margin: 5px 0; white-space: pre-wrap; }
.meta { display: flex; gap: 14px; flex-wrap: wrap; color: var(--muted); font-size: 12px; }
.badge { border-radius: 99px; padding: 4px 9px; font-size: 11px; font-weight: 500; background: var(--panel-2); color: var(--text-soft); white-space: nowrap; }
.badge.ok { background: var(--success-soft); color: var(--signal); }
.badge.fail { background: var(--danger-soft); color: var(--danger); }
.badge.run { background: var(--warning-soft); color: var(--warning); }
.details { margin-top: 12px; border-top: 1px solid var(--line); padding-top: 10px; }
.todo-item strong { font-size: 12px; line-height: 18px; }
.todo-item small { margin-top: 2px; color: var(--muted); font-size: 10px; }
.todo-item p { margin: 5px 0 0; color: var(--text-soft); font-size: 11px; line-height: 16px; white-space: pre-wrap; overflow-wrap: anywhere; }
/* Management pages */
.content-page { overflow: auto; padding: 24px 28px 40px; }
.toolbar { display: flex; align-items: center; justify-content: space-between; gap: 12px; margin-bottom: 18px; }
.filters { align-items: center; }
.cards { display: grid; gap: 10px; }
.card, .panel, .metric { border: 1px solid var(--line); border-radius: var(--radius); background: var(--panel); box-shadow: var(--shadow-2); }
.card { padding: 15px 17px; transition: border-color .1s ease; }
.card:hover { border-color: var(--line-strong); }
.card-row { display: flex; align-items: flex-start; justify-content: space-between; gap: 18px; }
.card h3 { margin: 0 0 6px; font-size: 14px; }
.card p { margin: 5px 0; color: var(--text-soft); font-size: 13px; line-height: 20px; white-space: pre-wrap; }
.meta { display: flex; flex-wrap: wrap; gap: 12px; color: var(--muted); font-size: 11px; }
.badge { display: inline-flex; align-items: center; min-height: 22px; padding: 2px 8px; border-radius: 4px; color: var(--text-soft); background: var(--panel-2); font-size: 11px; font-weight: 600; white-space: nowrap; }
.badge.ok { color: var(--signal); background: var(--signal-soft); }
.badge.fail { color: var(--danger); background: var(--danger-soft); }
.badge.run { color: var(--warning); background: var(--warning-soft); }
.details { margin-top: 12px; padding-top: 10px; border-top: 1px solid var(--line); }
.run-row + .run-row { margin-top: 7px; }
.metrics { display: grid; grid-template-columns: repeat(3, 1fr); gap: 10px; margin-bottom: 16px; }
.metric { background: var(--panel); border: 1px solid var(--line); border-radius: 12px; padding: 14px; box-shadow: var(--edge), var(--shadow-sm); transition: border-color .18s ease; }
.metric:hover { border-color: var(--line-strong); }
.metric b { font-size: 26px; font-weight: 700; letter-spacing: -0.01em; display: block; font-variant-numeric: tabular-nums; }
.metric { padding: 15px 16px; }
.metric b { display: block; font: 600 28px/36px "Segoe UI Variable Display", var(--font-ui); font-variant-numeric: tabular-nums; }
.metric small { color: var(--muted); }
.memory-content { font-size: 14px !important; }
.memory-key { color: var(--accent); font-family: var(--font-mono); }
.log-view { margin: 0; background: var(--code-bg); border: 1px solid var(--line); border-radius: 12px; padding: 16px; color: var(--text-soft); min-height: calc(100vh - 190px); font: 12px/1.7 var(--font-mono); white-space: pre-wrap; overflow: auto; }
.log-meta { color: var(--muted); font-size: 12px; margin: -8px 0 8px; }
.switch-label { color: var(--muted); font-size: 13px; display: flex; gap: 7px; align-items: center; cursor: pointer; }
.switch { width: 34px; height: 20px; padding: 2px; border: 1px solid var(--line); border-radius: 99px; background: var(--panel); cursor: pointer; }
.switch[data-state="checked"] { background: var(--accent); border-color: var(--accent); }
.switch-thumb { display: block; width: 14px; height: 14px; background: var(--muted); border-radius: 50%; transition: transform .15s, background .15s; }
.switch-thumb[data-state="checked"] { transform: translateX(14px); background: var(--accent-contrast); }
.settings-grid { display: grid; grid-template-columns: 170px minmax(0, 1fr); gap: 18px; }
.settings-nav { display: flex; flex-direction: column; gap: 4px; }
.editor-card { border: 1px solid var(--line); background: var(--panel); border-radius: var(--radius); overflow: hidden; }
.editor-head { height: 62px; padding: 0 15px; display: flex; justify-content: space-between; align-items: center; border-bottom: 1px solid var(--line); }
.log-view { min-height: calc(100vh - 190px); margin: 0; padding: 14px 16px; overflow: auto; border: 1px solid var(--line); border-radius: 8px; color: var(--text-soft); background: var(--code-bg); font: 12px/1.7 var(--font-mono); white-space: pre-wrap; }
.log-meta { margin: -8px 0 8px; color: var(--muted); font-size: 12px; }
.switch-label { display: flex; align-items: center; gap: 7px; color: var(--muted); font-size: 13px; cursor: pointer; }
.switch { width: 40px; height: 20px; padding: 2px; border: 0; border-radius: 10px; background: var(--line-strong); cursor: pointer; }
.switch[data-state="checked"] { background: var(--accent); }
.switch-thumb { display: block; width: 16px; height: 16px; border-radius: 50%; background: #fff; box-shadow: var(--shadow-2); transition: transform .12s; }
.switch-thumb[data-state="checked"] { transform: translateX(20px); }
.settings-grid { display: grid; grid-template-columns: 168px minmax(0, 1fr); gap: 18px; }
.settings-nav { display: flex; flex-direction: column; gap: 2px; }
.editor-card { overflow: hidden; border: 1px solid var(--line); border-radius: 8px; background: var(--panel); box-shadow: var(--shadow-2); }
.editor-head { min-height: 62px; display: flex; align-items: center; justify-content: space-between; padding: 0 15px; border-bottom: 1px solid var(--line); }
.editor-head strong, .editor-head small { display: block; }
.editor-head strong { font-weight: 600; }
.editor-head small { color: var(--muted); font-size: 11px; margin-top: 4px; }
.editor-card textarea { display: block; width: 100%; height: calc(100vh - 275px); min-height: 420px; resize: vertical; border: 0; outline: 0; padding: 18px; background: var(--code-bg); color: var(--text); font: 13px/1.65 var(--font-mono); tab-size: 2; }
.notice { padding: 11px 15px; color: var(--muted); font-size: 12px; border-top: 1px solid var(--line); }
.editor-head small { margin-top: 3px; color: var(--muted); font-size: 11px; }
.editor-card textarea { width: 100%; height: calc(100vh - 275px); min-height: 420px; display: block; resize: vertical; padding: 18px; border: 0; outline: 0; color: var(--text); background: var(--code-bg); font: 13px/1.65 var(--font-mono); tab-size: 2; }
.notice { padding: 10px 15px; border-top: 1px solid var(--line); color: var(--muted); font-size: 12px; }
.cap { display: inline-flex; align-items: center; gap: 5px; padding: 3px 7px; border-radius: 4px; font-size: 10.5px; font-weight: 600; }
.cap.signal { border: 1px solid var(--signal-border); color: var(--signal); background: var(--signal-soft); }
.cap.accent { border: 1px solid var(--accent-border); color: var(--accent); background: var(--accent-soft); }
.cap.danger { border: 1px solid var(--danger-border); color: var(--danger); background: var(--danger-soft); }
.cap.info { border: 1px solid var(--info-border); color: var(--info); background: var(--info-soft); }
.label-caps { color: var(--muted); font-size: 11px; font-weight: 600; }
.mono { font-family: var(--font-mono); font-variant-numeric: tabular-nums; }
code { color: var(--accent); }
#toast { position: fixed; z-index: 100; right: 22px; bottom: 22px; color: var(--text); background: var(--overlay); border: 1px solid var(--line); border-radius: 10px; padding: 11px 15px; font-size: 13px; opacity: 0; transform: translateY(8px); pointer-events: none; transition: .2s; box-shadow: var(--shadow); }
#toast.show { opacity: 1; transform: none; }
#toast.error { border-color: var(--danger); color: var(--danger); }
.loading, .empty-card { text-align: center; color: var(--muted); padding: 50px; }
.loading, .empty-card { padding: 50px; color: var(--muted); text-align: center; }
.empty-card.compact { padding: 24px 8px; }
.error-text { color: var(--danger) !important; }
.mono { font-family: var(--font-mono); font-variant-numeric: tabular-nums; }
.label-caps { font-family: var(--font-mono); font-size: 10px; font-weight: 500; letter-spacing: .14em; color: var(--faint); }
.panel { background: var(--panel); border: 1px solid var(--line); border-radius: var(--radius); box-shadow: var(--edge), var(--shadow-sm); }
.cap { display: inline-flex; align-items: center; gap: 4px; font-size: 10.5px; font-weight: 600; border-radius: 6px; padding: 3px 8px; }
.cap.signal { color: var(--signal); background: var(--signal-soft); border: 1px solid var(--signal-border); }
.cap.accent { color: var(--accent); background: var(--accent-soft); border: 1px solid var(--accent-border); }
.cap.danger { color: var(--danger); background: var(--danger-soft); border: 1px solid var(--danger-border); }
.cap.info { color: var(--info); background: var(--info-soft); border: 1px solid var(--info-border); }
@keyframes spine-pulse { 0%,100% { opacity: 1; } 50% { opacity: .35; } }
.pulse-dot { width: 7px; height: 7px; border-radius: 50%; display: inline-block; animation: spine-pulse 1.6s ease-in-out infinite; }
.pulse-dot.active { background: var(--spine-accent); box-shadow: 0 0 10px var(--spine-accent); }
.pulse-dot.idle { background: var(--spine-signal); animation: none; }
#toast { position: fixed; z-index: 100; right: 20px; bottom: 20px; max-width: min(420px, calc(100vw - 40px)); padding: 11px 14px; border: 1px solid var(--line); border-left: 4px solid var(--accent); border-radius: 6px; color: var(--text); background: var(--overlay); box-shadow: var(--shadow-16); font-size: 13px; opacity: 0; transform: translateY(8px); pointer-events: none; transition: opacity .16s, transform .16s; }
#toast.show { opacity: 1; transform: none; }
#toast.error { border-left-color: var(--danger); color: var(--danger); }
@keyframes pulse { 50% { opacity: .3; } }
@keyframes message-in { from { opacity: 0; transform: translateY(4px); } }
@keyframes page-in { from { opacity: 0; } }
@keyframes spine-pulse { 50% { opacity: .35; } }
.pulse-dot { width: 7px; height: 7px; display: inline-block; border-radius: 50%; }
.pulse-dot.active { background: var(--accent); animation: spine-pulse 1.5s ease-in-out infinite; }
.pulse-dot.idle { background: var(--signal); }
@media (prefers-reduced-motion: reduce) {
*, *::before, *::after { animation-duration: .01ms !important; animation-iteration-count: 1 !important; transition-duration: .01ms !important; }
.pulse-dot, .pulse { animation: none; }
*, *::before, *::after { scroll-behavior: auto !important; animation-duration: .01ms !important; animation-iteration-count: 1 !important; transition-duration: .01ms !important; }
}
@media (max-width: 960px) {
.theme-toggle { min-width: 32px; width: 32px; padding: 0; }
.theme-toggle span { display: none; }
.page-heading { min-width: 150px; }
}
@media (max-width: 800px) {
.shell { grid-template-columns: 1fr; }
.sidebar { position: fixed; z-index: 10; inset: 0 auto 0 0; width: 232px; transform: translateX(-100%); transition: .2s; box-shadow: var(--shadow); }
.sidebar { position: fixed; inset: 0 auto 0 0; width: 248px; transform: translateX(-100%); transition: transform .16s ease; box-shadow: var(--shadow-16); }
.sidebar.open { transform: none; }
.topbar .menu { display: block; }
.chat-layout { grid-template-columns: 1fr; }
.chat-layout.todo-open { grid-template-columns: 1fr; }
.nav-scrim { position: fixed; z-index: 25; inset: 0; display: block; border: 0; background: rgb(0 0 0 / 42%); }
.topbar { min-height: 62px; flex-basis: 62px; padding: 0 14px; }
.topbar .menu { display: inline-grid; }
.page-heading p { display: none; }
.chat-layout, .chat-layout.todo-open { grid-template-columns: 1fr; }
.sessions-panel { display: none; }
.todo-open .todo-panel { position: fixed; z-index: 20; top: 76px; right: 0; bottom: 0; width: min(340px, 92vw); box-shadow: var(--shadow); }
.content-page { padding: 16px; }
.todo-open .todo-panel { position: fixed; z-index: 20; top: 62px; right: 0; bottom: 0; width: min(340px, 92vw); box-shadow: var(--shadow-16); }
.content-page { padding: 18px 16px 32px; }
.settings-grid { grid-template-columns: 1fr; }
.settings-nav { flex-direction: row; overflow: auto; }
.settings-nav { flex-direction: row; overflow-x: auto; }
.metrics { grid-template-columns: 1fr; }
.filters { align-items: stretch; flex-wrap: wrap; }
.grow { max-width: none; width: 100%; flex-basis: 100%; }
.message-content { max-width: 88%; }
.theme-toggle span:last-child { display: none; }
.message-content { max-width: 90%; }
.messages { padding-right: 16px; padding-left: 16px; }
.composer { margin-right: 12px; margin-left: 12px; }
}
@media (max-width: 520px) {
.topbar h1 { font-size: 18px; }
.page-heading { min-width: 0; }
.messages { padding-top: 16px; }
.message-content { max-width: calc(100% - 38px); }
.toolbar { align-items: stretch; flex-wrap: wrap; }
.toolbar .tabs { width: 100%; overflow-x: auto; }
.card-row { gap: 10px; }
}