Repository navigation
feat(git): switch branches from the Git panel and composer chip - #17628
kyleseaman wants to merge 2 commits into
Conversation
Sensitive Change ReviewSensitive files changed:
How to clear this check:
Only the author's comment counts this way. A reviewer can instead |
Design Review (Opus 5.5) — 🟡 CONCERNSDesign-level review of Design-Verdict: CONCERNS A dashboard branch picker forces a busy-registry into every ACP prompt write, sub-agent run and workflow step, and it changes workflow behaviour even for users who never switch branches. Watch
Suggestions
[DESIGN-REVIEWED] 84c2088 Description read[DESCRIPTION-READ] b20855b7c8836b371d39799317af45d6c3aa03f1f94e73250cb28bf917967f14 That sha256 names everything this verdict read: the PR description, plus the 1 evidence file(s) the workflow collected from it and handed this review. It covers that evidence because the media strip replaces every attachment URL with the same placeholder, so an edit swapping one attachment for another would leave the description's captured text identical while what this review judged changed. Recompute it with |
First Principles Review (Opus 5.5) — 🔴 BLOCKPremise-level review of First-Principles-Verdict: BLOCK product-shape change without accepted RFC: a workflow step that fails or is cancelled now ends its session, so a named workflow session loses its conversation, branch switcher used or not. Not justified as shipped
What this change shipsInventory (10 items) — 9 justifiedIntent: switch, check out and create git branches from the dashboard without opening a terminal. ADDITION.
Blockers
[FIRST-PRINCIPLES-REVIEWED] 84c2088 Description read[DESCRIPTION-READ] 25d0f0a9a5f14d42091fca674822cfc6563e523d9879325f59456b60923d56d8 That sha256 names the PR description this verdict read. Recompute it with |
UX Review (Opus 5.5) — 🟡 CONCERNSUX-level review of UX-Verdict: CONCERNS Branch rows, the main switch target, were read correctly but only as "a guess", with the reader willing to click "only a little". Several notices name things the user cannot find. Watch
Suggestions
[UX-REVIEWED] 84c2088 Description read[DESCRIPTION-READ] 8f8b9f03a6bea5e71c3e5b7a257ad5de46f405c55fd68047d2b9bbacd05bddf6 That sha256 names everything this verdict read: the PR description, plus the 23 evidence file(s) the workflow collected from it and handed this review. It covers that evidence because the media strip replaces every attachment URL with the same placeholder, so an edit swapping one attachment for another would leave the description's captured text identical while what this review judged changed. Recompute it with |
GPT 6.1 Review — 🔴 changes requested (blocking)GPT 6.1 found at least one blocking issue that must be resolved before merging This comment is updated in place on each push. BLOCKING -- src/kiro_crew/dashboard/handlers/git_branches.py:821 -- Cancelled checkout requests can complete without SEL auditing
Adjudication (Opus 5.5) — is blocking on each finding proportionate?F1 (fenced): I upheld this one. The scenario is uncommon but nothing about it is contradictory, and once it happens nothing brings the audit record back.
False positive or not applicable? A repository writer can comment: |
Opus 5.5 Review — ✅ no blocking findingsReviewed Review detailsBranch switching can stay refused until the gateway restarts after a single knowledge-pool timeout. FINDING — src/kiro_crew/knowledge/llm_pool.py:904 — [OPUS-REVIEWED] 84c2088 Verdict parsed from the review's SHA-scoped output markers for commit False positive or not applicable? A repository writer can comment: |
a48259a to
20485b4
Compare
20485b4 to
d31fff7
Compare
d31fff7 to
a69e356
Compare
a69e356 to
ae6d5fc
Compare
6bdc91c to
5fd2b16
Compare
5fd2b16 to
a7be5ba
Compare
a7be5ba to
8343a00
Compare
8343a00 to
ce4ef66
Compare
ce4ef66 to
ec33897
Compare
ec33897 to
5e1fe8c
Compare
|
Holding at 26aa2bd. This needs a human maintainer and author replies before it can be approved.
I did not read every line of the 7,400-line diff, so this is not a full code review. — crew-pr-reviewer (r111) |
Clicking the branch name in the Git panel header, or the branch segment of the project chip under the chat box, opens a searchable picker that switches local branches, checks out remote-only branches as tracking branches, or creates a branch from the current commit. GET /api/project/git/branches lists branches; POST /api/project/git/switch is owner-gated, runs git sandboxed with hooks off, never discards work, and refuses for subfolder projects and repos declaring content filters.
- Refuse a switch with 409 git_switch_session_busy while any chat slot working in the same repository has a turn running or sub-agents pending; the picker's own guard only sees its own chat. - Pass --no-recurse-submodules on every git switch so a submodule's own filter drivers are never run. - Redact git's diagnostic line before clipping it. - Mark remote rows switchable only when the short name passes the follow-up branch grammar the track path enforces. - Drop the sha, upstream, upstreamGone and previous fields no client reads; the same-branch no-op reads HEAD with symbolic-ref. - Replace _run_git's free-form env_overrides with a c_locale flag. - Add sandbox-free tests covering the handler where real git cannot run, patched through _floor_monkeypatch and portable to Windows paths. - Keep the branch copyable while a response runs, with a visible reason and an ErrorNotice when the copy fails. - Picker says what a switch does, confirms a switch, hides the safety line beside a refusal, leads refusals with the outcome, mutes rows when switching is blocked, and explains ahead/behind counts.
|
Hi @kyleseaman, a few of us want branch switching in the dashboard (#22670), so I picked this up in #23102: this PR rebased onto current main, with the CI reds fixed and the review findings addressed. The biggest change is dropping the workflow session teardown that First Principles blocked on, since the prompt tracking at the transport already covers that case. You're credited as co-author. If you'd rather keep this one, say so and I'll close mine and hand the changes over. |
Problem / Motivation
Goal: Switch, check out and create git branches from the dashboard's Git panel and from the project chip under the chat box, the way VS Code and Zed do from their status bar.
Today the Git (Changes) panel header and the composer's project chip only show the current branch with a click-to-copy button. Changing branch means leaving the dashboard for a terminal, then coming back and waiting for the panel to notice.
Why it matters
Moving between local branches is the most common git action while reviewing an agent's work in a session's project folder. Without a switcher the dashboard shows the branch but cannot act on it, so every switch costs a context change to a terminal, and the file tree and Changes list lag until they repoll.
Not a goal
What changed (motivation → approach → change)
Goal → a single searchable picker reused on both surfaces, backed by two narrow endpoints → built:
GET /api/project/git/branches(src/kiro_crew/dashboard/handlers/git_branches.py): local branches newest-commit first (author, age, subject, ahead/behind), plus remote branches that have no local twin. Subjects and the list are capped and the response says when it is truncated. Branch names, subjects and errors pass through the shared redactor.POST /api/project/git/switch: owner-only (same gate as saving a file), audited, git run through the existing sandboxed_run_gitwith hooks off andLC_ALL=C(newc_localeflag on_run_git) so refusals are classified on git's English text. Switches use--no-overwrite-ignore, so neither tracked uncommitted work nor ignored local files are ever overwritten, and--no-recurse-submodules, so a repo'ssubmodule.recursecannot make a checkout reach a submodule's own filter drivers; git's own refusal is shown in plain language, naming the branch. git's diagnostic line is redacted before it is clipped. Refused for subfolder projects and for repos whose config declares a checkout content filter (a checkout would run the filter program). Refused with 409git_switch_session_busywhile ANY chat slot working in the same repository (its root, a folder inside it, or a folder containing it) has a turn running or sub-agents pending (a running turn counts in every folder it works in, not just the slot's current project: the project it started in, the project its session was allocated for, and the folder that session reports, plus the live session's own folder, since a project picked mid-turn,set_projectand a member rebinding move the slot's project but not the running session, and a kept or resumed session can run where no project value names; while a turn is between starting to allocate its session and that session reporting its folder it counts as busy in every repository), or a workflow step's session runs in it (workflow steps open their own sessions outside chat slots and the sub-agent manager, so each step registers itself with the busy check: busy everywhere until its session reports a folder, then busy in that folder and in the project its prompt is built against; a step whose turn the backend did not itself report finished (it failed, was cancelled, was ended by the session layer, or was cut off at its tool-call ceiling) ends its session before the busy check stops seeing it, since none of those stops the session's native turn: the session is destroyed, never a successor under the same name, and the step stays busy until the runtime confirms the process tree exited, retrying if it has not; a named workflow session loses its conversation when one of its steps ends that way, and a runtime that never confirms its process tree exited keeps that repository's switch refused until restart; each retry re-checks the descendants a teardown left for the orphan sweep (AcpRuntime.kill), so the step stops counting once they are gone), or any agent prompt is in flight there, whatever drives it (a messaging-channel turn on Slack, Telegram, Discord and the rest, a monitor wake, a cron or heartbeat turn, an app's turn: every harness sends a turn as an ACP request throughAcpClient._send_requestfor the adapter harnesses (claude, codex, goose, opencode and the rest) or one ofAcpRuntime's three request writers for the shared kiro/KAS/codex runtime, and each prompt (a turn, a kiro slash command, or a mid-turn steer, which counts until answered and, when the adapter answers that it started a turn of its own, until the process tree is confirmed gone) is recorded on its session's folder, or as busy in every repository when that is unknown, from before its write until the backend's own answer to it (matched on the exact integer id issued) is read off the wire or the runtime confirms the process tree exited; the knowledge pool's legacy stream-jsonCCWorker, the one agent worker that speaks no ACP, counts its prompts the same way until their result is read; a cancel, a timeout, an end of turn the session layer makes up, or a caller that stops reading leaves it counted, since none of those stops the backend's tools; before the write a prompt waits out a switch of its folder, adding no wait when none is in progress; a process whose teardown left survivors keeps its prompts counted until a later kill confirms them gone, so a runtime that never confirms that keeps the repository's switch refused until restart), or any running sub-agent works inside the repository through its own folder (itscwd, or the folder its session started in) or an ancestor run's (a sub-agent whose ancestry or folder is unknown counts as busy: a dismissed parent card, or a run with nocwdwhose session has not started yet, since the allocator picks its folder, e.g. a crew member's workspace), checked under the per-repo lock before git runs, so a second tab, a cron turn on another slot or a lingering sub-agent cannot have files rewritten under it. Neither turn nor sub-agent admission takes that lock, so the switch also reserves the repository (newrepo_checkout_guard.py, outside the dashboard package so the ACP transport and workflows do not import dashboard code; the dashboard-only slot and sub-agent readers live indashboard/repo_checkout_guard.py) before the busy check._run_chatwaits at its entry, again (for any checkout anywhere) just before it captures its binding and claims its session, and once more on the folder the claimed session reports; every sub-agent run waits just before it executes (apre_run_gateon the manager, covering spawn, retry and continuation), while a reservation covers their folder: work either is already visible to the busy check or starts only after git finishes. The wait has no deadline, and the reservation is released by the checkout worker itself, so a cancelled request cannot drop it mid-checkout. Every error goes through the coded{error, code}body. A gap that predates this PR and is unchanged: a prewarmed session can start kiro-cli in a repository mid-switch, though the turn that later claims it waits for the switch. A sub-agent run counts as busy from start until its teardown is over (its run or recovery task, teardown gate and retained kill handles), not just until it is marked done, and a teardown entry whose run record was already cleared counts in every repository. A sub-agent whose kill failed, or whose shared-runtime cancel did not stop its native turn, stays counted through its unanswered prompt (above) until the backend answers it or the process tree is confirmed gone.BranchSwitcher.tsx: the picker (filter, arrow keys, Enter/Tab to switch to an existing branch, Escape; a "New branch "name" from " row below a divider, which shows "Press Ctrl+Enter or click to create it" only while it is highlighted, and which only Ctrl+Enter / ⌘+Enter or a click activates, so a mistyped search plus Enter never creates a branch; the initial highlight lands on a matching branch, not the create row; footer copy chip and a "↑ ahead · ↓ behind its remote branch" legend whenever a row shows a count). Used in the Git panel header and as achipvariant on the composer project chip, which opens upward. A line above the list, in normal body text, says what a switch does and then the guarantee ("Switching updates the files in this project folder to that branch's version. Your uncommitted changes are kept. If a switch would overwrite any of them, nothing changes."), using "uncommitted changes", the Changes badge's own term, in every branch-picker string, and each switchable row repeats the guarantee in its tooltip ("Your uncommitted changes are kept."); the search field's placeholder is "Find a branch or name a new one…", so it does not suggest Enter creates; the search field is described by the line, so a screen reader hears it while arrowing through rows; it is hidden while a refusal is shown. A successful switch shows "✓ Switched to {{branch}}" beside the trigger and announces it to screen readers. Refusals lead with the outcome and the next step ("Not switched: this branch would overwrite some of your uncommitted changes. Commit them, or ask the agent in this chat to commit or stash them, then switch again. (In a terminal: git commit or git stash.) Files in the way: maps/MOC-tools.md.", naming up to five of the paths git lists, then "+N"; "Switching and creating branches are off for this repository. Its Git settings ask Git to run another program whenever it switches branches, and this panel won't run it. Switch in a terminal instead."). When rows sit below the list's scroll edge, a chevron strip under the list (it covers no row) shows there is more, so a section heading that lands on the edge does not read as an empty section. Ahead/behind pills carry a tooltip and screen-reader text ("2 commits ahead of its remote branch", the same term as the legend and the Remote section). While a response is running, switching is off on both surfaces and the trigger copies the branch name instead. The trigger itself looks busy then (muted text and glyph, a copy glyph, a copy cursor; all static, since it lasts the whole response), and the reason is shown in a visible note on hover and focus ("Copy branch name. To switch branch, first stop the response with ■ next to Send."), so reading the branch stays available mid-run; a failed copy shows anErrorNoticebeside the trigger, quoting the name ("Couldn't copy “develop” to the clipboard."), with a Try again button (the footer copy inside the picker does the same). An open picker closes if a turn starts, because a checkout would change files under the running turn. When a content filter blocks switching, rows are muted and inert and the search box is disabled and drawn dimmed. Remote rows show the branch name without its<remote>/prefix (kept only when two remotes carry the same name) and a line leading with the outcome ("Switch to feature/x (makes a local copy; uncommitted changes are kept)"). Remote rows whose short name fails the new-branch grammar are shown but not offered. After a switch the Changes/Commits lists, file tree and composer chip refresh.LazyBranchSwitcher.tsx: both hosts load the picker through thisimport()boundary, so its ~17 KB stays out of the always-loadedChatMessageListchunk (main has 8 KB of headroom under the Bundle Size Gate's 500 KB budget; this PR adds ~1 KB there). Until the chunk arrives on first render, a non-interactive placeholder of the same size shows the branch, so the chip and the header do not shift.GitPanel.tsx,GitReposPanel.tsx): since feat(git): list every repo a chat works in on the Git tab #18233 the Git tab shows one collapsible section per repository the chat works in. The switcher is on the chat project's section only, since the switch route accepts slot projects, not repositories a tool call merely touched; other sections keep the plain branch label. A section heading is one toggle button and a button cannot nest in a button, so on the project section the status line follows the toggle and the toggle's hit area (::after) stretches over it: a click on the change count still toggles, the switcher takes its own clicks, and the line is the toggle's accessible description.src/kiro_crew/workflows/agent_exec.py,agent_pool.py): each step wraps its session work inrepo_checkout_guard.tracked_work, so the busy check sees it, and the step waits for a switch in progress before it starts its session.src/kiro_crew/providers/acp.py): a step keeps its session only on the backend's own completion event (stream_and_collect's existingon_complete);AcpProvider.process_tree_confirmed_deadexposes the runtime's own evidence that a shutdown ended the process tree, which is what lets the busy check stop counting a step whose session it ended.website/scripts/capture-branch-switcher.mjs: the screenshot harness for the frames below, following the repo'scapture-*.mjsconvention (mocked git responses against the production build; each mocked branch has its own commit history, so frames after a switch show the Commits list change).Backwards compatibility
Compatible: two new routes and a new optional
c_localeflag on_run_git; existing git endpoints and their responses are unchanged.Changes one click target, removes no capability: when idle, clicking the branch name on the composer chip or Git panel header opens the picker instead of copying (copy is in the picker footer); while a response runs, the same click still copies, as before. The pin "leaves the branch copyable while a response is running" is kept.
Changes one workflow behaviour: a workflow step whose turn the backend did not report finished (failure, cancel, tool-call ceiling, or a terminal the session layer made up) now ends its session (
agent_pool._run_unpooled_tracked,_WorkflowSessionWorker), so a named workflow session loses its conversation after such a step instead of carrying it to the next one. Pinned intest/test_repo_checkout_guard.pyandtest/test_workflows_agent_pool_unpooled_teardown.py; a step the backend reported finished keeps its session as before.Tests
test/test_project_git_branches.py(46 tests, real git repositories): listing order and fields, remote-twin filtering, truncation, switching, creating, tracking, the owner gate, subfolder and content-filter refusals, uncommitted-change refusal, ignored-file preservation under--no-overwrite-ignore(fails against plaingit switch), non-English git classification, not-a-repo vs ownership-check verdicts, redacted names, andsubmodule.recurse=trueleaving the submodule untouched.test/test_repo_checkout_guard.py: the reservation registry; both admission orderings, including the reported race (a turn admitted while the busy check's sub-agent probe is suspended waits until git exits; fails with the entry wait removed); a long checkout waited out rather than abandoned; sub-agent runs gated on every folder in their lineage, a Stop during that wait, and the busy check seeing a running sub-agent that works in the repository from a chat outside it; a running turn seen through every folder it works in (mid-turn pick, the live session's folder, an unplaced turn counting everywhere, an unreadable session registry), a turn allocating its session waiting out a checkout of an unrelated repository, and a claimed session's reported folder recorded and waited on; a workflow step tracked on each of its session paths (cold, pooled worker start, reset and turn, named session), unplaced while it claims and placed where its session runs and where its prompt reads; a step's session kept only after a backend-reported end of turn and destroyed, while the step is still busy, after a failure, a cancel, the ceiling, or any terminal the session layer made up, on each session path, never a successor under the same name; the step kept busy while a sibling's turn holds the session, while the destroy raises, and while a destroy that returned leaves the process tree standing; a session another teardown removed shut down on retry; a cancelled retry keeping the record; and a second cancel finishing the teardown rather than restarting it.test/test_project_git_branches_coverage.py(115 tests, no git, no sandbox): the real-git file skips on CI shards without a namespace sandbox, so this file fakes the git seams and covers every refusal, parse, switch and exception path (100% lines measured on its own), including a token straddling the 300-character diagnostic cut, the repository reserved before the busy check, a second switch refused while one holds it, and a cancelled request keeping the reservation until git exits.website/src/test/BranchSwitcher.test.tsx,LazyBranchSwitcher.test.tsx,GitPanel.test.tsx,GitReposPanel.test.tsx,ChatInput.projectBranch.test.tsx: the switcher on the project section only and outside the section toggle, the lazy boundary (placeholder, hand-over, props pass-through, no static import from either host), picker behaviour, keyboard navigation (Enter/Tab never create; Ctrl+Enter, ⌘+Enter and click do), initial highlight, create row (its base branch or commit, and a key hint shown only while it is highlighted), ahead/behind legend, disabled search when filter-blocked, error notices naming the branch, copy-while-running on both surfaces, the busy-looking trigger, the scroll-edge cue, muted filter-blocked rows, refresh after switch.test/test_prompt_checkout_seam.py(54 tests): every prompt counted at both ACP transports from before its write until its exact answer is read (a cancelled caller, a later turn reading it, an error answer), a prompt with no folder busy everywhere, a prompt waiting out a switch of its folder before the write, a switch reserved during the off-loop folder match waited out, no suspension when no switch is in progress, all three runtime request writers and the client's, steers (astartedNewTurnanswer latched against any duplicate until the process tree is confirmed gone), KAS checkpoint restores, exact integer ids only (1.9,"1",trueend nothing), a respawned process never answering its predecessor, teardowns ending prompts only on a confirmed tree exit, and the knowledge pool's legacy worker. Each protection was disabled in turn and a test failed (27 of 27).test/test_json_object_body_guard.pyregisters the switch body's cap decision;ApiClient.refactor.surface.test.tspins the two new client methods.Local, on this head: black, isort, flake8 and mypy on every changed Python file; tsc, eslint and
i18n:check; 25 vitest files (723 tests); 27,266 backend tests across the guard, the ACP transports and runtime, sessions, KAS, harness parity, chat turns, workflows, sub-agents, channels, the knowledge pool, steering and the rule/ratchet scans, whose 20 failures reproduce on cleanorigin/mainon this host (19 deterministic, plustest_session_tree_push, which also fails 1 run in 6 there); the comment-history and line-cap checks.test:electroncould not run locally. Full CI is the authority.Manual verification
Driven in the production build with mocked git responses (not a live gateway): open/filter/switch, dirty-tree refusal, filter-blocked state, composer chip open upward and after-switch refresh, dark and light themes.
Screenshots / video
Git panel picker, open and filtered:
Composer chip opening the same picker upward:
Uncommitted changes would be overwritten, so git refuses and the picker says so:
More states
Related Issues
N/A — requested directly by a maintainer.
Checklist
Contribution License Agreement
Contributions are accepted under the project's Apache-2.0 license.