@zihanw/pi-forge 0.3.0 → 0.3.2
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 +87 -2
- package/README.zh-CN.md +72 -2
- package/dist/compiler.d.ts +10 -0
- package/dist/compiler.d.ts.map +1 -0
- package/dist/compiler.js +490 -0
- package/dist/compiler.js.map +1 -0
- package/dist/extension-registry.d.ts +25 -0
- package/dist/extension-registry.d.ts.map +1 -0
- package/dist/extension-registry.js +6 -0
- package/dist/extension-registry.js.map +1 -0
- package/dist/forge-extensions.d.ts +28 -0
- package/dist/forge-extensions.d.ts.map +1 -0
- package/dist/forge-extensions.js +182 -0
- package/dist/forge-extensions.js.map +1 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +376 -0
- package/dist/index.js.map +1 -0
- package/dist/lifecycle.d.ts +14 -0
- package/dist/lifecycle.d.ts.map +1 -0
- package/dist/lifecycle.js +139 -0
- package/dist/lifecycle.js.map +1 -0
- package/dist/loader.d.ts +8 -0
- package/dist/loader.d.ts.map +1 -0
- package/dist/loader.js +345 -0
- package/dist/loader.js.map +1 -0
- package/dist/macro-engine.d.ts +23 -0
- package/dist/macro-engine.d.ts.map +1 -0
- package/dist/macro-engine.js +262 -0
- package/dist/macro-engine.js.map +1 -0
- package/dist/payload-capture.d.ts +15 -0
- package/dist/payload-capture.d.ts.map +1 -0
- package/dist/payload-capture.js +86 -0
- package/dist/payload-capture.js.map +1 -0
- package/dist/payload-command.d.ts +10 -0
- package/dist/payload-command.d.ts.map +1 -0
- package/dist/payload-command.js +112 -0
- package/dist/payload-command.js.map +1 -0
- package/dist/policy.d.ts +6 -0
- package/dist/policy.d.ts.map +1 -0
- package/dist/policy.js +38 -0
- package/dist/policy.js.map +1 -0
- package/dist/preset-command.d.ts +14 -0
- package/dist/preset-command.d.ts.map +1 -0
- package/dist/preset-command.js +232 -0
- package/dist/preset-command.js.map +1 -0
- package/dist/preview.d.ts +12 -0
- package/dist/preview.d.ts.map +1 -0
- package/dist/preview.js +179 -0
- package/dist/preview.js.map +1 -0
- package/dist/regex.d.ts +7 -0
- package/dist/regex.d.ts.map +1 -0
- package/dist/regex.js +432 -0
- package/dist/regex.js.map +1 -0
- package/dist/render-helpers.d.ts +44 -0
- package/dist/render-helpers.d.ts.map +1 -0
- package/dist/render-helpers.js +128 -0
- package/dist/render-helpers.js.map +1 -0
- package/dist/runtime-state.d.ts +26 -0
- package/dist/runtime-state.d.ts.map +1 -0
- package/dist/runtime-state.js +15 -0
- package/dist/runtime-state.js.map +1 -0
- package/dist/sillytavern-importer/items.d.ts +3 -0
- package/dist/sillytavern-importer/items.d.ts.map +1 -0
- package/dist/sillytavern-importer/items.js +88 -0
- package/dist/sillytavern-importer/items.js.map +1 -0
- package/dist/sillytavern-importer/macros.d.ts +15 -0
- package/dist/sillytavern-importer/macros.d.ts.map +1 -0
- package/dist/sillytavern-importer/macros.js +141 -0
- package/dist/sillytavern-importer/macros.js.map +1 -0
- package/dist/sillytavern-importer/prompt-order.d.ts +6 -0
- package/dist/sillytavern-importer/prompt-order.d.ts.map +1 -0
- package/dist/sillytavern-importer/prompt-order.js +38 -0
- package/dist/sillytavern-importer/prompt-order.js.map +1 -0
- package/dist/sillytavern-importer/regex.d.ts +3 -0
- package/dist/sillytavern-importer/regex.d.ts.map +1 -0
- package/dist/sillytavern-importer/regex.js +275 -0
- package/dist/sillytavern-importer/regex.js.map +1 -0
- package/dist/sillytavern-importer/report.d.ts +21 -0
- package/dist/sillytavern-importer/report.d.ts.map +1 -0
- package/dist/sillytavern-importer/report.js +166 -0
- package/dist/sillytavern-importer/report.js.map +1 -0
- package/dist/sillytavern-importer/types.d.ts +106 -0
- package/dist/sillytavern-importer/types.d.ts.map +1 -0
- package/dist/sillytavern-importer/types.js +2 -0
- package/dist/sillytavern-importer/types.js.map +1 -0
- package/dist/sillytavern-importer.d.ts +5 -0
- package/dist/sillytavern-importer.d.ts.map +1 -0
- package/dist/sillytavern-importer.js +117 -0
- package/dist/sillytavern-importer.js.map +1 -0
- package/dist/slot-renderers.d.ts +25 -0
- package/dist/slot-renderers.d.ts.map +1 -0
- package/dist/slot-renderers.js +334 -0
- package/dist/slot-renderers.js.map +1 -0
- package/dist/stack-migration.d.ts +29 -0
- package/dist/stack-migration.d.ts.map +1 -0
- package/dist/stack-migration.js +126 -0
- package/dist/stack-migration.js.map +1 -0
- package/dist/storage.d.ts +10 -0
- package/dist/storage.d.ts.map +1 -0
- package/dist/storage.js +36 -0
- package/dist/storage.js.map +1 -0
- package/dist/types.d.ts +173 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +2 -0
- package/dist/types.js.map +1 -0
- package/dist/web-editor/index.d.ts +3 -0
- package/dist/web-editor/index.d.ts.map +1 -0
- package/dist/web-editor/index.js +2 -0
- package/dist/web-editor/index.js.map +1 -0
- package/dist/web-editor/page.d.ts +2 -0
- package/dist/web-editor/page.d.ts.map +1 -0
- package/dist/web-editor/page.js +3331 -0
- package/dist/web-editor/page.js.map +1 -0
- package/dist/web-editor/server.d.ts +4 -0
- package/dist/web-editor/server.d.ts.map +1 -0
- package/dist/web-editor/server.js +263 -0
- package/dist/web-editor/server.js.map +1 -0
- package/dist/web-editor/types.d.ts +125 -0
- package/dist/web-editor/types.d.ts.map +1 -0
- package/dist/web-editor/types.js +2 -0
- package/dist/web-editor/types.js.map +1 -0
- package/dist/web-host.d.ts +29 -0
- package/dist/web-host.d.ts.map +1 -0
- package/dist/web-host.js +174 -0
- package/dist/web-host.js.map +1 -0
- package/examples/custom-system-status-extension/README.md +85 -0
- package/examples/custom-system-status-extension/index.ts +136 -0
- package/examples/custom-system-status-extension/prompt-stack.json +39 -0
- package/examples/image-reader-prompt-stack.json +124 -0
- package/package.json +28 -2
- package/src/compiler.ts +2 -472
- package/src/extension-registry.ts +33 -0
- package/src/forge-extensions.ts +223 -0
- package/src/index.ts +208 -653
- package/src/lifecycle.ts +171 -0
- package/src/macro-engine.ts +358 -0
- package/src/payload-command.ts +138 -0
- package/src/preset-command.ts +280 -0
- package/src/preview.ts +226 -0
- package/src/render-helpers.ts +169 -0
- package/src/runtime-state.ts +40 -0
- package/src/sillytavern-importer/items.ts +98 -0
- package/src/sillytavern-importer/macros.ts +159 -0
- package/src/sillytavern-importer/prompt-order.ts +54 -0
- package/src/sillytavern-importer/regex.ts +270 -0
- package/src/sillytavern-importer/report.ts +202 -0
- package/src/sillytavern-importer/types.ts +120 -0
- package/src/sillytavern-importer.ts +55 -865
- package/src/slot-renderers.ts +414 -0
- package/src/storage.ts +17 -0
- package/src/types.ts +3 -14
- package/src/web-editor/page.ts +516 -30
- package/src/web-editor/server.ts +9 -4
- package/src/web-editor/types.ts +18 -4
- package/src/web-host.ts +16 -13
package/src/web-editor/page.ts
CHANGED
|
@@ -192,6 +192,11 @@ html, body {
|
|
|
192
192
|
overflow: auto;
|
|
193
193
|
height: calc(100% - 74px);
|
|
194
194
|
}
|
|
195
|
+
.side-empty {
|
|
196
|
+
color: var(--muted);
|
|
197
|
+
font-size: 12px;
|
|
198
|
+
padding: 8px;
|
|
199
|
+
}
|
|
195
200
|
.stack-row {
|
|
196
201
|
display: block;
|
|
197
202
|
width: 100%;
|
|
@@ -579,6 +584,17 @@ html, body {
|
|
|
579
584
|
color: var(--muted);
|
|
580
585
|
padding: 24px;
|
|
581
586
|
}
|
|
587
|
+
.empty-title {
|
|
588
|
+
color: var(--text);
|
|
589
|
+
font-weight: 650;
|
|
590
|
+
margin-bottom: 4px;
|
|
591
|
+
}
|
|
592
|
+
.empty-actions {
|
|
593
|
+
display: flex;
|
|
594
|
+
gap: 8px;
|
|
595
|
+
flex-wrap: wrap;
|
|
596
|
+
margin-top: 14px;
|
|
597
|
+
}
|
|
582
598
|
.diagnostics {
|
|
583
599
|
border-top: 1px solid var(--line);
|
|
584
600
|
background: var(--pane);
|
|
@@ -798,7 +814,7 @@ html, body {
|
|
|
798
814
|
grid-template-columns: minmax(160px, 240px) minmax(220px, 1fr) minmax(180px, 260px) 168px;
|
|
799
815
|
}
|
|
800
816
|
.policy-row {
|
|
801
|
-
grid-template-columns: minmax(
|
|
817
|
+
grid-template-columns: minmax(110px, 150px) minmax(230px, 290px) minmax(220px, 1fr) minmax(220px, 1fr) minmax(150px, 220px);
|
|
802
818
|
}
|
|
803
819
|
.policy-title {
|
|
804
820
|
font-weight: 650;
|
|
@@ -812,11 +828,77 @@ html, body {
|
|
|
812
828
|
.policy-patterns {
|
|
813
829
|
min-height: 96px;
|
|
814
830
|
}
|
|
831
|
+
.selected-patterns {
|
|
832
|
+
display: flex;
|
|
833
|
+
flex-wrap: wrap;
|
|
834
|
+
gap: 6px;
|
|
835
|
+
margin-bottom: 8px;
|
|
836
|
+
}
|
|
837
|
+
.selected-pattern-chip {
|
|
838
|
+
min-height: 26px;
|
|
839
|
+
padding: 2px 7px;
|
|
840
|
+
font-size: 12px;
|
|
841
|
+
border-color: var(--accent);
|
|
842
|
+
background: var(--accent-bg);
|
|
843
|
+
color: var(--accent);
|
|
844
|
+
overflow-wrap: anywhere;
|
|
845
|
+
}
|
|
846
|
+
.selected-pattern-chip span {
|
|
847
|
+
margin-left: 6px;
|
|
848
|
+
color: var(--muted);
|
|
849
|
+
}
|
|
850
|
+
.selected-pattern-empty {
|
|
851
|
+
color: var(--muted);
|
|
852
|
+
font-size: 12px;
|
|
853
|
+
min-height: 26px;
|
|
854
|
+
display: inline-flex;
|
|
855
|
+
align-items: center;
|
|
856
|
+
}
|
|
815
857
|
.policy-summary {
|
|
816
858
|
color: var(--muted);
|
|
817
859
|
font-size: 12px;
|
|
818
860
|
overflow-wrap: anywhere;
|
|
819
861
|
}
|
|
862
|
+
.resource-picker label {
|
|
863
|
+
display: block;
|
|
864
|
+
color: var(--muted);
|
|
865
|
+
font-size: 12px;
|
|
866
|
+
margin-bottom: 4px;
|
|
867
|
+
}
|
|
868
|
+
.resource-list {
|
|
869
|
+
display: flex;
|
|
870
|
+
flex-wrap: wrap;
|
|
871
|
+
gap: 6px;
|
|
872
|
+
max-height: 132px;
|
|
873
|
+
overflow: auto;
|
|
874
|
+
padding-right: 2px;
|
|
875
|
+
}
|
|
876
|
+
.resource-filter {
|
|
877
|
+
margin-bottom: 8px;
|
|
878
|
+
}
|
|
879
|
+
.resource-chip {
|
|
880
|
+
max-width: 100%;
|
|
881
|
+
min-height: 26px;
|
|
882
|
+
padding: 2px 7px;
|
|
883
|
+
font-size: 12px;
|
|
884
|
+
overflow-wrap: anywhere;
|
|
885
|
+
}
|
|
886
|
+
.resource-chip.active {
|
|
887
|
+
border-color: var(--accent);
|
|
888
|
+
background: var(--accent-bg);
|
|
889
|
+
color: var(--accent);
|
|
890
|
+
}
|
|
891
|
+
.resource-chip.hidden {
|
|
892
|
+
border-style: dashed;
|
|
893
|
+
opacity: .72;
|
|
894
|
+
}
|
|
895
|
+
.resource-empty {
|
|
896
|
+
color: var(--muted);
|
|
897
|
+
font-size: 12px;
|
|
898
|
+
border: 1px dashed var(--line);
|
|
899
|
+
border-radius: 6px;
|
|
900
|
+
padding: 8px;
|
|
901
|
+
}
|
|
820
902
|
.regex-row {
|
|
821
903
|
grid-template-columns: 72px minmax(0, 1fr) 86px;
|
|
822
904
|
}
|
|
@@ -939,10 +1021,11 @@ html, body {
|
|
|
939
1021
|
</aside>
|
|
940
1022
|
<main class="main">
|
|
941
1023
|
<div class="main-actions">
|
|
1024
|
+
<button id="newStackBtn" data-icon="+" title="Create a new prompt stack (Ctrl/Cmd+N)">New stack</button>
|
|
942
1025
|
<button id="activateBtn" class="primary" data-icon="▶" title="Make this stack active for the current Pi session">Activate</button>
|
|
943
|
-
<button id="saveBtn" class="primary" data-icon="✓" title="Save the edited stack JSON to disk">Save</button>
|
|
944
|
-
<button id="validateBtn" data-icon="!" title="Validate the edited stack without saving">Validate</button>
|
|
945
|
-
<button id="previewBtn" data-icon="◱" title="Preview the compiled prompt without sending it">Preview</button>
|
|
1026
|
+
<button id="saveBtn" class="primary" data-icon="✓" title="Save the edited stack JSON to disk (Ctrl/Cmd+S)">Save</button>
|
|
1027
|
+
<button id="validateBtn" data-icon="!" title="Validate the edited stack without saving (Ctrl/Cmd+Shift+Enter)">Validate</button>
|
|
1028
|
+
<button id="previewBtn" data-icon="◱" title="Preview the compiled prompt without sending it (Ctrl/Cmd+Enter)">Preview</button>
|
|
946
1029
|
<button id="payloadBtn" data-icon="◆" title="Capture the next provider payload in the browser">Arm payload</button>
|
|
947
1030
|
<button id="forkBtn" data-icon="⑂" title="Create a new stack from the current edits">Fork</button>
|
|
948
1031
|
<button id="importBtn" data-icon="⇪" title="Import pi-forge stack JSON or SillyTavern preset JSON">Import JSON</button>
|
|
@@ -972,7 +1055,8 @@ html, body {
|
|
|
972
1055
|
<span id="itemCount" class="stack-meta"></span>
|
|
973
1056
|
</div>
|
|
974
1057
|
<div class="item-tools">
|
|
975
|
-
<button id="addItemBtn" data-icon="+" title="Add
|
|
1058
|
+
<button id="addItemBtn" data-icon="+" title="Add a prompt block item">Add block</button>
|
|
1059
|
+
<button id="addSlotBtn" data-icon="+" title="Add a runtime slot item">Add slot</button>
|
|
976
1060
|
<span class="item-tools-spacer"></span>
|
|
977
1061
|
<button id="deleteItemBtn" class="danger" data-icon="×" title="Delete the selected stack item">Delete item</button>
|
|
978
1062
|
</div>
|
|
@@ -1008,6 +1092,7 @@ let sidebarCollapsed = false;
|
|
|
1008
1092
|
let slotOptionsMode = "form";
|
|
1009
1093
|
let previewCopyTexts = [];
|
|
1010
1094
|
let payloadSnapshot = { status: "idle" };
|
|
1095
|
+
let policyResources = { tools: [], skills: [] };
|
|
1011
1096
|
let latestDiagnostics = [];
|
|
1012
1097
|
let stackVariablesError = "";
|
|
1013
1098
|
let regexRulesError = "";
|
|
@@ -1097,11 +1182,29 @@ function renderDirtyState() {
|
|
|
1097
1182
|
const badge = el("dirtyBadge");
|
|
1098
1183
|
if (!badge) return;
|
|
1099
1184
|
badge.classList.toggle("visible", dirty);
|
|
1185
|
+
updateActionState();
|
|
1186
|
+
}
|
|
1187
|
+
|
|
1188
|
+
function updateActionState() {
|
|
1189
|
+
const hasStack = !!currentStack;
|
|
1190
|
+
for (const id of ["activateBtn", "saveBtn", "validateBtn", "previewBtn", "forkBtn", "exportBtn", "deleteStackBtn", "addItemBtn", "addSlotBtn", "metadataToggleBtn"]) {
|
|
1191
|
+
const button = el(id);
|
|
1192
|
+
if (button) button.disabled = !hasStack;
|
|
1193
|
+
}
|
|
1194
|
+
const deleteItemButton = el("deleteItemBtn");
|
|
1195
|
+
if (deleteItemButton) deleteItemButton.disabled = !hasStack || selectedItemIndex < 0;
|
|
1196
|
+
document.querySelectorAll("[data-tab]").forEach((button) => {
|
|
1197
|
+
button.disabled = !hasStack;
|
|
1198
|
+
});
|
|
1100
1199
|
}
|
|
1101
1200
|
|
|
1102
1201
|
async function loadStacks(preferId = selectedId) {
|
|
1103
|
-
const data = await
|
|
1202
|
+
const [data, resources] = await Promise.all([
|
|
1203
|
+
api("/api/stacks"),
|
|
1204
|
+
api("/api/resources"),
|
|
1205
|
+
]);
|
|
1104
1206
|
stacks = data.stacks || [];
|
|
1207
|
+
policyResources = normalizePolicyResources(resources);
|
|
1105
1208
|
cwd = data.cwd || "";
|
|
1106
1209
|
el("cwd").textContent = cwd;
|
|
1107
1210
|
renderStackList();
|
|
@@ -1134,6 +1237,7 @@ function renderAll(diagnostics = []) {
|
|
|
1134
1237
|
renderActiveTab();
|
|
1135
1238
|
renderDiagnostics(diagnostics);
|
|
1136
1239
|
hidePreview();
|
|
1240
|
+
updateActionState();
|
|
1137
1241
|
}
|
|
1138
1242
|
|
|
1139
1243
|
function renderActiveTab() {
|
|
@@ -1160,6 +1264,10 @@ function renderActiveTab() {
|
|
|
1160
1264
|
function renderStackList() {
|
|
1161
1265
|
const list = el("stackList");
|
|
1162
1266
|
list.innerHTML = "";
|
|
1267
|
+
if (!stacks.length) {
|
|
1268
|
+
list.innerHTML = '<div class="side-empty">No prompt stacks in this project.</div>';
|
|
1269
|
+
return;
|
|
1270
|
+
}
|
|
1163
1271
|
for (const stack of stacks) {
|
|
1164
1272
|
const row = document.createElement("button");
|
|
1165
1273
|
row.className = "stack-row" + (stack.active ? " active" : "") + (stack.id === selectedId ? " selected" : "");
|
|
@@ -1172,6 +1280,26 @@ function renderStackList() {
|
|
|
1172
1280
|
}
|
|
1173
1281
|
}
|
|
1174
1282
|
|
|
1283
|
+
function normalizePolicyResources(value) {
|
|
1284
|
+
return {
|
|
1285
|
+
tools: normalizeResourceList(value?.tools),
|
|
1286
|
+
skills: normalizeResourceList(value?.skills),
|
|
1287
|
+
};
|
|
1288
|
+
}
|
|
1289
|
+
|
|
1290
|
+
function normalizeResourceList(value) {
|
|
1291
|
+
if (!Array.isArray(value)) return [];
|
|
1292
|
+
return value
|
|
1293
|
+
.filter((resource) => resource && typeof resource === "object" && typeof resource.name === "string" && resource.name.trim())
|
|
1294
|
+
.map((resource) => ({
|
|
1295
|
+
name: resource.name.trim(),
|
|
1296
|
+
description: typeof resource.description === "string" ? resource.description : "",
|
|
1297
|
+
source: typeof resource.source === "string" ? resource.source : "",
|
|
1298
|
+
active: resource.active === true,
|
|
1299
|
+
hidden: resource.hidden === true,
|
|
1300
|
+
}));
|
|
1301
|
+
}
|
|
1302
|
+
|
|
1175
1303
|
function renderSettings() {
|
|
1176
1304
|
const settings = el("settings");
|
|
1177
1305
|
if (!currentStack) {
|
|
@@ -1492,6 +1620,9 @@ function renderSlotOptionsForm(item, options) {
|
|
|
1492
1620
|
if (["tools", "tool-guidelines", "skills", "project-context"].includes(item.slot)) {
|
|
1493
1621
|
fields.push(optionSelect("format", "Format", options.format || "xml", ["xml", "plain"]));
|
|
1494
1622
|
}
|
|
1623
|
+
if (["date", "date-cwd"].includes(item.slot)) {
|
|
1624
|
+
fields.push(optionCheckbox("includeTime", "Include current time", options.includeTime === true));
|
|
1625
|
+
}
|
|
1495
1626
|
if (fields.length === 0) {
|
|
1496
1627
|
fields.push('<div class="wide option-note">This slot has no structured options yet. Use JSON mode for advanced settings.</div>');
|
|
1497
1628
|
}
|
|
@@ -1555,7 +1686,7 @@ function setSlotOption(item, key, value, defaultValue) {
|
|
|
1555
1686
|
|
|
1556
1687
|
function defaultSlotOptionValue(key) {
|
|
1557
1688
|
if (["includeLastUserMessage", "includeSummaries", "includeStatic", "includeSession", "includeTurn"].includes(key)) return true;
|
|
1558
|
-
if (
|
|
1689
|
+
if (["stripAssistantThinking", "includeTime"].includes(key)) return false;
|
|
1559
1690
|
if (key === "toolMode") return "keep";
|
|
1560
1691
|
if (key === "format") return "xml";
|
|
1561
1692
|
return undefined;
|
|
@@ -1591,6 +1722,7 @@ function optionHelp(key) {
|
|
|
1591
1722
|
includeStatic: "Include static stack variables in this variables slot.",
|
|
1592
1723
|
includeSession: "Include session variables created by template macros.",
|
|
1593
1724
|
includeTurn: "Include temporary turn variables created during prompt compilation.",
|
|
1725
|
+
includeTime: "Render the current time in HH:MM:SS after the current date.",
|
|
1594
1726
|
format: "Choose XML or compact plain text rendering.",
|
|
1595
1727
|
};
|
|
1596
1728
|
return descriptions[key] || "Advanced slot option.";
|
|
@@ -1652,7 +1784,7 @@ function renderPolicyTab() {
|
|
|
1652
1784
|
'<div class="tab-section-title">Tool and skill policy</div>' +
|
|
1653
1785
|
'<div class="tab-section-meta">Choose one mode per resource. Patterns support exact names and * wildcards.</div>' +
|
|
1654
1786
|
'<div class="data-table" id="policyRows">' +
|
|
1655
|
-
'<div class="data-row header policy-row"><div>Resource</div><div>Mode</div><div>Patterns</div><div>Status</div></div>' +
|
|
1787
|
+
'<div class="data-row header policy-row"><div>Resource</div><div>Mode</div><div>Patterns</div><div>Available</div><div>Status</div></div>' +
|
|
1656
1788
|
policyRowHtml("tools", "Tools") +
|
|
1657
1789
|
policyRowHtml("skills", "Skills") +
|
|
1658
1790
|
'</div>' +
|
|
@@ -1663,7 +1795,9 @@ function renderPolicyTab() {
|
|
|
1663
1795
|
function policyRowHtml(kind, label) {
|
|
1664
1796
|
const policy = stackPolicyObject(kind);
|
|
1665
1797
|
const mode = policyMode(policy);
|
|
1666
|
-
const
|
|
1798
|
+
const rawPatterns = mode === "deny" ? policy.deny : mode === "allow" ? policy.allow : [];
|
|
1799
|
+
const patterns = Array.isArray(rawPatterns) ? rawPatterns : [];
|
|
1800
|
+
const patternText = policyPatternsToText(patterns);
|
|
1667
1801
|
const disabled = mode === "none" ? " disabled" : "";
|
|
1668
1802
|
return '<div class="data-row policy-row" data-policy-row data-policy-kind="' + attr(kind) + '" data-policy-mode="' + attr(mode) + '">' +
|
|
1669
1803
|
'<div><div class="policy-title">' + escapeHtml(label) + '</div><div class="modal-meta">' + escapeHtml(kind) + '</div></div>' +
|
|
@@ -1672,28 +1806,186 @@ function policyRowHtml(kind, label) {
|
|
|
1672
1806
|
policyModeButton("allow", "Allow", mode) +
|
|
1673
1807
|
policyModeButton("deny", "Deny", mode) +
|
|
1674
1808
|
'</div></div>' +
|
|
1675
|
-
'<div class="field"><label>Patterns</label
|
|
1809
|
+
'<div class="field"><label>Patterns</label>' + selectedPolicyPatternsHtml(patterns) + '<textarea class="policy-patterns" data-policy-patterns spellcheck="false" placeholder="' + attr(policyPatternPlaceholder(mode)) + '"' + disabled + '>' + escapeHtml(patternText) + '</textarea></div>' +
|
|
1810
|
+
resourcePickerHtml(kind, patterns) +
|
|
1676
1811
|
'<div class="policy-summary" data-policy-summary>' + escapeHtml(policySummary(kind, policy)) + '</div>' +
|
|
1677
1812
|
'</div>';
|
|
1678
1813
|
}
|
|
1679
1814
|
|
|
1815
|
+
function selectedPolicyPatternsHtml(patterns) {
|
|
1816
|
+
return '<div class="selected-patterns" data-selected-patterns>' + selectedPolicyPatternButtonsHtml(patterns) + '</div>';
|
|
1817
|
+
}
|
|
1818
|
+
|
|
1819
|
+
function selectedPolicyPatternButtonsHtml(patterns) {
|
|
1820
|
+
if (!patterns.length) return '<span class="selected-pattern-empty">No selected patterns.</span>';
|
|
1821
|
+
return patterns.map((pattern) =>
|
|
1822
|
+
'<button type="button" class="selected-pattern-chip" data-remove-policy-pattern="' + attr(pattern) + '" title="Remove selected pattern">' +
|
|
1823
|
+
escapeHtml(pattern) + '<span aria-hidden="true">x</span></button>'
|
|
1824
|
+
).join("");
|
|
1825
|
+
}
|
|
1826
|
+
|
|
1827
|
+
function resourcePickerHtml(kind, selectedPatterns = []) {
|
|
1828
|
+
const resources = policyResources[kind] || [];
|
|
1829
|
+
if (!resources.length) {
|
|
1830
|
+
return '<div class="resource-picker"><label>Available ' + escapeHtml(kind) + '</label><div class="resource-empty">No registered ' + escapeHtml(kind) + ' reported.</div></div>';
|
|
1831
|
+
}
|
|
1832
|
+
const listId = "resource-options-" + kind;
|
|
1833
|
+
return '<div class="resource-picker"><label>Available ' + escapeHtml(kind) + '</label><div>' +
|
|
1834
|
+
'<input class="resource-filter" data-resource-filter list="' + attr(listId) + '" placeholder="Type to filter or add">' +
|
|
1835
|
+
'<datalist id="' + attr(listId) + '" data-resource-options>' + resourceOptionsHtml(kind, selectedPatterns) + '</datalist>' +
|
|
1836
|
+
'<div class="resource-list" data-resource-list>' + resourceListHtml(kind, selectedPatterns) + '</div>' +
|
|
1837
|
+
'</div></div>';
|
|
1838
|
+
}
|
|
1839
|
+
|
|
1840
|
+
function resourceOptionsHtml(kind, selectedPatterns = []) {
|
|
1841
|
+
return availablePolicyResources(kind, selectedPatterns)
|
|
1842
|
+
.map((resource) => '<option value="' + attr(resource.name) + '"></option>')
|
|
1843
|
+
.join("");
|
|
1844
|
+
}
|
|
1845
|
+
|
|
1846
|
+
function resourceListHtml(kind, selectedPatterns = [], filter = "") {
|
|
1847
|
+
const resources = availablePolicyResources(kind, selectedPatterns, filter);
|
|
1848
|
+
if (!resources.length) return '<div class="resource-empty">No matching unselected ' + escapeHtml(kind) + '.</div>';
|
|
1849
|
+
return resources.map((resource) => resourceChipHtml(resource)).join("");
|
|
1850
|
+
}
|
|
1851
|
+
|
|
1852
|
+
function availablePolicyResources(kind, selectedPatterns = [], filter = "") {
|
|
1853
|
+
const selected = new Set(selectedPatterns);
|
|
1854
|
+
const needle = filter.trim().toLowerCase();
|
|
1855
|
+
return (policyResources[kind] || [])
|
|
1856
|
+
.filter((resource) => !selected.has(resource.name))
|
|
1857
|
+
.filter((resource) => !needle || policyResourceMatchesFilter(resource, needle));
|
|
1858
|
+
}
|
|
1859
|
+
|
|
1860
|
+
function policyResourceMatchesFilter(resource, needle) {
|
|
1861
|
+
return [resource.name, resource.description, resource.source]
|
|
1862
|
+
.filter(Boolean)
|
|
1863
|
+
.some((value) => String(value).toLowerCase().includes(needle));
|
|
1864
|
+
}
|
|
1865
|
+
|
|
1866
|
+
function resourceChipHtml(resource) {
|
|
1867
|
+
const classes = ["resource-chip"];
|
|
1868
|
+
if (resource.active) classes.push("active");
|
|
1869
|
+
if (resource.hidden) classes.push("hidden");
|
|
1870
|
+
const notes = [
|
|
1871
|
+
resource.description,
|
|
1872
|
+
resource.source ? "Source: " + resource.source : "",
|
|
1873
|
+
resource.active ? "Active tool" : "",
|
|
1874
|
+
resource.hidden ? "Hidden from model invocation" : "",
|
|
1875
|
+
].filter(Boolean);
|
|
1876
|
+
const suffix = resource.active ? " *" : resource.hidden ? " hidden" : "";
|
|
1877
|
+
return '<button type="button" class="' + attr(classes.join(" ")) + '" data-resource-name="' + attr(resource.name) + '" title="' + attr(notes.join("\n") || resource.name) + '">' +
|
|
1878
|
+
escapeHtml(resource.name + suffix) +
|
|
1879
|
+
'</button>';
|
|
1880
|
+
}
|
|
1881
|
+
|
|
1680
1882
|
function policyModeButton(value, label, current) {
|
|
1681
1883
|
return '<button type="button" data-policy-mode-option="' + attr(value) + '" class="' + (value === current ? "active" : "") + '">' + escapeHtml(label) + '</button>';
|
|
1682
1884
|
}
|
|
1683
1885
|
|
|
1684
1886
|
function bindPolicyEditor() {
|
|
1685
|
-
|
|
1686
|
-
|
|
1687
|
-
|
|
1688
|
-
|
|
1689
|
-
|
|
1887
|
+
const rows = el("policyRows");
|
|
1888
|
+
rows.onclick = (event) => {
|
|
1889
|
+
const modeButton = event.target.closest?.("[data-policy-mode-option]");
|
|
1890
|
+
if (modeButton) {
|
|
1891
|
+
const row = modeButton.closest("[data-policy-row]");
|
|
1892
|
+
setPolicyRowMode(row, modeButton.dataset.policyModeOption);
|
|
1893
|
+
if (modeButton.dataset.policyModeOption === "none") row.querySelector("[data-policy-patterns]").value = "";
|
|
1690
1894
|
syncResourcePolicyFromTab();
|
|
1691
|
-
|
|
1692
|
-
|
|
1693
|
-
|
|
1694
|
-
|
|
1695
|
-
|
|
1895
|
+
return;
|
|
1896
|
+
}
|
|
1897
|
+
|
|
1898
|
+
const removeButton = event.target.closest?.("[data-remove-policy-pattern]");
|
|
1899
|
+
if (removeButton) {
|
|
1900
|
+
const row = removeButton.closest("[data-policy-row]");
|
|
1901
|
+
removePolicyPattern(row, removeButton.dataset.removePolicyPattern || "");
|
|
1902
|
+
syncResourcePolicyFromTab();
|
|
1903
|
+
return;
|
|
1904
|
+
}
|
|
1905
|
+
|
|
1906
|
+
const resourceButton = event.target.closest?.("[data-resource-name]");
|
|
1907
|
+
if (resourceButton) {
|
|
1908
|
+
const row = resourceButton.closest("[data-policy-row]");
|
|
1909
|
+
addPolicyPattern(row, resourceButton.dataset.resourceName || "");
|
|
1910
|
+
clearPolicyResourceFilter(row);
|
|
1911
|
+
syncResourcePolicyFromTab();
|
|
1912
|
+
}
|
|
1913
|
+
};
|
|
1914
|
+
rows.oninput = (event) => {
|
|
1915
|
+
const target = event.target;
|
|
1916
|
+
if (target.matches?.("[data-policy-patterns]")) {
|
|
1917
|
+
syncResourcePolicyFromTab();
|
|
1918
|
+
return;
|
|
1919
|
+
}
|
|
1920
|
+
if (target.matches?.("[data-resource-filter]")) {
|
|
1921
|
+
refreshPolicyResourceControls(target.closest("[data-policy-row]"));
|
|
1922
|
+
}
|
|
1923
|
+
};
|
|
1924
|
+
rows.onkeydown = (event) => {
|
|
1925
|
+
const target = event.target;
|
|
1926
|
+
if (!target.matches?.("[data-resource-filter]") || event.key !== "Enter") return;
|
|
1927
|
+
event.preventDefault();
|
|
1928
|
+
const row = target.closest("[data-policy-row]");
|
|
1929
|
+
const name = policyResourceAutocompleteValue(row, target.value);
|
|
1930
|
+
if (!name) return;
|
|
1931
|
+
addPolicyPattern(row, name);
|
|
1932
|
+
target.value = "";
|
|
1933
|
+
syncResourcePolicyFromTab();
|
|
1934
|
+
};
|
|
1696
1935
|
refreshPolicySummaries();
|
|
1936
|
+
refreshPolicyResourceControls();
|
|
1937
|
+
}
|
|
1938
|
+
|
|
1939
|
+
function clearPolicyResourceFilter(row) {
|
|
1940
|
+
const filter = row?.querySelector("[data-resource-filter]");
|
|
1941
|
+
if (filter) filter.value = "";
|
|
1942
|
+
}
|
|
1943
|
+
|
|
1944
|
+
function policyResourceAutocompleteValue(row, value) {
|
|
1945
|
+
if (!row) return "";
|
|
1946
|
+
const typed = value.trim();
|
|
1947
|
+
if (!typed) return "";
|
|
1948
|
+
const kind = row.dataset.policyKind;
|
|
1949
|
+
const selected = selectedPolicyPatterns(row);
|
|
1950
|
+
const resources = availablePolicyResources(kind, selected, typed);
|
|
1951
|
+
const exact = resources.find((resource) => resource.name.toLowerCase() === typed.toLowerCase());
|
|
1952
|
+
return exact?.name || resources[0]?.name || typed;
|
|
1953
|
+
}
|
|
1954
|
+
|
|
1955
|
+
function removePolicyPattern(row, pattern) {
|
|
1956
|
+
if (!row || !pattern) return;
|
|
1957
|
+
const textarea = row.querySelector("[data-policy-patterns]");
|
|
1958
|
+
const patterns = parsePolicyPatterns(textarea.value).filter((candidate) => candidate !== pattern);
|
|
1959
|
+
textarea.value = patterns.join("\n");
|
|
1960
|
+
}
|
|
1961
|
+
|
|
1962
|
+
function refreshPolicyResourceControls(root = document) {
|
|
1963
|
+
const rows = root.matches?.("[data-policy-row]") ? [root] : root.querySelectorAll("[data-policy-row]");
|
|
1964
|
+
rows.forEach((row) => {
|
|
1965
|
+
const kind = row.dataset.policyKind;
|
|
1966
|
+
const patterns = selectedPolicyPatterns(row);
|
|
1967
|
+
const selected = row.querySelector("[data-selected-patterns]");
|
|
1968
|
+
if (selected) selected.innerHTML = selectedPolicyPatternButtonsHtml(patterns);
|
|
1969
|
+
const filter = row.querySelector("[data-resource-filter]")?.value || "";
|
|
1970
|
+
const options = row.querySelector("[data-resource-options]");
|
|
1971
|
+
if (options) options.innerHTML = resourceOptionsHtml(kind, patterns);
|
|
1972
|
+
const list = row.querySelector("[data-resource-list]");
|
|
1973
|
+
if (list) list.innerHTML = resourceListHtml(kind, patterns, filter);
|
|
1974
|
+
});
|
|
1975
|
+
}
|
|
1976
|
+
|
|
1977
|
+
function selectedPolicyPatterns(row) {
|
|
1978
|
+
if (!row || (row.dataset.policyMode || "none") === "none") return [];
|
|
1979
|
+
return parsePolicyPatterns(row.querySelector("[data-policy-patterns]")?.value || "");
|
|
1980
|
+
}
|
|
1981
|
+
|
|
1982
|
+
function addPolicyPattern(row, name) {
|
|
1983
|
+
if (!row || !name) return;
|
|
1984
|
+
if ((row.dataset.policyMode || "none") === "none") setPolicyRowMode(row, "allow");
|
|
1985
|
+
const textarea = row.querySelector("[data-policy-patterns]");
|
|
1986
|
+
const patterns = parsePolicyPatterns(textarea.value);
|
|
1987
|
+
if (!patterns.includes(name)) patterns.push(name);
|
|
1988
|
+
textarea.value = patterns.join("\n");
|
|
1697
1989
|
}
|
|
1698
1990
|
|
|
1699
1991
|
function syncResourcePolicyFromTab() {
|
|
@@ -1717,6 +2009,7 @@ function syncResourcePolicyFromTab() {
|
|
|
1717
2009
|
stackPolicyError = errors[0] || "";
|
|
1718
2010
|
markDirty();
|
|
1719
2011
|
refreshPolicySummaries();
|
|
2012
|
+
refreshPolicyResourceControls();
|
|
1720
2013
|
if (stackPolicyError) setStatus(stackPolicyError, "error");
|
|
1721
2014
|
}
|
|
1722
2015
|
|
|
@@ -2284,7 +2577,7 @@ async function createStackRemote(stack, options = {}) {
|
|
|
2284
2577
|
}
|
|
2285
2578
|
}
|
|
2286
2579
|
|
|
2287
|
-
async function
|
|
2580
|
+
async function createAndOpenStack(stack, activate, actionLabel, extraOptions = {}) {
|
|
2288
2581
|
const data = await createStackRemote(stack, { ...extraOptions, activate });
|
|
2289
2582
|
stacks = data.stacks || stacks;
|
|
2290
2583
|
selectedId = data.stack?.id || stack.id;
|
|
@@ -2296,6 +2589,143 @@ async function openImportedStack(stack, activate, actionLabel, extraOptions = {}
|
|
|
2296
2589
|
if (data.importReport) showImportReport(data.importReport, selectedId);
|
|
2297
2590
|
}
|
|
2298
2591
|
|
|
2592
|
+
async function createNewStack() {
|
|
2593
|
+
if (dirty && !confirm("Discard unsaved changes?")) return;
|
|
2594
|
+
const promptedId = prompt("New stack id", uniqueStackId("new-stack"));
|
|
2595
|
+
if (promptedId === null) return;
|
|
2596
|
+
const id = sanitizeStackId(promptedId);
|
|
2597
|
+
if (!id) throw new Error("Stack id must not be empty.");
|
|
2598
|
+
if (id !== promptedId.trim() && !confirm("Use stack id '" + id + "'?")) return;
|
|
2599
|
+
const promptedName = prompt("Stack display name", "Default Pi Prompt Mirror");
|
|
2600
|
+
if (promptedName === null) return;
|
|
2601
|
+
const stack = defaultNewStack(id, promptedName.trim() || id);
|
|
2602
|
+
const activate = stacks.length === 0 || confirm("Activate new stack now?");
|
|
2603
|
+
await createAndOpenStack(stack, activate, "Created");
|
|
2604
|
+
}
|
|
2605
|
+
|
|
2606
|
+
function defaultNewStack(id, name) {
|
|
2607
|
+
return {
|
|
2608
|
+
schemaVersion: 1,
|
|
2609
|
+
type: "pi-forge.prompt-stack",
|
|
2610
|
+
id,
|
|
2611
|
+
name,
|
|
2612
|
+
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.",
|
|
2613
|
+
autoActivate: stacks.length === 0,
|
|
2614
|
+
mode: "replace",
|
|
2615
|
+
defaults: {
|
|
2616
|
+
syntheticMessagesVisible: false,
|
|
2617
|
+
unresolvedMacroPolicy: "warn",
|
|
2618
|
+
},
|
|
2619
|
+
context: {
|
|
2620
|
+
allowDuplicateChatHistory: false,
|
|
2621
|
+
},
|
|
2622
|
+
tools: {
|
|
2623
|
+
allow: ["*"],
|
|
2624
|
+
},
|
|
2625
|
+
skills: {
|
|
2626
|
+
allow: ["*"],
|
|
2627
|
+
},
|
|
2628
|
+
items: [
|
|
2629
|
+
{
|
|
2630
|
+
kind: "block",
|
|
2631
|
+
id: "main-role",
|
|
2632
|
+
name: "Pi Default Role",
|
|
2633
|
+
enabled: true,
|
|
2634
|
+
role: "system",
|
|
2635
|
+
source: {
|
|
2636
|
+
package: "@earendil-works/pi-coding-agent",
|
|
2637
|
+
file: "dist/core/system-prompt.js",
|
|
2638
|
+
},
|
|
2639
|
+
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.",
|
|
2640
|
+
},
|
|
2641
|
+
{
|
|
2642
|
+
kind: "slot",
|
|
2643
|
+
id: "tools",
|
|
2644
|
+
name: "Available Tools",
|
|
2645
|
+
enabled: true,
|
|
2646
|
+
role: "system",
|
|
2647
|
+
slot: "tools",
|
|
2648
|
+
options: {
|
|
2649
|
+
format: "plain",
|
|
2650
|
+
onlyWithSnippets: true,
|
|
2651
|
+
},
|
|
2652
|
+
},
|
|
2653
|
+
{
|
|
2654
|
+
kind: "block",
|
|
2655
|
+
id: "custom-tools-note",
|
|
2656
|
+
name: "Custom Tools Note",
|
|
2657
|
+
enabled: true,
|
|
2658
|
+
role: "system",
|
|
2659
|
+
content: "In addition to the tools above, you may have access to other custom tools depending on the project.",
|
|
2660
|
+
},
|
|
2661
|
+
{
|
|
2662
|
+
kind: "slot",
|
|
2663
|
+
id: "tool-guidelines",
|
|
2664
|
+
name: "Guidelines",
|
|
2665
|
+
enabled: true,
|
|
2666
|
+
role: "system",
|
|
2667
|
+
slot: "tool-guidelines",
|
|
2668
|
+
options: {
|
|
2669
|
+
format: "plain",
|
|
2670
|
+
heading: "Guidelines:",
|
|
2671
|
+
includePiDefaultGuidelines: true,
|
|
2672
|
+
piStyle: true,
|
|
2673
|
+
},
|
|
2674
|
+
},
|
|
2675
|
+
{
|
|
2676
|
+
kind: "slot",
|
|
2677
|
+
id: "pi-docs",
|
|
2678
|
+
name: "Pi Documentation Guidance",
|
|
2679
|
+
enabled: true,
|
|
2680
|
+
role: "system",
|
|
2681
|
+
slot: "pi-docs",
|
|
2682
|
+
},
|
|
2683
|
+
{
|
|
2684
|
+
kind: "slot",
|
|
2685
|
+
id: "append-system-prompt",
|
|
2686
|
+
name: "User Append System Prompt",
|
|
2687
|
+
enabled: true,
|
|
2688
|
+
role: "system",
|
|
2689
|
+
slot: "append-system-prompt",
|
|
2690
|
+
},
|
|
2691
|
+
{
|
|
2692
|
+
kind: "slot",
|
|
2693
|
+
id: "project-context",
|
|
2694
|
+
name: "Project Context",
|
|
2695
|
+
enabled: true,
|
|
2696
|
+
role: "system",
|
|
2697
|
+
slot: "project-context",
|
|
2698
|
+
},
|
|
2699
|
+
{
|
|
2700
|
+
kind: "slot",
|
|
2701
|
+
id: "skills",
|
|
2702
|
+
name: "Available Skills",
|
|
2703
|
+
enabled: true,
|
|
2704
|
+
role: "system",
|
|
2705
|
+
slot: "skills",
|
|
2706
|
+
options: {
|
|
2707
|
+
requireReadTool: true,
|
|
2708
|
+
},
|
|
2709
|
+
},
|
|
2710
|
+
{
|
|
2711
|
+
kind: "slot",
|
|
2712
|
+
id: "date-cwd",
|
|
2713
|
+
name: "Date and Working Directory",
|
|
2714
|
+
enabled: true,
|
|
2715
|
+
role: "system",
|
|
2716
|
+
slot: "date-cwd",
|
|
2717
|
+
},
|
|
2718
|
+
{
|
|
2719
|
+
kind: "slot",
|
|
2720
|
+
id: "chat-history",
|
|
2721
|
+
name: "Chat History",
|
|
2722
|
+
enabled: true,
|
|
2723
|
+
slot: "chat-history",
|
|
2724
|
+
},
|
|
2725
|
+
],
|
|
2726
|
+
};
|
|
2727
|
+
}
|
|
2728
|
+
|
|
2299
2729
|
function showImportReport(report, stackId) {
|
|
2300
2730
|
showStackModal(
|
|
2301
2731
|
"SillyTavern import report",
|
|
@@ -2324,7 +2754,7 @@ async function handleImportFile(event) {
|
|
|
2324
2754
|
const characterId = promptSillyTavernCharacterId(imported);
|
|
2325
2755
|
if (characterId === null) return;
|
|
2326
2756
|
const activate = confirm("Convert and activate imported SillyTavern stack now?");
|
|
2327
|
-
await
|
|
2757
|
+
await createAndOpenStack(imported, activate, "Imported", { sourceName: file.name, characterId });
|
|
2328
2758
|
return;
|
|
2329
2759
|
}
|
|
2330
2760
|
|
|
@@ -2338,7 +2768,7 @@ async function handleImportFile(event) {
|
|
|
2338
2768
|
if (!stack.schemaVersion) stack.schemaVersion = 1;
|
|
2339
2769
|
if (!stack.type) stack.type = "pi-forge.prompt-stack";
|
|
2340
2770
|
const activate = confirm("Activate imported stack now?");
|
|
2341
|
-
await
|
|
2771
|
+
await createAndOpenStack(stack, activate, "Imported");
|
|
2342
2772
|
}
|
|
2343
2773
|
|
|
2344
2774
|
function isSillyTavernImport(value) {
|
|
@@ -2372,7 +2802,7 @@ async function forkStack() {
|
|
|
2372
2802
|
if (forkName && forkName.trim()) fork.name = forkName;
|
|
2373
2803
|
fork.autoActivate = false;
|
|
2374
2804
|
const activate = confirm("Activate fork now?");
|
|
2375
|
-
await
|
|
2805
|
+
await createAndOpenStack(fork, activate, "Forked");
|
|
2376
2806
|
}
|
|
2377
2807
|
|
|
2378
2808
|
async function exportStackJson() {
|
|
@@ -2414,6 +2844,15 @@ function uniqueForkId(baseId) {
|
|
|
2414
2844
|
return candidate;
|
|
2415
2845
|
}
|
|
2416
2846
|
|
|
2847
|
+
function uniqueStackId(baseId) {
|
|
2848
|
+
const base = sanitizeStackId(baseId || "stack") || "stack";
|
|
2849
|
+
const existing = new Set(stacks.map((stack) => stack.id));
|
|
2850
|
+
let candidate = base;
|
|
2851
|
+
let index = 2;
|
|
2852
|
+
while (existing.has(candidate)) candidate = base + "-" + index++;
|
|
2853
|
+
return candidate;
|
|
2854
|
+
}
|
|
2855
|
+
|
|
2417
2856
|
function sanitizeStackId(value) {
|
|
2418
2857
|
return String(value || "")
|
|
2419
2858
|
.trim()
|
|
@@ -2716,16 +3155,33 @@ function renderEmpty() {
|
|
|
2716
3155
|
currentStack = null;
|
|
2717
3156
|
selectedId = "";
|
|
2718
3157
|
dirty = false;
|
|
3158
|
+
activeTab = "items";
|
|
2719
3159
|
stackPolicyError = "";
|
|
2720
3160
|
renderDirtyState();
|
|
3161
|
+
document.querySelectorAll("[data-tab]").forEach((button) => {
|
|
3162
|
+
button.classList.toggle("active", button.dataset.tab === activeTab);
|
|
3163
|
+
});
|
|
3164
|
+
el("workspace").style.display = "";
|
|
2721
3165
|
el("metadataPanel").style.display = "none";
|
|
2722
3166
|
el("settings").innerHTML = "";
|
|
3167
|
+
el("itemCount").textContent = "";
|
|
2723
3168
|
el("itemList").innerHTML = "";
|
|
2724
|
-
el("itemEditor").innerHTML =
|
|
3169
|
+
el("itemEditor").innerHTML =
|
|
3170
|
+
'<div class="empty">' +
|
|
3171
|
+
'<div class="empty-title">No prompt stacks found.</div>' +
|
|
3172
|
+
'<div>Create a stack in this project, or import an existing pi-forge/SillyTavern JSON file.</div>' +
|
|
3173
|
+
'<div class="empty-actions">' +
|
|
3174
|
+
'<button id="emptyNewStackBtn" class="primary" data-icon="+" title="Create a new prompt stack">New stack</button>' +
|
|
3175
|
+
'<button id="emptyImportBtn" data-icon="⇪" title="Import pi-forge stack JSON or SillyTavern preset JSON">Import JSON</button>' +
|
|
3176
|
+
'</div>' +
|
|
3177
|
+
'</div>';
|
|
2725
3178
|
el("tabPanel").classList.remove("open");
|
|
2726
3179
|
el("tabPanel").innerHTML = "";
|
|
2727
3180
|
renderDiagnostics([]);
|
|
3181
|
+
el("emptyNewStackBtn").onclick = () => run(createNewStack);
|
|
3182
|
+
el("emptyImportBtn").onclick = () => run(importStackJson);
|
|
2728
3183
|
setStatus("No prompt stacks found");
|
|
3184
|
+
updateActionState();
|
|
2729
3185
|
}
|
|
2730
3186
|
|
|
2731
3187
|
function field(label, control, className = "") {
|
|
@@ -2774,6 +3230,7 @@ function toggleSidebar() {
|
|
|
2774
3230
|
|
|
2775
3231
|
el("sidebarToggleBtn").onclick = toggleSidebar;
|
|
2776
3232
|
el("themeBtn").onclick = toggleTheme;
|
|
3233
|
+
el("newStackBtn").onclick = () => run(createNewStack);
|
|
2777
3234
|
el("reloadBtn").onclick = () => run(reloadFromDisk);
|
|
2778
3235
|
el("disableBtn").onclick = () => run(disableStacks);
|
|
2779
3236
|
el("activateBtn").onclick = () => run(activateStack);
|
|
@@ -2823,15 +3280,44 @@ el("preview").onclick = (event) => {
|
|
|
2823
3280
|
run(() => copyPreviewText(Number(button.dataset.copyIndex)));
|
|
2824
3281
|
};
|
|
2825
3282
|
el("addItemBtn").onclick = () => addItem("block");
|
|
3283
|
+
el("addSlotBtn").onclick = () => addItem("slot");
|
|
2826
3284
|
el("deleteItemBtn").onclick = deleteSelectedItem;
|
|
2827
3285
|
document.addEventListener("dragover", handleDocumentItemDragOver);
|
|
2828
3286
|
document.addEventListener("drop", handleDocumentItemDrop);
|
|
2829
3287
|
window.onbeforeunload = () => dirty ? "Unsaved changes" : undefined;
|
|
2830
|
-
window.addEventListener("keydown",
|
|
2831
|
-
|
|
2832
|
-
|
|
2833
|
-
|
|
2834
|
-
|
|
3288
|
+
window.addEventListener("keydown", handleEditorShortcut);
|
|
3289
|
+
|
|
3290
|
+
function handleEditorShortcut(event) {
|
|
3291
|
+
if (event.key === "Escape") {
|
|
3292
|
+
if (el("preview").classList.contains("open")) hidePreview();
|
|
3293
|
+
else if (el("stackModal").classList.contains("open")) closeStackModal();
|
|
3294
|
+
return;
|
|
3295
|
+
}
|
|
3296
|
+
|
|
3297
|
+
const modifier = event.ctrlKey || event.metaKey;
|
|
3298
|
+
if (!modifier || event.altKey) return;
|
|
3299
|
+
const key = event.key.toLowerCase();
|
|
3300
|
+
|
|
3301
|
+
if (key === "n") {
|
|
3302
|
+
event.preventDefault();
|
|
3303
|
+
run(createNewStack);
|
|
3304
|
+
return;
|
|
3305
|
+
}
|
|
3306
|
+
if (key === "s") {
|
|
3307
|
+
event.preventDefault();
|
|
3308
|
+
if (currentStack) run(saveStack);
|
|
3309
|
+
return;
|
|
3310
|
+
}
|
|
3311
|
+
if (key === "enter" && event.shiftKey) {
|
|
3312
|
+
event.preventDefault();
|
|
3313
|
+
if (currentStack) run(validateStack);
|
|
3314
|
+
return;
|
|
3315
|
+
}
|
|
3316
|
+
if (key === "enter") {
|
|
3317
|
+
event.preventDefault();
|
|
3318
|
+
if (currentStack) run(previewStack);
|
|
3319
|
+
}
|
|
3320
|
+
}
|
|
2835
3321
|
|
|
2836
3322
|
run(async () => {
|
|
2837
3323
|
await loadStacks();
|