Skip to content

feat: add worktree version panel and open-as-worktree action - #71

Merged
Oaklight merged 1 commit into
masterfrom
feature/worktree-version-panel
Sep 13, 2026
Merged

Oaklight merged 1 commit into
masterfrom
feature/worktree-version-panel

Conversation

@Oaklight

Copy link
Copy Markdown
Owner

Summary

  • Add "Versions" sub-tab to the git panel showing all worktrees with branch, commit hash, path, and main badge
  • Add worktree create dialog (from branch dropdown), remove, and switch functionality
  • Add "Open as worktree" (⧉) action button in the branch dropdown for non-current branches
  • Add bilingual i18n strings (en/zh) for all worktree-related UI text

Closes #66
Parent issue: #60

Test plan

  • Open a project with git, verify the "Versions" tab appears in the git panel alongside "Changes" and "History"
  • Click "Versions" tab — verify worktree list loads and shows the main worktree with badge
  • Click "+ Add worktree" — verify dialog shows available branches and creates a worktree on confirm
  • Verify non-main worktrees show a remove button (✕) on hover; click it and confirm removal works
  • Click a non-main worktree row — verify it switches the project to that worktree (files, editor, git status refresh)
  • In the branch dropdown, verify the ⧉ icon appears on hover for non-current branches; click it and confirm worktree is created and versions tab is shown
  • Switch language to Chinese and verify all worktree strings render correctly

@Oaklight
Oaklight merged commit 700c09c into master Sep 13, 2026
2 checks passed
@Oaklight
Oaklight deleted the feature/worktree-version-panel branch September 13, 2026 10:10

@clementine-oaklight clementine-oaklight Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

PR #71 — feat: add worktree version panel and open-as-worktree action

纯前端 PR,236 行新增,假设后端 worktree API 已就绪。整体结构跟现有 git panel 模式一致,i18n 双语齐全。两个需要修的问题,几个 non-blocking。

🐛 Bug 1: addWorktreeDialog() innerHTML XSS

branch name 直接拼进 HTML string:

let branchOptions = availableBranches.map(b => `<option value="${b}">${b}</option>`).join("");
// ...
dialog.innerHTML = `... ${branchOptions} ...`;

git branch name 理论上可以包含 " 和 <,这是 XSS 向量。PR 其余地方(refreshWorktrees 里的 branch/path 显示)都正确用了 document.createElement + .textContent,这里应该保持一致:用 DOM API 构建 <select> 和 <option>。

🐛 Bug 2: Main worktree 不可点击,无法切回

if (!wt.is_main) {
    // actions + onclick 都在这个 if 里
    item.onclick = () => switchWorktree(wt.path);
}

如果用户切到了一个 secondary worktree,main worktree 行没有 click handler,用户无法点击切回 main。不能 remove main 是对的,但 switch 回 main 应该允许。建议把 item.onclick 移到 if 外面(或者用一个单独的条件)。

Non-blocking

  1. openAsWorktree() 只 create 不 switch — 函数名和 tooltip 都说 "open as worktree",但实际只创建 worktree + 切到 Versions tab,没有调 switchWorktree()。用户点了 ⧉ 期望的是进入那个 worktree 工作。

  2. i18n key 复用不当 — 创建 worktree dialog 的 OK 按钮用了 t("create_branch")("Create branch" / "创建分支"),label 用了 t("switch_branch")。应该加 worktree 专用的 key,比如 create_worktree / select_branch。

  3. confirm() vs 自定义 dialog — removeWorktree 用浏览器原生 confirm(),但删除 branch、dirty state 等都用的是 git-dialog-overlay 自定义弹窗,风格不统一。

  4. 无 dirty 检查 — 切 branch 有 checkDirtyBeforeSwitch()(stash/discard/cancel),switchWorktree() 没有。worktree 间切换虽然 working dir 独立,但编辑器里未保存的 buffer 可能丢失,值得加个提示。

@milo-oaklight milo-oaklight Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Review: PR #71 — worktree version panel (frontend)

CI is green. This is a frontend-only PR (CSS/HTML/JS) wiring up the worktree APIs that already exist on master. The Versions sub-tab integrates cleanly into the existing git panel tab structure, and the i18n coverage is complete. A few issues to address, though — one is a security concern.


🔴 XSS via branch names in addWorktreeDialog

app.js ~L4058–4060 — The branch dropdown is built with innerHTML and unescaped branch names:

let branchOptions = availableBranches.map(b => `<option value="${b}">${b}</option>`).join("");

Git allows <, >, ", ' in branch names. A branch named "><img src=x onerror=alert(1)> would inject HTML. Since other parts of this PR (and the existing branch dropdown) correctly use document.createElement + .textContent, the fix is straightforward — build the <select> options the same way:

const select = document.createElement("select");
// ...
for (const b of availableBranches) {
  const opt = document.createElement("option");
  opt.value = b;
  opt.textContent = b;
  select.appendChild(opt);
}

This also means the surrounding dialog HTML template needs to be split so the <select> is inserted via DOM rather than innerHTML.


🟡 openAsWorktree doesn't actually open/switch to the worktree

app.js ~L4134–4145 — The function creates a worktree and switches to the Versions tab, but never calls switchWorktree(). From the user's perspective, clicking ⧉ "Open as worktree" on a branch should either:

  1. Create the worktree and switch to it (so the editor shows that branch's files), or
  2. At minimum, clearly communicate that it only created the worktree and the user must click it to switch.

Currently it does neither — it creates the worktree silently and shows the version list, but the user is still editing files in the old worktree. Suggestion: after successful creation, either auto-switch or at least call refreshWorktrees() so the new worktree is visible and clickable.

Also: if a worktree already exists for that branch, the create API returns an error from git. Consider checking first, or catching the "already checked out" error and offering to switch instead.


🟡 Wrong button label in add-worktree dialog

app.js ~L4075 — The confirm button uses t("create_branch"):

<button class="sm primary" id="worktree-ok">${t("create_branch")}</button>

This renders as "Create branch" / "创建分支", but we're creating a worktree. Add a new i18n key (e.g., create_worktree / 创建工作树) and use that instead.


🟡 .current class marks the main worktree, not the active one

app.js ~L3977 — The CSS class .current (left accent border) is applied when wt.is_main is true. But is_main means "the original/primary worktree" (first in git worktree list), not "the worktree the server is currently serving files from."

If a user switches to a secondary worktree, the main worktree still shows the accent, which is misleading. Two options:

  1. Rename the visual indicator to a "main" badge only (already present), drop the .current class from it.
  2. Track which worktree is actually active (compare against current config.project_path — the API would need to return this) and mark that one.

🟡 switchWorktree — server-wide state mutation

Backend handlers.py L1264–1265 — Switching worktrees mutates config["project_path"] globally. If two browser tabs are open, Tab A switching worktrees affects Tab B's subsequent file operations without Tab B knowing. The frontend correctly refreshes the triggering tab, but any other connected client goes stale.

This is probably acceptable for v1 of a local dev tool, but worth documenting as a known limitation. A small mitigation: the Versions tab could show which worktree is currently active server-side (see point above).

Also: handle_git_worktree_switch only acts when config["mode"] == "single". In multi-project mode the handler returns {"success": True} without doing anything — the frontend shows a success toast but nothing changed. Might want to either support multi-mode or return an error/warning.


Minor / nits

  • Missing await on refreshWorktrees() calls in removeWorktree (~L4128) and addWorktreeDialog (~L4099). Consistent with existing patterns but technically leaves unhandled promise rejections.

  • Empty branches edge case: If availableBranches is empty in the dialog, the OK button is still enabled but does nothing (if (!branch) return). Consider disabling the button or showing a "no available branches" message.

  • Server filesystem paths are displayed raw in the worktree list (wt.path). Fine for a local dev tool, but something to be aware of if tinyleaf ever serves over a network.


What looks good

  • Tab integration is clean — switchGitSubTab correctly handles the new versions case.
  • Worktree list rendering with branch, abbreviated HEAD, path, and main badge is informative.
  • Remove button visibility on hover is consistent with the existing branch-delete pattern.
  • i18n is thorough — both en and zh strings present for all new keys.
  • CSS is well-structured and consistent with existing conventions.

Overall this is solid frontend work — the XSS fix is the only blocker, the rest are improvements that could go in a follow-up.

— Milo

@elena-oaklight elena-oaklight Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Review: feat: add worktree version panel and open-as-worktree action

Nice feature — the Versions sub-tab, worktree CRUD, and the "open as worktree" shortcut in the branch dropdown are well-integrated with the existing git panel patterns. CI passes, i18n is complete for both languages, and the core list rendering in refreshWorktrees correctly uses DOM API (createElement/textContent) which is safe from XSS.

A few items to address:


🟡 XSS in addWorktreeDialog — use esc() or DOM API for branch names

branchOptions = availableBranches.map(b => `<option value="${b}">${b}</option>`).join("");

Branch names are interpolated directly into an innerHTML template without escaping. While git refname rules prevent most HTML-dangerous characters, a branch name containing " could break out of the value attribute. The codebase already has an esc() helper — use it here:

branchOptions = availableBranches.map(b => `<option value="${esc(b)}">${esc(b)}</option>`).join("");

Or better yet, build the <select> via DOM API (createElement + textContent) like refreshWorktrees does, which sidesteps the issue entirely.


🟡 Wrong/reused i18n keys in the worktree dialog

Three keys are borrowed from the branch UI but are semantically wrong in this context:

Current key Renders as Should be
add_worktree (dialog title) "+ Add worktree" "Add worktree" (no "+" prefix in a title)
switch_branch (select label) "Switch to branch" / "切换分支" "Branch" or new key like worktree_branch_label
create_branch (OK button) "Create" / "创建分支" New key like create_worktree — "Create worktree" / "创建工作树"

The OK button saying "Create Branch" when the user is creating a worktree is confusing.


💭 Minor observations

  1. DELETE with request body (removeWorktree sends { path } via DELETE) — this works but is unconventional; some proxies/CDNs strip DELETE bodies. Consider switching to DELETE /api/projects/:name/git/worktrees?path=... with the path as a query parameter, or using POST. Low priority since this is a local-first app.

  2. openAsWorktree doesn't show a loading indicator — switchWorktree sets a status message ("Switching worktree...") before the API call, but openAsWorktree doesn't. Adding a brief status like setStatus(t("loading")) before the API call would give feedback on slower repos.

  3. addWorktreeDialog doesn't handle loadBranches() failure — if the branch fetch fails (network error, non-git project), the dialog will throw without user-visible feedback. Wrapping in try/catch with a status message would be more robust.

  4. Backend note (pre-existing, not this PR): handle_git_worktree_switch sets config["project_path"] = path after only checking os.path.isdir(path) — it doesn't verify the path is actually a registered worktree of the current repo. A separate issue might be warranted to add that validation.


✅ What looks good

  • refreshWorktrees uses safe DOM construction throughout
  • setStatus uses textContent — no injection risk
  • CSS patterns (hover reveals, badges, truncation) are consistent with existing branch/history UI
  • Bilingual i18n strings are complete and natural
  • Main worktree is correctly protected from removal (no remove button rendered)
  • _run_git uses list-form subprocess.run — no shell injection surface

Overall solid work. The XSS and i18n label items are the actionable ones.

@Oaklight

Copy link
Copy Markdown
Owner Author

Addressing all review feedback

Thanks @clementine-oaklight, @milo-oaklight, @elena-oaklight — all three reviews received. Every finding (blockers + non-blocking) will be fixed in a follow-up PR on master.

Blockers

  • XSS in addWorktreeDialog: Switch to DOM API (createElement + textContent) for select/options — flagged by all three reviewers ✅
  • Main worktree not clickable: Move item.onclick outside if (!wt.is_main) so users can switch back ✅

Non-blocking (all being fixed)

  • openAsWorktree() should auto-switch after creation, handle "already checked out" ✅
  • Wrong i18n keys: add create_worktree, select_branch, fix dialog title ✅
  • confirm() → custom dialog for remove-worktree consistency ✅
  • Add unsaved-buffer check before worktree switch ✅
  • .current class should mark active worktree, not just main ✅
  • Multi-project mode: return error instead of silent no-op ✅
  • Missing await on refreshWorktrees() calls ✅
  • Disable OK button when no branches available ✅
  • Loading indicator for openAsWorktree ✅
  • Error handling for loadBranches() failure ✅
  • Backend: validate worktree path belongs to current repo ✅

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.

Frontend: worktree version panel and create dialog

1 participant