feat(pi): customize compact prompt layout #40
@@ -81,4 +81,5 @@ The domain model (Host, Module, Skeleton, Auto-loader, Enable convention, overla
|
|||||||
- Flake-managed Pi extension, prompt, and skill directories may still be written directly for throwaway development or local experiments.
|
- Flake-managed Pi extension, prompt, and skill directories may still be written directly for throwaway development or local experiments.
|
||||||
The risk is that a later Home Manager activation can overwrite or hide those unmanaged files, so finished work must be promoted into the dotfiles module before it counts as deployed.
|
The risk is that a later Home Manager activation can overwrite or hide those unmanaged files, so finished work must be promoted into the dotfiles module before it counts as deployed.
|
||||||
- Pi's tool discovery checks `~/.pi/agent/bin` before `PATH`, and downloaded generic Linux binaries there can be unusable on NixOS with the stub-ld error.
|
- Pi's tool discovery checks `~/.pi/agent/bin` before `PATH`, and downloaded generic Linux binaries there can be unusable on NixOS with the stub-ld error.
|
||||||
A copied or patched Pi launcher that only prepends Nix `fd`/`rg` to `PATH` may still break `@` autocomplete unless the local tool path is removed or Pi validates the local binary before using it.
|
This flake patches Pi to validate local tool binaries before selecting them, so it falls back to usable `fd`/`rg` from `PATH` instead.
|
||||||
|
Stale unpatched launchers are the remaining failure mode for broken `@` autocomplete.
|
||||||
|
|||||||
254
modules/agents/pi/extensions/compact-status.ts
Normal file
254
modules/agents/pi/extensions/compact-status.ts
Normal file
@@ -0,0 +1,254 @@
|
|||||||
|
import { execFileSync } from "node:child_process";
|
||||||
|
import { existsSync, readFileSync } from "node:fs";
|
||||||
|
import { homedir } from "node:os";
|
||||||
|
import { basename, join } from "node:path";
|
||||||
|
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
||||||
|
import { truncateToWidth, visibleWidth } from "@earendil-works/pi-tui";
|
||||||
|
|
||||||
|
type QuotaState =
|
||||||
|
| { status: "idle" | "loading" }
|
||||||
|
| { status: "ok"; detail: string; refreshedAt: number; weeklyRemaining?: number; shortRemaining?: number }
|
||||||
|
| { status: "missing" | "error"; detail: string; refreshedAt?: number };
|
||||||
|
|
||||||
|
const CODEX_USAGE_ENDPOINTS = [
|
||||||
|
"https://chatgpt.com/backend-api/wham/usage",
|
||||||
|
"https://chatgpt.com/backend-api/codex/usage",
|
||||||
|
];
|
||||||
|
const QUOTA_REFRESH_MS = 5 * 60 * 1000;
|
||||||
|
const REQUEST_TIMEOUT_MS = 10_000;
|
||||||
|
|
||||||
|
let quotaState: QuotaState = { status: "idle" };
|
||||||
|
let quotaRefreshPromise: Promise<void> | null = null;
|
||||||
|
|
||||||
|
function shortCwd(cwd: string): string {
|
||||||
|
const home = process.env.HOME;
|
||||||
|
if (home && cwd.startsWith(`${home}/`)) return `~/${basename(cwd)}`;
|
||||||
|
return basename(cwd) || cwd;
|
||||||
|
}
|
||||||
|
|
||||||
|
function gitBranch(cwd: string): string | null {
|
||||||
|
try {
|
||||||
|
const out = execFileSync("git", ["--no-optional-locks", "symbolic-ref", "--quiet", "--short", "HEAD"], {
|
||||||
|
cwd,
|
||||||
|
encoding: "utf8",
|
||||||
|
stdio: ["ignore", "pipe", "ignore"],
|
||||||
|
}).trim();
|
||||||
|
return out || null;
|
||||||
|
} catch {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function authPath(): string {
|
||||||
|
return join(process.env.PI_CODING_AGENT_DIR ?? join(homedir(), ".pi", "agent"), "auth.json");
|
||||||
|
}
|
||||||
|
|
||||||
|
function readCodexCredentials(): { access: string; accountId?: string } | null {
|
||||||
|
const file = authPath();
|
||||||
|
if (!existsSync(file)) return null;
|
||||||
|
try {
|
||||||
|
const auth = JSON.parse(readFileSync(file, "utf8"));
|
||||||
|
const credential = auth?.["openai-codex"];
|
||||||
|
if (credential?.type !== "oauth" || typeof credential.access !== "string") return null;
|
||||||
|
if (typeof credential.expires === "number" && credential.expires <= Date.now() + 30_000) return null;
|
||||||
|
return {
|
||||||
|
access: credential.access,
|
||||||
|
accountId: typeof credential.accountId === "string" ? credential.accountId : undefined,
|
||||||
|
};
|
||||||
|
} catch {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function numberValue(value: unknown): number | undefined {
|
||||||
|
if (typeof value === "number" && Number.isFinite(value)) return value;
|
||||||
|
if (typeof value === "string" && value.trim() !== "") {
|
||||||
|
const parsed = Number(value);
|
||||||
|
if (Number.isFinite(parsed)) return parsed;
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
function objectValue(value: unknown): Record<string, unknown> | undefined {
|
||||||
|
return value && typeof value === "object" && !Array.isArray(value) ? (value as Record<string, unknown>) : undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
function windowSeconds(raw: Record<string, unknown>): number | undefined {
|
||||||
|
const seconds = numberValue(raw.limit_window_seconds ?? raw.windowSeconds);
|
||||||
|
if (seconds !== undefined) return seconds;
|
||||||
|
const mins = numberValue(raw.windowDurationMins ?? raw.window_duration_mins);
|
||||||
|
return mins === undefined ? undefined : mins * 60;
|
||||||
|
}
|
||||||
|
|
||||||
|
function usedPercent(raw: Record<string, unknown>): number | undefined {
|
||||||
|
const value = numberValue(raw.used_percent ?? raw.usedPercent);
|
||||||
|
if (value === undefined) return undefined;
|
||||||
|
return Math.max(0, Math.min(100, value));
|
||||||
|
}
|
||||||
|
|
||||||
|
function collectWindows(raw: unknown, out: Array<{ seconds?: number; used: number; key: string }> = [], key = "root") {
|
||||||
|
if (Array.isArray(raw)) {
|
||||||
|
raw.forEach((item, index) => collectWindows(item, out, `${key}.${index}`));
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
const obj = objectValue(raw);
|
||||||
|
if (!obj) return out;
|
||||||
|
const used = usedPercent(obj);
|
||||||
|
if (used !== undefined) out.push({ seconds: windowSeconds(obj), used, key });
|
||||||
|
for (const [childKey, value] of Object.entries(obj)) {
|
||||||
|
if (value && typeof value === "object") collectWindows(value, out, `${key}.${childKey}`);
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
function pickQuotaWindows(raw: unknown): { weeklyRemaining?: number; shortRemaining?: number } | null {
|
||||||
|
const windows = collectWindows(raw);
|
||||||
|
if (windows.length === 0) return null;
|
||||||
|
const weekly = windows.find((window) => window.seconds !== undefined && Math.abs(window.seconds - 604_800) <= 60 * 60)
|
||||||
|
?? windows.find((window) => /week|weekly|secondary/i.test(window.key));
|
||||||
|
const short = windows.find((window) => window.seconds !== undefined && Math.abs(window.seconds - 18_000) <= 60 * 30)
|
||||||
|
?? windows.find((window) => /five|session|primary|short/i.test(window.key));
|
||||||
|
return {
|
||||||
|
weeklyRemaining: weekly ? Math.max(0, Math.min(100, 100 - weekly.used)) : undefined,
|
||||||
|
shortRemaining: short ? Math.max(0, Math.min(100, 100 - short.used)) : undefined,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function safeFg(theme: any, color: string, text: string): string {
|
||||||
|
try {
|
||||||
|
return theme.fg(color, text);
|
||||||
|
} catch {
|
||||||
|
return theme.fg("accent", text);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function contextColor(percent: number): string {
|
||||||
|
if (percent >= 90) return "error";
|
||||||
|
if (percent >= 70) return "warning";
|
||||||
|
return "success";
|
||||||
|
}
|
||||||
|
|
||||||
|
function quotaColor(percent: number): string {
|
||||||
|
if (percent >= 80) return "error";
|
||||||
|
if (percent >= 50) return "warning";
|
||||||
|
return "border";
|
||||||
|
}
|
||||||
|
|
||||||
|
function bar(theme: any, width: number, percent: number | null, glyph: string, colorForPercent: (percent: number) => string): string {
|
||||||
|
const barWidth = Math.max(12, width);
|
||||||
|
if (percent === null) return theme.fg("muted", glyph.repeat(barWidth));
|
||||||
|
const clamped = Math.max(0, Math.min(100, percent));
|
||||||
|
const filled = Math.max(0, Math.min(barWidth, Math.round((clamped / 100) * barWidth)));
|
||||||
|
const empty = Math.max(0, barWidth - filled);
|
||||||
|
return safeFg(theme, colorForPercent(clamped), glyph.repeat(filled)) + theme.fg("dim", glyph.repeat(empty));
|
||||||
|
}
|
||||||
|
|
||||||
|
async function fetchCodexQuota(force = false): Promise<void> {
|
||||||
|
const fresh = quotaState.status === "ok" && Date.now() - quotaState.refreshedAt < QUOTA_REFRESH_MS;
|
||||||
|
if (!force && fresh) return;
|
||||||
|
if (quotaRefreshPromise) return quotaRefreshPromise;
|
||||||
|
|
||||||
|
quotaState = { status: "loading" };
|
||||||
|
quotaRefreshPromise = (async () => {
|
||||||
|
const credentials = readCodexCredentials();
|
||||||
|
if (!credentials) {
|
||||||
|
quotaState = { status: "missing", detail: "OpenAI Codex OAuth credentials were not found or are expired" };
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let lastError = "quota unavailable";
|
||||||
|
for (const endpoint of CODEX_USAGE_ENDPOINTS) {
|
||||||
|
const controller = new AbortController();
|
||||||
|
const timeout = setTimeout(() => controller.abort(), REQUEST_TIMEOUT_MS);
|
||||||
|
try {
|
||||||
|
const headers: Record<string, string> = { Authorization: `Bearer ${credentials.access}` };
|
||||||
|
if (credentials.accountId) headers["ChatGPT-Account-Id"] = credentials.accountId;
|
||||||
|
const response = await fetch(endpoint, { headers, signal: controller.signal });
|
||||||
|
if (!response.ok) {
|
||||||
|
lastError = `${response.status} ${response.statusText}`;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const windows = pickQuotaWindows(await response.json());
|
||||||
|
if (!windows || (windows.weeklyRemaining === undefined && windows.shortRemaining === undefined)) {
|
||||||
|
lastError = "response had no recognized quota windows";
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const details = [];
|
||||||
|
if (windows.weeklyRemaining !== undefined) details.push(`weekly ${Math.round(windows.weeklyRemaining)}%`);
|
||||||
|
if (windows.shortRemaining !== undefined) details.push(`short ${Math.round(windows.shortRemaining)}%`);
|
||||||
|
quotaState = {
|
||||||
|
status: "ok",
|
||||||
|
detail: `Codex quota remaining: ${details.join(", ")}`,
|
||||||
|
weeklyRemaining: windows.weeklyRemaining,
|
||||||
|
shortRemaining: windows.shortRemaining,
|
||||||
|
refreshedAt: Date.now(),
|
||||||
|
};
|
||||||
|
return;
|
||||||
|
} catch (error) {
|
||||||
|
lastError = error instanceof Error ? error.message : String(error);
|
||||||
|
} finally {
|
||||||
|
clearTimeout(timeout);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
quotaState = { status: "error", detail: `Codex quota failed: ${lastError}`, refreshedAt: Date.now() };
|
||||||
|
})().finally(() => {
|
||||||
|
quotaRefreshPromise = null;
|
||||||
|
});
|
||||||
|
return quotaRefreshPromise;
|
||||||
|
}
|
||||||
|
|
||||||
|
function statusLines(ctx: any, theme: any, width: number): string[] {
|
||||||
|
const cwd = ctx.sessionManager?.getCwd?.() ?? ctx.cwd ?? process.cwd();
|
||||||
|
const branch = gitBranch(cwd);
|
||||||
|
const where = branch ? ` ${shortCwd(cwd)} ${branch}` : ` ${shortCwd(cwd)}`;
|
||||||
|
const model = ctx.model?.id ?? process.env.PI_MODEL ?? "no-model";
|
||||||
|
const thinking = ctx.thinkingLevel ?? process.env.PI_REASONING_LEVEL ?? "off";
|
||||||
|
const left = theme.fg("accent", where);
|
||||||
|
const right = theme.fg("dim", `${model} • ${thinking}`);
|
||||||
|
const pad = " ".repeat(Math.max(1, width - visibleWidth(left) - visibleWidth(right)));
|
||||||
|
const contextPercentRaw = ctx.getContextUsage?.()?.percent;
|
||||||
|
const contextPercent = typeof contextPercentRaw === "number" && Number.isFinite(contextPercentRaw) ? contextPercentRaw : null;
|
||||||
|
const quotaConsumed = quotaState.status === "ok" && quotaState.weeklyRemaining !== undefined
|
||||||
|
? 100 - quotaState.weeklyRemaining
|
||||||
|
: null;
|
||||||
|
return [
|
||||||
|
truncateToWidth(left + pad + right, width),
|
||||||
|
bar(theme, width, contextPercent, "▃", contextColor),
|
||||||
|
bar(theme, width, quotaConsumed, "▔", quotaColor),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
function setCompactStatusUi(ctx: any) {
|
||||||
|
if (!ctx.hasUI) return;
|
||||||
|
ctx.ui.setWidget("compact-status", (_tui: any, theme: any) => ({
|
||||||
|
invalidate() {},
|
||||||
|
render(width: number) {
|
||||||
|
return statusLines(ctx, theme, width);
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
ctx.ui.setFooter(() => ({ invalidate() {}, render: () => [] }));
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function compactStatus(pi: ExtensionAPI) {
|
||||||
|
function refreshUi(ctx: any) {
|
||||||
|
setCompactStatusUi(ctx);
|
||||||
|
}
|
||||||
|
|
||||||
|
pi.on("session_start", (_event, ctx) => {
|
||||||
|
refreshUi(ctx);
|
||||||
|
void fetchCodexQuota(false).then(() => refreshUi(ctx));
|
||||||
|
});
|
||||||
|
pi.on("model_select", (_event, ctx) => refreshUi(ctx));
|
||||||
|
pi.on("agent_settled", (_event, ctx) => refreshUi(ctx));
|
||||||
|
|
||||||
|
pi.registerCommand("codex-quota", {
|
||||||
|
description: "Refresh and show ChatGPT Codex quota",
|
||||||
|
handler: async (_args, ctx) => {
|
||||||
|
refreshUi(ctx);
|
||||||
|
await fetchCodexQuota(true);
|
||||||
|
refreshUi(ctx);
|
||||||
|
const level = quotaState.status === "ok" ? "info" : quotaState.status === "missing" ? "warning" : "error";
|
||||||
|
ctx.ui.notify(quotaState.status === "idle" || quotaState.status === "loading" ? "Codex quota refresh in progress" : quotaState.detail, level);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
169
modules/agents/pi/patches/pi-flex-spacer.patch
Normal file
169
modules/agents/pi/patches/pi-flex-spacer.patch
Normal file
@@ -0,0 +1,169 @@
|
|||||||
|
diff --git a/packages/tui/src/tui.ts b/packages/tui/src/tui.ts
|
||||||
|
--- a/packages/tui/src/tui.ts 2026-08-02 00:16:00.000000000 -0400
|
||||||
|
+++ b/packages/tui/src/tui.ts 2026-08-02 00:16:00.000000000 -0400
|
||||||
|
@@ -310,7 +310,7 @@ export class TUI extends Container {
|
||||||
|
private cursorRow = 0; // Logical cursor row (end of rendered content)
|
||||||
|
private hardwareCursorRow = 0; // Actual terminal cursor row (may differ due to IME positioning)
|
||||||
|
private showHardwareCursor = process.env.PI_HARDWARE_CURSOR === "1";
|
||||||
|
- private clearOnShrink = process.env.PI_CLEAR_ON_SHRINK === "1"; // Clear empty rows when content shrinks (default: off)
|
||||||
|
+ private clearOnShrink = process.env.PI_CLEAR_ON_SHRINK !== "0"; // Clear empty rows when content shrinks (default: on)
|
||||||
|
private maxLinesRendered = 0; // Track terminal's working area (max lines ever rendered)
|
||||||
|
private previousViewportTop = 0; // Track previous viewport top for resize-aware cursor moves
|
||||||
|
private fullRedrawCount = 0;
|
||||||
|
|
||||||
|
diff --git a/packages/coding-agent/src/core/settings-manager.ts b/packages/coding-agent/src/core/settings-manager.ts
|
||||||
|
--- a/packages/coding-agent/src/core/settings-manager.ts 2026-08-02 00:34:00.000000000 -0400
|
||||||
|
+++ b/packages/coding-agent/src/core/settings-manager.ts 2026-08-02 00:34:00.000000000 -0400
|
||||||
|
@@ -1093,11 +1093,11 @@ export class SettingsManager {
|
||||||
|
}
|
||||||
|
|
||||||
|
getClearOnShrink(): boolean {
|
||||||
|
- // Settings takes precedence, then env var, then default false
|
||||||
|
+ // Settings takes precedence, then env var, then default true
|
||||||
|
if (this.settings.terminal?.clearOnShrink !== undefined) {
|
||||||
|
return this.settings.terminal.clearOnShrink;
|
||||||
|
}
|
||||||
|
- return process.env.PI_CLEAR_ON_SHRINK === "1";
|
||||||
|
+ return process.env.PI_CLEAR_ON_SHRINK !== "0";
|
||||||
|
}
|
||||||
|
|
||||||
|
setClearOnShrink(enabled: boolean): void {
|
||||||
|
|
||||||
|
diff --git a/packages/coding-agent/src/modes/interactive/interactive-mode.ts b/packages/coding-agent/src/modes/interactive/interactive-mode.ts
|
||||||
|
--- a/packages/coding-agent/src/modes/interactive/interactive-mode.ts 2026-08-01 18:41:36.963495957 -0400
|
||||||
|
+++ b/packages/coding-agent/src/modes/interactive/interactive-mode.ts 2026-08-01 18:43:04.876341236 -0400
|
||||||
|
@@ -210,6 +210,45 @@
|
||||||
|
return code !== undefined && DEAD_TERMINAL_ERROR_CODES.has(code);
|
||||||
|
}
|
||||||
|
|
||||||
|
+class FlexSpacerBottomLayout implements Component {
|
||||||
|
+ private readonly ui: TUI;
|
||||||
|
+ private readonly flowChildren: Component[];
|
||||||
|
+ private readonly pinnedChildren: Component[];
|
||||||
|
+
|
||||||
|
+ constructor(ui: TUI, flowChildren: Component[], pinnedChildren: Component[]) {
|
||||||
|
+ this.ui = ui;
|
||||||
|
+ this.flowChildren = flowChildren;
|
||||||
|
+ this.pinnedChildren = pinnedChildren;
|
||||||
|
+ }
|
||||||
|
+
|
||||||
|
+ invalidate(): void {
|
||||||
|
+ for (const child of [...this.flowChildren, ...this.pinnedChildren]) {
|
||||||
|
+ child.invalidate();
|
||||||
|
+ }
|
||||||
|
+ }
|
||||||
|
+
|
||||||
|
+ private renderGroup(children: Component[], width: number): string[] {
|
||||||
|
+ const lines: string[] = [];
|
||||||
|
+ for (const child of children) {
|
||||||
|
+ lines.push(...child.render(width));
|
||||||
|
+ }
|
||||||
|
+ return lines;
|
||||||
|
+ }
|
||||||
|
+
|
||||||
|
+ render(width: number): string[] {
|
||||||
|
+ const flowLines = this.renderGroup(this.flowChildren, width);
|
||||||
|
+ const pinnedLines = this.renderGroup(this.pinnedChildren, width);
|
||||||
|
+ const terminalRows = this.ui.terminal.rows;
|
||||||
|
+ const spacerRows = Math.max(0, terminalRows - flowLines.length - pinnedLines.length);
|
||||||
|
+
|
||||||
|
+ return [
|
||||||
|
+ ...flowLines,
|
||||||
|
+ ...Array.from({ length: spacerRows }, () => ""),
|
||||||
|
+ ...pinnedLines,
|
||||||
|
+ ];
|
||||||
|
+ }
|
||||||
|
+}
|
||||||
|
+
|
||||||
|
const ANTHROPIC_SUBSCRIPTION_AUTH_WARNING =
|
||||||
|
"Anthropic subscription auth is active. Third-party harness usage draws from extra usage and is billed per token, not your Claude plan limits. Manage extra usage at https://claude.ai/settings/usage. Disable this warning in /settings.";
|
||||||
|
|
||||||
|
@@ -335,6 +374,7 @@
|
||||||
|
private fdPath: string | undefined;
|
||||||
|
private editorContainer: Container;
|
||||||
|
private footer: FooterComponent;
|
||||||
|
+ private footerContainer: Container;
|
||||||
|
private footerDataProvider: FooterDataProvider;
|
||||||
|
// Stored so the same manager can be injected into custom editors, selectors, and extension UI.
|
||||||
|
private keybindings: KeybindingsManager;
|
||||||
|
@@ -477,7 +517,9 @@
|
||||||
|
this.editorContainer = new Container();
|
||||||
|
this.editorContainer.addChild(this.editor as Component);
|
||||||
|
this.footerDataProvider = new FooterDataProvider(this.sessionManager.getCwd());
|
||||||
|
+ this.footerContainer = new Container();
|
||||||
|
this.footer = new FooterComponent(this.session, this.footerDataProvider);
|
||||||
|
+ this.footerContainer.addChild(this.footer);
|
||||||
|
this.footer.setAutoCompactEnabled(this.session.autoCompactionEnabled);
|
||||||
|
|
||||||
|
// Load hide thinking block setting
|
||||||
|
@@ -704,19 +746,25 @@
|
||||||
|
console.log(theme.fg("dim", `Model scope: ${modelList}${cycleHint}`));
|
||||||
|
}
|
||||||
|
|
||||||
|
- // Add header container as first child. Populate it after applying theme settings.
|
||||||
|
- // Keep loaded resources before chat so restored session messages never precede them.
|
||||||
|
- this.ui.addChild(this.headerContainer);
|
||||||
|
- this.ui.addChild(this.loadedResourcesContainer);
|
||||||
|
-
|
||||||
|
- this.ui.addChild(this.chatContainer);
|
||||||
|
- this.ui.addChild(this.pendingMessagesContainer);
|
||||||
|
- this.ui.addChild(this.statusContainer);
|
||||||
|
this.renderWidgets(); // Initialize with default spacer
|
||||||
|
- this.ui.addChild(this.widgetContainerAbove);
|
||||||
|
- this.ui.addChild(this.editorContainer);
|
||||||
|
- this.ui.addChild(this.widgetContainerBelow);
|
||||||
|
- this.ui.addChild(this.footer);
|
||||||
|
+ this.ui.addChild(
|
||||||
|
+ new FlexSpacerBottomLayout(
|
||||||
|
+ this.ui,
|
||||||
|
+ [
|
||||||
|
+ this.headerContainer,
|
||||||
|
+ this.loadedResourcesContainer,
|
||||||
|
+ this.chatContainer,
|
||||||
|
+ ],
|
||||||
|
+ [
|
||||||
|
+ this.pendingMessagesContainer,
|
||||||
|
+ this.statusContainer,
|
||||||
|
+ this.widgetContainerAbove,
|
||||||
|
+ this.editorContainer,
|
||||||
|
+ this.widgetContainerBelow,
|
||||||
|
+ this.footerContainer,
|
||||||
|
+ ],
|
||||||
|
+ ),
|
||||||
|
+ );
|
||||||
|
this.ui.setFocus(this.editor);
|
||||||
|
|
||||||
|
this.setupKeyHandlers();
|
||||||
|
@@ -2033,25 +2081,25 @@
|
||||||
|
| ((tui: TUI, thm: Theme, footerData: ReadonlyFooterDataProvider) => Component & { dispose?(): void })
|
||||||
|
| undefined,
|
||||||
|
): void {
|
||||||
|
- // Dispose existing custom footer
|
||||||
|
+ // Dispose existing custom footer
|
||||||
|
if (this.customFooter?.dispose) {
|
||||||
|
this.customFooter.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
- // Remove current footer from UI
|
||||||
|
+ // Remove current footer from its pinned layout slot.
|
||||||
|
if (this.customFooter) {
|
||||||
|
- this.ui.removeChild(this.customFooter);
|
||||||
|
+ this.footerContainer.removeChild(this.customFooter);
|
||||||
|
} else {
|
||||||
|
- this.ui.removeChild(this.footer);
|
||||||
|
+ this.footerContainer.removeChild(this.footer);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (factory) {
|
||||||
|
// Create and add custom footer, passing the data provider
|
||||||
|
this.customFooter = factory(this.ui, theme, this.footerDataProvider);
|
||||||
|
- this.ui.addChild(this.customFooter);
|
||||||
|
+ this.footerContainer.addChild(this.customFooter);
|
||||||
|
} else {
|
||||||
|
// Restore built-in footer
|
||||||
|
this.customFooter = undefined;
|
||||||
|
- this.ui.addChild(this.footer);
|
||||||
|
+ this.footerContainer.addChild(this.footer);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.ui.requestRender();
|
||||||
22
modules/agents/pi/patches/pi-tool-lookup-validation.patch
Normal file
22
modules/agents/pi/patches/pi-tool-lookup-validation.patch
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
diff --git a/packages/coding-agent/src/utils/tools-manager.ts b/packages/coding-agent/src/utils/tools-manager.ts
|
||||||
|
--- a/packages/coding-agent/src/utils/tools-manager.ts 2026-08-01 18:41:36.970496010 -0400
|
||||||
|
+++ b/packages/coding-agent/src/utils/tools-manager.ts 2026-08-01 18:41:37.028186009 -0400
|
||||||
|
@@ -74,8 +74,7 @@
|
||||||
|
function commandExists(cmd: string): boolean {
|
||||||
|
try {
|
||||||
|
const result = spawnSync(cmd, ["--version"], { stdio: "pipe" });
|
||||||
|
- // Check for ENOENT error (command not found)
|
||||||
|
- return result.error === undefined || result.error === null;
|
||||||
|
+ return (result.error === undefined || result.error === null) && result.status === 0;
|
||||||
|
} catch {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
@@ -88,7 +87,7 @@
|
||||||
|
|
||||||
|
// Check our tools directory first
|
||||||
|
const localPath = join(TOOLS_DIR, config.binaryName + (platform() === "win32" ? ".exe" : ""));
|
||||||
|
- if (existsSync(localPath)) {
|
||||||
|
+ if (existsSync(localPath) && commandExists(localPath)) {
|
||||||
|
return localPath;
|
||||||
|
}
|
||||||
|
|
||||||
@@ -11,6 +11,12 @@ let
|
|||||||
cfg = config.modules.agents.pi;
|
cfg = config.modules.agents.pi;
|
||||||
user = config.user.name;
|
user = config.user.name;
|
||||||
piDir = "${config.users.users.${user}.home}/.pi/agent";
|
piDir = "${config.users.users.${user}.home}/.pi/agent";
|
||||||
|
patchedPi = pkgs.pi-coding-agent.overrideAttrs (old: {
|
||||||
|
patches = (old.patches or [ ]) ++ [
|
||||||
|
./patches/pi-flex-spacer.patch
|
||||||
|
./patches/pi-tool-lookup-validation.patch
|
||||||
|
];
|
||||||
|
});
|
||||||
herdrPiIntegration = pkgs.stdenvNoCC.mkDerivation {
|
herdrPiIntegration = pkgs.stdenvNoCC.mkDerivation {
|
||||||
name = "herdr-pi-integration";
|
name = "herdr-pi-integration";
|
||||||
nativeBuildInputs = [ pkgs.herdr ];
|
nativeBuildInputs = [ pkgs.herdr ];
|
||||||
@@ -40,6 +46,7 @@ in
|
|||||||
home-manager.users.${user} = {
|
home-manager.users.${user} = {
|
||||||
programs.pi-coding-agent = {
|
programs.pi-coding-agent = {
|
||||||
enable = true;
|
enable = true;
|
||||||
|
package = patchedPi;
|
||||||
|
|
||||||
settings = {
|
settings = {
|
||||||
defaultProvider = "openai-codex";
|
defaultProvider = "openai-codex";
|
||||||
|
|||||||
Reference in New Issue
Block a user