@@ -256,10 +312,28 @@ export async function render() {
_hostedAutoStopEl = page.querySelector('#hosted-agent-auto-stop')
_hostedSaveBtn = page.querySelector('#hosted-agent-save')
_hostedCloseBtn = page.querySelector('#hosted-agent-close')
+ _workspaceBtn = page.querySelector('#btn-chat-workspace')
+ _workspacePanelEl = page.querySelector('#chat-workspace-panel')
+ _workspaceAgentBadgeEl = page.querySelector('#chat-workspace-agent-badge')
+ _workspaceAgentTitleEl = page.querySelector('#chat-workspace-agent-title')
+ _workspacePathEl = page.querySelector('#chat-workspace-path')
+ _workspaceCoreListEl = page.querySelector('#chat-workspace-core-list')
+ _workspaceTreeEl = page.querySelector('#chat-workspace-tree')
+ _workspaceCurrentFileEl = page.querySelector('#chat-workspace-current-file')
+ _workspaceMetaEl = page.querySelector('#chat-workspace-editor-meta')
+ _workspaceEditorEl = page.querySelector('#chat-workspace-editor')
+ _workspacePreviewEl = page.querySelector('#chat-workspace-preview')
+ _workspaceEmptyEl = page.querySelector('#chat-workspace-empty')
+ _workspaceSaveBtn = page.querySelector('#chat-workspace-save')
+ _workspaceReloadBtn = page.querySelector('#chat-workspace-reload')
+ _workspacePreviewBtn = page.querySelector('#chat-workspace-preview-toggle')
page.querySelector('#chat-sidebar')?.classList.toggle('open', getSidebarOpen())
bindEvents(page)
bindConnectOverlay(page)
+ const workspaceOpen = getWorkspacePanelOpen()
+ applyWorkspacePanelVisibility(workspaceOpen)
+ if (!workspaceOpen) syncWorkspaceContext(false)
// 首次使用引导提示
showPageGuide(_messagesEl)
@@ -357,6 +431,83 @@ function bindEvents(page) {
page.querySelector('#btn-cmd').addEventListener('click', () => toggleCmdPanel())
page.querySelector('#btn-reset-session').addEventListener('click', () => resetCurrentSession())
page.querySelector('#btn-refresh-models')?.addEventListener('click', () => loadModelOptions(true))
+ _workspaceBtn?.addEventListener('click', async (e) => {
+ e.stopPropagation()
+ if (getWorkspacePanelOpen() && _workspaceDirty) {
+ const yes = await confirmWorkspaceDiscardIfNeeded()
+ if (!yes) return
+ discardWorkspaceChanges()
+ }
+ toggleWorkspacePanel()
+ })
+ page.querySelector('#chat-workspace-close')?.addEventListener('click', async () => {
+ if (_workspaceDirty) {
+ const yes = await confirmWorkspaceDiscardIfNeeded()
+ if (!yes) return
+ discardWorkspaceChanges()
+ }
+ toggleWorkspacePanel(false)
+ })
+ page.querySelector('#chat-workspace-refresh')?.addEventListener('click', async () => {
+ if (_workspaceDirty) {
+ const yes = await confirmWorkspaceDiscardIfNeeded()
+ if (!yes) return
+ discardWorkspaceChanges()
+ }
+ loadWorkspacePanelData(true)
+ })
+ _workspaceCoreListEl?.addEventListener('click', async (e) => {
+ const item = e.target.closest('[data-core-path]')
+ if (!item) return
+ const relativePath = item.dataset.corePath || ''
+ if (!relativePath) return
+ if (item.dataset.coreExists === '1') await openWorkspaceFile(relativePath, { kind: 'core' })
+ else {
+ const yes = await confirmWorkspaceDiscardIfNeeded()
+ if (!yes) return
+ discardWorkspaceChanges()
+ prepareWorkspaceDraftFile(relativePath, { kind: 'core' })
+ }
+ })
+ _workspaceTreeEl?.addEventListener('click', async (e) => {
+ const toggle = e.target.closest('[data-tree-toggle]')
+ if (toggle) {
+ try {
+ await toggleWorkspaceDirectory(toggle.dataset.treeToggle || '')
+ } catch (err) {
+ toast(`${t('chat.workspaceLoadFailed')}: ${err?.message || err}`, 'error')
+ }
+ return
+ }
+ const link = e.target.closest('[data-tree-path]')
+ if (!link) return
+ const relativePath = link.dataset.treePath || ''
+ if (!relativePath) return
+ if (link.dataset.treeType === 'dir') {
+ try {
+ await toggleWorkspaceDirectory(relativePath)
+ } catch (err) {
+ toast(`${t('chat.workspaceLoadFailed')}: ${err?.message || err}`, 'error')
+ }
+ return
+ }
+ await openWorkspaceFile(relativePath, { kind: 'tree' })
+ })
+ _workspaceEditorEl?.addEventListener('input', () => {
+ if (!_workspaceCurrentFile || !_workspaceEditorEl) return
+ _workspaceDirty = _workspaceEditorEl.value !== _workspaceLoadedContent
+ if (_workspacePreviewMode) renderWorkspacePreview()
+ updateWorkspaceEditorState()
+ })
+ _workspaceEditorEl?.addEventListener('keydown', (e) => {
+ if ((e.ctrlKey || e.metaKey) && e.key.toLowerCase() === 's') {
+ e.preventDefault()
+ saveWorkspaceCurrentFile()
+ }
+ })
+ _workspaceReloadBtn?.addEventListener('click', () => reloadWorkspaceCurrentFile())
+ _workspacePreviewBtn?.addEventListener('click', () => toggleWorkspacePreview())
+ _workspaceSaveBtn?.addEventListener('click', () => saveWorkspaceCurrentFile())
// 文件上传
page.querySelector('#chat-attach-btn').addEventListener('click', () => _fileInputEl.click())
@@ -473,6 +624,383 @@ function setSidebarOpen(open) {
localStorage.setItem(STORAGE_SIDEBAR_KEY, open ? '1' : '0')
}
+function getWorkspacePanelOpen() {
+ return localStorage.getItem(STORAGE_WORKSPACE_PANEL_KEY) === '1'
+}
+
+function setWorkspacePanelOpen(open) {
+ localStorage.setItem(STORAGE_WORKSPACE_PANEL_KEY, open ? '1' : '0')
+}
+
+function formatWorkspaceFileSize(bytes) {
+ const size = Number(bytes) || 0
+ if (size < 1024) return `${size} B`
+ if (size < 1024 * 1024) return `${(size / 1024).toFixed(1)} KB`
+ return `${(size / (1024 * 1024)).toFixed(1)} MB`
+}
+
+function formatWorkspaceFileTime(value) {
+ if (!value) return ''
+ const date = new Date(value)
+ if (Number.isNaN(date.getTime())) return ''
+ return date.toLocaleString()
+}
+
+function isMarkdownWorkspaceFile(relativePath) {
+ return /\.(md|markdown|mdx)$/i.test(relativePath || '')
+}
+
+async function confirmWorkspaceDiscardIfNeeded() {
+ if (!_workspaceDirty) return true
+ return showConfirm(t('chat.confirmDiscardWorkspaceChanges'))
+}
+
+function discardWorkspaceChanges() {
+ if (!_workspaceCurrentFile) {
+ _workspaceDirty = false
+ updateWorkspaceEditorState()
+ return
+ }
+ if (_workspaceEditorEl) _workspaceEditorEl.value = _workspaceLoadedContent
+ _workspaceDirty = false
+ if (_workspacePreviewMode) renderWorkspacePreview()
+ updateWorkspaceEditorState()
+}
+
+function getCurrentWorkspaceAgentId() {
+ return parseSessionAgent(_sessionKey) || wsClient.snapshot?.sessionDefaults?.defaultAgentId || 'main'
+}
+
+function getWorkspaceAgentTitle() {
+ if (_sessionKey) return getDisplayLabel(_sessionKey)
+ if (_workspaceCurrentAgentId === 'main') return t('chat.mainSession')
+ return _workspaceCurrentAgentId || t('chat.workspace')
+}
+
+async function syncWorkspaceContext(reload = true) {
+ const nextAgentId = getCurrentWorkspaceAgentId()
+ const prevAgentId = _workspaceCurrentAgentId
+ _workspaceCurrentAgentId = nextAgentId || 'main'
+
+ const triggerAgentEl = _page?.querySelector('#chat-workspace-trigger-agent')
+ if (triggerAgentEl) triggerAgentEl.textContent = _workspaceCurrentAgentId
+ if (_workspaceAgentBadgeEl) _workspaceAgentBadgeEl.textContent = _workspaceCurrentAgentId
+ if (_workspaceAgentTitleEl) {
+ _workspaceAgentTitleEl.textContent = getWorkspaceAgentTitle()
+ }
+
+ if (!_workspacePanelEl || !getWorkspacePanelOpen()) return
+ if (!reload && prevAgentId === _workspaceCurrentAgentId && _workspaceInfo) return
+
+ if (prevAgentId !== _workspaceCurrentAgentId) {
+ _workspaceDirty = false
+ _workspaceCurrentFile = null
+ }
+
+ await loadWorkspacePanelData(prevAgentId === _workspaceCurrentAgentId)
+}
+
+function applyWorkspacePanelVisibility(open) {
+ if (!_workspacePanelEl) return
+ _workspacePanelEl.style.display = open ? '' : 'none'
+ _workspaceBtn?.classList.toggle('is-active', open)
+ if (open) syncWorkspaceContext(true)
+}
+
+function toggleWorkspacePanel(force) {
+ const nextOpen = typeof force === 'boolean' ? force : !getWorkspacePanelOpen()
+ setWorkspacePanelOpen(nextOpen)
+ applyWorkspacePanelVisibility(nextOpen)
+}
+
+function renderWorkspacePanelMeta() {
+ if (_workspaceAgentBadgeEl) _workspaceAgentBadgeEl.textContent = _workspaceCurrentAgentId
+ if (_workspaceAgentTitleEl) {
+ _workspaceAgentTitleEl.textContent = getWorkspaceAgentTitle()
+ }
+ if (_workspacePathEl) {
+ const path = _workspaceInfo?.workspacePath || ''
+ _workspacePathEl.textContent = path || t('chat.workspaceUnavailable')
+ _workspacePathEl.title = path || ''
+ }
+}
+
+function renderWorkspaceCoreFiles() {
+ if (!_workspaceCoreListEl) return
+ if (!_workspaceCoreFiles.length) {
+ _workspaceCoreListEl.innerHTML = `
${t('chat.workspaceNoCoreFiles')}
`
+ return
+ }
+
+ _workspaceCoreListEl.innerHTML = _workspaceCoreFiles.map(file => {
+ const active = _workspaceCurrentFile?.relativePath === file.name ? ' active' : ''
+ const status = file.exists ? t('common.edit') : t('common.add')
+ return `
+
+ `
+ }).join('')
+}
+
+function renderWorkspaceTreeNode(entry, depth) {
+ const isDir = entry.type === 'dir'
+ const expanded = isDir && _workspaceExpandedDirs.has(entry.relativePath)
+ const active = _workspaceCurrentFile?.relativePath === entry.relativePath ? ' active' : ''
+ const children = expanded
+ ? (_workspaceTreeCache.get(entry.relativePath) || []).map(child => renderWorkspaceTreeNode(child, depth + 1)).join('')
+ : ''
+
+ return `
+
+
+ ${isDir
+ ? ``
+ : ''}
+
+
+ ${children}
+
+ `
+}
+
+function renderWorkspaceTree() {
+ if (!_workspaceTreeEl) return
+ const rootEntries = _workspaceTreeCache.get('') || []
+ if (!rootEntries.length) {
+ _workspaceTreeEl.innerHTML = `
${t('chat.workspaceTreeEmpty')}
`
+ return
+ }
+ _workspaceTreeEl.innerHTML = rootEntries.map(entry => renderWorkspaceTreeNode(entry, 0)).join('')
+}
+
+function renderWorkspacePreview() {
+ if (!_workspacePreviewEl || !_workspaceEditorEl) return
+ _workspacePreviewEl.innerHTML = renderMarkdown(_workspaceEditorEl.value || '')
+}
+
+function updateWorkspaceEditorState() {
+ const hasFile = !!_workspaceCurrentFile
+ const canSaveDraft = hasFile && _workspaceCurrentFile?.exists === false
+ if (_workspaceCurrentFileEl) {
+ _workspaceCurrentFileEl.textContent = hasFile
+ ? `${_workspaceCurrentFile.relativePath}${_workspaceDirty ? ' *' : ''}`
+ : t('chat.selectWorkspaceFile')
+ }
+ if (_workspaceSaveBtn) _workspaceSaveBtn.disabled = !hasFile || (!canSaveDraft && !_workspaceDirty) || _workspaceLoading
+ if (_workspaceReloadBtn) _workspaceReloadBtn.disabled = !hasFile || _workspaceLoading
+ if (_workspacePreviewBtn) _workspacePreviewBtn.disabled = !hasFile || !_workspaceCurrentFile?.previewable || _workspaceLoading
+ const previewLabelEl = _page?.querySelector('#chat-workspace-preview-label')
+ if (previewLabelEl) previewLabelEl.textContent = _workspacePreviewMode ? t('chat.editWorkspaceFile') : t('chat.previewWorkspaceFile')
+ if (_workspaceEditorEl) {
+ _workspaceEditorEl.disabled = !hasFile || _workspaceLoading
+ _workspaceEditorEl.style.display = hasFile && !_workspacePreviewMode ? '' : 'none'
+ }
+ if (_workspacePreviewEl) {
+ _workspacePreviewEl.style.display = hasFile && _workspacePreviewMode ? '' : 'none'
+ }
+ if (_workspaceEmptyEl) {
+ _workspaceEmptyEl.style.display = hasFile ? 'none' : ''
+ }
+ if (hasFile && _workspacePreviewMode) renderWorkspacePreview()
+}
+
+function resetWorkspaceEditor(emptyText = t('chat.workspaceEmptyState')) {
+ _workspaceCurrentFile = null
+ _workspacePreviewMode = false
+ _workspaceDirty = false
+ _workspaceLoadedContent = ''
+ if (_workspaceMetaEl) _workspaceMetaEl.textContent = ''
+ if (_workspaceEditorEl) {
+ _workspaceEditorEl.value = ''
+ _workspaceEditorEl.placeholder = t('chat.selectWorkspaceFile')
+ }
+ if (_workspacePreviewEl) {
+ _workspacePreviewEl.innerHTML = ''
+ _workspacePreviewEl.style.display = 'none'
+ }
+ if (_workspaceEmptyEl) _workspaceEmptyEl.textContent = emptyText
+ renderWorkspaceCoreFiles()
+ renderWorkspaceTree()
+ updateWorkspaceEditorState()
+}
+
+function prepareWorkspaceDraftFile(relativePath, options = {}) {
+ const { kind = 'core', previewable = isMarkdownWorkspaceFile(relativePath) } = options
+ _workspaceCurrentFile = { agentId: _workspaceCurrentAgentId, relativePath, kind, previewable, exists: false }
+ _workspacePreviewMode = false
+ _workspaceDirty = false
+ _workspaceLoadedContent = ''
+ if (_workspaceEditorEl) {
+ _workspaceEditorEl.value = ''
+ _workspaceEditorEl.placeholder = t('chat.workspaceDraftHint')
+ }
+ if (_workspaceMetaEl) _workspaceMetaEl.textContent = t('chat.workspaceDraftHint')
+ renderWorkspaceCoreFiles()
+ renderWorkspaceTree()
+ updateWorkspaceEditorState()
+}
+
+async function loadWorkspacePanelData(preserveCurrentFile = false) {
+ if (!_workspaceCoreListEl || !_workspaceTreeEl) return
+ const loadSeq = ++_workspaceLoadSeq
+ const agentId = _workspaceCurrentAgentId || 'main'
+ _workspaceLoading = true
+ renderWorkspacePanelMeta()
+ _workspaceCoreListEl.innerHTML = `
${t('common.loading')}
`
+ _workspaceTreeEl.innerHTML = `
${t('common.loading')}
`
+ updateWorkspaceEditorState()
+
+ try {
+ const previousFile = preserveCurrentFile ? _workspaceCurrentFile : null
+ const [info, coreFiles, rootEntries] = await Promise.all([
+ api.getAgentWorkspaceInfo(agentId),
+ api.listAgentFiles(agentId),
+ api.listAgentWorkspaceEntries(agentId, ''),
+ ])
+
+ if (loadSeq !== _workspaceLoadSeq || agentId !== _workspaceCurrentAgentId) return
+
+ _workspaceInfo = info || null
+ _workspaceCoreFiles = Array.isArray(coreFiles) ? coreFiles : []
+ _workspaceTreeCache = new Map([['', Array.isArray(rootEntries) ? rootEntries : []]])
+ _workspaceExpandedDirs = new Set()
+ renderWorkspacePanelMeta()
+ renderWorkspaceCoreFiles()
+ renderWorkspaceTree()
+
+ if (previousFile && previousFile.agentId === agentId) {
+ if (previousFile.kind === 'core' && previousFile.exists === false) {
+ prepareWorkspaceDraftFile(previousFile.relativePath, previousFile)
+ } else {
+ await openWorkspaceFile(previousFile.relativePath, { kind: previousFile.kind, force: true, silent: true })
+ }
+ } else {
+ resetWorkspaceEditor(t('chat.workspaceEmptyState'))
+ }
+ } catch (e) {
+ if (loadSeq !== _workspaceLoadSeq || agentId !== _workspaceCurrentAgentId) return
+ _workspaceInfo = null
+ _workspaceCoreFiles = []
+ _workspaceTreeCache = new Map([['', []]])
+ _workspaceExpandedDirs = new Set()
+ resetWorkspaceEditor(t('chat.workspaceUnavailable'))
+ renderWorkspacePanelMeta()
+ const message = e?.message || String(e)
+ _workspaceCoreListEl.innerHTML = `
${escapeAttr(message)}
`
+ _workspaceTreeEl.innerHTML = `
${escapeAttr(message)}
`
+ toast(`${t('chat.workspaceLoadFailed')}: ${message}`, 'error')
+ } finally {
+ if (loadSeq !== _workspaceLoadSeq) return
+ _workspaceLoading = false
+ updateWorkspaceEditorState()
+ }
+}
+
+async function toggleWorkspaceDirectory(relativePath) {
+ if (!relativePath) return
+ if (_workspaceExpandedDirs.has(relativePath)) {
+ _workspaceExpandedDirs.delete(relativePath)
+ renderWorkspaceTree()
+ return
+ }
+
+ try {
+ if (!_workspaceTreeCache.has(relativePath)) {
+ const entries = await api.listAgentWorkspaceEntries(_workspaceCurrentAgentId, relativePath)
+ _workspaceTreeCache.set(relativePath, Array.isArray(entries) ? entries : [])
+ }
+
+ _workspaceExpandedDirs.add(relativePath)
+ renderWorkspaceTree()
+ } catch (e) {
+ toast(`${t('common.loadFailed')}: ${e?.message || e}`, 'error')
+ }
+}
+
+async function openWorkspaceFile(relativePath, options = {}) {
+ const { kind = 'tree', force = false, silent = false } = options
+ if (!force && !(await confirmWorkspaceDiscardIfNeeded())) return
+ const openSeq = ++_workspaceOpenSeq
+ const agentId = _workspaceCurrentAgentId
+
+ try {
+ const file = await api.readAgentWorkspaceFile(agentId, relativePath)
+ if (openSeq !== _workspaceOpenSeq || agentId !== _workspaceCurrentAgentId) return
+ _workspaceCurrentFile = {
+ agentId,
+ relativePath,
+ kind,
+ previewable: !!file.previewable,
+ exists: true,
+ }
+ _workspaceLoadedContent = file.content || ''
+ _workspacePreviewMode = false
+ _workspaceDirty = false
+
+ if (_workspaceEditorEl) {
+ _workspaceEditorEl.value = _workspaceLoadedContent
+ _workspaceEditorEl.placeholder = t('chat.selectWorkspaceFile')
+ }
+
+ const metaParts = []
+ if (typeof file.size === 'number') metaParts.push(formatWorkspaceFileSize(file.size))
+ const timeText = formatWorkspaceFileTime(file.mtime)
+ if (timeText) metaParts.push(timeText)
+ if (_workspaceMetaEl) _workspaceMetaEl.textContent = metaParts.join(' · ')
+
+ renderWorkspaceCoreFiles()
+ renderWorkspaceTree()
+ updateWorkspaceEditorState()
+ } catch (e) {
+ if (openSeq !== _workspaceOpenSeq || agentId !== _workspaceCurrentAgentId) return
+ if (!silent) toast(`${t('chat.workspaceOpenFailed')}: ${e?.message || e}`, 'error')
+ }
+}
+
+async function reloadWorkspaceCurrentFile(force = false) {
+ if (!_workspaceCurrentFile) return
+ if (!force && !(await confirmWorkspaceDiscardIfNeeded())) return
+ if (_workspaceCurrentFile.kind === 'core' && _workspaceCurrentFile.exists === false) {
+ prepareWorkspaceDraftFile(_workspaceCurrentFile.relativePath, _workspaceCurrentFile)
+ return
+ }
+ await openWorkspaceFile(_workspaceCurrentFile.relativePath, { kind: _workspaceCurrentFile.kind, force: true })
+}
+
+function toggleWorkspacePreview() {
+ if (!_workspaceCurrentFile?.previewable) return
+ _workspacePreviewMode = !_workspacePreviewMode
+ updateWorkspaceEditorState()
+}
+
+async function saveWorkspaceCurrentFile() {
+ if (!_workspaceCurrentFile || !_workspaceEditorEl) return
+ const text = _workspaceEditorEl.value
+ const wasExisting = _workspaceCurrentFile.exists !== false
+ try {
+ await api.writeAgentWorkspaceFile(_workspaceCurrentAgentId, _workspaceCurrentFile.relativePath, text)
+ _workspaceCurrentFile = { ..._workspaceCurrentFile, exists: true }
+ _workspaceLoadedContent = text
+ _workspaceDirty = false
+ try {
+ await loadWorkspacePanelData(true)
+ } catch (refreshError) {
+ console.warn('[chat] workspace refresh after save failed:', refreshError)
+ }
+ toast(wasExisting ? t('common.saveSuccess') : t('chat.workspaceFileCreated'), 'success')
+ } catch (e) {
+ toast(`${t('common.saveFailed')}: ${e?.message || e}`, 'error')
+ }
+}
+
async function applySelectedModel() {
if (!_selectedModel) {
toast(t('chat.loadingModels'), 'warning')
@@ -675,6 +1203,8 @@ async function connectGateway() {
_sessionKey = saved || sessionKey
updateSessionTitle()
loadHistory()
+ } else {
+ syncWorkspaceContext(false)
}
// 始终刷新会话列表(无论是否有 sessionKey)
refreshSessionList()
@@ -757,7 +1287,7 @@ function renderSessionList(sessions) {
const delBtn = e.target.closest('[data-del]')
if (delBtn) { e.stopPropagation(); deleteSession(delBtn.dataset.del); return }
const item = e.target.closest('[data-key]')
- if (item) switchSession(item.dataset.key)
+ if (item) void switchSession(item.dataset.key)
}
_sessionListEl.ondblclick = (e) => {
const labelEl = e.target.closest('.chat-session-label')
@@ -796,8 +1326,15 @@ function parseSessionLabel(key) {
return `${agent} / ${channel}`
}
-function switchSession(newKey) {
- if (newKey === _sessionKey) return
+async function switchSession(newKey, options = {}) {
+ const { forceWorkspace = false } = options
+ if (newKey === _sessionKey) return false
+ const nextAgentId = parseSessionAgent(newKey) || 'main'
+ if (!forceWorkspace && _workspaceDirty && nextAgentId !== _workspaceCurrentAgentId) {
+ const yes = await confirmWorkspaceDiscardIfNeeded()
+ if (!yes) return false
+ discardWorkspaceChanges()
+ }
_sessionKey = newKey
localStorage.setItem(STORAGE_SESSION_KEY, newKey)
_lastHistoryHash = ''
@@ -806,6 +1343,7 @@ function switchSession(newKey) {
clearMessages()
loadHistory()
refreshSessionList()
+ return true
}
async function showNewSessionDialog() {
@@ -832,8 +1370,9 @@ async function showNewSessionDialog() {
toast(t('chat.createAgentHint'), 'info')
return
}
- switchSession(`agent:${agent}:${name}`)
- toast(t('chat.sessionCreated'), 'success')
+ switchSession(`agent:${agent}:${name}`).then((switched) => {
+ if (switched) toast(t('chat.sessionCreated'), 'success')
+ })
}
})
@@ -868,7 +1407,7 @@ async function deleteSession(key) {
try {
await wsClient.sessionsDelete(key)
toast(t('chat.sessionDeleted'), 'success')
- if (key === _sessionKey) switchSession(mainKey)
+ if (key === _sessionKey) void switchSession(mainKey, { forceWorkspace: true })
else refreshSessionList()
} catch (e) {
toast(`${t('common.operationFailed')}: ${e.message}`, 'error')
@@ -894,6 +1433,7 @@ async function resetCurrentSession() {
function updateSessionTitle() {
const el = _page?.querySelector('#chat-title')
if (el) el.textContent = getDisplayLabel(_sessionKey)
+ syncWorkspaceContext(false)
}
function renameSession(key, labelEl) {
@@ -2530,4 +3070,31 @@ export function cleanup() {
_hostedDefaults = null
_hostedRuntime = { ...HOSTED_RUNTIME_DEFAULT }
_hostedBusy = false
+ _workspaceBtn = null
+ _workspacePanelEl = null
+ _workspaceAgentBadgeEl = null
+ _workspaceAgentTitleEl = null
+ _workspacePathEl = null
+ _workspaceCoreListEl = null
+ _workspaceTreeEl = null
+ _workspaceCurrentFileEl = null
+ _workspaceMetaEl = null
+ _workspaceEditorEl = null
+ _workspacePreviewEl = null
+ _workspaceEmptyEl = null
+ _workspaceSaveBtn = null
+ _workspaceReloadBtn = null
+ _workspacePreviewBtn = null
+ _workspaceInfo = null
+ _workspaceCoreFiles = []
+ _workspaceTreeCache = new Map()
+ _workspaceExpandedDirs = new Set()
+ _workspaceCurrentAgentId = 'main'
+ _workspaceCurrentFile = null
+ _workspacePreviewMode = false
+ _workspaceDirty = false
+ _workspaceLoadedContent = ''
+ _workspaceLoading = false
+ _workspaceLoadSeq = 0
+ _workspaceOpenSeq = 0
}
diff --git a/src/style/chat.css b/src/style/chat.css
index 4179109..f063648 100644
--- a/src/style/chat.css
+++ b/src/style/chat.css
@@ -106,6 +106,384 @@
cursor: default;
}
+.chat-workspace-trigger {
+ display: inline-flex;
+ align-items: center;
+ gap: 8px;
+ min-height: 34px;
+ padding: 0 10px;
+ border: 1px solid transparent;
+ transition: background 0.2s ease, border-color 0.2s ease, color 0.2s ease;
+}
+
+.chat-workspace-trigger.is-active {
+ background: color-mix(in srgb, var(--accent) 10%, var(--bg-secondary));
+ border-color: color-mix(in srgb, var(--accent) 24%, var(--border-primary));
+}
+
+.chat-workspace-trigger-label {
+ white-space: nowrap;
+}
+
+.chat-workspace-trigger-agent {
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ min-width: 32px;
+ padding: 1px 8px;
+ border-radius: 999px;
+ background: color-mix(in srgb, var(--accent) 12%, var(--bg-tertiary));
+ color: var(--accent);
+ font-size: 11px;
+ font-weight: 700;
+}
+
+.chat-workspace-panel {
+ position: absolute;
+ top: 60px;
+ right: 16px;
+ bottom: 84px;
+ width: min(560px, 48vw);
+ min-width: 360px;
+ display: flex;
+ flex-direction: column;
+ min-height: 0;
+ background: color-mix(in srgb, var(--bg-card) 92%, var(--bg-primary));
+ border: 1px solid var(--border-primary);
+ border-radius: 18px;
+ box-shadow: 0 18px 48px rgba(15, 23, 42, 0.24);
+ overflow: hidden;
+ z-index: 24;
+}
+
+.chat-workspace-header {
+ display: flex;
+ align-items: flex-start;
+ justify-content: space-between;
+ gap: 12px;
+ padding: 14px 16px;
+ border-bottom: 1px solid var(--border-primary);
+ background: color-mix(in srgb, var(--bg-secondary) 88%, var(--bg-card));
+}
+
+.chat-workspace-header-copy {
+ min-width: 0;
+ flex: 1;
+}
+
+.chat-workspace-title-row {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ min-width: 0;
+}
+
+.chat-workspace-agent-badge {
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ min-width: 32px;
+ padding: 2px 8px;
+ border-radius: 999px;
+ background: color-mix(in srgb, var(--accent) 10%, var(--bg-primary));
+ color: var(--accent);
+ font-size: 11px;
+ font-weight: 700;
+}
+
+.chat-workspace-agent-title {
+ margin-top: 6px;
+ font-size: 12px;
+ color: var(--text-primary);
+ font-weight: 600;
+}
+
+.chat-workspace-path {
+ margin-top: 4px;
+ font-size: 11px;
+ color: var(--text-secondary);
+ line-height: 1.5;
+ overflow-wrap: anywhere;
+}
+
+.chat-workspace-header-actions {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+}
+
+.chat-workspace-icon-btn {
+ width: 32px;
+ height: 32px;
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ border: 1px solid var(--border-primary);
+ border-radius: 10px;
+ background: var(--bg-primary);
+ color: var(--text-secondary);
+ cursor: pointer;
+ transition: background 0.18s ease, border-color 0.18s ease, color 0.18s ease;
+}
+
+.chat-workspace-icon-btn:hover {
+ background: var(--bg-hover);
+ color: var(--text-primary);
+ border-color: var(--accent);
+}
+
+.chat-workspace-body {
+ flex: 1;
+ min-height: 0;
+ display: grid;
+ grid-template-columns: minmax(220px, 0.42fr) minmax(0, 0.58fr);
+}
+
+.chat-workspace-sidebar-pane {
+ min-width: 0;
+ min-height: 0;
+ overflow-y: auto;
+ border-right: 1px solid var(--border-primary);
+ background: color-mix(in srgb, var(--bg-secondary) 76%, var(--bg-card));
+}
+
+.chat-workspace-section {
+ padding: 14px 12px;
+}
+
+.chat-workspace-section + .chat-workspace-section {
+ border-top: 1px solid var(--border-primary);
+}
+
+.chat-workspace-section-title {
+ font-size: 11px;
+ font-weight: 700;
+ letter-spacing: 0.04em;
+ text-transform: uppercase;
+ color: var(--text-secondary);
+ margin-bottom: 10px;
+}
+
+.chat-workspace-core-list,
+.chat-workspace-tree {
+ display: flex;
+ flex-direction: column;
+ gap: 6px;
+}
+
+.chat-workspace-core-item {
+ width: 100%;
+ display: flex;
+ align-items: center;
+ gap: 10px;
+ padding: 10px 12px;
+ border: 1px solid var(--border-primary);
+ border-radius: 12px;
+ background: var(--bg-primary);
+ color: var(--text-primary);
+ text-align: left;
+ cursor: pointer;
+ transition: transform 0.18s ease, border-color 0.18s ease, background 0.18s ease;
+}
+
+.chat-workspace-core-item:hover,
+.chat-workspace-core-item.active {
+ transform: translateY(-1px);
+ border-color: color-mix(in srgb, var(--accent) 28%, var(--border-primary));
+ background: color-mix(in srgb, var(--accent) 8%, var(--bg-primary));
+}
+
+.chat-workspace-core-icon {
+ flex-shrink: 0;
+ color: var(--accent);
+}
+
+.chat-workspace-core-copy {
+ min-width: 0;
+ display: flex;
+ flex-direction: column;
+ gap: 3px;
+}
+
+.chat-workspace-core-name {
+ font-size: 13px;
+ font-weight: 600;
+ color: var(--text-primary);
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+}
+
+.chat-workspace-core-status {
+ font-size: 11px;
+ color: var(--text-secondary);
+}
+
+.chat-workspace-core-status.exists {
+ color: var(--success);
+}
+
+.chat-workspace-core-status.missing {
+ color: var(--warning);
+}
+
+.chat-workspace-tree-node {
+ display: block;
+}
+
+.chat-workspace-tree-row {
+ display: flex;
+ align-items: center;
+ gap: 6px;
+ min-height: 34px;
+ border-radius: 10px;
+ transition: background 0.18s ease;
+}
+
+.chat-workspace-tree-row:hover,
+.chat-workspace-tree-row.active {
+ background: color-mix(in srgb, var(--accent) 8%, transparent);
+}
+
+.chat-workspace-tree-toggle {
+ width: 18px;
+ height: 18px;
+ padding: 0;
+ border: none;
+ background: transparent;
+ color: var(--text-secondary);
+ cursor: pointer;
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ flex-shrink: 0;
+}
+
+.chat-workspace-tree-toggle.is-spacer {
+ display: inline-block;
+}
+
+.chat-workspace-tree-link {
+ flex: 1;
+ min-width: 0;
+ display: inline-flex;
+ align-items: center;
+ gap: 8px;
+ padding: 0;
+ border: none;
+ background: transparent;
+ color: inherit;
+ text-align: left;
+ cursor: pointer;
+}
+
+.chat-workspace-tree-name {
+ min-width: 0;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ font-size: 12px;
+}
+
+.chat-workspace-editor-pane {
+ min-width: 0;
+ min-height: 0;
+ display: flex;
+ flex-direction: column;
+ background: var(--bg-primary);
+}
+
+.chat-workspace-editor-toolbar {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ gap: 12px;
+ padding: 14px 16px 10px;
+ border-bottom: 1px solid var(--border-primary);
+}
+
+.chat-workspace-current-file {
+ min-width: 0;
+ font-size: 13px;
+ font-weight: 700;
+ color: var(--text-primary);
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+}
+
+.chat-workspace-editor-actions {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ flex-wrap: wrap;
+ justify-content: flex-end;
+}
+
+.chat-workspace-editor-meta {
+ padding: 10px 16px 0;
+ font-size: 11px;
+ color: var(--text-secondary);
+ min-height: 18px;
+}
+
+.chat-workspace-editor {
+ flex: 1;
+ min-height: 0;
+ width: 100%;
+ border: none;
+ background: transparent;
+ color: var(--text-primary);
+ padding: 14px 16px 18px;
+ resize: none;
+ outline: none;
+ font-size: 13px;
+ line-height: 1.65;
+ font-family: 'Cascadia Code', 'SF Mono', Consolas, monospace;
+}
+
+.chat-workspace-preview {
+ flex: 1;
+ min-height: 0;
+ overflow: auto;
+ padding: 14px 16px 18px;
+ font-size: 13px;
+ line-height: 1.7;
+ color: var(--text-primary);
+}
+
+.chat-workspace-preview pre {
+ background: var(--bg-secondary);
+ border: 1px solid var(--border-primary);
+ border-radius: 10px;
+ padding: 12px 14px;
+ overflow: auto;
+}
+
+.chat-workspace-preview code {
+ font-family: 'Cascadia Code', 'SF Mono', Consolas, monospace;
+}
+
+.chat-workspace-empty,
+.chat-workspace-note {
+ padding: 16px 14px;
+ border: 1px dashed var(--border-primary);
+ border-radius: 12px;
+ color: var(--text-secondary);
+ font-size: 12px;
+ line-height: 1.6;
+ background: color-mix(in srgb, var(--bg-secondary) 70%, transparent);
+}
+
+.chat-workspace-empty {
+ margin: 14px 16px 18px;
+}
+
+.chat-workspace-note.is-error {
+ color: var(--error);
+ border-color: color-mix(in srgb, var(--error) 35%, var(--border-primary));
+}
+
/* 状态指示点 */
.status-dot {
width: 8px;
@@ -1039,6 +1417,34 @@
padding: var(--space-sm) var(--space-sm);
gap: 6px;
}
+ .chat-workspace-trigger-label {
+ display: none;
+ }
+ .chat-workspace-panel {
+ top: 0;
+ right: 0;
+ bottom: 0;
+ left: 0;
+ width: auto;
+ min-width: 0;
+ border-radius: 0;
+ }
+ .chat-workspace-body {
+ grid-template-columns: 1fr;
+ grid-template-rows: minmax(0, 38%) minmax(0, 62%);
+ }
+ .chat-workspace-sidebar-pane {
+ border-right: none;
+ border-bottom: 1px solid var(--border-primary);
+ }
+ .chat-workspace-editor-toolbar {
+ flex-direction: column;
+ align-items: flex-start;
+ }
+ .chat-workspace-editor-actions {
+ width: 100%;
+ justify-content: flex-start;
+ }
.chat-toggle-sidebar {
width: 36px;
height: 36px;
@@ -1055,6 +1461,15 @@
}
}
+@media (prefers-reduced-motion: reduce) {
+ .chat-workspace-trigger,
+ .chat-workspace-core-item,
+ .chat-workspace-tree-row,
+ .chat-workspace-icon-btn {
+ transition: none;
+ }
+}
+
/* 托管 Agent */
.chat-hosted-btn {
display: flex;