Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
d347022
feat(core): add git worktree management operations
xiaoyumuxi Sep 2, 2026
d4f658a
feat(macos): connect worktree operations to git feature model
xiaoyumuxi Sep 2, 2026
b592c8f
feat(macos-ui): implement worktree management workspace
xiaoyumuxi Sep 2, 2026
8e5fa9e
test(macos): cover worktree localization and lifecycle
xiaoyumuxi Sep 2, 2026
d01f31c
feat(git): create worktrees from a specific revision
xiaoyumuxi Sep 2, 2026
be1ca70
fix(macos-ui): show worktree history and revision controls
xiaoyumuxi Sep 2, 2026
a2c5853
fix(core): use revision only when creating worktrees
xiaoyumuxi Sep 2, 2026
2d3e4d4
fix(macos-ui): explain unavailable primary worktree actions
xiaoyumuxi Sep 2, 2026
9c51d0c
feat(macos-ui): add AI worktree directory option
xiaoyumuxi Sep 2, 2026
6fa4c0b
fix(macos): keep worktree history visible when status fails
xiaoyumuxi Sep 2, 2026
60480f6
feat(macos): show repository path on worktree tab
xiaoyumuxi Sep 2, 2026
70ed2f3
fix(macos): resolve run and git test diagnostics
xiaoyumuxi Sep 2, 2026
8ca041f
feat(macos): make worktree panes resizable
xiaoyumuxi Sep 2, 2026
972cba9
perf(macos): reuse cached primary worktree state
xiaoyumuxi Sep 2, 2026
28a1456
refactor(macos): reuse standard split handles for worktrees
xiaoyumuxi Sep 2, 2026
f4a4613
docs: require high-performance resizable UI patterns
xiaoyumuxi Sep 2, 2026
29d8d9d
perf(macos): load worktree history incrementally
xiaoyumuxi Sep 2, 2026
e07474b
perf(macos): expire cached git history
xiaoyumuxi Sep 2, 2026
79b65ce
perf(macos-ui): compact worktree history loading
xiaoyumuxi Sep 2, 2026
654fbba
feat(macos): paginate worktree commit history
xiaoyumuxi Sep 2, 2026
2b493f0
fix(macos): cap primary worktree history page
xiaoyumuxi Sep 2, 2026
3c59218
docs: translate AGENTS instructions to Chinese
xiaoyumuxi Sep 2, 2026
11e4336
fix(macos): 提交历史优先显示并记录 Git 耗时
xiaoyumuxi Sep 2, 2026
ffc7715
fix(macos): clear deleted worktree details
xiaoyumuxi Sep 2, 2026
be1f25d
docs: translate AGENTS instructions to English
xiaoyumuxi Sep 2, 2026
7ddcfae
fix cross-platform worktree path matching
xiaoyumuxi Sep 2, 2026
552b124
prevent stale worktree inspection updates
xiaoyumuxi Sep 2, 2026
e9d395f
Merge preview into worktree management
xiaoyumuxi Sep 2, 2026
531ff41
fix(macos): preserve deletion metadata in git results
xiaoyumuxi Sep 2, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
44 changes: 41 additions & 3 deletions AGENTS.md
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
# Lithe Agent Entry Point

Before any work in this repository, load and follow the `develop-lithe` skill
at `.agents/skills/develop-lithe/SKILL.md`. That Skill is the single source of
at `.agents/skills/develop-lithe/SKILL.md`. That skill is the single source of
truth for AI coding and verification rules, including the required Rust Core
comment standard.

If a task creates, modifies, or reviews test code or test infrastructure,
additionally load `.agents/skills/write-stable-tests/SKILL.md` before
proceeding. That Skill defines the mandatory bounded-wait, deterministic-time,
proceeding. That skill defines the mandatory bounded-wait, deterministic-time,
cleanup, and per-test timing rules for both macOS and Windows.

If a task prepares, validates, or publishes a stable Lithe release,
Expand All @@ -18,7 +18,7 @@ If the task involves building, running, diagnosing, or transferring files to the
Windows product through a Parallels guest VM, additionally load
`.agents/skills/debug-windows-on-parallels/SKILL.md` before proceeding.

## Test process lifecycle and cleanup
## Test Process Lifecycle and Cleanup

Unless the user gives a specific instruction to keep a process running, any
Lithe application started for building, testing, debugging, previewing, or
Expand All @@ -29,3 +29,41 @@ back. Do not launch duplicate Lithe instances during repeated checks, and do
not leave test-built applications open in the user's application list. If a
process cannot be stopped cleanly, report it explicitly and make a bounded
best-effort cleanup before continuing.

## High-Performance UI Interaction and Resizable Layout Requirements

When working on draggable splitters, resizable panels, continuous dragging,
scrolling, or other high-frequency UI interactions, prioritize reusing the
project's existing high-performance layout containers and interaction
components. Do not quickly implement these behaviors by stacking custom
`DragGesture` handlers in a business parent view, writing to multiple `@State`
properties on every event, or duplicating splitter logic.

- Resizable macOS panels must use `LitheSplitPaneView` and `SplitHandleView`
whenever possible. If they genuinely cannot be reused, explain why in the
change summary and preserve the same behavioral contract.
- Drag handling must use a stable coordinate space, preferably global
coordinates for continuous dragging, to prevent the moving splitter from
changing the coordinate origin and causing jumps.
- High-frequency drag events must be throttled, coalesced, or filtered with a
dead zone. Do not trigger unnecessary parent-view reconstruction on every
pointer event. Keep mutable size state in a local layout container whenever
possible so dragging does not recompute the entire feature page.
- Provide explicit minimum and maximum sizes and available-space constraints so
adjacent panels retain their minimum usable widths. Window resizing, panel
hiding, and panel restoration must not produce negative sizes or layout
overflow.
- Interaction behavior should match existing Git, editor, and tool windows,
including hover/drag highlighting, the platform-appropriate resize cursor,
help text, and accessibility labels.
- If sizes must persist across refreshes or restarts, commit the final size
through the existing layout-persistence mechanism rather than continuously
writing to persistent storage during dragging.
- After adding or modifying this type of UI, at minimum complete the relevant
product build, `git diff --check`, and boundary checks. During code review,
explicitly confirm that dragging does not cause high-frequency full-page
redraws.

These requirements apply to both macOS and Windows. Each platform may use its
own native implementation, but interaction semantics, performance goals, size
constraints, and accessibility requirements must remain consistent.
107 changes: 107 additions & 0 deletions macos/Resources/en.lproj/Localizable.strings
Original file line number Diff line number Diff line change
Expand Up @@ -128,3 +128,110 @@
"Retry debugging" = "Retry debugging";
"Pull Requests integration is under development" = "Pull Requests integration is under development";
"GitHub sign-in and pull request management are temporarily unavailable." = "GitHub sign-in and pull request management are temporarily unavailable.";

/* Git worktree workbench. */
"Worktrees" = "Worktrees";
"New Worktree" = "New Worktree";
"Search worktree or path" = "Search worktree or path";
"Refresh worktrees" = "Refresh worktrees";
"Main Worktree" = "Main Worktree";
"Current" = "Current";
"Branch: %@" = "Branch: %@";
"Detached HEAD" = "Detached HEAD";
"Worktree" = "Worktree";
"Copy worktree path" = "Copy worktree path";
"Commit History" = "Commit History";
"Commit History (%lld)" = "Commit History (%lld)";
"Settings" = "Settings";
"Basic Information" = "Basic Information";
"Path" = "Path";
"Branch" = "Branch";
"HEAD" = "HEAD";
"Role" = "Role";
"Primary worktree" = "Primary worktree";
"Linked worktree" = "Linked worktree";
"Status" = "Status";
"Registration" = "Registration";
"Stale record" = "Stale record";
"Valid" = "Valid";
"Protection" = "Protection";
"Locked" = "Locked";
"Unlocked" = "Unlocked";
"Local changes" = "Local changes";
"Actions" = "Actions";
"Open in Lithe" = "Open in Lithe";
"Show in Finder" = "Show in Finder";
"Copy Path" = "Copy Path";
"Unlock Worktree" = "Unlock Worktree";
"Lock Worktree" = "Lock Worktree";
"Remove Worktree…" = "Remove Worktree…";
"Prune Stale Records" = "Prune Stale Records";
"%lld worktree records need attention" = "%lld worktree records need attention";
"A missing checkout can make the worktree list inaccurate." = "A missing checkout can make the worktree list inaccurate.";
"Quick Information" = "Quick Information";
"Primary" = "Primary";
"State" = "State";
"Reason" = "Reason";
"Path Missing" = "Path Missing";
"Modified" = "Modified";
"Available" = "Available";
"Open to inspect" = "Open to inspect";
"No changes" = "No changes";
"%lld changed files" = "%lld changed files";
"Loading worktrees" = "Loading worktrees";
"Reading registered checkouts." = "Reading registered checkouts.";
"Worktrees are unavailable" = "Worktrees are unavailable";
"No matches" = "No matches";
"Try a branch name or checkout path." = "Try a branch name or checkout path.";
"No worktrees" = "No worktrees";
"Create a checkout to get started." = "Create a checkout to get started.";
"Select a worktree" = "Select a worktree";
"Its details and actions will appear here." = "Its details and actions will appear here.";
"Remove worktree?" = "Remove worktree?";
"Remove '%@' and its checkout directory? The branch is kept. If Git refuses because files have changed, you can review a separate force-removal warning." = "Remove '%@' and its checkout directory? The branch is kept. If Git refuses because files have changed, you can review a separate force-removal warning.";
"Remove" = "Remove";
"Review Force Remove…" = "Review Force Remove…";
"Force remove worktree?" = "Force remove worktree?";
"This permanently deletes uncommitted and untracked files in '%@'. The branch itself is kept." = "This permanently deletes uncommitted and untracked files in '%@'. The branch itself is kept.";
"Force Remove" = "Force Remove";
"Prune stale worktree records?" = "Prune stale worktree records?";
"This removes Git registrations whose checkout directories no longer exist. It does not delete branches." = "This removes Git registrations whose checkout directories no longer exist. It does not delete branches.";
"Create an independent checkout and a new branch from the selected reference." = "Create an independent checkout and a new branch from the selected reference.";
"Start from" = "Start from";
"New branch" = "New branch";
"Checkout path" = "Checkout path";
"Worktree destination" = "Worktree destination";
"Choose Parent…" = "Choose Parent…";
"Use AI worktree directory (/private/tmp)" = "Use AI worktree directory (/private/tmp)";
"Recommended: keep worktrees in a persistent folder next to the repository. You can choose /private/tmp manually for disposable checkouts." = "Recommended: keep worktrees in a persistent folder next to the repository. You can choose /private/tmp manually for disposable checkouts.";
"Create" = "Create";
"Enter a branch name" = "Enter a branch name";
"Created worktree for %@" = "Created worktree for %@";
"Removed %@" = "Removed %@";
"Locked %@" = "Locked %@";
"Unlocked %@" = "Unlocked %@";
"Pruned stale worktree records" = "Pruned stale worktree records";
"Repair Worktree Records" = "Repair Worktree Records";
"Repaired worktree records" = "Repaired worktree records";
"The checkout path does not exist" = "The checkout path does not exist";
"Could not inspect this worktree" = "Could not inspect this worktree";
"Loading worktree details" = "Loading worktree details";
"Reading changes and recent commits." = "Reading changes and recent commits.";
"No local changes" = "No local changes";
"This worktree has no uncommitted changes." = "This worktree has no uncommitted changes.";
"No commits" = "No commits";
"No commits were found for this branch." = "No commits were found for this branch.";
"Staged" = "Staged";
"Unstaged" = "Unstaged";
"Worktree Settings" = "Worktree Settings";
"Maintenance" = "Maintenance";
"Danger Zone" = "Danger Zone";
"Checkout Path Missing" = "Checkout Path Missing";
"The checkout directory no longer exists. Repair moved worktree metadata or prune the stale Git record." = "The checkout directory no longer exists. Repair moved worktree metadata or prune the stale Git record.";
"The primary worktree cannot be locked." = "The primary worktree cannot be locked.";
"Repair or prune the missing checkout before changing its lock." = "Repair or prune the missing checkout before changing its lock.";
"The primary worktree cannot be removed." = "The primary worktree cannot be removed.";
"The current worktree cannot be removed here." = "The current worktree cannot be removed here.";
"Unlock the worktree before removing it." = "Unlock the worktree before removing it.";
"Prune the stale record instead." = "Prune the stale record instead.";
"Loading…" = "Loading…";
95 changes: 95 additions & 0 deletions macos/Resources/zh-Hans.lproj/Localizable.strings
Original file line number Diff line number Diff line change
Expand Up @@ -1300,3 +1300,98 @@
"Console" = "控制台";
"Debugger" = "调试器";
"Breakpoints" = "断点";

/* Git 工作树管理台。 */
"Worktrees" = "工作树";
"New Worktree" = "新建工作树";
"Search worktree or path" = "搜索工作树或路径";
"Refresh worktrees" = "刷新工作树";
"Main Worktree" = "主工作区(主)";
"Branch: %@" = "分支:%@";
"Detached HEAD" = "分离 HEAD";
"Worktree" = "工作树";
"Copy worktree path" = "复制工作树路径";
"Commit History" = "提交历史";
"Commit History (%lld)" = "提交历史(%lld 条)";
"Basic Information" = "基本信息";
"Path" = "路径";
"Branch" = "分支";
"HEAD" = "HEAD";
"Role" = "类型";
"Primary worktree" = "主工作区";
"Linked worktree" = "关联工作树";
"Registration" = "注册状态";
"Stale record" = "陈旧记录";
"Valid" = "有效";
"Protection" = "保护状态";
"Locked" = "已锁定";
"Unlocked" = "未锁定";
"Local changes" = "本地更改";
"Open in Lithe" = "在 Lithe 中打开";
"Unlock Worktree" = "解锁工作树";
"Lock Worktree" = "锁定工作树";
"Remove Worktree…" = "删除工作树…";
"Prune Stale Records" = "清理陈旧记录";
"%lld worktree records need attention" = "%lld 个工作树需要修复";
"A missing checkout can make the worktree list inaccurate." = "路径不存在或无效的工作树会影响列表的准确性。";
"Quick Information" = "快速信息";
"Primary" = "主工作区";
"Reason" = "原因";
"Path Missing" = "路径不存在";
"Available" = "可用";
"Open to inspect" = "打开后检查";
"No changes" = "没有更改";
"%lld changed files" = "%lld 个文件有更改";
"Loading worktrees" = "正在加载工作树";
"Reading registered checkouts." = "正在读取 Git 中注册的检出目录。";
"Worktrees are unavailable" = "无法加载工作树";
"Try a branch name or checkout path." = "请尝试输入分支名称或检出路径。";
"No worktrees" = "没有工作树";
"Create a checkout to get started." = "新建一个检出目录以开始使用。";
"Select a worktree" = "选择工作树";
"Its details and actions will appear here." = "工作树详情和可用操作会显示在这里。";
"Remove worktree?" = "删除工作树?";
"Remove '%@' and its checkout directory? The branch is kept. If Git refuses because files have changed, you can review a separate force-removal warning." = "删除“%@”及其检出目录?对应分支会保留。如果 Git 因文件已更改而拒绝删除,你可以继续查看单独的强制删除警告。";
"Review Force Remove…" = "查看强制删除警告…";
"Force remove worktree?" = "强制删除工作树?";
"This permanently deletes uncommitted and untracked files in '%@'. The branch itself is kept." = "这会永久删除“%@”中的未提交文件和未跟踪文件,但会保留对应分支。";
"Force Remove" = "强制删除";
"Prune stale worktree records?" = "清理陈旧的工作树记录?";
"This removes Git registrations whose checkout directories no longer exist. It does not delete branches." = "这会移除检出目录已不存在的 Git 注册记录,但不会删除分支。";
"Create an independent checkout and a new branch from the selected reference." = "从所选引用创建独立检出目录和新分支。";
"Start from" = "起始引用";
"New branch" = "新分支";
"Checkout path" = "检出路径";
"Worktree destination" = "工作树目标路径";
"Choose Parent…" = "选择父目录…";
"Use AI worktree directory (/private/tmp)" = "使用 AI 工作树目录(/private/tmp)";
"Recommended: keep worktrees in a persistent folder next to the repository. You can choose /private/tmp manually for disposable checkouts." = "建议将工作树放在仓库旁的持久目录中。临时检出时可以手动选择 /private/tmp。";
"Created worktree for %@" = "已为 %@ 创建工作树";
"Removed %@" = "已删除 %@";
"Locked %@" = "已锁定 %@";
"Unlocked %@" = "已解锁 %@";
"Pruned stale worktree records" = "已清理陈旧的工作树记录";
"Repair Worktree Records" = "修复工作树记录";
"Repaired worktree records" = "已修复工作树记录";
"The checkout path does not exist" = "检出路径不存在";
"Could not inspect this worktree" = "无法检查此工作树";
"Loading worktree details" = "正在加载工作树详情";
"Reading changes and recent commits." = "正在读取文件更改和最近提交。";
"No local changes" = "没有本地更改";
"This worktree has no uncommitted changes." = "此工作树没有未提交的更改。";
"No commits" = "没有提交记录";
"No commits were found for this branch." = "此分支没有找到提交记录。";
"Staged" = "已暂存";
"Unstaged" = "未暂存";
"Worktree Settings" = "工作树设置";
"Maintenance" = "维护";
"Danger Zone" = "危险操作";
"Checkout Path Missing" = "检出路径不存在";
"The checkout directory no longer exists. Repair moved worktree metadata or prune the stale Git record." = "检出目录已不存在。你可以修复移动后的工作树元数据,或清理失效的 Git 记录。";
"The primary worktree cannot be locked." = "主工作区不能锁定。";
"Repair or prune the missing checkout before changing its lock." = "请先修复或清理路径缺失的工作树,再更改锁定状态。";
"The primary worktree cannot be removed." = "主工作区不能删除。";
"The current worktree cannot be removed here." = "不能在这里删除当前工作树。";
"Unlock the worktree before removing it." = "请先解锁工作树再删除。";
"Prune the stale record instead." = "请改用清理陈旧记录。";
"Loading…" = "正在加载…";
41 changes: 41 additions & 0 deletions macos/Sources/Lithe/Core/Rust/RustCoreBridge.swift
Original file line number Diff line number Diff line change
Expand Up @@ -1133,6 +1133,40 @@ struct RustCoreBridge: Sendable {
}
}

struct GitWorktreesPayload: Decodable, Sendable {
struct Worktree: Decodable, Sendable {
let path: String
let head: String
let branch: String?
let isCurrent: Bool
let isPrimary: Bool
let isBare: Bool
let isDetached: Bool
let isLocked: Bool
let lockReason: String?
let isPrunable: Bool
let pruneReason: String?

func makeModel() -> GitWorktree {
GitWorktree(
path: path,
head: head,
branch: branch,
isCurrent: isCurrent,
isPrimary: isPrimary,
isBare: isBare,
isDetached: isDetached,
isLocked: isLocked,
lockReason: lockReason,
isPrunable: isPrunable,
pruneReason: pruneReason
)
}
}

let worktrees: [Worktree]
}

struct GitPullRequestContextPayload: Decodable, Sendable {
let currentBranch: String?
let suggestedBaseBranch: String?
Expand Down Expand Up @@ -2610,6 +2644,13 @@ struct RustCoreBridge: Sendable {
return try? result.get()
}

func gitWorktrees(at rootURL: URL) -> GitWorktreesPayload? {
execute(
command: "git.worktrees",
payload: GitStatusRequest(root: rootURL.standardizedFileURL.path)
)
}

func gitPullRequestContext(
at rootURL: URL
) -> Result<GitPullRequestContextPayload, CoreCallError> {
Expand Down
50 changes: 50 additions & 0 deletions macos/Sources/Lithe/Core/Rust/RustGitOperations.swift
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,52 @@ struct RustGitOperations: GitOperations, Sendable {
)
}

func createWorktree(
named name: String,
from reference: GitReference,
revision: String? = nil,
at destination: URL,
repositoryRoot: URL
) -> GitProcessResult? {
write(
at: repositoryRoot,
operation: "createWorktree",
gitReference: reference,
revision: revision,
name: name,
destination: destination
)
}

func removeWorktree(
_ worktree: GitWorktree,
force: Bool,
at rootURL: URL
) -> GitProcessResult? {
write(
at: rootURL,
operation: "removeWorktree",
destination: worktree.url,
force: force
)
}

func lockWorktree(_ worktree: GitWorktree, at rootURL: URL) -> GitProcessResult? {
write(at: rootURL, operation: "lockWorktree", destination: worktree.url)
}

func unlockWorktree(_ worktree: GitWorktree, at rootURL: URL) -> GitProcessResult? {
write(at: rootURL, operation: "unlockWorktree", destination: worktree.url)
}

func repairWorktrees(at rootURL: URL) -> GitProcessResult? {
write(at: rootURL, operation: "repairWorktrees")
}

func pruneWorktrees(at rootURL: URL) -> GitProcessResult? {
write(at: rootURL, operation: "pruneWorktrees")
}

func renameBranch(_ reference: GitReference, to name: String, at rootURL: URL) -> GitProcessResult? {
write(at: rootURL, operation: "renameBranch", gitReference: reference, name: name)
}
Expand Down Expand Up @@ -360,6 +406,10 @@ struct RustGitOperations: GitOperations, Sendable {
core.gitWatchContext(at: rootURL)?.makeContext()
}

func worktrees(at rootURL: URL) -> [GitWorktree]? {
core.gitWorktrees(at: rootURL)?.worktrees.map { $0.makeModel() }
}

func diffPatch(
at rootURL: URL,
pathspecs: [String],
Expand Down
Loading
Loading