Skip to content

feat(git): git worktree 支持与侧边栏项目分组 - #404

Open
Mieluoxxx wants to merge 6 commits into
Stack-Cairn:mainfrom
Mieluoxxx:feat/git-worktree-groups
Open

feat(git): git worktree 支持与侧边栏项目分组#404
Mieluoxxx wants to merge 6 commits into
Stack-Cairn:mainfrom
Mieluoxxx:feat/git-worktree-groups

Conversation

@Mieluoxxx

@Mieluoxxx Mieluoxxx commented Aug 9, 2026

Copy link
Copy Markdown
Contributor

Closes #403

概述

为分支选择器添加 git worktree 创建/移除能力,并在侧边栏支持项目分组,worktree 自动聚合到原始仓库项目下。桌面端(agent-gui)与 Gateway WebUI 均已同步支持。

Worktree

  • 在「创建新分支」旁新增「新建 Worktree」入口:创建于 ~/.liveagent/worktree/<repo_id>/<name>(repo_id = 规范化仓库根路径的清洗名 + fnv1a64 哈希),检出同名新分支,并在侧边栏打开为工作区。
  • 被 worktree 检出的分支现在提供「删除 Worktree」(移除 worktree + 分支;含未提交改动时有强制移除的二次确认),替代了 git 必然拒绝的分支删除。
  • 后端在移除前校验路径必须在 git worktree list 登记列表中。
  • WebUI 通过 Gateway Git 动作提供相同能力,并复用同一套路径安全校验;写操作遵循 enableWebGit 门控。

侧边栏分组

  • 项目列表分组 CRUD(新建/重命名/删除/移动/折叠),桌面端与 WebUI 行为一致。
  • Worktree 自动聚合到以原始仓库项目命名的分组;分组按 sourceProjectPath 匹配,重命名后仍复用;嵌套 worktree 溯源回原始仓库。
  • 区块按组内最早成员排序(pinned/running 提升整组);折叠视图不拆组。
  • WebUI 分组设置经 Gateway settings 同步与桌面端保持一致。

修复

  • workspaceProjectGroups 缺失于设置保存白名单导致重启丢失——现已持久化,含 round-trip 测试。
  • Worktree 名称输入框禁用自动大写/自动更正。

验证

  • cargo test:676 通过(含 worktree 创建/移除/校验与设置 round-trip)
  • 前端:tsc --noEmit、1460 个 node 测试、biome 干净(仅基线警告)
  • cargo fmt --check 干净
  • Gateway:WebUI 测试 546 通过、Go 测试全绿、WebUI 生产构建通过

Screenshots / preview

新建 Worktree 弹窗(桌面端):

new-worktree-dialog

侧边栏项目分组(worktree 自动聚合到源项目分组):

workspace-groups

说明

  • 桌面端与 Gateway WebUI 行为对齐;WebUI 的 worktree/分组操作受 enableWebGit 写权限门控。

@StackCairn
StackCairn marked this pull request as draft August 9, 2026 06:16
@github-actions

github-actions Bot commented Aug 9, 2026

Copy link
Copy Markdown
Contributor

PR governance checks passed. Awaiting human review.

@Mieluoxxx Mieluoxxx changed the title feat(git): git worktree support with sidebar project grouping feat(git): git worktree 支持与侧边栏项目分组 Aug 9, 2026
@Mieluoxxx
Mieluoxxx marked this pull request as ready for review August 9, 2026 06:18
@StackCairn
StackCairn marked this pull request as draft August 9, 2026 06:19
@Mieluoxxx

Copy link
Copy Markdown
Contributor Author
522a8cb9eefb87a98a67c46eff72c022 4052a686e0fadd74616db67db2786ce9

@Mieluoxxx
Mieluoxxx marked this pull request as ready for review August 9, 2026 06:19
@StackCairn
StackCairn marked this pull request as draft August 9, 2026 06:19
@su-fen

su-fen commented Aug 9, 2026

Copy link
Copy Markdown
Member

希望webui同步功能呢

@Mieluoxxx
Mieluoxxx marked this pull request as ready for review August 9, 2026 11:36
@StackCairn
StackCairn marked this pull request as draft August 9, 2026 11:36
@Mieluoxxx
Mieluoxxx marked this pull request as ready for review August 9, 2026 11:40
@su-fen

su-fen commented Aug 9, 2026

Copy link
Copy Markdown
Member

难绷,又有冲突了

@coder-hhx coder-hhx left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

整体检视了一遍,质量很高:worktree 名称校验(check-ref-format + 路径分隔符/控制字符拒绝)、移除前的登记列表校验、enableWebGit 写门控正反向测试、桌面端与 WebUI 的 i18n key 完全一致、settings 白名单修复带 round-trip 测试,Rust 侧 8 个 worktree 测试覆盖了 stale prune 和 force 保护等边缘场景。👍

发现 1 个建议合并前修复的功能问题(M1)和 3 个小问题,已按行内评论标注:

  • M1(建议修复):git worktree list --porcelain 的第一个条目是主工作树本身(带 branch refs/heads/<当前分支>),git_worktrees_sync 未过滤。在 worktree 工作区打开分支选择器时,主仓库检出的分支会被误判为"被 worktree 检出",删除入口错误显示为"删除 Worktree",确认后 git 拒绝移除主工作树,用户看到报错。无数据损失(git 自身兜底),但属于新功能日常路径上的语义错误。
  • L1:assignWorkspaceProjectToGroup 结尾 return touched ? next : next; 两分支相同,touched 无效,幂等调用也会触发 settings 写入。
  • L2:git_remove_worktree_syncfs::canonicalize(用户输入) 要求路径存在,stale worktree(目录已被手动删)走不了该 API;好在 delete_branch 的 prune 前置已覆盖。仅提示。
  • L3:repo_worktree_id 把 fnv1a64 截断为 32 位,建议用满 64 位,零成本消除碰撞顾虑。非阻塞。

结论:建议修复 M1 后合并(L1 顺手带上,L2/L3 可 follow-up)。CI 已全绿。

}
}
if !path.is_empty() {
worktrees.push(GitWorktreeInfo { path, branch });

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

M1:主工作树混入列表。 git worktree list --porcelain 的第一个 block 是主仓库自身,且带 branch refs/heads/<当前分支>(已在本地 git 实测确认)。这里未过滤,导致前端把"被主仓库检出的分支"当成"被 linked worktree 检出"。

触发场景:在 worktree 工作区打开分支选择器 → 主仓库检出的分支(如 main,对当前工作区非 current,满足删除入口显示条件)被匹配到主仓库路径 → 入口显示"删除 Worktree"而非"删除分支" → 确认后执行 git worktree remove <主仓库路径> → git 拒绝,用户看到报错。

建议:解析时跳过第一个 block,或过滤 path == repo_root 的条目。同时建议 git_remove_worktree_sync 的登记校验也显式排除主工作树(目前主仓库也算"已登记",虽然 git 最终会拒绝,防御深度上最好前置拦截)。

}, [branchAction, confirm, gitClient, resetBranchAction, runSheetMutation, t, workdir]);

// 分支被 worktree 检出时,删除入口切换为删除 worktree(连带分支)。
const checkedOutWorktreePath = branchAction

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

M1 的前端侧:worktrees 含主工作树条目时,这里的 find 会把主仓库检出的分支误判为被 worktree 检出(详见后端 git_worktrees_sync 处的评论)。后端过滤掉主工作树后此处无需改动;若选择在前端过滤,可对比 state.repoRoot 排除。

: group.projectPaths.filter((path) => workspaceProjectPathKey(path) !== targetKey),
};
});
return touched ? next : next;

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

L1:touched ? next : next 两个分支相同,touched 完全未生效。意图应是无变化时返回原引用(groups),让两端 updateWorkspaceProjectGroups 里的 next === prev.system.workspaceProjectGroups 引用相等短路生效;现在恒返回新数组,幂等操作(如重复打开已入组的 worktree)也会触发一次 settings 写入/同步。

if trimmed.is_empty() {
return Err("Worktree 路径不能为空。".to_string());
}
let canonical =

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

L2(提示,非阻塞):fs::canonicalize 要求路径存在——worktree 目录被手动删除(stale 登记)时此 API 直接报"无法解析路径",无法用于清理;好在 git_delete_branch_syncworktree prune 前置覆盖了这条路径且有测试。另外校验用 canonical 路径、执行传原始 trimmed,存在极小 TOCTOU 窗口,本地桌面场景可接受。


/// 稳定且唯一的 repo id:`<sanitized-basename>-<fnv1a64 低 32 位 hex>`。
/// 同一仓库根路径永远映射到同一 id,目录可读;32 位哈希碰撞概率低,但非绝对。
fn repo_worktree_id(repo_root: &str) -> String {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

L3(非阻塞):既然已经算出 fnv1a64 的 64 位哈希,建议直接用满 16 个 hex 字符而不是截断成 32 位(as u32)——零成本消除注释里提到的碰撞顾虑。

@coder-hhx

Copy link
Copy Markdown
Collaborator

Code review

Found 1 issue:

  1. 侧边栏项目渲染上限失效:sliceWorkspaceProjectSections 只对 sections.groupedslice(0, maxGrouped)sections.ungrouped 原样透传,hiddenProjectCount 也只统计被裁掉的分组成员。分组是可选的(目前仅 worktree 自动建组),常见场景下所有项目都在 ungrouped——此时 30 条的上限完全不生效:大量未分组项目会一次性全部挂载到这个非虚拟化列表,且 "show all" 按钮永远不出现(hiddenProjectCount > 0 恒为 false)。改动前 activeProjects.slice(0, SIDEBAR_PROJECT_RENDER_CAP) 对全部 active 行生效;本次删掉的注释 "Projects are not virtualized; cap the rendered rows…" 正是该约束的出处,保留下来的邻近注释 "the render cap only applies to the active rows" 已与实际行为不符。(bug due to sliceWorkspaceProjectSections 未裁剪 ungrouped)

export function sliceWorkspaceProjectSections(
sections: WorkspaceProjectSections,
maxGrouped: number,
): { sections: WorkspaceProjectSections; hiddenProjectCount: number } {
const grouped = sections.grouped.slice(0, maxGrouped);
const totalMembers =
sections.ungrouped.length +
sections.grouped.reduce((sum, section) => sum + section.projects.length, 0);
const visibleMembers =
sections.ungrouped.length + grouped.reduce((sum, section) => sum + section.projects.length, 0);
return {
sections: { grouped, ungrouped: sections.ungrouped },
hiddenProjectCount: Math.max(0, totalMembers - visibleMembers),
};
}

调用点与 show-all 条件:

() =>
showAllProjects
? { sections: projectSections, hiddenProjectCount: 0 }
: sliceWorkspaceProjectSections(projectSections, PROJECT_LIST_COLLAPSED_MAX),
[projectSections, showAllProjects],
);
const renderedSections = slicedSections.sections;

) : null}
{hiddenProjectCount > 0 || showAllProjects ? (
<button

🤖 Generated with Claude Code

- If this code review was useful, please react with 👍. Otherwise, react with 👎.

@coder-hhx

Copy link
Copy Markdown
Collaborator

补充两点(承接上面的 code review 与此前 review 意见):

1. 先前 review 的 M1/L1/L2/L3 在当前 head(5e95e86)均未修复。 Review 之后仅合入了一个 upstream/main 的 merge commit,所有被评论的行原文未变,M1(git_worktrees_sync 未过滤主工作树条目)依然存在。

2. 追加一个建议顺手修复的问题:删除 worktree 时 force 未传导到分支删除。 git_remove_worktree_sync 中分支删除硬编码 git branch -dforce 参数只作用于 worktree remove 阶段:

// worktree 移除成功后,尝试删除其检出的分支;分支删除失败时
// worktree 已不在(重试会提示未登记),错误信息必须说明这一点。
let branch = delete_branch.as_deref().map(str::trim).unwrap_or("");
if branch.is_empty() {
Ok(GitOutput {
stdout: String::new(),
stderr: String::new(),
})
} else {
match git_success(&state.repo_root, &["branch", "-d", branch]) {
Ok(output) => Ok(output),

当 worktree 移除成功、但分支因存在未合并提交而删除失败时(活跃 worktree 分支的常态),返回"Worktree 已移除,但分支删除失败"。而前端 deleteWorktreeFlow 的强制重试只匹配 contains modified or untracked files(worktree 移除失败的场景),"not fully merged" 不会命中,用户只看到报错、无引导恢复;且此时重试整个流程会因 worktree 已注销而报 "not registered"(代码注释里也承认了这一点):

}
// Worktree 含未提交改动时 git 拒绝移除 → 提供强制移除的二次确认。
if (!/contains modified or untracked files/i.test(actionErrorRef.current)) return;
const forced = await confirm({
title: t("git.branchSelector.deleteWorktreeForceTitle"),

兜底路径存在(普通删分支入口先 worktree prune 再可 -D),所以不阻塞;建议与 M1 一并处理:要么 force 时用 -D,要么前端对 "not fully merged" 也给出强制删除的引导。

🤖 Generated with Claude Code

@Mieluoxxx
Mieluoxxx force-pushed the feat/git-worktree-groups branch from 5e95e86 to 642beec Compare August 10, 2026 07:24

@coder-hhx coder-hhx left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

检视意见(Comment)

整体质量很高:后端路径安全校验、Gateway enableWebGit 门控、双端(桌面/WebUI)镜像、i18n key 一致性、测试覆盖都到位,顺手修掉的 workspaceProjectGroups 持久化 bug 也补了 round-trip 测试。CI 全绿,不阻塞合并。以下 2 个建议级问题望跟进,备注级供参考。

建议级

M1 · 无分组场景 pinned 分隔线疑似回归(ChatHistorySidebar.tsx)

旧逻辑在扁平项目列表中,pinned 块与非 pinned 块之间有分隔线。新的 firstUnpinnedSectionIndex 只在分组区块之间和「未分组」标题前渲染分隔线;当用户没有任何分组(最常见场景)且存在 pinned 项目时,计算走到 ungrouped[0]?.isPinned === true → return -1,加上 ungrouped 列表内部没有逐项分隔线渲染点,分隔线整体消失。建议在纯 ungrouped 渲染路径上恢复 pinned/非 pinned 之间的分隔线。

M2 · 强制移除 worktree 时未合并分支被静默 -D(deleteWorktreeFlow + git_remove_worktree_sync)

脏 worktree 路径:removeWorktree(force=false) 被 git 拒绝 → 弹「强制移除」确认(文案只提"丢弃未提交改动")→ removeWorktree(force=true, branch),后端 force 同时把分支删除从 -d 升级为 -D。若该分支还有未合并提交,用户只确认了丢弃未提交改动,提交也被一并删除——绕过了非脏路径下专门的「分支尚未完全合并」二次确认。

建议:强制移除 worktree 时分支删除仍用 -d,失败后复用已有的 not-fully-merged 确认流(isGitWorktreeBranchNotFullyMergedError 分支已存在);或至少在 deleteWorktreeForceDescription 文案中明确「分支及其未合并提交也将被删除」。

备注级(可不改)

  • isGitWorktreeBranchNotFullyMergedError 正则匹配后端中文错误前缀(worktree .*分支删除失败),前后端字符串耦合。目前有测试锚定、git 侧有 LC_ALL=C 保障,但后端文案一改即静默失效,长期看结构化错误码更稳。
  • sliceWorkspaceProjectSections 遇到第一个放不下的分组即 break,后续更小的分组也全部隐藏;首组成员数 >30 时折叠视图一个分组都不显示。边缘场景,行为可预期,提一下。
  • 设置归一化中硬编码 "未命名分组" 中文 fallback,英文 locale 下会露中文(理解非 React 上下文拿不到 t())。
  • 有分组存在时,pinned 的未分组项目排在所有分组之后、不再浮顶——如果是分组布局的预期取舍,建议在 PR 描述或代码注释中明确一句。

- Create git worktrees under ~/.liveagent/worktree/<repo_id>/<name> from
  the branch selector; the new worktree opens as a workspace in the sidebar
- Detect branches checked out in a worktree and offer "Delete Worktree"
  (removes the worktree and its branch) instead of a failing branch delete,
  with a force-removal escalation for dirty worktrees
- Add user-defined sidebar project groups with create/rename/delete/move
  and collapsed state; worktrees auto-group under their source repository
  project, reusing renamed groups via sourceProjectPath
- Persist workspaceProjectGroups in system settings (the save whitelist
  previously dropped the field, losing groups on restart)
- Move the "New Worktree" entry next to "Create New Branch" in the git
  menu; keep worktree names free of auto-capitalization
…empotency

- M1: git_worktrees_sync 过滤主工作树(path == repo_root),主仓库检出的
  分支不再被误判为被 linked worktree 检出;git_remove_worktree_sync 的
  登记校验随之天然排除主仓库(防御深度前置)。
- L1: assignWorkspaceProjectToGroup 无变化时返回原引用,让两端
  updateWorkspaceProjectGroups 的引用相等短路生效,幂等操作不再触发
  多余的 settings 写入/同步。
- L3: repo_worktree_id 使用完整 64 位 fnv1a64 hex(16 字符),消除
  32 位截断的碰撞顾虑。
- Bug: sliceWorkspaceProjectSections 改为按成员数裁剪(分组整组纳入、
  绝不拆开),未分组项目也受渲染上限约束,hiddenProjectCount 统计
  全部被隐藏成员,show-all 按钮恢复生效。
- 附带 rebase 至最新 upstream 后的对齐修正(settings 测试 row_count、
  import 布局、i18n 格式、loader 路径)。
- git_remove_worktree_sync: force 传导到分支删除,--force 时用 git branch -D
  让未合并提交的分支也能被清理
- GitBranchSelector: worktree 已移除但分支未完全合并时,不再重试已注销的
  worktree,直接对残留分支提供强制删除确认(复用 deleteForce 文案)
- 新增 isGitWorktreeBranchNotFullyMergedError 错误分类纯函数及单测
- Rust 集成测试覆盖普通失败保留分支与 force 成功删除分支两条路径
@Mieluoxxx
Mieluoxxx force-pushed the feat/git-worktree-groups branch from 642beec to 412205b Compare August 11, 2026 02:19
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feat(git): git worktree 支持与侧边栏项目分组

3 participants