fix(tui): gated git status line work
Added a git.enabled setting and skipped footer/status-line git probes when integration is disabled or no git-backed status segment is rendered. Fixes #2847
This commit is contained in:
@@ -59,6 +59,8 @@
|
||||
- Fixed startup model fallback choosing the plain OpenAI `gpt-5.5` provider before the Codex OAuth provider when both shared the same default model id, which could surface a misleading OpenAI 401 despite valid Codex credentials ([#2807](https://github.com/can1357/oh-my-pi/issues/2807)).
|
||||
- Fixed local auto-thinking classification for reasoning-capable tiny models by giving them the same safe answer budget as online reasoning classifiers, with a larger local floor for non-reasoning tiny models ([#2808](https://github.com/can1357/oh-my-pi/issues/2808)).
|
||||
|
||||
- Fixed WSL2 TUI stutter by adding a `git.enabled` setting and skipping footer/status-line git probes when disabled or when no git-backed status segment is visible ([#2847](https://github.com/can1357/oh-my-pi/issues/2847)).
|
||||
|
||||
### Removed
|
||||
|
||||
- Removed the built-in `render_mermaid` tool and its `renderMermaid.enabled` setting, so it can no longer be invoked directly
|
||||
|
||||
@@ -415,6 +415,16 @@ export const SETTINGS_SCHEMA = {
|
||||
},
|
||||
},
|
||||
shellPath: { type: "string", default: undefined },
|
||||
"git.enabled": {
|
||||
type: "boolean",
|
||||
default: true,
|
||||
ui: {
|
||||
tab: "interaction",
|
||||
group: "Git",
|
||||
label: "Enable Git Integration",
|
||||
description: "Show git branch, status, and PR information in the TUI and watch repository metadata.",
|
||||
},
|
||||
},
|
||||
|
||||
extensions: { type: "array", default: EMPTY_STRING_ARRAY },
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ import { stripVTControlCharacters } from "node:util";
|
||||
import { ThinkingLevel } from "@oh-my-pi/pi-agent-core";
|
||||
import { type Component, padding, truncateToWidth, visibleWidth } from "@oh-my-pi/pi-tui";
|
||||
import { formatNumber, getProjectDir } from "@oh-my-pi/pi-utils";
|
||||
import { settings } from "../../config/settings";
|
||||
import { theme } from "../../modes/theme/theme";
|
||||
import type { AgentSession } from "../../session/agent-session";
|
||||
import { shortenPath } from "../../tools/render-utils";
|
||||
@@ -58,6 +59,8 @@ export class FooterComponent implements Component {
|
||||
this.#gitWatcher = null;
|
||||
}
|
||||
|
||||
if (!settings.get("git.enabled")) return;
|
||||
|
||||
void git.head
|
||||
.resolve(getProjectDir())
|
||||
.then(head => {
|
||||
@@ -102,6 +105,7 @@ export class FooterComponent implements Component {
|
||||
* Returns null if not in a git repo, branch name otherwise.
|
||||
*/
|
||||
#getCurrentBranch(): string | null {
|
||||
if (!settings.get("git.enabled")) return null;
|
||||
if (this.#cachedBranch !== undefined) {
|
||||
return this.#cachedBranch;
|
||||
}
|
||||
|
||||
@@ -104,6 +104,13 @@ const EMPTY_MESSAGES: readonly AgentMessage[] = [];
|
||||
function hasContextSegment(segments: readonly StatusLineSegmentId[]): boolean {
|
||||
return segments.includes("context_pct") || segments.includes("context_total");
|
||||
}
|
||||
function hasGitSegment(segments: readonly StatusLineSegmentId[]): boolean {
|
||||
return segments.includes("git");
|
||||
}
|
||||
|
||||
function hasPrSegment(segments: readonly StatusLineSegmentId[]): boolean {
|
||||
return segments.includes("pr");
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
// StatusLineComponent
|
||||
@@ -166,6 +173,9 @@ export class StatusLineComponent implements Component {
|
||||
transparent: settings.get("statusLine.transparent"),
|
||||
};
|
||||
}
|
||||
#gitEnabled(): boolean {
|
||||
return settings.get("git.enabled");
|
||||
}
|
||||
|
||||
/**
|
||||
* Re-point the status line at another session (focus proxy). Invalidate: model/context/usage all derive
|
||||
@@ -241,6 +251,11 @@ export class StatusLineComponent implements Component {
|
||||
this.#gitWatcher = null;
|
||||
}
|
||||
|
||||
if (!this.#gitEnabled()) {
|
||||
this.#invalidateGitCaches();
|
||||
return;
|
||||
}
|
||||
|
||||
const repository = git.repo.resolveSync(getProjectDir());
|
||||
if (!repository) return;
|
||||
|
||||
@@ -286,6 +301,8 @@ export class StatusLineComponent implements Component {
|
||||
this.#cachedPrContext = undefined;
|
||||
}
|
||||
#getCurrentBranch(): string | null {
|
||||
if (!this.#gitEnabled()) return null;
|
||||
|
||||
const cwd = getProjectDir();
|
||||
if (this.#cachedBranch !== undefined && this.#cachedBranchCwd === cwd) {
|
||||
return this.#cachedBranch;
|
||||
@@ -322,6 +339,7 @@ export class StatusLineComponent implements Component {
|
||||
}
|
||||
|
||||
#getGitStatus(): { staged: number; unstaged: number; untracked: number } | null {
|
||||
if (!this.#gitEnabled()) return null;
|
||||
if (this.#gitStatusInFlight || Date.now() - this.#gitStatusLastFetch < 1000) {
|
||||
return this.#cachedGitStatus;
|
||||
}
|
||||
@@ -343,6 +361,8 @@ export class StatusLineComponent implements Component {
|
||||
}
|
||||
|
||||
#lookupPr(): { number: number; url: string } | null {
|
||||
if (!this.#gitEnabled()) return null;
|
||||
|
||||
const branch = this.#getCurrentBranch();
|
||||
const currentContext = branch ? createPrCacheContext(branch, this.#cachedBranchRepoId ?? null) : null;
|
||||
|
||||
@@ -550,6 +570,8 @@ export class StatusLineComponent implements Component {
|
||||
width: number,
|
||||
segmentOptions: StatusLineSettings["segmentOptions"],
|
||||
includeContext: boolean,
|
||||
includeGit: boolean,
|
||||
includePr: boolean,
|
||||
): SegmentContext {
|
||||
const state = this.session.state;
|
||||
|
||||
@@ -587,6 +609,10 @@ export class StatusLineComponent implements Component {
|
||||
contextPercent = collabState.contextUsage.percent ?? contextPercent;
|
||||
}
|
||||
|
||||
const gitBranch = includeGit || includePr ? this.#getCurrentBranch() : null;
|
||||
const gitStatus = includeGit ? this.#getGitStatus() : null;
|
||||
const gitPr = includePr ? this.#lookupPr() : null;
|
||||
|
||||
return {
|
||||
session: this.session,
|
||||
focusedAgentId: this.#focusedAgentId,
|
||||
@@ -603,9 +629,9 @@ export class StatusLineComponent implements Component {
|
||||
subagentCount: this.#subagentCount,
|
||||
sessionStartTime: this.#sessionStartTime,
|
||||
git: {
|
||||
branch: this.#getCurrentBranch(),
|
||||
status: this.#getGitStatus(),
|
||||
pr: this.#lookupPr(),
|
||||
branch: gitBranch,
|
||||
status: gitStatus,
|
||||
pr: gitPr,
|
||||
},
|
||||
usage: this.#cachedUsage,
|
||||
};
|
||||
@@ -656,7 +682,19 @@ export class StatusLineComponent implements Component {
|
||||
const effectiveSettings = this.#resolveSettings();
|
||||
const includeContext =
|
||||
hasContextSegment(effectiveSettings.leftSegments) || hasContextSegment(effectiveSettings.rightSegments);
|
||||
const ctx = this.#buildSegmentContext(width, effectiveSettings.segmentOptions, includeContext);
|
||||
const gitEnabled = this.#gitEnabled();
|
||||
const includeGit =
|
||||
gitEnabled &&
|
||||
(hasGitSegment(effectiveSettings.leftSegments) || hasGitSegment(effectiveSettings.rightSegments));
|
||||
const includePr =
|
||||
gitEnabled && (hasPrSegment(effectiveSettings.leftSegments) || hasPrSegment(effectiveSettings.rightSegments));
|
||||
const ctx = this.#buildSegmentContext(
|
||||
width,
|
||||
effectiveSettings.segmentOptions,
|
||||
includeContext,
|
||||
includeGit,
|
||||
includePr,
|
||||
);
|
||||
const separatorDef = getSeparator(effectiveSettings.separator ?? "powerline-thin", theme);
|
||||
|
||||
// `transparent` reuses the empty-string sentinel (`\x1b[49m`) so the bar
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { afterEach, beforeEach, describe, expect, it } from "bun:test";
|
||||
import { afterEach, beforeEach, describe, expect, it, spyOn } from "bun:test";
|
||||
import * as fs from "node:fs";
|
||||
import * as os from "node:os";
|
||||
import * as path from "node:path";
|
||||
@@ -7,6 +7,7 @@ import { Settings } from "@oh-my-pi/pi-coding-agent/config/settings";
|
||||
import { StatusLineComponent, type StatusLineSettings } from "@oh-my-pi/pi-coding-agent/modes/components/status-line";
|
||||
import { STATUS_LINE_PRESETS } from "@oh-my-pi/pi-coding-agent/modes/components/status-line/presets";
|
||||
import { initTheme } from "@oh-my-pi/pi-coding-agent/modes/theme/theme";
|
||||
import * as git from "@oh-my-pi/pi-coding-agent/utils/git";
|
||||
import { setProjectDir } from "@oh-my-pi/pi-utils";
|
||||
import { beginSettingsTest, restoreSettingsTestState, type SettingsTestState } from "./helpers/settings-test-state";
|
||||
|
||||
@@ -196,4 +197,54 @@ describe("StatusLineComponent effective settings cache", () => {
|
||||
expect(nextEffective).not.toBe(effective);
|
||||
expect(component.getEffectiveSettingsForTest()).toBe(nextEffective);
|
||||
});
|
||||
it("skips git probes when git integration is disabled", async () => {
|
||||
const headSpy = spyOn(git.head, "resolveSync").mockReturnValue(null);
|
||||
const statusSpy = spyOn(git.status, "summary").mockResolvedValue({ staged: 0, unstaged: 0, untracked: 0 });
|
||||
const repoSpy = spyOn(git.repo, "resolveSync").mockReturnValue(null);
|
||||
try {
|
||||
Settings.instance.override("git.enabled", false);
|
||||
const component = makeComponent({
|
||||
preset: "custom",
|
||||
leftSegments: ["git", "pr"],
|
||||
rightSegments: [],
|
||||
sessionAccent: false,
|
||||
});
|
||||
|
||||
component.watchBranch(() => {
|
||||
throw new Error("git watcher should not fire while git integration is disabled");
|
||||
});
|
||||
component.getTopBorder(100);
|
||||
await Promise.resolve();
|
||||
|
||||
expect(repoSpy).not.toHaveBeenCalled();
|
||||
expect(headSpy).not.toHaveBeenCalled();
|
||||
expect(statusSpy).not.toHaveBeenCalled();
|
||||
} finally {
|
||||
repoSpy.mockRestore();
|
||||
statusSpy.mockRestore();
|
||||
headSpy.mockRestore();
|
||||
}
|
||||
});
|
||||
|
||||
it("skips git probes when no git-backed segment is visible", async () => {
|
||||
const headSpy = spyOn(git.head, "resolveSync").mockReturnValue(null);
|
||||
const statusSpy = spyOn(git.status, "summary").mockResolvedValue({ staged: 0, unstaged: 0, untracked: 0 });
|
||||
try {
|
||||
const component = makeComponent({
|
||||
preset: "custom",
|
||||
leftSegments: ["pi"],
|
||||
rightSegments: ["session_name"],
|
||||
sessionAccent: false,
|
||||
});
|
||||
|
||||
component.getTopBorder(100);
|
||||
await Promise.resolve();
|
||||
|
||||
expect(headSpy).not.toHaveBeenCalled();
|
||||
expect(statusSpy).not.toHaveBeenCalled();
|
||||
} finally {
|
||||
statusSpy.mockRestore();
|
||||
headSpy.mockRestore();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user