diff --git a/emrg/gui/renderer/app.css b/emrg/gui/renderer/app.css
deleted file mode 100644
index 8d42fe6..0000000
--- a/emrg/gui/renderer/app.css
+++ /dev/null
@@ -1,316 +0,0 @@
-/* app.css — EMRG GUI 暗色主题(对齐 TUI 终端风格:近黑背景 + 绿/黄/红状态色) */
-:root {
- --bg: #1a1b26;
- --bg-panel: #16161e;
- --bg-input: #0f1017;
- --border: #2a2b3a;
- --text: #c0caf5;
- --text-dim: #565f89;
- --green: #9ece6a;
- --yellow: #e0af68;
- --red: #f7768e;
- --blue: #7aa2f7;
- --user-bubble: #283457;
- --assistant-bg: #1f2335;
-}
-
-* { box-sizing: border-box; margin: 0; padding: 0; }
-
-html, body {
- height: 100%;
- font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "PingFang SC", "Noto Sans CJK SC", sans-serif;
- font-size: 14px;
- background: var(--bg);
- color: var(--text);
- overflow: hidden;
-}
-
-#app {
- display: flex;
- height: 100vh;
-}
-
-/* ── 左侧栏 ─────────────────────────── */
-#sidebar {
- width: 240px;
- min-width: 200px;
- background: var(--bg-panel);
- border-right: 1px solid var(--border);
- display: flex;
- flex-direction: column;
-}
-
-#sidebar-header {
- padding: 12px;
- border-bottom: 1px solid var(--border);
-}
-#sidebar-header h1 {
- font-size: 16px;
- letter-spacing: 1px;
- color: var(--green);
-}
-#sidebar-header .sub { font-size: 11px; color: var(--text-dim); margin-top: 2px; }
-
-#session-list {
- flex: 1;
- overflow-y: auto;
- padding: 6px;
-}
-.session-item {
- padding: 8px 10px;
- border-radius: 6px;
- cursor: pointer;
- margin-bottom: 2px;
- display: flex;
- justify-content: space-between;
- gap: 6px;
- font-size: 13px;
-}
-.session-item:hover { background: var(--bg-input); }
-.session-item.active { background: var(--user-bubble); color: #fff; }
-.session-item.placeholder { color: var(--text-dim); cursor: default; }
-.sess-title {
- overflow: hidden;
- text-overflow: ellipsis;
- white-space: nowrap;
-}
-.sess-count { color: var(--text-dim); font-size: 11px; flex-shrink: 0; }
-.session-item.active .sess-count { color: #a9b1d6; }
-
-#sidebar-footer {
- padding: 10px 12px;
- border-top: 1px solid var(--border);
- display: flex;
- flex-direction: column;
- gap: 6px;
- font-size: 12px;
-}
-#sidebar-footer .row { display: flex; align-items: center; gap: 6px; }
-.dot { width: 9px; height: 9px; border-radius: 50%; display: inline-block; }
-.dot.green { background: var(--green); }
-.dot.red { background: var(--red); }
-.dot.gray { background: var(--text-dim); }
-#server-id { color: var(--text-dim); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
-#project-name { color: var(--text-dim); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
-
-.sidebar-btn {
- background: none;
- border: 1px solid var(--border);
- color: var(--text);
- padding: 6px 10px;
- border-radius: 6px;
- cursor: pointer;
- font-size: 12px;
- text-align: left;
-}
-.sidebar-btn:hover { border-color: var(--blue); }
-
-/* ── 主区域 ─────────────────────────── */
-#main {
- flex: 1;
- display: flex;
- flex-direction: column;
- min-width: 0;
-}
-
-#topbar {
- height: 44px;
- border-bottom: 1px solid var(--border);
- display: flex;
- align-items: center;
- justify-content: space-between;
- padding: 0 16px;
- font-size: 13px;
- color: var(--text-dim);
-}
-#topbar .model { color: var(--yellow); }
-
-#chat-view {
- flex: 1;
- overflow-y: auto;
- padding: 16px;
- display: flex;
- flex-direction: column;
- gap: 10px;
- background: var(--bg);
-}
-
-.msg {
- max-width: 85%;
- padding: 10px 14px;
- border-radius: 10px;
- line-height: 1.55;
- word-break: break-word;
- white-space: pre-wrap;
-}
-.msg.user {
- align-self: flex-end;
- background: var(--user-bubble);
- color: #e8ecf8;
-}
-.msg.assistant {
- align-self: flex-start;
- background: var(--assistant-bg);
- border: 1px solid var(--border);
-}
-.msg.assistant.remote { border-color: var(--yellow); }
-.msg.system {
- align-self: center;
- background: none;
- color: var(--text-dim);
- font-size: 12px;
- padding: 4px 10px;
- max-width: 95%;
-}
-.remote-label { font-size: 11px; color: var(--yellow); margin-bottom: 4px; }
-
-/* markdown 渲染 */
-.msg-body { color: var(--text); }
-.msg-body p { margin: 0 0 8px; }
-.msg-body p:last-child { margin-bottom: 0; }
-.msg-body pre {
- background: var(--bg-input);
- border: 1px solid var(--border);
- border-radius: 8px;
- padding: 10px;
- overflow-x: auto;
- font-size: 12.5px;
- margin: 8px 0;
-}
-.msg-body code {
- font-family: "SF Mono", "Fira Code", Menlo, Consolas, monospace;
- background: var(--bg-input);
- padding: 1px 5px;
- border-radius: 4px;
- font-size: 12.5px;
-}
-.msg-body pre code { padding: 0; background: none; }
-.msg-body ul, .msg-body ol { padding-left: 20px; margin: 6px 0; }
-.msg-body blockquote { border-left: 3px solid var(--blue); padding-left: 10px; color: var(--text-dim); margin: 6px 0; }
-.msg-body h1, .msg-body h2, .msg-body h3, .msg-body h4 { margin: 10px 0 6px; color: #e0e5f8; }
-.msg-body a { color: var(--blue); }
-
-/* 工具卡片 */
-.tool-card {
- align-self: flex-start;
- min-width: 320px;
- max-width: 85%;
- background: var(--bg-panel);
- border: 1px solid var(--border);
- border-left: 3px solid var(--yellow);
- border-radius: 8px;
- padding: 8px 12px;
- font-size: 13px;
- margin: 2px 0;
-}
-.tool-card.running { border-left-color: var(--yellow); }
-.tool-card.done { border-left-color: var(--green); }
-.tool-card.failed { border-left-color: var(--red); }
-.tool-name { font-family: "SF Mono", Menlo, monospace; font-weight: 600; }
-.tool-status { color: var(--text-dim); margin-left: 8px; font-size: 12px; }
-.tool-output {
- margin-top: 6px;
- background: var(--bg-input);
- border-radius: 6px;
- padding: 6px 10px;
- font-family: "SF Mono", Menlo, monospace;
- font-size: 12px;
- max-height: 180px;
- overflow-y: auto;
- white-space: pre-wrap;
- word-break: break-all;
-}
-.tool-expand {
- margin-top: 4px;
- background: none;
- border: none;
- color: var(--blue);
- cursor: pointer;
- font-size: 12px;
- padding: 0;
-}
-
-/* ── 输入区 ─────────────────────────── */
-#composer {
- border-top: 1px solid var(--border);
- padding: 10px 16px;
- display: flex;
- gap: 8px;
- align-items: flex-end;
- background: var(--bg-panel);
-}
-#input {
- flex: 1;
- resize: none;
- background: var(--bg-input);
- border: 1px solid var(--border);
- border-radius: 8px;
- color: var(--text);
- padding: 10px 12px;
- font-size: 14px;
- font-family: inherit;
- line-height: 1.5;
- max-height: 150px;
- outline: none;
-}
-#input:focus { border-color: var(--blue); }
-#input:disabled { opacity: 0.6; }
-
-#send-btn, #stop-btn {
- padding: 10px 18px;
- border-radius: 8px;
- border: none;
- cursor: pointer;
- font-size: 14px;
- font-weight: 600;
-}
-#send-btn { background: var(--blue); color: #fff; }
-#send-btn:hover { filter: brightness(1.1); }
-#send-btn:disabled { opacity: 0.5; cursor: default; }
-#stop-btn { display: none; background: var(--red); color: #fff; }
-
-/* ── 对话框(首启引导 / 设置)────────── */
-dialog {
- background: var(--bg-panel);
- color: var(--text);
- border: 1px solid var(--border);
- border-radius: 12px;
- padding: 24px;
- min-width: 420px;
- max-width: 90vw;
-}
-dialog::backdrop { background: rgba(0, 0, 0, 0.6); }
-dialog h2 { font-size: 16px; margin-bottom: 6px; color: var(--green); }
-dialog .desc { font-size: 12px; color: var(--text-dim); margin-bottom: 14px; }
-.form-row { margin-bottom: 12px; }
-.form-row label { display: block; font-size: 12px; color: var(--text-dim); margin-bottom: 4px; }
-.form-row input, .form-row select {
- width: 100%;
- background: var(--bg-input);
- border: 1px solid var(--border);
- border-radius: 6px;
- color: var(--text);
- padding: 8px 10px;
- font-size: 13px;
- outline: none;
-}
-.form-row input:focus { border-color: var(--blue); }
-.dialog-actions { display: flex; justify-content: flex-end; gap: 8px; margin-top: 16px; }
-.dialog-actions button {
- padding: 8px 16px;
- border-radius: 6px;
- border: 1px solid var(--border);
- background: none;
- color: var(--text);
- cursor: pointer;
- font-size: 13px;
-}
-.dialog-actions button.primary { background: var(--blue); border-color: var(--blue); color: #fff; font-weight: 600; }
-.dialog-actions button:hover { filter: brightness(1.1); }
-.dir-row { display: flex; gap: 6px; }
-.dir-row input { flex: 1; }
-.dir-row button { flex-shrink: 0; }
-
-::-webkit-scrollbar { width: 8px; height: 8px; }
-::-webkit-scrollbar-thumb { background: var(--border); border-radius: 4px; }
-::-webkit-scrollbar-track { background: transparent; }
diff --git a/emrg/gui/renderer/app.js b/emrg/gui/renderer/app.js
deleted file mode 100644
index 6343b31..0000000
--- a/emrg/gui/renderer/app.js
+++ /dev/null
@@ -1,647 +0,0 @@
-"use strict";
-/**
- * app.js — renderer UI 逻辑:聊天渲染、会话列表、工具状态、事件绑定。
- * 事件已由 main 分类(onEvent 收 {type, data}),renderer 不重复做帧分类(G67)。
- */
-
-const $ = (id) => document.getElementById(id);
-
-const state = {
- sessionId: null,
- sessions: [],
- busy: false,
- ownStreamRequestId: null,
- apiKeyConfigured: false,
- configExists: false,
- projectDir: "",
- model: "",
- serverId: "",
- autoScroll: true,
- groupNodes: new Map(), // requestId → DOM 节点
- toolCards: new Map(), // tool_call_id → DOM 节点
-};
-
-// ── 启动 ─────────────────────────────────────────────────
-
-async function boot() {
- try {
- const init = await window.emrg.init();
- state.configExists = init.config_exists;
- state.apiKeyConfigured = init.api_key_configured;
- state.projectDir = init.project_dir || "";
- state.serverId = init.server_id || "";
- state.model = init.model || "";
- updateStatusBar(init);
- updateEvolutionCount(init.evolution_count);
-
- if (!init.config_exists) {
- showWelcome(); // G71:config 缺失 → 首启引导(不拉起 daemon)
- return;
- }
- if (!init.api_key_configured) {
- showSettings(); // G36:config 存在但 key 空/占位符
- return;
- }
- if (!init.project_dir_valid) {
- addSystemMessage("⚠️ 项目目录无效或不可写,请到设置中修正。");
- showSettings();
- return;
- }
- if (init.sessions && init.sessions.length > 0) {
- renderSessions(init.sessions);
- // 保持当前会话(boot 会因 saveSettings/saveWelcome 重跑——保存设置后不应弹回最新创建会话)
- const current = init.sessions.find((s) => s.session_id === state.sessionId);
- if (current) {
- highlightActiveSession(state.sessionId);
- } else {
- await switchSession(init.sessions[0].session_id, { silent: true });
- }
- } else {
- await newSession();
- }
- } catch (e) {
- addSystemMessage(`启动失败: ${e.message}`);
- }
-}
-
-// ── UI 绑定 ──────────────────────────────────────────────
-
-function bindUi() {
- $("send-btn").addEventListener("click", sendMessage);
- $("stop-btn").addEventListener("click", () => window.emrg.cancel().catch(() => {})); // G141:断连瞬间 IPC 可能 reject——防 unhandled rejection
- $("new-session-btn").addEventListener("click", newSession);
- $("settings-btn").addEventListener("click", showSettings);
- $("settings-cancel").addEventListener("click", () => $("settings-dialog").close());
- $("settings-save").addEventListener("click", saveSettings);
- $("pick-dir-btn").addEventListener("click", async () => {
- const dir = await window.emrg.pickProjectDir();
- if (dir) $("set-project-dir").value = dir;
- });
- $("welcome-pick-btn").addEventListener("click", async () => {
- const dir = await window.emrg.pickProjectDir();
- if (dir) $("welcome-project-dir").value = dir;
- });
- $("welcome-save").addEventListener("click", saveWelcome);
-
- const input = $("input");
- input.addEventListener("keydown", (e) => {
- if (e.key === "Enter" && !e.shiftKey) {
- e.preventDefault();
- sendMessage();
- }
- });
- input.addEventListener("input", () => {
- input.style.height = "auto";
- input.style.height = Math.min(input.scrollHeight, 150) + "px";
- });
-
- const chatView = $("chat-view");
- chatView.addEventListener("scroll", () => {
- state.autoScroll = chatView.scrollTop + chatView.clientHeight >= chatView.scrollHeight - 40;
- });
-}
-
-// ── 发送 ─────────────────────────────────────────────────
-
-async function sendMessage() {
- const input = $("input");
- const text = input.value.trim();
- if (!text || state.busy) return;
- if (!state.sessionId) {
- addSystemMessage("请先创建/选择会话。");
- return;
- }
- state.busy = true;
- setComposerDisabled(true);
- addUserMessage(text);
- input.value = "";
- input.style.height = "auto";
- // G143:send 前预生成 requestId 并标记自有流——消除 IPC 往返竞态窗口
- // (之前 await sendMessage 返回后才赋值,delta/tool_start 可能先到 → 自有流被误标「来自其他客户端」)
- const requestId = genRequestId();
- state.ownStreamRequestId = requestId;
- try {
- const res = await window.emrg.sendMessage({ sessionId: state.sessionId, text, requestId });
- state.ownStreamRequestId = res.requestId || requestId; // G124:以 daemon 实际回显为准
- } catch (e) {
- state.busy = false;
- state.ownStreamRequestId = null;
- setComposerDisabled(false);
- addSystemMessage(`发送失败: ${e.message}(输入已恢复)`);
- input.value = text; // G49:失败恢复输入框
- }
-}
-
-// ── 会话 ─────────────────────────────────────────────────
-
-async function switchSession(sid, opts = {}) {
- // G65:busy 即自有流进行中/发送中(IPC 往返窗口内 ownStreamRequestId 尚未赋值,
- // 只查 busy 才能覆盖该窗口——与 main 侧 ownStream 锁语义对齐)
- if (state.busy) {
- addSystemMessage("当前有进行中的响应,请等待完成或停止后再切换。"); // G65
- return;
- }
- try {
- const res = await window.emrg.switchSession({ sessionId: sid });
- state.sessionId = sid;
- state.ownStreamRequestId = null; // G110:切会话清自有流标记
- clearChat();
- if (res.error === "session_not_found") {
- addSystemMessage("会话已被删除,已切换到最近会话。");
- if (res.next_session) {
- renderSessions(res.sessions || []);
- await switchSession(res.next_session, { silent: true });
- } else {
- await newSession();
- }
- return;
- }
- // G13:历史不通过 resume 返回——v1 不加载历史(G12)
- addSystemMessage(`已切换到会话 ${sid}`);
- highlightActiveSession(sid);
- } catch (e) {
- addSystemMessage(`切换失败: ${e.message}`);
- }
-}
-
-async function newSession() {
- if (state.busy) {
- addSystemMessage("当前有进行中的响应,请等待完成或停止后再新建会话。"); // G65 同款锁
- return;
- }
- try {
- const res = await window.emrg.newSession();
- const sid = res.session_id;
- // 本地切订阅(resume 不存在的新会话会被 daemon 自动创建)
- state.sessionId = sid;
- clearChat();
- addSystemMessage(`新会话 ${sid}`);
- await refreshSessions();
- highlightActiveSession(sid);
- } catch (e) {
- addSystemMessage(`新建失败: ${e.message}`);
- }
-}
-
-async function deleteSession(sid) {
- // G65:自有流运行中禁止删除当前会话(删除后自动切换会被 busy 锁拒绝 → sessionId 指向已删会话)
- if (state.busy && state.sessionId === sid) {
- addSystemMessage("当前有进行中的响应,请等待完成或停止后再删除会话。");
- return;
- }
- if (!confirm(`确定删除会话 ${sid}?`)) return; // G76
- try {
- await window.emrg.deleteSession({ sessionId: sid });
- if (state.sessionId === sid) {
- const remaining = state.sessions.filter((s) => s.session_id !== sid);
- if (remaining.length > 0) {
- await switchSession(remaining[0].session_id, { silent: true });
- } else {
- await newSession();
- }
- }
- await refreshSessions();
- } catch (e) {
- addSystemMessage(`删除失败: ${e.message}`);
- }
-}
-
-async function refreshSessions() {
- try {
- const sessions = await window.emrg.listSessions();
- renderSessions(sessions);
- } catch { /* 忽略 */ }
-}
-
-// ── 设置 ─────────────────────────────────────────────────
-
-async function showSettings() {
- try {
- const s = await window.emrg.getSettings();
- $("set-api-key").value = s.apiKey || "";
- $("set-base-url").value = s.baseUrl || "";
- $("set-project-dir").value = s.projectDir || "";
- const sel = $("set-model");
- sel.innerHTML = "";
- for (const m of s.models || []) {
- const opt = document.createElement("option");
- opt.value = m;
- opt.textContent = m;
- if (m === s.model) opt.selected = true;
- sel.appendChild(opt);
- }
- if (!s.models?.length && s.model) {
- const opt = document.createElement("option");
- opt.value = s.model;
- opt.textContent = s.model;
- opt.selected = true;
- sel.appendChild(opt);
- }
- if (sel.selectedIndex < 0) sel.selectedIndex = 0; // G144:默认选中第一项,防空 model 保存
- } catch (e) {
- addSystemMessage(`读取设置失败: ${e.message}`);
- }
- $("settings-dialog").showModal();
-}
-
-async function saveSettings() {
- const config = {
- apiKey: $("set-api-key").value.trim(),
- baseUrl: $("set-base-url").value.trim(),
- model: $("set-model").value,
- projectDir: $("set-project-dir").value.trim(),
- };
- if (!config.apiKey) {
- alert("API Key 必填"); // G76
- return;
- }
- try {
- const res = await window.emrg.saveSettings(config);
- if ($("settings-dialog").open) $("settings-dialog").close(); // form method=dialog 可能已关
- addSystemMessage("设置已保存。");
- state.apiKeyConfigured = true;
- state.projectDir = config.projectDir || state.projectDir;
- await boot();
- } catch (e) {
- addSystemMessage(`保存失败: ${e.message}`);
- }
-}
-
-// ── 首启引导(G36/G71/G82/G116/G117)──────────────────
-
-function showWelcome() {
- $("welcome-project-dir").value = ""; // 跳过 → 默认 ~/.emrg/evolution(G82)
- $("welcome-api-key").value = "";
- $("welcome-base-url").value = "";
- $("welcome-model").innerHTML = "";
- $("welcome-dialog").showModal();
- // 尝试预加载模型列表(config 缺失时 getSettings 返回默认值 G52)
- window.emrg.getSettings().then((s) => {
- const sel = $("welcome-model");
- sel.innerHTML = "";
- const models = s.models?.length ? s.models : ["deepseek-chat", "deepseek-v3", "gpt-4o"];
- for (const m of models) {
- const opt = document.createElement("option");
- opt.value = m;
- opt.textContent = m;
- if (m === s.model) opt.selected = true; // G144:config 存在时回填当前模型
- sel.appendChild(opt);
- }
- if (sel.selectedIndex < 0) sel.selectedIndex = 0; // G144:默认选中第一项,防空 model 保存
- });
-}
-
-async function saveWelcome() {
- const key = $("welcome-api-key").value.trim();
- if (!key) {
- alert("API Key 必填");
- return;
- }
- const config = {
- apiKey: key,
- baseUrl: $("welcome-base-url").value.trim(),
- model: $("welcome-model").value || "deepseek-chat", // G144:空 model 兜底默认模型
- projectDir: $("welcome-project-dir").value.trim() || "",
- };
- try {
- await window.emrg.saveSettings(config);
- if ($("welcome-dialog").open) $("welcome-dialog").close();
- addSystemMessage("初始设置完成,正在启动 daemon…");
- state.apiKeyConfigured = true;
- state.projectDir = config.projectDir || state.projectDir;
- await boot();
- } catch (e) {
- addSystemMessage(`初始化失败: ${e.message}`);
- }
-}
-
-// ── 渲染 ─────────────────────────────────────────────────
-
-function addUserMessage(text) {
- const node = document.createElement("div");
- node.className = "msg user";
- node.textContent = text;
- appendMsg(node);
- return node;
-}
-
-function addAssistantMessage(text) {
- const node = document.createElement("div");
- node.className = "msg assistant";
- node.textContent = text || "";
- appendMsg(node);
- return node;
-}
-
-function addSystemMessage(text) {
- const node = document.createElement("div");
- node.className = "msg system";
- node.textContent = text;
- appendMsg(node);
-}
-
-function appendMsg(node) {
- $("chat-view").appendChild(node);
- scrollToBottom();
-}
-
-function clearChat() {
- $("chat-view").innerHTML = "";
- state.groupNodes.clear();
- state.toolCards.clear();
-}
-
-function scrollToBottom() {
- if (state.autoScroll) {
- const cv = $("chat-view");
- cv.scrollTop = cv.scrollHeight;
- }
-}
-
-function renderSessions(sessions) {
- state.sessions = sessions || [];
- const list = $("session-list");
- list.innerHTML = "";
- if (!state.sessions.length) {
- list.innerHTML = "
暂无会话
";
- return;
- }
- for (const s of state.sessions) {
- const item = document.createElement("div");
- item.className = "session-item";
- const title = s.title || s.session_id; // G27:title 优先 session_id 兜底
- const count = s.message_count || 0;
- item.innerHTML = `${escapeHtml(title)}${count} msgs`;
- item.addEventListener("click", () => switchSession(s.session_id));
- item.addEventListener("contextmenu", (e) => {
- e.preventDefault();
- deleteSession(s.session_id);
- });
- list.appendChild(item);
- }
- highlightActiveSession(state.sessionId);
-}
-
-function highlightActiveSession(sid) {
- for (const item of $("session-list").children) {
- item.classList.toggle("active", item.textContent.includes(sid) && item.classList.contains("session-item"));
- }
-}
-
-function updateStatusBar(init) {
- $("daemon-dot").className = "dot " + (init.config_exists && init.api_key_configured ? "green" : "gray");
- $("server-id").textContent = init.server_id || "—";
- $("model-name").textContent = init.model || "—";
- $("project-name").textContent = init.project_dir ? `📁 ${init.project_dir}` : "";
-}
-
-function setComposerDisabled(disabled) {
- $("input").disabled = disabled;
- $("send-btn").disabled = disabled;
- $("stop-btn").style.display = disabled ? "inline-block" : "none";
-}
-
-function escapeHtml(s) {
- return String(s).replace(/&/g, "&").replace(//g, ">");
-}
-
-// G143:生成 UUID(secure context 下 crypto.randomUUID;低版本兜底)
-function genRequestId() {
- if (window.crypto && typeof window.crypto.randomUUID === "function") {
- return window.crypto.randomUUID();
- }
- return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, (c) => {
- const r = (Math.random() * 16) | 0;
- return (c === "x" ? r : (r & 0x3) | 0x8).toString(16);
- });
-}
-
-// ── 事件处理(main 已分类)───────────────────────────────
-
-async function handleEvent(evt) {
- const { type, data } = evt;
- switch (type) {
- case "message_delta":
- // G122:main 已批量(chunks 数组)
- handleDelta(data.chunks || [data]);
- break;
- case "done":
- handleDone(data);
- break;
- case "tool_started":
- handleToolStart(data);
- break;
- case "tool_finished":
- handleToolEnd(data);
- break;
- case "cancelled":
- state.busy = false;
- state.ownStreamRequestId = null;
- setComposerDisabled(false);
- break;
- case "error":
- handleError(data);
- break;
- case "pong":
- state.serverId = data.identity?.instance_id || state.serverId;
- state.model = data.model || state.model;
- $("server-id").textContent = state.serverId || "—";
- $("model-name").textContent = state.model || "—";
- updateEvolutionCount(data.evolution_count);
- break;
- case "status":
- handleStatus(data);
- break;
- case "sessions":
- renderSessions(data.sessions || []);
- break;
- case "disconnected":
- $("daemon-dot").className = "dot red";
- addSystemMessage("⏸ 与 daemon 的连接已断开,重连中…");
- // G89:断连时恢复输入条(流式中断连 → busy 永久卡死;不能依赖 30s 超时兜底)
- state.busy = false;
- state.ownStreamRequestId = null;
- setComposerDisabled(false);
- // G97:广播分组缓存清理(DOM 保留——历史消息应保留;仅清 Map 引用,防重连后"幽灵"分组缓存)
- // daemon_client 侧 clearGroups() 已 emit group_cleared,此处兜底直接清空
- state.groupNodes.clear();
- // G97:工具卡片 tool_start 已建 / tool_end 未到 → 标「结果未知——连接中断」
- // (无重试语义——工具副作用不可重放;重连后若收到真实 tool_end 会被 handleToolEnd 纠正)
- for (const card of state.toolCards.values()) {
- if (card.classList.contains("running")) {
- card.classList.remove("running");
- card.classList.add("failed");
- const st = card.querySelector(".tool-status");
- if (st) st.textContent = "结果未知——连接中断";
- }
- }
- break;
- case "group_cleared":
- // DOM 保留,仅清缓存
- state.groupNodes.delete(data.requestId);
- break;
- case "list_result":
- if (data.type === "sessions_list") renderSessions(data.sessions || []);
- break;
- case "command_result":
- if (data.type === "model_set") {
- state.model = data.model || state.model;
- $("model-name").textContent = state.model;
- }
- break;
- default:
- // 忽略未知
- break;
- }
-}
-
-function handleDelta(chunks) {
- // G3:广播分组——新 request_id 建新节点并标注「来自其他客户端」
- for (const chunk of chunks) {
- const rid = chunk.request_id;
- if (!rid) continue;
- let node = state.groupNodes.get(rid);
- if (!node) {
- const isOwn = state.ownStreamRequestId === rid;
- node = document.createElement("div");
- node.className = "msg assistant";
- if (!isOwn) node.classList.add("remote");
- const label = document.createElement("div");
- label.className = "remote-label";
- label.textContent = isOwn ? "" : "(来自其他客户端)";
- if (!isOwn) node.appendChild(label);
- const body = document.createElement("div");
- body.className = "msg-body";
- node.appendChild(body);
- appendMsg(node);
- state.groupNodes.set(rid, node);
- }
- const body = node.querySelector(".msg-body") || node;
- body.textContent += chunk.content || "";
- scrollToBottom();
- }
-}
-
-function handleDone(data) {
- const rid = data.request_id;
- // 找到分组节点 → 整体 marked 渲染(G127:requestIdleCallback 调度)
- const node = state.groupNodes.get(rid);
- if (node) {
- const body = node.querySelector(".msg-body") || node;
- const text = body.textContent;
- const render = () => {
- window.emrgMarkdown.renderMarkdown(text).then((html) => {
- body.innerHTML = html;
- scrollToBottom();
- });
- };
- if (window.requestIdleCallback) {
- window.requestIdleCallback(render, { timeout: 2000 });
- } else {
- render(); // rIC 不可用直接同步(G91 200KB 上限兜底)
- }
- state.groupNodes.delete(rid);
- }
- if (data.timeout) {
- addSystemMessage("⚠️ 响应超时(daemon 未返回完成帧)。");
- }
- if (state.ownStreamRequestId === rid || data.timeout) {
- state.busy = false;
- state.ownStreamRequestId = null;
- setComposerDisabled(false);
- }
-}
-
-function handleToolStart(data) {
- const rid = data.request_id;
- if (rid && !state.groupNodes.has(rid)) {
- // G104:tool_start 也建组(LLM 先出 tool_calls 后出文本)——与 handleDelta 一致,广播流标 remote
- const isOwn = state.ownStreamRequestId === rid;
- const node = document.createElement("div");
- node.className = "msg assistant";
- if (!isOwn) node.classList.add("remote");
- const label = document.createElement("div");
- label.className = "remote-label";
- label.textContent = isOwn ? "" : "(来自其他客户端)";
- if (!isOwn) node.appendChild(label);
- const body = document.createElement("div");
- body.className = "msg-body";
- node.appendChild(body);
- appendMsg(node);
- state.groupNodes.set(rid, node);
- }
- const card = document.createElement("div");
- card.className = "tool-card running";
- card.innerHTML = `🔧 ${escapeHtml(data.tool_name)} 运行中…`;
- appendMsg(card);
- state.toolCards.set(data.tool_call_id, card);
-}
-
-function handleToolEnd(data) {
- const card = state.toolCards.get(data.tool_call_id);
- if (!card) return;
- const ok = !data.error;
- card.className = "tool-card " + (ok ? "done" : "failed");
- const elapsed = data.elapsed !== undefined ? `${data.elapsed.toFixed(1)}s` : "";
- card.querySelector(".tool-status").textContent = ok ? `完成 ${elapsed}` : `失败 ${elapsed}`;
- // G91/G131:content 默认截断 2000 字符 + 展开(textContent 纯文本,不做 marked)
- const content = data.content || "";
- if (content) {
- const truncated = content.length > 2000 ? content.slice(0, 2000) + "…" : content;
- const body = document.createElement("div");
- body.className = "tool-output";
- body.textContent = truncated;
- card.appendChild(body);
- if (content.length > 2000) {
- const btn = document.createElement("button");
- btn.className = "tool-expand";
- btn.textContent = "展开全文";
- btn.addEventListener("click", () => {
- body.textContent = content;
- btn.remove();
- });
- card.appendChild(btn);
- }
- }
- scrollToBottom();
-}
-
-function handleError(data) {
- // G42:session busy(带 session_id G128)/ 普通错误
- if (data.error && String(data.error).includes("session busy")) {
- addSystemMessage(`⚠️ 会话正忙(${data.session_id || state.sessionId})——请等待当前响应完成。`);
- state.busy = false;
- state.ownStreamRequestId = null;
- setComposerDisabled(false);
- } else {
- addSystemMessage(`错误: ${data.error || "未知错误"}`);
- }
-}
-
-function handleStatus(data) {
- if (data.connected) {
- $("daemon-dot").className = "dot green";
- if (data.server_id) $("server-id").textContent = data.server_id;
- if (data.model) $("model-name").textContent = data.model;
- updateEvolutionCount(data.evolution_count);
- addSystemMessage("✓ 已重新连接");
- } else if (data.auth_failed) {
- $("daemon-dot").className = "dot red";
- addSystemMessage("⚠️ 认证失败——请检查 ~/.emrg 配置,手动重试。");
- } else {
- $("daemon-dot").className = "dot red";
- }
-}
-
-// G19:状态栏演化计数(pong evolution_count / init / status)
-function updateEvolutionCount(count) {
- const el = $("evolution-count");
- if (!el) return;
- el.textContent = count !== undefined && count !== null ? `演化 ${count} 次` : "";
-}
-
-// ── 启动 ─────────────────────────────────────────────────
-
-bindUi(); // 模块级只绑定一次(boot 可被 saveSettings/saveWelcome 重复调用,防重复 listener)
-window.emrg.onEvent(handleEvent); // 模块级只注册一次(防 ipcRenderer listener 泄漏 → delta 重复渲染)
-boot();
diff --git a/emrg/gui/renderer/css/animations.css b/emrg/gui/renderer/css/animations.css
new file mode 100644
index 0000000..07e9fd8
--- /dev/null
+++ b/emrg/gui/renderer/css/animations.css
@@ -0,0 +1,91 @@
+/* animations.css — 动效:快速 150ms / 中 250ms / 慢 350ms,克制、无弹跳无循环
+ * 原则:只动 transform/opacity(性能友好);消息上浮+淡入;光标脉动;对话框淡入缩放。
+ */
+
+/* 消息出现:轻微上浮 + 淡入 */
+@keyframes msg-in {
+ from {
+ opacity: 0;
+ transform: translateY(6px);
+ }
+ to {
+ opacity: 1;
+ transform: translateY(0);
+ }
+}
+
+/* 流式末尾柔和光标脉动 */
+@keyframes cursor-pulse {
+ 0%, 100% {
+ opacity: 1;
+ }
+ 50% {
+ opacity: 0.25;
+ }
+}
+
+/* 工具行旋转指示 */
+@keyframes spin {
+ to {
+ transform: rotate(360deg);
+ }
+}
+
+/* 对话框进入 */
+@keyframes dialog-in {
+ from {
+ opacity: 0;
+ transform: scale(0.96) translateY(8px);
+ }
+ to {
+ opacity: 1;
+ transform: scale(1) translateY(0);
+ }
+}
+
+/* 遮罩淡入 */
+@keyframes fade-in {
+ from {
+ opacity: 0;
+ }
+ to {
+ opacity: 1;
+ }
+}
+
+/* 横幅滑入 */
+@keyframes banner-in {
+ from {
+ opacity: 0;
+ transform: translateX(-50%) translateY(-8px);
+ }
+ to {
+ opacity: 1;
+ transform: translateX(-50%) translateY(0);
+ }
+}
+
+/* 下拉菜单进入 */
+@keyframes menu-in {
+ from {
+ opacity: 0;
+ transform: translateY(4px);
+ }
+ to {
+ opacity: 1;
+ transform: translateY(0);
+ }
+}
+
+/* 侧边栏 ✦ 温和闪烁(演化发生时) */
+@keyframes star-pulse {
+ 0%, 100% {
+ opacity: 1;
+ }
+ 50% {
+ opacity: 0.4;
+ }
+}
+.brand-star.pulse {
+ animation: star-pulse 1.2s var(--ease) 2;
+}
diff --git a/emrg/gui/renderer/css/base.css b/emrg/gui/renderer/css/base.css
new file mode 100644
index 0000000..d9c8158
--- /dev/null
+++ b/emrg/gui/renderer/css/base.css
@@ -0,0 +1,108 @@
+/* base.css — reset / 字体 / 滚动条 / 通用基础 */
+
+* {
+ box-sizing: border-box;
+ margin: 0;
+ padding: 0;
+}
+
+html,
+body {
+ height: 100%;
+ overflow: hidden;
+ font-family: var(--font-sans);
+ font-size: var(--fs-body);
+ line-height: var(--lh-body);
+ background: var(--bg);
+ color: var(--text-1);
+ -webkit-font-smoothing: antialiased;
+ text-rendering: optimizeLegibility;
+}
+
+/* 主题切换过渡(仅作用于整体背景/文字,避免闪烁) */
+body.theme-transition,
+body.theme-transition * {
+ transition:
+ background-color var(--dur-slow) var(--ease),
+ border-color var(--dur-slow) var(--ease),
+ color var(--dur-slow) var(--ease) !important;
+}
+
+button {
+ font-family: inherit;
+ font-size: var(--fs-secondary);
+ border: none;
+ background: none;
+ cursor: pointer;
+ color: inherit;
+ border-radius: var(--radius-btn);
+ transition:
+ background-color var(--dur-fast) var(--ease),
+ color var(--dur-fast) var(--ease),
+ box-shadow var(--dur-fast) var(--ease),
+ transform var(--dur-fast) var(--ease);
+}
+button:active {
+ transform: scale(0.97);
+}
+button:disabled {
+ opacity: 0.5;
+ cursor: not-allowed;
+}
+
+textarea,
+input,
+select {
+ font-family: inherit;
+ font-size: var(--fs-body);
+ color: var(--text-1);
+ background: var(--bg-panel);
+ border: 1px solid var(--border);
+ border-radius: var(--radius-input);
+ outline: none;
+ transition:
+ border-color var(--dur-fast) var(--ease),
+ box-shadow var(--dur-fast) var(--ease),
+ background-color var(--dur-fast) var(--ease);
+}
+textarea:focus,
+input:focus,
+select:focus {
+ border-color: var(--accent);
+ box-shadow: 0 0 0 3px var(--accent-soft);
+}
+
+::placeholder {
+ color: var(--text-3);
+}
+
+/* 滚动条(WebKit) */
+::-webkit-scrollbar {
+ width: 8px;
+ height: 8px;
+}
+::-webkit-scrollbar-thumb {
+ background: var(--scrollbar-thumb);
+ border-radius: 4px;
+}
+::-webkit-scrollbar-thumb:hover {
+ background: var(--scrollbar-hover);
+}
+::-webkit-scrollbar-track {
+ background: transparent;
+}
+
+/* 代码块等宽字体 */
+code,
+pre,
+.tool-output {
+ font-family: var(--font-mono);
+}
+
+.hidden {
+ display: none !important;
+}
+
+[hidden] {
+ display: none !important;
+}
diff --git a/emrg/gui/renderer/css/components.css b/emrg/gui/renderer/css/components.css
new file mode 100644
index 0000000..d4bed6d
--- /dev/null
+++ b/emrg/gui/renderer/css/components.css
@@ -0,0 +1,714 @@
+/* components.css — 组件:对话项 / 消息气泡 / 输入按钮 / 工具友好行 / 对话框 / 空状态 / 模型切换器 */
+
+/* ── 侧边栏对话项 ─────────────────────── */
+.conv-item {
+ display: flex;
+ align-items: center;
+ gap: var(--sp-2);
+ padding: 9px var(--sp-3);
+ border-radius: 10px;
+ cursor: pointer;
+ color: var(--text-2);
+ font-size: var(--fs-secondary);
+ margin-bottom: 1px;
+ transition:
+ background-color var(--dur-fast) var(--ease),
+ color var(--dur-fast) var(--ease);
+}
+.conv-item:hover {
+ background: var(--bg-soft);
+ color: var(--text-1);
+}
+.conv-item.active {
+ background: var(--accent-soft);
+ color: var(--accent);
+ font-weight: 500;
+}
+.conv-title {
+ flex: 1;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+}
+.conv-item.placeholder {
+ color: var(--text-3);
+ cursor: default;
+ justify-content: center;
+}
+
+/* ── 消息 ─────────────────────────────── */
+.msg {
+ max-width: 100%;
+}
+.msg.user {
+ align-self: flex-end;
+ background: var(--accent-soft);
+ color: var(--text-1);
+ border-radius: var(--radius-bubble);
+ border-bottom-right-radius: 6px;
+ padding: 10px 16px;
+ max-width: 72%;
+ white-space: pre-wrap;
+ word-break: break-word;
+ animation: msg-in var(--dur-med) var(--ease);
+}
+.msg.assistant {
+ display: flex;
+ flex-direction: column;
+ gap: var(--sp-1);
+}
+.msg-assistant-mark {
+ color: var(--accent);
+ font-weight: 600;
+ user-select: none;
+}
+.msg-body {
+ color: var(--text-1);
+ font-size: var(--fs-body);
+ line-height: var(--lh-body);
+ word-break: break-word;
+}
+.msg-body.typing::after {
+ content: "▍";
+ color: var(--accent);
+ animation: cursor-pulse 1s ease-in-out infinite;
+}
+.msg.system {
+ text-align: center;
+ font-size: var(--fs-secondary);
+ color: var(--text-3);
+ padding: var(--sp-1) 0;
+ user-select: none;
+}
+.msg.remote-label {
+ font-size: var(--fs-aux);
+ color: var(--text-3);
+}
+
+/* Markdown 内容排版 */
+.msg-body p {
+ margin: 0 0 0.6em;
+}
+.msg-body p:last-child {
+ margin-bottom: 0;
+}
+.msg-body h1, .msg-body h2, .msg-body h3, .msg-body h4 {
+ margin: 0.8em 0 0.4em;
+ font-weight: 600;
+ line-height: 1.4;
+}
+.msg-body h1 { font-size: 1.35em; }
+.msg-body h2 { font-size: 1.2em; }
+.msg-body h3 { font-size: 1.1em; }
+.msg-body ul, .msg-body ol {
+ margin: 0.4em 0;
+ padding-left: 1.5em;
+}
+.msg-body li {
+ margin: 0.2em 0;
+}
+.msg-body code {
+ background: var(--bg-soft);
+ border-radius: 4px;
+ padding: 0.1em 0.35em;
+ font-size: 0.88em;
+}
+.msg-body pre {
+ background: var(--bg-soft);
+ border: 1px solid var(--border);
+ border-radius: var(--radius-card);
+ padding: var(--sp-3);
+ overflow-x: auto;
+ margin: 0.5em 0;
+}
+.msg-body pre code {
+ background: none;
+ padding: 0;
+ font-size: 0.85em;
+ line-height: 1.55;
+}
+.msg-body blockquote {
+ border-left: 3px solid var(--border);
+ color: var(--text-2);
+ margin: 0.5em 0;
+ padding-left: var(--sp-3);
+}
+.msg-body a {
+ color: var(--accent);
+ text-decoration: none;
+}
+.msg-body a:hover {
+ text-decoration: underline;
+}
+.msg-body table {
+ border-collapse: collapse;
+ margin: 0.5em 0;
+ font-size: 0.92em;
+}
+.msg-body th, .msg-body td {
+ border: 1px solid var(--border);
+ padding: 6px 10px;
+ text-align: left;
+}
+.msg-body img {
+ max-width: 100%;
+ border-radius: var(--radius-card);
+}
+.msg-body hr {
+ border: none;
+ border-top: 1px solid var(--border);
+ margin: 1em 0;
+}
+
+/* ── 工具友好状态行 ───────────────────── */
+.tool-row {
+ display: flex;
+ align-items: center;
+ gap: var(--sp-2);
+ font-size: var(--fs-secondary);
+ color: var(--text-2);
+ padding: 4px 2px;
+ border-radius: 8px;
+ cursor: pointer;
+ user-select: none;
+ transition:
+ background-color var(--dur-fast) var(--ease),
+ color var(--dur-fast) var(--ease);
+}
+.tool-row:hover {
+ background: var(--bg-soft);
+}
+.tool-row .tool-spinner {
+ display: inline-block;
+ width: 14px;
+ height: 14px;
+ border: 2px solid var(--amber-soft);
+ border-top-color: var(--amber);
+ border-radius: 50%;
+ animation: spin 0.8s linear infinite;
+ flex-shrink: 0;
+}
+.tool-row.running {
+ color: var(--amber);
+}
+.tool-row.done {
+ color: var(--text-2);
+}
+.tool-row.done .tool-check {
+ color: var(--accent);
+ font-weight: 700;
+}
+.tool-row.failed {
+ color: var(--red);
+}
+.tool-row .tool-chevron {
+ font-size: 10px;
+ color: var(--text-3);
+ margin-left: auto;
+ transition: transform var(--dur-med) var(--ease);
+}
+.tool-row.expanded .tool-chevron {
+ transform: rotate(180deg);
+}
+.tool-output {
+ font-size: var(--fs-secondary);
+ line-height: 1.6;
+ background: var(--bg-soft);
+ border: 1px solid var(--border);
+ border-radius: 10px;
+ padding: var(--sp-3);
+ margin: 2px 0 6px;
+ max-height: 320px;
+ overflow-y: auto;
+ white-space: pre-wrap;
+ word-break: break-word;
+}
+.tool-expand-btn {
+ font-size: var(--fs-aux);
+ color: var(--accent);
+ padding: 2px 6px;
+ margin: 0 0 6px 2px;
+}
+
+/* ── 空状态欢迎屏 ─────────────────────── */
+#empty-state {
+ flex: 1;
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ justify-content: center;
+ gap: var(--sp-4);
+ padding: var(--sp-6);
+ text-align: center;
+}
+.empty-hello {
+ font-size: 26px;
+ font-weight: 600;
+ color: var(--text-1);
+ display: flex;
+ align-items: center;
+ gap: var(--sp-2);
+}
+.empty-hello .brand-star {
+ color: var(--accent);
+ font-size: 26px;
+}
+.empty-sub {
+ color: var(--text-2);
+ font-size: var(--fs-secondary);
+ max-width: 420px;
+}
+.example-grid {
+ display: grid;
+ grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
+ gap: var(--sp-3);
+ max-width: 520px;
+ width: 100%;
+}
+.example-card {
+ background: var(--bg-panel);
+ border: 1px solid var(--border);
+ border-radius: var(--radius-card);
+ padding: var(--sp-4);
+ font-size: var(--fs-secondary);
+ color: var(--text-2);
+ text-align: left;
+ box-shadow: var(--shadow-sm);
+ transition:
+ border-color var(--dur-fast) var(--ease),
+ box-shadow var(--dur-fast) var(--ease),
+ transform var(--dur-fast) var(--ease);
+}
+.example-card:hover {
+ border-color: var(--accent);
+ box-shadow: var(--shadow-md);
+ transform: translateY(-1px);
+ color: var(--text-1);
+}
+
+/* ── 按钮 ─────────────────────────────── */
+.btn {
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ gap: var(--sp-2);
+ padding: 8px var(--sp-4);
+ border-radius: var(--radius-btn);
+ font-size: var(--fs-secondary);
+ min-height: 36px;
+ transition:
+ background-color var(--dur-fast) var(--ease),
+ box-shadow var(--dur-fast) var(--ease),
+ transform var(--dur-fast) var(--ease);
+}
+.btn-primary {
+ background: var(--accent);
+ color: #fff;
+ font-weight: 500;
+ box-shadow: var(--shadow-sm);
+}
+.btn-primary:hover {
+ background: var(--accent-strong);
+ box-shadow: var(--shadow-md);
+}
+.btn-ghost {
+ background: var(--bg-soft);
+ color: var(--text-2);
+}
+.btn-ghost:hover {
+ background: var(--border);
+ color: var(--text-1);
+}
+.btn-danger {
+ background: var(--red);
+ color: #fff;
+}
+.btn-danger:hover {
+ background: #d0343a;
+}
+
+/* 发送 / 停止按钮(圆形) */
+.send-btn,
+.stop-btn {
+ width: 38px;
+ height: 38px;
+ min-width: 38px;
+ border-radius: 50%;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ font-size: 18px;
+ flex-shrink: 0;
+ transition:
+ background-color var(--dur-fast) var(--ease),
+ box-shadow var(--dur-fast) var(--ease),
+ transform var(--dur-fast) var(--ease);
+}
+.send-btn {
+ background: var(--accent);
+ color: #fff;
+ box-shadow: var(--shadow-sm);
+}
+.send-btn:hover:not(:disabled) {
+ background: var(--accent-strong);
+ box-shadow: var(--shadow-md);
+}
+.stop-btn {
+ background: var(--red);
+ color: #fff;
+}
+.stop-btn:hover {
+ background: #d0343a;
+}
+
+/* ── 模型切换器 ───────────────────────── */
+.model-switcher {
+ display: inline-flex;
+ align-items: center;
+ gap: 4px;
+ padding: 4px 10px;
+ border-radius: 999px;
+ font-size: var(--fs-secondary);
+ color: var(--text-2);
+ background: transparent;
+ border: 1px solid transparent;
+ cursor: pointer;
+ transition:
+ background-color var(--dur-fast) var(--ease),
+ border-color var(--dur-fast) var(--ease);
+}
+.model-switcher:hover {
+ background: var(--bg-soft);
+ border-color: var(--border);
+}
+.model-switcher.highlight {
+ background: var(--accent-soft);
+ color: var(--accent);
+ border-color: var(--accent);
+}
+.model-menu {
+ position: absolute;
+ bottom: 52px;
+ left: var(--sp-5);
+ background: var(--bg-panel);
+ border: 1px solid var(--border);
+ border-radius: var(--radius-card);
+ box-shadow: var(--shadow-lg);
+ padding: var(--sp-2);
+ min-width: 240px;
+ z-index: 30;
+ animation: menu-in var(--dur-fast) var(--ease);
+}
+.model-menu-item {
+ display: flex;
+ align-items: center;
+ gap: var(--sp-2);
+ padding: 8px 10px;
+ border-radius: 8px;
+ cursor: pointer;
+ font-size: var(--fs-secondary);
+ color: var(--text-1);
+}
+.model-menu-item:hover {
+ background: var(--bg-soft);
+}
+.model-menu-item .model-check {
+ color: var(--accent);
+ font-weight: 700;
+}
+.model-menu-item .model-vision {
+ font-size: var(--fs-aux);
+ color: var(--text-3);
+}
+.model-menu-empty {
+ padding: var(--sp-3);
+ font-size: var(--fs-secondary);
+ color: var(--text-3);
+ text-align: center;
+}
+.model-menu-empty a {
+ color: var(--accent);
+ cursor: pointer;
+}
+
+/* ── 连接状态圆点 ─────────────────────── */
+.status-dot {
+ width: 9px;
+ height: 9px;
+ border-radius: 50%;
+ background: var(--text-3);
+ flex-shrink: 0;
+ transition: background-color var(--dur-med) var(--ease);
+}
+.status-dot.green {
+ background: var(--accent);
+ box-shadow: 0 0 0 3px var(--accent-soft);
+}
+.status-dot.red {
+ background: var(--red);
+ box-shadow: 0 0 0 3px var(--red-soft);
+}
+.status-dot.gray {
+ background: var(--text-3);
+}
+
+/* ── 对话框 ───────────────────────────── */
+dialog {
+ border: none;
+ padding: 0;
+ background: transparent;
+ border-radius: var(--radius-dialog);
+}
+dialog::backdrop {
+ background: rgba(0, 0, 0, 0.35);
+ backdrop-filter: blur(2px);
+ animation: fade-in var(--dur-slow) var(--ease);
+}
+.dialog-card {
+ background: var(--bg-panel);
+ border: 1px solid var(--border);
+ border-radius: var(--radius-dialog);
+ box-shadow: var(--shadow-lg);
+ padding: var(--sp-5);
+ min-width: 400px;
+ max-width: 520px;
+ animation: dialog-in var(--dur-slow) var(--ease);
+}
+.dialog-card h2 {
+ font-size: var(--fs-title);
+ font-weight: 600;
+ margin-bottom: var(--sp-4);
+ color: var(--text-1);
+}
+.dialog-card label {
+ display: block;
+ font-size: var(--fs-secondary);
+ color: var(--text-2);
+ margin-bottom: var(--sp-3);
+}
+.dialog-card label > input,
+.dialog-card label > select {
+ width: 100%;
+ margin-top: var(--sp-1);
+ padding: 8px 10px;
+}
+.dialog-card .hint {
+ font-size: var(--fs-aux);
+ color: var(--text-3);
+ margin-top: 4px;
+}
+.dialog-actions {
+ display: flex;
+ justify-content: flex-end;
+ gap: var(--sp-2);
+ margin-top: var(--sp-5);
+}
+.dir-row {
+ display: flex;
+ gap: var(--sp-2);
+ margin-top: var(--sp-1);
+}
+.dir-row input {
+ flex: 1;
+ padding: 8px 10px;
+}
+
+/* 设置分组 */
+.settings-group {
+ margin-bottom: var(--sp-4);
+}
+.settings-group-title {
+ font-size: var(--fs-secondary);
+ font-weight: 600;
+ color: var(--text-1);
+ margin-bottom: var(--sp-2);
+}
+
+/* 多模型管理列表 */
+#model-list {
+ display: flex;
+ flex-direction: column;
+ gap: 6px;
+ margin-top: 4px;
+}
+.model-item {
+ display: flex;
+ align-items: center;
+ gap: 10px;
+ padding: 8px 10px;
+ border: 1px solid var(--border);
+ border-radius: 10px;
+ background: var(--bg);
+ transition:
+ border-color var(--dur-fast) var(--ease),
+ box-shadow var(--dur-fast) var(--ease);
+}
+.model-item.default {
+ border-color: var(--accent);
+ background: var(--accent-soft);
+}
+.model-radio {
+ width: 18px;
+ height: 18px;
+ min-width: 18px;
+ border-radius: 50%;
+ border: 2px solid var(--text-3);
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ font-size: 10px;
+ color: var(--accent);
+ padding: 0;
+ background: transparent;
+ transition: border-color var(--dur-fast) var(--ease);
+}
+.model-radio.checked {
+ border-color: var(--accent);
+}
+.model-radio:hover {
+ border-color: var(--accent);
+}
+.model-name {
+ font-size: var(--fs-secondary);
+ font-weight: 500;
+ color: var(--text-1);
+ display: flex;
+ align-items: center;
+ gap: 6px;
+}
+.model-badge {
+ font-size: 10px;
+ background: var(--accent);
+ color: #fff;
+ border-radius: 999px;
+ padding: 1px 8px;
+ font-weight: 600;
+}
+.model-id {
+ font-size: var(--fs-aux);
+ color: var(--text-3);
+ font-family: var(--font-mono);
+}
+.model-vision {
+ font-size: var(--fs-aux);
+ color: var(--text-2);
+}
+.model-actions {
+ margin-left: auto;
+ display: flex;
+ gap: 4px;
+ flex-shrink: 0;
+}
+.model-action-btn {
+ font-size: var(--fs-aux);
+ color: var(--text-2);
+ padding: 4px 8px;
+ border-radius: 6px;
+}
+.model-action-btn:hover {
+ background: var(--bg-soft);
+ color: var(--text-1);
+}
+.model-action-btn.danger:hover {
+ background: var(--red-soft);
+ color: var(--red);
+}
+.model-list-empty {
+ font-size: var(--fs-aux);
+ color: var(--text-3);
+ text-align: center;
+ padding: var(--sp-2);
+}
+
+/* 行内模型表单 */
+.model-form {
+ margin-top: 8px;
+ padding: var(--sp-3);
+ border: 1px dashed var(--border);
+ border-radius: 10px;
+ background: var(--bg-soft);
+}
+.model-form-row {
+ display: flex;
+ gap: var(--sp-3);
+}
+.model-form-label {
+ flex: 1;
+ margin-bottom: 0;
+}
+.model-form-label input {
+ width: 100%;
+ margin-top: 2px;
+ padding: 6px 10px;
+ font-size: var(--fs-secondary);
+}
+.model-form-footer {
+ display: flex;
+ align-items: center;
+ margin-top: var(--sp-2);
+}
+.model-form-vision {
+ display: flex;
+ align-items: center;
+ gap: 6px;
+ font-size: var(--fs-secondary);
+ color: var(--text-2);
+ margin-bottom: 0;
+ cursor: pointer;
+}
+.model-form-vision input {
+ accent-color: var(--accent);
+}
+.theme-options {
+ display: flex;
+ gap: var(--sp-2);
+}
+.theme-option {
+ flex: 1;
+ padding: 8px;
+ border: 1px solid var(--border);
+ border-radius: var(--radius-btn);
+ font-size: var(--fs-secondary);
+ text-align: center;
+ color: var(--text-2);
+ transition:
+ border-color var(--dur-fast) var(--ease),
+ background-color var(--dur-fast) var(--ease),
+ color var(--dur-fast) var(--ease);
+}
+.theme-option:hover {
+ border-color: var(--accent);
+}
+.theme-option.active {
+ border-color: var(--accent);
+ background: var(--accent-soft);
+ color: var(--accent);
+ font-weight: 500;
+}
+
+/* 首启向导 */
+.welcome-brand {
+ text-align: center;
+ margin-bottom: var(--sp-4);
+}
+.welcome-brand .brand-star {
+ font-size: 34px;
+ color: var(--accent);
+}
+.welcome-brand h1 {
+ font-size: var(--fs-title-lg);
+ font-weight: 600;
+ margin-top: var(--sp-2);
+}
+.welcome-brand p {
+ color: var(--text-2);
+ font-size: var(--fs-secondary);
+ margin-top: var(--sp-1);
+}
+
+/* ── 关于(演化计数) ──────────────────── */
+.about-line {
+ font-size: var(--fs-secondary);
+ color: var(--text-2);
+ margin-bottom: var(--sp-2);
+}
diff --git a/emrg/gui/renderer/css/layout.css b/emrg/gui/renderer/css/layout.css
new file mode 100644
index 0000000..8e908a5
--- /dev/null
+++ b/emrg/gui/renderer/css/layout.css
@@ -0,0 +1,194 @@
+/* layout.css — 整体布局:左侧边栏 + 主区(居中单栏聊天 + 输入卡片)
+ * 结构:
+ * #app
+ * #sidebar(品牌区 + 新对话 + 分组列表 + 底部设置/连接圆点)
+ * #main(#chat-view 滚动区 + #empty-state 欢迎屏 + #composer-wrap 输入卡片)
+ */
+
+#app {
+ display: flex;
+ height: 100vh;
+ width: 100vw;
+}
+
+/* ── 侧边栏 ─────────────────────────────── */
+#sidebar {
+ width: 264px;
+ min-width: 220px;
+ display: flex;
+ flex-direction: column;
+ background: var(--bg-panel);
+ border-right: 1px solid var(--border);
+}
+
+.sidebar-brand {
+ display: flex;
+ align-items: center;
+ gap: var(--sp-2);
+ padding: var(--sp-4) var(--sp-4) var(--sp-3);
+ font-size: var(--fs-title-lg);
+ font-weight: 600;
+ color: var(--text-1);
+}
+.sidebar-brand .brand-star {
+ color: var(--accent);
+ font-size: 18px;
+}
+
+.new-chat-btn {
+ margin: 0 var(--sp-3) var(--sp-3);
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ gap: var(--sp-2);
+ padding: 9px var(--sp-3);
+ background: var(--accent);
+ color: #fff;
+ font-size: var(--fs-secondary);
+ font-weight: 500;
+ border-radius: var(--radius-btn);
+ box-shadow: var(--shadow-sm);
+ transition:
+ background-color var(--dur-fast) var(--ease),
+ box-shadow var(--dur-fast) var(--ease),
+ transform var(--dur-fast) var(--ease);
+}
+.new-chat-btn:hover {
+ background: var(--accent-strong);
+ box-shadow: var(--shadow-md);
+}
+
+#conv-list {
+ flex: 1;
+ overflow-y: auto;
+ padding: 0 var(--sp-2) var(--sp-2);
+}
+
+.conv-group-label {
+ padding: var(--sp-3) var(--sp-2) var(--sp-1);
+ font-size: var(--fs-aux);
+ font-weight: 600;
+ color: var(--text-3);
+ letter-spacing: 0.02em;
+}
+
+.sidebar-footer {
+ display: flex;
+ align-items: center;
+ gap: var(--sp-2);
+ padding: var(--sp-3) var(--sp-4);
+ border-top: 1px solid var(--border);
+}
+
+/* ── 主区 ───────────────────────────────── */
+#main {
+ flex: 1;
+ display: flex;
+ flex-direction: column;
+ min-width: 0;
+ position: relative;
+ background: var(--bg);
+}
+
+/* 聊天滚动区:居中单栏最大 760px,左右留白 ≥24px */
+#chat-view {
+ flex: 1;
+ overflow-y: auto;
+ display: flex;
+ flex-direction: column;
+ gap: var(--sp-5);
+ padding: var(--sp-5) var(--sp-5) var(--sp-4);
+ scroll-behavior: smooth;
+}
+#chat-view > * {
+ width: 100%;
+ max-width: 760px;
+ margin-left: auto;
+ margin-right: auto;
+}
+
+/* 输入区 */
+#composer-wrap {
+ padding: var(--sp-2) var(--sp-5) var(--sp-4);
+ max-width: 808px;
+ margin: 0 auto;
+ width: 100%;
+}
+
+.composer-card {
+ display: flex;
+ align-items: flex-end;
+ gap: var(--sp-2);
+ background: var(--bg-panel);
+ border: 1px solid var(--border);
+ border-radius: var(--radius-input);
+ box-shadow: var(--shadow-md);
+ padding: var(--sp-2) var(--sp-2) var(--sp-2) var(--sp-4);
+ transition:
+ border-color var(--dur-fast) var(--ease),
+ box-shadow var(--dur-fast) var(--ease);
+}
+.composer-card:focus-within {
+ border-color: var(--accent);
+ box-shadow: 0 0 0 3px var(--accent-soft), var(--shadow-md);
+}
+
+#input {
+ flex: 1;
+ border: none;
+ background: transparent;
+ box-shadow: none;
+ resize: none;
+ max-height: 150px;
+ line-height: 1.6;
+ padding: var(--sp-2) 0;
+}
+#input:focus {
+ border: none;
+ box-shadow: none;
+}
+
+.composer-hint {
+ text-align: center;
+ font-size: var(--fs-aux);
+ color: var(--text-3);
+ margin-top: var(--sp-2);
+}
+
+/* ── 断连横幅 ───────────────────────────── */
+#conn-banner {
+ position: absolute;
+ top: var(--sp-3);
+ left: 50%;
+ transform: translateX(-50%);
+ background: var(--amber-soft);
+ color: var(--amber);
+ border: 1px solid var(--border);
+ border-radius: 999px;
+ padding: 6px var(--sp-4);
+ font-size: var(--fs-secondary);
+ box-shadow: var(--shadow-md);
+ z-index: 20;
+ animation: banner-in var(--dur-med) var(--ease);
+}
+
+/* ── 侧边栏折叠(⌘B / 窄屏) ────────────── */
+body.sidebar-collapsed #sidebar {
+ display: none;
+}
+body.sidebar-collapsed #app {
+ /* 主区自动占满 */
+}
+
+/* 窄屏自动收起侧边栏 */
+@media (max-width: 720px) {
+ #sidebar {
+ display: none;
+ }
+ #chat-view {
+ padding: var(--sp-4) var(--sp-4) var(--sp-3);
+ }
+ #composer-wrap {
+ padding: var(--sp-2) var(--sp-3) var(--sp-3);
+ }
+}
diff --git a/emrg/gui/renderer/css/tokens.css b/emrg/gui/renderer/css/tokens.css
new file mode 100644
index 0000000..a9eafe2
--- /dev/null
+++ b/emrg/gui/renderer/css/tokens.css
@@ -0,0 +1,139 @@
+/* tokens.css — 设计 token:双主题色板 / 字体 / 圆角 / 阴影 / 间距 / 动效
+ * 设计来源:docs/design/gui-redesign.md(rant 2026-08-05T21:59:47 完整复述)
+ * 双主题:浅色 + 深色。默认 prefers-color-scheme 跟随系统,
+ * 手动覆盖:(设置里三选)。
+ */
+:root {
+ /* 字体 */
+ --font-sans: -apple-system, BlinkMacSystemFont, "Segoe UI", "PingFang SC",
+ "Noto Sans CJK SC", "Microsoft YaHei", sans-serif;
+ --font-mono: "SF Mono", "Cascadia Code", Consolas, "JetBrains Mono", "Courier New", monospace;
+ --fs-body: 15px;
+ --fs-secondary: 13px;
+ --fs-aux: 12px;
+ --fs-title: 17px;
+ --fs-title-lg: 20px;
+ --lh-body: 1.7;
+
+ /* 圆角 */
+ --radius-bubble: 18px;
+ --radius-input: 12px;
+ --radius-btn: 12px;
+ --radius-card: 14px;
+ --radius-dialog: 20px;
+
+ /* 间距(8px 基准网格) */
+ --sp-1: 4px;
+ --sp-2: 8px;
+ --sp-3: 12px;
+ --sp-4: 16px;
+ --sp-5: 24px;
+ --sp-6: 32px;
+
+ /* 动效 */
+ --dur-fast: 150ms;
+ --dur-med: 250ms;
+ --dur-slow: 350ms;
+ --ease: cubic-bezier(0.2, 0.8, 0.3, 1);
+
+ /* 浅色主题(默认) */
+ --bg: #fafafa;
+ --bg-panel: #ffffff;
+ --bg-soft: #f2f3f5;
+ --text-1: #1a1d21;
+ --text-2: #5f6672;
+ --text-3: #9aa1ad;
+ --border: #e8eaed;
+ --accent: #16a06c; /* 生机绿 */
+ --accent-strong: #0e8a5c;
+ --accent-soft: #e6f5ee;
+ --blue: #3b82f6;
+ --red: #e5484d;
+ --red-soft: #fdecec;
+ --amber: #f5a623;
+ --amber-soft: #fdf3e0;
+
+ /* 阴影(浅色三档柔和) */
+ --shadow-sm: 0 1px 2px rgba(16, 24, 40, 0.06);
+ --shadow-md: 0 4px 12px rgba(16, 24, 40, 0.08);
+ --shadow-lg: 0 12px 32px rgba(16, 24, 40, 0.14);
+
+ /* 滚动条 */
+ --scrollbar-thumb: #d3d6db;
+ --scrollbar-hover: #b9bdc4;
+}
+
+/* 深色主题(跟随系统) */
+@media (prefers-color-scheme: dark) {
+ :root:not([data-theme="light"]) {
+ --bg: #17181c;
+ --bg-panel: #1f2127;
+ --bg-soft: #26282f;
+ --text-1: #eceef2;
+ --text-2: #a4a9b4;
+ --text-3: #6b7078;
+ --border: #2e3138;
+ --accent: #2fbf83;
+ --accent-strong: #3fd493;
+ --accent-soft: #1d3a2f;
+ --blue: #5b9cf6;
+ --red: #ef5d62;
+ --red-soft: #3a2326;
+ --amber: #f0a940;
+ --amber-soft: #3a3120;
+ /* 深色阴影弱化,靠底色分层 */
+ --shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.3);
+ --shadow-md: 0 4px 12px rgba(0, 0, 0, 0.35);
+ --shadow-lg: 0 12px 32px rgba(0, 0, 0, 0.45);
+ --scrollbar-thumb: #3a3d45;
+ --scrollbar-hover: #4a4e58;
+ }
+}
+
+/* 手动强制深色 */
+:root[data-theme="dark"] {
+ --bg: #17181c;
+ --bg-panel: #1f2127;
+ --bg-soft: #26282f;
+ --text-1: #eceef2;
+ --text-2: #a4a9b4;
+ --text-3: #6b7078;
+ --border: #2e3138;
+ --accent: #2fbf83;
+ --accent-strong: #3fd493;
+ --accent-soft: #1d3a2f;
+ --blue: #5b9cf6;
+ --red: #ef5d62;
+ --red-soft: #3a2326;
+ --amber: #f0a940;
+ --amber-soft: #3a3120;
+ --shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.3);
+ --shadow-md: 0 4px 12px rgba(0, 0, 0, 0.35);
+ --shadow-lg: 0 12px 32px rgba(0, 0, 0, 0.45);
+ --scrollbar-thumb: #3a3d45;
+ --scrollbar-hover: #4a4e58;
+}
+
+/* 手动强制浅色 */
+:root[data-theme="light"] {
+ --bg: #fafafa;
+ --bg-panel: #ffffff;
+ --bg-soft: #f2f3f5;
+ --text-1: #1a1d21;
+ --text-2: #5f6672;
+ --text-3: #9aa1ad;
+ --border: #e8eaed;
+ --accent: #16a06c;
+ --accent-strong: #0e8a5c;
+ --accent-soft: #e6f5ee;
+ --blue: #3b82f6;
+ --red: #e5484d;
+ --red-soft: #fdecec;
+ --amber: #f5a623;
+ --amber-soft: #fdf3e0;
+ --shadow-sm: 0 1px 2px rgba(16, 24, 40, 0.06);
+ --shadow-md: 0 4px 12px rgba(16, 24, 40, 0.08);
+ --shadow-lg: 0 12px 32px rgba(16, 24, 40, 0.14);
+ --scrollbar-thumb: #d3d6db;
+ --scrollbar-hover: #b9bdc4;
+}
diff --git a/emrg/gui/renderer/index.html b/emrg/gui/renderer/index.html
index 019fc9c..86eca2a 100644
--- a/emrg/gui/renderer/index.html
+++ b/emrg/gui/renderer/index.html
@@ -4,90 +4,171 @@
EMRG
-
+
+
+
+
+
-
+