Skip to content

Commit f97a424

Browse files
committed
fix(tui): retry a skipped branch read and keep the last label
A turn-end refresh that overlapped an in-flight lookup was dropped, so the border kept a stale branch until a later turn. Remember one follow-up and run it when the lookup settles. A null result after a known branch is a timeout or failure, not a reason to clear the suffix.
1 parent 9a0b1a9 commit f97a424

3 files changed

Lines changed: 73 additions & 9 deletions

File tree

‎src/tui/runner-host.test.ts‎

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,30 @@ describe("mountRunnerHost branch label", () => {
120120
},
121121
);
122122
});
123+
124+
test("a null git result after a known branch leaves the previous suffix", async () => {
125+
const emitter = new EventEmitter();
126+
let branch: string | null = "main";
127+
await withRunnerHost(
128+
async (host) => {
129+
await Promise.resolve();
130+
await Promise.resolve();
131+
expect(ruleOf(host.shell.promptBottomRule)).toContain("(main)");
132+
133+
branch = null;
134+
emitter.emit("event", { type: "inference.done" });
135+
await Promise.resolve();
136+
await Promise.resolve();
137+
expect(host.shell.workspace.branch).toBe("main");
138+
expect(ruleOf(host.shell.promptBottomRule)).toContain("(main)");
139+
},
140+
{
141+
eventEmitter: emitter,
142+
cwd: "/repo",
143+
fetchBranch: async () => branch,
144+
},
145+
);
146+
});
123147
});
124148

125149
function session(over: Partial<SubAgentSession>): SubAgentSession {

‎src/tui/workspace-watch.test.ts‎

Lines changed: 25 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -23,12 +23,13 @@ describe("watchGitBranch", () => {
2323
watch.stop();
2424
});
2525

26-
test("skips a refresh while the previous lookup is still outstanding", async () => {
26+
test("one refresh during an outstanding lookup runs a single follow-up and reports the later branch", async () => {
2727
let calls = 0;
2828
const pending: ((branch: string | null) => void)[] = [];
29+
const seen: (string | null)[] = [];
2930
const watch = watchGitBranch({
3031
cwd: "/repo",
31-
onBranch: () => undefined,
32+
onBranch: (b) => seen.push(b),
3233
fetchBranch: () => {
3334
calls += 1;
3435
return new Promise((resolve) => {
@@ -42,8 +43,29 @@ describe("watchGitBranch", () => {
4243

4344
pending[0]?.("main");
4445
await flush();
45-
watch.refresh();
4646
expect(calls).toBe(2);
47+
expect(seen).toEqual(["main"]);
48+
49+
pending[1]?.("topic");
50+
await flush();
51+
expect(calls).toBe(2);
52+
expect(seen).toEqual(["main", "topic"]);
53+
watch.stop();
54+
});
55+
56+
test("a null result after a known branch is not reported", async () => {
57+
const seen: (string | null)[] = [];
58+
let branch: string | null = "main";
59+
const watch = watchGitBranch({
60+
cwd: "/repo",
61+
onBranch: (b) => seen.push(b),
62+
fetchBranch: async () => branch,
63+
});
64+
await flush();
65+
branch = null;
66+
watch.refresh();
67+
await flush();
68+
expect(seen).toEqual(["main"]);
4769
watch.stop();
4870
});
4971

‎src/tui/workspace-watch.ts‎

Lines changed: 24 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,14 @@
55
* interval used to fork `git rev-parse` every few seconds for the whole TUI
66
* lifetime, including idle. Callers refresh on an event that can change the
77
* branch (a finished turn); staleness between those events is acceptable.
8-
* While a lookup is outstanding a refresh is skipped, so a git process hung
9-
* on a network filesystem cannot accumulate children. A lookup that lands
10-
* after stop is dropped. Nothing here blocks a paint — the border keeps the
11-
* last branch it was given, including across a failed lookup.
8+
* A refresh that arrives while a lookup is outstanding is remembered and
9+
* runs once that lookup settles: one follow-up child, not a loop, and not
10+
* a dropped turn-end read. A lookup that lands after stop is dropped.
11+
* Nothing here blocks a paint. The border keeps the last branch it was
12+
* given across a rejected lookup and across a null result: `getGitBranch`
13+
* resolves null for timeout, a non-zero exit, and a non-repo, and that
14+
* must not be treated as detached HEAD clearing the suffix. A null before
15+
* any branch was shown still reports, so a non-repo stays unlabeled.
1216
*/
1317

1418
import { getGitBranch } from "../agent/environment.js";
@@ -33,17 +37,31 @@ export function watchGitBranch(input: BranchWatchInput): BranchWatch {
3337
const fetchBranch = input.fetchBranch ?? getGitBranch;
3438
let inFlight = false;
3539
let stopped = false;
40+
let again = false;
41+
let shown: string | null = null;
3642

3743
const refresh = (): void => {
38-
if (inFlight || stopped) return;
44+
if (stopped) return;
45+
if (inFlight) {
46+
again = true;
47+
return;
48+
}
3949
inFlight = true;
50+
again = false;
4051
fetchBranch(input.cwd).then(
4152
(branch) => {
53+
// Null after a real branch is a failed lookup (or detached HEAD, which
54+
// shares that null). Keep the suffix; do not clear it.
55+
if (!stopped && !(branch === null && shown !== null)) {
56+
shown = branch;
57+
input.onBranch(branch);
58+
}
4259
inFlight = false;
43-
if (!stopped) input.onBranch(branch);
60+
if (!stopped && again) refresh();
4461
},
4562
() => {
4663
inFlight = false;
64+
if (!stopped && again) refresh();
4765
},
4866
);
4967
};

0 commit comments

Comments
 (0)