@zihanw/pi-forge 0.4.0-beta.1 → 0.4.1
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/CHANGELOG.md +45 -1
- package/PUBLIC_API.md +3 -26
- package/README.md +90 -601
- package/README.zh-CN.md +86 -585
- package/SUBAGENT_ADAPTER_CONTRACT.md +3 -197
- package/dist/agent-profile.d.ts +24 -1
- package/dist/agent-profile.d.ts.map +1 -1
- package/dist/agent-profile.js +146 -36
- package/dist/agent-profile.js.map +1 -1
- package/dist/catalog.d.ts +27 -0
- package/dist/catalog.d.ts.map +1 -0
- package/dist/catalog.js +59 -0
- package/dist/catalog.js.map +1 -0
- package/dist/forge-config.d.ts +106 -0
- package/dist/forge-config.d.ts.map +1 -1
- package/dist/forge-config.js +305 -18
- package/dist/forge-config.js.map +1 -1
- package/dist/index.d.ts +5 -5
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +55 -8
- package/dist/index.js.map +1 -1
- package/dist/lifecycle.d.ts +2 -1
- package/dist/lifecycle.d.ts.map +1 -1
- package/dist/lifecycle.js +7 -4
- package/dist/lifecycle.js.map +1 -1
- package/dist/loader.d.ts +17 -1
- package/dist/loader.d.ts.map +1 -1
- package/dist/loader.js +84 -25
- package/dist/loader.js.map +1 -1
- package/dist/preset-command.d.ts +1 -1
- package/dist/preset-command.d.ts.map +1 -1
- package/dist/preset-command.js +38 -10
- package/dist/preset-command.js.map +1 -1
- package/dist/profile-command.d.ts.map +1 -1
- package/dist/profile-command.js +46 -21
- package/dist/profile-command.js.map +1 -1
- package/dist/profile-service.d.ts +6 -3
- package/dist/profile-service.d.ts.map +1 -1
- package/dist/profile-service.js +55 -13
- package/dist/profile-service.js.map +1 -1
- package/dist/resource-identity.d.ts +33 -0
- package/dist/resource-identity.d.ts.map +1 -0
- package/dist/resource-identity.js +56 -0
- package/dist/resource-identity.js.map +1 -0
- package/dist/runtime/profile-runtime.d.ts.map +1 -1
- package/dist/runtime/profile-runtime.js +7 -2
- package/dist/runtime/profile-runtime.js.map +1 -1
- package/dist/runtime/prompt-stack-runtime.d.ts +1 -1
- package/dist/runtime/prompt-stack-runtime.d.ts.map +1 -1
- package/dist/runtime/prompt-stack-runtime.js +22 -12
- package/dist/runtime/prompt-stack-runtime.js.map +1 -1
- package/dist/runtime/subagent-runtime.d.ts +23 -8
- package/dist/runtime/subagent-runtime.d.ts.map +1 -1
- package/dist/runtime/subagent-runtime.js +296 -67
- package/dist/runtime/subagent-runtime.js.map +1 -1
- package/dist/storage.d.ts +11 -0
- package/dist/storage.d.ts.map +1 -1
- package/dist/storage.js +51 -2
- package/dist/storage.js.map +1 -1
- package/dist/subagent/canonical.d.ts +19 -7
- package/dist/subagent/canonical.d.ts.map +1 -1
- package/dist/subagent/canonical.js +19 -47
- package/dist/subagent/canonical.js.map +1 -1
- package/dist/subagent/contract.d.ts +1 -2
- package/dist/subagent/contract.d.ts.map +1 -1
- package/dist/subagent/contract.js +1 -2
- package/dist/subagent/contract.js.map +1 -1
- package/dist/subagent/index.d.ts +4 -3
- package/dist/subagent/index.d.ts.map +1 -1
- package/dist/subagent/index.js +4 -3
- package/dist/subagent/index.js.map +1 -1
- package/dist/subagent/plan.d.ts +5 -1
- package/dist/subagent/plan.d.ts.map +1 -1
- package/dist/subagent/plan.js +29 -32
- package/dist/subagent/plan.js.map +1 -1
- package/dist/subagent/request.d.ts.map +1 -1
- package/dist/subagent/request.js +36 -4
- package/dist/subagent/request.js.map +1 -1
- package/dist/subagent/types.d.ts +66 -178
- package/dist/subagent/types.d.ts.map +1 -1
- package/dist/subagent/types.js +1 -1
- package/dist/subagent/types.js.map +1 -1
- package/dist/subagent/validation.d.ts +14 -14
- package/dist/subagent/validation.d.ts.map +1 -1
- package/dist/subagent/validation.js +52 -238
- package/dist/subagent/validation.js.map +1 -1
- package/dist/subagent-command.d.ts +2 -1
- package/dist/subagent-command.d.ts.map +1 -1
- package/dist/subagent-command.js +115 -19
- package/dist/subagent-command.js.map +1 -1
- package/dist/subagent-host.d.ts.map +1 -1
- package/dist/subagent-host.js +43 -16
- package/dist/subagent-host.js.map +1 -1
- package/dist/subagent-profile-tool.d.ts +25 -2
- package/dist/subagent-profile-tool.d.ts.map +1 -1
- package/dist/subagent-profile-tool.js +41 -9
- package/dist/subagent-profile-tool.js.map +1 -1
- package/dist/subagent-tool.d.ts +31 -4
- package/dist/subagent-tool.d.ts.map +1 -1
- package/dist/subagent-tool.js +311 -137
- package/dist/subagent-tool.js.map +1 -1
- package/dist/types.d.ts +3 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js.map +1 -1
- package/dist/web-editor/client-script.generated.d.ts +1 -1
- package/dist/web-editor/client-script.generated.d.ts.map +1 -1
- package/dist/web-editor/client-script.generated.js +1 -1
- package/dist/web-editor/client-script.generated.js.map +1 -1
- package/dist/web-editor/client-styles.d.ts +2 -0
- package/dist/web-editor/client-styles.d.ts.map +1 -0
- package/dist/web-editor/client-styles.generated.d.ts +2 -0
- package/dist/web-editor/client-styles.generated.d.ts.map +1 -0
- package/dist/web-editor/client-styles.generated.js +3 -0
- package/dist/web-editor/client-styles.generated.js.map +1 -0
- package/dist/web-editor/client-styles.js +2 -0
- package/dist/web-editor/client-styles.js.map +1 -0
- package/dist/web-editor/page.d.ts +2 -0
- package/dist/web-editor/page.d.ts.map +1 -1
- package/dist/web-editor/page.js +11 -73
- package/dist/web-editor/page.js.map +1 -1
- package/dist/web-editor/server.d.ts.map +1 -1
- package/dist/web-editor/server.js +176 -1
- package/dist/web-editor/server.js.map +1 -1
- package/dist/web-editor/styles.d.ts.map +1 -1
- package/dist/web-editor/styles.js +60 -3
- package/dist/web-editor/styles.js.map +1 -1
- package/dist/web-editor/types.d.ts +87 -0
- package/dist/web-editor/types.d.ts.map +1 -1
- package/dist/web-host.d.ts +13 -2
- package/dist/web-host.d.ts.map +1 -1
- package/dist/web-host.js +371 -27
- package/dist/web-host.js.map +1 -1
- package/docs/README.md +41 -0
- package/docs/concepts/agent-profiles.md +60 -0
- package/docs/concepts/prompt-stacks.md +95 -0
- package/docs/design/README.md +17 -0
- package/docs/design/roadmap-0.4-archive.md +216 -0
- package/docs/design/subagents/design-review.md +220 -0
- package/docs/design/subagents/interface-design.md +274 -0
- package/docs/design/subagents/sdk-spike-findings.md +117 -0
- package/docs/development/complexity-review.md +86 -0
- package/docs/development/release.md +31 -0
- package/docs/development/roadmap.md +36 -0
- package/docs/development/scoped-global-profiles-stacks.md +325 -0
- package/docs/development/setup.md +75 -0
- package/docs/getting-started.md +95 -0
- package/docs/guides/custom-macros-and-slots.md +68 -0
- package/docs/guides/debugging.md +39 -0
- package/docs/guides/delegation.md +107 -0
- package/docs/guides/sillytavern-import.md +47 -0
- package/docs/guides/use-cases.md +65 -0
- package/docs/guides/web-editor.md +75 -0
- package/docs/reference/commands.md +60 -0
- package/docs/reference/configuration.md +66 -0
- package/docs/reference/features.md +280 -0
- package/docs/reference/macros-and-slots.md +82 -0
- package/docs/reference/public-api.md +28 -0
- package/docs/reference/stack-schema.md +167 -0
- package/docs/reference/subagent-adapter.md +204 -0
- package/docs/zh-CN/README.md +37 -0
- package/docs/zh-CN/concepts/agent-profiles.md +46 -0
- package/docs/zh-CN/concepts/prompt-stacks.md +42 -0
- package/docs/zh-CN/getting-started.md +81 -0
- package/docs/zh-CN/guides/delegation.md +66 -0
- package/docs/zh-CN/guides/web-editor.md +45 -0
- package/docs/zh-CN/reference/commands.md +60 -0
- package/package.json +29 -15
- package/dist/subagent/backend-registry.d.ts +0 -75
- package/dist/subagent/backend-registry.d.ts.map +0 -1
- package/dist/subagent/backend-registry.js +0 -463
- package/dist/subagent/backend-registry.js.map +0 -1
- package/dist/subagent/diagnostics.d.ts +0 -3
- package/dist/subagent/diagnostics.d.ts.map +0 -1
- package/dist/subagent/diagnostics.js +0 -5
- package/dist/subagent/diagnostics.js.map +0 -1
- package/dist/subagent/pi-model-runtime.d.ts +0 -8
- package/dist/subagent/pi-model-runtime.d.ts.map +0 -1
- package/dist/subagent/pi-model-runtime.js +0 -22
- package/dist/subagent/pi-model-runtime.js.map +0 -1
- package/dist/subagent/pi-sdk-backend.d.ts +0 -23
- package/dist/subagent/pi-sdk-backend.d.ts.map +0 -1
- package/dist/subagent/pi-sdk-backend.js +0 -383
- package/dist/subagent/pi-sdk-backend.js.map +0 -1
- package/dist/subagent/pi-subprocess-backend.d.ts +0 -72
- package/dist/subagent/pi-subprocess-backend.d.ts.map +0 -1
- package/dist/subagent/pi-subprocess-backend.js +0 -756
- package/dist/subagent/pi-subprocess-backend.js.map +0 -1
- package/dist/subagent/subprocess-bridge.d.ts +0 -21
- package/dist/subagent/subprocess-bridge.d.ts.map +0 -1
- package/dist/subagent/subprocess-bridge.js +0 -87
- package/dist/subagent/subprocess-bridge.js.map +0 -1
- package/dist/subagent/subprocess-report.d.ts +0 -4
- package/dist/subagent/subprocess-report.d.ts.map +0 -1
- package/dist/subagent/subprocess-report.js +0 -55
- package/dist/subagent/subprocess-report.js.map +0 -1
- package/dist/subagent-contract.d.ts +0 -8
- package/dist/subagent-contract.d.ts.map +0 -1
- package/dist/subagent-contract.js +0 -8
- package/dist/subagent-contract.js.map +0 -1
- package/dist/web-editor/client/api.d.ts +0 -9
- package/dist/web-editor/client/api.d.ts.map +0 -1
- package/dist/web-editor/client/api.js +0 -26
- package/dist/web-editor/client/api.js.map +0 -1
- package/dist/web-editor/client/dom.d.ts +0 -13
- package/dist/web-editor/client/dom.d.ts.map +0 -1
- package/dist/web-editor/client/dom.js +0 -30
- package/dist/web-editor/client/dom.js.map +0 -1
- package/dist/web-editor/client/inspector.d.ts +0 -22
- package/dist/web-editor/client/inspector.d.ts.map +0 -1
- package/dist/web-editor/client/inspector.js +0 -226
- package/dist/web-editor/client/inspector.js.map +0 -1
- package/dist/web-editor/client/main.d.ts +0 -2
- package/dist/web-editor/client/main.d.ts.map +0 -1
- package/dist/web-editor/client/main.js +0 -1468
- package/dist/web-editor/client/main.js.map +0 -1
- package/dist/web-editor/client/policy-editor.d.ts +0 -16
- package/dist/web-editor/client/policy-editor.d.ts.map +0 -1
- package/dist/web-editor/client/policy-editor.js +0 -330
- package/dist/web-editor/client/policy-editor.js.map +0 -1
- package/dist/web-editor/client/regex-editor.d.ts +0 -19
- package/dist/web-editor/client/regex-editor.d.ts.map +0 -1
- package/dist/web-editor/client/regex-editor.js +0 -281
- package/dist/web-editor/client/regex-editor.js.map +0 -1
- package/dist/web-editor/client/types.d.ts +0 -60
- package/dist/web-editor/client/types.d.ts.map +0 -1
- package/dist/web-editor/client/types.js +0 -2
- package/dist/web-editor/client/types.js.map +0 -1
|
@@ -1,1468 +0,0 @@
|
|
|
1
|
-
import { createEditorApi, EditorApiError } from "./api.js";
|
|
2
|
-
import { attr, el, escapeHtml, query, queryAll } from "./dom.js";
|
|
3
|
-
import { createInspector } from "./inspector.js";
|
|
4
|
-
import { createPolicyEditor } from "./policy-editor.js";
|
|
5
|
-
import { createRegexEditor } from "./regex-editor.js";
|
|
6
|
-
const token = new URLSearchParams(location.search).get("token") || "";
|
|
7
|
-
const api = createEditorApi(token);
|
|
8
|
-
let stacks = [];
|
|
9
|
-
let cwd = "";
|
|
10
|
-
let selectedId = "";
|
|
11
|
-
let currentStack = null;
|
|
12
|
-
let currentFilePath = "";
|
|
13
|
-
let selectedItemIndex = -1;
|
|
14
|
-
let dirty = false;
|
|
15
|
-
let dragIndex = -1;
|
|
16
|
-
let dragDropIndex = -1;
|
|
17
|
-
let dragScrollFrame = 0;
|
|
18
|
-
let dragScrollSpeed = 0;
|
|
19
|
-
let dragClientY = 0;
|
|
20
|
-
let optionsText = "";
|
|
21
|
-
let optionsError = "";
|
|
22
|
-
let sidebarCollapsed = false;
|
|
23
|
-
let slotOptionsMode = "form";
|
|
24
|
-
let policyResources = { tools: [], skills: [] };
|
|
25
|
-
let latestDiagnostics = [];
|
|
26
|
-
let stackVariablesError = "";
|
|
27
|
-
let activeTab = "items";
|
|
28
|
-
let metadataCollapsed = true;
|
|
29
|
-
let currentTheme = readStoredTheme() || (window.matchMedia?.("(prefers-color-scheme: dark)")?.matches ? "dark" : "light");
|
|
30
|
-
const policyEditor = createPolicyEditor({
|
|
31
|
-
getStack: () => currentStack,
|
|
32
|
-
getResources: () => policyResources,
|
|
33
|
-
markDirty,
|
|
34
|
-
setStatus,
|
|
35
|
-
});
|
|
36
|
-
const { validateStack, previewStack, refreshPayloadCapture, armPayloadCapture, clearPayloadCapture, openPayloadCapture, hidePreview, copyPreviewText, copyTextToClipboard, } = createInspector({
|
|
37
|
-
api,
|
|
38
|
-
getSelectedId: () => selectedId,
|
|
39
|
-
stackForSubmit,
|
|
40
|
-
renderDiagnostics,
|
|
41
|
-
renderItemList,
|
|
42
|
-
setStatus,
|
|
43
|
-
});
|
|
44
|
-
const regexEditor = createRegexEditor({
|
|
45
|
-
getStack: () => currentStack,
|
|
46
|
-
markDirty,
|
|
47
|
-
setStatus,
|
|
48
|
-
showModal: showStackModal,
|
|
49
|
-
run,
|
|
50
|
-
validateStack,
|
|
51
|
-
});
|
|
52
|
-
const slotNames = [
|
|
53
|
-
"chat-history", "tools", "tool-guidelines", "skills", "project-context",
|
|
54
|
-
"append-system-prompt", "variables", "date", "cwd", "date-cwd",
|
|
55
|
-
"active-model", "pi-docs"
|
|
56
|
-
];
|
|
57
|
-
const roles = ["", "system", "user", "assistant", "custom"];
|
|
58
|
-
function applyTheme(theme) {
|
|
59
|
-
currentTheme = theme === "dark" ? "dark" : "light";
|
|
60
|
-
document.body.dataset.theme = currentTheme;
|
|
61
|
-
const button = el("themeBtn");
|
|
62
|
-
if (button) {
|
|
63
|
-
button.textContent = currentTheme === "dark" ? "Light" : "Dark";
|
|
64
|
-
button.title = currentTheme === "dark" ? "Switch to light theme" : "Switch to dark theme";
|
|
65
|
-
}
|
|
66
|
-
}
|
|
67
|
-
function toggleTheme() {
|
|
68
|
-
const next = currentTheme === "dark" ? "light" : "dark";
|
|
69
|
-
writeStoredTheme(next);
|
|
70
|
-
applyTheme(next);
|
|
71
|
-
setStatus(next === "dark" ? "Dark theme enabled" : "Light theme enabled", "success");
|
|
72
|
-
}
|
|
73
|
-
applyTheme(currentTheme);
|
|
74
|
-
function readStoredTheme() {
|
|
75
|
-
try {
|
|
76
|
-
const theme = localStorage.getItem("pi-forge-theme");
|
|
77
|
-
return theme === "light" || theme === "dark" ? theme : "";
|
|
78
|
-
}
|
|
79
|
-
catch {
|
|
80
|
-
return "";
|
|
81
|
-
}
|
|
82
|
-
}
|
|
83
|
-
function writeStoredTheme(theme) {
|
|
84
|
-
try {
|
|
85
|
-
localStorage.setItem("pi-forge-theme", theme);
|
|
86
|
-
}
|
|
87
|
-
catch {
|
|
88
|
-
// Ignore storage failures; the current page can still switch themes.
|
|
89
|
-
}
|
|
90
|
-
}
|
|
91
|
-
function setStatus(text, tone = "") {
|
|
92
|
-
el("status").textContent = text;
|
|
93
|
-
el("status").style.color = tone === "error" ? "var(--error)" : tone === "success" ? "var(--success)" : "var(--muted)";
|
|
94
|
-
}
|
|
95
|
-
function markDirty() {
|
|
96
|
-
dirty = true;
|
|
97
|
-
renderDirtyState();
|
|
98
|
-
setStatus("Unsaved changes");
|
|
99
|
-
}
|
|
100
|
-
function renderDirtyState() {
|
|
101
|
-
const badge = el("dirtyBadge");
|
|
102
|
-
if (!badge)
|
|
103
|
-
return;
|
|
104
|
-
badge.classList.toggle("visible", dirty);
|
|
105
|
-
updateActionState();
|
|
106
|
-
}
|
|
107
|
-
function updateActionState() {
|
|
108
|
-
const hasStack = !!currentStack;
|
|
109
|
-
for (const id of ["activateBtn", "saveBtn", "validateBtn", "previewBtn", "forkBtn", "exportBtn", "deleteStackBtn", "addItemBtn", "addSlotBtn", "metadataToggleBtn"]) {
|
|
110
|
-
const button = el(id);
|
|
111
|
-
if (button)
|
|
112
|
-
button.disabled = !hasStack;
|
|
113
|
-
}
|
|
114
|
-
const deleteItemButton = el("deleteItemBtn");
|
|
115
|
-
if (deleteItemButton)
|
|
116
|
-
deleteItemButton.disabled = !hasStack || selectedItemIndex < 0;
|
|
117
|
-
document.querySelectorAll("[data-tab]").forEach((button) => {
|
|
118
|
-
button.disabled = !hasStack;
|
|
119
|
-
});
|
|
120
|
-
}
|
|
121
|
-
async function loadStacks(preferId = selectedId) {
|
|
122
|
-
const [data, resources] = await Promise.all([
|
|
123
|
-
api("/api/stacks"),
|
|
124
|
-
api("/api/resources"),
|
|
125
|
-
]);
|
|
126
|
-
stacks = data.stacks || [];
|
|
127
|
-
policyResources = normalizePolicyResources(resources);
|
|
128
|
-
cwd = data.cwd || "";
|
|
129
|
-
el("cwd").textContent = cwd;
|
|
130
|
-
renderStackList();
|
|
131
|
-
const next = stacks.find((stack) => stack.id === preferId) || stacks.find((stack) => stack.active) || stacks[0];
|
|
132
|
-
if (next)
|
|
133
|
-
await selectStack(next.id, { keepDirty: false });
|
|
134
|
-
else
|
|
135
|
-
renderEmpty();
|
|
136
|
-
}
|
|
137
|
-
async function selectStack(id, options = {}) {
|
|
138
|
-
if (dirty && !options.keepDirty && !confirm("Discard unsaved changes?"))
|
|
139
|
-
return;
|
|
140
|
-
const data = await api("/api/stacks/" + encodeURIComponent(id));
|
|
141
|
-
selectedId = id;
|
|
142
|
-
const loadedStack = structuredClone(data.stack);
|
|
143
|
-
currentStack = loadedStack;
|
|
144
|
-
currentFilePath = data.filePath || "";
|
|
145
|
-
selectedItemIndex = loadedStack.items.length ? 0 : -1;
|
|
146
|
-
dirty = false;
|
|
147
|
-
optionsError = "";
|
|
148
|
-
stackVariablesError = "";
|
|
149
|
-
regexEditor.reset();
|
|
150
|
-
policyEditor.reset();
|
|
151
|
-
renderDirtyState();
|
|
152
|
-
renderAll(data.diagnostics || []);
|
|
153
|
-
setStatus("Loaded " + loadedStack.id);
|
|
154
|
-
}
|
|
155
|
-
function renderAll(diagnostics = []) {
|
|
156
|
-
latestDiagnostics = diagnostics;
|
|
157
|
-
renderStackList();
|
|
158
|
-
renderSettings();
|
|
159
|
-
renderActiveTab();
|
|
160
|
-
renderDiagnostics(diagnostics);
|
|
161
|
-
hidePreview();
|
|
162
|
-
updateActionState();
|
|
163
|
-
}
|
|
164
|
-
function renderActiveTab() {
|
|
165
|
-
document.querySelectorAll("[data-tab]").forEach((button) => {
|
|
166
|
-
button.classList.toggle("active", button.dataset.tab === activeTab);
|
|
167
|
-
});
|
|
168
|
-
const workspace = el("workspace");
|
|
169
|
-
const panel = el("tabPanel");
|
|
170
|
-
if (activeTab === "items") {
|
|
171
|
-
workspace.style.display = "";
|
|
172
|
-
panel.classList.remove("open");
|
|
173
|
-
panel.innerHTML = "";
|
|
174
|
-
renderItemList();
|
|
175
|
-
renderItemEditor();
|
|
176
|
-
return;
|
|
177
|
-
}
|
|
178
|
-
workspace.style.display = "none";
|
|
179
|
-
panel.classList.add("open");
|
|
180
|
-
if (activeTab === "regex")
|
|
181
|
-
renderRegexTab();
|
|
182
|
-
else if (activeTab === "policy")
|
|
183
|
-
policyEditor.renderTab();
|
|
184
|
-
else if (activeTab === "stack")
|
|
185
|
-
renderStackTab();
|
|
186
|
-
}
|
|
187
|
-
function renderStackList() {
|
|
188
|
-
const list = el("stackList");
|
|
189
|
-
list.innerHTML = "";
|
|
190
|
-
if (!stacks.length) {
|
|
191
|
-
list.innerHTML = '<div class="side-empty">No prompt stacks in this project.</div>';
|
|
192
|
-
return;
|
|
193
|
-
}
|
|
194
|
-
for (const stack of stacks) {
|
|
195
|
-
const row = document.createElement("button");
|
|
196
|
-
row.className = "stack-row" + (stack.active ? " active" : "") + (stack.id === selectedId ? " selected" : "");
|
|
197
|
-
const diag = stack.errors ? '<span class="badge error">' + stack.errors + ' error</span>' : stack.warnings ? '<span class="badge warning">' + stack.warnings + ' warning</span>' : "";
|
|
198
|
-
row.innerHTML = '<div class="stack-name">' + escapeHtml(stack.id) + (stack.active ? '<span class="badge">active</span>' : '') + diag + '</div>' +
|
|
199
|
-
'<div class="stack-meta">' + escapeHtml(stack.name || "(unnamed)") + '</div>' +
|
|
200
|
-
'<div class="stack-meta">' + stack.itemCount + ' items | ' + escapeHtml(stack.mode || "replace") + '</div>';
|
|
201
|
-
row.onclick = () => selectStack(stack.id);
|
|
202
|
-
list.appendChild(row);
|
|
203
|
-
}
|
|
204
|
-
}
|
|
205
|
-
function normalizePolicyResources(value) {
|
|
206
|
-
return {
|
|
207
|
-
tools: normalizeResourceList(value?.tools),
|
|
208
|
-
skills: normalizeResourceList(value?.skills),
|
|
209
|
-
};
|
|
210
|
-
}
|
|
211
|
-
function normalizeResourceList(value) {
|
|
212
|
-
if (!Array.isArray(value))
|
|
213
|
-
return [];
|
|
214
|
-
return value
|
|
215
|
-
.filter((resource) => resource && typeof resource === "object" && typeof resource.name === "string" && resource.name.trim())
|
|
216
|
-
.map((resource) => ({
|
|
217
|
-
name: resource.name.trim(),
|
|
218
|
-
description: typeof resource.description === "string" ? resource.description : "",
|
|
219
|
-
source: typeof resource.source === "string" ? resource.source : "",
|
|
220
|
-
active: resource.active === true,
|
|
221
|
-
hidden: resource.hidden === true,
|
|
222
|
-
}));
|
|
223
|
-
}
|
|
224
|
-
function renderSettings() {
|
|
225
|
-
const settings = el("settings");
|
|
226
|
-
if (!currentStack) {
|
|
227
|
-
settings.innerHTML = "";
|
|
228
|
-
el("metadataSummary").textContent = "";
|
|
229
|
-
return;
|
|
230
|
-
}
|
|
231
|
-
const stack = currentStack;
|
|
232
|
-
el("metadataPanel").style.display = "";
|
|
233
|
-
el("metadataSummary").textContent = [
|
|
234
|
-
currentStack.id || "(no id)",
|
|
235
|
-
currentStack.name || "(unnamed)",
|
|
236
|
-
currentStack.mode || "replace",
|
|
237
|
-
currentFilePath || "",
|
|
238
|
-
].filter(Boolean).join(" | ");
|
|
239
|
-
settings.style.display = metadataCollapsed ? "none" : "grid";
|
|
240
|
-
el("metadataToggleBtn").textContent = metadataCollapsed ? "Show metadata" : "Hide metadata";
|
|
241
|
-
el("metadataToggleBtn").dataset.icon = metadataCollapsed ? "▸" : "▾";
|
|
242
|
-
settings.innerHTML = [
|
|
243
|
-
field("Stack ID", '<input id="stackId" value="' + attr(stack.id) + '" readonly title="Stack IDs are immutable; use Fork to create a new ID.">'),
|
|
244
|
-
field("Name", '<input id="stackName" value="' + attr(stack.name || "") + '">'),
|
|
245
|
-
field("Mode", '<select id="stackMode"><option value="replace">replace</option><option value="append">append</option><option value="prepend">prepend</option></select>'),
|
|
246
|
-
field("Auto activate", '<label class="checkline"><input id="stackAuto" type="checkbox"> enabled</label>'),
|
|
247
|
-
field("Description", '<textarea id="stackDescription" class="wide">' + escapeHtml(stack.description || "") + '</textarea>', "wide"),
|
|
248
|
-
field("File", '<input value="' + attr(currentFilePath) + '" disabled>', "wide"),
|
|
249
|
-
].join("");
|
|
250
|
-
el("stackMode").value = stack.mode || "replace";
|
|
251
|
-
el("stackAuto").checked = stack.autoActivate === true;
|
|
252
|
-
el("stackName").oninput = (event) => { setOptionalString(stack, "name", event.target.value); markDirty(); };
|
|
253
|
-
el("stackMode").onchange = (event) => { stack.mode = event.target.value; markDirty(); };
|
|
254
|
-
el("stackAuto").onchange = (event) => { stack.autoActivate = event.target.checked; markDirty(); };
|
|
255
|
-
el("stackDescription").oninput = (event) => { setOptionalString(stack, "description", event.target.value); markDirty(); };
|
|
256
|
-
}
|
|
257
|
-
function toggleMetadata() {
|
|
258
|
-
metadataCollapsed = !metadataCollapsed;
|
|
259
|
-
renderSettings();
|
|
260
|
-
setStatus(metadataCollapsed ? "Stack metadata hidden" : "Stack metadata shown");
|
|
261
|
-
}
|
|
262
|
-
function renderItemList() {
|
|
263
|
-
const list = el("itemList");
|
|
264
|
-
list.innerHTML = "";
|
|
265
|
-
list.classList.toggle("drag-active", dragIndex !== -1);
|
|
266
|
-
if (!currentStack)
|
|
267
|
-
return;
|
|
268
|
-
el("itemCount").textContent = currentStack.items.length + " total";
|
|
269
|
-
list.ondragover = handleItemListDragOver;
|
|
270
|
-
list.ondrop = handleItemListDrop;
|
|
271
|
-
const diagnosticsByItem = diagnosticsForItems();
|
|
272
|
-
currentStack.items.forEach((item, index) => {
|
|
273
|
-
const row = document.createElement("div");
|
|
274
|
-
row.className = "item-row" + (index === selectedItemIndex ? " selected" : "") + (item.enabled === false ? " disabled" : "");
|
|
275
|
-
row.dataset.itemIndex = String(index);
|
|
276
|
-
row.draggable = true;
|
|
277
|
-
const enabled = item.enabled !== false;
|
|
278
|
-
const itemDiagnostics = diagnosticsByItem[item.id] || [];
|
|
279
|
-
const errors = itemDiagnostics.filter((diag) => diag.level === "error").length;
|
|
280
|
-
const warnings = itemDiagnostics.filter((diag) => diag.level === "warning").length;
|
|
281
|
-
const diagBadge = errors
|
|
282
|
-
? '<span class="item-badge error" title="' + attr(diagnosticTitle(itemDiagnostics)) + '">' + errors + 'E</span>'
|
|
283
|
-
: warnings
|
|
284
|
-
? '<span class="item-badge warning" title="' + attr(diagnosticTitle(itemDiagnostics)) + '">' + warnings + 'W</span>'
|
|
285
|
-
: "";
|
|
286
|
-
row.innerHTML = '<div class="drag-handle" title="Drag to reorder">≡</div>' +
|
|
287
|
-
'<div><div class="item-title">' + escapeHtml(displayItemName(item)) + diagBadge + '</div>' +
|
|
288
|
-
'<div class="item-meta">' + escapeHtml(item.kind) + ' | id: ' + escapeHtml(item.id) + (item.role ? " | " + escapeHtml(item.role) : "") + (item.kind === "slot" ? " | " + escapeHtml(item.slot || "") : "") + '</div></div>' +
|
|
289
|
-
'<button type="button" class="item-toggle ' + (enabled ? "enabled" : "disabled") + '" title="Toggle item">' + (enabled ? "On" : "Off") + '</button>';
|
|
290
|
-
row.onclick = (event) => {
|
|
291
|
-
if (event.target?.classList?.contains("item-toggle"))
|
|
292
|
-
return;
|
|
293
|
-
selectedItemIndex = index;
|
|
294
|
-
renderItemList();
|
|
295
|
-
renderItemEditor();
|
|
296
|
-
};
|
|
297
|
-
query(row, ".item-toggle").onclick = (event) => {
|
|
298
|
-
event.stopPropagation();
|
|
299
|
-
item.enabled = item.enabled === false;
|
|
300
|
-
selectedItemIndex = index;
|
|
301
|
-
markDirty();
|
|
302
|
-
renderItemList();
|
|
303
|
-
renderItemEditor();
|
|
304
|
-
};
|
|
305
|
-
row.ondragstart = (event) => {
|
|
306
|
-
dragIndex = index;
|
|
307
|
-
dragDropIndex = index;
|
|
308
|
-
row.classList.add("dragging");
|
|
309
|
-
list.classList.add("drag-active");
|
|
310
|
-
if (event.dataTransfer) {
|
|
311
|
-
event.dataTransfer.effectAllowed = "move";
|
|
312
|
-
event.dataTransfer.setData("text/plain", item.id || String(index));
|
|
313
|
-
}
|
|
314
|
-
updateItemDropIndicator();
|
|
315
|
-
};
|
|
316
|
-
row.ondragend = finishItemDrag;
|
|
317
|
-
row.ondragover = handleItemListDragOver;
|
|
318
|
-
row.ondrop = handleItemListDrop;
|
|
319
|
-
list.appendChild(row);
|
|
320
|
-
});
|
|
321
|
-
updateItemDropIndicator();
|
|
322
|
-
}
|
|
323
|
-
function handleItemListDragOver(event) {
|
|
324
|
-
if (dragIndex === -1 || !currentStack)
|
|
325
|
-
return;
|
|
326
|
-
event.preventDefault();
|
|
327
|
-
dragClientY = event.clientY;
|
|
328
|
-
updateItemDragAutoScroll(event.clientY);
|
|
329
|
-
setItemDropIndex(dropIndexFromClientY(event.clientY));
|
|
330
|
-
}
|
|
331
|
-
function handleItemListDrop(event) {
|
|
332
|
-
if (dragIndex === -1 || !currentStack)
|
|
333
|
-
return;
|
|
334
|
-
event.preventDefault();
|
|
335
|
-
dropDraggedItem();
|
|
336
|
-
}
|
|
337
|
-
function dropDraggedItem() {
|
|
338
|
-
if (!currentStack || dragIndex < 0 || dragIndex >= currentStack.items.length) {
|
|
339
|
-
finishItemDrag();
|
|
340
|
-
return;
|
|
341
|
-
}
|
|
342
|
-
let insertIndex = dragDropIndex;
|
|
343
|
-
if (insertIndex < 0)
|
|
344
|
-
insertIndex = dragIndex;
|
|
345
|
-
insertIndex = Math.max(0, Math.min(insertIndex, currentStack.items.length));
|
|
346
|
-
const [moved] = currentStack.items.splice(dragIndex, 1);
|
|
347
|
-
if (!moved) {
|
|
348
|
-
finishItemDrag();
|
|
349
|
-
return;
|
|
350
|
-
}
|
|
351
|
-
if (dragIndex < insertIndex)
|
|
352
|
-
insertIndex--;
|
|
353
|
-
insertIndex = Math.max(0, Math.min(insertIndex, currentStack.items.length));
|
|
354
|
-
currentStack.items.splice(insertIndex, 0, moved);
|
|
355
|
-
selectedItemIndex = insertIndex;
|
|
356
|
-
const changed = dragIndex !== insertIndex;
|
|
357
|
-
finishItemDrag(false);
|
|
358
|
-
if (changed)
|
|
359
|
-
markDirty();
|
|
360
|
-
renderItemList();
|
|
361
|
-
renderItemEditor();
|
|
362
|
-
}
|
|
363
|
-
function setItemDropIndex(index) {
|
|
364
|
-
if (!currentStack)
|
|
365
|
-
return;
|
|
366
|
-
const next = Math.max(0, Math.min(index, currentStack.items.length));
|
|
367
|
-
if (dragDropIndex === next)
|
|
368
|
-
return;
|
|
369
|
-
dragDropIndex = next;
|
|
370
|
-
updateItemDropIndicator();
|
|
371
|
-
}
|
|
372
|
-
function dropIndexFromClientY(clientY) {
|
|
373
|
-
const list = el("itemList");
|
|
374
|
-
const rows = [...queryAll(list, ".item-row")];
|
|
375
|
-
if (!rows.length)
|
|
376
|
-
return 0;
|
|
377
|
-
const listRect = list.getBoundingClientRect();
|
|
378
|
-
if (clientY <= listRect.top)
|
|
379
|
-
return 0;
|
|
380
|
-
if (clientY >= listRect.bottom)
|
|
381
|
-
return rows.length;
|
|
382
|
-
for (const row of rows) {
|
|
383
|
-
const rect = row.getBoundingClientRect();
|
|
384
|
-
const index = Number(row.dataset.itemIndex);
|
|
385
|
-
if (clientY < rect.top + rect.height / 2)
|
|
386
|
-
return index;
|
|
387
|
-
}
|
|
388
|
-
return rows.length;
|
|
389
|
-
}
|
|
390
|
-
function updateItemDropIndicator() {
|
|
391
|
-
const list = el("itemList");
|
|
392
|
-
const rows = [...list.querySelectorAll(".item-row")];
|
|
393
|
-
for (const row of rows)
|
|
394
|
-
row.classList.remove("drop-before", "drop-after");
|
|
395
|
-
list.classList.toggle("drag-active", dragIndex !== -1);
|
|
396
|
-
if (dragIndex === -1 || dragDropIndex === -1 || !rows.length)
|
|
397
|
-
return;
|
|
398
|
-
if (dragDropIndex <= 0)
|
|
399
|
-
rows[0].classList.add("drop-before");
|
|
400
|
-
else if (dragDropIndex >= rows.length)
|
|
401
|
-
rows[rows.length - 1].classList.add("drop-after");
|
|
402
|
-
else
|
|
403
|
-
rows[dragDropIndex].classList.add("drop-before");
|
|
404
|
-
}
|
|
405
|
-
function updateItemDragAutoScroll(clientY) {
|
|
406
|
-
const list = el("itemList");
|
|
407
|
-
const rect = list.getBoundingClientRect();
|
|
408
|
-
const edge = Math.min(72, Math.max(36, rect.height / 5));
|
|
409
|
-
let speed = 0;
|
|
410
|
-
if (clientY < rect.top)
|
|
411
|
-
speed = -Math.min(30, 8 + (rect.top - clientY) / 3);
|
|
412
|
-
else if (clientY < rect.top + edge)
|
|
413
|
-
speed = -Math.min(22, (rect.top + edge - clientY) / 3);
|
|
414
|
-
else if (clientY > rect.bottom)
|
|
415
|
-
speed = Math.min(30, 8 + (clientY - rect.bottom) / 3);
|
|
416
|
-
else if (clientY > rect.bottom - edge)
|
|
417
|
-
speed = Math.min(22, (clientY - (rect.bottom - edge)) / 3);
|
|
418
|
-
dragScrollSpeed = speed;
|
|
419
|
-
if (speed !== 0 && !dragScrollFrame)
|
|
420
|
-
dragScrollFrame = requestAnimationFrame(runItemDragAutoScroll);
|
|
421
|
-
}
|
|
422
|
-
function runItemDragAutoScroll() {
|
|
423
|
-
dragScrollFrame = 0;
|
|
424
|
-
if (dragIndex === -1 || dragScrollSpeed === 0)
|
|
425
|
-
return;
|
|
426
|
-
const list = el("itemList");
|
|
427
|
-
list.scrollTop += dragScrollSpeed;
|
|
428
|
-
setItemDropIndex(dropIndexFromClientY(dragClientY));
|
|
429
|
-
dragScrollFrame = requestAnimationFrame(runItemDragAutoScroll);
|
|
430
|
-
}
|
|
431
|
-
function finishItemDrag(clearIndicator = true) {
|
|
432
|
-
dragIndex = -1;
|
|
433
|
-
dragDropIndex = -1;
|
|
434
|
-
dragScrollSpeed = 0;
|
|
435
|
-
dragClientY = 0;
|
|
436
|
-
if (dragScrollFrame) {
|
|
437
|
-
cancelAnimationFrame(dragScrollFrame);
|
|
438
|
-
dragScrollFrame = 0;
|
|
439
|
-
}
|
|
440
|
-
if (clearIndicator)
|
|
441
|
-
updateItemDropIndicator();
|
|
442
|
-
}
|
|
443
|
-
function handleDocumentItemDragOver(event) {
|
|
444
|
-
if (dragIndex === -1 || !currentStack)
|
|
445
|
-
return;
|
|
446
|
-
event.preventDefault();
|
|
447
|
-
dragClientY = event.clientY;
|
|
448
|
-
updateItemDragAutoScroll(event.clientY);
|
|
449
|
-
setItemDropIndex(dropIndexFromClientY(event.clientY));
|
|
450
|
-
}
|
|
451
|
-
function handleDocumentItemDrop(event) {
|
|
452
|
-
if (dragIndex === -1 || !currentStack)
|
|
453
|
-
return;
|
|
454
|
-
event.preventDefault();
|
|
455
|
-
dropDraggedItem();
|
|
456
|
-
}
|
|
457
|
-
function diagnosticsForItems() {
|
|
458
|
-
const grouped = {};
|
|
459
|
-
for (const diagnostic of latestDiagnostics || []) {
|
|
460
|
-
if (!diagnostic.itemId)
|
|
461
|
-
continue;
|
|
462
|
-
if (!grouped[diagnostic.itemId])
|
|
463
|
-
grouped[diagnostic.itemId] = [];
|
|
464
|
-
grouped[diagnostic.itemId].push(diagnostic);
|
|
465
|
-
}
|
|
466
|
-
return grouped;
|
|
467
|
-
}
|
|
468
|
-
function diagnosticTitle(diagnostics) {
|
|
469
|
-
return diagnostics.map((diag) => (diag.level || "info").toUpperCase() + ": " + (diag.message || "")).join("\n");
|
|
470
|
-
}
|
|
471
|
-
function renderItemEditor() {
|
|
472
|
-
const editor = el("itemEditor");
|
|
473
|
-
if (!currentStack || selectedItemIndex < 0 || !currentStack.items[selectedItemIndex]) {
|
|
474
|
-
editor.innerHTML = '<div class="empty">No item selected.</div>';
|
|
475
|
-
el("deleteItemBtn").disabled = true;
|
|
476
|
-
return;
|
|
477
|
-
}
|
|
478
|
-
el("deleteItemBtn").disabled = false;
|
|
479
|
-
const stack = currentStack;
|
|
480
|
-
const item = stack.items[selectedItemIndex];
|
|
481
|
-
optionsText = JSON.stringify(item.options || {}, null, 2);
|
|
482
|
-
optionsError = "";
|
|
483
|
-
const slotSelect = '<select id="itemSlot">' + slotNames.map((slot) => '<option value="' + attr(slot) + '">' + escapeHtml(slot) + '</option>').join("") + '</select>';
|
|
484
|
-
const roleSelect = '<select id="itemRole">' + roles.map((role) => '<option value="' + attr(role) + '">' + escapeHtml(role || "(none)") + '</option>').join("") + '</select>';
|
|
485
|
-
const kindSelect = '<select id="itemKind"><option value="block">block</option><option value="slot">slot</option></select>';
|
|
486
|
-
const topFields = '<div class="item-fields">' +
|
|
487
|
-
field("Kind", kindSelect) +
|
|
488
|
-
field("ID", '<input id="itemId" value="' + attr(item.id) + '">') +
|
|
489
|
-
field("Name", '<input id="itemName" value="' + attr(item.name || "") + '">') +
|
|
490
|
-
field("Role", roleSelect) +
|
|
491
|
-
(item.kind === "slot" ? field("Slot", slotSelect) : "") +
|
|
492
|
-
'</div>';
|
|
493
|
-
const body = item.kind === "block"
|
|
494
|
-
? field("Content", '<textarea id="itemContent">' + escapeHtml(item.content || "") + '</textarea>', "content-field")
|
|
495
|
-
: renderSlotOptionsEditor(item);
|
|
496
|
-
editor.innerHTML = '<div class="item-form">' + topFields + '<div class="item-body">' + body + '</div></div>';
|
|
497
|
-
el("itemKind").value = item.kind;
|
|
498
|
-
el("itemRole").value = item.role || "";
|
|
499
|
-
if (item.kind === "slot")
|
|
500
|
-
el("itemSlot").value = item.slot || "chat-history";
|
|
501
|
-
el("itemKind").onchange = (event) => {
|
|
502
|
-
if (event.target.value === item.kind)
|
|
503
|
-
return;
|
|
504
|
-
const base = { id: item.id, name: item.name, enabled: item.enabled, role: item.role, tags: item.tags, source: item.source };
|
|
505
|
-
stack.items[selectedItemIndex] = event.target.value === "slot"
|
|
506
|
-
? { ...base, kind: "slot", slot: "chat-history" }
|
|
507
|
-
: { ...base, kind: "block", content: "" };
|
|
508
|
-
markDirty();
|
|
509
|
-
renderItemList();
|
|
510
|
-
renderItemEditor();
|
|
511
|
-
};
|
|
512
|
-
el("itemId").oninput = (event) => { item.id = event.target.value; markDirty(); renderItemList(); };
|
|
513
|
-
el("itemName").oninput = (event) => { setOptionalString(item, "name", event.target.value); markDirty(); };
|
|
514
|
-
el("itemRole").onchange = (event) => { setOptionalString(item, "role", event.target.value); markDirty(); renderItemList(); };
|
|
515
|
-
if (item.kind === "block") {
|
|
516
|
-
el("itemContent").oninput = (event) => { item.content = event.target.value; markDirty(); };
|
|
517
|
-
}
|
|
518
|
-
else {
|
|
519
|
-
el("itemSlot").onchange = (event) => { item.slot = event.target.value; markDirty(); renderItemList(); };
|
|
520
|
-
bindSlotOptionsEditor(item);
|
|
521
|
-
}
|
|
522
|
-
}
|
|
523
|
-
function renderSlotOptionsEditor(item) {
|
|
524
|
-
const options = item.options || {};
|
|
525
|
-
const jsonActive = slotOptionsMode === "json";
|
|
526
|
-
const formButton = '<button id="slotOptionsFormBtn" type="button" class="' + (!jsonActive ? "active" : "") + '">Form</button>';
|
|
527
|
-
const jsonButton = '<button id="slotOptionsJsonBtn" type="button" class="' + (jsonActive ? "active" : "") + '">JSON</button>';
|
|
528
|
-
const body = jsonActive
|
|
529
|
-
? '<textarea id="itemOptions" class="json-options">' + escapeHtml(optionsText) + '</textarea>'
|
|
530
|
-
: renderSlotOptionsForm(item, options);
|
|
531
|
-
return '<div class="field wide slot-options"><label>Slot options</label><div class="segmented">' + formButton + jsonButton + '</div>' + body + '</div>';
|
|
532
|
-
}
|
|
533
|
-
function renderSlotOptionsForm(item, options) {
|
|
534
|
-
const fields = [];
|
|
535
|
-
if (item.slot === "chat-history") {
|
|
536
|
-
fields.push(optionCheckbox("includeLastUserMessage", "Include last user message", options.includeLastUserMessage !== false), optionCheckbox("stripAssistantThinking", "Strip assistant thinking", options.stripAssistantThinking === true), optionCheckbox("includeSummaries", "Include summaries", options.includeSummaries !== false), optionSelect("toolMode", "Tool history", options.toolMode || "keep", ["keep", "drop"]), optionText("roles", "Roles", Array.isArray(options.roles) ? options.roles.join(", ") : ""), optionNumber("maxMessages", "Max messages", options.maxMessages ?? ""), optionNumber("maxChars", "Max chars", options.maxChars ?? ""));
|
|
537
|
-
}
|
|
538
|
-
if (item.slot === "variables") {
|
|
539
|
-
fields.push(optionCheckbox("includeStatic", "Include static variables", options.includeStatic !== false), optionCheckbox("includeSession", "Include session variables", options.includeSession !== false), optionCheckbox("includeTurn", "Include turn variables", options.includeTurn !== false), optionSelect("format", "Format", options.format || "xml", ["xml", "plain"]));
|
|
540
|
-
}
|
|
541
|
-
if (["tools", "tool-guidelines", "skills", "project-context"].includes(item.slot)) {
|
|
542
|
-
fields.push(optionSelect("format", "Format", options.format || "xml", ["xml", "plain"]));
|
|
543
|
-
}
|
|
544
|
-
if (["date", "date-cwd"].includes(item.slot)) {
|
|
545
|
-
fields.push(optionCheckbox("includeTime", "Include current time", options.includeTime === true));
|
|
546
|
-
}
|
|
547
|
-
if (fields.length === 0) {
|
|
548
|
-
fields.push('<div class="wide option-note">This slot has no structured options yet. Use JSON mode for advanced settings.</div>');
|
|
549
|
-
}
|
|
550
|
-
fields.push('<div class="wide option-note">Unknown option keys are preserved. Use JSON mode for advanced settings.</div>');
|
|
551
|
-
return '<div class="options-grid">' + fields.join("") + '</div>';
|
|
552
|
-
}
|
|
553
|
-
function bindSlotOptionsEditor(item) {
|
|
554
|
-
el("slotOptionsFormBtn").onclick = () => {
|
|
555
|
-
slotOptionsMode = "form";
|
|
556
|
-
renderItemEditor();
|
|
557
|
-
};
|
|
558
|
-
el("slotOptionsJsonBtn").onclick = () => {
|
|
559
|
-
slotOptionsMode = "json";
|
|
560
|
-
renderItemEditor();
|
|
561
|
-
};
|
|
562
|
-
if (slotOptionsMode === "json") {
|
|
563
|
-
el("itemOptions").oninput = (event) => {
|
|
564
|
-
optionsText = event.target.value;
|
|
565
|
-
try {
|
|
566
|
-
const parsed = optionsText.trim() ? JSON.parse(optionsText) : {};
|
|
567
|
-
item.options = Object.keys(parsed).length ? parsed : undefined;
|
|
568
|
-
optionsError = "";
|
|
569
|
-
markDirty();
|
|
570
|
-
}
|
|
571
|
-
catch (error) {
|
|
572
|
-
optionsError = error instanceof Error ? error.message : String(error);
|
|
573
|
-
setStatus("Invalid item options JSON", "error");
|
|
574
|
-
}
|
|
575
|
-
};
|
|
576
|
-
return;
|
|
577
|
-
}
|
|
578
|
-
document.querySelectorAll("[data-option]").forEach((control) => {
|
|
579
|
-
control.onchange = (event) => {
|
|
580
|
-
const target = event.target;
|
|
581
|
-
const key = target.dataset.option;
|
|
582
|
-
if (!key)
|
|
583
|
-
return;
|
|
584
|
-
if (target.type === "checkbox") {
|
|
585
|
-
setSlotOption(item, key, target.checked, defaultSlotOptionValue(key));
|
|
586
|
-
}
|
|
587
|
-
else if (target.type === "number") {
|
|
588
|
-
const value = target.value.trim();
|
|
589
|
-
setSlotOption(item, key, value ? Number(value) : undefined);
|
|
590
|
-
}
|
|
591
|
-
else if (target.dataset.array === "true") {
|
|
592
|
-
const values = target.value.split(",").map((part) => part.trim()).filter(Boolean);
|
|
593
|
-
setSlotOption(item, key, values.length ? values : undefined);
|
|
594
|
-
}
|
|
595
|
-
else {
|
|
596
|
-
setSlotOption(item, key, target.value || undefined, defaultSlotOptionValue(key));
|
|
597
|
-
}
|
|
598
|
-
markDirty();
|
|
599
|
-
};
|
|
600
|
-
});
|
|
601
|
-
}
|
|
602
|
-
function setSlotOption(item, key, value, defaultValue = undefined) {
|
|
603
|
-
const options = { ...(item.options || {}) };
|
|
604
|
-
if (value === undefined || value === defaultValue)
|
|
605
|
-
delete options[key];
|
|
606
|
-
else
|
|
607
|
-
options[key] = value;
|
|
608
|
-
item.options = Object.keys(options).length ? options : undefined;
|
|
609
|
-
}
|
|
610
|
-
function defaultSlotOptionValue(key) {
|
|
611
|
-
if (["includeLastUserMessage", "includeSummaries", "includeStatic", "includeSession", "includeTurn"].includes(key))
|
|
612
|
-
return true;
|
|
613
|
-
if (["stripAssistantThinking", "includeTime"].includes(key))
|
|
614
|
-
return false;
|
|
615
|
-
if (key === "toolMode")
|
|
616
|
-
return "keep";
|
|
617
|
-
if (key === "format")
|
|
618
|
-
return "xml";
|
|
619
|
-
return undefined;
|
|
620
|
-
}
|
|
621
|
-
function optionCheckbox(key, label, checked) {
|
|
622
|
-
return '<label class="checkline" title="' + attr(optionHelp(key)) + '"><input type="checkbox" data-option="' + attr(key) + '" ' + (checked ? "checked" : "") + '> ' + escapeHtml(label) + '</label>';
|
|
623
|
-
}
|
|
624
|
-
function optionSelect(key, label, value, choices) {
|
|
625
|
-
return '<div class="field" title="' + attr(optionHelp(key)) + '"><label>' + escapeHtml(label) + '</label><select data-option="' + attr(key) + '">' +
|
|
626
|
-
choices.map((choice) => '<option value="' + attr(choice) + '"' + (choice === value ? " selected" : "") + '>' + escapeHtml(choice) + '</option>').join("") +
|
|
627
|
-
'</select></div>';
|
|
628
|
-
}
|
|
629
|
-
function optionText(key, label, value) {
|
|
630
|
-
return '<div class="field" title="' + attr(optionHelp(key)) + '"><label>' + escapeHtml(label) + '</label><input data-option="' + attr(key) + '" data-array="true" value="' + attr(value) + '" placeholder="comma,separated"></div>';
|
|
631
|
-
}
|
|
632
|
-
function optionNumber(key, label, value) {
|
|
633
|
-
return '<div class="field" title="' + attr(optionHelp(key)) + '"><label>' + escapeHtml(label) + '</label><input type="number" min="1" data-option="' + attr(key) + '" value="' + attr(value) + '"></div>';
|
|
634
|
-
}
|
|
635
|
-
function optionHelp(key) {
|
|
636
|
-
const descriptions = {
|
|
637
|
-
includeLastUserMessage: "Keep the latest user message inside the inserted chat history.",
|
|
638
|
-
stripAssistantThinking: "Remove prior assistant thinking blocks from inserted chat history while keeping visible text, tool calls, and tool results.",
|
|
639
|
-
includeSummaries: "Keep Pi branch and compaction summary messages inside inserted chat history.",
|
|
640
|
-
toolMode: "Keep tool calls/results or drop prior tool history from inserted chat history.",
|
|
641
|
-
roles: "Optional comma-separated message roles to keep, such as user, assistant, toolResult, compactionSummary.",
|
|
642
|
-
maxMessages: "Keep only the most recent N chat-history messages after filtering.",
|
|
643
|
-
maxChars: "Keep only the most recent chat-history messages within an approximate character budget.",
|
|
644
|
-
includeStatic: "Include static stack variables in this variables slot.",
|
|
645
|
-
includeSession: "Include session variables created by template macros.",
|
|
646
|
-
includeTurn: "Include temporary turn variables created during prompt compilation.",
|
|
647
|
-
includeTime: "Render the current time in HH:MM:SS after the current date.",
|
|
648
|
-
format: "Choose XML or compact plain text rendering.",
|
|
649
|
-
};
|
|
650
|
-
return descriptions[key] || "Advanced slot option.";
|
|
651
|
-
}
|
|
652
|
-
function showStackModal(title, meta, body, options = {}) {
|
|
653
|
-
const pane = el("stackModal");
|
|
654
|
-
pane.innerHTML = '<div class="modal-dialog" role="dialog" aria-modal="true" aria-label="' + attr(title) + '">' +
|
|
655
|
-
'<div class="modal-head"><div><div class="modal-title">' + escapeHtml(title) + '</div><div class="modal-meta">' + escapeHtml(meta || "") + '</div></div>' +
|
|
656
|
-
'<div class="modal-actions"><button data-modal-close="true" data-icon="×" title="Close this dialog">Close</button></div></div>' +
|
|
657
|
-
'<div class="modal-body ' + attr(options.bodyClass || "") + '">' + body + '</div></div>';
|
|
658
|
-
pane.classList.add("open");
|
|
659
|
-
}
|
|
660
|
-
function closeStackModal() {
|
|
661
|
-
const pane = el("stackModal");
|
|
662
|
-
pane.classList.remove("open");
|
|
663
|
-
pane.innerHTML = "";
|
|
664
|
-
}
|
|
665
|
-
function openContextEditor() {
|
|
666
|
-
if (!currentStack)
|
|
667
|
-
return;
|
|
668
|
-
showStackModal("Context options", "Stack-level behavior for how pi-forge rewrites Pi conversation context.", '<div class="modal-toolbar"><span class="modal-meta">Save writes these changes to the stack JSON.</span></div>' +
|
|
669
|
-
'<div class="data-table">' +
|
|
670
|
-
'<div class="data-row">' +
|
|
671
|
-
'<label class="checkline" title="Allow multiple enabled chat-history slots. When off, only the first enabled chat-history slot is expanded.">' +
|
|
672
|
-
'<input id="allowDuplicateChatHistoryInput" type="checkbox" ' + (currentStack.context?.allowDuplicateChatHistory === true ? "checked" : "") + '> Allow duplicate chat-history slots</label>' +
|
|
673
|
-
'<div class="option-note">Keep this off unless you intentionally want the same conversation history injected more than once.</div>' +
|
|
674
|
-
'</div>' +
|
|
675
|
-
'</div>');
|
|
676
|
-
el("allowDuplicateChatHistoryInput").onchange = (event) => {
|
|
677
|
-
setContextOption("allowDuplicateChatHistory", event.target.checked, false);
|
|
678
|
-
markDirty();
|
|
679
|
-
};
|
|
680
|
-
}
|
|
681
|
-
function setContextOption(key, value, defaultValue) {
|
|
682
|
-
const stack = currentStack;
|
|
683
|
-
const context = { ...(stack.context || {}) };
|
|
684
|
-
if (value === defaultValue || value === undefined)
|
|
685
|
-
delete context[key];
|
|
686
|
-
else
|
|
687
|
-
context[key] = value;
|
|
688
|
-
if (Object.keys(context).length)
|
|
689
|
-
stack.context = context;
|
|
690
|
-
else
|
|
691
|
-
delete stack.context;
|
|
692
|
-
}
|
|
693
|
-
function renderRegexTab() {
|
|
694
|
-
if (!currentStack)
|
|
695
|
-
return;
|
|
696
|
-
el("tabPanel").innerHTML = '<div class="tab-section">' + regexEditor.renderBody() + '</div>';
|
|
697
|
-
regexEditor.bind();
|
|
698
|
-
}
|
|
699
|
-
function renderStackTab() {
|
|
700
|
-
if (!currentStack)
|
|
701
|
-
return;
|
|
702
|
-
const json = JSON.stringify(stackForDisplay(), null, 2);
|
|
703
|
-
const variableRows = Object.entries(currentStack.variables || {})
|
|
704
|
-
.sort(([a], [b]) => a.localeCompare(b))
|
|
705
|
-
.map(([name, value]) => variableRowHtml(name, value))
|
|
706
|
-
.join("");
|
|
707
|
-
el("tabPanel").innerHTML =
|
|
708
|
-
'<div class="tab-section">' +
|
|
709
|
-
'<div class="tab-section-title">Context options</div>' +
|
|
710
|
-
'<div class="tab-section-meta">Stack-level behavior for how pi-forge rewrites Pi conversation context.</div>' +
|
|
711
|
-
'<label class="checkline" title="Allow multiple enabled chat-history slots. When off, only the first enabled chat-history slot is expanded.">' +
|
|
712
|
-
'<input id="allowDuplicateChatHistoryInput" type="checkbox" ' + (currentStack.context?.allowDuplicateChatHistory === true ? "checked" : "") + '> Allow duplicate chat-history slots</label>' +
|
|
713
|
-
'<div class="option-note">Keep this off unless you intentionally want the same conversation history injected more than once.</div>' +
|
|
714
|
-
'</div>' +
|
|
715
|
-
'<div class="tab-section">' +
|
|
716
|
-
'<div class="tab-section-title">Stack variables</div>' +
|
|
717
|
-
'<div class="tab-section-meta">Static string variables available to template macros and variables slots.</div>' +
|
|
718
|
-
'<div class="modal-toolbar"><button id="addVariableBtn" data-icon="+" title="Add a static stack variable">Add variable</button><span class="modal-spacer"></span><span class="modal-meta">Saved in stack.variables.</span></div>' +
|
|
719
|
-
'<div class="data-table" id="variablesRows">' +
|
|
720
|
-
'<div class="data-row header variable-row"><div>Name</div><div>Value</div><div></div></div>' +
|
|
721
|
-
variableRows +
|
|
722
|
-
'</div></div>' +
|
|
723
|
-
'<div class="tab-section">' +
|
|
724
|
-
'<div class="tab-section-title">Stack JSON</div>' +
|
|
725
|
-
'<div class="tab-section-meta">Raw recovery view for advanced fields. Apply updates the editor; Save writes to disk.</div>' +
|
|
726
|
-
'<div class="modal-toolbar">' +
|
|
727
|
-
'<button id="copyStackJsonBtn" data-icon="□" title="Copy this JSON to the clipboard">Copy</button>' +
|
|
728
|
-
'<button id="applyStackJsonBtn" class="primary" data-icon="✓" title="Apply this JSON to the editor without saving">Apply to editor</button>' +
|
|
729
|
-
'<span class="modal-spacer"></span><span id="stackJsonStatus" class="modal-meta">Unsaved stack JSON draft.</span>' +
|
|
730
|
-
'</div>' +
|
|
731
|
-
'<textarea id="stackJsonText" class="raw-json-editor" spellcheck="false">' + escapeHtml(json) + '</textarea>' +
|
|
732
|
-
'</div>';
|
|
733
|
-
el("allowDuplicateChatHistoryInput").onchange = (event) => {
|
|
734
|
-
setContextOption("allowDuplicateChatHistory", event.target.checked, false);
|
|
735
|
-
markDirty();
|
|
736
|
-
};
|
|
737
|
-
bindVariablesEditor();
|
|
738
|
-
el("copyStackJsonBtn").onclick = () => run(copyRawStackJson);
|
|
739
|
-
el("applyStackJsonBtn").onclick = () => run(applyRawStackJson);
|
|
740
|
-
}
|
|
741
|
-
function openRawStackJsonEditor() {
|
|
742
|
-
if (!currentStack)
|
|
743
|
-
return;
|
|
744
|
-
const json = JSON.stringify(stackForDisplay(), null, 2);
|
|
745
|
-
showStackModal("Stack JSON", "Raw recovery view for advanced fields. Apply updates the editor; Save writes to disk.", '<div class="modal-toolbar">' +
|
|
746
|
-
'<button id="copyStackJsonBtn" data-icon="□" title="Copy this JSON to the clipboard">Copy</button>' +
|
|
747
|
-
'<button id="applyStackJsonBtn" class="primary" data-icon="✓" title="Apply this JSON to the editor without saving">Apply to editor</button>' +
|
|
748
|
-
'<span class="modal-spacer"></span><span id="stackJsonStatus" class="modal-meta">Unsaved stack JSON draft.</span>' +
|
|
749
|
-
'</div>' +
|
|
750
|
-
'<textarea id="stackJsonText" class="raw-json-editor" spellcheck="false">' + escapeHtml(json) + '</textarea>', { bodyClass: "json-modal" });
|
|
751
|
-
el("copyStackJsonBtn").onclick = () => run(copyRawStackJson);
|
|
752
|
-
el("applyStackJsonBtn").onclick = () => run(applyRawStackJson);
|
|
753
|
-
}
|
|
754
|
-
function stackForDisplay() {
|
|
755
|
-
if (!currentStack)
|
|
756
|
-
throw new Error("No stack selected.");
|
|
757
|
-
const clone = structuredClone(currentStack);
|
|
758
|
-
if (!clone.type)
|
|
759
|
-
clone.type = "pi-forge.prompt-stack";
|
|
760
|
-
if (!clone.schemaVersion)
|
|
761
|
-
clone.schemaVersion = 1;
|
|
762
|
-
return clone;
|
|
763
|
-
}
|
|
764
|
-
async function copyRawStackJson() {
|
|
765
|
-
await copyTextToClipboard(el("stackJsonText").value);
|
|
766
|
-
el("stackJsonStatus").textContent = "Copied JSON.";
|
|
767
|
-
setStatus("Copied stack JSON", "success");
|
|
768
|
-
}
|
|
769
|
-
async function applyRawStackJson() {
|
|
770
|
-
const text = el("stackJsonText").value;
|
|
771
|
-
const parsed = JSON.parse(text);
|
|
772
|
-
validateRawStackJson(parsed);
|
|
773
|
-
const stack = parsed;
|
|
774
|
-
currentStack = stack;
|
|
775
|
-
if (!stack.schemaVersion)
|
|
776
|
-
stack.schemaVersion = 1;
|
|
777
|
-
if (!stack.type)
|
|
778
|
-
stack.type = "pi-forge.prompt-stack";
|
|
779
|
-
selectedItemIndex = stack.items.length ? Math.min(Math.max(selectedItemIndex, 0), stack.items.length - 1) : -1;
|
|
780
|
-
optionsError = "";
|
|
781
|
-
stackVariablesError = "";
|
|
782
|
-
regexEditor.reset();
|
|
783
|
-
policyEditor.reset();
|
|
784
|
-
closeStackModal();
|
|
785
|
-
markDirty();
|
|
786
|
-
renderAll(latestDiagnostics);
|
|
787
|
-
setStatus("Applied stack JSON to editor", "success");
|
|
788
|
-
}
|
|
789
|
-
function validateRawStackJson(stack) {
|
|
790
|
-
if (!stack || typeof stack !== "object" || Array.isArray(stack))
|
|
791
|
-
throw new Error("Stack JSON must be an object.");
|
|
792
|
-
if (typeof stack.id !== "string" || !stack.id.trim())
|
|
793
|
-
throw new Error("Stack JSON needs a non-empty string id.");
|
|
794
|
-
if (!Array.isArray(stack.items))
|
|
795
|
-
throw new Error("Stack JSON needs an items array.");
|
|
796
|
-
stack.items.forEach((item, index) => {
|
|
797
|
-
const label = "Item " + (index + 1);
|
|
798
|
-
if (!item || typeof item !== "object" || Array.isArray(item))
|
|
799
|
-
throw new Error(label + " must be an object.");
|
|
800
|
-
if (item.kind !== "block" && item.kind !== "slot")
|
|
801
|
-
throw new Error(label + " kind must be block or slot.");
|
|
802
|
-
if (typeof item.id !== "string" || !item.id.trim())
|
|
803
|
-
throw new Error(label + " needs a non-empty string id.");
|
|
804
|
-
if (item.kind === "block" && typeof item.content !== "string")
|
|
805
|
-
throw new Error(label + " block content must be a string.");
|
|
806
|
-
if (item.kind === "slot" && typeof item.slot !== "string")
|
|
807
|
-
throw new Error(label + " slot must be a string.");
|
|
808
|
-
});
|
|
809
|
-
}
|
|
810
|
-
function openVariablesEditor() {
|
|
811
|
-
if (!currentStack)
|
|
812
|
-
return;
|
|
813
|
-
const rows = Object.entries(currentStack.variables || {})
|
|
814
|
-
.sort(([a], [b]) => a.localeCompare(b))
|
|
815
|
-
.map(([name, value]) => variableRowHtml(name, value))
|
|
816
|
-
.join("");
|
|
817
|
-
showStackModal("Stack variables", "Static string variables available to macros and variables slots.", '<div class="modal-toolbar"><button id="addVariableBtn" data-icon="+" title="Add a static stack variable">Add variable</button><span class="modal-spacer"></span><span class="modal-meta">Save writes these changes to the stack JSON.</span></div>' +
|
|
818
|
-
'<div class="data-table" id="variablesRows">' +
|
|
819
|
-
'<div class="data-row header variable-row"><div>Name</div><div>Value</div><div></div></div>' +
|
|
820
|
-
rows +
|
|
821
|
-
'</div>');
|
|
822
|
-
bindVariablesEditor();
|
|
823
|
-
}
|
|
824
|
-
function variableRowHtml(name = "", value = "") {
|
|
825
|
-
return '<div class="data-row variable-row" data-var-row>' +
|
|
826
|
-
'<input data-var-name value="' + attr(name) + '" placeholder="char">' +
|
|
827
|
-
'<input data-var-value value="' + attr(value) + '" placeholder="泉此方">' +
|
|
828
|
-
'<button type="button" class="danger" data-delete-row="true" data-icon="×" title="Delete this stack variable">Delete</button>' +
|
|
829
|
-
'</div>';
|
|
830
|
-
}
|
|
831
|
-
function bindVariablesEditor() {
|
|
832
|
-
el("addVariableBtn").onclick = () => {
|
|
833
|
-
el("variablesRows").insertAdjacentHTML("beforeend", variableRowHtml(uniqueVariableName()));
|
|
834
|
-
bindVariablesEditor();
|
|
835
|
-
syncVariablesFromModal();
|
|
836
|
-
};
|
|
837
|
-
document.querySelectorAll("[data-var-row] input").forEach((input) => {
|
|
838
|
-
input.oninput = () => syncVariablesFromModal();
|
|
839
|
-
});
|
|
840
|
-
document.querySelectorAll("[data-var-row] [data-delete-row]").forEach((button) => {
|
|
841
|
-
button.onclick = (event) => {
|
|
842
|
-
event.target.closest("[data-var-row]").remove();
|
|
843
|
-
syncVariablesFromModal();
|
|
844
|
-
};
|
|
845
|
-
});
|
|
846
|
-
}
|
|
847
|
-
function uniqueVariableName() {
|
|
848
|
-
const existing = new Set(Object.keys(currentStack?.variables || {}));
|
|
849
|
-
let index = existing.size + 1;
|
|
850
|
-
let name = "var" + index;
|
|
851
|
-
while (existing.has(name))
|
|
852
|
-
name = "var" + (++index);
|
|
853
|
-
return name;
|
|
854
|
-
}
|
|
855
|
-
function syncVariablesFromModal() {
|
|
856
|
-
if (!currentStack)
|
|
857
|
-
return;
|
|
858
|
-
const variables = {};
|
|
859
|
-
const seen = new Set();
|
|
860
|
-
let duplicate = false;
|
|
861
|
-
document.querySelectorAll("[data-var-row]").forEach((row) => {
|
|
862
|
-
const name = row.querySelector("[data-var-name]").value.trim();
|
|
863
|
-
const value = row.querySelector("[data-var-value]").value;
|
|
864
|
-
if (!name)
|
|
865
|
-
return;
|
|
866
|
-
if (seen.has(name))
|
|
867
|
-
duplicate = true;
|
|
868
|
-
seen.add(name);
|
|
869
|
-
variables[name] = value;
|
|
870
|
-
});
|
|
871
|
-
if (Object.keys(variables).length)
|
|
872
|
-
currentStack.variables = variables;
|
|
873
|
-
else
|
|
874
|
-
delete currentStack.variables;
|
|
875
|
-
stackVariablesError = duplicate ? "Duplicate stack variable names." : "";
|
|
876
|
-
markDirty();
|
|
877
|
-
if (duplicate)
|
|
878
|
-
setStatus(stackVariablesError, "error");
|
|
879
|
-
}
|
|
880
|
-
function addItem(kind) {
|
|
881
|
-
if (!currentStack)
|
|
882
|
-
return;
|
|
883
|
-
const id = nextNumericItemId();
|
|
884
|
-
const insertIndex = selectedItemIndex >= 0 && selectedItemIndex < currentStack.items.length
|
|
885
|
-
? selectedItemIndex + 1
|
|
886
|
-
: currentStack.items.length;
|
|
887
|
-
currentStack.items.splice(insertIndex, 0, kind === "slot"
|
|
888
|
-
? { kind: "slot", id, enabled: true, slot: "chat-history" }
|
|
889
|
-
: { kind: "block", id, enabled: true, role: "user", content: "" });
|
|
890
|
-
selectedItemIndex = insertIndex;
|
|
891
|
-
markDirty();
|
|
892
|
-
renderItemList();
|
|
893
|
-
renderItemEditor();
|
|
894
|
-
}
|
|
895
|
-
function nextNumericItemId() {
|
|
896
|
-
const existing = new Set((currentStack?.items || []).map((item) => String(item.id)));
|
|
897
|
-
let index = 1;
|
|
898
|
-
while (existing.has(String(index)))
|
|
899
|
-
index++;
|
|
900
|
-
return String(index);
|
|
901
|
-
}
|
|
902
|
-
function deleteSelectedItem() {
|
|
903
|
-
if (!currentStack || selectedItemIndex < 0)
|
|
904
|
-
return;
|
|
905
|
-
const item = currentStack.items[selectedItemIndex];
|
|
906
|
-
if (!confirm("Delete item " + item.id + "?"))
|
|
907
|
-
return;
|
|
908
|
-
currentStack.items.splice(selectedItemIndex, 1);
|
|
909
|
-
selectedItemIndex = Math.min(selectedItemIndex, currentStack.items.length - 1);
|
|
910
|
-
markDirty();
|
|
911
|
-
renderItemList();
|
|
912
|
-
renderItemEditor();
|
|
913
|
-
}
|
|
914
|
-
async function saveStack() {
|
|
915
|
-
const stack = stackForSubmit();
|
|
916
|
-
const data = await api("/api/stacks/" + encodeURIComponent(selectedId), { method: "PUT", body: { stack } });
|
|
917
|
-
stacks = data.stacks || stacks;
|
|
918
|
-
selectedId = data.stack?.id || stack.id;
|
|
919
|
-
currentStack = structuredClone(stack);
|
|
920
|
-
dirty = false;
|
|
921
|
-
renderDirtyState();
|
|
922
|
-
renderAll(data.stack?.diagnostics || []);
|
|
923
|
-
setStatus("Saved " + selectedId, "success");
|
|
924
|
-
await selectStack(selectedId, { keepDirty: true });
|
|
925
|
-
}
|
|
926
|
-
async function createStackRemote(stack, options = {}) {
|
|
927
|
-
try {
|
|
928
|
-
return await api("/api/stacks", { method: "POST", body: { stack, ...options } });
|
|
929
|
-
}
|
|
930
|
-
catch (error) {
|
|
931
|
-
if (error instanceof EditorApiError && error.status === 409 && !options.overwrite && confirm((error.message || "Stack already exists.") + "\n\nOverwrite it?")) {
|
|
932
|
-
return await api("/api/stacks", { method: "POST", body: { stack, ...options, overwrite: true } });
|
|
933
|
-
}
|
|
934
|
-
throw error;
|
|
935
|
-
}
|
|
936
|
-
}
|
|
937
|
-
async function createAndOpenStack(stack, activate, actionLabel, extraOptions = {}) {
|
|
938
|
-
const data = await createStackRemote(stack, { ...extraOptions, activate });
|
|
939
|
-
stacks = data.stacks || stacks;
|
|
940
|
-
selectedId = data.stack?.id || stack.id;
|
|
941
|
-
dirty = false;
|
|
942
|
-
renderDirtyState();
|
|
943
|
-
await selectStack(selectedId, { keepDirty: true });
|
|
944
|
-
const converted = data.importFormat === "sillytavern" ? " from SillyTavern" : "";
|
|
945
|
-
setStatus(actionLabel + converted + " " + selectedId, "success");
|
|
946
|
-
if (data.importReport)
|
|
947
|
-
showImportReport(data.importReport, selectedId);
|
|
948
|
-
}
|
|
949
|
-
async function createNewStack() {
|
|
950
|
-
if (dirty && !confirm("Discard unsaved changes?"))
|
|
951
|
-
return;
|
|
952
|
-
const promptedId = prompt("New stack id", uniqueStackId("new-stack"));
|
|
953
|
-
if (promptedId === null)
|
|
954
|
-
return;
|
|
955
|
-
const id = sanitizeStackId(promptedId);
|
|
956
|
-
if (!id)
|
|
957
|
-
throw new Error("Stack id must not be empty.");
|
|
958
|
-
if (id !== promptedId.trim() && !confirm("Use stack id '" + id + "'?"))
|
|
959
|
-
return;
|
|
960
|
-
const promptedName = prompt("Stack display name", "Default Pi Prompt Mirror");
|
|
961
|
-
if (promptedName === null)
|
|
962
|
-
return;
|
|
963
|
-
const stack = defaultNewStack(id, promptedName.trim() || id);
|
|
964
|
-
const activate = stacks.length === 0 || confirm("Activate new stack now?");
|
|
965
|
-
await createAndOpenStack(stack, activate, "Created");
|
|
966
|
-
}
|
|
967
|
-
function defaultNewStack(id, name) {
|
|
968
|
-
return {
|
|
969
|
-
schemaVersion: 1,
|
|
970
|
-
type: "pi-forge.prompt-stack",
|
|
971
|
-
id,
|
|
972
|
-
name,
|
|
973
|
-
description: "Recreates Pi's built-in prompt layout with pi-forge slots, exposing tools, guidelines, docs, append-system-prompt, project context, skills, date/cwd, and chat history as movable pieces.",
|
|
974
|
-
autoActivate: stacks.length === 0,
|
|
975
|
-
mode: "replace",
|
|
976
|
-
defaults: {
|
|
977
|
-
syntheticMessagesVisible: false,
|
|
978
|
-
unresolvedMacroPolicy: "warn",
|
|
979
|
-
},
|
|
980
|
-
context: {
|
|
981
|
-
allowDuplicateChatHistory: false,
|
|
982
|
-
},
|
|
983
|
-
tools: {
|
|
984
|
-
allow: ["*"],
|
|
985
|
-
},
|
|
986
|
-
skills: {
|
|
987
|
-
allow: ["*"],
|
|
988
|
-
},
|
|
989
|
-
items: [
|
|
990
|
-
{
|
|
991
|
-
kind: "block",
|
|
992
|
-
id: "main-role",
|
|
993
|
-
name: "Pi Default Role",
|
|
994
|
-
enabled: true,
|
|
995
|
-
role: "system",
|
|
996
|
-
source: {
|
|
997
|
-
package: "@earendil-works/pi-coding-agent",
|
|
998
|
-
file: "dist/core/system-prompt.js",
|
|
999
|
-
},
|
|
1000
|
-
content: "You are an expert coding assistant operating inside pi, a coding agent harness. You help users by reading files, executing commands, editing code, and writing new files.",
|
|
1001
|
-
},
|
|
1002
|
-
{
|
|
1003
|
-
kind: "slot",
|
|
1004
|
-
id: "tools",
|
|
1005
|
-
name: "Available Tools",
|
|
1006
|
-
enabled: true,
|
|
1007
|
-
role: "system",
|
|
1008
|
-
slot: "tools",
|
|
1009
|
-
options: {
|
|
1010
|
-
format: "plain",
|
|
1011
|
-
onlyWithSnippets: true,
|
|
1012
|
-
},
|
|
1013
|
-
},
|
|
1014
|
-
{
|
|
1015
|
-
kind: "block",
|
|
1016
|
-
id: "custom-tools-note",
|
|
1017
|
-
name: "Custom Tools Note",
|
|
1018
|
-
enabled: true,
|
|
1019
|
-
role: "system",
|
|
1020
|
-
content: "In addition to the tools above, you may have access to other custom tools depending on the project.",
|
|
1021
|
-
},
|
|
1022
|
-
{
|
|
1023
|
-
kind: "slot",
|
|
1024
|
-
id: "tool-guidelines",
|
|
1025
|
-
name: "Guidelines",
|
|
1026
|
-
enabled: true,
|
|
1027
|
-
role: "system",
|
|
1028
|
-
slot: "tool-guidelines",
|
|
1029
|
-
options: {
|
|
1030
|
-
format: "plain",
|
|
1031
|
-
heading: "Guidelines:",
|
|
1032
|
-
includePiDefaultGuidelines: true,
|
|
1033
|
-
piStyle: true,
|
|
1034
|
-
},
|
|
1035
|
-
},
|
|
1036
|
-
{
|
|
1037
|
-
kind: "slot",
|
|
1038
|
-
id: "pi-docs",
|
|
1039
|
-
name: "Pi Documentation Guidance",
|
|
1040
|
-
enabled: true,
|
|
1041
|
-
role: "system",
|
|
1042
|
-
slot: "pi-docs",
|
|
1043
|
-
},
|
|
1044
|
-
{
|
|
1045
|
-
kind: "slot",
|
|
1046
|
-
id: "append-system-prompt",
|
|
1047
|
-
name: "User Append System Prompt",
|
|
1048
|
-
enabled: true,
|
|
1049
|
-
role: "system",
|
|
1050
|
-
slot: "append-system-prompt",
|
|
1051
|
-
},
|
|
1052
|
-
{
|
|
1053
|
-
kind: "slot",
|
|
1054
|
-
id: "project-context",
|
|
1055
|
-
name: "Project Context",
|
|
1056
|
-
enabled: true,
|
|
1057
|
-
role: "system",
|
|
1058
|
-
slot: "project-context",
|
|
1059
|
-
},
|
|
1060
|
-
{
|
|
1061
|
-
kind: "slot",
|
|
1062
|
-
id: "skills",
|
|
1063
|
-
name: "Available Skills",
|
|
1064
|
-
enabled: true,
|
|
1065
|
-
role: "system",
|
|
1066
|
-
slot: "skills",
|
|
1067
|
-
options: {
|
|
1068
|
-
requireReadTool: true,
|
|
1069
|
-
},
|
|
1070
|
-
},
|
|
1071
|
-
{
|
|
1072
|
-
kind: "slot",
|
|
1073
|
-
id: "date-cwd",
|
|
1074
|
-
name: "Date and Working Directory",
|
|
1075
|
-
enabled: true,
|
|
1076
|
-
role: "system",
|
|
1077
|
-
slot: "date-cwd",
|
|
1078
|
-
},
|
|
1079
|
-
{
|
|
1080
|
-
kind: "slot",
|
|
1081
|
-
id: "chat-history",
|
|
1082
|
-
name: "Chat History",
|
|
1083
|
-
enabled: true,
|
|
1084
|
-
slot: "chat-history",
|
|
1085
|
-
},
|
|
1086
|
-
],
|
|
1087
|
-
};
|
|
1088
|
-
}
|
|
1089
|
-
function showImportReport(report, stackId) {
|
|
1090
|
-
showStackModal("SillyTavern import report", stackId || "", '<div class="modal-toolbar"><button id="copyImportReportBtn" data-icon="□" title="Copy this import report">Copy report</button><span class="modal-spacer"></span><span class="modal-meta">Report-only notes; stack changes are already saved.</span></div>' +
|
|
1091
|
-
'<pre class="preview-text">' + escapeHtml(report) + '</pre>');
|
|
1092
|
-
el("copyImportReportBtn").onclick = () => run(async () => {
|
|
1093
|
-
await copyTextToClipboard(report);
|
|
1094
|
-
setStatus("Copied import report", "success");
|
|
1095
|
-
});
|
|
1096
|
-
}
|
|
1097
|
-
async function importStackJson() {
|
|
1098
|
-
el("importFileInput").value = "";
|
|
1099
|
-
el("importFileInput").click();
|
|
1100
|
-
}
|
|
1101
|
-
async function handleImportFile(event) {
|
|
1102
|
-
const file = event.target.files?.[0];
|
|
1103
|
-
if (!file)
|
|
1104
|
-
return;
|
|
1105
|
-
const text = await file.text();
|
|
1106
|
-
const imported = JSON.parse(text);
|
|
1107
|
-
if (!imported || typeof imported !== "object" || Array.isArray(imported))
|
|
1108
|
-
throw new Error("Imported JSON must be an object.");
|
|
1109
|
-
if (isSillyTavernImport(imported)) {
|
|
1110
|
-
const characterId = promptSillyTavernCharacterId(imported);
|
|
1111
|
-
if (characterId === null)
|
|
1112
|
-
return;
|
|
1113
|
-
const activate = confirm("Convert and activate imported SillyTavern stack now?");
|
|
1114
|
-
await createAndOpenStack(imported, activate, "Imported", { sourceName: file.name, characterId });
|
|
1115
|
-
return;
|
|
1116
|
-
}
|
|
1117
|
-
const stack = imported;
|
|
1118
|
-
if (!stack.id || typeof stack.id !== "string") {
|
|
1119
|
-
const promptedId = prompt("Stack id", sanitizeStackId(file.name.replace(/\.json$/i, "")));
|
|
1120
|
-
if (!promptedId)
|
|
1121
|
-
return;
|
|
1122
|
-
stack.id = promptedId.trim();
|
|
1123
|
-
}
|
|
1124
|
-
if (!Array.isArray(stack.items))
|
|
1125
|
-
throw new Error("Imported stack must contain an items array.");
|
|
1126
|
-
if (!stack.schemaVersion)
|
|
1127
|
-
stack.schemaVersion = 1;
|
|
1128
|
-
if (!stack.type)
|
|
1129
|
-
stack.type = "pi-forge.prompt-stack";
|
|
1130
|
-
const activate = confirm("Activate imported stack now?");
|
|
1131
|
-
await createAndOpenStack(stack, activate, "Imported");
|
|
1132
|
-
}
|
|
1133
|
-
function isSillyTavernImport(value) {
|
|
1134
|
-
return value && typeof value === "object" && !Array.isArray(value) && Array.isArray(value.prompts) && !Array.isArray(value.items);
|
|
1135
|
-
}
|
|
1136
|
-
function promptSillyTavernCharacterId(value) {
|
|
1137
|
-
const ids = Array.isArray(value.prompt_order)
|
|
1138
|
-
? value.prompt_order
|
|
1139
|
-
.map((entry) => entry && entry.character_id)
|
|
1140
|
-
.filter((id) => Number.isInteger(id))
|
|
1141
|
-
: [];
|
|
1142
|
-
const uniqueIds = [...new Set(ids)];
|
|
1143
|
-
if (uniqueIds.length <= 1)
|
|
1144
|
-
return undefined;
|
|
1145
|
-
const answer = prompt("SillyTavern character_id (" + uniqueIds.join(", ") + ")", String(uniqueIds[0]));
|
|
1146
|
-
if (answer === null)
|
|
1147
|
-
return null;
|
|
1148
|
-
const parsed = Number(answer.trim());
|
|
1149
|
-
if (!Number.isInteger(parsed) || !uniqueIds.includes(parsed)) {
|
|
1150
|
-
throw new Error("Choose one of these character_id values: " + uniqueIds.join(", "));
|
|
1151
|
-
}
|
|
1152
|
-
return parsed;
|
|
1153
|
-
}
|
|
1154
|
-
async function forkStack() {
|
|
1155
|
-
const source = stackForSubmit();
|
|
1156
|
-
const forkId = prompt("New fork stack id", uniqueForkId(source.id || "stack"));
|
|
1157
|
-
if (!forkId)
|
|
1158
|
-
return;
|
|
1159
|
-
const forkName = prompt("Fork display name", ((source.name || source.id || "Prompt stack") + " fork"));
|
|
1160
|
-
const fork = structuredClone(source);
|
|
1161
|
-
fork.id = forkId.trim();
|
|
1162
|
-
if (forkName && forkName.trim())
|
|
1163
|
-
fork.name = forkName;
|
|
1164
|
-
fork.autoActivate = false;
|
|
1165
|
-
const activate = confirm("Activate fork now?");
|
|
1166
|
-
await createAndOpenStack(fork, activate, "Forked");
|
|
1167
|
-
}
|
|
1168
|
-
async function exportStackJson() {
|
|
1169
|
-
const stack = stackForSubmit();
|
|
1170
|
-
const json = JSON.stringify(stack, null, 2) + "\n";
|
|
1171
|
-
const downloaded = downloadTextFile(sanitizeStackId(stack.id || "prompt-stack") + ".json", json, "application/json");
|
|
1172
|
-
if (downloaded) {
|
|
1173
|
-
setStatus("Exported " + (stack.id || "prompt stack"), "success");
|
|
1174
|
-
return;
|
|
1175
|
-
}
|
|
1176
|
-
await copyTextToClipboard(json);
|
|
1177
|
-
setStatus("Copied " + (stack.id || "prompt stack") + " JSON", "success");
|
|
1178
|
-
}
|
|
1179
|
-
function downloadTextFile(filename, text, type) {
|
|
1180
|
-
if (typeof Blob === "undefined" || typeof URL === "undefined" || !URL.createObjectURL)
|
|
1181
|
-
return false;
|
|
1182
|
-
const blob = new Blob([text], { type });
|
|
1183
|
-
const url = URL.createObjectURL(blob);
|
|
1184
|
-
const link = document.createElement("a");
|
|
1185
|
-
if (!("download" in link)) {
|
|
1186
|
-
URL.revokeObjectURL(url);
|
|
1187
|
-
return false;
|
|
1188
|
-
}
|
|
1189
|
-
link.href = url;
|
|
1190
|
-
link.download = filename;
|
|
1191
|
-
document.body.appendChild(link);
|
|
1192
|
-
link.click();
|
|
1193
|
-
link.remove();
|
|
1194
|
-
URL.revokeObjectURL(url);
|
|
1195
|
-
return true;
|
|
1196
|
-
}
|
|
1197
|
-
function uniqueForkId(baseId) {
|
|
1198
|
-
const base = sanitizeStackId(baseId || "stack") || "stack";
|
|
1199
|
-
const existing = new Set(stacks.map((stack) => stack.id));
|
|
1200
|
-
let candidate = base + "-fork";
|
|
1201
|
-
let index = 2;
|
|
1202
|
-
while (existing.has(candidate))
|
|
1203
|
-
candidate = base + "-fork-" + index++;
|
|
1204
|
-
return candidate;
|
|
1205
|
-
}
|
|
1206
|
-
function uniqueStackId(baseId) {
|
|
1207
|
-
const base = sanitizeStackId(baseId || "stack") || "stack";
|
|
1208
|
-
const existing = new Set(stacks.map((stack) => stack.id));
|
|
1209
|
-
let candidate = base;
|
|
1210
|
-
let index = 2;
|
|
1211
|
-
while (existing.has(candidate))
|
|
1212
|
-
candidate = base + "-" + index++;
|
|
1213
|
-
return candidate;
|
|
1214
|
-
}
|
|
1215
|
-
function sanitizeStackId(value) {
|
|
1216
|
-
return String(value || "")
|
|
1217
|
-
.trim()
|
|
1218
|
-
.replace(/[^A-Za-z0-9_-]+/g, "-")
|
|
1219
|
-
.replace(/-+/g, "-")
|
|
1220
|
-
.replace(/^-|-$/g, "")
|
|
1221
|
-
.toLowerCase();
|
|
1222
|
-
}
|
|
1223
|
-
async function activateStack() {
|
|
1224
|
-
if (!currentStack)
|
|
1225
|
-
return;
|
|
1226
|
-
const data = await api("/api/stacks/" + encodeURIComponent(selectedId) + "/activate", { method: "POST" });
|
|
1227
|
-
stacks = data.stacks || stacks;
|
|
1228
|
-
renderStackList();
|
|
1229
|
-
setStatus("Activated " + selectedId, "success");
|
|
1230
|
-
}
|
|
1231
|
-
async function disableStacks() {
|
|
1232
|
-
const data = await api("/api/disable", { method: "POST" });
|
|
1233
|
-
stacks = data.stacks || stacks;
|
|
1234
|
-
renderStackList();
|
|
1235
|
-
setStatus("Prompt stack disabled", "success");
|
|
1236
|
-
}
|
|
1237
|
-
async function deleteCurrentStack() {
|
|
1238
|
-
if (!currentStack)
|
|
1239
|
-
return;
|
|
1240
|
-
const id = selectedId;
|
|
1241
|
-
const message = "Delete prompt stack '" + id + "'?\n\nThis removes its JSON file from prompt-stack storage.";
|
|
1242
|
-
if (!confirm(message))
|
|
1243
|
-
return;
|
|
1244
|
-
const data = await api("/api/stacks/" + encodeURIComponent(id), { method: "DELETE" });
|
|
1245
|
-
stacks = data.stacks || [];
|
|
1246
|
-
dirty = false;
|
|
1247
|
-
renderDirtyState();
|
|
1248
|
-
const next = stacks.find((stack) => stack.active) || stacks[0];
|
|
1249
|
-
if (next) {
|
|
1250
|
-
await selectStack(next.id, { keepDirty: true });
|
|
1251
|
-
setStatus("Deleted " + id, "success");
|
|
1252
|
-
}
|
|
1253
|
-
else {
|
|
1254
|
-
renderStackList();
|
|
1255
|
-
renderEmpty();
|
|
1256
|
-
setStatus("Deleted " + id + "; no stacks remain", "success");
|
|
1257
|
-
}
|
|
1258
|
-
}
|
|
1259
|
-
async function reloadFromDisk() {
|
|
1260
|
-
if (dirty && !confirm("Discard unsaved changes?"))
|
|
1261
|
-
return;
|
|
1262
|
-
const data = await api("/api/reload", { method: "POST" });
|
|
1263
|
-
stacks = data.stacks || [];
|
|
1264
|
-
renderStackList();
|
|
1265
|
-
await loadStacks(selectedId);
|
|
1266
|
-
setStatus("Reloaded from disk", "success");
|
|
1267
|
-
}
|
|
1268
|
-
function stackForSubmit() {
|
|
1269
|
-
if (!currentStack)
|
|
1270
|
-
throw new Error("No stack selected.");
|
|
1271
|
-
if (optionsError)
|
|
1272
|
-
throw new Error("Invalid item options JSON: " + optionsError);
|
|
1273
|
-
if (stackVariablesError)
|
|
1274
|
-
throw new Error(stackVariablesError);
|
|
1275
|
-
if (regexEditor.getError())
|
|
1276
|
-
throw new Error(regexEditor.getError());
|
|
1277
|
-
if (policyEditor.getError())
|
|
1278
|
-
throw new Error(policyEditor.getError());
|
|
1279
|
-
const clone = structuredClone(currentStack);
|
|
1280
|
-
if (!clone.type)
|
|
1281
|
-
clone.type = "pi-forge.prompt-stack";
|
|
1282
|
-
if (!clone.schemaVersion)
|
|
1283
|
-
clone.schemaVersion = 1;
|
|
1284
|
-
return clone;
|
|
1285
|
-
}
|
|
1286
|
-
function renderDiagnostics(diagnostics) {
|
|
1287
|
-
latestDiagnostics = diagnostics || [];
|
|
1288
|
-
const pane = el("diagnostics");
|
|
1289
|
-
if (!latestDiagnostics.length) {
|
|
1290
|
-
pane.innerHTML = '<div class="diagnostic info">No diagnostics.</div>';
|
|
1291
|
-
return;
|
|
1292
|
-
}
|
|
1293
|
-
pane.innerHTML = latestDiagnostics.map((diag) => {
|
|
1294
|
-
const level = diag.level || "info";
|
|
1295
|
-
const item = diag.itemId ? " [" + escapeHtml(diag.itemId) + "]" : "";
|
|
1296
|
-
return '<div class="diagnostic ' + attr(level) + '"><strong>' + escapeHtml(level.toUpperCase()) + item + '</strong>: ' + escapeHtml(diag.message || "") + '</div>';
|
|
1297
|
-
}).join("");
|
|
1298
|
-
}
|
|
1299
|
-
function renderEmpty() {
|
|
1300
|
-
currentStack = null;
|
|
1301
|
-
selectedId = "";
|
|
1302
|
-
dirty = false;
|
|
1303
|
-
activeTab = "items";
|
|
1304
|
-
policyEditor.reset();
|
|
1305
|
-
renderDirtyState();
|
|
1306
|
-
document.querySelectorAll("[data-tab]").forEach((button) => {
|
|
1307
|
-
button.classList.toggle("active", button.dataset.tab === activeTab);
|
|
1308
|
-
});
|
|
1309
|
-
el("workspace").style.display = "";
|
|
1310
|
-
el("metadataPanel").style.display = "none";
|
|
1311
|
-
el("settings").innerHTML = "";
|
|
1312
|
-
el("itemCount").textContent = "";
|
|
1313
|
-
el("itemList").innerHTML = "";
|
|
1314
|
-
el("itemEditor").innerHTML =
|
|
1315
|
-
'<div class="empty">' +
|
|
1316
|
-
'<div class="empty-title">No prompt stacks found.</div>' +
|
|
1317
|
-
'<div>Create a stack in this project, or import an existing pi-forge/SillyTavern JSON file.</div>' +
|
|
1318
|
-
'<div class="empty-actions">' +
|
|
1319
|
-
'<button id="emptyNewStackBtn" class="primary" data-icon="+" title="Create a new prompt stack">New stack</button>' +
|
|
1320
|
-
'<button id="emptyImportBtn" data-icon="⇪" title="Import pi-forge stack JSON or SillyTavern preset JSON">Import JSON</button>' +
|
|
1321
|
-
'</div>' +
|
|
1322
|
-
'</div>';
|
|
1323
|
-
el("tabPanel").classList.remove("open");
|
|
1324
|
-
el("tabPanel").innerHTML = "";
|
|
1325
|
-
renderDiagnostics([]);
|
|
1326
|
-
el("emptyNewStackBtn").onclick = () => run(createNewStack);
|
|
1327
|
-
el("emptyImportBtn").onclick = () => run(importStackJson);
|
|
1328
|
-
setStatus("No prompt stacks found");
|
|
1329
|
-
updateActionState();
|
|
1330
|
-
}
|
|
1331
|
-
function field(label, control, className = "") {
|
|
1332
|
-
return '<div class="field ' + className + '"><label>' + escapeHtml(label) + '</label>' + control + '</div>';
|
|
1333
|
-
}
|
|
1334
|
-
function displayItemName(item) {
|
|
1335
|
-
if (item.name)
|
|
1336
|
-
return item.name;
|
|
1337
|
-
if (item.source && typeof item.source.previousName === "string" && item.source.previousName.trim())
|
|
1338
|
-
return item.source.previousName;
|
|
1339
|
-
if (item.kind === "slot" && item.slot)
|
|
1340
|
-
return item.slot;
|
|
1341
|
-
if (item.kind === "block" && item.content) {
|
|
1342
|
-
const firstLine = item.content.trim().split(/\n/)[0]?.trim();
|
|
1343
|
-
if (firstLine)
|
|
1344
|
-
return firstLine.length > 46 ? firstLine.slice(0, 43) + "..." : firstLine;
|
|
1345
|
-
}
|
|
1346
|
-
return item.id || "(unnamed)";
|
|
1347
|
-
}
|
|
1348
|
-
function setOptionalString(target, key, value) {
|
|
1349
|
-
const trimmed = value.trim();
|
|
1350
|
-
if (trimmed)
|
|
1351
|
-
target[key] = value;
|
|
1352
|
-
else
|
|
1353
|
-
delete target[key];
|
|
1354
|
-
}
|
|
1355
|
-
async function run(action) {
|
|
1356
|
-
try {
|
|
1357
|
-
await action();
|
|
1358
|
-
}
|
|
1359
|
-
catch (error) {
|
|
1360
|
-
setStatus(error instanceof Error ? error.message : String(error), "error");
|
|
1361
|
-
}
|
|
1362
|
-
}
|
|
1363
|
-
function toggleSidebar() {
|
|
1364
|
-
sidebarCollapsed = !sidebarCollapsed;
|
|
1365
|
-
el("shell").classList.toggle("sidebar-collapsed", sidebarCollapsed);
|
|
1366
|
-
el("sidebarToggleBtn").title = sidebarCollapsed ? "Show prompt stacks sidebar" : "Hide prompt stacks sidebar";
|
|
1367
|
-
setStatus(sidebarCollapsed ? "Prompt stacks sidebar hidden" : "Prompt stacks sidebar shown");
|
|
1368
|
-
}
|
|
1369
|
-
el("sidebarToggleBtn").onclick = toggleSidebar;
|
|
1370
|
-
el("themeBtn").onclick = toggleTheme;
|
|
1371
|
-
el("newStackBtn").onclick = () => run(createNewStack);
|
|
1372
|
-
el("reloadBtn").onclick = () => run(reloadFromDisk);
|
|
1373
|
-
el("disableBtn").onclick = () => run(disableStacks);
|
|
1374
|
-
el("activateBtn").onclick = () => run(activateStack);
|
|
1375
|
-
el("saveBtn").onclick = () => run(saveStack);
|
|
1376
|
-
el("validateBtn").onclick = () => run(validateStack);
|
|
1377
|
-
el("previewBtn").onclick = () => run(previewStack);
|
|
1378
|
-
el("payloadBtn").onclick = () => run(openPayloadCapture);
|
|
1379
|
-
el("metadataToggleBtn").onclick = toggleMetadata;
|
|
1380
|
-
document.querySelectorAll("[data-tab]").forEach((button) => {
|
|
1381
|
-
button.onclick = () => {
|
|
1382
|
-
activeTab = button.dataset.tab || "items";
|
|
1383
|
-
renderActiveTab();
|
|
1384
|
-
hidePreview();
|
|
1385
|
-
};
|
|
1386
|
-
});
|
|
1387
|
-
el("forkBtn").onclick = () => run(forkStack);
|
|
1388
|
-
el("importBtn").onclick = () => run(importStackJson);
|
|
1389
|
-
el("exportBtn").onclick = () => run(exportStackJson);
|
|
1390
|
-
el("importFileInput").onchange = (event) => run(() => handleImportFile(event));
|
|
1391
|
-
el("deleteStackBtn").onclick = () => run(deleteCurrentStack);
|
|
1392
|
-
el("stackModal").onclick = (event) => {
|
|
1393
|
-
if (event.target === el("stackModal") || event.target.closest?.("[data-modal-close]")) {
|
|
1394
|
-
closeStackModal();
|
|
1395
|
-
}
|
|
1396
|
-
};
|
|
1397
|
-
el("preview").onclick = (event) => {
|
|
1398
|
-
if (event.target === el("preview") || event.target.closest?.("[data-preview-close]")) {
|
|
1399
|
-
hidePreview();
|
|
1400
|
-
return;
|
|
1401
|
-
}
|
|
1402
|
-
if (event.target.closest?.("[data-payload-arm]")) {
|
|
1403
|
-
event.preventDefault();
|
|
1404
|
-
event.stopPropagation();
|
|
1405
|
-
run(() => armPayloadCapture(true));
|
|
1406
|
-
return;
|
|
1407
|
-
}
|
|
1408
|
-
if (event.target.closest?.("[data-payload-clear]")) {
|
|
1409
|
-
event.preventDefault();
|
|
1410
|
-
event.stopPropagation();
|
|
1411
|
-
run(clearPayloadCapture);
|
|
1412
|
-
return;
|
|
1413
|
-
}
|
|
1414
|
-
const button = event.target.closest?.("[data-copy-index]");
|
|
1415
|
-
if (!button)
|
|
1416
|
-
return;
|
|
1417
|
-
event.preventDefault();
|
|
1418
|
-
event.stopPropagation();
|
|
1419
|
-
run(() => copyPreviewText(Number(button.dataset.copyIndex)));
|
|
1420
|
-
};
|
|
1421
|
-
el("addItemBtn").onclick = () => addItem("block");
|
|
1422
|
-
el("addSlotBtn").onclick = () => addItem("slot");
|
|
1423
|
-
el("deleteItemBtn").onclick = deleteSelectedItem;
|
|
1424
|
-
document.addEventListener("dragover", handleDocumentItemDragOver);
|
|
1425
|
-
document.addEventListener("drop", handleDocumentItemDrop);
|
|
1426
|
-
window.onbeforeunload = () => dirty ? "Unsaved changes" : undefined;
|
|
1427
|
-
window.addEventListener("keydown", handleEditorShortcut);
|
|
1428
|
-
function handleEditorShortcut(event) {
|
|
1429
|
-
if (event.key === "Escape") {
|
|
1430
|
-
if (el("preview").classList.contains("open"))
|
|
1431
|
-
hidePreview();
|
|
1432
|
-
else if (el("stackModal").classList.contains("open"))
|
|
1433
|
-
closeStackModal();
|
|
1434
|
-
return;
|
|
1435
|
-
}
|
|
1436
|
-
const modifier = event.ctrlKey || event.metaKey;
|
|
1437
|
-
if (!modifier || event.altKey)
|
|
1438
|
-
return;
|
|
1439
|
-
const key = event.key.toLowerCase();
|
|
1440
|
-
if (key === "n") {
|
|
1441
|
-
event.preventDefault();
|
|
1442
|
-
run(createNewStack);
|
|
1443
|
-
return;
|
|
1444
|
-
}
|
|
1445
|
-
if (key === "s") {
|
|
1446
|
-
event.preventDefault();
|
|
1447
|
-
if (currentStack)
|
|
1448
|
-
run(saveStack);
|
|
1449
|
-
return;
|
|
1450
|
-
}
|
|
1451
|
-
if (key === "enter" && event.shiftKey) {
|
|
1452
|
-
event.preventDefault();
|
|
1453
|
-
if (currentStack)
|
|
1454
|
-
run(validateStack);
|
|
1455
|
-
return;
|
|
1456
|
-
}
|
|
1457
|
-
if (key === "enter") {
|
|
1458
|
-
event.preventDefault();
|
|
1459
|
-
if (currentStack)
|
|
1460
|
-
run(previewStack);
|
|
1461
|
-
}
|
|
1462
|
-
}
|
|
1463
|
-
run(async () => {
|
|
1464
|
-
await loadStacks();
|
|
1465
|
-
await refreshPayloadCapture();
|
|
1466
|
-
setInterval(() => run(() => refreshPayloadCapture({ autoOpen: true })), 2000);
|
|
1467
|
-
});
|
|
1468
|
-
//# sourceMappingURL=main.js.map
|