const projects = [ { id: 'buddy', name: 'GoodBuddy', kind: 'local', location: '本地工作空间' }, { id: 'web', name: '品牌官网', kind: 'local', location: '本地工作空间' }, { id: 'data', name: '数据分析', kind: 'remote', host: 'dev-server', location: '远程 · dev-server' }, { id: 'api', name: '后端服务', kind: 'remote', host: 'dev-server', location: '远程 · dev-server' }, { id: 'lab', name: '模型实验', kind: 'remote', host: 'gpu-lab', location: '远程 · gpu-lab' }, { id: 'channel', name: '产品讨论群', kind: 'channel', location: '消息通道' } ]; const hosts = [{ name: 'dev-server', status: '就绪' }, { name: 'gpu-lab', status: '未连接' }]; for (let index = 3; index <= 20; index++) { projects.push({ id: `local-${index}`, name: `本地项目 ${String(index).padStart(2, '0')}`, kind: 'local', location: '本地工作空间' }); } for (let index = 4; index <= 20; index++) { const host = index % 2 ? 'gpu-lab' : 'dev-server'; projects.push({ id: `remote-${index}`, name: `远程项目 ${String(index).padStart(2, '0')}`, kind: 'remote', host, location: `远程 · ${host}` }); } projects.push( { id: 'channel-2', name: '研发协作群', kind: 'channel', location: '消息通道' }, { id: 'channel-3', name: '运维通知群', kind: 'channel', location: '消息通道' } ); const icons = { all: '', local: '', remote: '', channel: '', folder: '' }; const icon = (name) => ``; for (const project of projects) { project.rootPath = project.kind === 'remote' ? `/srv/projects/${project.id}` : `D:/workspace/${project.id}`; project.description = ''; project.defaultMode = 'ask'; project.runtime = 'OpenCode'; } const initialProjects = structuredClone(projects); const initial = [ { id: 1, project: 'buddy', title: '工作空间交互设计', state: 'idle', detail: '设计讨论已保存,随时可以继续。', prompt: '让项目与活动会话的关系更直观,做出操作系统的感觉。', answer: '左上角就是工作入口。\n\n展开项目与活动菜单,可以查看多个项目正在进行的会话,也可以直接处理等待你的工作。选择项目只筛选列表,进入会话才切换工作现场。\n\n所有状态都在这个下拉菜单中,不需要再打开独立页面。' }, { id: 2, project: 'buddy', title: '登录流程调整', state: 'waiting', detail: '需要你选择:使用单点登录还是邮箱登录?', prompt: '帮我完善内部用户的登录流程。', answer: '已梳理现有认证入口。需要确定首选登录方式,然后继续调整界面。', question: true }, { id: 3, project: 'buddy', title: '导航组件测试', state: 'running', detail: '正在检查键盘导航与焦点恢复。', prompt: '检查导航组件的交互和测试覆盖。', answer: '已经完成组件检查,正在验证键盘导航与焦点恢复。' }, { id: 4, project: 'web', title: '发布新版首页', state: 'waiting', detail: '预览构建已就绪,等待你批准部署。', prompt: '构建新版首页,发布前让我确认。', answer: '预览构建已完成,页面检查通过。请确认是否继续部署。' }, { id: 5, project: 'web', title: '首页文案精修', state: 'done', detail: '完成 3 处文案调整,结果等待查看。', prompt: '把首页介绍改得更简洁、自然。', answer: '首页文案调整已完成。\n\n首屏标题:把想法,变成已完成的工作。\n产品介绍:查资料、写方案、改代码。在一个工作空间里,与 AI 一起推进。\n行动按钮:开始你的第一项工作。' }, { id: 6, project: 'data', title: '本周使用情况分析', state: 'running', detail: '正在汇总各项目的使用趋势。', prompt: '整理本周各项目的使用情况。', answer: '已完成示例数据整理,正在汇总各项目的使用趋势。' }, { id: 7, project: 'api', title: '接口变更记录', state: 'idle', detail: '昨天的会话', prompt: '整理本次接口变更。', answer: '接口变更记录已整理。' }, { id: 8, project: 'lab', title: '推理参数对比', state: 'idle', detail: '上次会话', prompt: '对比两组推理参数。', answer: '上次对比结果已保存。' }, { id: 9, project: 'channel', title: '版本发布讨论', state: 'idle', detail: '今天的会话', prompt: '整理群里的版本讨论。', answer: '本轮发布讨论已整理。' } ]; for (const project of projects) { if (!initial.some((session) => session.project === project.id)) { initial.push({ id: initial.length + 1, project: project.id, title: '最近工作记录', state: 'idle', detail: '上次会话', prompt: '整理当前工作。', answer: '工作记录已保存。' }); } } const labels = { waiting: '待处理', running: '运行中', done: '已完成' }; const symbols = { waiting: '!', running: '◌', done: '✓' }; const statusLabel = (session) => session.state === 'waiting' ? session.question ? '等待你的回答' : '等待审批' : labels[session.state] || ''; const $ = (id) => document.getElementById(id); const escapeHtml = (value) => String(value).replace(/[&<>"']/g, (c) => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' })[c]); let sessions, current, scope = 'all', filter = 'all', projectTab = 'local', toastTimer, menuTrigger, activityOrder, settingsProjectId; const remembered = {}; const drafts = {}; const overview = $('overview'); const projectOf = (session) => projects.find((p) => p.id === session.project); const badge = (state) => `${symbols[state]} ${labels[state]}`; function notify(text) { $('toast').textContent = text; $('toast').hidden = false; clearTimeout(toastTimer); toastTimer = setTimeout(() => { $('toast').hidden = true; }, 3500); } function render() { const session = sessions.find((s) => s.id === current); const project = projectOf(session); $('project-name').textContent = project.name; $('breadcrumb').textContent = `${project.name} / ${session.title}`; $('activity-trigger').innerHTML = '全部' + ['waiting', 'running', 'done'].filter((state) => sessions.some((s) => s.state === state)).map((state) => `${symbols[state]} ${sessions.filter((s) => s.state === state).length} ${labels[state]}`).join(''); $('activity-trigger').hidden = !sessions.some((s) => labels[s.state]); $('sessions').innerHTML = sessions.filter((s) => s.project === session.project).map((s) => ``).join(''); $('conversation').innerHTML = `${escapeHtml(project.location)}${statusLabel(session) ? ` / ${statusLabel(session)}` : ''}

${escapeHtml(session.title)}

${escapeHtml(session.prompt)}
G / GoodBuddy
${escapeHtml(session.answer)}
${session.state === 'waiting' ? `
! ${statusLabel(session)}

${escapeHtml(session.detail)}

${session.question ? ' ' : ' '}
` : session.state === 'running' ? `
${badge('running')}

${escapeHtml(session.detail)} 切换到其他工作空间后仍会继续。

` : ''}`; $('simulate').disabled = !sessions.some((s) => s.state === 'running'); renderOverview(); } function renderOverview() { const focused = document.activeElement; const focusScope = focused?.dataset.scope; const focusFilter = focused?.dataset.filter; const projectButton = (p) => { const list = sessions.filter((s) => p.id === 'all' || s.project === p.id); const counts = ['waiting', 'running', 'done'].filter((state) => list.some((s) => s.state === state)).map((state) => `${symbols[state]} ${list.filter((s) => s.state === state).length} 个${labels[state]}`).join(' '); const active = sessions.find((s) => s.id === current).project === p.id; const detail = p.kind === 'local' ? `本地目录 · ${p.rootPath}` : p.kind === 'channel' ? `消息通道 · ${p.rootPath}` : p.rootPath; return `
`; }; const query = $('project-search').value.trim().toLocaleLowerCase(); const kinds = { local: '本地', remote: '远程', channel: '消息通道' }; if (!$('project-tabs').children.length) { $('project-tabs').innerHTML = Object.entries(kinds).map(([kind, label]) => ``).join(''); } $('project-tabs').hidden = Boolean(query); $('project-tabs').querySelectorAll('[data-tab]').forEach((button) => { const selected = button.dataset.tab === projectTab; button.setAttribute('aria-selected', String(selected)); button.tabIndex = selected ? 0 : -1; button.querySelector('small').textContent = projects.filter((p) => p.kind === button.dataset.tab).length; }); $('desktops').setAttribute('aria-label', query ? '搜索结果' : kinds[projectTab]); $('all-projects').setAttribute('aria-pressed', String(scope === 'all')); const shown = projects.filter((p) => query ? `${p.name} ${p.rootPath} ${p.host || ''} ${kinds[p.kind]}`.toLocaleLowerCase().includes(query) : p.kind === projectTab); const projectMarkup = query ? shown.map((p) => `
${projectButton(p)}${kinds[p.kind]}${p.host ? ` · ${escapeHtml(p.host)}` : ''}
`).join('') : projectTab === 'remote' ? hosts.map((host) => `
${host.name}${host.status}${shown.filter((p) => p.host === host.name).map(projectButton).join('')}
`).join('') : shown.map(projectButton).join(''); // Keep hovered project nodes stable while the right-hand list changes. if ($('desktops').dataset.markup !== projectMarkup) { $('desktops').innerHTML = projectMarkup || '
无匹配项目
'; $('desktops').dataset.markup = projectMarkup; } $('desktops').querySelectorAll('[data-scope]').forEach((button) => { button.setAttribute('aria-pressed', String(button.dataset.scope === scope)); }); const list = sessions.filter((s) => scope === 'all' || s.project === scope); $('filters').innerHTML = ['all', 'waiting', 'running', 'done'].map((state) => ``).join(''); const targetProject = projects.find((p) => p.id === (scope === 'all' ? sessions.find((s) => s.id === current).project : scope)); $('menu-new-chat').title = `在 ${targetProject.name} 中新建会话`; $('menu-new-chat').setAttribute('aria-label', $('menu-new-chat').title); const groups = filter === 'all' ? ['waiting', 'running', 'recent'] : [filter]; $('windows').innerHTML = groups.map((state) => { const sorted = list.filter((s) => state === 'recent' ? s.state === 'done' || s.state === 'idle' : s.state === state) .sort((a, b) => b.lastActivity - a.lastActivity); const group = state === 'recent' ? sorted.slice(0, 5) : sorted; if (!group.length) return ''; return `

${state === 'recent' ? '最近会话' : badge(state)} ${group.length}

${group.map((s) => ``).join('')}
`; }).join('') || '
暂无会话
'; if (focusScope) overview.querySelector(`[data-scope="${focusScope}"]`)?.focus(); if (focusFilter) overview.querySelector(`[data-filter="${focusFilter}"]`)?.focus(); } function openSession(id) { const session = sessions.find((s) => s.id === id); drafts[current] = $('message').value; current = id; session.lastActivity = ++activityOrder; remembered[session.project] = id; if (session.state === 'done') { session.state = 'idle'; session.detail = '结果已查看,可以继续这项工作。'; } closeMenu(); $('message').value = drafts[id] || ''; render(); $('conversation').scrollTop = 0; } function positionMenu() { const anchor = document.querySelector('.workspace-control').getBoundingClientRect(); const top = Math.max(8, Math.min(anchor.bottom + 8, window.innerHeight - 240)); overview.style.left = `${Math.max(8, Math.min(anchor.left, window.innerWidth - overview.offsetWidth - 8))}px`; overview.style.top = `${top}px`; overview.style.maxHeight = `${window.innerHeight - top - 8}px`; } function closeMenu() { if (overview.matches(':popover-open')) overview.hidePopover(); } function showOverview(project = 'all', trigger = $('workspace-trigger')) { if (overview.matches(':popover-open')) { closeMenu(); return; } menuTrigger = trigger; scope = project; projectTab = projects.find((p) => p.id === (project === 'all' ? sessions.find((s) => s.id === current).project : project)).kind; $('project-search').value = ''; filter = 'all'; $('project-form').hidden = true; $('new-project').setAttribute('aria-expanded', 'false'); renderOverview(); overview.showPopover(); positionMenu(); overview.querySelector('[data-scope][aria-pressed="true"]').focus(); } document.addEventListener('click', (event) => { const button = event.target.closest('button'); if (!button) return; if (button.dataset.settings) { settingsProjectId = button.dataset.settings; const project = projects.find((p) => p.id === settingsProjectId); $('settings-name').value = project.name; $('settings-name').readOnly = project.kind === 'channel'; $('settings-description').value = project.description || ''; $('settings-space').value = project.location; $('settings-path').value = project.rootPath; $('settings-mode').value = project.defaultMode || 'ask'; $('settings-runtime').value = project.runtime || 'OpenCode'; $('settings-runtime').querySelector('option:last-child').disabled = project.kind === 'remote'; closeMenu(); $('project-settings').showModal(); } if (button.dataset.tab) { projectTab = button.dataset.tab; renderOverview(); $('desktops').scrollTop = 0; } if (button.dataset.open) openSession(Number(button.dataset.open)); if (button.dataset.scope === 'all') { scope = 'all'; renderOverview(); } else if (button.dataset.scope) { const project = button.dataset.scope; const session = sessions.find((s) => s.id === remembered[project]) || sessions.filter((s) => s.project === project).sort((a, b) => b.lastActivity - a.lastActivity)[0]; if (session) openSession(session.id); else createSession(project); } if (button.dataset.filter) { filter = button.dataset.filter; renderOverview(); } if (button.dataset.resolve) { const session = sessions.find((s) => s.id === current); const declined = button.dataset.resolve === '暂不部署'; session.state = declined ? 'idle' : 'running'; session.lastActivity = ++activityOrder; session.detail = declined ? '已暂缓部署,等待后续安排。' : `已确认${button.dataset.resolve},正在继续执行。`; session.answer = session.detail; render(); notify(declined ? '已暂缓本次部署' : '已处理,工作继续进行'); } }); $('desktops').addEventListener('pointerover', (event) => { if (event.pointerType === 'touch') return; const button = event.target.closest('[data-scope]'); if (!button || button.dataset.scope === scope) return; scope = button.dataset.scope; renderOverview(); }); $('project-search').addEventListener('input', () => { renderOverview(); $('desktops').scrollTop = 0; }); $('project-tabs').addEventListener('keydown', (event) => { const tabs = [...$('project-tabs').querySelectorAll('[data-tab]')]; const index = tabs.indexOf(document.activeElement); if (index < 0 || !['ArrowLeft', 'ArrowRight', 'Home', 'End'].includes(event.key)) return; event.preventDefault(); const next = event.key === 'Home' ? 0 : event.key === 'End' ? tabs.length - 1 : (index + (event.key === 'ArrowRight' ? 1 : -1) + tabs.length) % tabs.length; tabs[next].focus(); tabs[next].click(); }); $('settings-form').onsubmit = (event) => { event.preventDefault(); const project = projects.find((p) => p.id === settingsProjectId); const name = $('settings-name').value.trim(); const path = $('settings-path').value.trim(); if (!name || !path) return; if (project.kind !== 'channel') project.name = name; project.rootPath = path; project.description = $('settings-description').value; project.defaultMode = $('settings-mode').value; project.runtime = $('settings-runtime').value; render(); $('project-settings').close(); }; $('close-settings').onclick = $('cancel-settings').onclick = () => $('project-settings').close(); $('project-settings').addEventListener('close', () => { showOverview(settingsProjectId); overview.querySelector(`[data-settings="${settingsProjectId}"]`)?.focus(); }); $('workspace-trigger').onclick = () => showOverview(sessions.find((s) => s.id === current).project); $('activity-trigger').onclick = () => showOverview('all', $('activity-trigger')); $('close-overview').onclick = () => { closeMenu(); menuTrigger?.focus(); }; $('simulate').onclick = () => { const session = sessions.find((s) => s.state === 'running' && s.id !== current) || sessions.find((s) => s.state === 'running'); if (!session) return; const visible = session.id === current && !overview.matches(':popover-open') && !document.hidden; session.state = visible ? 'idle' : 'done'; session.lastActivity = ++activityOrder; session.detail = visible ? '结果已查看,可以继续这项工作。' : '工作已完成,结果等待查看。'; session.answer = '本轮工作已完成。\n\n示例结果:检查项已通过,相关说明已整理。你可以继续追问,或者切换到下一项工作。'; render(); notify(`${projectOf(session).name} / ${session.title} 已完成`); }; overview.addEventListener('toggle', (event) => { const open = event.newState === 'open'; $('workspace-trigger').setAttribute('aria-expanded', String(open)); $('activity-trigger').setAttribute('aria-expanded', String(open)); if (open) return; const session = sessions.find((s) => s.id === current); if (session.state === 'done') { session.state = 'idle'; session.detail = '结果已查看,可以继续这项工作。'; render(); } }); $('theme').onclick = () => { const dark = document.documentElement.dataset.theme !== 'dark'; document.documentElement.dataset.theme = dark ? 'dark' : 'light'; $('theme').textContent = dark ? '切换浅色' : '切换深色'; }; function createSession(project) { const id = Math.max(...sessions.map((s) => s.id)) + 1; sessions.push({ id, project, title: '新的工作', state: 'idle', detail: '等待你的第一条消息。', prompt: '开始一项新的工作。', answer: '在下方输入你的想法。' }); openSession(id); $('message').focus(); } $('new-chat').onclick = () => createSession(sessions.find((s) => s.id === current).project); $('menu-new-chat').onclick = () => createSession(scope === 'all' ? sessions.find((s) => s.id === current).project : scope); $('new-project').onclick = () => { $('project-form').hidden = !$('project-form').hidden; $('new-project').setAttribute('aria-expanded', String(!$('project-form').hidden)); if (!$('project-form').hidden) $('project-input').focus(); }; $('cancel-project').onclick = () => { $('project-form').hidden = true; $('new-project').setAttribute('aria-expanded', 'false'); $('new-project').focus(); }; $('project-form').onsubmit = (event) => { event.preventDefault(); const name = $('project-input').value.trim(); if (!name) { $('project-input').focus(); return; } const id = `project-${projects.length + 1}`; projects.push({ id, name, kind: 'local', location: '本地工作空间', rootPath: `D:/workspace/${id}`, description: '', defaultMode: 'ask', runtime: 'OpenCode' }); scope = id; $('project-form').reset(); $('project-form').hidden = true; $('new-project').setAttribute('aria-expanded', 'false'); createSession(id); }; $('composer').onsubmit = (event) => { event.preventDefault(); const text = $('message').value.trim(); if (!text) return; const session = sessions.find((s) => s.id === current); if (session.state === 'running' || session.state === 'waiting') { notify('请先完成当前模拟执行或处理待办,再发送消息。'); return; } session.prompt = text; session.state = 'running'; session.lastActivity = ++activityOrder; session.detail = '正在处理你的消息。'; session.answer = '已开始模拟执行。你可以切换工作空间,然后点击页面上方“模拟后台完成”查看通知效果。'; $('message').value = ''; render(); }; function reset() { projects.splice(0, projects.length, ...structuredClone(initialProjects)); scope = 'all'; projectTab = 'local'; $('project-search').value = ''; filter = 'all'; $('project-form').reset(); sessions = structuredClone(initial); sessions.forEach((session, index) => { session.lastActivity = initial.length - index; }); activityOrder = initial.length; current = 1; Object.keys(remembered).forEach((key) => delete remembered[key]); Object.keys(drafts).forEach((key) => delete drafts[key]); remembered.buddy = 1; closeMenu(); $('message').value = ''; render(); } $('reset').onclick = reset; document.addEventListener('keydown', (event) => { if ($('project-settings').open) return; if ((event.ctrlKey || event.metaKey) && event.shiftKey && event.code === 'KeyK') { event.preventDefault(); if (overview.matches(':popover-open')) { closeMenu(); menuTrigger?.focus(); } else showOverview(); } if (event.key === 'Escape' && overview.matches(':popover-open')) { event.preventDefault(); closeMenu(); menuTrigger?.focus(); } }); window.addEventListener('resize', () => { if (overview.matches(':popover-open')) positionMenu(); }); reset();