fix(coding-agent): persist implicit project trust on reload
This commit is contained in:
@@ -11,6 +11,7 @@
|
|||||||
|
|
||||||
### Fixed
|
### 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 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 `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)).
|
- 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)).
|
||||||
|
|||||||
@@ -94,9 +94,13 @@ function withTrustFileLock<T>(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 {
|
export function hasProjectTrustInputs(cwd: string): boolean {
|
||||||
let currentDir = canonicalizePath(resolvePath(cwd));
|
let currentDir = canonicalizePath(resolvePath(cwd));
|
||||||
if (existsSync(join(currentDir, CONFIG_DIR_NAME))) {
|
if (hasProjectConfigDir(currentDir)) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -647,9 +647,12 @@ export async function main(args: string[], options?: MainOptions) {
|
|||||||
time("createSessionManager");
|
time("createSessionManager");
|
||||||
|
|
||||||
const trustStore = new ProjectTrustStore(agentDir);
|
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 trustPromptMode: AppMode = parsed.help || parsed.listModels !== undefined ? "print" : appMode;
|
||||||
const projectTrustedForSession = await resolveProjectTrusted({
|
const projectTrustedForSession = await resolveProjectTrusted({
|
||||||
cwd: sessionManager.getCwd(),
|
cwd: sessionCwd,
|
||||||
trustStore,
|
trustStore,
|
||||||
trustOverride: parsed.projectTrustOverride,
|
trustOverride: parsed.projectTrustOverride,
|
||||||
appMode: trustPromptMode,
|
appMode: trustPromptMode,
|
||||||
@@ -827,6 +830,7 @@ export async function main(args: string[], options?: MainOptions) {
|
|||||||
const interactiveMode = new InteractiveMode(runtime, {
|
const interactiveMode = new InteractiveMode(runtime, {
|
||||||
migratedProviders,
|
migratedProviders,
|
||||||
modelFallbackMessage,
|
modelFallbackMessage,
|
||||||
|
autoTrustOnReloadCwd,
|
||||||
initialMessage,
|
initialMessage,
|
||||||
initialImages,
|
initialImages,
|
||||||
initialMessages: parsed.messages,
|
initialMessages: parsed.messages,
|
||||||
|
|||||||
@@ -85,7 +85,7 @@ import { BUILTIN_SLASH_COMMANDS } from "../../core/slash-commands.ts";
|
|||||||
import type { SourceInfo } from "../../core/source-info.ts";
|
import type { SourceInfo } from "../../core/source-info.ts";
|
||||||
import { isInstallTelemetryEnabled } from "../../core/telemetry.ts";
|
import { isInstallTelemetryEnabled } from "../../core/telemetry.ts";
|
||||||
import type { TruncationResult } from "../../core/tools/truncate.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 { getChangelogPath, getNewEntries, parseChangelog } from "../../utils/changelog.ts";
|
||||||
import { copyToClipboard } from "../../utils/clipboard.ts";
|
import { copyToClipboard } from "../../utils/clipboard.ts";
|
||||||
import { extensionForImageMimeType, readClipboardImage } from "../../utils/clipboard-image.ts";
|
import { extensionForImageMimeType, readClipboardImage } from "../../utils/clipboard-image.ts";
|
||||||
@@ -249,6 +249,8 @@ export interface InteractiveModeOptions {
|
|||||||
migratedProviders?: string[];
|
migratedProviders?: string[];
|
||||||
/** Warning message if session model couldn't be restored */
|
/** Warning message if session model couldn't be restored */
|
||||||
modelFallbackMessage?: string;
|
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) */
|
/** Initial message to send on startup (can include @file content) */
|
||||||
initialMessage?: string;
|
initialMessage?: string;
|
||||||
/** Images to attach to the initial message */
|
/** Images to attach to the initial message */
|
||||||
@@ -367,6 +369,7 @@ export class InteractiveMode {
|
|||||||
private customHeader: (Component & { dispose?(): void }) | undefined = undefined;
|
private customHeader: (Component & { dispose?(): void }) | undefined = undefined;
|
||||||
|
|
||||||
private options: InteractiveModeOptions;
|
private options: InteractiveModeOptions;
|
||||||
|
private autoTrustOnReloadCwd: string | undefined;
|
||||||
|
|
||||||
// Convenience accessors
|
// Convenience accessors
|
||||||
private get session(): AgentSession {
|
private get session(): AgentSession {
|
||||||
@@ -385,6 +388,7 @@ export class InteractiveMode {
|
|||||||
constructor(runtimeHost: AgentSessionRuntime, options: InteractiveModeOptions = {}) {
|
constructor(runtimeHost: AgentSessionRuntime, options: InteractiveModeOptions = {}) {
|
||||||
this.runtimeHost = runtimeHost;
|
this.runtimeHost = runtimeHost;
|
||||||
this.options = options;
|
this.options = options;
|
||||||
|
this.autoTrustOnReloadCwd = options.autoTrustOnReloadCwd;
|
||||||
this.runtimeHost.setBeforeSessionInvalidate(() => {
|
this.runtimeHost.setBeforeSessionInvalidate(() => {
|
||||||
this.resetExtensionUI();
|
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 {
|
private showTrustSelector(): void {
|
||||||
const cwd = this.sessionManager.getCwd();
|
const cwd = this.sessionManager.getCwd();
|
||||||
const trustStore = new ProjectTrustStore(this.runtimeHost.services.agentDir);
|
const trustStore = new ProjectTrustStore(this.runtimeHost.services.agentDir);
|
||||||
@@ -5047,11 +5077,16 @@ export class InteractiveMode {
|
|||||||
force: false,
|
force: false,
|
||||||
showDiagnosticsWhenQuiet: true,
|
showDiagnosticsWhenQuiet: true,
|
||||||
});
|
});
|
||||||
|
const savedImplicitProjectTrust = this.maybeSaveImplicitProjectTrustAfterReload();
|
||||||
const modelsJsonError = this.session.modelRegistry.getError();
|
const modelsJsonError = this.session.modelRegistry.getError();
|
||||||
if (modelsJsonError) {
|
if (modelsJsonError) {
|
||||||
this.showError(`models.json error: ${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) {
|
} catch (error) {
|
||||||
dismissReloadBox(previousEditor as Component);
|
dismissReloadBox(previousEditor as Component);
|
||||||
this.showError(`Reload failed: ${error instanceof Error ? error.message : String(error)}`);
|
this.showError(`Reload failed: ${error instanceof Error ? error.message : String(error)}`);
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|||||||
import { tmpdir } from "node:os";
|
import { tmpdir } from "node:os";
|
||||||
import { join } from "node:path";
|
import { join } from "node:path";
|
||||||
import { afterEach, beforeEach, describe, expect, it } from "vitest";
|
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", () => {
|
describe("ProjectTrustStore", () => {
|
||||||
let tempDir: string;
|
let tempDir: string;
|
||||||
@@ -44,9 +44,11 @@ describe("ProjectTrustStore", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("detects project trust inputs", () => {
|
it("detects project trust inputs", () => {
|
||||||
|
expect(hasProjectConfigDir(cwd)).toBe(false);
|
||||||
expect(hasProjectTrustInputs(cwd)).toBe(false);
|
expect(hasProjectTrustInputs(cwd)).toBe(false);
|
||||||
|
|
||||||
mkdirSync(join(cwd, ".pi"), { recursive: true });
|
mkdirSync(join(cwd, ".pi"), { recursive: true });
|
||||||
|
expect(hasProjectConfigDir(cwd)).toBe(true);
|
||||||
expect(hasProjectTrustInputs(cwd)).toBe(true);
|
expect(hasProjectTrustInputs(cwd)).toBe(true);
|
||||||
rmSync(join(cwd, ".pi"), { recursive: true, force: true });
|
rmSync(join(cwd, ".pi"), { recursive: true, force: true });
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user