From 38f18be44727e669eb0a6e2eb8edb51b0232d83c Mon Sep 17 00:00:00 2001 From: Mario Zechner Date: Mon, 8 Jun 2026 10:08:01 +0200 Subject: [PATCH] fix(coding-agent): persist implicit project trust on reload --- packages/coding-agent/CHANGELOG.md | 1 + .../coding-agent/src/core/trust-manager.ts | 6 ++- packages/coding-agent/src/main.ts | 6 ++- .../src/modes/interactive/interactive-mode.ts | 39 ++++++++++++++++++- .../coding-agent/test/trust-manager.test.ts | 4 +- 5 files changed, 51 insertions(+), 5 deletions(-) diff --git a/packages/coding-agent/CHANGELOG.md b/packages/coding-agent/CHANGELOG.md index d3328fce..afd84aa9 100644 --- a/packages/coding-agent/CHANGELOG.md +++ b/packages/coding-agent/CHANGELOG.md @@ -11,6 +11,7 @@ ### Fixed +- Fixed `/reload` to persist project trust when an implicitly trusted session creates a project `.pi` directory. - Fixed the compaction summarization system prompt to use neutral AI assistant wording for non-coding agents ([#5401](https://github.com/earendil-works/pi/issues/5401)). - Fixed `models.json` schema support for OpenAI Responses `compat.supportsDeveloperRole` ([#5456](https://github.com/earendil-works/pi/issues/5456)). - Fixed tmux setup documentation to require tmux 3.5 for `extended-keys-format csi-u` and document the tmux 3.2-3.4 fallback ([#5432](https://github.com/earendil-works/pi/issues/5432)). diff --git a/packages/coding-agent/src/core/trust-manager.ts b/packages/coding-agent/src/core/trust-manager.ts index e8e59323..c86c8518 100644 --- a/packages/coding-agent/src/core/trust-manager.ts +++ b/packages/coding-agent/src/core/trust-manager.ts @@ -94,9 +94,13 @@ function withTrustFileLock(path: string, fn: () => T): T { } } +export function hasProjectConfigDir(cwd: string): boolean { + return existsSync(join(canonicalizePath(resolvePath(cwd)), CONFIG_DIR_NAME)); +} + export function hasProjectTrustInputs(cwd: string): boolean { let currentDir = canonicalizePath(resolvePath(cwd)); - if (existsSync(join(currentDir, CONFIG_DIR_NAME))) { + if (hasProjectConfigDir(currentDir)) { return true; } diff --git a/packages/coding-agent/src/main.ts b/packages/coding-agent/src/main.ts index a3fbb719..99846761 100644 --- a/packages/coding-agent/src/main.ts +++ b/packages/coding-agent/src/main.ts @@ -647,9 +647,12 @@ export async function main(args: string[], options?: MainOptions) { time("createSessionManager"); const trustStore = new ProjectTrustStore(agentDir); + const sessionCwd = sessionManager.getCwd(); + const autoTrustOnReloadCwd = + parsed.projectTrustOverride === undefined && !hasProjectTrustInputs(sessionCwd) ? sessionCwd : undefined; const trustPromptMode: AppMode = parsed.help || parsed.listModels !== undefined ? "print" : appMode; const projectTrustedForSession = await resolveProjectTrusted({ - cwd: sessionManager.getCwd(), + cwd: sessionCwd, trustStore, trustOverride: parsed.projectTrustOverride, appMode: trustPromptMode, @@ -827,6 +830,7 @@ export async function main(args: string[], options?: MainOptions) { const interactiveMode = new InteractiveMode(runtime, { migratedProviders, modelFallbackMessage, + autoTrustOnReloadCwd, initialMessage, initialImages, initialMessages: parsed.messages, diff --git a/packages/coding-agent/src/modes/interactive/interactive-mode.ts b/packages/coding-agent/src/modes/interactive/interactive-mode.ts index 5e0ecaf9..c0e22282 100644 --- a/packages/coding-agent/src/modes/interactive/interactive-mode.ts +++ b/packages/coding-agent/src/modes/interactive/interactive-mode.ts @@ -85,7 +85,7 @@ import { BUILTIN_SLASH_COMMANDS } from "../../core/slash-commands.ts"; import type { SourceInfo } from "../../core/source-info.ts"; import { isInstallTelemetryEnabled } from "../../core/telemetry.ts"; import type { TruncationResult } from "../../core/tools/truncate.ts"; -import { hasProjectTrustInputs, ProjectTrustStore } from "../../core/trust-manager.ts"; +import { hasProjectConfigDir, hasProjectTrustInputs, ProjectTrustStore } from "../../core/trust-manager.ts"; import { getChangelogPath, getNewEntries, parseChangelog } from "../../utils/changelog.ts"; import { copyToClipboard } from "../../utils/clipboard.ts"; import { extensionForImageMimeType, readClipboardImage } from "../../utils/clipboard-image.ts"; @@ -249,6 +249,8 @@ export interface InteractiveModeOptions { migratedProviders?: string[]; /** Warning message if session model couldn't be restored */ modelFallbackMessage?: string; + /** Cwd to trust after reload if it gained a .pi directory during this implicitly trusted session. */ + autoTrustOnReloadCwd?: string; /** Initial message to send on startup (can include @file content) */ initialMessage?: string; /** Images to attach to the initial message */ @@ -367,6 +369,7 @@ export class InteractiveMode { private customHeader: (Component & { dispose?(): void }) | undefined = undefined; private options: InteractiveModeOptions; + private autoTrustOnReloadCwd: string | undefined; // Convenience accessors private get session(): AgentSession { @@ -385,6 +388,7 @@ export class InteractiveMode { constructor(runtimeHost: AgentSessionRuntime, options: InteractiveModeOptions = {}) { this.runtimeHost = runtimeHost; this.options = options; + this.autoTrustOnReloadCwd = options.autoTrustOnReloadCwd; this.runtimeHost.setBeforeSessionInvalidate(() => { this.resetExtensionUI(); }); @@ -4163,6 +4167,32 @@ export class InteractiveMode { } } + private maybeSaveImplicitProjectTrustAfterReload(): boolean { + const cwd = this.sessionManager.getCwd(); + if (this.autoTrustOnReloadCwd !== cwd) { + return false; + } + if (!this.settingsManager.isProjectTrusted() || !hasProjectConfigDir(cwd)) { + return false; + } + + const trustStore = new ProjectTrustStore(this.runtimeHost.services.agentDir); + try { + if (trustStore.get(cwd) !== null) { + this.autoTrustOnReloadCwd = undefined; + return false; + } + trustStore.set(cwd, true); + this.autoTrustOnReloadCwd = undefined; + return true; + } catch (error) { + this.showWarning( + `Could not save project trust after reload: ${error instanceof Error ? error.message : String(error)}`, + ); + return false; + } + } + private showTrustSelector(): void { const cwd = this.sessionManager.getCwd(); const trustStore = new ProjectTrustStore(this.runtimeHost.services.agentDir); @@ -5047,11 +5077,16 @@ export class InteractiveMode { force: false, showDiagnosticsWhenQuiet: true, }); + const savedImplicitProjectTrust = this.maybeSaveImplicitProjectTrustAfterReload(); const modelsJsonError = this.session.modelRegistry.getError(); if (modelsJsonError) { this.showError(`models.json error: ${modelsJsonError}`); } - this.showStatus("Reloaded keybindings, extensions, skills, prompts, themes"); + this.showStatus( + savedImplicitProjectTrust + ? "Reloaded keybindings, extensions, skills, prompts, themes; saved project trust" + : "Reloaded keybindings, extensions, skills, prompts, themes", + ); } catch (error) { dismissReloadBox(previousEditor as Component); this.showError(`Reload failed: ${error instanceof Error ? error.message : String(error)}`); diff --git a/packages/coding-agent/test/trust-manager.test.ts b/packages/coding-agent/test/trust-manager.test.ts index 3bb01634..d91dde49 100644 --- a/packages/coding-agent/test/trust-manager.test.ts +++ b/packages/coding-agent/test/trust-manager.test.ts @@ -2,7 +2,7 @@ import { mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs"; import { tmpdir } from "node:os"; import { join } from "node:path"; import { afterEach, beforeEach, describe, expect, it } from "vitest"; -import { hasProjectTrustInputs, ProjectTrustStore } from "../src/core/trust-manager.ts"; +import { hasProjectConfigDir, hasProjectTrustInputs, ProjectTrustStore } from "../src/core/trust-manager.ts"; describe("ProjectTrustStore", () => { let tempDir: string; @@ -44,9 +44,11 @@ describe("ProjectTrustStore", () => { }); it("detects project trust inputs", () => { + expect(hasProjectConfigDir(cwd)).toBe(false); expect(hasProjectTrustInputs(cwd)).toBe(false); mkdirSync(join(cwd, ".pi"), { recursive: true }); + expect(hasProjectConfigDir(cwd)).toBe(true); expect(hasProjectTrustInputs(cwd)).toBe(true); rmSync(join(cwd, ".pi"), { recursive: true, force: true });