From 7a144cea8acbf1b5a678a0061e063cb79611e312 Mon Sep 17 00:00:00 2001 From: xiaoxixi Date: Wed, 15 Jul 2026 18:16:18 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BC=98=E5=8C=96=E5=89=8D=E7=AB=AF=E6=98=BE?= =?UTF-8?q?=E7=A4=BA=EF=BC=8C=E6=B8=B2=E6=9F=93markdown=E3=80=81=E6=96=B0?= =?UTF-8?q?=E5=A2=9E=E9=BB=91=E7=99=BD=E4=B8=BB=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- AGENTS.md | 1 + README.md | 3 +- docs/ARCHITECTURE.md | 2 +- src/channels/cli_chat.rs | 11 +- src/protocol.rs | 6 + tests/test_request_format.rs | 39 +++++- webui/index.html | 4 +- webui/package-lock.json | 23 ++++ webui/package.json | 2 + webui/src/App.svelte | 14 +++ webui/src/lib/Markdown.svelte | 20 +++ webui/src/lib/ToolCallCard.svelte | 32 +++++ webui/src/pages/ChatPage.svelte | 31 ++++- webui/src/styles.css | 198 +++++++++++++++++++++--------- 14 files changed, 318 insertions(+), 68 deletions(-) create mode 100644 webui/src/lib/Markdown.svelte create mode 100644 webui/src/lib/ToolCallCard.svelte diff --git a/AGENTS.md b/AGENTS.md index edd1b7f..92659cb 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -82,6 +82,7 @@ Scheduler → SessionManager scheduled execution → AgentLoop → Scheduler del - **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 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; session history must preserve structured tool-call metadata so calls and results remain independently collapsible - **Providers** are pure HTTP clients; no bus/session/channel awareness - **Tools** are executed by `AgentLoop`; they receive raw arguments and return string results diff --git a/README.md b/README.md index a5f4135..019407d 100644 --- a/README.md +++ b/README.md @@ -103,7 +103,8 @@ http://127.0.0.1:19876/ WebUI 随二进制嵌入,不需要 Node.js、npm 或单独部署静态文件,提供: -- 在线聊天、会话创建/切换、历史回放,以及基于 Gateway 实时命令清单的 `/` 斜杠命令补全。 +- 在线聊天、会话创建/切换、历史回放、Markdown 消息、可折叠工具调用卡片,以及基于 Gateway 实时命令清单的 `/` 斜杠命令补全。 +- 可持久化的浅色/深色主题,首次访问时跟随系统偏好。 - Cron 定时任务、最近运行记录和后台子任务状态。 - 当前聊天 session 的可展开 Todo 侧栏;计划变化时自动展开,其他 session 的变化显示未读提示。 - Knowledge/Timeline 记忆的分类与全文检索。 diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index e91b502..d7a93f3 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -197,7 +197,7 @@ WebSocket 每个客户端的 writer task 是连接局部任务,由连接 handl ### 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;聊天页的 Todo 侧栏默认隐藏,按 session 保存快照和未读状态,并通过结构化 `session_plan`/`plan_updated` 帧刷新,计划变化不会写入聊天历史;斜杠命令补全通过 `get_slash_commands` 获取 Gateway 的实时命令与别名清单,不在前端重复定义;WebUI 不直接调用 Provider 或 SessionManager。 +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 在本轮完成后刷新历史并将其渲染为默认折叠的工具卡片;聊天页的 Todo 侧栏默认隐藏,按 session 保存快照和未读状态,并通过结构化 `session_plan`/`plan_updated` 帧刷新,计划变化不会写入聊天历史;斜杠命令补全通过 `get_slash_commands` 获取 Gateway 的实时命令与别名清单,不在前端重复定义;WebUI 不直接调用 Provider 或 SessionManager。 同源 `/api/*` 管理接口只提供显式白名单能力: diff --git a/src/channels/cli_chat.rs b/src/channels/cli_chat.rs index 3f061b5..c3e2ad9 100644 --- a/src/channels/cli_chat.rs +++ b/src/channels/cli_chat.rs @@ -389,13 +389,22 @@ impl CliChatChannel { })) => { let messages = messages .into_iter() - .filter(|message| !message.content.is_empty()) + .filter(|message| { + !message.content.is_empty() + || message.tool_calls.is_some() + || message.role == "tool" + }) .map(|message| HistoryMessage { id: message.id, seq: message.seq, role: message.role, content: message.content, created_at: message.created_at, + tool_call_id: message.tool_call_id, + tool_name: message.tool_name, + tool_calls: message + .tool_calls + .and_then(|calls| serde_json::from_str(&calls).ok()), }) .collect(); let _ = client diff --git a/src/protocol.rs b/src/protocol.rs index 3dc6f08..637c68f 100644 --- a/src/protocol.rs +++ b/src/protocol.rs @@ -26,6 +26,12 @@ pub struct HistoryMessage { pub role: String, pub content: String, pub created_at: i64, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub tool_call_id: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub tool_name: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub tool_calls: Option>, } #[derive(Debug, Clone, Serialize, Deserialize)] diff --git a/tests/test_request_format.rs b/tests/test_request_format.rs index fc41047..bf2d10b 100644 --- a/tests/test_request_format.rs +++ b/tests/test_request_format.rs @@ -1,5 +1,5 @@ use picobot::protocol::{HistoryMessage, SessionSummary, WsInbound, WsOutbound}; -use picobot::providers::{ChatCompletionRequest, Message}; +use picobot::providers::{ChatCompletionRequest, Message, ToolCall}; use picobot::work::{TaskItem, TaskPlan}; /// Test that message with special characters is properly escaped @@ -136,6 +136,9 @@ fn test_bounded_session_history_protocol() { role: "user".to_string(), content: "你好".to_string(), created_at: 123, + tool_call_id: None, + tool_name: None, + tool_calls: None, }], }; let decoded: WsOutbound = @@ -149,6 +152,40 @@ fn test_bounded_session_history_protocol() { } } +#[test] +fn test_session_history_preserves_tool_call_metadata() { + let outbound = WsOutbound::SessionHistory { + session_id: "cli_chat:client:dialog".to_string(), + messages: vec![HistoryMessage { + id: "m-tool".to_string(), + seq: 2, + role: "assistant".to_string(), + content: String::new(), + created_at: 124, + tool_call_id: None, + tool_name: None, + tool_calls: Some(vec![ToolCall { + id: "call-1".to_string(), + name: "read_file".to_string(), + arguments: serde_json::json!({ "path": "README.md" }), + }]), + }], + }; + + let json = serde_json::to_string(&outbound).unwrap(); + assert!(json.contains(r#""tool_calls""#)); + assert!(json.contains(r#""read_file""#)); + let decoded: WsOutbound = serde_json::from_str(&json).unwrap(); + match decoded { + WsOutbound::SessionHistory { messages, .. } => { + let calls = messages[0].tool_calls.as_ref().unwrap(); + assert_eq!(calls[0].name, "read_file"); + assert_eq!(calls[0].arguments["path"], "README.md"); + } + other => panic!("unexpected decoded variant: {other:?}"), + } +} + #[test] fn test_session_plan_protocol_is_structured_and_versioned() { let inbound = WsInbound::GetSessionPlan { diff --git a/webui/index.html b/webui/index.html index 3c9c102..0c1f0b7 100644 --- a/webui/index.html +++ b/webui/index.html @@ -3,8 +3,8 @@ - - + + PicoBot Console diff --git a/webui/package-lock.json b/webui/package-lock.json index 6d8d414..4cc20e5 100644 --- a/webui/package-lock.json +++ b/webui/package-lock.json @@ -9,6 +9,8 @@ "version": "1.1.2", "dependencies": { "bits-ui": "^2.0.0", + "dompurify": "^3.4.12", + "marked": "^18.0.6", "svelte": "^5.0.0" }, "devDependencies": { @@ -1081,6 +1083,15 @@ "integrity": "sha512-4CXDYRBGqN+57wVJkuXBYmpAVUSg3L6JAQa/DFqm238G73E1wuyc/JhGQJzN7vUf/CMphYau2zXbfWzDR5aTEw==", "license": "MIT" }, + "node_modules/dompurify": { + "version": "3.4.12", + "resolved": "https://registry.npmmirror.com/dompurify/-/dompurify-3.4.12.tgz", + "integrity": "sha512-zQvGet8Z2sWbQhCmfFz/T5QWH2oBmjnqK3qvOjaqaNLrLEF912WamU+ohnTp0TCep/MFVHpdJuCZEdFOdTnEFg==", + "license": "(MPL-2.0 OR Apache-2.0)", + "optionalDependencies": { + "@types/trusted-types": "^2.0.7" + } + }, "node_modules/esbuild": { "version": "0.28.1", "resolved": "https://mirrors.cloud.tencent.com/npm/esbuild/-/esbuild-0.28.1.tgz", @@ -1217,6 +1228,18 @@ "@jridgewell/sourcemap-codec": "^1.5.5" } }, + "node_modules/marked": { + "version": "18.0.6", + "resolved": "https://registry.npmmirror.com/marked/-/marked-18.0.6.tgz", + "integrity": "sha512-MrV5puXBfuiy6wl6DLaq3BtIJQAJToAd5zt/ZKhRfGRAuFPALE7/4Y7jnxRQoEgK/pBgurGqLyAuRgZ2xOjr6w==", + "license": "MIT", + "bin": { + "marked": "bin/marked.js" + }, + "engines": { + "node": ">= 20" + } + }, "node_modules/mri": { "version": "1.2.0", "resolved": "https://mirrors.cloud.tencent.com/npm/mri/-/mri-1.2.0.tgz", diff --git a/webui/package.json b/webui/package.json index de63068..b73e41d 100644 --- a/webui/package.json +++ b/webui/package.json @@ -13,6 +13,8 @@ }, "dependencies": { "bits-ui": "^2.0.0", + "dompurify": "^3.4.12", + "marked": "^18.0.6", "svelte": "^5.0.0" }, "devDependencies": { diff --git a/webui/src/App.svelte b/webui/src/App.svelte index 71ef18f..bed4180 100644 --- a/webui/src/App.svelte +++ b/webui/src/App.svelte @@ -20,6 +20,7 @@ let menuOpen = $state(false); let online = $state(false); let version = $state("Gateway"); + let theme = $state("dark"); let toast; const meta = $derived(pages.find(([name]) => name === current) || pages[0]); @@ -38,7 +39,17 @@ menuOpen = false; } + function applyTheme(next) { + theme = next; + document.documentElement.dataset.theme = next; + document.documentElement.style.colorScheme = next; + document.querySelector('meta[name="theme-color"]')?.setAttribute("content", next === "dark" ? "#0d1117" : "#f6f7f9"); + localStorage.setItem("picobot-theme", next); + } + onMount(() => { + const saved = localStorage.getItem("picobot-theme"); + applyTheme(saved === "light" || saved === "dark" ? saved : (matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light")); health(); const timer = setInterval(health, 30_000); return () => clearInterval(timer); @@ -60,6 +71,9 @@

{meta[2]}

{meta[3]}

+
{#if current === "chat"} toast.show(text, error)} /> {:else if current === "tasks"} diff --git a/webui/src/lib/Markdown.svelte b/webui/src/lib/Markdown.svelte new file mode 100644 index 0000000..9a6afcf --- /dev/null +++ b/webui/src/lib/Markdown.svelte @@ -0,0 +1,20 @@ + + +
{@html html}
diff --git a/webui/src/lib/ToolCallCard.svelte b/webui/src/lib/ToolCallCard.svelte new file mode 100644 index 0000000..543cb1c --- /dev/null +++ b/webui/src/lib/ToolCallCard.svelte @@ -0,0 +1,32 @@ + + +
+ + {#if open} +
+
参数
{argumentsText}
+ {#if result} +
结果
+ {/if} +
+ {/if} +
+ diff --git a/webui/src/pages/ChatPage.svelte b/webui/src/pages/ChatPage.svelte index 8f4446f..33668ab 100644 --- a/webui/src/pages/ChatPage.svelte +++ b/webui/src/pages/ChatPage.svelte @@ -2,6 +2,8 @@ import { onMount, tick } from "svelte"; import { Tooltip } from "bits-ui"; import { clientId, formatTime } from "../lib/api.js"; + import Markdown from "../lib/Markdown.svelte"; + import ToolCallCard from "../lib/ToolCallCard.svelte"; let { notify } = $props(); let socket = $state(null); @@ -89,7 +91,7 @@ break; case "session_history": if (frame.session_id === currentId) { - messages = (frame.messages || []).filter((item) => ["user", "assistant"].includes(item.role)); + messages = frame.messages || []; scrollToBottom(); } break; @@ -123,7 +125,10 @@ break; case "assistant_response": thinking = false; - if (!frame.session_id || frame.session_id === currentId) appendMessage(frame.role || "assistant", frame.content); + if (!frame.session_id || frame.session_id === currentId) { + appendMessage(frame.role || "assistant", frame.content); + if (currentId) send({ type: "get_session_history", session_id: currentId, limit: 1000 }); + } send({ type: "list_sessions", include_archived: false }); break; case "system_notification": @@ -160,6 +165,10 @@ return "○"; } + function toolResult(callId) { + return messages.find((message) => message.role === "tool" && message.tool_call_id === callId) || null; + } + function submit() { const content = draft.trim(); if (!content || !connected) return; @@ -274,9 +283,21 @@

今天想做些什么?

消息与 CLI 客户端使用同一套会话、记忆和工具能力。

{/if} {#each messages as message (message.id)} -
-
{message.role === "user" ? "你" : "P"}
{message.content}
-
+ {#if message.role !== "tool" && (message.content || message.tool_calls?.length)} +
+
{message.role === "user" ? "你" : "P"}
+
+ {#if message.content}
{/if} + {#if message.tool_calls?.length} +
+ {#each message.tool_calls as call (call.id)} + + {/each} +
+ {/if} +
+
+ {/if} {/each} {#if thinking}
P
正在思考…
{/if} diff --git a/webui/src/styles.css b/webui/src/styles.css index a0d3181..f5a952c 100644 --- a/webui/src/styles.css +++ b/webui/src/styles.css @@ -1,53 +1,98 @@ :root { font-family: Inter, ui-sans-serif, system-ui, -apple-system, "Segoe UI", sans-serif; - color: #edf1f5; - background: #0b0d10; + color: #e8edf2; + background: #0d1117; font-synthesis: none; - --bg: #0b0d10; - --panel: #111419; - --panel-2: #171b21; - --line: #242a32; - --muted: #89919d; - --text: #edf1f5; - --accent: #c8ff52; - --danger: #ff6b6b; - --radius: 14px; + --bg: #0d1117; + --panel: #151a21; + --panel-2: #1b222c; + --sidebar: #10151c; + --header: rgb(13 17 23 / 82%); + --line: #28313d; + --muted: #8b96a5; + --text: #e8edf2; + --text-soft: #c2cad4; + --accent: #8b72ff; + --accent-hover: #9e89ff; + --accent-contrast: #ffffff; + --accent-soft: #231f3d; + --accent-border: #4d4089; + --danger: #ff7b86; + --danger-soft: #321d23; + --warning: #f1c75b; + --warning-soft: #302a18; + --success-soft: #1d3029; + --overlay: #1d2530; + --code-bg: #0b0f14; + --user-bubble: #26213f; + --shadow: 0 16px 45px rgb(0 0 0 / 35%); + --radius: 13px; +} + +:root[data-theme="light"] { + color: #1c2430; + background: #f6f7f9; + --bg: #f6f7f9; + --panel: #ffffff; + --panel-2: #f1f3f6; + --sidebar: #fbfbfc; + --header: rgb(246 247 249 / 86%); + --line: #dde2e8; + --muted: #687386; + --text: #1c2430; + --text-soft: #4f5b6b; + --accent: #6748e8; + --accent-hover: #5739d4; + --accent-contrast: #ffffff; + --accent-soft: #eeeaff; + --accent-border: #c9befd; + --danger: #d94354; + --danger-soft: #fff0f2; + --warning: #9b6b00; + --warning-soft: #fff7df; + --success-soft: #e8f7ef; + --overlay: #ffffff; + --code-bg: #f4f5f7; + --user-bubble: #eeeaff; + --shadow: 0 16px 45px rgb(31 41 55 / 12%); } * { box-sizing: border-box; } -body { margin: 0; min-width: 320px; background: radial-gradient(circle at 85% -20%, #273219 0, transparent 32%), var(--bg); } +body { margin: 0; min-width: 320px; color: var(--text); background: var(--bg); transition: color .2s, background .2s; } 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: #0d1014; display: flex; flex-direction: column; padding: 20px 14px; } +.sidebar { border-right: 1px solid var(--line); background: 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: #10130b; font-weight: 900; border-radius: 10px; } +.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: 35px; height: 35px; } .brand strong, .brand small { display: block; } .brand small { color: var(--muted); font-size: 11px; margin-top: 2px; } .sidebar nav { display: grid; gap: 5px; } .sidebar nav button, .settings-nav button { border: 0; background: transparent; color: var(--muted); text-align: left; padding: 11px 13px; border-radius: 9px; cursor: pointer; } .sidebar nav button span { display: inline-block; width: 25px; } -.sidebar nav button:hover, .sidebar nav button.active, .settings-nav button:hover, .settings-nav button[data-state="active"] { background: #1b201d; color: var(--text); } +.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); } .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%; background: #d8a444; box-shadow: 0 0 12px currentColor; } -.gateway-status i.online { background: var(--accent); } +.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: #48b985; background: currentColor; } .gateway-status b, .gateway-status small { display: block; font-size: 12px; } .gateway-status small { color: var(--muted); margin-top: 2px; } main { min-width: 0; height: 100vh; display: flex; flex-direction: column; } -.topbar { height: 76px; flex: 0 0 76px; border-bottom: 1px solid var(--line); display: flex; align-items: center; padding: 0 26px; background: rgb(11 13 16 / 76%); backdrop-filter: blur(16px); } +.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: 18px; margin: 0; } .topbar p { font-size: 12px; 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: 12px; } +.theme-toggle:hover { color: var(--text); border-color: var(--accent-border); } .page { min-height: 0; flex: 1; } .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: rgb(14 17 21 / 66%); overflow: auto; } +.sessions-panel { border-right: 1px solid var(--line); padding: 16px; background: color-mix(in srgb, var(--sidebar) 72%, transparent); overflow: auto; } .primary, .secondary { border-radius: 9px; padding: 9px 14px; font-weight: 650; cursor: pointer; } -.primary { border: 1px solid var(--accent); background: var(--accent); color: #11160a; } -.primary:hover { background: #ddff91; } +.primary { border: 1px solid var(--accent); background: var(--accent); color: var(--accent-contrast); } +.primary:hover { border-color: var(--accent-hover); background: var(--accent-hover); } .secondary { border: 1px solid var(--line); background: var(--panel-2); color: var(--text); } .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); } @@ -66,11 +111,11 @@ main { min-width: 0; height: 100vh; display: flex; flex-direction: column; } .chat-heading strong { font-size: 13px; } .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: 11px; cursor: pointer; } -.todo-toggle.active { border-color: #657d32; color: var(--accent); } +.todo-toggle.active { border-color: var(--accent-border); color: var(--accent); background: var(--accent-soft); } .chat-heading small { font-size: 10px; color: var(--muted); margin-top: 3px; } .icon-button { border: 0; background: none; color: var(--muted); font-size: 20px; cursor: pointer; border-radius: 6px; } -.tooltip { z-index: 50; border: 1px solid #3c4652; background: #222831; color: var(--text); padding: 6px 9px; border-radius: 7px; font-size: 11px; box-shadow: 0 8px 24px #0008; } -.tooltip-arrow { fill: #222831; } +.tooltip { z-index: 50; border: 1px solid var(--line); background: var(--overlay); color: var(--text); padding: 6px 9px; border-radius: 7px; font-size: 11px; box-shadow: var(--shadow); } +.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; } @@ -79,47 +124,85 @@ main { min-width: 0; height: 100vh; display: flex; flex-direction: column; } .message { display: flex; gap: 12px; margin: 15px 0; align-items: flex-start; } .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: 11px; background: var(--panel-2); } -.message.assistant .avatar { background: var(--accent); color: #11160a; border-color: var(--accent); font-weight: bold; } -.bubble { max-width: min(78%, 720px); padding: 11px 14px; border: 1px solid var(--line); border-radius: 12px; background: var(--panel); font-size: 14px; line-height: 1.62; white-space: pre-wrap; overflow-wrap: anywhere; } -.message.user .bubble { background: #20271a; border-color: #3a4726; } +.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; } +.message.user .message-content { justify-items: end; } +.bubble { max-width: 100%; padding: 11px 14px; border: 1px solid var(--line); border-radius: 12px; background: var(--panel); font-size: 14px; line-height: 1.62; overflow-wrap: anywhere; } +.message.user .bubble { background: var(--user-bubble); border-color: var(--accent-border); } .typing .bubble { color: var(--muted); } .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); } } -.composer { position: relative; margin: 0 max(18px, calc((100% - 850px) / 2)) 18px; border: 1px solid #343d47; background: var(--panel-2); border-radius: 14px; padding: 10px 11px 6px; display: grid; grid-template-columns: 1fr 38px; box-shadow: 0 10px 35px #0006; } +.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 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 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 ui-monospace, SFMono-Regular, Consolas, monospace; } +.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 pre code { padding: 0; border: 0; background: transparent; } +.markdown-body table { display: block; max-width: 100%; border-collapse: collapse; overflow-x: auto; } +.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); } +.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: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: 13px; } +.tool-call-title small, .tool-call-title strong { display: block; } +.tool-call-title small { margin-bottom: 1px; color: var(--muted); font-size: 9px; font-weight: 500; } +.tool-call-title strong { font: 600 12px/1.4 ui-monospace, SFMono-Regular, Consolas, monospace; } +.tool-call-status { color: var(--muted); font-size: 10px; } +.tool-call-chevron { color: var(--muted); transition: transform .18s; } +.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-size: 9px; font-weight: 700; letter-spacing: .08em; 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: 11px/1.55 ui-monospace, SFMono-Regular, Consolas, monospace; 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: 1fr 38px; box-shadow: var(--shadow); } .composer textarea { resize: none; max-height: 180px; background: transparent; border: 0; outline: 0; color: var(--text); padding: 7px; line-height: 1.5; } -.send { width: 36px; height: 36px; border-radius: 10px; background: var(--accent); border: 0; font-size: 19px; cursor: pointer; } +.send { width: 36px; height: 36px; border-radius: 10px; color: var(--accent-contrast); background: var(--accent); border: 0; font-size: 19px; cursor: pointer; } .composer small { grid-column: 1 / -1; display: flex; justify-content: space-between; padding: 3px 7px; color: var(--muted); font-size: 10px; } .composer small .online { color: var(--accent); } -.todo-panel { display: none; min-width: 0; border-left: 1px solid var(--line); background: #0e1115; overflow: hidden; } +.todo-panel { display: none; min-width: 0; border-left: 1px solid var(--line); background: var(--sidebar); overflow: hidden; } .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 small, .todo-heading strong { display: block; } .todo-heading small { color: var(--muted); font-size: 10px; margin-bottom: 5px; } .todo-heading strong { font-size: 13px; line-height: 1.4; } -.todo-progress { height: 3px; background: #252b32; } +.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: 10px; } .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: #b8bec6; } +.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: #202519; color: var(--text); } -.todo-item.blocked { background: #28191c; } -.todo-item.done { color: #727a83; } +.todo-item.running { background: var(--accent-soft); color: var(--text); } +.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: 11px; } -.todo-item.running .todo-icon { color: var(--accent); border-color: #657d32; } -.todo-item.blocked .todo-icon { color: #ff8d8d; border-color: #74363b; } +.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-item strong, .todo-item small { display: block; } .todo-item strong { font-size: 12px; line-height: 1.45; } .todo-item small { color: var(--muted); font-size: 10px; margin-top: 3px; } -.todo-item p { margin: 6px 0 0; color: #aab1ba; font-size: 10px; line-height: 1.45; 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 #3b454f; border-radius: 13px; background: #14181d; box-shadow: 0 18px 50px #000b; } -.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: #14181df2; font-size: 10px; } -.command-menu-heading kbd { color: #89919a; font: inherit; } +.todo-item p { margin: 6px 0 0; color: var(--text-soft); font-size: 10px; line-height: 1.45; 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: 10px; } +.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: #252d22; } +.command-menu button:hover, .command-menu button.selected { background: var(--accent-soft); } .command-menu code { color: var(--accent); font: 600 12px/1.4 ui-monospace, SFMono-Regular, Consolas, monospace; } -.command-menu button span { color: #b8bec5; font-size: 12px; line-height: 1.4; } +.command-menu button span { color: var(--text-soft); font-size: 12px; 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; } @@ -131,12 +214,12 @@ main { min-width: 0; height: 100vh; display: flex; flex-direction: column; } .card { background: var(--panel); border: 1px solid var(--line); border-radius: var(--radius); padding: 15px 17px; } .card-row { display: flex; justify-content: space-between; gap: 18px; align-items: flex-start; } .card h3 { font-size: 14px; margin: 0 0 7px; } -.card p { color: #c4c9d0; font-size: 12px; line-height: 1.55; margin: 5px 0; white-space: pre-wrap; } +.card p { color: var(--text-soft); font-size: 12px; line-height: 1.55; margin: 5px 0; white-space: pre-wrap; } .meta { display: flex; gap: 14px; flex-wrap: wrap; color: var(--muted); font-size: 11px; } -.badge { border-radius: 99px; padding: 4px 8px; font-size: 10px; background: #222831; color: #bac1cb; white-space: nowrap; } -.badge.ok { background: #26331b; color: var(--accent); } -.badge.fail { background: #351d20; color: #ff9a9a; } -.badge.run { background: #302a18; color: #ffd66b; } +.badge { border-radius: 99px; padding: 4px 8px; font-size: 10px; background: var(--panel-2); color: var(--text-soft); white-space: nowrap; } +.badge.ok { background: var(--success-soft); color: #38a877; } +.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; } .run-row + .run-row { margin-top: 7px; } .metrics { display: grid; grid-template-columns: repeat(3, 1fr); gap: 10px; margin-bottom: 16px; } @@ -145,43 +228,44 @@ main { min-width: 0; height: 100vh; display: flex; flex-direction: column; } .metric small { color: var(--muted); } .memory-content { font-size: 13px !important; } .memory-key { color: var(--accent); font-family: ui-monospace, monospace; } -.log-view { margin: 0; background: #090b0e; border: 1px solid var(--line); border-radius: 12px; padding: 16px; color: #b9c1ca; min-height: calc(100vh - 190px); font: 11px/1.65 ui-monospace, SFMono-Regular, Consolas, monospace; white-space: pre-wrap; overflow: auto; } +.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: 11px/1.65 ui-monospace, SFMono-Regular, Consolas, monospace; white-space: pre-wrap; overflow: auto; } .log-meta { color: var(--muted); font-size: 11px; margin: -8px 0 8px; } .switch-label { color: var(--muted); font-size: 12px; 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: #11160a; } +.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); } .editor-head strong, .editor-head small { display: block; } .editor-head small { color: var(--muted); font-size: 10px; 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: #0a0c0f; color: #d5dae0; font: 12px/1.6 ui-monospace, SFMono-Regular, Consolas, monospace; tab-size: 2; } +.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: 12px/1.6 ui-monospace, SFMono-Regular, Consolas, monospace; tab-size: 2; } .notice { padding: 11px 15px; color: var(--muted); font-size: 11px; border-top: 1px solid var(--line); } code { color: var(--accent); } -#toast { position: fixed; z-index: 100; right: 22px; bottom: 22px; background: #222831; border: 1px solid #3c4652; border-radius: 10px; padding: 11px 15px; font-size: 12px; opacity: 0; transform: translateY(8px); pointer-events: none; transition: .2s; } +#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: 12px; opacity: 0; transform: translateY(8px); pointer-events: none; transition: .2s; box-shadow: var(--shadow); } #toast.show { opacity: 1; transform: none; } -#toast.error { border-color: #6b3030; color: #ffaaaa; } +#toast.error { border-color: var(--danger); color: var(--danger); } .loading, .empty-card { text-align: center; color: var(--muted); padding: 50px; } .empty-card.compact { padding: 24px 8px; } -.error-text { color: #ff9a9a !important; } +.error-text { color: var(--danger) !important; } @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: 20px 0 50px #000; } + .sidebar { position: fixed; z-index: 10; inset: 0 auto 0 0; width: 232px; transform: translateX(-100%); transition: .2s; box-shadow: var(--shadow); } .sidebar.open { transform: none; } .topbar .menu { display: block; } .chat-layout { grid-template-columns: 1fr; } .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: -18px 0 50px #000b; } + .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; } .settings-grid { grid-template-columns: 1fr; } .settings-nav { flex-direction: row; overflow: auto; } .metrics { grid-template-columns: 1fr; } .filters { align-items: stretch; flex-wrap: wrap; } .grow { max-width: none; width: 100%; flex-basis: 100%; } - .bubble { max-width: 88%; } + .message-content { max-width: 88%; } + .theme-toggle span:last-child { display: none; } }