fix: simplify workspace file list metadata
Show only modification time beside file names and share its space with hover actions. Remove the creation-time column and fixed minimum tree width to keep narrow workspace panels compact. Release note: Workspace file lists now show only modification time and keep file names stable when more actions appear.
This commit is contained in:
@@ -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`
|
||||
|
||||
@@ -15,16 +15,17 @@ Host 的项目目录。支持二进制和空文件,不调用文档解析器,
|
||||
显示。写入失败时尝试移除本次新建的部分文件,清理失败则报告残留路径。取消文件选择不写入,
|
||||
导入期间禁用重复提交。完成后刷新目录。当前不提供传输中取消或进度百分比。
|
||||
|
||||
FR-WF3:名称默认升序。名称、修改时间和有可用数据时的创建时间列均可点击切换排序;首次
|
||||
FR-WF3:文件列表仅显示名称和修改时间,不显示创建时间列。名称默认升序,两列均可点击切换排序;首次
|
||||
选择时间列按最新优先。每一级目录独立排序且目录优先,缺失时间放在同类条目末尾,显示 `-`。
|
||||
每目录沿用最多 500 条的读取上限,排序只覆盖已读取条目,截断提示保留。窄栏横向滚动仅发生在
|
||||
文件视口,工具栏保持可见。
|
||||
每目录沿用最多 500 条的读取上限,排序只覆盖已读取条目,截断提示保留。修改时间常显月日和时分,
|
||||
悬停显示完整日期时间;行悬停、键盘聚焦或菜单展开时,在时间位置显示更多操作,文件名宽度保持不变。
|
||||
窄栏中文件名省略,不因时间列撑宽列表。
|
||||
|
||||
US-WF1:选中子目录后导入二进制及空文件,目录刷新后可见,原文件不变。同名项失败而已有
|
||||
内容保留;成功项仍落在所选目录。
|
||||
|
||||
US-WF2:切换排序后文件按真实文件系统时间显示;不支持创建时间的文件系统不显示创建时间列,
|
||||
混合数据中缺失值保留为空。Linux 的 inode 状态变更时间不冒充创建时间。
|
||||
US-WF2:切换排序后文件按真实修改时间显示;即使文件系统提供创建时间,列表也只显示修改时间。
|
||||
缺失修改时间显示 `-`。文件夹使用目录本身的修改时间,不递归计算子文件的最新时间。
|
||||
|
||||
US-WF3:升级前会话图片在聊天和重新打开应用后仍可读取;打开工作区不会把会话图片或附件复制
|
||||
到项目目录。生成的普通文件由 Runtime 文件工具写入工作区并出现在同一目录列表。
|
||||
|
||||
@@ -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(<WorkspaceFilesPanel projectId="project" changedFiles={[]} onLoadDiff={vi.fn()} onOpenFile={vi.fn()}
|
||||
onListDirectory={async path => ({ 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)
|
||||
|
||||
@@ -94,7 +94,7 @@ export function flattenFileTree(
|
||||
expandedPaths: ReadonlySet<string>,
|
||||
loadingPaths: ReadonlySet<string>,
|
||||
errors: Readonly<Record<string, string | undefined>>,
|
||||
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 ? <FolderOpen size={15} /> : <Folder size={15} />}
|
||||
<span title={entry.path}>{entry.name}</span>
|
||||
</button>
|
||||
{renderTimes(entry)}
|
||||
{renderModifiedTime(entry)}
|
||||
{entryMenu(entry)}
|
||||
</div>
|
||||
)
|
||||
@@ -505,20 +505,18 @@ export function WorkspaceFilesPanel({
|
||||
</small>
|
||||
)}
|
||||
</button>
|
||||
{renderTimes(entry)}
|
||||
{renderModifiedTime(entry)}
|
||||
{entryMenu(entry)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
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 =>
|
||||
<time className="workspace-files__time" key={field} dateTime={entry[field]} title={entry[field] ? new Date(entry[field]).toLocaleString() : undefined}>
|
||||
{entry[field] ? new Date(entry[field]).toLocaleString(undefined, { year: 'numeric', month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit' }) : '-'}
|
||||
</time>)}
|
||||
</>
|
||||
const renderModifiedTime = (entry: WorkspaceDirectoryEntry): React.JSX.Element => (
|
||||
<time className="workspace-files__time" dateTime={entry.modifiedAt} title={entry.modifiedAt ? new Date(entry.modifiedAt).toLocaleString() : undefined}>
|
||||
{entry.modifiedAt ? new Date(entry.modifiedAt).toLocaleString(undefined, { month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit', hour12: false }) : '-'}
|
||||
</time>
|
||||
)
|
||||
const treeRows = useMemo(
|
||||
() => flattenFileTree(root?.entries ?? [], listings, expandedPaths, loadingPaths, errors, sort),
|
||||
[errors, expandedPaths, listings, loadingPaths, root, sort]
|
||||
@@ -566,7 +564,7 @@ export function WorkspaceFilesPanel({
|
||||
</pre>
|
||||
)
|
||||
return (
|
||||
<div ref={panelRef} className="workspace-files" data-created-time={showCreated}>
|
||||
<div ref={panelRef} className="workspace-files">
|
||||
{diff && <section className="assistant-sidebar__preview" aria-busy={!diff.value && !diff.error}>
|
||||
<header>
|
||||
<button ref={backRef} className="assistant-sidebar__back" type="button" onClick={() => {
|
||||
@@ -667,8 +665,8 @@ export function WorkspaceFilesPanel({
|
||||
<>
|
||||
<div ref={treeRef} className="workspace-files__tree" onBlur={treeWindow.onBlur} onFocus={treeWindow.onFocus}>
|
||||
<div className="workspace-files__columns">
|
||||
{(['name', 'modifiedAt', ...(showCreated ? ['createdAt' as const] : [])] as const).map(field => <button type="button" key={field} aria-pressed={sort.field === field} onClick={() => setSort(current => ({ field, descending: current.field === field ? !current.descending : field !== 'name' }))}>
|
||||
{t(field === 'name' ? 'management.name' : field === 'modifiedAt' ? 'management.modified' : 'management.created')}{sort.field === field ? (sort.descending ? ' ↓' : ' ↑') : ''}
|
||||
{(['name', 'modifiedAt'] as const).map(field => <button type="button" key={field} aria-pressed={sort.field === field} onClick={() => setSort(current => ({ field, descending: current.field === field ? !current.descending : field !== 'name' }))}>
|
||||
{t(field === 'name' ? 'management.name' : 'management.modified')}{sort.field === field ? (sort.descending ? ' ↓' : ' ↑') : ''}
|
||||
</button>)}
|
||||
</div>
|
||||
{treeWindow.segments.map((segment) => {
|
||||
|
||||
+18
-15
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user