From 914fc99b4cc7ac067c4c8036ed3917f0517ffccc Mon Sep 17 00:00:00 2001
From: SaladDay
Date: Mon, 3 Aug 2026 03:23:57 +0000
Subject: [PATCH] feat(pi): expose explicit prompt library reconciliation
---
src-tauri/src/commands/prompt.rs | 14 +-
src-tauri/src/lib.rs | 2 +
src-tauri/src/services/prompt.rs | 164 ++++++++++++++++++
src/components/prompts/PromptPanel.tsx | 42 ++++-
src/hooks/usePromptActions.ts | 27 ++-
src/i18n/locales/en.json | 6 +
src/i18n/locales/zh.json | 6 +
src/lib/api/index.ts | 2 +-
src/lib/api/prompts.ts | 15 ++
.../PromptPanel.piReconciliation.test.tsx | 67 +++++++
10 files changed, 341 insertions(+), 4 deletions(-)
create mode 100644 tests/components/PromptPanel.piReconciliation.test.tsx
diff --git a/src-tauri/src/commands/prompt.rs b/src-tauri/src/commands/prompt.rs
index 38cd13d65..8f0ebcdc9 100644
--- a/src-tauri/src/commands/prompt.rs
+++ b/src-tauri/src/commands/prompt.rs
@@ -9,7 +9,7 @@ use crate::services::pi_prompt_files::{
PiPromptFileKind, PiPromptFileService, PiPromptFileSnapshot, PiPromptTemplate,
PiPromptTemplateService,
};
-use crate::services::PromptService;
+use crate::services::prompt::{PiPromptLibraryStatus, PromptService};
use crate::store::AppState;
#[tauri::command]
@@ -67,6 +67,18 @@ pub async fn get_current_prompt_file_content(app: String) -> Result
)}
+ {appId === "pi" && piLibraryStatus?.needsReconciliation && (
+
+
+
+
+
+ {t("pi.prompts.libraryDriftTitle")}
+
+
+ {t(
+ piLibraryStatus.nativeExists
+ ? "pi.prompts.libraryDriftNative"
+ : "pi.prompts.libraryDriftMissing",
+ )}
+
+
+
+
+
+ )}
{loading ? (
{t("prompts.loading")}
diff --git a/src/hooks/usePromptActions.ts b/src/hooks/usePromptActions.ts
index 538d6ee40..6fd533a95 100644
--- a/src/hooks/usePromptActions.ts
+++ b/src/hooks/usePromptActions.ts
@@ -1,7 +1,12 @@
import { useState, useCallback } from "react";
import { useTranslation } from "react-i18next";
import { toast } from "sonner";
-import { promptsApi, type Prompt, type AppId } from "@/lib/api";
+import {
+ promptsApi,
+ type Prompt,
+ type AppId,
+ type PiPromptLibraryStatus,
+} from "@/lib/api";
export function usePromptActions(appId: AppId) {
const { t } = useTranslation();
@@ -10,12 +15,19 @@ export function usePromptActions(appId: AppId) {
const [currentFileContent, setCurrentFileContent] = useState
(
null,
);
+ const [piLibraryStatus, setPiLibraryStatus] =
+ useState(null);
const reload = useCallback(async () => {
setLoading(true);
try {
const data = await promptsApi.getPrompts(appId);
setPrompts(data);
+ if (appId === "pi") {
+ setPiLibraryStatus(await promptsApi.getPiPromptLibraryStatus());
+ } else {
+ setPiLibraryStatus(null);
+ }
// 同时加载当前文件内容
try {
@@ -138,15 +150,28 @@ export function usePromptActions(appId: AppId) {
}
}, [appId, reload, t]);
+ const reconcilePiLibrary = useCallback(async () => {
+ try {
+ await promptsApi.reconcilePiPromptLibrary();
+ await reload();
+ toast.success(t("pi.prompts.libraryReconciled"), { closeButton: true });
+ } catch (error) {
+ toast.error(t("pi.prompts.libraryReconcileFailed"));
+ throw error;
+ }
+ }, [reload, t]);
+
return {
prompts,
loading,
currentFileContent,
+ piLibraryStatus,
reload,
savePrompt,
deletePrompt,
enablePrompt,
toggleEnabled,
importFromFile,
+ reconcilePiLibrary,
};
}
diff --git a/src/i18n/locales/en.json b/src/i18n/locales/en.json
index 9f7db782b..0ad5d827d 100644
--- a/src/i18n/locales/en.json
+++ b/src/i18n/locales/en.json
@@ -962,6 +962,12 @@
"nativeDescription": "File presence is activation. SYSTEM.md replaces Pi's system prompt, APPEND_SYSTEM.md appends to it, and /template-name expands files from the prompts directory.",
"agentsLibrary": "AGENTS.md library",
"agentsLibraryDescription": "The enabled library entry is projected to AGENTS.md. External edits are detected and must be imported before switching.",
+ "libraryDriftTitle": "AGENTS.md and the library differ",
+ "libraryDriftNative": "Pi is using native AGENTS.md content that is not the library's saved selection. Reconcile to import and select the exact live content.",
+ "libraryDriftMissing": "AGENTS.md is absent, so Pi is not using a global prompt even though the library still has an enabled selection.",
+ "reconcileLibrary": "Reconcile",
+ "libraryReconciled": "AGENTS.md library reconciled with native Pi state",
+ "libraryReconcileFailed": "Failed to reconcile the AGENTS.md library",
"systemOverride": "System override",
"systemOverrideDescription": "SYSTEM.md completely replaces Pi's built-in system prompt while the file exists.",
"systemAppend": "System append",
diff --git a/src/i18n/locales/zh.json b/src/i18n/locales/zh.json
index 533cd7a0a..7950a2cc9 100644
--- a/src/i18n/locales/zh.json
+++ b/src/i18n/locales/zh.json
@@ -962,6 +962,12 @@
"nativeDescription": "文件存在即启用。SYSTEM.md 替换 Pi 系统提示,APPEND_SYSTEM.md 追加系统提示,/模板名 会展开 prompts 目录中的文件。",
"agentsLibrary": "AGENTS.md 提示库",
"agentsLibraryDescription": "已启用的提示库条目会投影到 AGENTS.md;检测到外部修改时,必须先导入才能切换。",
+ "libraryDriftTitle": "AGENTS.md 与提示库不一致",
+ "libraryDriftNative": "Pi 正在使用的原生 AGENTS.md 内容不是提示库中保存的选中项。协调后会导入并选中完全一致的实时内容。",
+ "libraryDriftMissing": "AGENTS.md 不存在,因此 Pi 当前没有使用全局提示词,但提示库仍保留了启用项。",
+ "reconcileLibrary": "协调",
+ "libraryReconciled": "AGENTS.md 提示库已与 Pi 原生状态协调",
+ "libraryReconcileFailed": "协调 AGENTS.md 提示库失败",
"systemOverride": "系统提示替换",
"systemOverrideDescription": "SYSTEM.md 存在期间会完整替换 Pi 内置系统提示。",
"systemAppend": "系统提示追加",
diff --git a/src/lib/api/index.ts b/src/lib/api/index.ts
index 09f8e5fb9..9156ac143 100644
--- a/src/lib/api/index.ts
+++ b/src/lib/api/index.ts
@@ -23,7 +23,7 @@ export * as configApi from "./config";
export * as authApi from "./auth";
export * as copilotApi from "./copilot";
export type { ProviderSwitchEvent } from "./providers";
-export type { Prompt } from "./prompts";
+export type { PiPromptLibraryStatus, Prompt } from "./prompts";
export type { Profile, ProfilePayload, ProfilesResponse } from "./profiles";
export type {
CopilotDeviceCodeResponse,
diff --git a/src/lib/api/prompts.ts b/src/lib/api/prompts.ts
index cbc3797ce..04a10c121 100644
--- a/src/lib/api/prompts.ts
+++ b/src/lib/api/prompts.ts
@@ -30,6 +30,13 @@ export interface PiPromptTemplate {
revision: string;
}
+export interface PiPromptLibraryStatus {
+ nativeExists: boolean;
+ nativeRevision: string;
+ matchedPromptId: string | null;
+ needsReconciliation: boolean;
+}
+
export const promptsApi = {
async getPrompts(app: AppId): Promise> {
return await invoke("get_prompts", { app });
@@ -55,6 +62,14 @@ export const promptsApi = {
return await invoke("get_current_prompt_file_content", { app });
},
+ async getPiPromptLibraryStatus(): Promise {
+ return await invoke("get_pi_prompt_library_status");
+ },
+
+ async reconcilePiPromptLibrary(): Promise {
+ return await invoke("reconcile_pi_prompt_library");
+ },
+
async getPiPromptFile(kind: PiPromptFileKind): Promise {
return await invoke("get_pi_prompt_file", { kind });
},
diff --git a/tests/components/PromptPanel.piReconciliation.test.tsx b/tests/components/PromptPanel.piReconciliation.test.tsx
new file mode 100644
index 000000000..d0d5b9b64
--- /dev/null
+++ b/tests/components/PromptPanel.piReconciliation.test.tsx
@@ -0,0 +1,67 @@
+import { fireEvent, render, screen } from "@testing-library/react";
+import { beforeEach, describe, expect, it, vi } from "vitest";
+import PromptPanel from "@/components/prompts/PromptPanel";
+
+const mocks = vi.hoisted(() => ({
+ reconcilePiLibrary: vi.fn(),
+ reload: vi.fn(),
+}));
+
+vi.mock("react-i18next", () => ({
+ useTranslation: () => ({
+ t: (key: string) => key,
+ }),
+}));
+
+vi.mock("@/hooks/useTauriEvent", () => ({
+ useTauriEvent: vi.fn(),
+}));
+
+vi.mock("@/components/prompts/PiNativePromptResources", () => ({
+ PiNativePromptResources: () => native resources
,
+}));
+
+vi.mock("@/hooks/usePromptActions", () => ({
+ usePromptActions: () => ({
+ prompts: {},
+ loading: false,
+ currentFileContent: "external native content",
+ piLibraryStatus: {
+ nativeExists: true,
+ nativeRevision: "native-revision",
+ matchedPromptId: null,
+ needsReconciliation: true,
+ },
+ reload: mocks.reload,
+ savePrompt: vi.fn(),
+ deletePrompt: vi.fn(),
+ enablePrompt: vi.fn(),
+ toggleEnabled: vi.fn(),
+ importFromFile: vi.fn(),
+ reconcilePiLibrary: mocks.reconcilePiLibrary,
+ }),
+}));
+
+describe("PromptPanel Pi native reconciliation", () => {
+ beforeEach(() => {
+ mocks.reconcilePiLibrary.mockReset();
+ mocks.reconcilePiLibrary.mockResolvedValue(undefined);
+ mocks.reload.mockReset();
+ });
+
+ it("shows native drift and exposes the explicit reconciliation action", () => {
+ render();
+
+ expect(
+ screen.getByText("pi.prompts.libraryDriftTitle"),
+ ).toBeInTheDocument();
+ expect(
+ screen.getByText("pi.prompts.libraryDriftNative"),
+ ).toBeInTheDocument();
+
+ fireEvent.click(
+ screen.getByRole("button", { name: "pi.prompts.reconcileLibrary" }),
+ );
+ expect(mocks.reconcilePiLibrary).toHaveBeenCalledTimes(1);
+ });
+});