Merge remote-tracking branch 'origin/main' into dev

This commit is contained in:
David Brailovsky 2026-08-14 09:33:00 +02:00
commit fd389abc46
17 changed files with 244 additions and 58 deletions

View file

@ -222,7 +222,7 @@ export function createFindToolDefinition(
}
// Default implementation uses fd.
const fdPath = await ensureTool("fd", true);
const fdPath = await ensureTool("fd");
if (signal?.aborted) {
settle(() => reject(new Error("Operation aborted")));
return;

View file

@ -174,7 +174,7 @@ export function createGrepToolDefinition(
(async () => {
try {
const rgPath = await ensureTool("rg", true);
const rgPath = await ensureTool("rg");
if (!rgPath) {
settle(() => reject(new Error("ripgrep (rg) is not available and could not be downloaded")));
return;

View file

@ -107,7 +107,7 @@ import { openBrowser } from "../../utils/open-browser.ts";
import { getCwdRelativePath } from "../../utils/paths.ts";
import { getPiUserAgent } from "../../utils/pi-user-agent.ts";
import { killTrackedDetachedChildren } from "../../utils/shell.ts";
import { ensureTool } from "../../utils/tools-manager.ts";
import { ensureTool, type ToolStatus } from "../../utils/tools-manager.ts";
import { checkForNewPiVersion, type LatestPiRelease } from "../../utils/version-check.ts";
import { ArminComponent } from "./components/armin.ts";
import { AssistantMessageComponent } from "./components/assistant-message.ts";
@ -436,6 +436,7 @@ export class InteractiveMode {
// Status line tracking (for mutating immediately-sequential status updates)
private lastStatusSpacer: Spacer | undefined = undefined;
private lastStatusText: Text | undefined = undefined;
private managedToolStatusStarted = false;
// Streaming message tracking
private streamingComponent: AssistantMessageComponent | undefined = undefined;
@ -851,11 +852,6 @@ export class InteractiveMode {
// Load changelog (only show new entries, skip for resumed sessions)
this.changelogMarkdown = this.getChangelogForDisplay();
// Ensure fd and rg are available (downloads if missing, adds to PATH via getBinDir)
// Both are needed: fd for autocomplete, rg for grep tool and bash commands
const [fdPath] = await Promise.all([ensureTool("fd"), ensureTool("rg")]);
this.fdPath = fdPath;
if (this.session.scopedModels.length > 0 && (this.options.verbose || !this.settingsManager.getQuietStartup())) {
const modelList = this.session.scopedModels
.map((sm) => {
@ -901,11 +897,12 @@ export class InteractiveMode {
this.widgetContainerBelow,
this.footerContainer,
]);
// Accept text while startup completes, but only enable interrupt, exit, and submission feedback.
this.defaultEditor.onAction("app.clear", () => this.handleCtrlC());
this.defaultEditor.onCtrlD = () => this.handleCtrlD();
this.defaultEditor.onSubmit = (text) => this.handleStartupSubmit(text);
this.ui.setFocus(this.editor);
this.setupKeyHandlers();
this.setupEditorSubmitHandler();
// Start the UI before initializing extensions so session_start handlers can use interactive dialogs
this.ui.start();
this.isInitialized = true;
@ -974,6 +971,20 @@ export class InteractiveMode {
}
this.ui.requestRender();
// Ensure fd and rg are available after mounting the TUI (downloads if missing, adds to PATH via getBinDir)
// so slow downloads do not make startup appear frozen.
// Both are needed: fd for autocomplete, rg for grep tool and bash commands.
const [fdPath] = await Promise.all([
ensureTool("fd", (status) => this.showManagedToolStatus(status)),
ensureTool("rg", (status) => this.showManagedToolStatus(status)),
]);
this.fdPath = fdPath;
// Enable the remaining input handlers only after managed-tool setup completes.
this.setupKeyHandlers();
this.setupEditorSubmitHandler();
this.ui.requestRender();
// Initialize extensions first so resources are shown before messages
await this.rebindCurrentSession();
@ -2873,6 +2884,11 @@ export class InteractiveMode {
}
}
private handleStartupSubmit(text: string): void {
this.editor.setText(text);
this.showStatus("Startup is still in progress");
}
private setupEditorSubmitHandler(): void {
this.defaultEditor.onSubmit = async (text: string) => {
text = text.trim();
@ -3408,6 +3424,20 @@ export class InteractiveMode {
return textBlocks.map((c) => (c as { text: string }).text).join("");
}
/** Show a managed-tool status update in the chat. */
private showManagedToolStatus(status: ToolStatus): void {
if (!this.managedToolStatusStarted) {
this.chatContainer.addChild(new Spacer(1));
this.managedToolStatusStarted = true;
}
const message = status.type === "warning" ? `Warning: ${status.message}` : status.message;
const color = status.type === "warning" ? "warning" : "dim";
this.chatContainer.addChild(new Text(theme.fg(color, message), 1, 0));
this.lastStatusSpacer = undefined;
this.lastStatusText = undefined;
this.ui.requestRender();
}
/**
* Show a status message in the chat.
*

View file

@ -1,4 +1,3 @@
import chalk from "chalk";
import { type SpawnSyncReturns, spawnSync } from "child_process";
import { chmodSync, createWriteStream, existsSync, mkdirSync, readdirSync, renameSync, rmSync } from "fs";
import { arch, platform } from "os";
@ -323,9 +322,20 @@ const TERMUX_PACKAGES: Record<string, string> = {
rg: "ripgrep",
};
// Ensure a tool is available, downloading if necessary
// Returns the path to the tool, or null if unavailable
export async function ensureTool(tool: "fd" | "rg", silent: boolean = false): Promise<string | undefined> {
export interface ToolStatus {
type: "info" | "warning";
message: string;
}
/**
* Ensure a tool is available, downloading if necessary.
* Reports progress through `onStatus`; status messages are otherwise silent.
* Returns the tool path, or undefined if unavailable.
*/
export async function ensureTool(
tool: "fd" | "rg",
onStatus?: (status: ToolStatus) => void,
): Promise<string | undefined> {
const existingPath = getToolPath(tool);
if (existingPath) {
return existingPath;
@ -335,9 +345,7 @@ export async function ensureTool(tool: "fd" | "rg", silent: boolean = false): Pr
if (!config) return undefined;
if (isOfflineModeEnabled()) {
if (!silent) {
console.log(chalk.yellow(`${config.name} not found. Offline mode enabled, skipping download.`));
}
onStatus?.({ type: "warning", message: `${config.name} not found. Offline mode enabled, skipping download.` });
return undefined;
}
@ -345,27 +353,22 @@ export async function ensureTool(tool: "fd" | "rg", silent: boolean = false): Pr
// Users must install via pkg.
if (platform() === "android") {
const pkgName = TERMUX_PACKAGES[tool] ?? tool;
if (!silent) {
console.log(chalk.yellow(`${config.name} not found. Install with: pkg install ${pkgName}`));
}
onStatus?.({ type: "warning", message: `${config.name} not found. Install with: pkg install ${pkgName}` });
return undefined;
}
// Tool not found - download it
if (!silent) {
console.log(chalk.dim(`${config.name} not found. Downloading...`));
}
onStatus?.({ type: "info", message: `${config.name} not found. Downloading...` });
try {
const path = await downloadTool(tool);
if (!silent) {
console.log(chalk.dim(`${config.name} installed to ${path}`));
}
onStatus?.({ type: "info", message: `${config.name} installed to ${path}` });
return path;
} catch (e) {
if (!silent) {
console.log(chalk.yellow(`Failed to download ${config.name}: ${e instanceof Error ? e.message : e}`));
}
onStatus?.({
type: "warning",
message: `Failed to download ${config.name}: ${e instanceof Error ? e.message : e}`,
});
return undefined;
}
}