diff --git a/docs/features/assistant-workbar/progress.md b/docs/features/assistant-workbar/progress.md index 062884e3..76ba1e0f 100644 --- a/docs/features/assistant-workbar/progress.md +++ b/docs/features/assistant-workbar/progress.md @@ -1,5 +1,14 @@ # 工作栏实现与验证进度 +## 2026-10-05:文件列表只保留修改时间 + +按 [FR-WF3、US-WF2](./workspace-files.md) 移除创建时间列及排序入口,修改时间采用紧凑格式, +完整日期保留在悬停提示中。更多操作与时间共用行尾空间,悬停、聚焦及菜单展开时文件名宽度不变。 +本机与远程共用 Renderer,无文件访问或 Agent 协议变化。 + +验证:`WorkspaceFilesPanel.test.tsx` 41 项通过,Renderer TypeScript 检查、相关文件 ESLint +及 `git diff --check` 通过。尚未进行真实 Electron 窗口视觉验收。 + ## 2026-10-05:Runtime 提交失败与启动取消 本机 OpenCode 提交失败后会结束所属 SSE 等待;远程原生 Session 启动等待期间, @@ -12,7 +21,7 @@ Windows/Linux x64 实测、真实数据检查及本轮 5 次文本模型调用 ## 2026-10-04:移除成果页,工作区导入与文件时间 对应 [FR-WF1~FR-WF3、US-WF1~US-WF3](./workspace-files.md)。成果 Tab、目录注册及独立导入 -界面已移除,旧布局过滤成果实例。工作区显示实际文件的修改时间与可用创建时间,支持同级排序, +界面已移除,旧布局过滤成果实例。工作区显示实际文件的修改时间(2026-10-05 收敛为唯一时间列),支持同级排序, 向所选目录导入原件。本机及远程共用分块管理动作,同名文件不覆盖,失败项独立反馈并清理部分文件。 来源检查确认:`AssistantDatabase.createImageArtifact` 把图片保存为 `artifacts.inline_content` diff --git a/docs/features/assistant-workbar/workspace-files.md b/docs/features/assistant-workbar/workspace-files.md index f1326c69..68528eb4 100644 --- a/docs/features/assistant-workbar/workspace-files.md +++ b/docs/features/assistant-workbar/workspace-files.md @@ -15,16 +15,17 @@ Host 的项目目录。支持二进制和空文件,不调用文档解析器, 显示。写入失败时尝试移除本次新建的部分文件,清理失败则报告残留路径。取消文件选择不写入, 导入期间禁用重复提交。完成后刷新目录。当前不提供传输中取消或进度百分比。 -FR-WF3:名称默认升序。名称、修改时间和有可用数据时的创建时间列均可点击切换排序;首次 +FR-WF3:文件列表仅显示名称和修改时间,不显示创建时间列。名称默认升序,两列均可点击切换排序;首次 选择时间列按最新优先。每一级目录独立排序且目录优先,缺失时间放在同类条目末尾,显示 `-`。 -每目录沿用最多 500 条的读取上限,排序只覆盖已读取条目,截断提示保留。窄栏横向滚动仅发生在 -文件视口,工具栏保持可见。 +每目录沿用最多 500 条的读取上限,排序只覆盖已读取条目,截断提示保留。修改时间常显月日和时分, +悬停显示完整日期时间;行悬停、键盘聚焦或菜单展开时,在时间位置显示更多操作,文件名宽度保持不变。 +窄栏中文件名省略,不因时间列撑宽列表。 US-WF1:选中子目录后导入二进制及空文件,目录刷新后可见,原文件不变。同名项失败而已有 内容保留;成功项仍落在所选目录。 -US-WF2:切换排序后文件按真实文件系统时间显示;不支持创建时间的文件系统不显示创建时间列, -混合数据中缺失值保留为空。Linux 的 inode 状态变更时间不冒充创建时间。 +US-WF2:切换排序后文件按真实修改时间显示;即使文件系统提供创建时间,列表也只显示修改时间。 +缺失修改时间显示 `-`。文件夹使用目录本身的修改时间,不递归计算子文件的最新时间。 US-WF3:升级前会话图片在聊天和重新打开应用后仍可读取;打开工作区不会把会话图片或附件复制 到项目目录。生成的普通文件由 Runtime 文件工具写入工作区并出现在同一目录列表。 diff --git a/src/renderer/src/WorkspaceFilesPanel.test.tsx b/src/renderer/src/WorkspaceFilesPanel.test.tsx index 660f8053..ddbb4b1a 100644 --- a/src/renderer/src/WorkspaceFilesPanel.test.tsx +++ b/src/renderer/src/WorkspaceFilesPanel.test.tsx @@ -35,10 +35,10 @@ describe('WorkspaceFilesPanel', () => { }) }) - it('sorts siblings by real modification times, keeps directories first and omits unavailable creation times', async () => { + it('sorts siblings by modification time and shows only that time even when creation times are available', async () => { const { container } = render( ({ path, truncated: false, entries: [ - { name: 'old', path: 'old', type: 'file', modifiedAt: '2026-01-01T00:00:00.000Z' }, + { name: 'old', path: 'old', type: 'file', modifiedAt: '2026-01-01T00:00:00.000Z', createdAt: '2025-12-01T00:00:00.000Z' }, { name: 'new', path: 'new', type: 'file', modifiedAt: '2026-10-01T00:00:00.000Z' }, { name: 'unknown', path: 'unknown', type: 'file' }, { name: 'docs', path: 'docs', type: 'directory' } @@ -50,6 +50,9 @@ describe('WorkspaceFilesPanel', () => { expect(names()).toEqual(['docs', 'old', 'new', 'unknown']) expect(screen.queryByRole('button', { name: '创建时间' })).not.toBeInTheDocument() expect(container.querySelector('time[datetime="2026-01-01T00:00:00.000Z"]')).toBeInTheDocument() + expect(container.querySelectorAll('time')).toHaveLength(4) + expect(container.querySelector('time[datetime="2025-12-01T00:00:00.000Z"]')).not.toBeInTheDocument() + expect(container.querySelector('time[datetime="2026-01-01T00:00:00.000Z"]')).toHaveAttribute('title', new Date('2026-01-01T00:00:00.000Z').toLocaleString()) }) it('groups view switching and refresh, and only shows actions for the active view', async () => { const onRefresh = vi.fn(async () => undefined) diff --git a/src/renderer/src/WorkspaceFilesPanel.tsx b/src/renderer/src/WorkspaceFilesPanel.tsx index 6c670414..49863be5 100644 --- a/src/renderer/src/WorkspaceFilesPanel.tsx +++ b/src/renderer/src/WorkspaceFilesPanel.tsx @@ -94,7 +94,7 @@ export function flattenFileTree( expandedPaths: ReadonlySet, loadingPaths: ReadonlySet, errors: Readonly>, - sort: { field: 'name' | 'modifiedAt' | 'createdAt'; descending: boolean } = { field: 'name', descending: false } + sort: { field: 'name' | 'modifiedAt'; descending: boolean } = { field: 'name', descending: false } ): FileTreeRow[] { const rows: FileTreeRow[] = [] const visit = (items: readonly WorkspaceDirectoryEntry[], depth: number): void => { @@ -194,7 +194,7 @@ export function WorkspaceFilesPanel({ const [refreshing, setRefreshing] = useState(false) const [importing, setImporting] = useState(false) const importingRef = useRef(false) - const [sort, setSort] = useState<{ field: 'name' | 'modifiedAt' | 'createdAt'; descending: boolean }>({ field: 'name', descending: false }) + const [sort, setSort] = useState<{ field: 'name' | 'modifiedAt'; descending: boolean }>({ field: 'name', descending: false }) const repositoryKnown = useRef<{ projectId?: string; available: boolean }>({ available: false }) useEffect(() => { if (!gitError && isRepository !== undefined) repositoryKnown.current = { projectId, available: isRepository } }, [gitError, isRepository, projectId]) const showGit = isRepository === true || (Boolean(gitError) && repositoryKnown.current.projectId === projectId && repositoryKnown.current.available) @@ -477,7 +477,7 @@ export function WorkspaceFilesPanel({ {expanded ? : } {entry.name} - {renderTimes(entry)} + {renderModifiedTime(entry)} {entryMenu(entry)} ) @@ -505,20 +505,18 @@ export function WorkspaceFilesPanel({ )} - {renderTimes(entry)} + {renderModifiedTime(entry)} {entryMenu(entry)} ) } const root = listings[browsedPath] - const showCreated = useMemo(() => Object.values(listings).some(listing => listing.entries.some(entry => entry.createdAt)), [listings]) - const renderTimes = (entry: WorkspaceDirectoryEntry): React.JSX.Element => <> - {(['modifiedAt', ...(showCreated ? ['createdAt' as const] : [])] as const).map(field => - )} - + const renderModifiedTime = (entry: WorkspaceDirectoryEntry): React.JSX.Element => ( + + ) const treeRows = useMemo( () => flattenFileTree(root?.entries ?? [], listings, expandedPaths, loadingPaths, errors, sort), [errors, expandedPaths, listings, loadingPaths, root, sort] @@ -566,7 +564,7 @@ export function WorkspaceFilesPanel({ ) return ( -
+
{diff &&
)}
{treeWindow.segments.map((segment) => { diff --git a/src/renderer/src/styles.css b/src/renderer/src/styles.css index 21f24703..f36e7363 100644 --- a/src/renderer/src/styles.css +++ b/src/renderer/src/styles.css @@ -5563,29 +5563,23 @@ button > svg { .workspace-files__row { padding: var(--space-2); - padding-right: 38px; + min-width: 0; grid-template-columns: auto auto minmax(0, 1fr) auto; } .workspace-files__entry { position: relative; display: grid; - grid-template-columns: minmax(160px, 1fr) 152px; + grid-template-columns: minmax(0, 1fr) 112px; align-items: center; - padding-right: 32px; } .workspace-files__columns { display: grid; - grid-template-columns: minmax(160px, 1fr) 152px; - padding-right: 32px; + grid-template-columns: minmax(0, 1fr) 112px; border-bottom: 1px solid var(--border-subtle); } -.workspace-files[data-created-time="true"] :is(.workspace-files__entry, .workspace-files__columns) { - grid-template-columns: minmax(160px, 1fr) 152px 152px; -} - .workspace-files__columns button { min-height: 32px; border: 0; @@ -5596,11 +5590,21 @@ button > svg { } .workspace-files__time { + overflow: hidden; + padding-right: var(--space-2); color: var(--text-muted); font-size: var(--font-caption); + text-align: right; + text-overflow: ellipsis; white-space: nowrap; } +.workspace-files__entry:hover .workspace-files__time, +.workspace-files__entry:focus-within .workspace-files__time, +.workspace-files__entry:has(.workspace-files__more[aria-expanded="true"]) .workspace-files__time { + opacity: 0; +} + .workspace-files__destination { overflow-wrap: anywhere; color: var(--text-secondary); @@ -5667,11 +5671,7 @@ button > svg { } .workspace-files__tree { - min-width: 344px; -} - -.workspace-files[data-created-time="true"] .workspace-files__tree { - min-width: 496px; + min-width: 0; } .workspace-files__children { @@ -19493,7 +19493,10 @@ textarea.field-control { .workspace-git__commit { display: grid; width: 100%; padding: var(--space-2); grid-template-columns: minmax(0, 1fr); gap: var(--space-1); border-bottom: 1px solid var(--border-subtle); text-align: left; } .workspace-git__commit strong, .workspace-git__commit small { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } .workspace-git__commit span, .workspace-git__commit small { color: var(--text-secondary); font-size: var(--font-caption); } -@media (hover: none), (pointer: coarse) { .workspace-files__more { opacity: 1; } } +@media (hover: none), (pointer: coarse) { + .workspace-files__more { opacity: 1; } + .workspace-files__time { padding-right: 36px; } +} .document-result-preview { display: flex; flex-direction: column;