mirror of
https://github.com/getcompanion-ai/co-mono.git
synced 2026-04-15 22:03:45 +00:00
Merge pull request #600 from nicobailon/feature/footer-data-provider
feat(coding-agent): add FooterDataProvider for git branch and extension statuses
This commit is contained in:
commit
22b2a18952
7 changed files with 220 additions and 223 deletions
|
|
@ -22,6 +22,7 @@ import type { BashResult } from "../bash-executor.js";
|
|||
import type { CompactionPreparation, CompactionResult } from "../compaction/index.js";
|
||||
import type { EventBus } from "../event-bus.js";
|
||||
import type { ExecOptions, ExecResult } from "../exec.js";
|
||||
import type { ReadonlyFooterDataProvider } from "../footer-data-provider.js";
|
||||
import type { KeybindingsManager } from "../keybindings.js";
|
||||
import type { CustomMessage } from "../messages.js";
|
||||
import type { ModelRegistry } from "../model-registry.js";
|
||||
|
|
@ -82,8 +83,17 @@ export interface ExtensionUIContext {
|
|||
setWidget(key: string, content: string[] | undefined): void;
|
||||
setWidget(key: string, content: ((tui: TUI, theme: Theme) => Component & { dispose?(): void }) | undefined): void;
|
||||
|
||||
/** Set a custom footer component, or undefined to restore the built-in footer. */
|
||||
setFooter(factory: ((tui: TUI, theme: Theme) => Component & { dispose?(): void }) | undefined): void;
|
||||
/** Set a custom footer component, or undefined to restore the built-in footer.
|
||||
*
|
||||
* The factory receives a FooterDataProvider for data not otherwise accessible:
|
||||
* git branch and extension statuses from setStatus(). Token stats, model info,
|
||||
* etc. are available via ctx.sessionManager and ctx.model.
|
||||
*/
|
||||
setFooter(
|
||||
factory:
|
||||
| ((tui: TUI, theme: Theme, footerData: ReadonlyFooterDataProvider) => Component & { dispose?(): void })
|
||||
| undefined,
|
||||
): void;
|
||||
|
||||
/** Set a custom header component (shown at startup, above chat), or undefined to restore the built-in header. */
|
||||
setHeader(factory: ((tui: TUI, theme: Theme) => Component & { dispose?(): void }) | undefined): void;
|
||||
|
|
|
|||
121
packages/coding-agent/src/core/footer-data-provider.ts
Normal file
121
packages/coding-agent/src/core/footer-data-provider.ts
Normal file
|
|
@ -0,0 +1,121 @@
|
|||
import { existsSync, type FSWatcher, readFileSync, statSync, watch } from "fs";
|
||||
import { dirname, join, resolve } from "path";
|
||||
|
||||
/**
|
||||
* Find the git HEAD path by walking up from cwd.
|
||||
* Handles both regular git repos (.git is a directory) and worktrees (.git is a file).
|
||||
*/
|
||||
function findGitHeadPath(): string | null {
|
||||
let dir = process.cwd();
|
||||
while (true) {
|
||||
const gitPath = join(dir, ".git");
|
||||
if (existsSync(gitPath)) {
|
||||
try {
|
||||
const stat = statSync(gitPath);
|
||||
if (stat.isFile()) {
|
||||
const content = readFileSync(gitPath, "utf8").trim();
|
||||
if (content.startsWith("gitdir: ")) {
|
||||
const gitDir = content.slice(8);
|
||||
const headPath = resolve(dir, gitDir, "HEAD");
|
||||
if (existsSync(headPath)) return headPath;
|
||||
}
|
||||
} else if (stat.isDirectory()) {
|
||||
const headPath = join(gitPath, "HEAD");
|
||||
if (existsSync(headPath)) return headPath;
|
||||
}
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
const parent = dirname(dir);
|
||||
if (parent === dir) return null;
|
||||
dir = parent;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Provides git branch and extension statuses - data not otherwise accessible to extensions.
|
||||
* Token stats, model info available via ctx.sessionManager and ctx.model.
|
||||
*/
|
||||
export class FooterDataProvider {
|
||||
private extensionStatuses = new Map<string, string>();
|
||||
private cachedBranch: string | null | undefined = undefined;
|
||||
private gitWatcher: FSWatcher | null = null;
|
||||
private branchChangeCallbacks = new Set<() => void>();
|
||||
|
||||
constructor() {
|
||||
this.setupGitWatcher();
|
||||
}
|
||||
|
||||
/** Current git branch, null if not in repo, "detached" if detached HEAD */
|
||||
getGitBranch(): string | null {
|
||||
if (this.cachedBranch !== undefined) return this.cachedBranch;
|
||||
|
||||
try {
|
||||
const gitHeadPath = findGitHeadPath();
|
||||
if (!gitHeadPath) {
|
||||
this.cachedBranch = null;
|
||||
return null;
|
||||
}
|
||||
const content = readFileSync(gitHeadPath, "utf8").trim();
|
||||
this.cachedBranch = content.startsWith("ref: refs/heads/") ? content.slice(16) : "detached";
|
||||
} catch {
|
||||
this.cachedBranch = null;
|
||||
}
|
||||
return this.cachedBranch;
|
||||
}
|
||||
|
||||
/** Extension status texts set via ctx.ui.setStatus() */
|
||||
getExtensionStatuses(): ReadonlyMap<string, string> {
|
||||
return this.extensionStatuses;
|
||||
}
|
||||
|
||||
/** Subscribe to git branch changes. Returns unsubscribe function. */
|
||||
onBranchChange(callback: () => void): () => void {
|
||||
this.branchChangeCallbacks.add(callback);
|
||||
return () => this.branchChangeCallbacks.delete(callback);
|
||||
}
|
||||
|
||||
/** Internal: set extension status */
|
||||
setExtensionStatus(key: string, text: string | undefined): void {
|
||||
if (text === undefined) {
|
||||
this.extensionStatuses.delete(key);
|
||||
} else {
|
||||
this.extensionStatuses.set(key, text);
|
||||
}
|
||||
}
|
||||
|
||||
/** Internal: cleanup */
|
||||
dispose(): void {
|
||||
if (this.gitWatcher) {
|
||||
this.gitWatcher.close();
|
||||
this.gitWatcher = null;
|
||||
}
|
||||
this.branchChangeCallbacks.clear();
|
||||
}
|
||||
|
||||
private setupGitWatcher(): void {
|
||||
if (this.gitWatcher) {
|
||||
this.gitWatcher.close();
|
||||
this.gitWatcher = null;
|
||||
}
|
||||
|
||||
const gitHeadPath = findGitHeadPath();
|
||||
if (!gitHeadPath) return;
|
||||
|
||||
try {
|
||||
this.gitWatcher = watch(gitHeadPath, () => {
|
||||
this.cachedBranch = undefined;
|
||||
for (const cb of this.branchChangeCallbacks) cb();
|
||||
});
|
||||
} catch {
|
||||
// Silently fail if we can't watch
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** Read-only view for extensions - excludes setExtensionStatus and dispose */
|
||||
export type ReadonlyFooterDataProvider = Pick<
|
||||
FooterDataProvider,
|
||||
"getGitBranch" | "getExtensionStatuses" | "onBranchChange"
|
||||
>;
|
||||
Loading…
Add table
Add a link
Reference in a new issue