From b593d6385609cbe3cc9bd278db12d54a1c255261 Mon Sep 17 00:00:00 2001 From: JOJO <1498581755@qq.com> Date: Thu, 23 Jul 2026 16:47:49 +0800 Subject: [PATCH] =?UTF-8?q?feat(frontend):=20=E4=BE=A7=E8=BE=B9=E6=A0=8F?= =?UTF-8?q?=E7=B4=A7=E5=87=91=E5=8C=96=E4=B8=8E=E6=8C=A4=E5=8E=8B=E5=BC=8F?= =?UTF-8?q?=E9=87=8D=E8=AE=BE=E8=AE=A1=EF=BC=8C=E7=A7=BB=E9=99=A4=E5=B7=A5?= =?UTF-8?q?=E4=BD=9C=E5=8C=BA=E9=9D=A2=E6=9D=BF=E5=8A=9F=E8=83=BD=E6=8B=86?= =?UTF-8?q?=E5=88=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 对话侧边栏:行高/字号/圆角紧凑化,fixed 覆盖式改为文档流挤压式展开,移除工作区/虚拟显示器按钮 - 新增 WorkspaceSwitcher 浮层组件:列表即切换器,内联重命名/删除确认/新建表单动画,「…」菜单 fixed 逃逸裁切,刚好 7 行内部滚动 - 移除 LeftPanel 及两个旧工作区对话框:管理工作区迁入侧边栏,文件管理迁入输入栏 + 菜单(最多 4 项滚动) - 个人空间移除默认隐藏工作区/隐藏快捷窗口设置项;删除 panelMode 门控轮询(QuickDock 常驻轮询 + socket 推送替代) - 修复 flex 压缩导致菜单行高失效、分组对话 viewport 裁切最后一行、action-wrap 撑高对话项、/new 残留最近对话待办(无对话 id 不再请求 todo-list) - 工作区入口按钮改内联 layers 图标并统一 18px,host/docker 模式 localStorage 缓存避免按钮首屏延迟出现 --- demo/workspace_menu_redesign/app.js | 496 ++++++++ demo/workspace_menu_redesign/index.html | 157 +++ demo/workspace_menu_redesign/style.css | 666 +++++++++++ static/src/App.vue | 157 +-- static/src/app.ts | 13 +- static/src/app/components.ts | 10 - static/src/app/computed.ts | 4 - static/src/app/lifecycle.ts | 6 - static/src/app/methods/ui/dialog.ts | 14 - static/src/app/methods/ui/hostWorkspace.ts | 28 +- static/src/app/methods/ui/menu.ts | 11 - static/src/app/methods/ui/panel.ts | 47 - static/src/app/methods/ui/socket.ts | 3 +- static/src/app/methods/versioning.ts | 2 + static/src/app/state.ts | 30 +- static/src/components/input/InputComposer.vue | 4 +- static/src/components/input/QuickMenu.vue | 31 + .../overlay/HostWorkspaceCreateDialog.vue | 220 ---- .../overlay/HostWorkspaceManageDialog.vue | 409 ------- static/src/components/panels/LeftPanel.vue | 470 -------- .../personalization/PersonalizationDrawer.vue | 65 +- .../sidebar/ConversationSidebar.vue | 138 ++- .../components/sidebar/WorkspaceSwitcher.vue | 917 +++++++++++++++ static/src/composables/usePanelResize.ts | 8 +- static/src/stores/backgroundCommand.ts | 20 - static/src/stores/file.ts | 15 +- static/src/stores/personalization.ts | 12 +- static/src/stores/subAgent.ts | 20 - static/src/stores/ui.ts | 44 +- .../styles/components/panels/_left-panel.scss | 1025 ----------------- .../components/sidebar/_conversation.scss | 99 +- static/src/styles/index.scss | 1 - 32 files changed, 2502 insertions(+), 2640 deletions(-) create mode 100644 demo/workspace_menu_redesign/app.js create mode 100644 demo/workspace_menu_redesign/index.html create mode 100644 demo/workspace_menu_redesign/style.css delete mode 100644 static/src/components/overlay/HostWorkspaceCreateDialog.vue delete mode 100644 static/src/components/overlay/HostWorkspaceManageDialog.vue delete mode 100644 static/src/components/panels/LeftPanel.vue create mode 100644 static/src/components/sidebar/WorkspaceSwitcher.vue delete mode 100644 static/src/styles/components/panels/_left-panel.scss diff --git a/demo/workspace_menu_redesign/app.js b/demo/workspace_menu_redesign/app.js new file mode 100644 index 00000000..f62c763c --- /dev/null +++ b/demo/workspace_menu_redesign/app.js @@ -0,0 +1,496 @@ +/* ============================================================ + 工作区菜单重设计 Demo — 交互 + 数据字段对齐真实结构:workspace_id / label / path / running_task_count + ============================================================ */ + +(function () { + 'use strict'; + + /* ---------- 示例数据(字段语义与 HostWorkspaceManageDialog 一致) ---------- */ + const DATA = { + host: [ + { workspace_id: 'default', label: '默认工作区', path: '~/Desktop/agents', running_task_count: 2 }, + { workspace_id: 'ws-client-a', label: '客户 A 项目', path: '~/Desktop/work/client-a', running_task_count: 0 }, + { workspace_id: 'ws-research', label: '论文实验', path: '~/Documents/research/thesis', running_task_count: 1 }, + { workspace_id: 'ws-playground', label: '临时测试', path: '/tmp/agent-playground', running_task_count: 0 } + ], + docker: [ + { workspace_id: 'default', label: '默认项目', path: '', running_task_count: 0 }, + { workspace_id: 'ws-client-a', label: '客户 A 项目', path: '', running_task_count: 0 }, + { workspace_id: 'ws-research', label: '论文实验', path: '', running_task_count: 0 } + ] + }; + + const state = { + mode: 'host', // host=宿主机(工作区) / docker=项目 + workspaces: [], + currentId: 'default', + defaultId: 'default', + menuOpenId: null, // 「…」菜单展开的行 + renamingId: null, // 重命名内联编辑的行 + deletingId: null, // 删除确认内联的行 + popoverOpen: true, + createOpen: false + }; + + const cloneData = (mode) => DATA[mode].map((w) => ({ ...w })); + state.workspaces = cloneData('host'); + + /* ---------- DOM ---------- */ + const canvas = document.getElementById('canvas'); + const popover = document.getElementById('wsPopover'); + const listEl = document.getElementById('wsList'); + const countEl = document.getElementById('wsCount'); + const headerTitle = document.getElementById('headerTitle'); + const entryBtn = document.getElementById('workspaceEntry'); + const entryLabel = document.getElementById('entryLabel'); + const entryCurrentName = document.getElementById('entryCurrentName'); + const bottomEl = document.getElementById('wsBottom'); + const footerEl = document.getElementById('wsFooter'); + const createBtn = document.getElementById('createBtn'); + const createBtnText = document.getElementById('createBtnText'); + const createForm = document.getElementById('createForm'); + const createName = document.getElementById('createName'); + const createPath = document.getElementById('createPath'); + const createCancel = document.getElementById('createCancel'); + const statusEl = document.getElementById('stageStatus'); + + const esc = (s) => + String(s == null ? '' : s) + .replace(/&/g, '&') + .replace(//g, '>') + .replace(/"/g, '"'); + + const setStatus = (msg) => { statusEl.textContent = msg; }; + + const isDocker = () => state.mode === 'docker'; + const noun = () => (isDocker() ? '项目' : '工作区'); + + /* 内联 SVG 图标(file:// 协议下 会被 Chrome 拦截,故内联) */ + const ICON = { + folder: '', + dots: '' + }; + + /* ============================================================ + 浮层定位:锚定「工作区」入口按钮右侧,并夹紧在画布内 + ============================================================ */ + function positionPopover() { + const c = canvas.getBoundingClientRect(); + const b = entryBtn.getBoundingClientRect(); + const ph = popover.offsetHeight || 0; + let top = b.top - c.top - 6; + const maxTop = Math.max(8, c.height - ph - 42); /* 42 = 底部状态条 + 余量 */ + top = Math.max(8, Math.min(top, maxTop)); + popover.style.left = b.right - c.left + 8 + 'px'; + popover.style.top = top + 'px'; + } + + function setPopoverOpen(open) { + state.popoverOpen = open; + popover.classList.toggle('open', open); + entryBtn.classList.toggle('active', open); + entryBtn.setAttribute('aria-expanded', String(open)); + if (open) { + positionPopover(); + } else { + closeTransient(); + if (state.createOpen) closeCreateForm(true); + } + } + + function closeTransient() { + closeMenu(); + state.renamingId = null; + state.deletingId = null; + } + + /* ============================================================ + 「…」二级菜单:fixed 浮层,逃逸容器裁切 + (与 PersonalizationDrawer 的 settings-floating-menu 同方案) + ============================================================ */ + let menuEl = null; + + function ensureMenuEl() { + if (menuEl) return menuEl; + menuEl = document.createElement('div'); + menuEl.className = 'ws-menu-floating'; + document.body.appendChild(menuEl); + menuEl.addEventListener('click', (e) => { + const btn = e.target.closest('[data-act]'); + if (!btn || btn.disabled) return; + e.stopPropagation(); + handleMenuAction(btn.dataset.act); + }); + return menuEl; + } + + function openMenu(rowEl, id) { + state.menuOpenId = id; + state.renamingId = null; + state.deletingId = null; + renderRows(); + + const ws = state.workspaces.find((w) => w.workspace_id === id); + if (!ws) return; + const isDefault = id === state.defaultId; + + const el = ensureMenuEl(); + el.innerHTML = + '' + + '' + + '' + + '
' + + ''; + + el.style.display = 'flex'; + /* renderRows 后旧行元素已摘除,必须按 data-id 重新查询再取坐标 */ + const newRow = listEl.querySelector('.ws-row[data-id="' + id + '"]'); + const btn = newRow ? newRow.querySelector('.ws-more') : null; + if (!btn) { closeMenu(); return; } + const rect = btn.getBoundingClientRect(); + const mw = el.offsetWidth; + const mh = el.offsetHeight; + let left = rect.right - mw; + let top = rect.bottom + 4; + left = Math.max(8, Math.min(left, window.innerWidth - mw - 8)); + if (top + mh > window.innerHeight - 8) top = rect.top - mh - 4; + el.style.left = Math.round(left) + 'px'; + el.style.top = Math.round(top) + 'px'; + requestAnimationFrame(() => el.classList.add('open')); + } + + function closeMenu() { + state.menuOpenId = null; + if (menuEl) { + menuEl.classList.remove('open'); + menuEl.style.display = 'none'; + } + } + + function handleMenuAction(act) { + const id = state.menuOpenId; + const ws = state.workspaces.find((w) => w.workspace_id === id); + closeMenu(); + if (!ws) return; + + if (act === 'set-default') { + state.defaultId = id; + setStatus('已将「' + ws.label + '」设为默认' + noun()); + renderRows(); + } else if (act === 'rename') { + state.renamingId = id; + renderRows(); + } else if (act === 'reveal') { + /* 实现参考 InputComposer.vue 的 openProjectInFileManager */ + setStatus('在文件夹中打开:' + (ws.path || ws.label)); + } else if (act === 'delete') { + state.deletingId = id; + renderRows(); + } + } + + /* ============================================================ + 渲染列表行 + ============================================================ */ + function renderRows() { + const current = state.workspaces.find((w) => w.workspace_id === state.currentId); + entryCurrentName.textContent = current ? current.label : ''; + countEl.textContent = state.workspaces.length + ' 个'; + + listEl.innerHTML = ''; + for (const ws of state.workspaces) { + const li = document.createElement('div'); + const isCurrent = ws.workspace_id === state.currentId; + const isDefault = ws.workspace_id === state.defaultId; + + li.className = 'ws-row' + + (isDocker() ? ' docker' : '') + + (isCurrent ? ' current' : '') + + (state.menuOpenId === ws.workspace_id ? ' menu-open' : ''); + li.dataset.id = ws.workspace_id; + li.setAttribute('role', 'option'); + li.setAttribute('aria-selected', String(isCurrent)); + + if (state.deletingId === ws.workspace_id) { + /* --- 删除确认内联 --- */ + li.innerHTML = + '
' + + '
删除「' + esc(ws.label) + '」?
' + + '
' + + '' + + '' + + '
' + + '
'; + } else { + const metaHtml = + (isDefault ? '默认' : '') + + ''; + + const nameHtml = state.renamingId === ws.workspace_id + ? '' + : '' + esc(ws.label) + ''; + + const pathHtml = isDocker() + ? '' + : '' + esc(ws.path || '(未配置路径)') + ''; + + li.innerHTML = + '' + ICON.folder + '' + + '' + nameHtml + pathHtml + '' + + '' + metaHtml + ''; + } + + listEl.appendChild(li); + } + + /* 重命名输入框自动聚焦 */ + const renameInput = listEl.querySelector('[data-rename-input]'); + if (renameInput) { + renameInput.focus(); + renameInput.select(); + } + } + + /* ============================================================ + 行为 + ============================================================ */ + function switchWorkspace(id) { + if (id === state.currentId) return; + state.currentId = id; + const ws = state.workspaces.find((w) => w.workspace_id === id); + setStatus('已切换到「' + (ws ? ws.label : id) + '」'); + renderRows(); + } + + function commitRename(id, value) { + const label = value.trim(); + if (label) { + const ws = state.workspaces.find((w) => w.workspace_id === id); + if (ws && ws.label !== label) { + ws.label = label; + setStatus('已重命名为「' + label + '」'); + } + } + state.renamingId = null; + renderRows(); + } + + function removeWorkspace(id) { + const idx = state.workspaces.findIndex((w) => w.workspace_id === id); + if (idx === -1) return; + const removed = state.workspaces[idx]; + state.workspaces.splice(idx, 1); + if (state.currentId === id && state.workspaces.length) { + state.currentId = state.workspaces[0].workspace_id; + } + if (state.defaultId === id && state.workspaces.length) { + state.defaultId = state.workspaces[0].workspace_id; + } + state.deletingId = null; + setStatus('已删除「' + removed.label + '」'); + renderRows(); + } + + /* ============================================================ + 新建表单:底部区域高度动画 + 交叉淡入淡出 + ============================================================ */ + let bottomAnimating = false; + + function swapBottom(toForm, done) { + if (bottomAnimating) return; + bottomAnimating = true; + + const fromEl = toForm ? footerEl : createForm; + const toEl = toForm ? createForm : footerEl; + const h0 = fromEl.offsetHeight; + + bottomEl.style.height = h0 + 'px'; + fromEl.classList.add('fading'); + + setTimeout(() => { + fromEl.hidden = true; + fromEl.classList.remove('fading'); + toEl.hidden = false; + toEl.classList.add('fading'); + + const h1 = toEl.offsetHeight; + bottomEl.style.height = h1 + 'px'; + + requestAnimationFrame(() => { + requestAnimationFrame(() => toEl.classList.remove('fading')); + }); + + setTimeout(() => { + bottomEl.style.height = ''; + bottomAnimating = false; + if (typeof done === 'function') done(); + }, 210); + }, 130); + } + + function openCreateForm() { + if (state.createOpen) return; + state.createOpen = true; + closeMenu(); + createName.value = ''; + createPath.value = ''; + swapBottom(true, () => createName.focus()); + } + + function closeCreateForm(instant) { + if (!state.createOpen) return; + state.createOpen = false; + if (instant) { + createForm.hidden = true; + footerEl.hidden = false; + bottomEl.style.height = ''; + return; + } + swapBottom(false); + } + + createBtn.addEventListener('click', openCreateForm); + createCancel.addEventListener('click', () => closeCreateForm(false)); + + createForm.addEventListener('submit', (e) => { + e.preventDefault(); + const label = createName.value.trim() || (isDocker() ? '未命名项目' : '未命名工作区'); + const path = createPath.value.trim(); + const id = 'ws-' + Date.now().toString(36); + state.workspaces.push({ workspace_id: id, label: label, path: path, running_task_count: 0 }); + setStatus('已创建「' + label + '」'); + closeCreateForm(false); + renderRows(); + listEl.scrollTop = listEl.scrollHeight; + }); + + /* ============================================================ + 事件:列表行(事件委托) + ============================================================ */ + listEl.addEventListener('click', (e) => { + const actBtn = e.target.closest('[data-act]'); + const row = e.target.closest('.ws-row'); + const id = row ? row.dataset.id : null; + + if (actBtn) { + e.stopPropagation(); + const act = actBtn.dataset.act; + if (act === 'menu') { + if (state.menuOpenId === id) { + closeMenu(); + renderRows(); + } else { + openMenu(row, id); + } + return; + } + if (act === 'cancel-delete') { state.deletingId = null; renderRows(); return; } + if (act === 'confirm-delete') { removeWorkspace(id); return; } + } + + /* 点击行本体:切换工作区(重命名/删除确认态除外) */ + if (row && id && !state.renamingId && !state.deletingId) { + if (state.menuOpenId) { closeMenu(); renderRows(); return; } + switchWorkspace(id); + } + }); + + /* 重命名输入:Enter 提交 / Esc 取消 */ + listEl.addEventListener('keydown', (e) => { + const input = e.target.closest('[data-rename-input]'); + if (!input) return; + const row = input.closest('.ws-row'); + const id = row ? row.dataset.id : null; + if (e.key === 'Enter') { e.preventDefault(); commitRename(id, input.value); } + if (e.key === 'Escape') { e.preventDefault(); state.renamingId = null; renderRows(); } + e.stopPropagation(); + }); + + /* 列表滚动时收起浮动菜单 */ + listEl.addEventListener('scroll', () => { + if (state.menuOpenId) { closeMenu(); renderRows(); } + }); + + /* ============================================================ + 事件:入口按钮 / 外部点击 / Esc + ============================================================ */ + entryBtn.addEventListener('click', (e) => { + e.stopPropagation(); + setPopoverOpen(!state.popoverOpen); + }); + + popover.addEventListener('click', (e) => { e.stopPropagation(); }); + + document.addEventListener('click', (e) => { + if (state.menuOpenId && !(menuEl && menuEl.contains(e.target))) { + closeMenu(); + renderRows(); + } + if (state.popoverOpen && !popover.contains(e.target) && !entryBtn.contains(e.target)) { + setPopoverOpen(false); + } + }); + + document.addEventListener('keydown', (e) => { + if (e.key !== 'Escape') return; + if (state.menuOpenId) { closeMenu(); renderRows(); return; } + if (state.renamingId || state.deletingId) { state.renamingId = null; state.deletingId = null; renderRows(); return; } + if (state.createOpen) { closeCreateForm(false); return; } + if (state.popoverOpen) setPopoverOpen(false); + }); + + window.addEventListener('resize', () => { + if (state.popoverOpen) positionPopover(); + if (state.menuOpenId) { closeMenu(); renderRows(); } + }); + + /* ============================================================ + Demo 外框:主题切换 / 模式切换 + ============================================================ */ + const themeBtns = document.querySelectorAll('[data-theme-btn]'); + themeBtns.forEach((btn) => { + btn.addEventListener('click', () => { + document.documentElement.setAttribute('data-theme', btn.dataset.themeBtn); + themeBtns.forEach((b) => b.classList.toggle('active', b === btn)); + }); + }); + + const modeBtns = document.querySelectorAll('[data-mode-btn]'); + modeBtns.forEach((btn) => { + btn.addEventListener('click', () => { + const mode = btn.dataset.modeBtn; + if (mode === state.mode) return; + state.mode = mode; + modeBtns.forEach((b) => b.classList.toggle('active', b === btn)); + + /* 切换模式:重置数据与瞬时态 */ + state.workspaces = cloneData(mode); + state.currentId = state.workspaces[0] ? state.workspaces[0].workspace_id : ''; + state.defaultId = state.currentId; + closeTransient(); + if (state.createOpen) closeCreateForm(true); + + /* 文案切换 */ + headerTitle.textContent = noun(); + entryLabel.textContent = noun(); + createBtnText.textContent = '新建' + noun(); + createName.placeholder = isDocker() ? '项目名称' : '工作区名称(可选)'; + createPath.style.display = isDocker() ? 'none' : ''; + popover.setAttribute('aria-label', noun()); + popover.classList.toggle('docker', isDocker()); + + setStatus(isDocker() + ? 'Docker 模式:叫「项目」,不显示路径,「在文件夹中打开」禁用' + : '宿主机模式:显示路径,可在文件夹中打开'); + renderRows(); + positionPopover(); + }); + }); + + /* ---------- 初始化 ---------- */ + renderRows(); + setPopoverOpen(true); +})(); diff --git a/demo/workspace_menu_redesign/index.html b/demo/workspace_menu_redesign/index.html new file mode 100644 index 00000000..5096bb4c --- /dev/null +++ b/demo/workspace_menu_redesign/index.html @@ -0,0 +1,157 @@ + + + + + + 工作区菜单重设计 Demo + + + + +
+
+ 管理工作区菜单 · 重设计 Demo + 对话侧边栏内嵌的工作区切换 + 管理浮层 +
+
+
+ + +
+
+ + + +
+
+
+ +
+ +
+ + + + + + +
点击工作区行可切换;hover 行尾出现「…」菜单
+
+ + + +
+ + + + diff --git a/demo/workspace_menu_redesign/style.css b/demo/workspace_menu_redesign/style.css new file mode 100644 index 00000000..71d67c40 --- /dev/null +++ b/demo/workspace_menu_redesign/style.css @@ -0,0 +1,666 @@ +/* ============================================================ + 工作区菜单重设计 Demo — 样式 + 颜色全部走三主题语义 token(与 static/src/styles/base/_tokens.scss 对齐) + ============================================================ */ + +/* ---------- 主题 token(取自项目真实值) ---------- */ +:root[data-theme='classic'] { + --surface-base: #faf9f5; + --surface-panel: #faf9f5; + --surface-rail: #f5f0e8; + --surface-sidebar: #f5f0e8; + --surface-card: #efe9de; + --surface-raised: #ffffff; + --surface-soft: #f5f0e8; + --surface-muted: #e8e0d2; + --text-primary: #141413; + --text-secondary: #3d3d3a; + --text-tertiary: #6c6a64; + --text-muted: rgba(108, 106, 100, 0.55); + --border-default: #e6dfd8; + --border-strong: #d8cfc4; + --border-card-strong: #e6dfd8; + --accent: #cc785c; + --accent-strong: #a9583e; + --accent-hover: #bd6a4f; + --accent-active: #a9583e; + --on-accent: #ffffff; + --state-success: #10b981; + --state-warning: #f59e0b; + --state-danger: #dc2626; + --state-danger-strong: #b91c1c; + --shadow-pop: 0 1px 2px rgba(60, 50, 40, 0.06), 0 12px 32px rgba(60, 50, 40, 0.1); + --shadow-card: 0 1px 2px rgba(60, 50, 40, 0.05); +} + +:root[data-theme='light'] { + --surface-base: #ffffff; + --surface-panel: #ffffff; + --surface-rail: #f9f9f9; + --surface-sidebar: #f9f9f9; + --surface-card: #ffffff; + --surface-raised: #ffffff; + --surface-soft: #f9f9f9; + --surface-muted: #f3f3f3; + --text-primary: #0d0d0d; + --text-secondary: #5d5d5d; + --text-tertiary: #8f8f8f; + --text-muted: rgba(13, 13, 13, 0.35); + --border-default: rgba(13, 13, 13, 0.1); + --border-strong: rgba(13, 13, 13, 0.15); + --border-card-strong: rgba(13, 13, 13, 0.1); + --accent: #181818; + --accent-strong: #181818; + --accent-hover: #303030; + --accent-active: #414141; + --on-accent: #ffffff; + --state-success: #10b981; + --state-warning: #f59e0b; + --state-danger: #dc2626; + --state-danger-strong: #b91c1c; + --shadow-pop: 0 1px 2px rgba(0, 0, 0, 0.05), 0 12px 32px rgba(0, 0, 0, 0.1); + --shadow-card: 0 1px 2px rgba(0, 0, 0, 0.05); +} + +:root[data-theme='dark'] { + --surface-base: #1a1a1a; + --surface-panel: #1a1a1a; + --surface-rail: #181818; + --surface-sidebar: #181818; + --surface-card: #0a0a0a; + --surface-raised: #242424; + --surface-soft: #0f0f0f; + --surface-muted: #141414; + --text-primary: #ffffff; + --text-secondary: #a0a0a0; + --text-tertiary: #707070; + --text-muted: rgba(255, 255, 255, 0.3); + --border-default: rgba(255, 255, 255, 0.08); + --border-strong: rgba(255, 255, 255, 0.12); + --border-card-strong: rgba(255, 255, 255, 0.1); + --accent: #606060; + --accent-strong: #505050; + --accent-hover: #707070; + --accent-active: #505050; + --on-accent: #ffffff; + --state-success: #10b981; + --state-warning: #f59e0b; + --state-danger: #dc2626; + --state-danger-strong: #b91c1c; + --shadow-pop: 0 1px 2px rgba(0, 0, 0, 0.4), 0 12px 32px rgba(0, 0, 0, 0.5); + --shadow-card: 0 1px 2px rgba(0, 0, 0, 0.35); +} + +/* ---------- 基础 ---------- */ +* { box-sizing: border-box; margin: 0; padding: 0; } + +body { + font-family: -apple-system, BlinkMacSystemFont, 'PingFang SC', 'Segoe UI', 'Helvetica Neue', sans-serif; + background: var(--surface-base); + color: var(--text-primary); + font-size: 13px; + line-height: 1.45; + -webkit-font-smoothing: antialiased; + transition: background 160ms ease, color 160ms ease; +} + +button { + font: inherit; + color: inherit; + background: none; + border: 0; + cursor: pointer; +} + +input { + font: inherit; + color: var(--text-primary); + background: transparent; + border: 0; + outline: none; +} + +svg { display: block; width: 100%; height: 100%; } + +/* ============================================================ + Demo 外框(非产品部分) + ============================================================ */ +.demo-chrome { + display: flex; + align-items: center; + justify-content: space-between; + gap: 16px; + padding: 14px 20px; + border-bottom: 1px solid var(--border-default); +} + +.demo-chrome-title { display: flex; flex-direction: column; gap: 2px; } +.demo-chrome-title strong { font-size: 14px; font-weight: 600; } +.demo-chrome-title span { font-size: 12px; color: var(--text-tertiary); } + +.demo-chrome-controls { display: flex; align-items: center; gap: 10px; } + +.demo-theme-switch { + display: flex; + gap: 2px; + padding: 2px; + border: 1px solid var(--border-default); + border-radius: 8px; + background: var(--surface-soft); +} + +.demo-theme-switch button { + height: 26px; + padding: 0 12px; + border-radius: 6px; + font-size: 12px; + color: var(--text-secondary); +} + +.demo-theme-switch button.active { + background: var(--surface-raised); + color: var(--text-primary); + box-shadow: var(--shadow-card); +} + +.demo-stage { + display: grid; + grid-template-columns: minmax(0, 1fr) 300px; + gap: 24px; + padding: 24px; + align-items: start; +} + +.stage-canvas { + position: relative; + min-height: 560px; + border: 1px solid var(--border-default); + border-radius: 12px; + background: var(--surface-panel); + overflow: hidden; +} + +.stage-status { + position: absolute; + left: 0; + right: 0; + bottom: 0; + height: 34px; + display: flex; + align-items: center; + padding: 0 14px; + border-top: 1px solid var(--border-default); + background: var(--surface-soft); + color: var(--text-tertiary); + font-size: 12px; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +/* ============================================================ + 侧边栏 Mock(紧凑化后的未来形态) + ============================================================ */ +.sidebar-mock { + width: 232px; + height: 526px; /* 560 - 状态条 34 */ + display: flex; + flex-direction: column; + background: var(--surface-sidebar); + border-right: 1px solid var(--border-default); +} + +.side-nav { padding: 8px; display: flex; flex-direction: column; gap: 1px; } + +.side-nav-row { + display: flex; + align-items: center; + gap: 8px; + width: 100%; + height: 30px; /* 紧凑化:原 ~36px */ + padding: 0 8px; + border-radius: 6px; /* 紧凑化:原 ~10px */ + color: var(--text-secondary); + font-size: 12.5px; + text-align: left; +} + +.side-nav-row:hover { background: color-mix(in srgb, var(--text-primary) 5%, transparent); } + +.side-nav-row.active { + background: color-mix(in srgb, var(--text-primary) 7%, transparent); + color: var(--text-primary); +} + +.side-icon { + flex: none; + width: 16px; + height: 16px; + display: flex; + align-items: center; + justify-content: center; +} + +.side-icon svg { width: 15px; height: 15px; } + +.side-label { flex: none; } + +.side-current { + margin-left: auto; + min-width: 0; + max-width: 88px; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + font-size: 11px; + color: var(--text-tertiary); +} + +.side-search { + display: flex; + align-items: center; + gap: 7px; + margin: 0 8px; + height: 28px; + padding: 0 8px; + border: 1px solid var(--border-default); + border-radius: 6px; + color: var(--text-tertiary); +} + +.side-search .side-icon svg { width: 13px; height: 13px; } +.side-search input { flex: 1; min-width: 0; font-size: 12px; } +.side-search input::placeholder { color: var(--text-muted); } + +.side-list { + flex: 1; + min-height: 0; + overflow-y: auto; + padding: 8px; /* 与左右间距一致,hover 圆角四边均匀 */ + display: flex; + flex-direction: column; + gap: 1px; +} + +.conv-row { + display: flex; + align-items: center; + gap: 6px; + height: 27px; /* 紧凑化:原 ~34px */ + padding: 0 8px; + border-radius: 6px; + font-size: 12px; + color: var(--text-secondary); + cursor: pointer; +} + +.conv-row:hover { background: color-mix(in srgb, var(--text-primary) 4%, transparent); } + +.conv-row.active { + background: color-mix(in srgb, var(--text-primary) 7%, transparent); + color: var(--text-primary); +} + +.conv-title { + min-width: 0; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.conv-dot { + flex: none; + width: 5px; + height: 5px; + border-radius: 50%; + background: var(--state-success); +} + +.side-footer { padding: 8px; border-top: 1px solid var(--border-default); } + +/* ============================================================ + 工作区浮层(重设计主体) + ============================================================ */ +.ws-popover { + --ws-row-h: 50px; + position: absolute; + width: 300px; + display: flex; + flex-direction: column; + border: 1px solid var(--border-default); + border-radius: 12px; + background: var(--surface-raised); + box-shadow: var(--shadow-pop); + overflow: hidden; + opacity: 0; + transform: translateY(-4px) scale(0.98); + transform-origin: top left; + pointer-events: none; + transition: opacity 120ms ease, transform 150ms cubic-bezier(0.22, 1, 0.36, 1); +} + +.ws-popover.open { + opacity: 1; + transform: translateY(0) scale(1); + pointer-events: auto; +} + +.ws-popover.docker { --ws-row-h: 38px; } + +/* 头部:安静的类型标题,发丝线分隔 */ +.ws-header { + flex: none; + display: flex; + align-items: center; + justify-content: space-between; + height: 42px; + padding: 0 14px; + border-bottom: 1px solid var(--border-default); +} + +.ws-header-title { font-size: 12.5px; font-weight: 600; color: var(--text-secondary); } +.ws-header-count { font-size: 11px; color: var(--text-tertiary); } + +/* 列表:最多显示 5 个工作区,超出内部滚动;padding 四边一致保证 hover 圆角均匀 */ +.ws-list { + flex: none; + max-height: calc(var(--ws-row-h) * 5 + 16px); /* 5行 + 4条1px间隔 + 上下各6px padding */ + overflow-y: auto; + padding: 6px; + display: flex; + flex-direction: column; + gap: 1px; + scrollbar-width: thin; + scrollbar-color: var(--border-strong) transparent; +} + +.ws-list::-webkit-scrollbar { width: 8px; } +.ws-list::-webkit-scrollbar-thumb { + background: var(--border-strong); + border-radius: 4px; + border: 2px solid var(--surface-raised); +} +.ws-list::-webkit-scrollbar-track { background: transparent; } + +/* 工作区行:可选项,固定高度,宿主机两行 / Docker 单行 */ +.ws-row { + position: relative; + display: flex; + align-items: center; + gap: 10px; + width: 100%; + height: var(--ws-row-h); + padding: 0 8px; + border-radius: 8px; + text-align: left; + cursor: pointer; +} + +.ws-row:hover { background: color-mix(in srgb, var(--text-primary) 4%, transparent); } + +.ws-row.current { background: color-mix(in srgb, var(--text-primary) 6%, transparent); } +.ws-row.current:hover { background: color-mix(in srgb, var(--text-primary) 7%, transparent); } + +.ws-row-icon { + flex: none; + width: 17px; + height: 17px; + margin-top: -13px; /* 与首行文字视觉对齐,而非两行整体居中 */ + color: var(--text-tertiary); +} + +.ws-row.docker .ws-row-icon { margin-top: 0; } + +.ws-row.current .ws-row-icon { color: var(--text-secondary); } + +.ws-row-text { + flex: 1; + min-width: 0; + display: flex; + flex-direction: column; + gap: 1px; +} + +.ws-row-name { + font-size: 13px; + font-weight: 500; + color: var(--text-primary); + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +.ws-row-path { + font-size: 11.5px; + color: var(--text-tertiary); + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +.ws-row-meta { + flex: none; + display: flex; + align-items: center; + gap: 8px; +} + +.ws-tag { + font-size: 11px; + color: var(--text-tertiary); + white-space: nowrap; +} + +/* 行尾「…」:仅 hover / 菜单展开时出现 */ +.ws-more { + flex: none; + width: 24px; + height: 24px; + display: none; + align-items: center; + justify-content: center; + border-radius: 6px; + color: var(--text-tertiary); +} + +.ws-more svg { width: 15px; height: 15px; } +.ws-more:hover { color: var(--text-primary); background: color-mix(in srgb, var(--text-primary) 7%, transparent); } + +.ws-row:hover .ws-more, +.ws-row.menu-open .ws-more { display: flex; } + +/* 「…」二级菜单:fixed 浮层,逃逸浮层裁切(同个人空间二级菜单方案) */ +.ws-menu-floating { + position: fixed; + z-index: 3000; + min-width: 136px; + padding: 4px; + border: 1px solid var(--border-default); + border-radius: 10px; + background: var(--surface-raised); + box-shadow: var(--shadow-pop); + display: none; + flex-direction: column; + opacity: 0; + transform: translateY(-3px) scale(0.98); + transform-origin: top right; + transition: opacity 110ms ease, transform 140ms cubic-bezier(0.22, 1, 0.36, 1); +} + +.ws-menu-floating.open { opacity: 1; transform: translateY(0) scale(1); } + +.ws-menu-floating button { + display: flex; + align-items: center; + width: 100%; + height: 30px; + padding: 0 10px; + border-radius: 6px; + font-size: 12.5px; + color: var(--text-secondary); + text-align: left; + white-space: nowrap; +} + +.ws-menu-floating button:hover { background: color-mix(in srgb, var(--text-primary) 5%, transparent); color: var(--text-primary); } +.ws-menu-floating button.danger { color: var(--state-danger); } +.ws-menu-floating button.danger:hover { background: color-mix(in srgb, var(--state-danger) 8%, transparent); } +.ws-menu-floating button:disabled { color: var(--text-muted); cursor: default; } +.ws-menu-floating button:disabled:hover { background: none; color: var(--text-muted); } + +.ws-menu-sep { height: 1px; margin: 4px 6px; background: var(--border-default); } + +/* 重命名内联输入:替换名称文字 */ +.ws-rename-input { + font-size: 13px; + font-weight: 500; + color: var(--text-primary); + border-bottom: 1px solid var(--accent); + padding: 1px 0; + width: 100%; +} + +/* 删除确认内联:整行内容替换 */ +.ws-confirm { + display: flex; + flex-direction: column; + gap: 5px; + width: 100%; + padding: 2px 0; +} + +.ws-confirm-text { + font-size: 12px; + color: var(--text-secondary); + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +.ws-confirm-actions { display: flex; gap: 8px; } + +/* 通用小按钮 */ +.ws-btn { + height: 26px; + padding: 0 10px; + border-radius: 6px; + font-size: 12px; + border: 1px solid var(--border-strong); + color: var(--text-secondary); + white-space: nowrap; +} + +.ws-btn:hover { background: color-mix(in srgb, var(--text-primary) 5%, transparent); } + +.ws-btn.primary { + background: var(--accent); + border-color: var(--accent); + color: var(--on-accent); +} +.ws-btn.primary:hover { background: var(--accent-hover); } + +.ws-btn.danger { + background: var(--state-danger); + border-color: var(--state-danger); + color: #ffffff; +} +.ws-btn.danger:hover { background: var(--state-danger-strong); } + +.ws-btn.ghost { border-color: var(--border-default); } + +/* 底部区域:新建按钮 ⇄ 新建表单,容器高度动画 */ +.ws-bottom { + flex: none; + border-top: 1px solid var(--border-default); + overflow: hidden; + transition: height 200ms cubic-bezier(0.22, 1, 0.36, 1); +} + +.ws-footer { padding: 6px; transition: opacity 130ms ease; } +.ws-footer.fading { opacity: 0; } + +.ws-create-btn { + display: flex; + align-items: center; + gap: 8px; + width: 100%; + height: 34px; + padding: 0 8px; + border-radius: 8px; + font-size: 12.5px; + color: var(--text-secondary); +} + +.ws-create-btn:hover { background: color-mix(in srgb, var(--text-primary) 4%, transparent); color: var(--text-primary); } + +.ws-create-icon { flex: none; width: 15px; height: 15px; color: var(--text-tertiary); } +.ws-create-btn:hover .ws-create-icon { color: var(--text-secondary); } + +/* 新建表单:内联,平铺无卡片 */ +.ws-create-form { + display: flex; + flex-direction: column; + gap: 8px; + padding: 10px; + transition: opacity 150ms ease; +} +.ws-create-form.fading { opacity: 0; } + +.ws-create-form input { + height: 32px; + padding: 0 10px; + border: 1px solid var(--border-default); + border-radius: 8px; + font-size: 12.5px; + background: var(--surface-soft); +} + +.ws-create-form input:focus { border-color: var(--accent); } +.ws-create-form input::placeholder { color: var(--text-muted); } + +.ws-create-actions { display: flex; justify-content: flex-end; gap: 8px; } + +/* 隐藏互斥切换 */ +.ws-footer[hidden], .ws-create-form[hidden] { display: none; } + +/* ============================================================ + 设计说明栏 + ============================================================ */ +.stage-notes { + border: 1px solid var(--border-default); + border-radius: 12px; + background: var(--surface-soft); + padding: 16px 18px; + font-size: 12.5px; + color: var(--text-secondary); +} + +.stage-notes h2 { + font-size: 12px; + font-weight: 600; + color: var(--text-tertiary); + margin: 14px 0 8px; + letter-spacing: 0.02em; +} + +.stage-notes h2:first-child { margin-top: 0; } + +.stage-notes ul { list-style: none; display: flex; flex-direction: column; gap: 7px; } +.stage-notes li { padding-left: 12px; position: relative; line-height: 1.55; } +.stage-notes li::before { + content: ''; + position: absolute; + left: 0; + top: 8px; + width: 4px; + height: 4px; + border-radius: 50%; + background: var(--text-muted); +} +.stage-notes li b { color: var(--text-primary); font-weight: 600; } + +.notes-compare li { color: var(--text-tertiary); } + +@media (max-width: 980px) { + .demo-stage { grid-template-columns: 1fr; } +} diff --git a/static/src/App.vue b/static/src/App.vue index ad027864..3e1d8757 100644 --- a/static/src/App.vue +++ b/static/src/App.vue @@ -66,11 +66,13 @@ :current-task-in-progress="taskInProgress" :current-conversation-id="currentConversationId" :current-workspace-id="currentHostWorkspaceId" - :display-mode="chatDisplayMode" - :display-mode-disabled="displayModeSwitchDisabled" + :default-workspace-id="defaultHostWorkspaceId" :workspaces="hostWorkspaces" :workspace-kind="versioningHostMode ? 'workspace' : 'project'" :host-workspace-enabled="versioningHostMode || dockerProjectMode" + :workspace-busy="hostWorkspaceSwitching || hostWorkspaceManageSubmitting" + :workspace-create-submitting="hostWorkspaceCreateSubmitting" + :workspace-error="hostWorkspaceCreateError" :group-by-workspace="personalizationStore?.form?.group_sidebar_by_workspace" :versioning-host-mode="versioningHostMode" @toggle="toggleSidebar" @@ -84,8 +86,10 @@ @personal="openPersonalPage" @delete="deleteConversation" @duplicate="duplicateConversation" - @toggle-workspace="handleWorkspaceToggle" - @toggle-display-mode="handleDisplayModeToggle" + @switch-workspace="handleHostWorkspaceSwitch" + @create-workspace="submitHostWorkspaceCreateFromManage" + @delete-workspace="handleDeleteWorkspaceFromSwitcher" + @set-default-workspace="setDefaultHostWorkspace" @select-workspace-conversation="handleSelectWorkspaceConversation" @create-workspace-conversation="createWorkspaceConversation" @reveal-workspace="revealHostWorkspace" @@ -93,47 +97,9 @@ @pin-workspace="handlePinWorkspaceFromSidebar" /> -
- - -
-
-
- - - - - -
对话记录
- - -
- - -
{ }); const mobileMenuIcons = { - workspace: new URL('../icons/folder.svg', import.meta.url).href, personal: new URL('../icons/user.svg', import.meta.url).href, newChat: new URL('../icons/pencil.svg', import.meta.url).href, refresh: new URL('../icons/refresh-cw.svg', import.meta.url).href diff --git a/static/src/app.ts b/static/src/app.ts index ccaf8d17..58a59a33 100644 --- a/static/src/app.ts +++ b/static/src/app.ts @@ -56,12 +56,7 @@ const appOptions = { ...mapActions(useUiStore, { uiToggleSidebar: 'toggleSidebar', uiSetSidebarCollapsed: 'setSidebarCollapsed', - uiSetWorkspaceCollapsed: 'setWorkspaceCollapsed', - uiToggleWorkspaceCollapsed: 'toggleWorkspaceCollapsed', uiSetChatDisplayMode: 'setChatDisplayMode', - uiSetPanelMode: 'setPanelMode', - uiSetPanelMenuOpen: 'setPanelMenuOpen', - uiTogglePanelMenu: 'togglePanelMenu', uiSetMobileViewport: 'setIsMobileViewport', uiSetMobileOverlayMenuOpen: 'setMobileOverlayMenuOpen', uiToggleMobileOverlayMenu: 'toggleMobileOverlayMenu', @@ -186,14 +181,10 @@ const appOptions = { forceUnlockMonitor: 'resetVisualState' }), ...mapActions(useSubAgentStore, { - subAgentFetch: 'fetchSubAgents', - subAgentStartPolling: 'startPolling', - subAgentStopPolling: 'stopPolling' + subAgentFetch: 'fetchSubAgents' }), ...mapActions(useBackgroundCommandStore, { - backgroundCommandFetch: 'fetchCommands', - backgroundCommandStartPolling: 'startPolling', - backgroundCommandStopPolling: 'stopPolling' + backgroundCommandFetch: 'fetchCommands' }), ...mapActions(useFocusStore, { focusFetchFiles: 'fetchFocusedFiles', diff --git a/static/src/app/components.ts b/static/src/app/components.ts index 7f792454..41c65390 100644 --- a/static/src/app/components.ts +++ b/static/src/app/components.ts @@ -1,7 +1,6 @@ import { defineAsyncComponent } from 'vue'; import ChatArea from '../components/chat/ChatArea.vue'; import ConversationSidebar from '../components/sidebar/ConversationSidebar.vue'; -import LeftPanel from '../components/panels/LeftPanel.vue'; import ToolApprovalPanel from '../components/panels/ToolApprovalPanel.vue'; import GitChangesPanel from '../components/panels/GitChangesPanel.vue'; import TerminalPanel from '../components/panels/TerminalPanel.vue'; @@ -28,12 +27,6 @@ const BackgroundCommandDialog = defineAsyncComponent( const VersioningDialog = defineAsyncComponent( () => import('../components/overlay/VersioningDialog.vue') ); -const HostWorkspaceCreateDialog = defineAsyncComponent( - () => import('../components/overlay/HostWorkspaceCreateDialog.vue') -); -const HostWorkspaceManageDialog = defineAsyncComponent( - () => import('../components/overlay/HostWorkspaceManageDialog.vue') -); const UserQuestionDialog = defineAsyncComponent( () => import('../components/overlay/UserQuestionDialog.vue') ); @@ -50,7 +43,6 @@ const GoalProgressDialog = defineAsyncComponent( export const appComponents = { ChatArea, ConversationSidebar, - LeftPanel, ToolApprovalPanel, GitChangesPanel, TerminalPanel, @@ -66,8 +58,6 @@ export const appComponents = { SubAgentActivityDialog, BackgroundCommandDialog, VersioningDialog, - HostWorkspaceCreateDialog, - HostWorkspaceManageDialog, UserQuestionDialog, TutorialOverlay, NewUserTutorialPrompt, diff --git a/static/src/app/computed.ts b/static/src/app/computed.ts index 2ccea369..a501891f 100644 --- a/static/src/app/computed.ts +++ b/static/src/app/computed.ts @@ -67,11 +67,7 @@ export const computed = { ...mapState(useFileStore, ['contextMenu', 'fileTree', 'expandedFolders', 'todoList']), ...mapWritableState(useUiStore, [ 'sidebarCollapsed', - 'workspaceCollapsed', 'chatDisplayMode', - 'panelMode', - 'panelMenuOpen', - 'leftWidth', 'rightWidth', 'rightCollapsed', 'isResizing', diff --git a/static/src/app/lifecycle.ts b/static/src/app/lifecycle.ts index cfdc1a0b..132c7e31 100644 --- a/static/src/app/lifecycle.ts +++ b/static/src/app/lifecycle.ts @@ -70,7 +70,6 @@ export async function mounted() { } document.addEventListener('click', this.handleClickOutsideQuickMenu); - document.addEventListener('click', this.handleClickOutsidePanelMenu); document.addEventListener('click', this.handleClickOutsideHeaderMenu); document.addEventListener('click', this.handleClickOutsideMobileMenu); document.addEventListener('click', this.handleCopyCodeClick); @@ -80,9 +79,7 @@ export async function mounted() { this.setupMobileViewportWatcher(); this.subAgentFetch(); - this.subAgentStartPolling(); this.backgroundCommandFetch(); - this.backgroundCommandStartPolling(); this.$nextTick(() => { this.autoResizeInput(); @@ -112,7 +109,6 @@ export function beforeUnmount() { } document.removeEventListener('click', this.handleClickOutsideQuickMenu); - document.removeEventListener('click', this.handleClickOutsidePanelMenu); document.removeEventListener('click', this.handleClickOutsideHeaderMenu); document.removeEventListener('click', this.handleClickOutsideMobileMenu); document.removeEventListener('click', this.handleCopyCodeClick); @@ -120,8 +116,6 @@ export function beforeUnmount() { window.removeEventListener('keydown', this.handleMobileOverlayEscape); window.removeEventListener('beforeunload', this.handleBeforeUnloadDraftPersist); this.teardownMobileViewportWatcher(); - this.subAgentStopPolling(); - this.backgroundCommandStopPolling(); this.stopProjectGitSummaryIdleRefresh?.(); this.stopTerminalCountIdleRefresh?.(); this.resourceStopContainerStatsPolling(); diff --git a/static/src/app/methods/ui/dialog.ts b/static/src/app/methods/ui/dialog.ts index ab7e5262..f8fb3f92 100644 --- a/static/src/app/methods/ui/dialog.ts +++ b/static/src/app/methods/ui/dialog.ts @@ -30,20 +30,6 @@ export const dialogMethods = { } this.personalizationOpenDrawer(); }, - closeHostWorkspaceManageDialog() { - if (this.hostWorkspaceCreateSubmitting || this.hostWorkspaceManageSubmitting) { - return; - } - this.hostWorkspaceManageDialogOpen = false; - this.hostWorkspaceCreateError = ''; - }, - closeHostWorkspaceCreateDialog() { - if (this.hostWorkspaceCreateSubmitting) { - return; - } - this.hostWorkspaceCreateDialogOpen = false; - this.hostWorkspaceCreateError = ''; - }, minimizeUserQuestionDialog() { if (!Array.isArray(this.pendingUserQuestions) || !this.pendingUserQuestions.length) { return; diff --git a/static/src/app/methods/ui/hostWorkspace.ts b/static/src/app/methods/ui/hostWorkspace.ts index 94e4e21b..5a0a5d59 100644 --- a/static/src/app/methods/ui/hostWorkspace.ts +++ b/static/src/app/methods/ui/hostWorkspace.ts @@ -154,26 +154,6 @@ export const hostWorkspaceMethods = { this.hostWorkspaceSwitching = false; } }, - async handleCreateHostWorkspace() { - if (!(this.versioningHostMode || this.dockerProjectMode)) { - return; - } - if (this.hostWorkspaceSwitching || this.hostWorkspaceCreateSubmitting) { - return; - } - this.hostWorkspaceCreatePath = ''; - this.hostWorkspaceCreateLabel = ''; - this.hostWorkspaceCreateError = ''; - this.hostWorkspaceCreateDialogOpen = true; - }, - async openHostWorkspaceManageDialog() { - if (!(this.versioningHostMode || this.dockerProjectMode)) { - return; - } - this.hostWorkspaceCreateError = ''; - await this.fetchHostWorkspaces(); - this.hostWorkspaceManageDialogOpen = true; - }, async submitHostWorkspaceCreate() { if (!(this.versioningHostMode || this.dockerProjectMode)) { return; @@ -279,7 +259,11 @@ export const hostWorkspaceMethods = { this.hostWorkspaceManageSubmitting = false; } }, - async deleteHostWorkspace(item) { + async handleDeleteWorkspaceFromSwitcher(item) { + /* 切换浮层已内联确认,跳过全局确认弹窗 */ + await this.deleteHostWorkspace(item, { skipConfirm: true }); + }, + async deleteHostWorkspace(item, opts = {}) { if (!(this.versioningHostMode || this.dockerProjectMode)) { return; } @@ -287,7 +271,7 @@ export const hostWorkspaceMethods = { if (!workspaceId) return; const kindLabel = this.versioningHostMode ? '工作区' : '项目'; const wasCurrent = workspaceId === this.currentHostWorkspaceId; - const ok = await this.confirmAction({ + const ok = opts.skipConfirm ? true : await this.confirmAction({ title: `删除${kindLabel}`, message: this.versioningHostMode ? `确定要从列表中删除“${item?.label || workspaceId}”吗?不会删除磁盘上的工作区文件夹。` diff --git a/static/src/app/methods/ui/menu.ts b/static/src/app/methods/ui/menu.ts index 7b1cbc31..3e2a067c 100644 --- a/static/src/app/methods/ui/menu.ts +++ b/static/src/app/methods/ui/menu.ts @@ -75,16 +75,5 @@ export const menuMethods = { return; } this.closeHeaderMenu(); - }, - handleClickOutsidePanelMenu(event) { - if (!this.panelMenuOpen) { - return; - } - const leftPanel = this.$refs.leftPanel; - const wrapper = leftPanel && leftPanel.panelMenuWrapper ? leftPanel.panelMenuWrapper : null; - if (wrapper && wrapper.contains(event.target)) { - return; - } - this.uiSetPanelMenuOpen(false); } }; diff --git a/static/src/app/methods/ui/panel.ts b/static/src/app/methods/ui/panel.ts index b9a6aa73..39d986cc 100644 --- a/static/src/app/methods/ui/panel.ts +++ b/static/src/app/methods/ui/panel.ts @@ -31,24 +31,6 @@ export const panelMethods = { } this.uiToggleSidebar(); }, - togglePanelMenu() { - this.uiTogglePanelMenu(); - }, - selectPanelMode(mode) { - this.uiSetPanelMode(mode); - this.uiSetPanelMenuOpen(false); - if (mode === 'subAgents') { - // 切换到子智能体面板时立即刷新,避免等待轮询间隔 - this.subAgentFetch(); - this.subAgentStartPolling(); - return; - } - if (mode === 'backgroundCommands') { - // 切换到后台指令面板时立即刷新,避免等待轮询间隔 - this.backgroundCommandFetch(); - this.backgroundCommandStartPolling(); - } - }, toggleFocusPanel() { this.rightCollapsed = !this.rightCollapsed; if (!this.rightCollapsed && this.rightWidth < this.minPanelWidth) { @@ -138,35 +120,6 @@ export const panelMethods = { console.log('[UI_DEBUG] handleTokenPanelToggleClick: calling toggleTokenPanel'); this.toggleTokenPanel(); }, - handleWorkspaceToggle() { - if (this.isMobileViewport) { - return; - } - if (this.isPolicyBlocked('collapse_workspace', '工作区已被管理员强制折叠')) { - this.uiSetWorkspaceCollapsed(true); - return; - } - const nextState = !this.workspaceCollapsed; - this.uiSetWorkspaceCollapsed(nextState); - if (nextState) { - this.uiSetPanelMenuOpen(false); - } - }, - handleDisplayModeToggle() { - if (this.displayModeSwitchDisabled) { - // 显式提示管理员禁用 - this.isPolicyBlocked('block_virtual_monitor', '虚拟显示器已被管理员禁用'); - return; - } - if ( - this.chatDisplayMode === 'chat' && - this.isPolicyBlocked('block_virtual_monitor', '虚拟显示器已被管理员禁用') - ) { - return; - } - const next = this.chatDisplayMode === 'chat' ? 'monitor' : 'chat'; - this.uiSetChatDisplayMode(next); - }, getMessagesAreaElement() { const ref = this.$refs.messagesArea; if (!ref) { diff --git a/static/src/app/methods/ui/socket.ts b/static/src/app/methods/ui/socket.ts index e8737f8d..aa200265 100644 --- a/static/src/app/methods/ui/socket.ts +++ b/static/src/app/methods/ui/socket.ts @@ -1,5 +1,6 @@ // @ts-nocheck import { debugLog } from '../common'; +import { persistWorkspaceMode } from '../../state'; import { usePolicyStore } from '../../../stores/policy'; import { useModelStore } from '../../../stores/model'; import { usePersonalizationStore } from '../../../stores/personalization'; @@ -307,13 +308,13 @@ export const socketMethods = { const isHostMode = statusData?.container?.mode === 'host'; this.versioningHostMode = !!isHostMode; this.dockerProjectMode = !isHostMode; + persistWorkspaceMode(!!isHostMode); if (isHostMode) { this.fileMarkTreeUnavailable('宿主机模式下文件树不可用'); await this.fetchHostWorkspaces(); } else { this.fileMarkTreeUnavailable('Docker 模式下文件区已改为项目列表'); await this.fetchHostWorkspaces(); - this.hostWorkspaceCreateDialogOpen = false; this.hostWorkspaceCreatePath = ''; this.hostWorkspaceCreateLabel = ''; this.hostWorkspaceCreateError = ''; diff --git a/static/src/app/methods/versioning.ts b/static/src/app/methods/versioning.ts index e89db972..688c6740 100644 --- a/static/src/app/methods/versioning.ts +++ b/static/src/app/methods/versioning.ts @@ -1,5 +1,6 @@ // @ts-nocheck import { debugLog } from './common'; +import { persistWorkspaceMode } from '../state'; const normalizeTrackingMode = (value: any): 'workspace_and_conversation' | 'conversation_only' => { return String(value || '').toLowerCase() === 'conversation_only' @@ -23,6 +24,7 @@ export const versioningMethods = { } const payload = data.data || {}; this.versioningHostMode = !!payload.host_mode; + persistWorkspaceMode(!!payload.host_mode); this.versioningEnabled = !!payload.enabled; this.versioningMode = 'overwrite'; this.versioningRestoreMode = 'overwrite'; diff --git a/static/src/app/state.ts b/static/src/app/state.ts index 7a20972d..94626573 100644 --- a/static/src/app/state.ts +++ b/static/src/app/state.ts @@ -1,6 +1,30 @@ // @ts-nocheck import { ICONS, TOOL_CATEGORY_ICON_MAP } from '../utils/icons'; +/* host/docker 模式缓存:模式是部署级属性几乎不变,首屏用缓存值渲染 + 工作区入口按钮(按钮显隐依赖这两个 flag),socket 连接后校正并回写, + 避免按钮在初始化完成前「延迟零点几秒才出现」。 */ +const WORKSPACE_MODE_STORAGE_KEY = 'agents_workspace_mode'; + +const loadCachedWorkspaceMode = (): 'host' | 'docker' | null => { + try { + const v = window.localStorage.getItem(WORKSPACE_MODE_STORAGE_KEY); + return v === 'host' || v === 'docker' ? v : null; + } catch { + return null; + } +}; + +export const persistWorkspaceMode = (isHostMode: boolean): void => { + try { + window.localStorage.setItem(WORKSPACE_MODE_STORAGE_KEY, isHostMode ? 'host' : 'docker'); + } catch { + /* ignore */ + } +}; + +const cachedWorkspaceMode = loadCachedWorkspaceMode(); + export function dataState() { return { // 路由相关 @@ -150,18 +174,16 @@ export function dataState() { pathAuthorizationReadableDraft: '', pathAuthorizationDraft: '', pathAuthorizationSaving: false, - versioningHostMode: false, - dockerProjectMode: false, + versioningHostMode: cachedWorkspaceMode === 'host', + dockerProjectMode: cachedWorkspaceMode === 'docker', hostWorkspaces: [], currentHostWorkspaceId: '', defaultHostWorkspaceId: '', hostWorkspaceSwitching: false, - hostWorkspaceCreateDialogOpen: false, hostWorkspaceCreatePath: '', hostWorkspaceCreateLabel: '', hostWorkspaceCreateSubmitting: false, hostWorkspaceCreateError: '', - hostWorkspaceManageDialogOpen: false, hostWorkspaceManageSubmitting: false, versioningEnabled: false, versioningTrackingMode: 'conversation_only', diff --git a/static/src/components/input/InputComposer.vue b/static/src/components/input/InputComposer.vue index c4c11261..9fdca406 100644 --- a/static/src/components/input/InputComposer.vue +++ b/static/src/components/input/InputComposer.vue @@ -364,6 +364,7 @@ :multi-agent-mode="multiAgentMode" :has-running-task="hasRunningTask" @quick-upload="triggerQuickUpload" + @open-file-manager="$emit('open-file-manager')" @pick-images="$emit('pick-images')" @pick-video="$emit('pick-video')" @toggle-tool-menu="$emit('toggle-tool-menu')" @@ -532,6 +533,7 @@ const emit = defineEmits([ 'send-message', 'send-or-stop', 'quick-upload', + 'open-file-manager', 'pick-images', 'pick-video', 'toggle-tool-menu', @@ -2432,7 +2434,7 @@ const hasComposerContent = computed(() => { const showStopIcon = computed(() => { // 停止按钮只看主对话是否在 streaming。后台任务(子智能体/后台指令)的停止 - // 有独立的“x 个子智能体运行中”按钮和 LeftPanel 后台指令停止按钮,不再集成在停止按钮上。 + // 有独立的“x 个子智能体运行中”按钮和快捷窗口里的后台指令停止入口,不再集成在停止按钮上。 return !!props.streamingMessage && !hasComposerContent.value; }); diff --git a/static/src/components/input/QuickMenu.vue b/static/src/components/input/QuickMenu.vue index ec67c7cc..90b57823 100644 --- a/static/src/components/input/QuickMenu.vue +++ b/static/src/components/input/QuickMenu.vue @@ -1,6 +1,7 @@