diff --git a/src/App.tsx b/src/App.tsx index e919916..2addeab 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -8,7 +8,6 @@ import { Icon } from '@ui' import { store, useStore } from './store' import { TaskSidebar } from './components/TaskSidebar' import { Conversation } from './components/Conversation' -import { PreviewPane } from './components/PreviewPane' import { previewTabsKey } from './previewtabs' import { Workbench } from './components/Workbench' import { Workspaces, workspaceShortcuts } from './components/Workspaces' @@ -18,7 +17,8 @@ import { ProjectSettingsModal } from './components/ProjectSettingsModal' import { TopBar } from './components/TopBar' import { workspaceEntries } from './components/Workspaces' -// 布局:左(项目会话)/ 中(对话)/ 预览区(有标签时)/ 右(工作台)。 +// 布局(三栏工作台):左(项目与会话)/ 中(对话)/ 第三栏(工作|文件双入口)。 +// 文件预览不再是独立第四栏——预览在第三栏「文件」入口内,展开态铺满主区。 const LEFT_MIN = 220, LEFT_MAX = 420, LEFT_OVERSHOOT = 40 const RIGHT_MIN = 280, RIGHT_MAX = 560 const PREVIEW_MIN = 420, PREVIEW_MAX = 1100 @@ -32,11 +32,15 @@ export function App() { const ui = useStore(s => s.ui) // 预览键与 requestPreview 的落点一致(previewTabsKey):空态(未选会话)点开 - // 工件也要渲染预览区,否则点击只剩工件弹层关闭、毫无反馈。 + // 工件也要能进文件入口,否则点击只剩工件弹层关闭、毫无反馈。 const previewOpen = useStore(s => (s.previewTabs[previewTabsKey(s.currentConvId)]?.length ?? 0) > 0) + const rightTab = useStore(s => s.ui.rightTab) const toast = useStore(s => s.toast) const trackerRunId = useStore(s => s.ui.trackerRunId) - const previewExpanded = previewOpen && ui.previewExpanded + // 文件入口 + 预览标签在场时,第三栏用「阅读档」宽度(420–1100),不受工作档上限约束。 + const filesWide = rightTab === 'files' && previewOpen + const rightPanelWidth = filesWide ? ui.previewWidth : ui.rightWidth + const previewExpanded = filesWide && ui.previewExpanded const pendingBadge = useStore(s => s.cards.filter(c => c.status === 'action_required').length) // authState:checking → login(登录)→ mustchange(首登强制改密)→ ok(放行)。 const [authState, setAuthState] = useState<'checking' | 'login' | 'mustchange' | 'ok'>('checking') @@ -64,28 +68,31 @@ export function App() { store.refreshPlugins() }, [authState]) - const drag = useRef<{ side: 'left' | 'right' | 'preview'; startX: number; startW: number; overshoot: boolean } | null>(null) + const drag = useRef<{ side: 'left' | 'right'; startX: number; startW: number; overshoot: boolean; min: number; max: number; key: 'leftWidth' | 'rightWidth' | 'previewWidth' } | null>(null) - const onDividerDown = (side: 'left' | 'right' | 'preview') => (e: React.MouseEvent) => { + const onDividerDown = (side: 'left' | 'right') => (e: React.MouseEvent) => { e.preventDefault() - const startW = side === 'left' ? ui.leftWidth : side === 'preview' ? ui.previewWidth : ui.rightWidth - drag.current = { side, startX: e.clientX, startW, overshoot: false } + // 右侧分隔条按当前入口选宽度档:文件+预览在场走阅读档,否则工作档。 + const wide = side === 'right' && rightTab === 'files' && previewOpen + const key = side === 'left' ? 'leftWidth' : wide ? 'previewWidth' : 'rightWidth' + drag.current = { + side, startX: e.clientX, startW: ui[key], overshoot: false, + min: side === 'left' ? LEFT_MIN : wide ? PREVIEW_MIN : RIGHT_MIN, + max: side === 'left' ? LEFT_MAX : wide ? PREVIEW_MAX : RIGHT_MAX, + key, + } const move = (ev: MouseEvent) => { const d = drag.current if (!d) return if (d.side === 'left') { const w = d.startW + (ev.clientX - d.startX) - if (w < LEFT_MIN - LEFT_OVERSHOOT) { d.overshoot = true; return } + if (w < d.min - LEFT_OVERSHOOT) { d.overshoot = true; return } d.overshoot = false - store.setUI({ leftCollapsed: false, leftWidth: Math.min(LEFT_MAX, Math.max(LEFT_MIN, w)) }) - } else if (d.side === 'preview') { - const w = d.startW - (ev.clientX - d.startX) - if (w < PREVIEW_MIN) return - store.setUI({ previewWidth: Math.min(PREVIEW_MAX, w) }) + store.setUI({ leftCollapsed: false, leftWidth: Math.min(d.max, Math.max(d.min, w)) }) } else { const w = d.startW - (ev.clientX - d.startX) - if (w < RIGHT_MIN) return - store.setUI({ rightCollapsed: false, rightWidth: Math.min(RIGHT_MAX, w) }) + if (w < d.min) return + store.setUI({ rightCollapsed: false, [d.key]: Math.min(d.max, w) } as Partial) } } const up = () => { @@ -146,30 +153,21 @@ export function App() { - {previewOpen && ( - <> - {!previewExpanded && ( -
- )} -
- -
- - )} - {!ui.rightCollapsed ? ( <>
-
+
) : (
- +
)} diff --git a/src/components/ArtifactsPanel.tsx b/src/components/ArtifactsPanel.tsx index c502ecd..99ffcf1 100644 --- a/src/components/ArtifactsPanel.tsx +++ b/src/components/ArtifactsPanel.tsx @@ -1,5 +1,5 @@ -// 工件面板(工作区文件管理):搜索 + kind 过滤 + 上传;行内打开预览区或下载。 -// ⌘1 弹层与右栏「文件」页签共用这一份。删除仍是保留能力,系统默认不提供入口。 +// 文件列表(第三栏「文件」入口的主视图):搜索 + kind 过滤 + 上传; +// 行内打开预览(同入口切到预览子视图)或下载。删除仍是保留能力,系统默认不提供入口。 import { useState } from 'react' import { store, useStore } from '../store' import { api } from '../api' @@ -37,16 +37,16 @@ export function ArtifactsPanel() { : visible.length === 0 ?
没有匹配「{q}」的文件
: visible.map(a => ( - // 预览落在对话右侧预览区。工作区弹层盖住主区,先收起再打开。 + // 预览落位在第三栏「文件」入口:requestPreview 会切到预览子视图。
{ store.closeWorkspace(); store.requestPreview(a) }} title={`预览 ${a.name}`}> + onClick={() => store.requestPreview(a)} title={`预览 ${a.name}`}> {a.name} {a.kind} · {humanSize(a.size) || '—'} · {shortTime(a.updatedAt)} - + e.stopPropagation()}> diff --git a/src/components/Conversation.tsx b/src/components/Conversation.tsx index fc13d81..72b4948 100644 --- a/src/components/Conversation.tsx +++ b/src/components/Conversation.tsx @@ -4,7 +4,7 @@ import { Icon, Avatar, BrandMark, Collapsible, getPersistedOpen, setPersistedOpen, SafeMarkdown } from '@ui' import { useEffect, useMemo, useRef, useState } from 'react' import { api } from '../api' -import { store, useStore, activeRun, type LiveState, type ContextObs } from '../store' +import { store, useStore, activeRun, isTerminal, type LiveState, type ContextObs } from '../store' import { scrollRatio, scrollTopForRatio } from '../scrollsync' import { liveToMessage, livePhaseLabel, showProcessInline } from '../live' import { deriveShellPhase, isShellBusy, showLiveBubble, type ShellPhase } from '../runstate' @@ -484,6 +484,8 @@ export function Conversation() { }) const busy = isShellBusy(phase) const showStop = phase === 'running' + // 慢通道在跑的后台子代理:前台回合结束后仍在执行,完成后经通知回到对话汇报。 + const bgRunning = runs.filter(r => r.kind === 'subagent' && !isTerminal(r.status)).length useEffect(() => { if (!stickRef.current) return @@ -553,7 +555,6 @@ export function Conversation() { {!connected && 重连中} - {showStop && }
+ {/* 轻量状态提示(方案:快慢通道连续体验):忙碌时可继续输入排队; + 后台任务在回合结束后仍在执行,完成后回到对话汇报。 */} + {(busy || bgRunning > 0) && ( +
+ + {busy + ? 助手工作中{bgRunning > 0 ? ` · ${bgRunning} 个后台任务` : ''} + : {bgRunning} 个后台任务进行中} + {busy ? '输入仍可排队' : '完成后回到对话汇报'} + + +
+ )} {attachmentNames.length > 0 && (
{attachmentNames.map((n, i) => {n} diff --git a/src/components/PreviewPane.tsx b/src/components/PreviewPane.tsx index 75fec5e..19d9e02 100644 --- a/src/components/PreviewPane.tsx +++ b/src/components/PreviewPane.tsx @@ -1,5 +1,5 @@ -// 对话右侧预览区:多标签、可关、展开与下载。宽度由 App 的分隔条控制; -// 展开态铺满主区。文本类文件在这里编辑保存。 +// 第三栏「文件」入口的多标签预览:打开/切换/关闭标签、编辑保存、展开铺满与下载。 +// 列表 ⇄ 预览在同一入口内切换;宽度档(阅读档 420–1100)由 App 的分隔条控制。 import { useEffect, useState } from 'react' import { Btn, FilePreview, Icon, TabStrip } from '@ui' import { api } from '../api' @@ -44,6 +44,9 @@ export function PreviewPane() { onClose={id => store.closePreview(id)} />
+ {active.name} {art && ( {art.kind}{humanSize(art.size) ? ` · ${humanSize(art.size)}` : ''} diff --git a/src/components/TaskSidebar.tsx b/src/components/TaskSidebar.tsx index 591f600..4bdcbac 100644 --- a/src/components/TaskSidebar.tsx +++ b/src/components/TaskSidebar.tsx @@ -1,4 +1,4 @@ -// 左栏:项目列表、当前项目的会话、体系工具、账户。 +// 左栏(找上下文):顶部切换项目,中间放会话与搜索,底部放文件、工作区和账户。 // 创建、搜索、置顶与归档仍在会话段里;领域工具由服务端工作区声明,没有时装上说明行。 import { useEffect, useRef, useState } from 'react' import { store, useStore } from '../store' @@ -40,6 +40,7 @@ export function TaskSidebar() { const projects = useStore(s => s.projects) const tasks = useStore(s => s.tasks) const me = useStore(s => s.me) + const artifacts = useStore(s => s.artifacts) const currentConvId = useStore(s => s.currentConvId) const currentProjectId = useStore(s => s.currentProjectId) const showArchived = useStore(s => s.ui.showArchived) @@ -50,7 +51,7 @@ export function TaskSidebar() { s.projectCards.filter(c => c.status === 'action_required').length) const [q, setQ] = useState('') - const [menu, setMenu] = useState<{ kind: 'new' | 'account' | `row:${string}`; anchor: HTMLElement } | null>(null) + const [menu, setMenu] = useState<{ kind: 'new' | 'account' | 'project' | `row:${string}`; anchor: HTMLElement } | null>(null) const [creatingProject, setCreatingProject] = useState(false) const [newProjectName, setNewProjectName] = useState('') @@ -70,6 +71,7 @@ export function TaskSidebar() { const pinned = visible.filter(t => t.pinned) const rest = visible.filter(t => !t.pinned) const currentProject = projects.find(p => p.id === currentProjectId) + const stage = projectStage(currentProject?.settings) const rowTask = menu?.kind.startsWith('row:') ? tasks.find(t => t.id === menu.kind.slice(4)) : null const rowMenuAnchor = menu && menu.kind.startsWith('row:') ? menu.anchor : null @@ -86,6 +88,14 @@ export function TaskSidebar() { ) + /** 切换项目:优先恢复该项目下正在看的会话,否则进第一个未归档会话。 */ + const switchProject = (pid: string) => { + const next = tasks.find(t => t.projectId === pid && t.id === currentConvId) + ?? tasks.find(t => t.projectId === pid && !t.archived) + if (next) store.openTask(next.id) + else store.openProject(pid) + } + const taskRow = (t: Conversation) => (
store.openTask(t.id)} @@ -99,8 +109,15 @@ export function TaskSidebar() { return ( <> + {/* 项目身份唯一落点:顶部切换项目(方案:项目导航置顶)。 */}
- 项目 +
@@ -127,6 +144,7 @@ export function TaskSidebar() { onClick={() => { store.setUI({ showArchived: !showArchived }); store.refreshTasks(!showArchived) }}>
+ {/* 中段(方案:会话与搜索)。项目切换在顶部,文件与工作区沉底。 */}
{creatingProject && (
@@ -143,25 +161,6 @@ export function TaskSidebar() {
)} -
项目
- {projects.map(p => { - const stage = projectStage(p.settings) - const count = tasks.filter(t => t.projectId === p.id && !t.archived).length - return ( - - ) - })} - {projects.length === 0 &&
还没有项目——从「新建任务」旁的菜单创建
} - {pinned.length > 0 && ( <>
置顶
@@ -173,11 +172,15 @@ export function TaskSidebar() {
这个项目还没有会话——点击「新建任务」开始
)} {rest.map(t => taskRow(t))} +
-
- 体系工具 - {pendingBadge > 0 && {pendingBadge}} -
+ {/* 底部(方案:文件、工作区、账户沉底)。 */} +
+ {tools.map(entry => (
{/* 弹出菜单们 */} + setMenu(null)}> + {projects.map(p => { + const count = tasks.filter(t => t.projectId === p.id && !t.archived).length + const pstage = projectStage(p.settings) + return ( + { switchProject(p.id); setMenu(null) }}> + {p.name} + {p.id === currentProjectId ? '当前' : pstage ? `${pstage} · ` : ''}{count} 会话 + + ) + })} + {projects.length === 0 &&
还没有项目
} +
+ { setCreatingProject(true); setMenu(null) }}>新建项目… +
+ setMenu(null)}> { store.createTask(); setMenu(null) }}>新建任务 · 当前项目⌘N
diff --git a/src/components/TopBar.tsx b/src/components/TopBar.tsx index 7363502..068c979 100644 --- a/src/components/TopBar.tsx +++ b/src/components/TopBar.tsx @@ -1,20 +1,16 @@ -// 顶栏:产品标识、当前项目与阶段徽章、待处理铃铛。不参与具体操作。 +// 顶栏:产品标识与待处理铃铛。项目身份在左栏顶部切换器集中显示一次(方案), +// 阶段徽章随切换器;本栏不再重复。 import { BrandMark, Icon, pendingCount } from '@ui' import { store, useStore } from '../store' -import { projectStage } from '../projectmeta' export function TopBar() { - const project = useStore(s => s.projects.find(p => p.id === s.currentProjectId)) const pending = useStore(s => pendingCount(s.cards) + pendingCount(s.projectCards)) - const stage = projectStage(project?.settings) return (
Agent-System - {project && {project.name}} - {stage && {stage}} - - + + - - + {rightTab === 'work' && ( + + )} +
+ {rightTab === 'files' + ? + : } + + ) +} + +/** 文件入口:有预览标签且处于预览子视图时展示预览;否则展示文件列表(搜索/上传)。 */ +function FilesTab() { + const previewOpen = useStore(s => (s.previewTabs[previewTabsKey(s.currentConvId)]?.length ?? 0) > 0) + const filesView = useStore(s => s.ui.filesView) + if (previewOpen && filesView === 'preview') return + return
+} + +function WorkTab({ plan, todos, filter, inboxScope, projectCards, cards }: { + plan: Parameters[0]['plan'] + todos: Parameters[0]['todos'] + filter: InboxFilter + inboxScope: 'conv' | 'project' + projectCards: ActivityCard[] + cards: ActivityCard[] +}) { + return ( + <>
-
+
+ + +
{inboxScope === 'project' ? diff --git a/src/components/Workspaces.tsx b/src/components/Workspaces.tsx index e9cda44..b63bcc0 100644 --- a/src/components/Workspaces.tsx +++ b/src/components/Workspaces.tsx @@ -1,11 +1,11 @@ // 工作区弹层(提案 T4/D25):深读经 OverlayHost 单层呈现。 // 服务端声明的工作区(领域插件槽位)对所有用户可见;插件管理面板仍只在系统管理区。 +// 文件浏览已并入第三栏「文件」入口,本弹层只承载代理与领域工作区。 import { store, useStore } from '../store' import type { Run } from '../contracts' import { OverlayHost, type DocBridge } from '@ui' import { docBridge } from './Workbench' import { AgentRow } from './Workbench' -import { ArtifactsPanel } from './ArtifactsPanel' import { buildWorkspaceEntries, type WorkspaceEntry } from '../workspaces' /** 体系工具(顺序即 ⌘ 序号)。服务端工作区全员可见;插件管理面板只在系统管理区。 */ @@ -15,7 +15,6 @@ export function workspaceEntries(): WorkspaceEntry[] { export function Workspaces() { const active = useStore(s => s.ui.activeWorkspace) - const artifacts = useStore(s => s.artifacts) const runs = useStore(s => s.runs) const pluginPanels = useStore(s => s.pluginPanels) const serverWorkspaces = useStore(s => s.serverWorkspaces) @@ -23,11 +22,6 @@ export function Workspaces() { const entry = workspaceEntries().find(e => e.key === active) const close = () => store.closeWorkspace() - if (active === 'artifacts') { - return - - - } if (active === 'agents') { const convRuns = runs.filter(r => r.kind !== 'tool') return diff --git a/src/store.ts b/src/store.ts index 5eeabb2..915aa94 100644 --- a/src/store.ts +++ b/src/store.ts @@ -43,7 +43,10 @@ export interface UIState { leftCollapsed: boolean rightWidth: number rightCollapsed: boolean - rightTab: string + /** 第三栏双入口:工作(当前任务/收件箱)/ 文件(搜索、预览、编辑、下载)。 */ + rightTab: 'work' | 'files' + /** 文件入口子视图:列表 / 预览(预览标签存在时才有意义)。 */ + filesView: 'list' | 'preview' /** 收件箱筛选:全部 / 待处理。铃铛打开时切到待处理。 */ inboxFilter: 'all' | 'todo' theme: ThemeChoice @@ -106,9 +109,10 @@ interface AppState { const DEFAULT_UI: UIState = { leftWidth: 260, leftCollapsed: false, - rightWidth: 340, + rightWidth: 360, rightCollapsed: false, - rightTab: 'inbox', + rightTab: 'work', + filesView: 'list', inboxFilter: 'all', activeWorkspace: null, theme: 'dark', @@ -134,6 +138,9 @@ function loadUI(): UIState { if (!isThemeChoice(ui.theme)) ui.theme = 'dark' if (!isFontSizeChoice(ui.fontSize)) ui.fontSize = 14 if (ui.inboxFilter !== 'all' && ui.inboxFilter !== 'todo') ui.inboxFilter = 'all' + // 旧本地偏好里存的是 'inbox'——第三栏双入口上线时归一到 'work'。 + if (ui.rightTab !== 'work' && ui.rightTab !== 'files') ui.rightTab = 'work' + if (ui.filesView !== 'list' && ui.filesView !== 'preview') ui.filesView = 'list' // 预览标签不进本地偏好。展开态若随刷新残留,会在没有标签时藏起对话。 ui.previewExpanded = false return ui @@ -160,19 +167,14 @@ function set(patch: Partial) { listeners.forEach(l => l()) } -// 预览区打开前右栏是否收起。null = 当前没有因为预览而改过右栏。 -let previewRightWasCollapsed: boolean | null = null - -function syncPreviewRail(hasTabs: boolean) { +// 第三栏「文件」入口联动(方案:预览并入第三栏):开第一张标签自动切到文件入口的 +// 预览子视图并展开右栏;关掉最后一张回到文件列表。不再收起右栏——预览与工作同居第三栏。 +function syncFilesEntry(hasTabs: boolean) { if (hasTabs) { - if (previewRightWasCollapsed === null) previewRightWasCollapsed = state.ui.rightCollapsed - if (!state.ui.rightCollapsed) setUI({ rightCollapsed: true }) + setUI({ rightTab: 'files', rightCollapsed: false, filesView: 'preview' }) return } - if (previewRightWasCollapsed === null) return - const restore = previewRightWasCollapsed - previewRightWasCollapsed = null - setUI({ rightCollapsed: restore }) + if (state.ui.filesView === 'preview') setUI({ filesView: 'list' }) } function setUI(patch: Partial) { @@ -367,7 +369,6 @@ export const store = { previewActive: { ...state.previewActive, [convId]: active }, }) } - syncPreviewRail((state.previewTabs[convId] ?? []).length > 0) const resumeSeq = await store.reconcile() store.connectLive(resumeSeq ?? state.lastSeq) }, @@ -818,12 +819,14 @@ export const store = { const conv = state.cards.filter(c => c.status === 'action_required').length const proj = state.projectCards.filter(c => c.status === 'action_required').length if (conv === 0 && proj > 0) store.setInboxScope('project') - setUI({ rightCollapsed: false, rightTab: 'inbox', inboxFilter: 'todo' }) + setUI({ rightCollapsed: false, rightTab: 'work', inboxFilter: 'todo' }) }, openInbox(cardId?: string) { - setUI({ rightCollapsed: false, rightTab: 'inbox' }) + setUI({ rightCollapsed: false, rightTab: 'work' }) if (cardId) setTimeout(() => flashLocate('.right-panel', 'data-card-id', cardId), 60) }, + /** 左栏/收起轨的「文件」入口:打开第三栏文件入口(列表或既有预览)。 */ + openFiles() { setUI({ rightTab: 'files', rightCollapsed: false }) }, // 卡→消息互定位另一半(D32):从收件箱卡滚回产生它的对话消息。 locateMessage(messageId?: string) { if (!messageId) return @@ -831,8 +834,8 @@ export const store = { }, openTracker(runId: string) { setUI({ trackerRunId: runId }) }, /** - * 工件预览唯一入口:在对话右侧预览区打开或激活标签。 - * 打开第一张标签时收起右栏,关掉最后一张时恢复,并收回展开态。 + * 工件预览唯一入口:在第三栏「文件」入口打开或激活标签(多文件多标签)。 + * 打开第一张自动切到预览子视图;关掉最后一张回到文件列表,并收回展开态。 */ requestPreview(a: Artifact) { const conv = previewTabsKey(state.currentConvId) @@ -841,7 +844,7 @@ export const store = { previewTabs: { ...state.previewTabs, [conv]: opened.tabs }, previewActive: { ...state.previewActive, [conv]: opened.activeId }, }) - syncPreviewRail(true) + syncFilesEntry(true) }, togglePreviewExpanded() { setUI({ previewExpanded: !state.ui.previewExpanded }) }, activatePreview(id: string) { @@ -856,7 +859,7 @@ export const store = { previewActive: { ...state.previewActive, [conv]: next.activeId }, }) if (next.tabs.length === 0) setUI({ previewExpanded: false }) - syncPreviewRail(next.tabs.length > 0) + syncFilesEntry(next.tabs.length > 0) }, openSettings(open: boolean) { setUI({ settingsOpen: open }) }, openProjectSettings(id: string | null) { setUI({ projectSettingsId: id }) }, diff --git a/src/styles.css b/src/styles.css index f0f98a6..40ceea0 100644 --- a/src/styles.css +++ b/src/styles.css @@ -24,11 +24,6 @@ button { font-family: inherit; font-size: inherit; color: inherit; } border: 1px solid var(--border-sidebar); } .topbar-name { font-weight: 650; letter-spacing: .01em; } -.topbar-project { color: var(--text-sidebar-dim); font-size: var(--fs-ui-sm); max-width: 240px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } -.topbar-stage { - font-size: var(--fs-ui-xs); padding: 2px 8px; border-radius: var(--radius-full); - border: 1px solid var(--border-sidebar); color: var(--text-sidebar); -} .topbar-spacer { flex: 1; } .topbar-bell { position: relative; width: 28px; height: 28px; display: inline-flex; align-items: center; justify-content: center; @@ -56,8 +51,12 @@ button { font-family: inherit; font-size: inherit; color: inherit; } .main { flex: 1; min-width: 0; display: flex; flex-direction: column; background: var(--bg); border: 1px solid var(--border-soft); border-radius: var(--radius-lg); } /* 作者 display 声明会压过 UA 的 [hidden],此处显式恢复;展开预览靠它隐藏对话列 */ .main[hidden] { display: none !important; } +/* 第三栏(工作|文件):文件入口承载预览,展开态铺满主区 */ .right-panel { background: var(--bg-panel); border: 1px solid var(--border-soft); border-radius: var(--radius-lg); display: flex; flex-direction: column; flex-shrink: 0; min-width: 0; } +.right-panel.expanded { flex: 1 1 auto; width: auto; } .right-panel.collapsed { width: 44px !important; } +/* 预览嵌进第三栏后去掉独立面板外壳(圆角/描边由右栏提供,纸面仍用 --bg) */ +.right-panel .preview-pane { flex: 1; min-height: 0; border: none; border-radius: 0; background: var(--bg); } /* ---- 通用控件 ---- */ .btn { @@ -196,9 +195,7 @@ button { font-family: inherit; font-size: inherit; color: inherit; } .tl-check { color: var(--text-dim); font-size: var(--fs-ui-sm); margin: 4px 0; } .tl-tools .status-error { margin-left: 8px; color: var(--err-text); } -/* 对话右侧预览区 */ -.preview-slot { flex-shrink: 0; min-width: 0; display: flex; } -.preview-slot.expanded { flex: 1 1 auto; width: auto; } +/* 第三栏「文件」入口内的多标签预览(外壳样式见 .right-panel .preview-pane) */ .preview-pane { flex: 1; min-width: 0; display: flex; flex-direction: column; background: var(--bg); border: 1px solid var(--border-soft); border-radius: var(--radius-lg); overflow: hidden; } .preview-pane-bar { display: flex; align-items: center; gap: 8px; padding: 6px 10px; border-bottom: 1px solid var(--border-soft); } .preview-pane-name { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-size: var(--fs-ui-sm); color: var(--text-dim); } @@ -256,6 +253,11 @@ button { font-family: inherit; font-size: inherit; color: inherit; } /* 输入区:悬浮卡片式输入壳(16px 大圆角 = DESIGN.md 特批例外) */ .input-bar { padding: 8px max(16px, 6%) 16px; background: var(--bg); border-radius: 0 0 var(--radius-lg) var(--radius-lg); } +/* 轻量状态提示行(快慢通道):一行以内,弱色;后台任务在回合结束后仍显示 */ +.input-status { display: flex; align-items: center; gap: 8px; padding: 0 4px 6px; color: var(--text-faint); font-size: var(--fs-ui-sm); } +.input-status-note { color: var(--text-faint); } +.input-status-link { border: none; background: none; color: var(--accent-text); font-size: var(--fs-ui-sm); cursor: pointer; padding: 0; } +.input-status-link:hover { text-decoration: underline; } .outbox-trigger { width: 100%; text-align: left; background: var(--bg-inset); color: var(--text-dim); border: 1px solid var(--border-soft); border-radius: var(--radius-xs); padding: 6px 10px; font-size: var(--fs-ui-sm); cursor: pointer; } .outbox-list { list-style: none; margin: 6px 0 0; padding: 0; display: flex; flex-direction: column; gap: 6px; } .outbox-item { display: flex; gap: 6px; align-items: center; background: var(--bg-inset); border-radius: var(--radius-xs); padding: 6px 8px; } @@ -345,8 +347,13 @@ button { font-family: inherit; font-size: inherit; color: inherit; } /* ---- 品牌标(侧栏 / 登录页 / 头像共用的星芒渐变底;辅色为品牌令牌 --brand-2) ---- */ .brand-mark { background: linear-gradient(135deg, var(--accent), var(--brand-2)); color: var(--text-on-accent); display: inline-flex; align-items: center; justify-content: center; flex: none; box-shadow: inset 0 1px 0 rgba(255,255,255,.22); } -/* ---- 左栏:任务中心(方案 A;Zai 复刻重造型) ---- */ -.tsb-brand { display: flex; align-items: center; gap: 9px; padding: 14px 14px 8px; } +/* ---- 左栏:项目导航(方案 A;三栏工作台重排:切换置顶、会话居中、文件沉底) ---- */ +.tsb-brand { display: flex; align-items: center; gap: 6px; padding: 12px 12px 8px; } +.tsb-projrow { flex: 1; min-width: 0; display: flex; align-items: center; gap: 7px; height: 32px; padding: 0 9px; border-radius: var(--radius); border: 1px solid var(--border); background: var(--bg-raised); color: var(--text); cursor: pointer; font-size: var(--fs-ui-caption); font-weight: 600; } +.tsb-projrow:hover { border-color: var(--accent-sidebar); } +.tsb-projrow-name { min-width: 0; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } +.tsb-projrow-stage { flex: none; font-size: var(--fs-ui-xs); font-weight: 450; color: var(--text-faint); border: 1px solid var(--border); border-radius: var(--radius-full); padding: 0 6px; } +.tsb-projrow .tsb-user-chev { color: var(--text-faint); display: inline-flex; flex: none; } .tsb-logo { width: 24px; height: 24px; border-radius: var(--radius); background: linear-gradient(135deg, var(--accent), var(--brand-2)); color: var(--text-on-accent); font-size: var(--fs-ui-sm); font-weight: 700; display: flex; align-items: center; justify-content: center; flex: none; } .tsb-name { font-weight: 650; font-size: var(--fs-ui-caption); letter-spacing: -0.01em; } .tsb-iconbtn { width: 26px; height: 26px; border-radius: var(--radius-sm); background: none; border: none; color: var(--text-faint); cursor: pointer; font-size: var(--fs-ui-caption); display: inline-flex; align-items: center; justify-content: center; position: relative; } @@ -404,6 +411,8 @@ button { font-family: inherit; font-size: inherit; color: inherit; } .tsb-row-act:hover { background: var(--bg-active); color: var(--text); } .tsb-footer { display: flex; align-items: center; gap: 4px; padding: 10px 12px; border-top: 1px solid var(--border-soft); border-radius: 0 0 var(--radius-lg) var(--radius-lg); } +/* 底部沉底区(方案:文件、工作区在账户上方) */ +.tsb-bottom { display: flex; flex-direction: column; gap: 1px; padding: 6px 10px 2px; border-top: 1px solid var(--border-soft); } .tsb-user { flex: 1; display: flex; align-items: center; gap: 9px; min-width: 0; height: 40px; padding: 0 7px; border: none; border-radius: var(--radius); background: none; cursor: pointer; text-align: left; transition: background .12s; } .tsb-user:hover { background: var(--bg-hover); } .tsb-avatar { width: 27px; height: 27px; border-radius: 50%; background: linear-gradient(135deg, var(--accent), var(--brand-2)); color: var(--text-on-accent); font-size: var(--fs-ui-sm); font-weight: 600; display: flex; align-items: center; justify-content: center; flex: none; box-shadow: inset 0 1px 0 rgba(255,255,255,.2); } diff --git a/src/workspaces.ts b/src/workspaces.ts index 0f16af4..ddf96ef 100644 --- a/src/workspaces.ts +++ b/src/workspaces.ts @@ -1,5 +1,6 @@ // 体系工具排位:服务端声明的工作区对所有用户可见,按 shortcut 数字占位; -// 内置「工件」「代理」补进空位。插件管理面板不进这条清单。 +// 内置「代理」补进空位。文件浏览在第三栏「文件」入口(方案:入口收敛),不再占体系工具位。 +// 插件管理面板不进这条清单。 export interface WorkspaceEntry { key: string @@ -16,7 +17,6 @@ export interface WorkspaceSource { } const BUILTINS: Omit[] = [ - { key: 'artifacts', title: '工件', icon: '📦' }, { key: 'agents', title: '代理', icon: '🤖' }, ] @@ -56,7 +56,6 @@ export function buildWorkspaceEntries(server: WorkspaceSource[]): WorkspaceEntry return 99 } ranked.push({ rank: takeFree(1), entry: BUILTINS[0] }) - ranked.push({ rank: takeFree(2), entry: BUILTINS[1] }) for (const entry of unranked) ranked.push({ rank: takeFree(9), entry }) return ranked .filter(item => item.rank <= 9)