@zihanw/pi-forge 0.2.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 +154 -104
- package/README.zh-CN.md +151 -103
- package/examples/default-prompt-stack.json +52 -93
- package/examples/reviewer-prompt-stack.json +115 -0
- package/examples/sillytavern-dm-writer-prompt-stack.json +190 -0
- package/package.json +3 -8
- package/src/compiler.ts +546 -175
- package/src/index.ts +320 -660
- package/src/loader.ts +141 -56
- package/src/payload-capture.ts +85 -0
- package/src/policy.ts +42 -0
- package/src/regex.ts +500 -0
- package/src/sillytavern-importer.ts +404 -19
- package/src/stack-migration.ts +159 -0
- package/src/storage.ts +28 -0
- package/src/types.ts +80 -38
- 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/web-editor.ts +0 -1491
package/src/index.ts
CHANGED
|
@@ -1,47 +1,71 @@
|
|
|
1
|
-
import { existsSync, mkdirSync,
|
|
2
|
-
import { dirname,
|
|
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
|
|
17
|
-
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";
|
|
18
22
|
|
|
19
23
|
const STATE_ENTRY_TYPE = "pi-forge-prompt-stack-state";
|
|
20
24
|
const VARIABLE_ENTRY_TYPE = "pi-forge-variable-state";
|
|
21
|
-
const
|
|
25
|
+
const WEB_EDITOR_GLOBAL_KEY = "__piForgeWebEditor";
|
|
22
26
|
|
|
23
|
-
type
|
|
27
|
+
type PayloadDisplayTarget = "editor" | "web";
|
|
24
28
|
|
|
25
|
-
interface
|
|
26
|
-
|
|
27
|
-
|
|
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];
|
|
29
47
|
}
|
|
30
48
|
|
|
31
49
|
export default function piForge(pi: ExtensionAPI) {
|
|
50
|
+
const sharedWebEditors = getSharedWebEditorRegistry();
|
|
32
51
|
let stacks: LoadedPromptStack[] = [];
|
|
33
52
|
let active: LoadedPromptStack | undefined;
|
|
34
53
|
let currentSystemPromptOptions: BuildSystemPromptOptions | undefined;
|
|
35
54
|
let currentLatestUserMessage: string | undefined;
|
|
36
55
|
let currentVariableStore: PromptVariableStore | undefined;
|
|
37
56
|
let contextRewritePending = false;
|
|
38
|
-
let sessionVariables: Record<string,
|
|
57
|
+
let sessionVariables: Record<string, PromptVariableValue> = {};
|
|
39
58
|
let lastPersistedActiveId: string | undefined;
|
|
40
59
|
let interceptNextProviderPayload = false;
|
|
41
60
|
let interceptPayloadSavePath: string | undefined;
|
|
61
|
+
let interceptPayloadDisplayTarget: PayloadDisplayTarget = "editor";
|
|
62
|
+
let payloadCaptureArmedAt: string | undefined;
|
|
63
|
+
let latestProviderPayloadCapture: WebEditorPayloadCapture | undefined;
|
|
42
64
|
let latestCompileDiagnostics: PromptStackDiagnostic[] = [];
|
|
43
65
|
let webEditor: WebEditorServer | undefined;
|
|
44
66
|
let webEditorCwd: string | undefined;
|
|
67
|
+
let webEditorPreferredPort: number | undefined;
|
|
68
|
+
let toolPolicyBaseline: string[] | undefined;
|
|
45
69
|
|
|
46
70
|
function activeId(): string | undefined {
|
|
47
71
|
return active?.stack.id;
|
|
@@ -63,6 +87,7 @@ export default function piForge(pi: ExtensionAPI) {
|
|
|
63
87
|
active = undefined;
|
|
64
88
|
if (id) persistActiveSelection("none");
|
|
65
89
|
if (ctx) updateStatus(ctx);
|
|
90
|
+
syncActiveToolPolicy(ctx);
|
|
66
91
|
return true;
|
|
67
92
|
}
|
|
68
93
|
|
|
@@ -71,6 +96,7 @@ export default function piForge(pi: ExtensionAPI) {
|
|
|
71
96
|
active = found;
|
|
72
97
|
persistActiveSelection(found.stack.id);
|
|
73
98
|
if (ctx) updateStatus(ctx);
|
|
99
|
+
syncActiveToolPolicy(ctx);
|
|
74
100
|
return true;
|
|
75
101
|
}
|
|
76
102
|
|
|
@@ -78,6 +104,7 @@ export default function piForge(pi: ExtensionAPI) {
|
|
|
78
104
|
if (!ctx.isProjectTrusted()) {
|
|
79
105
|
stacks = [];
|
|
80
106
|
active = undefined;
|
|
107
|
+
syncActiveToolPolicy(ctx);
|
|
81
108
|
ctx.ui.notify("pi-forge: project is not trusted; prompt stacks are disabled.", "warning");
|
|
82
109
|
updateStatus(ctx);
|
|
83
110
|
return;
|
|
@@ -86,6 +113,7 @@ export default function piForge(pi: ExtensionAPI) {
|
|
|
86
113
|
stacks = loadPromptStacks(ctx.cwd);
|
|
87
114
|
active = chooseDefaultStack(stacks, preferredId);
|
|
88
115
|
updateStatus(ctx);
|
|
116
|
+
syncActiveToolPolicy(ctx);
|
|
89
117
|
}
|
|
90
118
|
|
|
91
119
|
function updateStatus(ctx: ExtensionContext): void {
|
|
@@ -98,6 +126,37 @@ export default function piForge(pi: ExtensionAPI) {
|
|
|
98
126
|
}
|
|
99
127
|
}
|
|
100
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
|
+
|
|
101
160
|
function notifyActivePreset(ctx: ExtensionContext, detail: string): void {
|
|
102
161
|
if (!active) return;
|
|
103
162
|
const errorCount = active.diagnostics.filter((d) => d.level === "error").length;
|
|
@@ -117,241 +176,129 @@ export default function piForge(pi: ExtensionAPI) {
|
|
|
117
176
|
ctx.ui.setStatus("pi-forge-diagnostics", undefined);
|
|
118
177
|
}
|
|
119
178
|
|
|
120
|
-
function
|
|
121
|
-
const errors = loaded.diagnostics.filter((d) => d.level === "error").length;
|
|
122
|
-
const warnings = loaded.diagnostics.filter((d) => d.level === "warning").length;
|
|
179
|
+
function webHostRuntime(ctx: ExtensionCommandContext): WebHostRuntime {
|
|
123
180
|
return {
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
function stackSummaries(): WebEditorStackSummary[] {
|
|
138
|
-
return stacks.map(stackSummary);
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
function createWebEditorHost(ctx: ExtensionCommandContext): WebEditorHost {
|
|
142
|
-
return {
|
|
143
|
-
cwd: ctx.cwd,
|
|
144
|
-
listStacks: () => stackSummaries(),
|
|
145
|
-
getStack: (id) => {
|
|
146
|
-
const loaded = stacks.find((candidate) => candidate.stack.id === id);
|
|
147
|
-
return loaded ? { stack: loaded.stack, filePath: loaded.filePath, diagnostics: loaded.diagnostics } : undefined;
|
|
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() };
|
|
148
192
|
},
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
return { ok: false, status: 403, error: "Project is not trusted; refusing to save prompt stacks." };
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
const target = stacks.find((candidate) => candidate.stack.id === id);
|
|
156
|
-
if (!target) return { ok: false, status: 404, error: `Unknown prompt stack: ${id}` };
|
|
157
|
-
if (!isInsidePromptStacksDir(ctx.cwd, target.filePath)) {
|
|
158
|
-
return { ok: false, status: 403, error: "Refusing to save outside .pi/prompt-stacks." };
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
writeFileSync(target.filePath, `${JSON.stringify(stack, null, 2)}\n`, "utf8");
|
|
162
|
-
const preferredId = active?.stack.id === id ? stack.id : selectedActiveId();
|
|
163
|
-
reloadStacks(ctx, preferredId);
|
|
164
|
-
const saved = stacks.find((candidate) => candidate.stack.id === stack.id) ?? stacks.find((candidate) => candidate.filePath === target.filePath);
|
|
165
|
-
if (!saved) return { ok: false, status: 500, error: "Saved stack could not be reloaded." };
|
|
166
|
-
return { ok: true, stack: stackSummary(saved), stacks: stackSummaries() };
|
|
167
|
-
},
|
|
168
|
-
deleteStack: (id) => deleteStackFile(ctx, id),
|
|
169
|
-
validateStack: (stack) => validatePromptStack(stack),
|
|
170
|
-
previewStack: (id, stack) => {
|
|
171
|
-
const target = stacks.find((candidate) => candidate.stack.id === id);
|
|
172
|
-
if (!target) return { ok: false, status: 404, error: `Unknown prompt stack: ${id}` };
|
|
173
|
-
const diagnostics = validatePromptStack(stack);
|
|
174
|
-
const text = renderPreview(ctx, { stack, filePath: target.filePath, diagnostics });
|
|
175
|
-
return { ok: true, text, diagnostics };
|
|
176
|
-
},
|
|
177
|
-
activateStack: (id) => {
|
|
178
|
-
if (!setActive(id, ctx)) return { ok: false, status: 404, error: `Unknown prompt stack: ${id}` };
|
|
179
|
-
return { ok: true, activeId: activeId(), stacks: stackSummaries() };
|
|
180
|
-
},
|
|
181
|
-
disableStacks: () => {
|
|
182
|
-
setActive("none", ctx);
|
|
183
|
-
return { ok: true, activeId: activeId(), stacks: stackSummaries() };
|
|
184
|
-
},
|
|
185
|
-
reloadStacks: () => {
|
|
186
|
-
reloadStacks(ctx, selectedActiveId());
|
|
187
|
-
return { ok: true, activeId: activeId(), stacks: stackSummaries() };
|
|
193
|
+
clearPayload: () => {
|
|
194
|
+
clearPayloadCapture(ctx);
|
|
195
|
+
return { ok: true, ...webPayloadSnapshot() };
|
|
188
196
|
},
|
|
189
197
|
};
|
|
190
198
|
}
|
|
191
199
|
|
|
192
|
-
function
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
): { ok: true; stack: WebEditorStackSummary; stacks: WebEditorStackSummary[] } | { ok: false; status?: number; error: string } {
|
|
197
|
-
if (!ctx.isProjectTrusted()) {
|
|
198
|
-
return { ok: false, status: 403, error: "Project is not trusted; refusing to create prompt stacks." };
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
const idError = validateWebStackId(stack.id);
|
|
202
|
-
if (idError) return { ok: false, status: 400, error: idError };
|
|
203
|
-
|
|
204
|
-
const stacksDir = promptStacksDir(ctx.cwd);
|
|
205
|
-
const targetPath = join(stacksDir, `${stack.id}.json`);
|
|
206
|
-
if (!isInsidePromptStacksDir(ctx.cwd, targetPath)) {
|
|
207
|
-
return { ok: false, status: 403, error: "Refusing to create outside .pi/prompt-stacks." };
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
const existingById = stacks.find((candidate) => candidate.stack.id === stack.id);
|
|
211
|
-
if (existingById && resolve(existingById.filePath) !== resolve(targetPath)) {
|
|
212
|
-
return { ok: false, status: 409, error: `Stack id already exists in ${existingById.filePath}.` };
|
|
213
|
-
}
|
|
214
|
-
if ((existsSync(targetPath) || existingById) && !options.overwrite) {
|
|
215
|
-
return { ok: false, status: 409, error: `Prompt stack already exists: ${stack.id}` };
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
const previousSelection = selectedActiveId();
|
|
219
|
-
mkdirSync(stacksDir, { recursive: true });
|
|
220
|
-
writeFileSync(targetPath, `${JSON.stringify(stack, null, 2)}\n`, "utf8");
|
|
221
|
-
reloadStacks(ctx, options.activate ? stack.id : (previousSelection ?? "none"));
|
|
222
|
-
if (options.activate) setActive(stack.id, ctx);
|
|
200
|
+
function sharedWebEditorForCwd(cwd: string): SharedWebEditorState {
|
|
201
|
+
sharedWebEditors.byCwd[cwd] ??= {};
|
|
202
|
+
return sharedWebEditors.byCwd[cwd];
|
|
203
|
+
}
|
|
223
204
|
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
205
|
+
function syncWebEditorFromShared(cwd: string): void {
|
|
206
|
+
const shared = sharedWebEditorForCwd(cwd);
|
|
207
|
+
webEditor = shared.server;
|
|
208
|
+
webEditorCwd = shared.cwd;
|
|
209
|
+
webEditorPreferredPort = shared.preferredPort;
|
|
227
210
|
}
|
|
228
211
|
|
|
229
|
-
function
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
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
|
+
}
|
|
236
221
|
|
|
237
|
-
|
|
238
|
-
if (
|
|
239
|
-
|
|
240
|
-
|
|
222
|
+
function clearWebEditor(server: WebEditorServer): void {
|
|
223
|
+
if (webEditor === server) {
|
|
224
|
+
webEditor = undefined;
|
|
225
|
+
webEditorCwd = undefined;
|
|
226
|
+
webEditorPreferredPort = undefined;
|
|
241
227
|
}
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
unlinkSync(target.filePath);
|
|
245
|
-
if (wasActive) {
|
|
246
|
-
setActive("none", ctx);
|
|
247
|
-
reloadStacks(ctx, "none");
|
|
248
|
-
} else {
|
|
249
|
-
reloadStacks(ctx, selectedActiveId());
|
|
228
|
+
for (const [cwd, shared] of Object.entries(sharedWebEditors.byCwd)) {
|
|
229
|
+
if (shared?.server === server) delete sharedWebEditors.byCwd[cwd];
|
|
250
230
|
}
|
|
251
|
-
return { ok: true, activeId: activeId(), stacks: stackSummaries() };
|
|
252
231
|
}
|
|
253
232
|
|
|
254
|
-
function
|
|
255
|
-
|
|
256
|
-
if (
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
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}`));
|
|
260
240
|
}
|
|
261
241
|
|
|
262
242
|
async function openWebEditor(ctx: ExtensionCommandContext, mode: "open" | "restart" = "open"): Promise<void> {
|
|
243
|
+
syncWebEditorFromShared(ctx.cwd);
|
|
263
244
|
const settings = loadWebEditorSettings(ctx);
|
|
264
245
|
for (const warning of settings.warnings) ctx.ui.notify(warning, "warning");
|
|
265
246
|
|
|
266
|
-
if (webEditor && (mode === "restart" ||
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
247
|
+
if (webEditor && (mode === "restart" || webEditorPreferredPort !== settings.preferredPort)) {
|
|
248
|
+
const server = webEditor;
|
|
249
|
+
await server.close();
|
|
250
|
+
clearWebEditor(server);
|
|
270
251
|
ctx.ui.setStatus("pi-forge-editor", undefined);
|
|
271
252
|
}
|
|
272
253
|
|
|
273
254
|
if (!webEditor) {
|
|
274
255
|
try {
|
|
275
|
-
webEditor = await startWebEditorServer(createWebEditorHost(ctx), { port: settings.
|
|
256
|
+
webEditor = await startWebEditorServer(createWebEditorHost(ctx, webHostRuntime(ctx)), { port: settings.preferredPort });
|
|
276
257
|
} catch (error) {
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
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
|
+
}
|
|
281
275
|
}
|
|
282
|
-
|
|
276
|
+
rememberWebEditor(webEditor, ctx.cwd, settings.preferredPort);
|
|
283
277
|
ctx.ui.setStatus("pi-forge-editor", ctx.ui.theme.fg("accent", `editor:${webEditor.port}`));
|
|
284
278
|
ctx.ui.notify(`pi-forge: stack editor running at ${webEditor.url}`, "info");
|
|
285
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}`));
|
|
286
283
|
ctx.ui.notify(`pi-forge: stack editor already running at ${webEditor.url}`, "info");
|
|
287
284
|
}
|
|
288
285
|
|
|
289
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}`);
|
|
290
287
|
}
|
|
291
288
|
|
|
292
|
-
function loadWebEditorSettings(ctx: ExtensionCommandContext): { port: number; configPath: string; warnings: string[] } {
|
|
293
|
-
const configPath = join(ctx.cwd, ".pi", "forge", "config.json");
|
|
294
|
-
if (!ctx.isProjectTrusted() || !existsSync(configPath)) {
|
|
295
|
-
return { port: DEFAULT_WEB_EDITOR_PORT, configPath, warnings: [] };
|
|
296
|
-
}
|
|
297
|
-
|
|
298
|
-
let raw: unknown;
|
|
299
|
-
try {
|
|
300
|
-
raw = JSON.parse(readFileSync(configPath, "utf8"));
|
|
301
|
-
} catch (error) {
|
|
302
|
-
return {
|
|
303
|
-
port: DEFAULT_WEB_EDITOR_PORT,
|
|
304
|
-
configPath,
|
|
305
|
-
warnings: [`pi-forge: failed to read ${configPath}; using editor port ${DEFAULT_WEB_EDITOR_PORT}. ${error instanceof Error ? error.message : String(error)}`],
|
|
306
|
-
};
|
|
307
|
-
}
|
|
308
|
-
|
|
309
|
-
if (!isPlainObject(raw)) {
|
|
310
|
-
return {
|
|
311
|
-
port: DEFAULT_WEB_EDITOR_PORT,
|
|
312
|
-
configPath,
|
|
313
|
-
warnings: [`pi-forge: ${configPath} must be a JSON object; using editor port ${DEFAULT_WEB_EDITOR_PORT}.`],
|
|
314
|
-
};
|
|
315
|
-
}
|
|
316
|
-
|
|
317
|
-
const webEditorConfig = isPlainObject(raw.webEditor) ? raw.webEditor : undefined;
|
|
318
|
-
const rawPort = webEditorConfig?.port ?? raw.webEditorPort;
|
|
319
|
-
if (rawPort === undefined) return { port: DEFAULT_WEB_EDITOR_PORT, configPath, warnings: [] };
|
|
320
|
-
|
|
321
|
-
if (typeof rawPort === "number" && Number.isInteger(rawPort) && rawPort >= 1 && rawPort <= 65535) {
|
|
322
|
-
return { port: rawPort, configPath, warnings: [] };
|
|
323
|
-
}
|
|
324
|
-
|
|
325
|
-
return {
|
|
326
|
-
port: DEFAULT_WEB_EDITOR_PORT,
|
|
327
|
-
configPath,
|
|
328
|
-
warnings: [`pi-forge: ${configPath} webEditor.port must be an integer from 1 to 65535; using editor port ${DEFAULT_WEB_EDITOR_PORT}.`],
|
|
329
|
-
};
|
|
330
|
-
}
|
|
331
|
-
|
|
332
|
-
function isPlainObject(value: unknown): value is Record<string, unknown> {
|
|
333
|
-
return !!value && typeof value === "object" && !Array.isArray(value);
|
|
334
|
-
}
|
|
335
|
-
|
|
336
289
|
async function stopWebEditor(ctx: ExtensionCommandContext): Promise<void> {
|
|
290
|
+
syncWebEditorFromShared(ctx.cwd);
|
|
337
291
|
if (!webEditor) {
|
|
338
292
|
ctx.ui.notify("pi-forge: stack editor is not running.", "info");
|
|
339
293
|
return;
|
|
340
294
|
}
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
295
|
+
const server = webEditor;
|
|
296
|
+
await server.close();
|
|
297
|
+
clearWebEditor(server);
|
|
344
298
|
ctx.ui.setStatus("pi-forge-editor", undefined);
|
|
345
299
|
ctx.ui.notify("pi-forge: stack editor stopped.", "info");
|
|
346
300
|
}
|
|
347
301
|
|
|
348
|
-
function isInsidePromptStacksDir(cwd: string, filePath: string): boolean {
|
|
349
|
-
const root = resolve(promptStacksDir(cwd));
|
|
350
|
-
const target = resolve(filePath);
|
|
351
|
-
const rel = relative(root, target);
|
|
352
|
-
return !!rel && !rel.startsWith("..") && !isAbsolute(rel);
|
|
353
|
-
}
|
|
354
|
-
|
|
355
302
|
function getCurrentBranchEntries(ctx: ExtensionContext): unknown[] {
|
|
356
303
|
const leafId = ctx.sessionManager.getLeafId();
|
|
357
304
|
if (leafId === null) return [];
|
|
@@ -373,13 +320,13 @@ export default function piForge(pi: ExtensionAPI) {
|
|
|
373
320
|
return undefined;
|
|
374
321
|
}
|
|
375
322
|
|
|
376
|
-
function getRestoredVariables(ctx: ExtensionContext): Record<string,
|
|
323
|
+
function getRestoredVariables(ctx: ExtensionContext): Record<string, PromptVariableValue> {
|
|
377
324
|
const entries = getCurrentBranchEntries(ctx);
|
|
378
325
|
for (let i = entries.length - 1; i >= 0; i--) {
|
|
379
326
|
const entry = entries[i] as { type?: string; customType?: string; data?: { variables?: unknown } };
|
|
380
327
|
if (entry.type !== "custom" || entry.customType !== VARIABLE_ENTRY_TYPE) continue;
|
|
381
328
|
if (!entry.data || typeof entry.data.variables !== "object" || Array.isArray(entry.data.variables)) return {};
|
|
382
|
-
return
|
|
329
|
+
return normalizeVariableRecord(entry.data.variables as Record<string, unknown>);
|
|
383
330
|
}
|
|
384
331
|
return {};
|
|
385
332
|
}
|
|
@@ -391,135 +338,25 @@ export default function piForge(pi: ExtensionAPI) {
|
|
|
391
338
|
markSessionVariablesClean(store);
|
|
392
339
|
}
|
|
393
340
|
|
|
394
|
-
function
|
|
395
|
-
const result: Record<string,
|
|
341
|
+
function normalizeVariableRecord(value: Record<string, unknown>): Record<string, PromptVariableValue> {
|
|
342
|
+
const result: Record<string, PromptVariableValue> = {};
|
|
396
343
|
for (const [key, raw] of Object.entries(value)) {
|
|
397
|
-
if (
|
|
344
|
+
if (isPromptVariableValue(raw)) result[key] = raw;
|
|
398
345
|
}
|
|
399
346
|
return result;
|
|
400
347
|
}
|
|
401
348
|
|
|
402
|
-
function
|
|
349
|
+
function isPromptVariableValue(value: unknown): value is PromptVariableValue {
|
|
403
350
|
if (value === null) return true;
|
|
404
351
|
const type = typeof value;
|
|
405
352
|
if (type === "string" || type === "boolean") return true;
|
|
406
353
|
if (type === "number") return Number.isFinite(value);
|
|
407
|
-
if (Array.isArray(value)) return value.every(
|
|
354
|
+
if (Array.isArray(value)) return value.every(isPromptVariableValue);
|
|
408
355
|
if (!value || typeof value !== "object") return false;
|
|
409
|
-
return Object.values(value as Record<string, unknown>).every(
|
|
410
|
-
}
|
|
411
|
-
|
|
412
|
-
function validateStateName(name: string): string | undefined {
|
|
413
|
-
if (!name.trim()) return "state name must not be empty";
|
|
414
|
-
if (!/^[A-Za-z0-9_.:-]+$/.test(name)) {
|
|
415
|
-
return "state name may only contain letters, numbers, underscore, dash, dot, and colon";
|
|
416
|
-
}
|
|
417
|
-
return undefined;
|
|
418
|
-
}
|
|
419
|
-
|
|
420
|
-
function validateStateUpdate(update: StateUpdateInput, actor: StateActor): string | undefined {
|
|
421
|
-
const permissionError = validateStateWritePermission(update.name, actor, actor === "agent" ? "write" : "set");
|
|
422
|
-
if (permissionError) return permissionError;
|
|
423
|
-
|
|
424
|
-
const definition = active?.stack.state?.definitions?.[update.name];
|
|
425
|
-
if (definition?.type) {
|
|
426
|
-
const typeError = validateStateValueType(update.value, definition.type);
|
|
427
|
-
if (typeError) return `${update.name}: expected ${definition.type}, got ${typeError}`;
|
|
428
|
-
}
|
|
429
|
-
return undefined;
|
|
430
|
-
}
|
|
431
|
-
|
|
432
|
-
function validateStateClear(name: string, actor: StateActor): string | undefined {
|
|
433
|
-
return validateStateWritePermission(name, actor, "clear");
|
|
434
|
-
}
|
|
435
|
-
|
|
436
|
-
function validateStateWritePermission(name: string, actor: StateActor, action: "set" | "write" | "clear"): string | undefined {
|
|
437
|
-
const nameError = validateStateName(name);
|
|
438
|
-
if (nameError) return `${name || "(empty)"}: ${nameError}`;
|
|
439
|
-
|
|
440
|
-
if (actor === "agent" && !name.startsWith(AGENT_VAR_PREFIX)) {
|
|
441
|
-
return `${name}: agents may only ${action} ${AGENT_VAR_PREFIX}* state`;
|
|
442
|
-
}
|
|
443
|
-
|
|
444
|
-
const definition = active?.stack.state?.definitions?.[name];
|
|
445
|
-
if (actor === "agent" && definition?.agentWritable === false) {
|
|
446
|
-
return `${name}: stack schema marks this state as not agent-writable`;
|
|
447
|
-
}
|
|
448
|
-
if (actor === "user" && definition?.userWritable === false) {
|
|
449
|
-
return `${name}: stack schema marks this state as not user-writable`;
|
|
450
|
-
}
|
|
451
|
-
return undefined;
|
|
452
|
-
}
|
|
453
|
-
|
|
454
|
-
function applyStatePatch(updates: StateUpdateInput[], clears: string[], actor: StateActor): { ok: true; updated: number; cleared: number } | { ok: false; error: string } {
|
|
455
|
-
for (const update of updates) {
|
|
456
|
-
const error = validateStateUpdate(update, actor);
|
|
457
|
-
if (error) return { ok: false, error };
|
|
458
|
-
}
|
|
459
|
-
for (const name of clears) {
|
|
460
|
-
const error = validateStateClear(name, actor);
|
|
461
|
-
if (error) return { ok: false, error };
|
|
462
|
-
}
|
|
463
|
-
|
|
464
|
-
for (const update of updates) {
|
|
465
|
-
sessionVariables[update.name] = update.value;
|
|
466
|
-
if (currentVariableStore) currentVariableStore.session[update.name] = update.value;
|
|
467
|
-
}
|
|
468
|
-
for (const name of clears) {
|
|
469
|
-
delete sessionVariables[name];
|
|
470
|
-
if (currentVariableStore) delete currentVariableStore.session[name];
|
|
471
|
-
}
|
|
472
|
-
|
|
473
|
-
pi.appendEntry(VARIABLE_ENTRY_TYPE, { variables: { ...sessionVariables } });
|
|
474
|
-
if (currentVariableStore) markSessionVariablesClean(currentVariableStore);
|
|
475
|
-
return { ok: true, updated: updates.length, cleared: clears.length };
|
|
476
|
-
}
|
|
477
|
-
|
|
478
|
-
function validateStateValueType(value: PromptStateValue, typeExpression: string): string | undefined {
|
|
479
|
-
const types = typeExpression.split("|").map((part) => part.trim()).filter(Boolean);
|
|
480
|
-
if (types.length === 0 || types.some((type) => stateValueMatchesType(value, type))) return undefined;
|
|
481
|
-
return inferRuntimeType(value);
|
|
482
|
-
}
|
|
483
|
-
|
|
484
|
-
function stateValueMatchesType(value: PromptStateValue, type: string): boolean {
|
|
485
|
-
if (type === "any" || type === "unknown" || type === "json" || type === "Json") return true;
|
|
486
|
-
if (type === "null") return value === null;
|
|
487
|
-
if (type === "array" || type === "unknown[]") return Array.isArray(value);
|
|
488
|
-
if (type === "object" || type === "Record<string, unknown>" || type === "Record<string, any>") {
|
|
489
|
-
return !!value && typeof value === "object" && !Array.isArray(value);
|
|
490
|
-
}
|
|
491
|
-
if (type.endsWith("[]")) {
|
|
492
|
-
if (!Array.isArray(value)) return false;
|
|
493
|
-
const itemType = type.slice(0, -2).trim();
|
|
494
|
-
return value.every((item) => stateValueMatchesType(item, itemType));
|
|
495
|
-
}
|
|
496
|
-
return typeof value === type;
|
|
497
|
-
}
|
|
498
|
-
|
|
499
|
-
function inferRuntimeType(value: PromptStateValue): string {
|
|
500
|
-
if (value === null) return "null";
|
|
501
|
-
if (Array.isArray(value)) return "array";
|
|
502
|
-
return typeof value;
|
|
503
|
-
}
|
|
504
|
-
|
|
505
|
-
function parseStateCommandValue(raw: string): PromptStateValue {
|
|
506
|
-
const trimmed = raw.trim();
|
|
507
|
-
if (/^(true|false|null|-?\d+(?:\.\d+)?(?:[eE][+-]?\d+)?|["[{])/.test(trimmed)) {
|
|
508
|
-
try {
|
|
509
|
-
const parsed = JSON.parse(trimmed);
|
|
510
|
-
if (isPromptStateValue(parsed)) return parsed;
|
|
511
|
-
} catch {
|
|
512
|
-
// Fall back to string for friendly command input.
|
|
513
|
-
}
|
|
514
|
-
}
|
|
515
|
-
return raw;
|
|
356
|
+
return Object.values(value as Record<string, unknown>).every(isPromptVariableValue);
|
|
516
357
|
}
|
|
517
358
|
|
|
518
|
-
function
|
|
519
|
-
return typeof value === "string" ? value : JSON.stringify(value, null, 2);
|
|
520
|
-
}
|
|
521
|
-
|
|
522
|
-
function restoreBranchScopedState(ctx: ExtensionContext): void {
|
|
359
|
+
function restoreBranchScopedRuntime(ctx: ExtensionContext): void {
|
|
523
360
|
sessionVariables = getRestoredVariables(ctx);
|
|
524
361
|
currentVariableStore = undefined;
|
|
525
362
|
const restoredActiveId = getRestoredActiveId(ctx);
|
|
@@ -528,21 +365,25 @@ export default function piForge(pi: ExtensionAPI) {
|
|
|
528
365
|
}
|
|
529
366
|
|
|
530
367
|
pi.on("session_start", async (event, ctx) => {
|
|
531
|
-
|
|
368
|
+
restoreBranchScopedRuntime(ctx);
|
|
369
|
+
refreshWebEditorHost(ctx);
|
|
532
370
|
notifyActivePreset(ctx, "after session " + event.reason);
|
|
533
371
|
});
|
|
534
372
|
|
|
535
373
|
pi.on("session_tree", async (_event, ctx) => {
|
|
536
|
-
|
|
374
|
+
restoreBranchScopedRuntime(ctx);
|
|
375
|
+
refreshWebEditorHost(ctx);
|
|
537
376
|
notifyActivePreset(ctx, "after tree navigation");
|
|
538
377
|
});
|
|
539
378
|
|
|
540
379
|
pi.on("session_compact", async (_event, ctx) => {
|
|
541
|
-
|
|
380
|
+
restoreBranchScopedRuntime(ctx);
|
|
381
|
+
refreshWebEditorHost(ctx);
|
|
542
382
|
notifyActivePreset(ctx, "after compaction");
|
|
543
383
|
});
|
|
544
384
|
|
|
545
385
|
pi.on("turn_start", async () => {
|
|
386
|
+
syncActiveToolPolicy();
|
|
546
387
|
const id = activeId();
|
|
547
388
|
if (id) persistActiveSelection(id);
|
|
548
389
|
});
|
|
@@ -588,6 +429,15 @@ export default function piForge(pi: ExtensionAPI) {
|
|
|
588
429
|
return { messages: result.messages };
|
|
589
430
|
});
|
|
590
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
|
+
|
|
591
441
|
pi.on("agent_end", async () => {
|
|
592
442
|
persistVariablesIfDirty(currentVariableStore);
|
|
593
443
|
currentSystemPromptOptions = undefined;
|
|
@@ -599,139 +449,61 @@ export default function piForge(pi: ExtensionAPI) {
|
|
|
599
449
|
pi.on("before_provider_request", async (event, ctx) => {
|
|
600
450
|
if (!interceptNextProviderPayload) return;
|
|
601
451
|
const savePath = interceptPayloadSavePath;
|
|
452
|
+
const displayTarget = interceptPayloadDisplayTarget;
|
|
602
453
|
interceptNextProviderPayload = false;
|
|
603
454
|
interceptPayloadSavePath = undefined;
|
|
455
|
+
interceptPayloadDisplayTarget = "editor";
|
|
456
|
+
payloadCaptureArmedAt = undefined;
|
|
604
457
|
ctx.ui.setStatus("pi-forge-intercept", undefined);
|
|
605
458
|
|
|
606
|
-
const
|
|
607
|
-
const approxTokens = estimatePayloadTokens(payload);
|
|
459
|
+
const capture = captureProviderPayload(event.payload, savePath);
|
|
608
460
|
if (savePath) {
|
|
609
461
|
if (!ctx.isProjectTrusted()) {
|
|
610
462
|
ctx.ui.notify("pi-forge: project is not trusted; refusing to save provider payload.", "warning");
|
|
611
463
|
} else {
|
|
612
464
|
const resolvedPath = savePath.startsWith("/") ? savePath : join(ctx.cwd, savePath);
|
|
613
465
|
mkdirSync(dirname(resolvedPath), { recursive: true });
|
|
614
|
-
writeFileSync(resolvedPath,
|
|
615
|
-
ctx.ui.notify(`pi-forge: provider payload saved to ${resolvedPath} (${
|
|
466
|
+
writeFileSync(resolvedPath, capture.text, "utf8");
|
|
467
|
+
ctx.ui.notify(`pi-forge: provider payload saved to ${resolvedPath} (${capture.chars} chars, ~${capture.approxTokens} tokens)`, "info");
|
|
616
468
|
}
|
|
617
469
|
}
|
|
618
470
|
|
|
619
|
-
if (
|
|
620
|
-
|
|
471
|
+
if (displayTarget === "web") {
|
|
472
|
+
ctx.ui.notify(`pi-forge: provider payload captured for web editor (${capture.chars} chars, ~${capture.approxTokens} tokens).`, "info");
|
|
621
473
|
return;
|
|
622
474
|
}
|
|
623
475
|
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
name: "forge_state_set",
|
|
629
|
-
label: "Set Prompt State",
|
|
630
|
-
description: "Batch update persistent prompt state for future turns. Only names starting with 'agent.' can be written by the agent.",
|
|
631
|
-
promptSnippet: "Batch update agent-scoped prompt state for cross-turn continuity. Use this when durable state should be visible in the prompt_state slot on future turns.",
|
|
632
|
-
promptGuidelines: [
|
|
633
|
-
"Use forge_state_set to persist concise cross-turn state such as task progress, story state, open questions, or durable facts the user asked you to remember.",
|
|
634
|
-
`Only state names starting with '${AGENT_VAR_PREFIX}' are writable by the agent. User and stack configuration state are read-only to the agent.`,
|
|
635
|
-
"Prefer one batch update at natural checkpoints instead of many small updates. Do not store secrets or large transcripts.",
|
|
636
|
-
"State written by this tool is primarily for future turns; the current prompt has already been built.",
|
|
637
|
-
],
|
|
638
|
-
parameters: Type.Object({
|
|
639
|
-
updates: Type.Optional(Type.Array(Type.Object({
|
|
640
|
-
name: Type.String({ description: `State name (must start with '${AGENT_VAR_PREFIX}')` }),
|
|
641
|
-
value: Type.Unknown({ description: "JSON-compatible value: string, number, boolean, null, array, or object" }),
|
|
642
|
-
reason: Type.Optional(Type.String({ description: "Brief reason for the update" })),
|
|
643
|
-
}))),
|
|
644
|
-
clears: Type.Optional(Type.Array(Type.String({ description: `State names to clear (must start with '${AGENT_VAR_PREFIX}')` }))),
|
|
645
|
-
}),
|
|
646
|
-
async execute(_toolCallId, params, _signal, _onUpdate, _ctx) {
|
|
647
|
-
const updatesRaw = Array.isArray(params.updates) ? params.updates : [];
|
|
648
|
-
const clears = Array.isArray(params.clears) ? params.clears.filter((name): name is string => typeof name === "string") : [];
|
|
649
|
-
const updates: StateUpdateInput[] = [];
|
|
650
|
-
|
|
651
|
-
for (const raw of updatesRaw) {
|
|
652
|
-
if (!raw || typeof raw !== "object") {
|
|
653
|
-
return {
|
|
654
|
-
content: [{ type: "text", text: "Error: every update must be an object." }],
|
|
655
|
-
details: { error: "invalid update" },
|
|
656
|
-
};
|
|
657
|
-
}
|
|
658
|
-
const update = raw as { name?: unknown; value?: unknown; reason?: unknown };
|
|
659
|
-
if (typeof update.name !== "string") {
|
|
660
|
-
return {
|
|
661
|
-
content: [{ type: "text", text: "Error: every update needs a string name." }],
|
|
662
|
-
details: { error: "invalid update name" },
|
|
663
|
-
};
|
|
664
|
-
}
|
|
665
|
-
if (!isPromptStateValue(update.value)) {
|
|
666
|
-
return {
|
|
667
|
-
content: [{ type: "text", text: `Error: ${update.name} value is not JSON-compatible.` }],
|
|
668
|
-
details: { error: "invalid value", name: update.name },
|
|
669
|
-
};
|
|
670
|
-
}
|
|
671
|
-
updates.push({
|
|
672
|
-
name: update.name,
|
|
673
|
-
value: update.value,
|
|
674
|
-
reason: typeof update.reason === "string" ? update.reason : undefined,
|
|
675
|
-
});
|
|
676
|
-
}
|
|
677
|
-
|
|
678
|
-
if (updates.length === 0 && clears.length === 0) {
|
|
679
|
-
return {
|
|
680
|
-
content: [{ type: "text", text: "No state updates or clears provided." }],
|
|
681
|
-
details: { updated: 0, cleared: 0 },
|
|
682
|
-
};
|
|
683
|
-
}
|
|
684
|
-
|
|
685
|
-
const result = applyStatePatch(updates, clears, "agent");
|
|
686
|
-
if (!result.ok) {
|
|
687
|
-
return {
|
|
688
|
-
content: [{ type: "text", text: `Error: ${result.error}` }],
|
|
689
|
-
details: { error: result.error },
|
|
690
|
-
};
|
|
691
|
-
}
|
|
476
|
+
if (ctx.hasUI) {
|
|
477
|
+
await ctx.ui.editor(`pi-forge: provider payload (${capture.chars} chars, ~${capture.approxTokens} tokens)`, capture.text);
|
|
478
|
+
return;
|
|
479
|
+
}
|
|
692
480
|
|
|
693
|
-
|
|
694
|
-
content: [{ type: "text", text: `State updated: ${result.updated} set, ${result.cleared} cleared.` }],
|
|
695
|
-
details: { updated: updates.map(({ name, value, reason }) => ({ name, value, reason })), cleared: clears },
|
|
696
|
-
};
|
|
697
|
-
},
|
|
481
|
+
console.log(capture.text);
|
|
698
482
|
});
|
|
699
483
|
|
|
700
|
-
|
|
701
|
-
name: "forge_set_var",
|
|
702
|
-
label: "Set Prompt Variable",
|
|
703
|
-
description: "Compatibility alias for forge_state_set. Set one persistent agent-scoped state value. Only variables starting with 'agent.' can be written by the agent.",
|
|
704
|
-
promptSnippet: "Compatibility alias for forge_state_set. Prefer forge_state_set for batch prompt state updates.",
|
|
705
|
-
promptGuidelines: [
|
|
706
|
-
"Prefer forge_state_set when updating prompt state. forge_set_var only exists for compatibility with older prompt stacks.",
|
|
707
|
-
`Only variable names starting with '${AGENT_VAR_PREFIX}' are writable.`,
|
|
708
|
-
],
|
|
709
|
-
parameters: Type.Object({
|
|
710
|
-
name: Type.String({ description: `Variable name (must start with '${AGENT_VAR_PREFIX}')` }),
|
|
711
|
-
value: Type.String({ description: "Variable value" }),
|
|
712
|
-
}),
|
|
713
|
-
async execute(_toolCallId, params, _signal, _onUpdate, _ctx) {
|
|
714
|
-
const result = applyStatePatch([{ name: params.name, value: params.value }], [], "agent");
|
|
715
|
-
if (!result.ok) {
|
|
716
|
-
return {
|
|
717
|
-
content: [{ type: "text", text: `Error: ${result.error}` }],
|
|
718
|
-
details: { error: result.error, name: params.name },
|
|
719
|
-
};
|
|
720
|
-
}
|
|
721
|
-
return {
|
|
722
|
-
content: [{ type: "text", text: `Variable ${params.name} set.` }],
|
|
723
|
-
details: { name: params.name, value: params.value },
|
|
724
|
-
};
|
|
725
|
-
},
|
|
726
|
-
});
|
|
727
|
-
|
|
728
|
-
function armPayloadIntercept(ctx: ExtensionCommandContext, savePath?: string): void {
|
|
484
|
+
function armPayloadIntercept(ctx: ExtensionCommandContext, savePath?: string, displayTarget: PayloadDisplayTarget = "editor"): void {
|
|
729
485
|
interceptNextProviderPayload = true;
|
|
730
486
|
interceptPayloadSavePath = savePath;
|
|
487
|
+
interceptPayloadDisplayTarget = displayTarget;
|
|
488
|
+
payloadCaptureArmedAt = new Date().toISOString();
|
|
489
|
+
latestProviderPayloadCapture = undefined;
|
|
731
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
|
+
}
|
|
732
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");
|
|
733
496
|
}
|
|
734
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
|
+
}
|
|
506
|
+
|
|
735
507
|
pi.registerCommand("intercept", {
|
|
736
508
|
description: "Display the next provider payload before it is sent",
|
|
737
509
|
handler: async (_args, ctx) => {
|
|
@@ -765,11 +537,11 @@ export default function piForge(pi: ExtensionAPI) {
|
|
|
765
537
|
});
|
|
766
538
|
|
|
767
539
|
pi.registerCommand("preset", {
|
|
768
|
-
description: "Manage pi-forge prompt stacks: list, use, preview, validate, reload,
|
|
540
|
+
description: "Manage pi-forge prompt stacks: list, use, preview, validate, reload, ui",
|
|
769
541
|
getArgumentCompletions: (prefix) => {
|
|
770
542
|
const parts = prefix.trimStart().split(/\s+/);
|
|
771
543
|
if (parts.length <= 1 && !prefix.endsWith(" ")) {
|
|
772
|
-
const commands = ["list", "use", "preview", "validate", "diagnostics", "reload", "status", "
|
|
544
|
+
const commands = ["list", "use", "preview", "validate", "diagnostics", "reload", "status", "import-silly", "migrate-stacks", "ui"];
|
|
773
545
|
return commands.filter((cmd) => cmd.startsWith(parts[0] ?? "")).map((cmd) => ({ value: cmd, label: cmd }));
|
|
774
546
|
}
|
|
775
547
|
const first = parts[0];
|
|
@@ -778,49 +550,20 @@ export default function piForge(pi: ExtensionAPI) {
|
|
|
778
550
|
const ids = ["none", ...stacks.map((loaded) => loaded.stack.id)];
|
|
779
551
|
return ids.filter((id) => id.startsWith(fragment)).map((id) => ({ value: `${first} ${id}`, label: id }));
|
|
780
552
|
}
|
|
781
|
-
if (first === "vars") {
|
|
782
|
-
const sub = parts[1] ?? "";
|
|
783
|
-
if (parts.length <= 2 && !prefix.endsWith(" ")) {
|
|
784
|
-
const subs = ["set", "get", "clear", "list"];
|
|
785
|
-
return subs.filter((s) => s.startsWith(sub)).map((s) => ({ value: `vars ${s}`, label: s }));
|
|
786
|
-
}
|
|
787
|
-
if (["set", "get", "clear"].includes(sub) && parts.length <= 3) {
|
|
788
|
-
const fragment = parts[2] ?? "";
|
|
789
|
-
const names = Object.keys(sessionVariables);
|
|
790
|
-
return names.filter((n) => n.startsWith(fragment)).map((n) => ({ value: `vars ${sub} ${n}`, label: n }));
|
|
791
|
-
}
|
|
792
|
-
}
|
|
793
553
|
if (first === "ui" && parts.length <= 2) {
|
|
794
554
|
const fragment = parts[1] ?? "";
|
|
795
555
|
const subs = ["stop", "restart"];
|
|
796
556
|
return subs.filter((s) => s.startsWith(fragment)).map((s) => ({ value: `ui ${s}`, label: s }));
|
|
797
557
|
}
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
},
|
|
803
|
-
});
|
|
804
|
-
|
|
805
|
-
pi.registerCommand("state", {
|
|
806
|
-
description: "Manage pi-forge prompt state",
|
|
807
|
-
getArgumentCompletions: (prefix) => {
|
|
808
|
-
const parts = prefix.trimStart().split(/\s+/);
|
|
809
|
-
if (parts.length <= 1 && !prefix.endsWith(" ")) {
|
|
810
|
-
const commands = ["list", "set", "get", "clear"];
|
|
811
|
-
return commands.filter((cmd) => cmd.startsWith(parts[0] ?? "")).map((cmd) => ({ value: cmd, label: cmd }));
|
|
812
|
-
}
|
|
813
|
-
const first = parts[0];
|
|
814
|
-
if (["get", "clear"].includes(first) && parts.length <= 2) {
|
|
815
|
-
const fragment = parts[1] ?? "";
|
|
816
|
-
return Object.keys(sessionVariables)
|
|
817
|
-
.filter((name) => name.startsWith(fragment))
|
|
818
|
-
.map((name) => ({ value: `${first} ${name}`, label: name }));
|
|
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 }));
|
|
819
562
|
}
|
|
820
563
|
return null;
|
|
821
564
|
},
|
|
822
565
|
handler: async (args, ctx) => {
|
|
823
|
-
await
|
|
566
|
+
await handlePresetCommand(args, ctx);
|
|
824
567
|
},
|
|
825
568
|
});
|
|
826
569
|
|
|
@@ -849,57 +592,6 @@ export default function piForge(pi: ExtensionAPI) {
|
|
|
849
592
|
return;
|
|
850
593
|
}
|
|
851
594
|
|
|
852
|
-
case "vars": {
|
|
853
|
-
const sub = rest[0];
|
|
854
|
-
|
|
855
|
-
if (sub === "set") {
|
|
856
|
-
const name = rest[1];
|
|
857
|
-
const value = rest.slice(2).join(" ");
|
|
858
|
-
if (!name || value === "") {
|
|
859
|
-
ctx.ui.notify("Usage: /preset vars set <name> <value>", "warning");
|
|
860
|
-
return;
|
|
861
|
-
}
|
|
862
|
-
const result = applyStatePatch([{ name, value }], [], "user");
|
|
863
|
-
if (!result.ok) {
|
|
864
|
-
ctx.ui.notify(`pi-forge variable error: ${result.error}`, "error");
|
|
865
|
-
return;
|
|
866
|
-
}
|
|
867
|
-
ctx.ui.notify(`pi-forge: set session variable ${name} = ${JSON.stringify(value)}`, "info");
|
|
868
|
-
return;
|
|
869
|
-
}
|
|
870
|
-
|
|
871
|
-
if (sub === "get") {
|
|
872
|
-
const name = rest[1];
|
|
873
|
-
if (!name) {
|
|
874
|
-
ctx.ui.notify("Usage: /preset vars get <name>", "warning");
|
|
875
|
-
return;
|
|
876
|
-
}
|
|
877
|
-
const value = sessionVariables[name];
|
|
878
|
-
if (value === undefined) {
|
|
879
|
-
await showText(ctx, `pi-forge variable: ${name}`, `# ${name}\n\n(not set)`);
|
|
880
|
-
} else {
|
|
881
|
-
await showText(ctx, `pi-forge variable: ${name}`, `# ${name}\n\n${formatStateValue(value)}`);
|
|
882
|
-
}
|
|
883
|
-
return;
|
|
884
|
-
}
|
|
885
|
-
|
|
886
|
-
if (sub === "clear") {
|
|
887
|
-
const name = rest[1];
|
|
888
|
-
const clears = name ? [name] : Object.keys(sessionVariables);
|
|
889
|
-
const result = applyStatePatch([], clears, "user");
|
|
890
|
-
if (!result.ok) {
|
|
891
|
-
ctx.ui.notify(`pi-forge variable error: ${result.error}`, "error");
|
|
892
|
-
return;
|
|
893
|
-
}
|
|
894
|
-
ctx.ui.notify(name ? `pi-forge: cleared session variable ${name}` : "pi-forge: cleared all session variables", "info");
|
|
895
|
-
return;
|
|
896
|
-
}
|
|
897
|
-
|
|
898
|
-
// No subcommand or "list" — show all variables
|
|
899
|
-
await showText(ctx, "pi-forge variables", renderVariablesList());
|
|
900
|
-
return;
|
|
901
|
-
}
|
|
902
|
-
|
|
903
595
|
case "use": {
|
|
904
596
|
const id = rest[0];
|
|
905
597
|
if (!id) {
|
|
@@ -939,6 +631,28 @@ export default function piForge(pi: ExtensionAPI) {
|
|
|
939
631
|
return;
|
|
940
632
|
}
|
|
941
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
|
+
|
|
942
656
|
case "import-silly": {
|
|
943
657
|
if (!ctx.isProjectTrusted()) {
|
|
944
658
|
ctx.ui.notify("pi-forge: project is not trusted; refusing to write imported prompt stacks.", "warning");
|
|
@@ -974,8 +688,9 @@ export default function piForge(pi: ExtensionAPI) {
|
|
|
974
688
|
return;
|
|
975
689
|
}
|
|
976
690
|
|
|
977
|
-
const
|
|
978
|
-
const stackPath =
|
|
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);
|
|
979
694
|
const reportDir = join(ctx.cwd, ".pi", "forge", "import-reports");
|
|
980
695
|
const reportPath = join(reportDir, `${result.stack.id}.md`);
|
|
981
696
|
|
|
@@ -1017,71 +732,16 @@ export default function piForge(pi: ExtensionAPI) {
|
|
|
1017
732
|
}
|
|
1018
733
|
}
|
|
1019
734
|
|
|
1020
|
-
async function handleStateCommand(args: string, ctx: ExtensionCommandContext): Promise<void> {
|
|
1021
|
-
const trimmed = args.trim();
|
|
1022
|
-
const [command = "list", ...rest] = trimmed ? trimmed.split(/\s+/) : ["list"];
|
|
1023
|
-
|
|
1024
|
-
switch (command) {
|
|
1025
|
-
case "list":
|
|
1026
|
-
case "status":
|
|
1027
|
-
await showText(ctx, "pi-forge state", renderVariablesList());
|
|
1028
|
-
return;
|
|
1029
|
-
|
|
1030
|
-
case "set": {
|
|
1031
|
-
const name = rest[0];
|
|
1032
|
-
const rawValue = rest.slice(1).join(" ");
|
|
1033
|
-
if (!name || rawValue === "") {
|
|
1034
|
-
ctx.ui.notify("Usage: /state set <name> <json-or-text-value>", "warning");
|
|
1035
|
-
return;
|
|
1036
|
-
}
|
|
1037
|
-
const value = parseStateCommandValue(rawValue);
|
|
1038
|
-
const result = applyStatePatch([{ name, value }], [], "user");
|
|
1039
|
-
if (!result.ok) {
|
|
1040
|
-
ctx.ui.notify(`pi-forge state error: ${result.error}`, "error");
|
|
1041
|
-
return;
|
|
1042
|
-
}
|
|
1043
|
-
ctx.ui.notify(`pi-forge: set state ${name} = ${JSON.stringify(value)}`, "info");
|
|
1044
|
-
return;
|
|
1045
|
-
}
|
|
1046
|
-
|
|
1047
|
-
case "get": {
|
|
1048
|
-
const name = rest[0];
|
|
1049
|
-
if (!name) {
|
|
1050
|
-
ctx.ui.notify("Usage: /state get <name>", "warning");
|
|
1051
|
-
return;
|
|
1052
|
-
}
|
|
1053
|
-
const value = sessionVariables[name];
|
|
1054
|
-
await showText(ctx, `pi-forge state: ${name}`, value === undefined ? `# ${name}\n\n(not set)` : `# ${name}\n\n${formatStateValue(value)}`);
|
|
1055
|
-
return;
|
|
1056
|
-
}
|
|
1057
|
-
|
|
1058
|
-
case "clear": {
|
|
1059
|
-
const name = rest[0];
|
|
1060
|
-
const clears = name ? [name] : Object.keys(sessionVariables);
|
|
1061
|
-
const result = applyStatePatch([], clears, "user");
|
|
1062
|
-
if (!result.ok) {
|
|
1063
|
-
ctx.ui.notify(`pi-forge state error: ${result.error}`, "error");
|
|
1064
|
-
return;
|
|
1065
|
-
}
|
|
1066
|
-
ctx.ui.notify(name ? `pi-forge: cleared state ${name}` : "pi-forge: cleared all session state", "info");
|
|
1067
|
-
return;
|
|
1068
|
-
}
|
|
1069
|
-
|
|
1070
|
-
default:
|
|
1071
|
-
ctx.ui.notify(`Unknown /state subcommand: ${command}`, "warning");
|
|
1072
|
-
return;
|
|
1073
|
-
}
|
|
1074
|
-
}
|
|
1075
|
-
|
|
1076
735
|
function renderStackList(ctx: ExtensionCommandContext): string {
|
|
1077
736
|
const lines = [
|
|
1078
|
-
|
|
737
|
+
"Prompt stack directories:",
|
|
738
|
+
...promptStackReadDirs(ctx.cwd).map((dir, index) => ` ${index === 0 ? "primary" : "legacy"}: ${dir}`),
|
|
1079
739
|
`Active stack: ${active?.stack.id ?? "(none)"}`,
|
|
1080
740
|
"",
|
|
1081
741
|
];
|
|
1082
742
|
|
|
1083
743
|
if (stacks.length === 0) {
|
|
1084
|
-
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.");
|
|
1085
745
|
return lines.join("\n");
|
|
1086
746
|
}
|
|
1087
747
|
|
|
@@ -1094,7 +754,7 @@ export default function piForge(pi: ExtensionAPI) {
|
|
|
1094
754
|
lines.push(` ${loaded.filePath}`);
|
|
1095
755
|
}
|
|
1096
756
|
|
|
1097
|
-
lines.push("", "Commands:", " /preset use <id|none>", " /preset preview [id]", " /preset validate [id]", " /preset diagnostics", " /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]");
|
|
1098
758
|
return lines.join("\n");
|
|
1099
759
|
}
|
|
1100
760
|
|
|
@@ -1107,36 +767,28 @@ export default function piForge(pi: ExtensionAPI) {
|
|
|
1107
767
|
return lines.join("\n");
|
|
1108
768
|
}
|
|
1109
769
|
|
|
1110
|
-
function
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
const staticEntries = Object.entries(active?.stack.variables ?? {}).sort(([a], [b]) => a.localeCompare(b));
|
|
1118
|
-
if (staticEntries.length === 0) lines.push("(none)");
|
|
1119
|
-
else for (const [key, value] of staticEntries) lines.push(`${key} = ${JSON.stringify(value)}`);
|
|
1120
|
-
|
|
1121
|
-
const definitions = Object.entries(active?.stack.state?.definitions ?? {}).sort(([a], [b]) => a.localeCompare(b));
|
|
1122
|
-
if (definitions.length > 0) {
|
|
1123
|
-
lines.push("", "## Active stack state definitions", "");
|
|
1124
|
-
for (const [name, definition] of definitions) {
|
|
1125
|
-
const details = [
|
|
1126
|
-
definition.type ? `type=${definition.type}` : undefined,
|
|
1127
|
-
definition.scope ? `scope=${definition.scope}` : undefined,
|
|
1128
|
-
definition.agentWritable !== undefined ? `agentWritable=${definition.agentWritable}` : undefined,
|
|
1129
|
-
definition.userWritable !== undefined ? `userWritable=${definition.userWritable}` : undefined,
|
|
1130
|
-
].filter(Boolean).join(", ");
|
|
1131
|
-
lines.push(`${name}${details ? ` (${details})` : ""}${definition.description ? ` — ${definition.description}` : ""}`);
|
|
1132
|
-
}
|
|
770
|
+
function webPayloadSnapshot(): WebEditorPayloadSnapshot {
|
|
771
|
+
if (interceptNextProviderPayload) {
|
|
772
|
+
return {
|
|
773
|
+
status: "armed",
|
|
774
|
+
armedAt: payloadCaptureArmedAt,
|
|
775
|
+
savePath: interceptPayloadSavePath,
|
|
776
|
+
};
|
|
1133
777
|
}
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
778
|
+
if (latestProviderPayloadCapture) {
|
|
779
|
+
return {
|
|
780
|
+
status: "captured",
|
|
781
|
+
capture: latestProviderPayloadCapture,
|
|
782
|
+
};
|
|
783
|
+
}
|
|
784
|
+
return { status: "idle" };
|
|
1137
785
|
}
|
|
1138
786
|
|
|
1139
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[] } {
|
|
1140
792
|
const options = ctx.getSystemPromptOptions();
|
|
1141
793
|
const sessionContext = buildSessionContext(ctx.sessionManager.getEntries(), ctx.sessionManager.getLeafId());
|
|
1142
794
|
const latestUserMessage = getLatestUserMessage(sessionContext.messages);
|
|
@@ -1145,8 +797,23 @@ export default function piForge(pi: ExtensionAPI) {
|
|
|
1145
797
|
const system = compileSystemPrompt(target.stack, runtime, ctx.getSystemPrompt());
|
|
1146
798
|
const messages = compileMessages(target.stack, runtime, sessionContext.messages);
|
|
1147
799
|
const diagnostics = [...target.diagnostics, ...system.diagnostics, ...messages.diagnostics];
|
|
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
|
+
};
|
|
1148
815
|
|
|
1149
|
-
|
|
816
|
+
const text = [
|
|
1150
817
|
`# Prompt stack preview: ${target.stack.id}`,
|
|
1151
818
|
"",
|
|
1152
819
|
"## System prompt",
|
|
@@ -1155,76 +822,69 @@ export default function piForge(pi: ExtensionAPI) {
|
|
|
1155
822
|
"",
|
|
1156
823
|
"## Message layout",
|
|
1157
824
|
"",
|
|
1158
|
-
|
|
825
|
+
renderPreviewSectionText(messageSections),
|
|
1159
826
|
"",
|
|
1160
827
|
"## Diagnostics",
|
|
1161
828
|
"",
|
|
1162
829
|
renderDiagnostics(diagnostics),
|
|
1163
830
|
].join("\n");
|
|
1164
|
-
}
|
|
1165
831
|
|
|
1166
|
-
|
|
1167
|
-
if (diagnostics.length === 0) return "No diagnostics.";
|
|
1168
|
-
return diagnostics.map((d) => `${d.level.toUpperCase()}${d.itemId ? ` [${d.itemId}]` : ""}: ${d.message}`).join("\n");
|
|
832
|
+
return { text, preview, diagnostics };
|
|
1169
833
|
}
|
|
1170
834
|
|
|
1171
|
-
|
|
1172
|
-
if (
|
|
1173
|
-
|
|
1174
|
-
return;
|
|
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}`;
|
|
1175
838
|
}
|
|
1176
|
-
|
|
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}`;
|
|
1177
847
|
}
|
|
1178
848
|
|
|
1179
|
-
function
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
const
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
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]`;
|
|
1186
857
|
}
|
|
858
|
+
return text.trimStart();
|
|
1187
859
|
}
|
|
1188
860
|
|
|
1189
|
-
function
|
|
1190
|
-
return
|
|
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
|
+
};
|
|
1191
870
|
}
|
|
1192
871
|
|
|
1193
|
-
function
|
|
1194
|
-
if (
|
|
1195
|
-
|
|
1196
|
-
if (value === null || typeof value !== "object") return value;
|
|
1197
|
-
if (Array.isArray(value)) {
|
|
1198
|
-
const maxItems = 80;
|
|
1199
|
-
const items = value.slice(0, maxItems).map((item) => redactPayload(item, depth + 1));
|
|
1200
|
-
if (value.length > maxItems) items.push(`[pi-forge: ${value.length - maxItems} more items omitted]`);
|
|
1201
|
-
return items;
|
|
1202
|
-
}
|
|
1203
|
-
|
|
1204
|
-
const result: Record<string, unknown> = {};
|
|
1205
|
-
let count = 0;
|
|
1206
|
-
for (const [key, raw] of Object.entries(value as Record<string, unknown>)) {
|
|
1207
|
-
if (++count > 120) {
|
|
1208
|
-
result["[pi-forge: omitted]"] = "object has more than 120 keys";
|
|
1209
|
-
break;
|
|
1210
|
-
}
|
|
1211
|
-
if (isSecretKey(key)) {
|
|
1212
|
-
result[key] = "[redacted]";
|
|
1213
|
-
continue;
|
|
1214
|
-
}
|
|
1215
|
-
result[key] = redactPayload(raw, depth + 1);
|
|
1216
|
-
}
|
|
1217
|
-
return result;
|
|
872
|
+
function renderDiagnostics(diagnostics: PromptStackDiagnostic[]): string {
|
|
873
|
+
if (diagnostics.length === 0) return "No diagnostics.";
|
|
874
|
+
return diagnostics.map((d) => `${d.level.toUpperCase()}${d.itemId ? ` [${d.itemId}]` : ""}: ${d.message}`).join("\n");
|
|
1218
875
|
}
|
|
1219
876
|
|
|
1220
|
-
function
|
|
1221
|
-
|
|
877
|
+
async function showText(ctx: ExtensionCommandContext, title: string, text: string): Promise<void> {
|
|
878
|
+
if (ctx.hasUI) {
|
|
879
|
+
await ctx.ui.editor(title, text);
|
|
880
|
+
return;
|
|
881
|
+
}
|
|
882
|
+
console.log(text);
|
|
1222
883
|
}
|
|
1223
884
|
|
|
1224
|
-
function
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
return value;
|
|
885
|
+
function captureProviderPayload(value: unknown, savePath?: string): WebEditorPayloadCapture {
|
|
886
|
+
const capture = createProviderPayloadCapture(value, { stackId: active?.stack.id, savePath });
|
|
887
|
+
latestProviderPayloadCapture = capture;
|
|
888
|
+
return capture;
|
|
1229
889
|
}
|
|
1230
890
|
}
|