# 使用说明 ## 仓库发现与面板布局 - **始终跟随当前工作空间**:面板经 Slot 标准 props(`useSessions` / `useWorkspaces`) 推导「当前会话 → 所属工作空间 → 路径」(回退 `recentWorkspaceId`),切换工作空间/ 会话时自动重新扫描;标题栏固定显示当前工作空间名(悬停可见完整路径)。 - 点击文件夹图标在系统文件管理器中打开当前工作空间目录:走插件自己的 `openInExplorer` RPC(host 半体起 `explorer.exe` 显式开新窗口,避开平台 `host.openPath`/Invoke-Item 对已打开目录只激活既有窗口、旧窗口不可见时 用户无感知的问题);host 半体未升级时自动回退平台 `workspaces.openPath`。 刷新图标始终重扫当前工作空间。未打开工作空间时主体显示空态、不发起扫描。 - 扫描根默认跟随当前工作空间路径(面板主动携带 root 调用,Host 不再回退 `sandboxPolicy.workspaceRoot`——在 dsh web 部署里那可能是无意义的进程启动目录); BFS 递归发现嵌套 Git 仓库(含 worktree 的 `.git` 文件形态);跳过 `node_modules`/ `dist`/`build` 等重目录;上限:深度 10 / 2000 目录 / 50 仓库(文件形态下可经组合行 config 的 `scanMaxDepth`/`scanMaxDirs`/`scanMaxRepos` 覆盖,见[安装详解](install.md))。 - **扫描结果两级缓存**:扫描结果按根目录缓存在内存中,并持久化到 `$DSH_HOME/git-panel/scan-cache.json`(上限 50 个根、7 天过期)。切换项目再切回、 或重启 dsh web 后首次打开,直接秒回缓存列表;命中时会并行校验每个仓库的 `.git` 是否仍存在(过滤已删除的仓库),并在后台静默重扫一次自我修正(新克隆的仓库在 下次切换/扫描时出现)。标题栏的刷新图标始终绕过缓存全量重扫,是兜底刷新手段。 - 每个仓库一张可折叠卡片:仓库名、当前分支、staged/unstaged/untracked 彩色圆点计数、 ↓↑ 落后/领先,以及刷新、Pull、分支、⋯ 更多按钮。 - **面板宽度可拖拽调整**:拖动左缘实时改宽(380px ~ 96vw),宽度记忆在 localStorage (`gp-panel-w`)。 - **布局模式(默认侧边栏停靠)**:标题栏齿轮按钮打开「面板设置」,两种模式即时切换, 偏好记忆在 localStorage(`gp-layout`): - **侧边栏模式(dock,默认)**:面板停靠在对话右侧,对话区域(含输入栏)自动收窄 让位,类似 VS Code 的 Chat 侧边栏。实现上通过宿主 AppFrame 覆盖层的稳定属性 `[data-shell-overlay]` 定位 frame,以 `padding-right` 挤压三栏 grid(`:has()` 选择器为主路径,不支持时自动退化为 JS 内联几何写路径);窗口宽度 <1200px 时该 模式临时按浮窗显示(不清除偏好),拉宽后自动恢复。 - **浮窗模式(overlay)**:面板浮在对话区域上方、带投影,不改变对话布局(v1.0 行为)。 - 已知小取舍:停靠时宿主原生「工具详情」列的自动收起阈值与拖拽手柄位置按 frame 外框宽计算,会略偏一个面板宽(仅影响该列悬停手柄的显形位置,无功能影响)。 - **中英双语**:跟随 DSH 语言设置(`locale` 服务 + `locale/change` 事件),Host 文案 经 `setLocale` RPC 同步切换。 - 所有图标为扁平 SVG 线性图标(stroke + currentColor,类 VS Code codicon),无 emoji。 ## 文件变更列表(VS Code Source Control 风格) - 分组 Staged Changes / Changes / Untracked Changes(大写小标题 + 计数 pill);每行 左侧彩色状态圆点 + 文件名(basename,悬停 title 显示完整路径)+ 目录 + 重命名来源 + 右侧状态字母徽标(M/A/D/R/U/C/T 着色)。 - **暂存即选择(无 checkbox)**:文件行悬停出现 +(暂存);Staged 组悬停出现 - (取消暂存);分组标题悬停可批量操作整组。 - 所有下拉菜单(分支/更多/规则)带全局透明遮罩,点击面板外任意区域自动关闭。 - 点击文件名从面板左缘滑出浮层 diff 抽屉(覆盖在聊天区上方,文件列表保持可见,点别的 文件直接切换):双列旧/新行号、整行柔和红绿底色、sticky @@ 分段头、文件头带状态 徽标与 +增/−删 统计;支持自动换行开关、左缘拖拽调宽(`gp-diff-w`)、Esc / 点遮罩关闭。 untracked 文件渲染为全新增(≤4000 行),untracked 目录渲染为两层目录树(≤200 条)。 ## 顶部提交区(仅处理已暂存文件) ``` ┌──────────────────────────────────────────────────────────┐ │ 提交信息输入框(自动撑高 2–6 行,Ctrl+Enter 提交) │ │ [✦ 生成] [⚙ 规则▾] 已暂存 N 个文件 │ │ [✓ 提交 ] [↑ 提交并推送 ] │ └──────────────────────────────────────────────────────────┘ 生成中(同一位置换成停止按钮,右侧是存活指示): │ [■ 停止] [⚙ 规则▾] ⟳ 生成中 12s · 已暂存 4 个文件 │ ``` - **生成 / 提交 / 提交并推送都只处理 Staged 文件**:Host 端 `commit` 不隐式 `git add`/`git reset`,只提交当前 index(部分提交用 pathspec 限定);提交信息经 stdin(`commit -F -`)传入,规避 Windows 命令行长度与特殊字符问题。 - **✦ 生成**:实时读当前生效规则 + 已暂存文件的 staged diff(总长 ≤120KB 截断)注入 LLM(`llm.stream`;模型优先取面板配置的生成模型,缺省取 `agentDefaultModel.currentSelection()`, 再回退第一个 provider/model;maxTokens 8000 / temperature 0.2),结果**只填入不提交**; 失败保留原内容并 toast 报错;生成中显示 spinner。 - **生成中可终止**:点击生成后「✦ 生成」立刻原位变为「■ 停止」(同一个位置、等宽, 右侧提示不会抖动),点它即调 `generateCancel` → Host 侧 `AbortController.abort()` 打断 `llm.stream`,不再继续消耗 token。**已经产出的内容会保留在输入框里**,不会清空, 可以接着手工改。可终止范围覆盖全流程:读 staged diff 的准备阶段(Host 逐文件检查终止 标志)与 LLM 流式阶段都能停。三态:空闲「✦ 生成」/ 生成中「■ 停止」/ 点击后 「⟳ 停止中…」(等 host 确认,期间禁用防重复点击)。停止晚于完成时提示 「生成已完成,未中断」,不算错误。 - **「正在生成」的视觉提示**(停止按钮本身是静止的,所以另有三处动效):① 提交区顶边 扫过一条光带;② 右侧存活指示「⟳ 生成中 12s ·」——转圈图标 + 递增秒数,秒数靠已有的 120ms 轮询刷新,不需要额外定时器;③ 输入框里逐字浮现的正文。另两个辅助状态: 「已暂存 N 个文件」始终保留,方便发现漏暂存;点停止后存活指示立即消失(不会「已经不 跑了还在转」)。关闭面板会顺带终止仍在跑的生成。 - **⚙ 规则 ▾**:编辑提交规则(编辑器顶部单选即切换当前仓库的生效来源)/ 复制生效规则到剪贴板(Host 经 `clip` 写入)/ 生成模型配置 / 显示当前生效来源 (仓库专属 > 全局 > 内置默认)。 - **提交 / 提交并推送**:整行按钮组,禁用态覆盖所有边界(消息为空、无暂存文件、任一 操作进行中)。提交并推送 = 提交成功后自动追加 push。 - **提交成功但推送失败 → 弹出收尾弹窗**:这是唯一需要用户明确选一条路的状态,所以用弹窗 而不是 4.6 秒后自行消失的 toast——弹窗说清「本地提交已保留、远端没有更新(分支 X)」, 并给三个出口(重试推送 / 取消上次提交 / 关闭)。报错原文**始终显示**在弹窗里的一个等宽框 中(没有展开收起、也没有复制按钮):git 自己的输出经 Host 失败的 RPC 带回(信封 `error.details.detail`,Client 摊平成 `res.detail`)。**只有「没有 upstream」这一类失败 不显示 git 原文**——它的原因是 git 自己那句话在各语言下不同、且重试永远好不了,所以那 一栏改为显示 Host 实测定稿的建议命令(`git push -u `,remote 取仓库 配置、缺省 origin,Client 不自己拼),框里永远有可执行的下一步。 - **重试推送**(最左,主按钮):只重推、不重新提交(提交已在本地),走与其它写操作同一把 锁(`busy = push`,重试期间提交 / Pull / 暂存全禁用);再失败时把新报错就地换掉旧的, 不关窗、不重新弹——**也不额外弹 toast**(同一条失败报两遍,且 toast 4.6 秒后自行消失, 只会留一段会自动消失的重复话术)。任何一条路径推送成功都会自动关掉该弹窗,避免它挂着 一个早已推上去的提交继续提供「取消上次提交」。 - **取消上次提交**:`git reset --soft HEAD~1`——撤销该提交、改动原样退回暂存区, 工作区内容不丢,并把提交信息恢复到输入框。撤销期间弹窗不关(按钮转圈并全禁用, **点遮罩、按 Esc 都当作无事发生**),**成功才收窗**:失败(例如 HEAD 已前移被 Host 拒绝) 时弹窗还在,重试推送这个出口不会跟着丢。⚠ 仅供尚未推送到远端的提交使用(已推送过再 撤销会造成本地与远端分叉)。撤销请求带弹窗出现时那次提交的 HEAD 短 hash,Host 侧比对 不上(例如弹窗还开着时用户又提交了一次)直接拒绝撤销,而不是撤错提交;倘若这个 hash 拿不到(status 读取失败),面板会先补读一次,仍拿不到就**不撤**——没有校验的撤销正是 这个出口要避免的事。仓库只有首个提交时也会提前给出可读结论,不走 `HEAD~1` 的 exit 128 原文(该前置校验只对「取消上次提交」这条 soft reset 路径生效;「更多操作 → Reset --hard」 仍是自己的危险确认弹窗 + git 原文)。 - **成功即收窗**:推送成功(任何一条路径)或撤销成功都会自动关掉弹窗——撤销成功后那个提交 已经不存在,若把弹窗留在屏幕上,按钮还能再点一次,而再按一次撤掉的是「新的 HEAD~1」, 也就是本不属于这次推送失败的提交。关闭弹窗本身不是失败——本地提交仍在,稍后可从 「更多操作 → Push」再推;弹窗开着时按 **Esc 或点遮罩**都能关(撤销进行中除外)。 - **内部约定**:Host 的「无上游」失败消息带 `[push-no-upstream]` 前缀,Client 在 RPC 边界 (`unwrapRpc`)统一剥掉,所以标记不会出现在任何用户可见文案里(toast 与弹窗同源); 要分流请读信封里的结构化字段 `reason: 'no-upstream'`,不要去匹配文案。 - **架构约定(血泪)**:本项目的 Client 曾是「一个巨型闭包 + 若干嵌套组件」的单文件, 跨组件引用局部函数或 state 时**语法完全合法**,但渲染到那一行就抛 `ReferenceError`、 整个面板 slot 崩掉。本项目因此崩过四次:`pushDetailOpen`(引用已删除的 state)、 `pushFailText` 与 `applyPushFail`(RepoCard 引用 CommitArea 的局部函数,后一个还把 一次成功的撤销报成「reset 失败」)、`isPushNoUpstream`(CommitArea 反过来引用 RepoCard 的局部函数)——并据此重构为 `src/client/` 多模块, 跨组件引用一律走 import/export,写错在打包期就报错,到不了运行期。仍然有效的两条规矩: 1. **共用逻辑放独立模块**(如 `api.js` 的 `isPushNoUpstream`、`lib/util.js` 的纯函数), 别塞进某个组件再从另一个组件引用; 2. **状态只在持有它的那一层写**——弹窗状态只在 `RepoCard` 写,`CommitArea` 只经 `onPushFail` / `onPushRetryFail` 回调上报;父层需要给子层判断依据时,给**只读查询** (如 `getPushRetryToken` / `isPushRetryStale`),不暴露 ref。 构建期守卫:`scripts/lint-client.mjs`(ESLint no-undef)兜住模块内部的漏 import / 拼错名,esbuild 打包兜住跨模块的漏导出(曾自研 AST 守卫顶过一阵,先后写错三版、 两次给出假安全)。`npm test` 里有 handleWriteResult 的 8 场景真值表与产物冒烟(bundle 装载 → factory → apply → 卸载)。 四次崩溃的完整表格与重构决策见 [架构与设计](architecture.md) 的 「Client 模块化:为什么不再用单文件闭包」一节。 ## 提交规则系统 - 存储:`$DSH_HOME/git-panel/rules/default.yaml`(全局)+ `{repo-name}-{路径哈希}.yaml` (仓库专属;路径哈希为规范化绝对路径的 FNV-1a 8 位十六进制,**同名仓库各持一份**, 重新克隆到原路径自动恢复生效)。旧版纯名字 `{repo-name}.yaml` 仅作读取回退, 下次保存自动迁移到新命名;首次扫描自动创建默认文件。`$DSH_HOME` 定位顺序: `settings.prepareDocument()` 返回路径推导 → `%USERPROFILE%\.dsh` 探测 → workspace 根 `.git-panel/rules` 兜底。 - 生效来源偏好:`$DSH_HOME/git-panel/git-repos.json`(**权威配置,非缓存**—— 永不 TTL/LRU 逐出,扫描不删除条目)。编辑器里切换「全局规则 / 仓库专属规则」 即写入 `ruleScope` 字段:`global` 时即使仓库文件存在也走全局;`repo` 但文件 缺失/非法时落回全局;无记录时按「仓库文件是否存在」推断(兼容旧版行为)。 保存仓库专属规则即自动置为 `repo`;「重置仓库专属」= 删除仓库规则文件并回退全局。 - 每次点击 ✦ 生成**实时读盘**,规则修改下次生成立即生效,无需重启。 - 编辑器弹窗:左 = `system_prompt` / `user_context` 双独立编辑框(键名固定展示不可编辑, 从根上避免误删 YAML 键),右 = 实时预览(占位符 `{repo_name}`/`{branch}`/`{file_list}`/ `{staged_diff}` 已替换),底部 = 保存 / 取消 / 恢复默认;顶部「全局规则 / 仓库专属规则」 单选直接切换生效来源(切到仓库专属时若文件不存在,以当前生效规则为底自动创建)。 - 内置默认规则内置于 `src/host.js`(**中英两版,跟随面板语言**):Conventional Commits 标题(type 白名单、scope 小写可省略、摘要动词开头 ≤50 字不加句号)、正文为要点式逻辑 变更清单(每行强制 "- " 前缀、一条一个改动点、保留参数/阈值等关键细节,3~8 条, 极简变更可只有标题;提示词内置 few-shot 格式示例防格式漂移)、footer 仅必要时输出、 消息语言跟随面板语言(除非代码库本身是其他语言)、只输出纯文本 commit message (无解释、无任何 Markdown 标记,标识符/路径裸写不加反引号)等;未编辑过的旧版默认 文件会随内置规则升级自动重写。 - **语言切换语义**:已创建的全局 `default.yaml` 仅当内容仍是未经修改的内置版本(中或 英)时才随语言切换重写为当前语言版;**用户编辑过的规则文件(全局/仓库专属)绝不因 语言切换被覆盖**;需要另一语言的默认版可用「重置全局默认」。仓库专属规则不受语言 影响。 ## Git 历史(每卡片可折叠) - 「历史」默认折叠;展开后为 SVG 图谱 + 无限滚动列表:Host 端 `git log --all --topo-order` 分页(每页 200 条,`--skip`/`-n`,滚动到底部前 800px 预取下一页), Client 端自行计算 lane 布局(≤8 条 lane 循环配色、合并/分支线为圆角肘形曲线、HEAD 节点外加光环、其余 lane 降透明度),面板高度 470px、行高 26px、字号 13。 - **悬停浮层**(VS Code hover 风格)显示提交详情:subject / author / email / date / 完整 message / diff stat,结果按 hash 缓存;离开行或浮层后延迟关闭,滚动立即关闭。 ## 其他操作 - Pull = `git fetch --all --prune` + `git merge --no-edit @{u}`(无上游则只 fetch)。 - 切换分支:下拉列表(含当前标记/上游)+ 新建分支(创建并切换,分支名校验)。 - ⋯ 更多:推送、Stash push / pop(列表展示已有 stash)、Reset `--soft|--hard HEAD~1`、 Clean untracked。 ## 合并冲突 `git status` 的未合并条目(`UU/AA/DD/UA/DU/AU/UD`)在 index 里带三个阶段,既不是 「已暂存」也不是「未暂存的改动」,因此单独成组,不再同时出现在暂存/未暂存两组里: - **冲突分组置顶**(含仓库头部的冲突计数徽标,卡片折叠时也能看见),该组的 + 即 `git add ` = 标记为已解决;解决后文件进入「暂存的更改」,照常提交即完成本次合并。 若解决结果与 HEAD 完全一致(例如整份取 ours),文件不会出现在任何分组里(index 相对 HEAD 没有差异),此时用提示条的「完成合并」收尾(见下)。 - **冲突文件行不给放弃按钮**:对未合并路径,`--ours/--theirs` 语义对用户是歧义, 放弃入口统一收敛到下面的整体出口。Host 端 `discard` 同样拒绝未合并路径(分组名归一化 与变更集校验两道口径都不认 `conflicted`),手工构造的 RPC 也放弃不了冲突文件。 - **提示条 + 中止合并**:只要有未解决冲突或 `MERGE_HEAD` 存在(含「冲突已全部标记但 未提交」),卡片顶部显示提示条;存在 `MERGE_HEAD` 时提示条右侧给出「中止合并」按钮, 执行 `git merge --abort` 把工作区恢复到合并前(走与 Reset/Clean 同级的确认弹窗, `⋯ 更多` 里也有同一入口)。stash pop 之类不产生 `MERGE_HEAD` 的冲突只提示解决方式。 - **完成合并(冲突已解决但无暂存差异)**:冲突全部标记为已解决、且解决结果与 HEAD 一致 时 `git status` 完全为空,提交按钮(要求 staged 非空)点不下去;此时提示条改文案并给出 「完成合并」按钮(悬停可见 git 将使用的提交信息,即 `MERGE_MSG`),执行 `git commit --no-edit` 完成合并。它与提交同级——只写一个提交、非破坏性,不弹确认窗。 若解决结果被**取消暂存**(或从未暂存),工作区里虽有内容但 index 与 HEAD 一致,提示条 会改说「索引没有可提交的内容」而不是宣称「与 HEAD 一致」;此时先点该行的 + 再提交, 或按当前索引收尾——未暂存的改动不会进入该合并提交,提交后的 toast 也会写明还有几个 未暂存文件未被包含。 - **rebase / cherry-pick / revert 冲突**:未合并条目同样进冲突组、同样用 + 标记为已解决, 但这三种状态不产生 `MERGE_HEAD`,所以面板不给「中止合并」也不给「完成合并」,提示条改说 明「当前进行的是 rebase/cherry-pick/revert,收尾请回终端 `--continue` / `--abort`」 (Host 侧靠 `REBASE_HEAD / CHERRY_PICK_HEAD / REVERT_HEAD` 判定,只在有冲突或 HEAD detached 时探测)。面板里点「提交」在 cherry-pick/rebase 下会被 git 接受并据此收尾, 但会用你填的提交信息替换掉原提交信息,提交按钮的悬停提示会写明这点。 - **未解决冲突时不给提交**:提交按钮在有未解决冲突时禁用并写明原因;Host 端 `opCommit` 在提交前也校验一次,直接返回「还有 n 个文件未解决」,不再把 git 的 "Committing is not possible because you have unmerged files" 原文抛给用户。 - **冲突文件 diff**:不走 git 的组合 diff(`diff --cc` 的双列前缀前端解析不了), 直接给工作区文件全文——里面本就带 `<<<<<<< / ======= / >>>>>>>` 标记,标记行在抽屉里 单独用 warn 色高亮(整份文件都是新增行,标记与正文同色就找不到冲突块边界)。 - Pull 遇冲突时错误文案直接给出未解决文件数;合并进行中再次 Pull 会快速失败并提示先 解决或用「中止合并」,不再抛 git 的 "You have not concluded your merge" 原文。 ## 审计日志 - **无审批门**:写操作(commit/pull/push/switch/stash/reset/clean/discard/merge-abort/ merge-commit)由面板用户显式点击触发后直接执行,Host 端没有第二道放行条件(与 VS Code 同侧重点);破坏性操作(`reset --hard`、Clean、放弃更改、中止合并)由 Client 弹一次确认窗 ——前三者在窗内标注不可恢复,中止合并则提示先备份已解决的内容(它丢弃的是本次合并与 解决成果,已提交的历史不受影响)。 - 写操作与其结果(ok/fail)写入 `$DSH_HOME/git-panel/logs/git-YYYY-MM-DD.log` (`[ISO时间] key=value` 格式;写入经 promise 链串行化防并发丢行;条目含 scan / diff / generate / rules-save / rules-reset / ok:git.* / fail:git.* 等)。 - 高频只读轮询(status/log)**不写审计**,避免日志噪声淹没写操作记录;`$DSH_HOME` 无法定位时打印一次告警后跳过审计,不影响功能。