文件页(FileTree/TreePanel)
- Ctrl/Cmd 多选 + Shift 按可见行序连选(Esc/点空白/右键收敛),批量条支持复制路径、批量删除(共享 ConfirmDialog + 顺序 fsRemove)
- Git 变更着色:行名按状态上色 + M/A/D/U 徽标,目录按 dirHasChanges 着色,非仓库静默降级(数据来自共享 git 状态存储)
- 新建文件夹:fs.mkdir 路由 + mkdirWorkspaceEntry + 内联输入行(Enter/Esc/blur/IME 守卫)
- 性能:FileRow/DirRow memo(含反向验证)、搜索 hidden 停车不再卸载树、level 拉取世代守卫 + AbortController、fs.tree truncated 提示、use-dir-watch 入参 memo
- 拖拽上传视觉 token 化(硬编码色清零);可见性下传后隐藏页签停止轮询
- 右键「打开方式」改为复用 DSH 自带 open-in-app(宿主探测的关联应用 + 文件管理器显示)
文件变动页(changes/diff)
- 36px 头(宿主 SegmentedControl)+ 单一 ChangeRow + SectionHeader 分组 + 吸底提交条单状态行 + 历史独立空态
- 修掉:双份空态、暂存失败 unhandled rejection、手动刷新被丢、仓库切换刷新两次、worktree 清单缺 repoRoot、工作树被自动切走、预览持旧快照、恢复高度未 clamp、session ops 无谓轮询
- DiffPane/DiffTab 共用 use-git-diff;修掉 DiffPane 的 untracked 预览从不渲染
全局组件与宿主接入
- 新增 src/client/ui(IconButton/Chip/SectionHeader/Notice/ConfirmDialog/StatusBadge + 共享 git 状态存储)
- 新增 src/client/open-in-app.ts(宿主 open-in-app 客户端适配),删除自研 open-with 全链路与 open.external 路由
- 客户端 inject 补 remote / remote.session:缺声明时 ctx.get('remote').session 会抛出并把整个面板打成错误条(真实挂载 lane 抓到)
- 词典:+25 键 / -20 openWith* / -noChanges,20 份同步
验证:typecheck / lint / test(126 文件 1322 通过)/ build 全绿;
DSH_CMD='npx -y --package @deepseek-ai/dsh@0.1.7-rc.1 dsh' pnpm test:mount 7/7 通过(含 tab sweep 与 perf lane)
63 lines
2.5 KiB
TypeScript
63 lines
2.5 KiB
TypeScript
/**
|
|
* Full-window upload progress over the files tree: a blurred scrim (same mask
|
|
* token as the repo's Modal primitive) with a card showing the target
|
|
* directory, file-level progress, and a cancel button. Esc cancels too —
|
|
* clicking the scrim does not, so a stray click can never abort an upload.
|
|
* Rendered inside TreePanel (absolute inset-0), so it covers only the file
|
|
* window and never the conversation column.
|
|
*/
|
|
import { useEffect, type ReactNode } from 'react'
|
|
import { IconUploadOutline16 } from './icons.tsx'
|
|
import { t } from './locales.ts'
|
|
import { uploadHintText } from './upload.ts'
|
|
import css from './sidebar.module.css'
|
|
|
|
export function UploadOverlay(props: {
|
|
/** Absolute upload directory (the session workspace or a tree directory). */
|
|
dir: string
|
|
done: number
|
|
total: number
|
|
/** Relative path of the file being uploaded ('' when none is in flight). */
|
|
current: string
|
|
onCancel: () => void
|
|
/** True while cancellation is in flight (disables the cancel button). */
|
|
cancelling?: boolean
|
|
}): ReactNode {
|
|
const { dir, done, total, current, onCancel, cancelling } = props
|
|
// Esc cancels (MermaidModal-style native listener; the overlay is not a
|
|
// focus-trapped dialog, so a window listener is the honest scope).
|
|
useEffect(() => {
|
|
const onKey = (event: KeyboardEvent): void => {
|
|
if (event.key === 'Escape') onCancel()
|
|
}
|
|
window.addEventListener('keydown', onKey)
|
|
return () => { window.removeEventListener('keydown', onKey) }
|
|
}, [onCancel])
|
|
|
|
const percent = total === 0 ? 0 : Math.min(100, Math.round((done / total) * 100))
|
|
return (
|
|
<div className={css.uploadOverlay} role="dialog" aria-modal="true" aria-label={t('uploadingTo', { dir })}>
|
|
<div className={css.uploadOverlayCard}>
|
|
<div className={css.uploadOverlayTitle} title={dir}>
|
|
<IconUploadOutline16 size={14} />
|
|
<span>{t('uploadingTo', { dir })}</span>
|
|
</div>
|
|
<div
|
|
className={css.uploadOverlayProgress}
|
|
role="progressbar"
|
|
aria-valuemin={0}
|
|
aria-valuemax={total}
|
|
aria-valuenow={done}
|
|
aria-valuetext={t('uploadProgress', { done, total, name: current })}
|
|
>
|
|
<div className={css.uploadOverlayProgressFill} style={{ width: `${percent}%` }} />
|
|
</div>
|
|
<div className={css.uploadOverlayStatus}>{uploadHintText(done, total, current, dir, t)}</div>
|
|
<button type="button" className={css.uploadOverlayCancel} disabled={cancelling} onClick={onCancel}>
|
|
{t('cancel')}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|