diff --git a/emrg/gui/main.js b/emrg/gui/main.js
index e22fbc57..dbb019bd 100644
--- a/emrg/gui/main.js
+++ b/emrg/gui/main.js
@@ -289,6 +289,14 @@ vision = false
return { ok: true };
});
+ ipcMain.handle("emrg:renameSession", async (_e, { sessionId, title }) => {
+ if (!validateSessionId(sessionId)) throw new Error("invalid session_id");
+ const clean = String(title || "").trim().slice(0, 80); // 截断超长标题
+ if (!clean) throw new Error("empty title");
+ const frame = await client.sendCommandAndWait("rename_session", { session_id: sessionId, cwd: projectDir, title: clean }, 5000);
+ return { ok: true, title: frame.title || clean };
+ });
+
ipcMain.handle("emrg:newSession", async () => {
// G14/G81:本地生成 session_id(无 new_session 消息)
const sid = generateSessionId();
diff --git a/emrg/gui/preload.js b/emrg/gui/preload.js
index b3485800..5c676cd9 100644
--- a/emrg/gui/preload.js
+++ b/emrg/gui/preload.js
@@ -13,6 +13,7 @@ const api = {
switchSession: (payload) => ipcRenderer.invoke("emrg:switchSession", payload),
newSession: () => ipcRenderer.invoke("emrg:newSession"),
deleteSession: (payload) => ipcRenderer.invoke("emrg:deleteSession", payload),
+ renameSession: (payload) => ipcRenderer.invoke("emrg:renameSession", payload),
setModel: (payload) => ipcRenderer.invoke("emrg:setModel", payload),
listModels: () => ipcRenderer.invoke("emrg:listModels"),
saveSettings: (payload) => ipcRenderer.invoke("emrg:saveSettings", payload),
diff --git a/emrg/gui/renderer/app.js b/emrg/gui/renderer/app.js
index 32e6fc24..55cd0590 100644
--- a/emrg/gui/renderer/app.js
+++ b/emrg/gui/renderer/app.js
@@ -175,7 +175,7 @@ async function newSession() {
// 本地切订阅(resume 不存在的新会话会被 daemon 自动创建)
state.sessionId = sid;
clearChat();
- addSystemMessage(`新会话 ${sid}`);
+ showWelcomeScreen(); // P3:空状态欢迎屏(设计 §3.5)
await refreshSessions();
highlightActiveSession(sid);
} catch (e) {
@@ -189,21 +189,8 @@ async function deleteSession(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}`);
- }
+ // P3:删除走友好确认对话框(sidebar.js),不再用原生 confirm()
+ await requestDeleteSession(sid);
}
async function refreshSessions() {
@@ -341,6 +328,7 @@ function addSystemMessage(text) {
}
function appendMsg(node) {
+ hideWelcomeScreen(); // P3:首条真实消息到达即移除欢迎屏
$("chat-view").appendChild(node);
scrollToBottom();
}
@@ -351,6 +339,45 @@ function clearChat() {
state.toolCards.clear();
}
+// ── 空状态欢迎屏(P3,设计 §3.5)──────────────────────
+
+const WELCOME_SUGGESTIONS = [
+ { icon: "📝", text: "帮我写一份周报" },
+ { icon: "🗂", text: "整理这个文件夹" },
+ { icon: "✈️", text: "规划一次旅行" },
+];
+
+function showWelcomeScreen() {
+ const cv = $("chat-view");
+ cv.innerHTML = "";
+ const wrap = document.createElement("div");
+ wrap.className = "welcome";
+ const cards = WELCOME_SUGGESTIONS.map(
+ (s) => ``
+ ).join("");
+ wrap.innerHTML = `
+
✦
+ 你好,我是 EMRG
+ 我可以帮你写作、整理、查资料、处理文件…
+ ${cards}
+ `;
+ for (const card of wrap.querySelectorAll(".welcome-card")) {
+ card.addEventListener("click", () => {
+ const input = $("input");
+ input.value = card.dataset.text;
+ input.style.height = "auto";
+ input.style.height = Math.min(input.scrollHeight, 150) + "px";
+ input.focus();
+ });
+ }
+ cv.appendChild(wrap);
+}
+
+function hideWelcomeScreen() {
+ const w = $("chat-view").querySelector(".welcome");
+ if (w) w.remove();
+}
+
function scrollToBottom() {
if (state.autoScroll) {
const cv = $("chat-view");
@@ -366,25 +393,29 @@ function renderSessions(sessions) {
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);
+ // P3:按时间分组(今天/昨天/更早),只显示标题(设计 §3.2——不显示 session ID/消息数)
+ for (const [label, group] of groupSessionsByTime(state.sessions)) {
+ if (!group.length) continue;
+ const gl = document.createElement("div");
+ gl.className = "session-group-label";
+ gl.textContent = label;
+ list.appendChild(gl);
+ for (const s of group) {
+ const item = document.createElement("div");
+ item.className = "session-item";
+ item.dataset.sid = s.session_id; // P3:右键菜单定位
+ const title = s.title || s.session_id; // G27:title 优先 session_id 兜底
+ item.innerHTML = `${escapeHtml(title)}`;
+ item.addEventListener("click", () => switchSession(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"));
+ for (const item of $("session-list").querySelectorAll(".session-item")) {
+ item.classList.toggle("active", item.dataset.sid === sid);
}
}
diff --git a/emrg/gui/renderer/css/components.css b/emrg/gui/renderer/css/components.css
index 5b1a89fe..a0e08aaf 100644
--- a/emrg/gui/renderer/css/components.css
+++ b/emrg/gui/renderer/css/components.css
@@ -28,8 +28,6 @@
text-overflow: ellipsis;
white-space: nowrap;
}
-.sess-count { color: var(--text-3); font-size: 11px; flex-shrink: 0; }
-.session-item.active .sess-count { color: var(--accent); }
/* ── 消息 ──────────────────────────── */
.msg {
@@ -197,6 +195,83 @@ dialog .desc { font-size: var(--text-aux); color: var(--text-2); margin-bottom:
.dir-row input { flex: 1; }
.dir-row button { flex-shrink: 0; }
+/* ── 空状态欢迎屏(P3,设计 §3.5)───────────────── */
+.welcome {
+ flex: 1;
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ justify-content: center;
+ gap: var(--space-3);
+ text-align: center;
+ padding: var(--space-5);
+ animation: msg-in var(--dur-med) var(--ease);
+}
+.welcome-mark {
+ font-size: 44px;
+ color: var(--accent);
+ line-height: 1;
+ margin-bottom: var(--space-2);
+}
+.welcome-title { font-size: 20px; font-weight: 600; color: var(--text-1); }
+.welcome-sub { font-size: var(--text-secondary); color: var(--text-2); }
+.welcome-cards {
+ display: flex;
+ flex-direction: column;
+ gap: var(--space-2);
+ margin-top: var(--space-4);
+ width: 100%;
+ max-width: 340px;
+}
+.welcome-card {
+ display: flex;
+ align-items: center;
+ gap: var(--space-3);
+ padding: var(--space-3) var(--space-4);
+ background: var(--bg-panel);
+ border: 1px solid var(--border);
+ border-radius: var(--radius-card);
+ color: var(--text-1);
+ font-size: var(--text-secondary);
+ text-align: left;
+ box-shadow: var(--shadow-sm);
+ transition: border-color var(--dur-fast), box-shadow var(--dur-fast), transform var(--dur-fast);
+}
+.welcome-card:hover {
+ border-color: var(--accent);
+ box-shadow: var(--shadow-md);
+ transform: translateY(-1px);
+}
+.welcome-icon { font-size: 18px; }
+
+/* ── 右键菜单(P3,设计 §3.2)────────────────── */
+.ctx-menu {
+ position: fixed;
+ z-index: 1000;
+ min-width: 150px;
+ background: var(--bg-panel);
+ border: 1px solid var(--border);
+ border-radius: var(--radius-card);
+ box-shadow: var(--shadow-lg);
+ padding: 4px;
+ animation: msg-in var(--dur-fast) var(--ease);
+}
+.ctx-item {
+ padding: 9px 12px;
+ border-radius: 8px;
+ font-size: var(--text-secondary);
+ color: var(--text-1);
+ cursor: pointer;
+ transition: background var(--dur-fast);
+}
+.ctx-item:hover { background: var(--bg-soft); }
+.ctx-item.danger { color: var(--red); }
+.ctx-item.danger:hover { background: color-mix(in srgb, var(--red) 10%, transparent); }
+
+/* ── 对话框危险按钮 ─────────────────────────── */
+.dialog-actions button.danger { background: var(--red); border-color: var(--red); color: #fff; font-weight: 600; }
+.dialog-actions button.danger:hover { filter: brightness(1.08); }
+
/* 消息入场动效:轻微上浮 + 淡入(克制) */
@keyframes msg-in {
from { opacity: 0; transform: translateY(8px); }
diff --git a/emrg/gui/renderer/css/layout.css b/emrg/gui/renderer/css/layout.css
index 650219bb..ab0dffe1 100644
--- a/emrg/gui/renderer/css/layout.css
+++ b/emrg/gui/renderer/css/layout.css
@@ -26,6 +26,37 @@
.brand-mark { color: var(--accent); font-size: 20px; line-height: 1; }
.brand-name { font-size: 17px; font-weight: 600; color: var(--text-1); letter-spacing: 0.2px; }
+/* 折叠按钮(⌘B):hover 出现,轻柔 */
+.btn-sidebar-toggle {
+ margin-left: auto;
+ width: 26px;
+ height: 26px;
+ border-radius: 8px;
+ border: none;
+ background: none;
+ color: var(--text-3);
+ font-size: 12px;
+ line-height: 1;
+ transition: background var(--dur-fast), color var(--dur-fast);
+}
+.btn-sidebar-toggle:hover { background: var(--bg-soft); color: var(--text-1); }
+
+/* 折叠态:只留品牌区(窄条),其余隐藏 */
+#sidebar.collapsed {
+ width: 52px;
+ min-width: 52px;
+}
+#sidebar.collapsed .brand-name,
+#sidebar.collapsed .btn-new,
+#sidebar.collapsed #session-list,
+#sidebar.collapsed .sidebar-footer .btn-settings {
+ display: none;
+}
+#sidebar.collapsed .brand { justify-content: center; padding: var(--space-3) 0; }
+#sidebar.collapsed .btn-sidebar-toggle { margin-left: 0; }
+#sidebar.collapsed .sidebar-footer { justify-content: center; padding: var(--space-3) 0; }
+#sidebar.collapsed .conn-dot { margin: 0 auto; }
+
/* 新对话按钮 */
.btn-new {
margin: 0 var(--space-4) var(--space-3);
diff --git a/emrg/gui/renderer/index.html b/emrg/gui/renderer/index.html
index 9b908f00..eb136caf 100644
--- a/emrg/gui/renderer/index.html
+++ b/emrg/gui/renderer/index.html
@@ -17,6 +17,7 @@
✦
EMRG
+
@@ -111,11 +112,39 @@
欢迎使用 EMRG
+
+
+
+
+
+
+
+
+
+