@zihanw/pi-forge 0.1.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +354 -185
- package/README.zh-CN.md +453 -0
- package/examples/default-prompt-stack.json +54 -51
- package/examples/reviewer-prompt-stack.json +115 -0
- package/examples/sillytavern-dm-writer-prompt-stack.json +190 -0
- package/package.json +4 -8
- package/src/compiler.ts +588 -87
- package/src/index.ts +509 -140
- package/src/loader.ts +179 -12
- package/src/payload-capture.ts +85 -0
- package/src/policy.ts +42 -0
- package/src/regex.ts +500 -0
- package/src/sillytavern-importer.ts +484 -50
- package/src/stack-migration.ts +159 -0
- package/src/storage.ts +28 -0
- package/src/types.ts +83 -2
- package/src/web-editor/index.ts +2 -0
- package/src/web-editor/page.ts +2844 -0
- package/src/web-editor/server.ts +289 -0
- package/src/web-editor/types.ts +84 -0
- package/src/web-host.ts +229 -0
package/src/index.ts
CHANGED
|
@@ -1,34 +1,71 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { join } from "node:path";
|
|
1
|
+
import { existsSync, mkdirSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { dirname, join } from "node:path";
|
|
3
3
|
import { buildSessionContext, type BuildSystemPromptOptions, type ExtensionAPI, type ExtensionCommandContext, type ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
4
|
-
import { Type } from "typebox";
|
|
5
4
|
import {
|
|
6
5
|
compileMessages,
|
|
7
6
|
compileSystemPrompt,
|
|
8
7
|
createPromptVariableStore,
|
|
8
|
+
agentMessageToPreviewText,
|
|
9
9
|
getLatestUserMessage,
|
|
10
10
|
markSessionVariablesClean,
|
|
11
|
-
renderPreviewMessages,
|
|
12
11
|
resetTurnVariables,
|
|
13
12
|
} from "./compiler.ts";
|
|
14
|
-
import { chooseDefaultStack, isDisabledPromptStackId, loadPromptStacks,
|
|
13
|
+
import { chooseDefaultStack, isDisabledPromptStackId, loadPromptStacks, promptStackPath, promptStackReadDirs } from "./loader.ts";
|
|
14
|
+
import { createProviderPayloadCapture, estimatePayloadTokens } from "./payload-capture.ts";
|
|
15
|
+
import { applyResourcePolicy, hasResourcePolicy } from "./policy.ts";
|
|
16
|
+
import { applyFinalizeRegexRulesToMessage } from "./regex.ts";
|
|
15
17
|
import { importSillyTavernPreset } from "./sillytavern-importer.ts";
|
|
16
|
-
import
|
|
18
|
+
import { migrateLegacyPromptStacks, renderMigrationReport } from "./stack-migration.ts";
|
|
19
|
+
import type { CompileMessageSource, LoadedPromptStack, PromptStackDiagnostic, PromptVariableValue, PromptVariableStore } from "./types.ts";
|
|
20
|
+
import { createWebEditorHost, loadWebEditorSettings, type WebHostRuntime } from "./web-host.ts";
|
|
21
|
+
import { startWebEditorServer, type WebEditorPayloadCapture, type WebEditorPayloadSnapshot, type WebEditorPreview, type WebEditorPreviewSection, type WebEditorServer } from "./web-editor/index.ts";
|
|
17
22
|
|
|
18
23
|
const STATE_ENTRY_TYPE = "pi-forge-prompt-stack-state";
|
|
19
24
|
const VARIABLE_ENTRY_TYPE = "pi-forge-variable-state";
|
|
20
|
-
const
|
|
25
|
+
const WEB_EDITOR_GLOBAL_KEY = "__piForgeWebEditor";
|
|
26
|
+
|
|
27
|
+
type PayloadDisplayTarget = "editor" | "web";
|
|
28
|
+
|
|
29
|
+
interface SharedWebEditorState {
|
|
30
|
+
server?: WebEditorServer;
|
|
31
|
+
cwd?: string;
|
|
32
|
+
preferredPort?: number;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
interface SharedWebEditorRegistry {
|
|
36
|
+
byCwd: Record<string, SharedWebEditorState | undefined>;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
type PiForgeGlobal = typeof globalThis & {
|
|
40
|
+
__piForgeWebEditor?: SharedWebEditorRegistry;
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
function getSharedWebEditorRegistry(): SharedWebEditorRegistry {
|
|
44
|
+
const globalScope = globalThis as PiForgeGlobal;
|
|
45
|
+
globalScope[WEB_EDITOR_GLOBAL_KEY] ??= { byCwd: {} };
|
|
46
|
+
return globalScope[WEB_EDITOR_GLOBAL_KEY];
|
|
47
|
+
}
|
|
21
48
|
|
|
22
49
|
export default function piForge(pi: ExtensionAPI) {
|
|
50
|
+
const sharedWebEditors = getSharedWebEditorRegistry();
|
|
23
51
|
let stacks: LoadedPromptStack[] = [];
|
|
24
52
|
let active: LoadedPromptStack | undefined;
|
|
25
53
|
let currentSystemPromptOptions: BuildSystemPromptOptions | undefined;
|
|
26
54
|
let currentLatestUserMessage: string | undefined;
|
|
27
55
|
let currentVariableStore: PromptVariableStore | undefined;
|
|
28
56
|
let contextRewritePending = false;
|
|
29
|
-
let sessionVariables: Record<string,
|
|
57
|
+
let sessionVariables: Record<string, PromptVariableValue> = {};
|
|
30
58
|
let lastPersistedActiveId: string | undefined;
|
|
31
59
|
let interceptNextProviderPayload = false;
|
|
60
|
+
let interceptPayloadSavePath: string | undefined;
|
|
61
|
+
let interceptPayloadDisplayTarget: PayloadDisplayTarget = "editor";
|
|
62
|
+
let payloadCaptureArmedAt: string | undefined;
|
|
63
|
+
let latestProviderPayloadCapture: WebEditorPayloadCapture | undefined;
|
|
64
|
+
let latestCompileDiagnostics: PromptStackDiagnostic[] = [];
|
|
65
|
+
let webEditor: WebEditorServer | undefined;
|
|
66
|
+
let webEditorCwd: string | undefined;
|
|
67
|
+
let webEditorPreferredPort: number | undefined;
|
|
68
|
+
let toolPolicyBaseline: string[] | undefined;
|
|
32
69
|
|
|
33
70
|
function activeId(): string | undefined {
|
|
34
71
|
return active?.stack.id;
|
|
@@ -50,6 +87,7 @@ export default function piForge(pi: ExtensionAPI) {
|
|
|
50
87
|
active = undefined;
|
|
51
88
|
if (id) persistActiveSelection("none");
|
|
52
89
|
if (ctx) updateStatus(ctx);
|
|
90
|
+
syncActiveToolPolicy(ctx);
|
|
53
91
|
return true;
|
|
54
92
|
}
|
|
55
93
|
|
|
@@ -58,6 +96,7 @@ export default function piForge(pi: ExtensionAPI) {
|
|
|
58
96
|
active = found;
|
|
59
97
|
persistActiveSelection(found.stack.id);
|
|
60
98
|
if (ctx) updateStatus(ctx);
|
|
99
|
+
syncActiveToolPolicy(ctx);
|
|
61
100
|
return true;
|
|
62
101
|
}
|
|
63
102
|
|
|
@@ -65,6 +104,7 @@ export default function piForge(pi: ExtensionAPI) {
|
|
|
65
104
|
if (!ctx.isProjectTrusted()) {
|
|
66
105
|
stacks = [];
|
|
67
106
|
active = undefined;
|
|
107
|
+
syncActiveToolPolicy(ctx);
|
|
68
108
|
ctx.ui.notify("pi-forge: project is not trusted; prompt stacks are disabled.", "warning");
|
|
69
109
|
updateStatus(ctx);
|
|
70
110
|
return;
|
|
@@ -73,6 +113,7 @@ export default function piForge(pi: ExtensionAPI) {
|
|
|
73
113
|
stacks = loadPromptStacks(ctx.cwd);
|
|
74
114
|
active = chooseDefaultStack(stacks, preferredId);
|
|
75
115
|
updateStatus(ctx);
|
|
116
|
+
syncActiveToolPolicy(ctx);
|
|
76
117
|
}
|
|
77
118
|
|
|
78
119
|
function updateStatus(ctx: ExtensionContext): void {
|
|
@@ -80,9 +121,42 @@ export default function piForge(pi: ExtensionAPI) {
|
|
|
80
121
|
ctx.ui.setStatus("pi-forge", ctx.ui.theme.fg("accent", "stack:" + active.stack.id));
|
|
81
122
|
} else {
|
|
82
123
|
ctx.ui.setStatus("pi-forge", undefined);
|
|
124
|
+
latestCompileDiagnostics = [];
|
|
125
|
+
ctx.ui.setStatus("pi-forge-diagnostics", undefined);
|
|
83
126
|
}
|
|
84
127
|
}
|
|
85
128
|
|
|
129
|
+
function syncActiveToolPolicy(ctx?: ExtensionContext): void {
|
|
130
|
+
const policy = active?.stack.tools;
|
|
131
|
+
if (!hasResourcePolicy(policy)) {
|
|
132
|
+
restoreToolPolicy(ctx);
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
const baseline = toolPolicyBaseline ?? pi.getActiveTools();
|
|
137
|
+
toolPolicyBaseline ??= [...baseline];
|
|
138
|
+
const nextTools = applyResourcePolicy(filterKnownTools(baseline), policy);
|
|
139
|
+
pi.setActiveTools(nextTools);
|
|
140
|
+
if (ctx) {
|
|
141
|
+
const label = nextTools.length > 0 ? `tools:${nextTools.length}` : "tools:none";
|
|
142
|
+
ctx.ui.setStatus("pi-forge-tools", ctx.ui.theme.fg(nextTools.length > 0 ? "accent" : "warning", label));
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function restoreToolPolicy(ctx?: ExtensionContext): void {
|
|
147
|
+
if (toolPolicyBaseline) {
|
|
148
|
+
pi.setActiveTools(filterKnownTools(toolPolicyBaseline));
|
|
149
|
+
toolPolicyBaseline = undefined;
|
|
150
|
+
}
|
|
151
|
+
if (ctx) ctx.ui.setStatus("pi-forge-tools", undefined);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function filterKnownTools(names: string[]): string[] {
|
|
155
|
+
const known = new Set(pi.getAllTools().map((tool) => tool.name));
|
|
156
|
+
if (known.size === 0) return names;
|
|
157
|
+
return names.filter((name) => known.has(name));
|
|
158
|
+
}
|
|
159
|
+
|
|
86
160
|
function notifyActivePreset(ctx: ExtensionContext, detail: string): void {
|
|
87
161
|
if (!active) return;
|
|
88
162
|
const errorCount = active.diagnostics.filter((d) => d.level === "error").length;
|
|
@@ -91,8 +165,152 @@ export default function piForge(pi: ExtensionAPI) {
|
|
|
91
165
|
ctx.ui.notify("pi-forge: active preset " + active.stack.id + suffix + " (" + detail + ")", errorCount ? "error" : "info");
|
|
92
166
|
}
|
|
93
167
|
|
|
168
|
+
function recordCompileDiagnostics(ctx: ExtensionContext, diagnostics: PromptStackDiagnostic[]): void {
|
|
169
|
+
latestCompileDiagnostics = diagnostics;
|
|
170
|
+
const errors = diagnostics.filter((d) => d.level === "error").length;
|
|
171
|
+
const warnings = diagnostics.filter((d) => d.level === "warning").length;
|
|
172
|
+
if (errors || warnings) {
|
|
173
|
+
ctx.ui.setStatus("pi-forge-diagnostics", ctx.ui.theme.fg(errors ? "error" : "warning", `forge:${errors}e/${warnings}w`));
|
|
174
|
+
return;
|
|
175
|
+
}
|
|
176
|
+
ctx.ui.setStatus("pi-forge-diagnostics", undefined);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
function webHostRuntime(ctx: ExtensionCommandContext): WebHostRuntime {
|
|
180
|
+
return {
|
|
181
|
+
getStacks: () => stacks,
|
|
182
|
+
getActive: () => active,
|
|
183
|
+
getActiveId: activeId,
|
|
184
|
+
getSelectedActiveId: selectedActiveId,
|
|
185
|
+
setActive: (id) => setActive(id, ctx),
|
|
186
|
+
reloadStacks: (preferredId) => reloadStacks(ctx, preferredId),
|
|
187
|
+
buildPreview: (target) => buildPreview(ctx, target),
|
|
188
|
+
getPayload: () => ({ ok: true, ...webPayloadSnapshot() }),
|
|
189
|
+
armPayload: (savePath) => {
|
|
190
|
+
armPayloadIntercept(ctx, savePath, "web");
|
|
191
|
+
return { ok: true, ...webPayloadSnapshot() };
|
|
192
|
+
},
|
|
193
|
+
clearPayload: () => {
|
|
194
|
+
clearPayloadCapture(ctx);
|
|
195
|
+
return { ok: true, ...webPayloadSnapshot() };
|
|
196
|
+
},
|
|
197
|
+
};
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
function sharedWebEditorForCwd(cwd: string): SharedWebEditorState {
|
|
201
|
+
sharedWebEditors.byCwd[cwd] ??= {};
|
|
202
|
+
return sharedWebEditors.byCwd[cwd];
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
function syncWebEditorFromShared(cwd: string): void {
|
|
206
|
+
const shared = sharedWebEditorForCwd(cwd);
|
|
207
|
+
webEditor = shared.server;
|
|
208
|
+
webEditorCwd = shared.cwd;
|
|
209
|
+
webEditorPreferredPort = shared.preferredPort;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
function rememberWebEditor(server: WebEditorServer, cwd: string, preferredPort: number | undefined): void {
|
|
213
|
+
const shared = sharedWebEditorForCwd(cwd);
|
|
214
|
+
webEditor = server;
|
|
215
|
+
webEditorCwd = cwd;
|
|
216
|
+
webEditorPreferredPort = preferredPort;
|
|
217
|
+
shared.server = server;
|
|
218
|
+
shared.cwd = cwd;
|
|
219
|
+
shared.preferredPort = preferredPort;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
function clearWebEditor(server: WebEditorServer): void {
|
|
223
|
+
if (webEditor === server) {
|
|
224
|
+
webEditor = undefined;
|
|
225
|
+
webEditorCwd = undefined;
|
|
226
|
+
webEditorPreferredPort = undefined;
|
|
227
|
+
}
|
|
228
|
+
for (const [cwd, shared] of Object.entries(sharedWebEditors.byCwd)) {
|
|
229
|
+
if (shared?.server === server) delete sharedWebEditors.byCwd[cwd];
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
function refreshWebEditorHost(ctx: ExtensionContext): void {
|
|
234
|
+
syncWebEditorFromShared(ctx.cwd);
|
|
235
|
+
if (!webEditor) return;
|
|
236
|
+
const commandCtx = ctx as ExtensionCommandContext;
|
|
237
|
+
webEditor.updateHost(createWebEditorHost(commandCtx, webHostRuntime(commandCtx)));
|
|
238
|
+
rememberWebEditor(webEditor, ctx.cwd, webEditorPreferredPort);
|
|
239
|
+
ctx.ui.setStatus("pi-forge-editor", ctx.ui.theme.fg("accent", `editor:${webEditor.port}`));
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
async function openWebEditor(ctx: ExtensionCommandContext, mode: "open" | "restart" = "open"): Promise<void> {
|
|
243
|
+
syncWebEditorFromShared(ctx.cwd);
|
|
244
|
+
const settings = loadWebEditorSettings(ctx);
|
|
245
|
+
for (const warning of settings.warnings) ctx.ui.notify(warning, "warning");
|
|
246
|
+
|
|
247
|
+
if (webEditor && (mode === "restart" || webEditorPreferredPort !== settings.preferredPort)) {
|
|
248
|
+
const server = webEditor;
|
|
249
|
+
await server.close();
|
|
250
|
+
clearWebEditor(server);
|
|
251
|
+
ctx.ui.setStatus("pi-forge-editor", undefined);
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
if (!webEditor) {
|
|
255
|
+
try {
|
|
256
|
+
webEditor = await startWebEditorServer(createWebEditorHost(ctx, webHostRuntime(ctx)), { port: settings.preferredPort });
|
|
257
|
+
} catch (error) {
|
|
258
|
+
if (settings.preferredPort !== undefined) {
|
|
259
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
260
|
+
ctx.ui.notify(`pi-forge: preferred editor port 127.0.0.1:${settings.preferredPort} was unavailable (${detail}); using an available port instead.`, "warning");
|
|
261
|
+
try {
|
|
262
|
+
webEditor = await startWebEditorServer(createWebEditorHost(ctx, webHostRuntime(ctx)));
|
|
263
|
+
} catch (fallbackError) {
|
|
264
|
+
const fallbackDetail = fallbackError instanceof Error ? fallbackError.message : String(fallbackError);
|
|
265
|
+
ctx.ui.setStatus("pi-forge-editor", undefined);
|
|
266
|
+
ctx.ui.notify(`pi-forge: failed to start stack editor on an available localhost port: ${fallbackDetail}.`, "error");
|
|
267
|
+
return;
|
|
268
|
+
}
|
|
269
|
+
} else {
|
|
270
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
271
|
+
ctx.ui.setStatus("pi-forge-editor", undefined);
|
|
272
|
+
ctx.ui.notify(`pi-forge: failed to start stack editor on an available localhost port: ${detail}.`, "error");
|
|
273
|
+
return;
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
rememberWebEditor(webEditor, ctx.cwd, settings.preferredPort);
|
|
277
|
+
ctx.ui.setStatus("pi-forge-editor", ctx.ui.theme.fg("accent", `editor:${webEditor.port}`));
|
|
278
|
+
ctx.ui.notify(`pi-forge: stack editor running at ${webEditor.url}`, "info");
|
|
279
|
+
} else {
|
|
280
|
+
webEditor.updateHost(createWebEditorHost(ctx, webHostRuntime(ctx)));
|
|
281
|
+
rememberWebEditor(webEditor, ctx.cwd, settings.preferredPort);
|
|
282
|
+
ctx.ui.setStatus("pi-forge-editor", ctx.ui.theme.fg("accent", `editor:${webEditor.port}`));
|
|
283
|
+
ctx.ui.notify(`pi-forge: stack editor already running at ${webEditor.url}`, "info");
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
await showText(ctx, "pi-forge stack editor", `Open the local stack editor:\n\n${webEditor.url}\n\nServer bound to 127.0.0.1:${webEditor.port}\nOptional config: ${settings.configPath}\nProject: ${webEditorCwd}`);
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
async function stopWebEditor(ctx: ExtensionCommandContext): Promise<void> {
|
|
290
|
+
syncWebEditorFromShared(ctx.cwd);
|
|
291
|
+
if (!webEditor) {
|
|
292
|
+
ctx.ui.notify("pi-forge: stack editor is not running.", "info");
|
|
293
|
+
return;
|
|
294
|
+
}
|
|
295
|
+
const server = webEditor;
|
|
296
|
+
await server.close();
|
|
297
|
+
clearWebEditor(server);
|
|
298
|
+
ctx.ui.setStatus("pi-forge-editor", undefined);
|
|
299
|
+
ctx.ui.notify("pi-forge: stack editor stopped.", "info");
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
function getCurrentBranchEntries(ctx: ExtensionContext): unknown[] {
|
|
303
|
+
const leafId = ctx.sessionManager.getLeafId();
|
|
304
|
+
if (leafId === null) return [];
|
|
305
|
+
const sessionManager = ctx.sessionManager as {
|
|
306
|
+
getBranch?: (fromId?: string) => unknown[];
|
|
307
|
+
getEntries: () => unknown[];
|
|
308
|
+
};
|
|
309
|
+
return sessionManager.getBranch ? sessionManager.getBranch(leafId ?? undefined) : sessionManager.getEntries();
|
|
310
|
+
}
|
|
311
|
+
|
|
94
312
|
function getRestoredActiveId(ctx: ExtensionContext): string | undefined {
|
|
95
|
-
const entries = ctx
|
|
313
|
+
const entries = getCurrentBranchEntries(ctx);
|
|
96
314
|
for (let i = entries.length - 1; i >= 0; i--) {
|
|
97
315
|
const entry = entries[i] as { type?: string; customType?: string; data?: { activeStackId?: unknown } };
|
|
98
316
|
if (entry.type === "custom" && entry.customType === STATE_ENTRY_TYPE) {
|
|
@@ -102,13 +320,13 @@ export default function piForge(pi: ExtensionAPI) {
|
|
|
102
320
|
return undefined;
|
|
103
321
|
}
|
|
104
322
|
|
|
105
|
-
function getRestoredVariables(ctx: ExtensionContext): Record<string,
|
|
106
|
-
const entries = ctx
|
|
323
|
+
function getRestoredVariables(ctx: ExtensionContext): Record<string, PromptVariableValue> {
|
|
324
|
+
const entries = getCurrentBranchEntries(ctx);
|
|
107
325
|
for (let i = entries.length - 1; i >= 0; i--) {
|
|
108
326
|
const entry = entries[i] as { type?: string; customType?: string; data?: { variables?: unknown } };
|
|
109
327
|
if (entry.type !== "custom" || entry.customType !== VARIABLE_ENTRY_TYPE) continue;
|
|
110
328
|
if (!entry.data || typeof entry.data.variables !== "object" || Array.isArray(entry.data.variables)) return {};
|
|
111
|
-
return
|
|
329
|
+
return normalizeVariableRecord(entry.data.variables as Record<string, unknown>);
|
|
112
330
|
}
|
|
113
331
|
return {};
|
|
114
332
|
}
|
|
@@ -120,32 +338,52 @@ export default function piForge(pi: ExtensionAPI) {
|
|
|
120
338
|
markSessionVariablesClean(store);
|
|
121
339
|
}
|
|
122
340
|
|
|
123
|
-
function
|
|
124
|
-
const result: Record<string,
|
|
341
|
+
function normalizeVariableRecord(value: Record<string, unknown>): Record<string, PromptVariableValue> {
|
|
342
|
+
const result: Record<string, PromptVariableValue> = {};
|
|
125
343
|
for (const [key, raw] of Object.entries(value)) {
|
|
126
|
-
if (
|
|
344
|
+
if (isPromptVariableValue(raw)) result[key] = raw;
|
|
127
345
|
}
|
|
128
346
|
return result;
|
|
129
347
|
}
|
|
130
348
|
|
|
131
|
-
|
|
349
|
+
function isPromptVariableValue(value: unknown): value is PromptVariableValue {
|
|
350
|
+
if (value === null) return true;
|
|
351
|
+
const type = typeof value;
|
|
352
|
+
if (type === "string" || type === "boolean") return true;
|
|
353
|
+
if (type === "number") return Number.isFinite(value);
|
|
354
|
+
if (Array.isArray(value)) return value.every(isPromptVariableValue);
|
|
355
|
+
if (!value || typeof value !== "object") return false;
|
|
356
|
+
return Object.values(value as Record<string, unknown>).every(isPromptVariableValue);
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
function restoreBranchScopedRuntime(ctx: ExtensionContext): void {
|
|
132
360
|
sessionVariables = getRestoredVariables(ctx);
|
|
133
361
|
currentVariableStore = undefined;
|
|
134
362
|
const restoredActiveId = getRestoredActiveId(ctx);
|
|
135
363
|
lastPersistedActiveId = restoredActiveId;
|
|
136
364
|
reloadStacks(ctx, restoredActiveId);
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
pi.on("session_start", async (event, ctx) => {
|
|
368
|
+
restoreBranchScopedRuntime(ctx);
|
|
369
|
+
refreshWebEditorHost(ctx);
|
|
137
370
|
notifyActivePreset(ctx, "after session " + event.reason);
|
|
138
371
|
});
|
|
139
372
|
|
|
140
373
|
pi.on("session_tree", async (_event, ctx) => {
|
|
374
|
+
restoreBranchScopedRuntime(ctx);
|
|
375
|
+
refreshWebEditorHost(ctx);
|
|
141
376
|
notifyActivePreset(ctx, "after tree navigation");
|
|
142
377
|
});
|
|
143
378
|
|
|
144
379
|
pi.on("session_compact", async (_event, ctx) => {
|
|
380
|
+
restoreBranchScopedRuntime(ctx);
|
|
381
|
+
refreshWebEditorHost(ctx);
|
|
145
382
|
notifyActivePreset(ctx, "after compaction");
|
|
146
383
|
});
|
|
147
384
|
|
|
148
385
|
pi.on("turn_start", async () => {
|
|
386
|
+
syncActiveToolPolicy();
|
|
149
387
|
const id = activeId();
|
|
150
388
|
if (id) persistActiveSelection(id);
|
|
151
389
|
});
|
|
@@ -164,6 +402,7 @@ export default function piForge(pi: ExtensionAPI) {
|
|
|
164
402
|
{ options: event.systemPromptOptions, ctx, latestUserMessage: event.prompt, now: new Date(), variables: currentVariableStore },
|
|
165
403
|
event.systemPrompt,
|
|
166
404
|
);
|
|
405
|
+
recordCompileDiagnostics(ctx, result.diagnostics);
|
|
167
406
|
persistVariablesIfDirty(currentVariableStore);
|
|
168
407
|
|
|
169
408
|
return { systemPrompt: result.systemPrompt };
|
|
@@ -185,10 +424,20 @@ export default function piForge(pi: ExtensionAPI) {
|
|
|
185
424
|
{ options: currentSystemPromptOptions, ctx, latestUserMessage, now: new Date(), variables: currentVariableStore },
|
|
186
425
|
event.messages,
|
|
187
426
|
);
|
|
427
|
+
recordCompileDiagnostics(ctx, [...latestCompileDiagnostics, ...result.diagnostics]);
|
|
188
428
|
persistVariablesIfDirty(currentVariableStore);
|
|
189
429
|
return { messages: result.messages };
|
|
190
430
|
});
|
|
191
431
|
|
|
432
|
+
pi.on("message_end", async (event, ctx) => {
|
|
433
|
+
if (!active) return;
|
|
434
|
+
const diagnostics: PromptStackDiagnostic[] = [];
|
|
435
|
+
const message = applyFinalizeRegexRulesToMessage(active.stack, event.message, diagnostics);
|
|
436
|
+
if (diagnostics.length > 0) recordCompileDiagnostics(ctx, [...latestCompileDiagnostics, ...diagnostics]);
|
|
437
|
+
if (!message) return;
|
|
438
|
+
return { message };
|
|
439
|
+
});
|
|
440
|
+
|
|
192
441
|
pi.on("agent_end", async () => {
|
|
193
442
|
persistVariablesIfDirty(currentVariableStore);
|
|
194
443
|
currentSystemPromptOptions = undefined;
|
|
@@ -199,64 +448,100 @@ export default function piForge(pi: ExtensionAPI) {
|
|
|
199
448
|
|
|
200
449
|
pi.on("before_provider_request", async (event, ctx) => {
|
|
201
450
|
if (!interceptNextProviderPayload) return;
|
|
451
|
+
const savePath = interceptPayloadSavePath;
|
|
452
|
+
const displayTarget = interceptPayloadDisplayTarget;
|
|
202
453
|
interceptNextProviderPayload = false;
|
|
454
|
+
interceptPayloadSavePath = undefined;
|
|
455
|
+
interceptPayloadDisplayTarget = "editor";
|
|
456
|
+
payloadCaptureArmedAt = undefined;
|
|
203
457
|
ctx.ui.setStatus("pi-forge-intercept", undefined);
|
|
204
458
|
|
|
205
|
-
const
|
|
459
|
+
const capture = captureProviderPayload(event.payload, savePath);
|
|
460
|
+
if (savePath) {
|
|
461
|
+
if (!ctx.isProjectTrusted()) {
|
|
462
|
+
ctx.ui.notify("pi-forge: project is not trusted; refusing to save provider payload.", "warning");
|
|
463
|
+
} else {
|
|
464
|
+
const resolvedPath = savePath.startsWith("/") ? savePath : join(ctx.cwd, savePath);
|
|
465
|
+
mkdirSync(dirname(resolvedPath), { recursive: true });
|
|
466
|
+
writeFileSync(resolvedPath, capture.text, "utf8");
|
|
467
|
+
ctx.ui.notify(`pi-forge: provider payload saved to ${resolvedPath} (${capture.chars} chars, ~${capture.approxTokens} tokens)`, "info");
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
if (displayTarget === "web") {
|
|
472
|
+
ctx.ui.notify(`pi-forge: provider payload captured for web editor (${capture.chars} chars, ~${capture.approxTokens} tokens).`, "info");
|
|
473
|
+
return;
|
|
474
|
+
}
|
|
475
|
+
|
|
206
476
|
if (ctx.hasUI) {
|
|
207
|
-
await ctx.ui.editor(
|
|
477
|
+
await ctx.ui.editor(`pi-forge: provider payload (${capture.chars} chars, ~${capture.approxTokens} tokens)`, capture.text);
|
|
208
478
|
return;
|
|
209
479
|
}
|
|
210
480
|
|
|
211
|
-
console.log(
|
|
481
|
+
console.log(capture.text);
|
|
212
482
|
});
|
|
213
483
|
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
sessionVariables[name] = value;
|
|
237
|
-
pi.appendEntry(VARIABLE_ENTRY_TYPE, { variables: { ...sessionVariables } });
|
|
238
|
-
return {
|
|
239
|
-
content: [{ type: "text", text: `Variable ${name} set.` }],
|
|
240
|
-
details: { name, value },
|
|
241
|
-
};
|
|
242
|
-
},
|
|
243
|
-
});
|
|
484
|
+
function armPayloadIntercept(ctx: ExtensionCommandContext, savePath?: string, displayTarget: PayloadDisplayTarget = "editor"): void {
|
|
485
|
+
interceptNextProviderPayload = true;
|
|
486
|
+
interceptPayloadSavePath = savePath;
|
|
487
|
+
interceptPayloadDisplayTarget = displayTarget;
|
|
488
|
+
payloadCaptureArmedAt = new Date().toISOString();
|
|
489
|
+
latestProviderPayloadCapture = undefined;
|
|
490
|
+
ctx.ui.setStatus("pi-forge-intercept", ctx.ui.theme.fg("warning", savePath ? "payload:armed+save" : "payload:armed"));
|
|
491
|
+
if (displayTarget === "web") {
|
|
492
|
+
ctx.ui.notify(savePath ? `pi-forge: next provider payload will be captured in the web editor and saved to ${savePath}.` : "pi-forge: next provider payload will be captured in the web editor.", "info");
|
|
493
|
+
return;
|
|
494
|
+
}
|
|
495
|
+
ctx.ui.notify(savePath ? `pi-forge: next provider payload will be displayed and saved to ${savePath}.` : "pi-forge: next provider payload will be displayed before sending.", "info");
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
function clearPayloadCapture(ctx: ExtensionCommandContext): void {
|
|
499
|
+
interceptNextProviderPayload = false;
|
|
500
|
+
interceptPayloadSavePath = undefined;
|
|
501
|
+
interceptPayloadDisplayTarget = "editor";
|
|
502
|
+
payloadCaptureArmedAt = undefined;
|
|
503
|
+
latestProviderPayloadCapture = undefined;
|
|
504
|
+
ctx.ui.setStatus("pi-forge-intercept", undefined);
|
|
505
|
+
}
|
|
244
506
|
|
|
245
507
|
pi.registerCommand("intercept", {
|
|
246
508
|
description: "Display the next provider payload before it is sent",
|
|
247
509
|
handler: async (_args, ctx) => {
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
510
|
+
armPayloadIntercept(ctx);
|
|
511
|
+
},
|
|
512
|
+
});
|
|
513
|
+
|
|
514
|
+
pi.registerCommand("payload", {
|
|
515
|
+
description: "Inspect or save provider payloads: /payload next [save=<path>]",
|
|
516
|
+
getArgumentCompletions: (prefix) => {
|
|
517
|
+
const parts = prefix.trimStart().split(/\s+/);
|
|
518
|
+
if (parts.length <= 1 && !prefix.endsWith(" ")) {
|
|
519
|
+
return ["next"].filter((cmd) => cmd.startsWith(parts[0] ?? "")).map((cmd) => ({ value: cmd, label: cmd }));
|
|
520
|
+
}
|
|
521
|
+
if (parts[0] === "next" && parts.length <= 2) {
|
|
522
|
+
const suggestion = "save=.pi/forge/payloads/last.json";
|
|
523
|
+
return suggestion.startsWith(parts[1] ?? "") ? [{ value: `next ${suggestion}`, label: suggestion }] : null;
|
|
524
|
+
}
|
|
525
|
+
return null;
|
|
526
|
+
},
|
|
527
|
+
handler: async (args, ctx) => {
|
|
528
|
+
const [command = "next", ...rest] = args.trim() ? args.trim().split(/\s+/) : ["next"];
|
|
529
|
+
if (command !== "next") {
|
|
530
|
+
ctx.ui.notify(`Unknown /payload subcommand: ${command}`, "warning");
|
|
531
|
+
return;
|
|
532
|
+
}
|
|
533
|
+
const saveArg = rest.find((arg) => arg.startsWith("save="));
|
|
534
|
+
const savePath = saveArg?.slice("save=".length).trim() || undefined;
|
|
535
|
+
armPayloadIntercept(ctx, savePath);
|
|
251
536
|
},
|
|
252
537
|
});
|
|
253
538
|
|
|
254
539
|
pi.registerCommand("preset", {
|
|
255
|
-
description: "Manage pi-forge prompt stacks: list, use, preview, validate, reload,
|
|
540
|
+
description: "Manage pi-forge prompt stacks: list, use, preview, validate, reload, ui",
|
|
256
541
|
getArgumentCompletions: (prefix) => {
|
|
257
542
|
const parts = prefix.trimStart().split(/\s+/);
|
|
258
543
|
if (parts.length <= 1 && !prefix.endsWith(" ")) {
|
|
259
|
-
const commands = ["list", "use", "preview", "validate", "
|
|
544
|
+
const commands = ["list", "use", "preview", "validate", "diagnostics", "reload", "status", "import-silly", "migrate-stacks", "ui"];
|
|
260
545
|
return commands.filter((cmd) => cmd.startsWith(parts[0] ?? "")).map((cmd) => ({ value: cmd, label: cmd }));
|
|
261
546
|
}
|
|
262
547
|
const first = parts[0];
|
|
@@ -265,17 +550,15 @@ export default function piForge(pi: ExtensionAPI) {
|
|
|
265
550
|
const ids = ["none", ...stacks.map((loaded) => loaded.stack.id)];
|
|
266
551
|
return ids.filter((id) => id.startsWith(fragment)).map((id) => ({ value: `${first} ${id}`, label: id }));
|
|
267
552
|
}
|
|
268
|
-
if (first === "
|
|
269
|
-
const
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
return names.filter((n) => n.startsWith(fragment)).map((n) => ({ value: `vars ${sub} ${n}`, label: n }));
|
|
278
|
-
}
|
|
553
|
+
if (first === "ui" && parts.length <= 2) {
|
|
554
|
+
const fragment = parts[1] ?? "";
|
|
555
|
+
const subs = ["stop", "restart"];
|
|
556
|
+
return subs.filter((s) => s.startsWith(fragment)).map((s) => ({ value: `ui ${s}`, label: s }));
|
|
557
|
+
}
|
|
558
|
+
if (first === "migrate-stacks") {
|
|
559
|
+
const fragment = parts[parts.length - 1] ?? "";
|
|
560
|
+
const flags = ["--dry-run", "--overwrite", "--delete-legacy"];
|
|
561
|
+
return flags.filter((flag) => flag.startsWith(fragment)).map((flag) => ({ value: `${parts.slice(0, -1).join(" ")} ${flag}`.trim(), label: flag }));
|
|
279
562
|
}
|
|
280
563
|
return null;
|
|
281
564
|
},
|
|
@@ -299,49 +582,13 @@ export default function piForge(pi: ExtensionAPI) {
|
|
|
299
582
|
ctx.ui.notify(`pi-forge: reloaded ${stacks.length} prompt stack(s).`, "info");
|
|
300
583
|
return;
|
|
301
584
|
|
|
302
|
-
case "
|
|
585
|
+
case "ui": {
|
|
303
586
|
const sub = rest[0];
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
const name = rest[1];
|
|
307
|
-
const value = rest.slice(2).join(" ");
|
|
308
|
-
if (!name || value === "") {
|
|
309
|
-
ctx.ui.notify("Usage: /preset vars set <name> <value>", "warning");
|
|
310
|
-
return;
|
|
311
|
-
}
|
|
312
|
-
sessionVariables[name] = value;
|
|
313
|
-
pi.appendEntry(VARIABLE_ENTRY_TYPE, { variables: { ...sessionVariables } });
|
|
314
|
-
ctx.ui.notify(`pi-forge: set session variable ${name} = ${JSON.stringify(value)}`, "info");
|
|
587
|
+
if (sub === "stop") {
|
|
588
|
+
await stopWebEditor(ctx);
|
|
315
589
|
return;
|
|
316
590
|
}
|
|
317
|
-
|
|
318
|
-
if (sub === "get") {
|
|
319
|
-
const name = rest[1];
|
|
320
|
-
if (!name) {
|
|
321
|
-
ctx.ui.notify("Usage: /preset vars get <name>", "warning");
|
|
322
|
-
return;
|
|
323
|
-
}
|
|
324
|
-
const value = sessionVariables[name];
|
|
325
|
-
if (value === undefined) {
|
|
326
|
-
await showText(ctx, `pi-forge variable: ${name}`, `# ${name}\n\n(not set)`);
|
|
327
|
-
} else {
|
|
328
|
-
await showText(ctx, `pi-forge variable: ${name}`, `# ${name}\n\n${value}`);
|
|
329
|
-
}
|
|
330
|
-
return;
|
|
331
|
-
}
|
|
332
|
-
|
|
333
|
-
if (sub === "clear") {
|
|
334
|
-
const name = rest[1];
|
|
335
|
-
if (name) delete sessionVariables[name];
|
|
336
|
-
else sessionVariables = {};
|
|
337
|
-
currentVariableStore = createPromptVariableStore(sessionVariables);
|
|
338
|
-
pi.appendEntry(VARIABLE_ENTRY_TYPE, { variables: sessionVariables });
|
|
339
|
-
ctx.ui.notify(name ? `pi-forge: cleared session variable ${name}` : "pi-forge: cleared all session variables", "info");
|
|
340
|
-
return;
|
|
341
|
-
}
|
|
342
|
-
|
|
343
|
-
// No subcommand or "list" — show all variables
|
|
344
|
-
await showText(ctx, "pi-forge variables", renderVariablesList());
|
|
591
|
+
await openWebEditor(ctx, sub === "restart" ? "restart" : "open");
|
|
345
592
|
return;
|
|
346
593
|
}
|
|
347
594
|
|
|
@@ -379,23 +626,59 @@ export default function piForge(pi: ExtensionAPI) {
|
|
|
379
626
|
return;
|
|
380
627
|
}
|
|
381
628
|
|
|
629
|
+
case "diagnostics": {
|
|
630
|
+
await showText(ctx, "pi-forge diagnostics", renderCurrentDiagnostics());
|
|
631
|
+
return;
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
case "migrate-stacks": {
|
|
635
|
+
const flags = new Set(rest);
|
|
636
|
+
const dryRun = flags.has("--dry-run");
|
|
637
|
+
if (!ctx.isProjectTrusted() && !dryRun) {
|
|
638
|
+
ctx.ui.notify("pi-forge: project is not trusted; refusing to migrate prompt stacks.", "warning");
|
|
639
|
+
return;
|
|
640
|
+
}
|
|
641
|
+
const report = migrateLegacyPromptStacks(ctx.cwd, {
|
|
642
|
+
dryRun,
|
|
643
|
+
overwrite: flags.has("--overwrite"),
|
|
644
|
+
deleteLegacy: flags.has("--delete-legacy"),
|
|
645
|
+
});
|
|
646
|
+
if (!dryRun) reloadStacks(ctx, selectedActiveId());
|
|
647
|
+
const changed = report.copied + report.overwritten;
|
|
648
|
+
const summary = dryRun
|
|
649
|
+
? `pi-forge: migration dry run found ${report.files.length} legacy stack file(s).`
|
|
650
|
+
: `pi-forge: migrated ${changed} legacy stack file(s), skipped ${report.skipped}, errors ${report.errors}.`;
|
|
651
|
+
ctx.ui.notify(summary, report.errors ? "warning" : "info");
|
|
652
|
+
await showText(ctx, "pi-forge prompt-stack migration", renderMigrationReport(report));
|
|
653
|
+
return;
|
|
654
|
+
}
|
|
655
|
+
|
|
382
656
|
case "import-silly": {
|
|
657
|
+
if (!ctx.isProjectTrusted()) {
|
|
658
|
+
ctx.ui.notify("pi-forge: project is not trusted; refusing to write imported prompt stacks.", "warning");
|
|
659
|
+
return;
|
|
660
|
+
}
|
|
661
|
+
|
|
383
662
|
const sourcePath = rest[0];
|
|
384
663
|
if (!sourcePath) {
|
|
385
|
-
ctx.ui.notify("Usage: /preset import-silly <path> [character_id]", "warning");
|
|
664
|
+
ctx.ui.notify("Usage: /preset import-silly <path> [character_id] [--dry-run] [--overwrite]", "warning");
|
|
386
665
|
return;
|
|
387
666
|
}
|
|
388
667
|
|
|
668
|
+
const charIdToken = rest[1]?.startsWith("--") ? undefined : rest[1];
|
|
669
|
+
const flags = new Set(rest.slice(charIdToken ? 2 : 1));
|
|
670
|
+
const dryRun = flags.has("--dry-run");
|
|
671
|
+
let overwrite = flags.has("--overwrite");
|
|
672
|
+
|
|
389
673
|
const resolvedPath = sourcePath.startsWith("/") ? sourcePath : join(ctx.cwd, sourcePath);
|
|
390
674
|
if (!existsSync(resolvedPath)) {
|
|
391
675
|
ctx.ui.notify(`File not found: ${resolvedPath}`, "error");
|
|
392
676
|
return;
|
|
393
677
|
}
|
|
394
678
|
|
|
395
|
-
const
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
ctx.ui.notify(`Invalid character_id: ${charIdStr}`, "error");
|
|
679
|
+
const charId = charIdToken ? Number(charIdToken) : undefined;
|
|
680
|
+
if (charIdToken && (Number.isNaN(charId) || !Number.isFinite(charId))) {
|
|
681
|
+
ctx.ui.notify(`Invalid character_id: ${charIdToken}`, "error");
|
|
399
682
|
return;
|
|
400
683
|
}
|
|
401
684
|
|
|
@@ -405,16 +688,34 @@ export default function piForge(pi: ExtensionAPI) {
|
|
|
405
688
|
return;
|
|
406
689
|
}
|
|
407
690
|
|
|
408
|
-
|
|
409
|
-
const
|
|
691
|
+
const existingStack = stacks.find((candidate) => candidate.stack.id === result.stack.id);
|
|
692
|
+
const stackPath = existingStack?.filePath ?? promptStackPath(ctx.cwd, result.stack.id);
|
|
693
|
+
const stacksDir = dirname(stackPath);
|
|
694
|
+
const reportDir = join(ctx.cwd, ".pi", "forge", "import-reports");
|
|
695
|
+
const reportPath = join(reportDir, `${result.stack.id}.md`);
|
|
696
|
+
|
|
697
|
+
if (dryRun) {
|
|
698
|
+
await showText(ctx, `pi-forge import dry run: ${result.stack.id}`, `Would write stack to: ${stackPath}\nWould write report to: ${reportPath}\n\n## Generated stack JSON\n\n\`\`\`json\n${JSON.stringify(result.stack, null, 2)}\n\`\`\`\n\n${result.report}`);
|
|
699
|
+
return;
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
const existingPaths = [stackPath, reportPath].filter((path) => existsSync(path));
|
|
703
|
+
if (existingPaths.length > 0 && !overwrite) {
|
|
704
|
+
if (!ctx.hasUI) {
|
|
705
|
+
ctx.ui.notify(`pi-forge: import would overwrite existing file(s): ${existingPaths.join(", ")}. Re-run with --overwrite.`, "error");
|
|
706
|
+
return;
|
|
707
|
+
}
|
|
708
|
+
overwrite = await ctx.ui.confirm("Overwrite pi-forge import output?", `These file(s) already exist:\n${existingPaths.join("\n")}\n\nOverwrite them?`);
|
|
709
|
+
if (!overwrite) {
|
|
710
|
+
ctx.ui.notify("pi-forge: import cancelled; existing files were left unchanged.", "info");
|
|
711
|
+
return;
|
|
712
|
+
}
|
|
713
|
+
}
|
|
714
|
+
|
|
410
715
|
if (!existsSync(stacksDir)) mkdirSync(stacksDir, { recursive: true });
|
|
411
|
-
const stackPath = join(stacksDir, `${result.stack.id}.json`);
|
|
412
716
|
writeFileSync(stackPath, JSON.stringify(result.stack, null, 2), "utf8");
|
|
413
717
|
|
|
414
|
-
// Write the import report
|
|
415
|
-
const reportDir = join(ctx.cwd, ".pi", "forge", "import-reports");
|
|
416
718
|
if (!existsSync(reportDir)) mkdirSync(reportDir, { recursive: true });
|
|
417
|
-
const reportPath = join(reportDir, `${result.stack.id}.md`);
|
|
418
719
|
writeFileSync(reportPath, result.report, "utf8");
|
|
419
720
|
|
|
420
721
|
// Reload stacks to pick up the new one
|
|
@@ -433,13 +734,14 @@ export default function piForge(pi: ExtensionAPI) {
|
|
|
433
734
|
|
|
434
735
|
function renderStackList(ctx: ExtensionCommandContext): string {
|
|
435
736
|
const lines = [
|
|
436
|
-
|
|
737
|
+
"Prompt stack directories:",
|
|
738
|
+
...promptStackReadDirs(ctx.cwd).map((dir, index) => ` ${index === 0 ? "primary" : "legacy"}: ${dir}`),
|
|
437
739
|
`Active stack: ${active?.stack.id ?? "(none)"}`,
|
|
438
740
|
"",
|
|
439
741
|
];
|
|
440
742
|
|
|
441
743
|
if (stacks.length === 0) {
|
|
442
|
-
lines.push("No prompt stacks found.", "Create .pi/prompt-stacks/default.json to auto-activate a stack.");
|
|
744
|
+
lines.push("No prompt stacks found.", "Create .pi/forge/prompt-stacks/default.json to auto-activate a stack.");
|
|
443
745
|
return lines.join("\n");
|
|
444
746
|
}
|
|
445
747
|
|
|
@@ -452,26 +754,41 @@ export default function piForge(pi: ExtensionAPI) {
|
|
|
452
754
|
lines.push(` ${loaded.filePath}`);
|
|
453
755
|
}
|
|
454
756
|
|
|
455
|
-
lines.push("", "Commands:", " /preset use <id|none>", " /preset preview [id]", " /preset validate [id]", " /preset reload", " /preset
|
|
757
|
+
lines.push("", "Commands:", " /preset use <id|none>", " /preset preview [id]", " /preset validate [id]", " /preset diagnostics", " /preset reload", " /preset migrate-stacks [--dry-run] [--overwrite] [--delete-legacy]", " /preset ui [stop|restart]");
|
|
456
758
|
return lines.join("\n");
|
|
457
759
|
}
|
|
458
760
|
|
|
459
|
-
function
|
|
460
|
-
const lines = ["# pi-forge
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
lines.push("", "## Active stack static variables", "");
|
|
466
|
-
const staticEntries = Object.entries(active?.stack.variables ?? {}).sort(([a], [b]) => a.localeCompare(b));
|
|
467
|
-
if (staticEntries.length === 0) lines.push("(none)");
|
|
468
|
-
else for (const [key, value] of staticEntries) lines.push(`${key} = ${JSON.stringify(value)}`);
|
|
469
|
-
|
|
470
|
-
lines.push("", "Turn variables are cleared for each user message and are only visible during prompt compilation.");
|
|
761
|
+
function renderCurrentDiagnostics(): string {
|
|
762
|
+
const lines = ["# pi-forge diagnostics", ""];
|
|
763
|
+
lines.push("## Active stack load/validation diagnostics", "");
|
|
764
|
+
lines.push(active ? renderDiagnostics(active.diagnostics) : "No active prompt stack.");
|
|
765
|
+
lines.push("", "## Latest runtime compile diagnostics", "");
|
|
766
|
+
lines.push(renderDiagnostics(latestCompileDiagnostics));
|
|
471
767
|
return lines.join("\n");
|
|
472
768
|
}
|
|
473
769
|
|
|
770
|
+
function webPayloadSnapshot(): WebEditorPayloadSnapshot {
|
|
771
|
+
if (interceptNextProviderPayload) {
|
|
772
|
+
return {
|
|
773
|
+
status: "armed",
|
|
774
|
+
armedAt: payloadCaptureArmedAt,
|
|
775
|
+
savePath: interceptPayloadSavePath,
|
|
776
|
+
};
|
|
777
|
+
}
|
|
778
|
+
if (latestProviderPayloadCapture) {
|
|
779
|
+
return {
|
|
780
|
+
status: "captured",
|
|
781
|
+
capture: latestProviderPayloadCapture,
|
|
782
|
+
};
|
|
783
|
+
}
|
|
784
|
+
return { status: "idle" };
|
|
785
|
+
}
|
|
786
|
+
|
|
474
787
|
function renderPreview(ctx: ExtensionCommandContext, target: LoadedPromptStack): string {
|
|
788
|
+
return buildPreview(ctx, target).text;
|
|
789
|
+
}
|
|
790
|
+
|
|
791
|
+
function buildPreview(ctx: ExtensionCommandContext, target: LoadedPromptStack): { text: string; preview: WebEditorPreview; diagnostics: PromptStackDiagnostic[] } {
|
|
475
792
|
const options = ctx.getSystemPromptOptions();
|
|
476
793
|
const sessionContext = buildSessionContext(ctx.sessionManager.getEntries(), ctx.sessionManager.getLeafId());
|
|
477
794
|
const latestUserMessage = getLatestUserMessage(sessionContext.messages);
|
|
@@ -480,8 +797,23 @@ export default function piForge(pi: ExtensionAPI) {
|
|
|
480
797
|
const system = compileSystemPrompt(target.stack, runtime, ctx.getSystemPrompt());
|
|
481
798
|
const messages = compileMessages(target.stack, runtime, sessionContext.messages);
|
|
482
799
|
const diagnostics = [...target.diagnostics, ...system.diagnostics, ...messages.diagnostics];
|
|
483
|
-
|
|
484
|
-
|
|
800
|
+
const messageSections = messages.messages.map((message, index) => {
|
|
801
|
+
const content = agentMessageToPreviewText(message);
|
|
802
|
+
const source = messages.messageSources[index];
|
|
803
|
+
return previewSection(`message-${index}`, previewMessageTitle(source, index), content, message.role);
|
|
804
|
+
});
|
|
805
|
+
const systemSection = previewSection("system", "System prompt", system.systemPrompt || "(empty)");
|
|
806
|
+
const totalChars = systemSection.chars + messageSections.reduce((sum, section) => sum + section.chars, 0);
|
|
807
|
+
const preview: WebEditorPreview = {
|
|
808
|
+
stackId: target.stack.id,
|
|
809
|
+
generatedAt: new Date().toISOString(),
|
|
810
|
+
system: systemSection,
|
|
811
|
+
messages: messageSections,
|
|
812
|
+
totalChars,
|
|
813
|
+
approxTokens: estimatePayloadTokens(`${system.systemPrompt}\n${messageSections.map((section) => section.content).join("\n")}`),
|
|
814
|
+
};
|
|
815
|
+
|
|
816
|
+
const text = [
|
|
485
817
|
`# Prompt stack preview: ${target.stack.id}`,
|
|
486
818
|
"",
|
|
487
819
|
"## System prompt",
|
|
@@ -490,12 +822,51 @@ export default function piForge(pi: ExtensionAPI) {
|
|
|
490
822
|
"",
|
|
491
823
|
"## Message layout",
|
|
492
824
|
"",
|
|
493
|
-
|
|
825
|
+
renderPreviewSectionText(messageSections),
|
|
494
826
|
"",
|
|
495
827
|
"## Diagnostics",
|
|
496
828
|
"",
|
|
497
829
|
renderDiagnostics(diagnostics),
|
|
498
830
|
].join("\n");
|
|
831
|
+
|
|
832
|
+
return { text, preview, diagnostics };
|
|
833
|
+
}
|
|
834
|
+
|
|
835
|
+
function previewMessageTitle(source: CompileMessageSource | undefined, index: number): string {
|
|
836
|
+
if (source?.kind === "stack-item") {
|
|
837
|
+
return source.itemName?.trim() || source.itemId || `Stack item ${index + 1}`;
|
|
838
|
+
}
|
|
839
|
+
if (source?.kind === "chat-history") {
|
|
840
|
+
const label = source.itemName?.trim() || "Chat history";
|
|
841
|
+
return `${label} #${source.historyIndex ?? index + 1}`;
|
|
842
|
+
}
|
|
843
|
+
if (source?.kind === "implicit-history") {
|
|
844
|
+
return `Conversation history #${source.historyIndex ?? index + 1}`;
|
|
845
|
+
}
|
|
846
|
+
return `Message ${index + 1}`;
|
|
847
|
+
}
|
|
848
|
+
|
|
849
|
+
function renderPreviewSectionText(sections: WebEditorPreviewSection[], maxChars = 8000): string {
|
|
850
|
+
let text = "";
|
|
851
|
+
for (const section of sections) {
|
|
852
|
+
const role = section.role ? ` (${section.role})` : "";
|
|
853
|
+
text += `\n--- ${section.title}${role} ---\n`;
|
|
854
|
+
text += section.content;
|
|
855
|
+
text += "\n";
|
|
856
|
+
if (text.length > maxChars) return `${text.slice(0, maxChars)}\n\n[preview truncated]`;
|
|
857
|
+
}
|
|
858
|
+
return text.trimStart();
|
|
859
|
+
}
|
|
860
|
+
|
|
861
|
+
function previewSection(id: string, title: string, content: string, role?: string): WebEditorPreviewSection {
|
|
862
|
+
return {
|
|
863
|
+
id,
|
|
864
|
+
title,
|
|
865
|
+
role,
|
|
866
|
+
content,
|
|
867
|
+
chars: content.length,
|
|
868
|
+
approxTokens: estimatePayloadTokens(content),
|
|
869
|
+
};
|
|
499
870
|
}
|
|
500
871
|
|
|
501
872
|
function renderDiagnostics(diagnostics: PromptStackDiagnostic[]): string {
|
|
@@ -511,11 +882,9 @@ export default function piForge(pi: ExtensionAPI) {
|
|
|
511
882
|
console.log(text);
|
|
512
883
|
}
|
|
513
884
|
|
|
514
|
-
function
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
return `Failed to stringify provider payload: ${error instanceof Error ? error.message : String(error)}`;
|
|
519
|
-
}
|
|
885
|
+
function captureProviderPayload(value: unknown, savePath?: string): WebEditorPayloadCapture {
|
|
886
|
+
const capture = createProviderPayloadCapture(value, { stackId: active?.stack.id, savePath });
|
|
887
|
+
latestProviderPayloadCapture = capture;
|
|
888
|
+
return capture;
|
|
520
889
|
}
|
|
521
890
|
}
|