fix: v2.5.11 — the commit box wraps only when the panel is narrow

The message keeps the row with the commit, Stage all and Unstage all
buttons while there is room (a 50/50 split, a full-width panel); in a
narrow panel the buttons wrap under it instead of squeezing the message
to a few characters. Client 1095/1095 on the committed tree.
This commit is contained in:
2026-10-07 15:40:43 +07:00
parent f0656603c3
commit 1619954fad
3 changed files with 71 additions and 62 deletions
@@ -2099,13 +2099,15 @@ export function LaneChangesPanel({
{(stagedCount > 0 || toStage.length > 0) && ( {(stagedCount > 0 || toStage.length > 0) && (
<div <div
data-testid="lane-changes-actions" data-testid="lane-changes-actions"
className="sticky top-0 z-10 mb-1.5 flex items-center gap-1.5 border-b border-border-light bg-surface-1 pb-2" className="sticky top-0 z-10 mb-1.5 flex flex-wrap items-center gap-1.5 border-b border-border-light bg-surface-1 pb-2"
> >
{/* ONE row: message, commit, then the two bulk index moves. */} {/* One row when there is room; in a narrow panel the buttons
wrap below rather than squeeze the message to a few
characters (its min width is what triggers the wrap). */}
{stagedCount > 0 && ( {stagedCount > 0 && (
<div <div
data-testid="lane-changes-commit" data-testid="lane-changes-commit"
className="flex min-w-0 flex-1 items-center gap-1.5" className="flex min-w-[14rem] flex-1 items-center gap-1.5"
> >
<input <input
value={message} value={message}
@@ -2138,63 +2140,69 @@ export function LaneChangesPanel({
<Sparkles className="h-3 w-3" aria-hidden /> <Sparkles className="h-3 w-3" aria-hidden />
)} )}
</button> </button>
<label
title={t("changes.amendHint")}
className="flex shrink-0 cursor-pointer items-center gap-1 text-[11px] text-fg-secondary"
>
<input
type="checkbox"
data-testid="lane-changes-amend"
checked={amend}
onChange={(e) => setAmend(e.target.checked)}
/>
{t("changes.amend")}
</label>
<button
type="button"
disabled={busy || suggesting || !(message.trim() || amend)}
onClick={() => void commitStaged()}
className="shrink-0 flex items-center gap-1 rounded border border-border-light px-1.5 py-0.5 text-[11px] text-fg-secondary hover:bg-surface-2 hover:text-fg-primary disabled:cursor-not-allowed disabled:opacity-40"
>
{amend
? t("changes.amend")
: t("changes.commitStaged", { count: stagedCount })}
</button>
</div> </div>
)} )}
{toStage.length > 0 && ( <div className="flex flex-wrap items-center gap-1.5">
<button {stagedCount > 0 && (
type="button" <>
disabled={busy} <label
onClick={() => title={t("changes.amendHint")}
void act( className="flex shrink-0 cursor-pointer items-center gap-1 text-[11px] text-fg-secondary"
"stage", >
toStage.map((f) => f.path) <input
) type="checkbox"
} data-testid="lane-changes-amend"
className="shrink-0 flex items-center gap-1 rounded border border-border-light px-1.5 py-0.5 text-[11px] text-fg-secondary hover:bg-surface-2 hover:text-fg-primary disabled:cursor-not-allowed disabled:opacity-40" checked={amend}
> onChange={(e) => setAmend(e.target.checked)}
<Plus className="h-3 w-3" /> />
{t("changes.stageAll", { count: toStage.length })} {t("changes.amend")}
</button> </label>
)} <button
{stagedCount > 0 && ( type="button"
<button disabled={busy || suggesting || !(message.trim() || amend)}
type="button" onClick={() => void commitStaged()}
data-testid="lane-changes-unstage-all" className="shrink-0 flex items-center gap-1 rounded border border-border-light px-1.5 py-0.5 text-[11px] text-fg-secondary hover:bg-surface-2 hover:text-fg-primary disabled:cursor-not-allowed disabled:opacity-40"
disabled={busy} >
onClick={() => {amend
void act( ? t("changes.amend")
"unstage", : t("changes.commitStaged", { count: stagedCount })}
files.filter((f) => f.staged).map((f) => f.path) </button>
) </>
} )}
className="shrink-0 flex items-center gap-1 rounded border border-border-light px-1.5 py-0.5 text-[11px] text-fg-secondary hover:bg-surface-2 hover:text-fg-primary disabled:cursor-not-allowed disabled:opacity-40" {toStage.length > 0 && (
> <button
<Minus className="h-3 w-3" /> type="button"
{t("changes.unstageAll", { count: stagedCount })} disabled={busy}
</button> onClick={() =>
)} void act(
"stage",
toStage.map((f) => f.path)
)
}
className="shrink-0 flex items-center gap-1 rounded border border-border-light px-1.5 py-0.5 text-[11px] text-fg-secondary hover:bg-surface-2 hover:text-fg-primary disabled:cursor-not-allowed disabled:opacity-40"
>
<Plus className="h-3 w-3" />
{t("changes.stageAll", { count: toStage.length })}
</button>
)}
{stagedCount > 0 && (
<button
type="button"
data-testid="lane-changes-unstage-all"
disabled={busy}
onClick={() =>
void act(
"unstage",
files.filter((f) => f.staged).map((f) => f.path)
)
}
className="shrink-0 flex items-center gap-1 rounded border border-border-light px-1.5 py-0.5 text-[11px] text-fg-secondary hover:bg-surface-2 hover:text-fg-primary disabled:cursor-not-allowed disabled:opacity-40"
>
<Minus className="h-3 w-3" />
{t("changes.unstageAll", { count: stagedCount })}
</button>
)}
</div>
</div> </div>
)} )}
{/* The work range splits like VS Code's Source Control: what the {/* The work range splits like VS Code's Source Control: what the
@@ -1069,7 +1069,7 @@ describe("LaneChangesPanel", () => {
expect(screen.getByTestId("lane-file-editor")).toHaveValue("xy"); expect(screen.getByTestId("lane-file-editor")).toHaveValue("xy");
}); });
it("unstages every staged file in one call, on the same row as Stage all and commit", async () => { it("unstages every staged file in one call, in the same pinned bar as Stage all and commit", async () => {
const user = userEvent.setup(); const user = userEvent.setup();
render(<LaneChangesPanel laneId={7} />); render(<LaneChangesPanel laneId={7} />);
await screen.findByTestId("lane-changes-overview"); await screen.findByTestId("lane-changes-overview");
+4 -3
View File
@@ -733,9 +733,10 @@ in every panel narrow enough to sit beside a console.
**Per-file writes are the panel's, per-repo writes are the git bar's.** Staging **Per-file writes are the panel's, per-repo writes are the git bar's.** Staging
one file means nothing unless a commit can take the index as it stands, so the one file means nothing unless a commit can take the index as it stands, so the
overview grows a commit box the moment anything is staged — above the list and overview grows a commit box the moment anything is staged — above the list and
pinned while it scrolls, on ONE row with a **Stage all** that stages every listed file pinned while it scrolls: the message (and its suggest button), the commit button, a **Stage all** that stages every listed file
the index lacks in one call and an **Unstage all** that unstages every staged one — it sends the index lacks in one call and an **Unstage all** that unstages every staged one — one row when the
`only: "staged"`, never `add -A`, and it is absent with a clean index because panel has room, the buttons wrapping under the message when it is narrow. The commit sends
`only: "staged"`, never `add -A`, and the box is absent with a clean index because
there is nothing it could mean. A file you never want committed (a local note, a scratch folder) can be marked **Never stage** (its row's eye-off button, or right-click): **Stage all** and a folder's stage button then skip it, the row is struck through, and the file is drawn orange in the Files tree (`--kept`, apart from the amber of a modified file). The list is kept in this browser per workspace (`agenthub.changes.neverStage.<lane id>`) and git is never told — nothing is written to the repo, and the file's own Stage still works. It is the dashboard's ONLY commit: the git bar there is nothing it could mean. A file you never want committed (a local note, a scratch folder) can be marked **Never stage** (its row's eye-off button, or right-click): **Stage all** and a folder's stage button then skip it, the row is struck through, and the file is drawn orange in the Files tree (`--kept`, apart from the amber of a modified file). The list is kept in this browser per workspace (`agenthub.changes.neverStage.<lane id>`) and git is never told — nothing is written to the repo, and the file's own Stage still works. It is the dashboard's ONLY commit: the git bar
above it has none, so no click can sweep the rest of the tree in with `add -A`. **Discard** is the one above it has none, so no click can sweep the rest of the tree in with `add -A`. **Discard** is the one
irreversible action in the panel: it asks twice in the UI, and the server irreversible action in the panel: it asks twice in the UI, and the server