@testsmith/api-spector 0.0.4 → 0.0.5

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.
@@ -8339,6 +8339,14 @@ const useStore = create()(
8339
8339
  s.activeEnvironmentId = env.id;
8340
8340
  if (s.workspace) s.workspace.environments.push(relPath);
8341
8341
  }),
8342
+ deleteEnvironment: (id2) => set2((s) => {
8343
+ const relPath = s.environments[id2]?.relPath;
8344
+ delete s.environments[id2];
8345
+ if (s.activeEnvironmentId === id2) s.activeEnvironmentId = null;
8346
+ if (relPath && s.workspace) {
8347
+ s.workspace.environments = s.workspace.environments.filter((p2) => p2 !== relPath);
8348
+ }
8349
+ }),
8342
8350
  // ── Globals ───────────────────────────────────────────────────────────────
8343
8351
  setGlobals: (globals) => set2((s) => {
8344
8352
  s.globals = globals;
@@ -9161,6 +9169,43 @@ function TagChips({
9161
9169
  )
9162
9170
  ] });
9163
9171
  }
9172
+ function ConfirmDialog({ message, onConfirm, onCancel }) {
9173
+ return /* @__PURE__ */ jsxRuntimeExports.jsx(
9174
+ "div",
9175
+ {
9176
+ className: "fixed inset-0 bg-black/50 z-[300] flex items-center justify-center",
9177
+ onClick: onCancel,
9178
+ children: /* @__PURE__ */ jsxRuntimeExports.jsxs(
9179
+ "div",
9180
+ {
9181
+ className: "bg-surface-900 border border-surface-700 rounded-lg shadow-2xl p-4 w-72 flex flex-col gap-4",
9182
+ onClick: (e) => e.stopPropagation(),
9183
+ children: [
9184
+ /* @__PURE__ */ jsxRuntimeExports.jsx("p", { className: "text-sm text-white", children: message }),
9185
+ /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex justify-end gap-2", children: [
9186
+ /* @__PURE__ */ jsxRuntimeExports.jsx(
9187
+ "button",
9188
+ {
9189
+ onClick: onCancel,
9190
+ className: "px-3 py-1.5 text-xs text-surface-400 hover:text-white transition-colors",
9191
+ children: "Cancel"
9192
+ }
9193
+ ),
9194
+ /* @__PURE__ */ jsxRuntimeExports.jsx(
9195
+ "button",
9196
+ {
9197
+ onClick: onConfirm,
9198
+ className: "px-3 py-1.5 text-xs bg-red-700 hover:bg-red-600 rounded transition-colors",
9199
+ children: "Delete"
9200
+ }
9201
+ )
9202
+ ] })
9203
+ ]
9204
+ }
9205
+ )
9206
+ }
9207
+ );
9208
+ }
9164
9209
  function IconBtn({
9165
9210
  title: title2,
9166
9211
  onClick,
@@ -9180,7 +9225,7 @@ function IconBtn({
9180
9225
  children
9181
9226
  }
9182
9227
  ),
9183
- /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "pointer-events-none absolute bottom-full left-1/2 -translate-x-1/2 mb-1 z-50 hidden group-hover/tip:block", children: /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "whitespace-nowrap rounded px-1.5 py-0.5 text-[10px] bg-[#1e1b2e] text-gray-300 border border-white/10 shadow-lg", children: title2 }) })
9228
+ /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "pointer-events-none absolute top-full left-1/2 -translate-x-1/2 mt-1 z-50 hidden group-hover/tip:block", children: /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "whitespace-nowrap rounded px-1.5 py-0.5 text-[10px] bg-[#1e1b2e] text-gray-300 border border-white/10 shadow-lg", children: title2 }) })
9184
9229
  ] });
9185
9230
  }
9186
9231
  function RunBtn({ onClick }) {
@@ -9192,7 +9237,7 @@ function RunBtn({ onClick }) {
9192
9237
  e.stopPropagation();
9193
9238
  onClick(e);
9194
9239
  },
9195
- className: "opacity-0 group-hover:opacity-100 px-1 py-0.5 rounded text-emerald-500 hover:text-emerald-400 transition-all",
9240
+ className: "px-1 py-0.5 rounded text-emerald-500 hover:text-emerald-400 transition-all",
9196
9241
  children: /* @__PURE__ */ jsxRuntimeExports.jsx("svg", { className: "w-3 h-3", viewBox: "0 0 20 20", fill: "currentColor", children: /* @__PURE__ */ jsxRuntimeExports.jsx("path", { fillRule: "evenodd", d: "M10 18a8 8 0 100-16 8 8 0 000 16zM9.555 7.168A1 1 0 008 8v4a1 1 0 001.555.832l3-2a1 1 0 000-1.664l-3-2z", clipRule: "evenodd" }) })
9197
9242
  }
9198
9243
  );
@@ -9219,19 +9264,22 @@ function CollectionTree() {
9219
9264
  const updateRequestTags = useStore((s) => s.updateRequestTags);
9220
9265
  const openRunner = useStore((s) => s.openRunner);
9221
9266
  const colList = Object.values(collections);
9267
+ const [pendingConfirm, setPendingConfirm] = reactExports.useState(null);
9268
+ function confirmThen(message, action) {
9269
+ setPendingConfirm({ message, onConfirm: () => {
9270
+ action();
9271
+ setPendingConfirm(null);
9272
+ } });
9273
+ }
9222
9274
  return /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex flex-col flex-1 min-h-0 select-none", children: [
9223
- /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "px-3 py-2 text-xs font-semibold text-surface-400 uppercase tracking-wider flex items-center justify-between flex-shrink-0", children: [
9224
- /* @__PURE__ */ jsxRuntimeExports.jsx("span", { children: "Collections" }),
9225
- /* @__PURE__ */ jsxRuntimeExports.jsx(
9226
- "button",
9227
- {
9228
- onClick: () => addCollection("New Collection"),
9229
- title: "New collection",
9230
- className: "text-surface-400 hover:text-blue-400 transition-colors px-1",
9231
- children: "+"
9232
- }
9233
- )
9234
- ] }),
9275
+ pendingConfirm && /* @__PURE__ */ jsxRuntimeExports.jsx(
9276
+ ConfirmDialog,
9277
+ {
9278
+ message: pendingConfirm.message,
9279
+ onConfirm: pendingConfirm.onConfirm,
9280
+ onCancel: () => setPendingConfirm(null)
9281
+ }
9282
+ ),
9235
9283
  /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex-1 overflow-y-auto", children: [
9236
9284
  colList.map(({ data: col }) => /* @__PURE__ */ jsxRuntimeExports.jsx(
9237
9285
  CollectionNode,
@@ -9245,13 +9293,9 @@ function CollectionTree() {
9245
9293
  onAddRequest: (folderId) => addRequest(col.id, folderId),
9246
9294
  onAddFolder: (parentId, name2) => addFolder(col.id, parentId, name2),
9247
9295
  onRenameCollection: (name2) => renameCollection(col.id, name2),
9248
- onDeleteCollection: () => {
9249
- if (confirm(`Delete collection "${col.name}"?`)) deleteCollection(col.id);
9250
- },
9296
+ onDeleteCollection: () => confirmThen(`Delete collection "${col.name}"?`, () => deleteCollection(col.id)),
9251
9297
  onRenameFolder: (folderId, name2) => renameFolder(col.id, folderId, name2),
9252
- onDeleteFolder: (folderId) => {
9253
- if (confirm("Delete this folder and all its requests?")) deleteFolder(col.id, folderId);
9254
- },
9298
+ onDeleteFolder: (folderId) => confirmThen("Delete this folder and all its requests?", () => deleteFolder(col.id, folderId)),
9255
9299
  onRenameRequest: renameRequest,
9256
9300
  onDeleteRequest: (reqId) => deleteRequest(col.id, reqId),
9257
9301
  onDuplicateRequest: (reqId) => duplicateRequest(col.id, reqId),
@@ -9324,15 +9368,15 @@ function CollectionNode({
9324
9368
  validate: (v2) => existingCollectionNames.filter((n2) => n2 !== col.name).includes(v2) ? `"${v2}" already exists` : null
9325
9369
  }
9326
9370
  ) : /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "text-xs font-semibold truncate block", children: col.name }) }),
9327
- /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-center shrink-0 gap-0", children: [
9371
+ /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "hidden group-hover:flex items-center shrink-0 gap-0", children: [
9328
9372
  /* @__PURE__ */ jsxRuntimeExports.jsx(RunBtn, { onClick: onRunCollection }),
9329
- /* @__PURE__ */ jsxRuntimeExports.jsx(IconBtn, { title: "Expand all folders", onClick: expandAll, children: /* @__PURE__ */ jsxRuntimeExports.jsx(ExpandAllIcon, {}) }),
9330
- /* @__PURE__ */ jsxRuntimeExports.jsx(IconBtn, { title: "Collapse all folders", onClick: collapseAll, children: /* @__PURE__ */ jsxRuntimeExports.jsx(CollapseAllIcon, {}) }),
9331
- /* @__PURE__ */ jsxRuntimeExports.jsx(IconBtn, { title: "Collection data (iterations)", onClick: onSelectCollection, children: /* @__PURE__ */ jsxRuntimeExports.jsx(TableIcon, {}) }),
9332
- /* @__PURE__ */ jsxRuntimeExports.jsx(IconBtn, { title: "Add request", onClick: () => onAddRequest(col.rootFolder.id), children: /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "text-xs", children: "+" }) }),
9333
- /* @__PURE__ */ jsxRuntimeExports.jsx(IconBtn, { title: "Add folder", onClick: () => onAddFolder(col.rootFolder.id, "New Folder"), children: /* @__PURE__ */ jsxRuntimeExports.jsx(FolderIcon, {}) }),
9334
- /* @__PURE__ */ jsxRuntimeExports.jsx(IconBtn, { title: "Rename", onClick: () => setRenaming(true), children: /* @__PURE__ */ jsxRuntimeExports.jsx(PencilIcon, {}) }),
9335
- /* @__PURE__ */ jsxRuntimeExports.jsx(IconBtn, { title: "Delete collection", onClick: onDeleteCollection, danger: true, children: /* @__PURE__ */ jsxRuntimeExports.jsx(TrashIcon, {}) })
9373
+ /* @__PURE__ */ jsxRuntimeExports.jsx(IconBtn, { title: "Expand all folders", onClick: expandAll, alwaysVisible: true, children: /* @__PURE__ */ jsxRuntimeExports.jsx(ExpandAllIcon, {}) }),
9374
+ /* @__PURE__ */ jsxRuntimeExports.jsx(IconBtn, { title: "Collapse all folders", onClick: collapseAll, alwaysVisible: true, children: /* @__PURE__ */ jsxRuntimeExports.jsx(CollapseAllIcon, {}) }),
9375
+ /* @__PURE__ */ jsxRuntimeExports.jsx(IconBtn, { title: "Collection data (iterations)", onClick: onSelectCollection, alwaysVisible: true, children: /* @__PURE__ */ jsxRuntimeExports.jsx(TableIcon, {}) }),
9376
+ /* @__PURE__ */ jsxRuntimeExports.jsx(IconBtn, { title: "Add request", onClick: () => onAddRequest(col.rootFolder.id), alwaysVisible: true, children: /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "text-xs", children: "+" }) }),
9377
+ /* @__PURE__ */ jsxRuntimeExports.jsx(IconBtn, { title: "Add folder", onClick: () => onAddFolder(col.rootFolder.id, "New Folder"), alwaysVisible: true, children: /* @__PURE__ */ jsxRuntimeExports.jsx(FolderIcon, {}) }),
9378
+ /* @__PURE__ */ jsxRuntimeExports.jsx(IconBtn, { title: "Rename", onClick: () => setRenaming(true), alwaysVisible: true, children: /* @__PURE__ */ jsxRuntimeExports.jsx(PencilIcon, {}) }),
9379
+ /* @__PURE__ */ jsxRuntimeExports.jsx(IconBtn, { title: "Delete collection", onClick: onDeleteCollection, danger: true, alwaysVisible: true, children: /* @__PURE__ */ jsxRuntimeExports.jsx(TrashIcon, {}) })
9336
9380
  ] })
9337
9381
  ]
9338
9382
  }
@@ -9415,16 +9459,16 @@ function FolderRow({
9415
9459
  }
9416
9460
  )
9417
9461
  ] }),
9418
- /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-center shrink-0 gap-0", children: [
9462
+ /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "hidden group-hover:flex items-center shrink-0 gap-0", children: [
9419
9463
  /* @__PURE__ */ jsxRuntimeExports.jsx(RunBtn, { onClick: onRun }),
9420
- /* @__PURE__ */ jsxRuntimeExports.jsx(IconBtn, { title: "Add request", onClick: onAddRequest, children: /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "text-xs", children: "+" }) }),
9421
- /* @__PURE__ */ jsxRuntimeExports.jsx(IconBtn, { title: "Add sub-folder", onClick: onAddFolder, children: /* @__PURE__ */ jsxRuntimeExports.jsx(FolderIcon, {}) }),
9422
- /* @__PURE__ */ jsxRuntimeExports.jsx(IconBtn, { title: "Folder auth & headers", onClick: () => setShowSettings(true), children: /* @__PURE__ */ jsxRuntimeExports.jsx(KeyIcon, {}) }),
9464
+ /* @__PURE__ */ jsxRuntimeExports.jsx(IconBtn, { title: "Add request", onClick: onAddRequest, alwaysVisible: true, children: /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "text-xs", children: "+" }) }),
9465
+ /* @__PURE__ */ jsxRuntimeExports.jsx(IconBtn, { title: "Add sub-folder", onClick: onAddFolder, alwaysVisible: true, children: /* @__PURE__ */ jsxRuntimeExports.jsx(FolderIcon, {}) }),
9466
+ /* @__PURE__ */ jsxRuntimeExports.jsx(IconBtn, { title: "Folder auth & headers", onClick: () => setShowSettings(true), alwaysVisible: true, children: /* @__PURE__ */ jsxRuntimeExports.jsx(KeyIcon, {}) }),
9423
9467
  /* @__PURE__ */ jsxRuntimeExports.jsx(IconBtn, { title: "Add tag", onClick: () => {
9424
- }, alwaysVisible: false, children: /* @__PURE__ */ jsxRuntimeExports.jsx(TagChips, { tags: [], onRemove: () => {
9468
+ }, alwaysVisible: true, children: /* @__PURE__ */ jsxRuntimeExports.jsx(TagChips, { tags: [], onRemove: () => {
9425
9469
  }, onAdd: (tag) => onUpdateTags([...tags2, tag]) }) }),
9426
- /* @__PURE__ */ jsxRuntimeExports.jsx(IconBtn, { title: "Rename", onClick: () => setRenaming(true), children: /* @__PURE__ */ jsxRuntimeExports.jsx(PencilIcon, {}) }),
9427
- /* @__PURE__ */ jsxRuntimeExports.jsx(IconBtn, { title: "Delete folder", onClick: onDelete, danger: true, children: /* @__PURE__ */ jsxRuntimeExports.jsx(TrashIcon, {}) })
9470
+ /* @__PURE__ */ jsxRuntimeExports.jsx(IconBtn, { title: "Rename", onClick: () => setRenaming(true), alwaysVisible: true, children: /* @__PURE__ */ jsxRuntimeExports.jsx(PencilIcon, {}) }),
9471
+ /* @__PURE__ */ jsxRuntimeExports.jsx(IconBtn, { title: "Delete folder", onClick: onDelete, danger: true, alwaysVisible: true, children: /* @__PURE__ */ jsxRuntimeExports.jsx(TrashIcon, {}) })
9428
9472
  ] })
9429
9473
  ]
9430
9474
  }
@@ -36977,6 +37021,24 @@ sp.variables.set("field_value", json.field);`
36977
37021
  label: "Save JSON field to environment",
36978
37022
  code: `const json = sp.response.json();
36979
37023
  sp.environment.set("token", json.token);`
37024
+ },
37025
+ {
37026
+ label: "Extract via JSONPath to variable",
37027
+ code: `const matches = sp.jsonPath(sp.response.json(), '$.data[0].id');
37028
+ sp.variables.set("extracted_value", String(matches[0] ?? ''));`
37029
+ },
37030
+ {
37031
+ label: "Extract via JSONPath to environment",
37032
+ code: `const matches = sp.jsonPath(sp.response.json(), '$.data[0].id');
37033
+ sp.environment.set("extracted_value", String(matches[0] ?? ''));`
37034
+ },
37035
+ {
37036
+ label: "Extract from XML to variable",
37037
+ code: `sp.variables.set("extracted_value", sp.response.xmlText("ElementName") ?? '');`
37038
+ },
37039
+ {
37040
+ label: "Extract from XML to environment",
37041
+ code: `sp.environment.set("extracted_value", sp.response.xmlText("ElementName") ?? '');`
36980
37042
  }
36981
37043
  ]
36982
37044
  }
@@ -36990,6 +37052,7 @@ function ScriptsTab({ request, onChange }) {
36990
37052
  if (activeTabId) setTabScriptTab(activeTabId, t2);
36991
37053
  };
36992
37054
  const [expandedGroup, setExpandedGroup] = reactExports.useState(SNIPPET_GROUPS[0].group);
37055
+ const [snippetsOpen, setSnippetsOpen] = reactExports.useState(true);
36993
37056
  const varNames = useVarNames();
36994
37057
  const varValues = useVarValues();
36995
37058
  const extensions = reactExports.useMemo(
@@ -37030,9 +37093,27 @@ function ScriptsTab({ request, onChange }) {
37030
37093
  }
37031
37094
  ) })
37032
37095
  ] }),
37033
- /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "w-52 flex-shrink-0 flex flex-col overflow-y-auto border-l border-surface-700 pl-2", children: [
37034
- /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "text-[10px] font-semibold text-surface-400 uppercase tracking-wider mb-2", children: "Snippets" }),
37035
- SNIPPET_GROUPS.map((group) => /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "mb-1", children: [
37096
+ /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: `flex-shrink-0 flex flex-col border-l border-surface-700 transition-all ${snippetsOpen ? "w-52 pl-2 overflow-y-auto" : "w-7"}`, children: [
37097
+ snippetsOpen ? /* @__PURE__ */ jsxRuntimeExports.jsxs(
37098
+ "button",
37099
+ {
37100
+ onClick: () => setSnippetsOpen(false),
37101
+ className: "flex items-center gap-1 text-[10px] font-semibold text-surface-400 uppercase tracking-wider mb-2 hover:text-white transition-colors w-full",
37102
+ children: [
37103
+ /* @__PURE__ */ jsxRuntimeExports.jsx("span", { children: "▾" }),
37104
+ /* @__PURE__ */ jsxRuntimeExports.jsx("span", { children: "Quick inserts" })
37105
+ ]
37106
+ }
37107
+ ) : /* @__PURE__ */ jsxRuntimeExports.jsx(
37108
+ "button",
37109
+ {
37110
+ onClick: () => setSnippetsOpen(true),
37111
+ className: "flex-1 flex items-center justify-center hover:bg-surface-800 transition-colors rounded-sm",
37112
+ title: "Expand quick inserts",
37113
+ children: /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "text-[10px] font-semibold text-surface-400 uppercase tracking-wider [writing-mode:vertical-rl] rotate-180", children: "Quick inserts" })
37114
+ }
37115
+ ),
37116
+ snippetsOpen && SNIPPET_GROUPS.map((group) => /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "mb-1", children: [
37036
37117
  /* @__PURE__ */ jsxRuntimeExports.jsxs(
37037
37118
  "button",
37038
37119
  {
@@ -43807,36 +43888,82 @@ function makeJsonSnippet(path, value, mode) {
43807
43888
  });`;
43808
43889
  }
43809
43890
  }
43891
+ function getAtPath(root, path) {
43892
+ let cur2 = root;
43893
+ for (const key of path) {
43894
+ if (cur2 == null || typeof cur2 !== "object") return void 0;
43895
+ cur2 = cur2[key];
43896
+ }
43897
+ return cur2;
43898
+ }
43899
+ function toJsonPathExpr(path, filterKey, filterValue) {
43900
+ let arrayIdx = -1;
43901
+ for (let i = path.length - 1; i >= 0; i--) {
43902
+ if (typeof path[i] === "number") {
43903
+ arrayIdx = i;
43904
+ break;
43905
+ }
43906
+ }
43907
+ if (arrayIdx < 0) return "";
43908
+ const arrayPart = "$." + path.slice(0, arrayIdx).join(".");
43909
+ const leafPart = path.slice(arrayIdx + 1).join(".");
43910
+ const filterVal = isNaN(Number(filterValue)) ? `"${filterValue.replace(/"/g, '\\"')}"` : filterValue;
43911
+ const expr = leafPart ? `${arrayPart}[?(@.${filterKey}==${filterVal})].${leafPart}` : `${arrayPart}[?(@.${filterKey}==${filterVal})]`;
43912
+ return expr;
43913
+ }
43914
+ function makeJsonPathSnippet(path, value, filterKey, filterValue) {
43915
+ const expr = toJsonPathExpr(path, filterKey, filterValue);
43916
+ const lit = toLit(value);
43917
+ return `sp.test('${expr} equals ${lit}', function() {
43918
+ const matches = sp.jsonPath(sp.response.json(), '${expr}');
43919
+ sp.expect(matches.length).to.be.above(0);
43920
+ sp.expect(matches[0]).to.equal(${lit});
43921
+ });`;
43922
+ }
43810
43923
  function makeXmlSnippet(selector, value, mode) {
43811
- const parse2 = `const doc = new DOMParser().parseFromString(sp.response.text(), "text/xml");`;
43812
- const query = `const el = doc.querySelector("${selector.replace(/"/g, '\\"')}");`;
43924
+ const sel = selector.replace(/"/g, '\\"');
43813
43925
  switch (mode) {
43814
43926
  case "equals":
43815
43927
  return `sp.test('${selector} equals "${esc(value)}"', function() {
43816
- ${parse2}
43817
- ${query}
43818
- sp.expect(el?.textContent?.trim()).to.equal("${esc(value)}");
43928
+ sp.expect(sp.response.xmlText("${sel}")).to.equal("${esc(value)}");
43819
43929
  });`;
43820
43930
  case "exists":
43821
43931
  return `sp.test('${selector} exists', function() {
43822
- ${parse2}
43823
- ${query}
43824
- sp.expect(el).to.not.equal(null);
43932
+ sp.expect(sp.response.xmlText("${sel}")).to.not.equal(null);
43825
43933
  });`;
43826
43934
  case "contains":
43827
43935
  return `sp.test('${selector} contains "${esc(value)}"', function() {
43828
- ${parse2}
43829
- ${query}
43830
- sp.expect(el?.textContent).to.include("${esc(value)}");
43936
+ sp.expect(sp.response.xmlText("${sel}")).to.include("${esc(value)}");
43831
43937
  });`;
43832
43938
  }
43833
43939
  }
43940
+ function varNameFromPath(path) {
43941
+ return path.filter((k2) => typeof k2 === "string").at(-1) ?? "extracted_value";
43942
+ }
43943
+ function makeJsonExtractSnippet(path, target) {
43944
+ const acc = jsonAccessor(path);
43945
+ const varName = varNameFromPath(path);
43946
+ return `const json = sp.response.json();
43947
+ sp.${target}.set("${varName}", String(${acc}));`;
43948
+ }
43949
+ function makeJsonPathExtractSnippet(path, filterKey, filterValue, target) {
43950
+ const expr = toJsonPathExpr(path, filterKey, filterValue);
43951
+ const varName = varNameFromPath(path);
43952
+ return `const matches = sp.jsonPath(sp.response.json(), '${expr}');
43953
+ sp.${target}.set("${varName}", String(matches[0] ?? ''));`;
43954
+ }
43955
+ function makeXmlExtractSnippet(selector, target) {
43956
+ return `sp.${target}.set("extracted_value", sp.response.xmlText("${selector.replace(/"/g, '\\"')}") ?? '');`;
43957
+ }
43834
43958
  function AssertMenu({
43835
43959
  state,
43836
43960
  onClose,
43837
43961
  onConfirm
43838
43962
  }) {
43839
43963
  const ref2 = reactExports.useRef(null);
43964
+ const [jpOpen, setJpOpen] = reactExports.useState(false);
43965
+ const [filterKey, setFilterKey] = reactExports.useState("");
43966
+ const [filterVal, setFilterVal] = reactExports.useState("");
43840
43967
  reactExports.useEffect(() => {
43841
43968
  function onMouse(e) {
43842
43969
  if (ref2.current && !ref2.current.contains(e.target)) onClose();
@@ -43853,8 +43980,10 @@ function AssertMenu({
43853
43980
  }, [onClose]);
43854
43981
  let title2 = "";
43855
43982
  let options = [];
43983
+ let jpSiblingKeys = [];
43984
+ let jpAvailable = false;
43856
43985
  if (state.type === "json") {
43857
- const { path, value } = state;
43986
+ const { path, value, root } = state;
43858
43987
  const isStr = typeof value === "string";
43859
43988
  const preview = isStr ? `"${value.length > 22 ? value.slice(0, 22) + "…" : value}"` : String(value);
43860
43989
  title2 = jsonPathLabel(path);
@@ -43864,6 +43993,26 @@ function AssertMenu({
43864
43993
  { label: `is ${value === null ? "null" : typeof value}`, snippet: makeJsonSnippet(path, value, "type") },
43865
43994
  ...isStr ? [{ label: `contains ${preview}`, snippet: makeJsonSnippet(path, value, "contains") }] : []
43866
43995
  ];
43996
+ const arrayIdx = [...path].reverse().findIndex((k2) => typeof k2 === "number");
43997
+ if (arrayIdx >= 0) {
43998
+ jpAvailable = true;
43999
+ const realIdx = path.length - 1 - arrayIdx;
44000
+ const itemObj = getAtPath(root, path.slice(0, realIdx + 1));
44001
+ if (itemObj != null && typeof itemObj === "object" && !Array.isArray(itemObj)) {
44002
+ jpSiblingKeys = Object.keys(itemObj).filter((k2) => {
44003
+ const v2 = itemObj[k2];
44004
+ return typeof v2 !== "object" || v2 === null;
44005
+ });
44006
+ }
44007
+ if (!filterKey && jpSiblingKeys.length > 0) {
44008
+ const defaultKey = jpSiblingKeys.find((k2) => k2 === "name" || k2 === "id") ?? jpSiblingKeys[0];
44009
+ setTimeout(() => {
44010
+ setFilterKey(defaultKey);
44011
+ const seed = getAtPath(root, [...path.slice(0, realIdx + 1), defaultKey]);
44012
+ setFilterVal(seed != null ? String(seed) : "");
44013
+ }, 0);
44014
+ }
44015
+ }
43867
44016
  } else {
43868
44017
  const { selector, value } = state;
43869
44018
  const preview = `"${value.length > 22 ? value.slice(0, 22) + "…" : value}"`;
@@ -43874,14 +44023,14 @@ function AssertMenu({
43874
44023
  { label: `contains ${preview}`, snippet: makeXmlSnippet(selector, value, "contains") }
43875
44024
  ];
43876
44025
  }
43877
- const x2 = Math.min(state.x, window.innerWidth - 260);
43878
- const y2 = Math.min(state.y, window.innerHeight - 220);
44026
+ const x2 = Math.min(state.x, window.innerWidth - 280);
44027
+ const y2 = Math.min(state.y, window.innerHeight - 280);
43879
44028
  return /* @__PURE__ */ jsxRuntimeExports.jsxs(
43880
44029
  "div",
43881
44030
  {
43882
44031
  ref: ref2,
43883
44032
  style: { top: y2, left: x2, position: "fixed" },
43884
- className: "z-[200] bg-surface-900 border border-surface-700 rounded-lg shadow-2xl p-2 min-w-[240px]",
44033
+ className: "z-[200] bg-surface-900 border border-surface-700 rounded-lg shadow-2xl p-2 min-w-[260px]",
43885
44034
  children: [
43886
44035
  /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "text-[10px] text-surface-500 font-mono px-1.5 pb-1.5 mb-1.5 border-b border-surface-800 truncate", children: title2 }),
43887
44036
  options.map((opt) => /* @__PURE__ */ jsxRuntimeExports.jsx(
@@ -43895,7 +44044,141 @@ function AssertMenu({
43895
44044
  children: opt.label
43896
44045
  },
43897
44046
  opt.label
43898
- ))
44047
+ )),
44048
+ jpAvailable && /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "mt-1 border-t border-surface-800 pt-1", children: [
44049
+ /* @__PURE__ */ jsxRuntimeExports.jsxs(
44050
+ "button",
44051
+ {
44052
+ onClick: () => setJpOpen((o) => !o),
44053
+ className: "w-full text-left text-xs text-blue-400 hover:text-blue-300 hover:bg-surface-800 rounded px-2 py-1.5 transition-colors flex items-center gap-1",
44054
+ children: [
44055
+ /* @__PURE__ */ jsxRuntimeExports.jsx("span", { children: jpOpen ? "▾" : "▸" }),
44056
+ /* @__PURE__ */ jsxRuntimeExports.jsx("span", { children: "JSONPath assert (with filter)" })
44057
+ ]
44058
+ }
44059
+ ),
44060
+ jpOpen && state.type === "json" && /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "mt-1 px-2 flex flex-col gap-1.5", children: [
44061
+ /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-center gap-1", children: [
44062
+ /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "text-[10px] text-surface-400 w-16 shrink-0", children: "filter by" }),
44063
+ /* @__PURE__ */ jsxRuntimeExports.jsx(
44064
+ "select",
44065
+ {
44066
+ value: filterKey,
44067
+ onChange: (e) => {
44068
+ const k2 = e.target.value;
44069
+ setFilterKey(k2);
44070
+ const arrayIdx2 = [...state.path].reverse().findIndex((seg) => typeof seg === "number");
44071
+ const realIdx2 = state.path.length - 1 - arrayIdx2;
44072
+ const itemObj2 = getAtPath(state.root, state.path.slice(0, realIdx2 + 1));
44073
+ const seed = itemObj2 != null ? itemObj2[k2] : void 0;
44074
+ setFilterVal(seed != null ? String(seed) : "");
44075
+ },
44076
+ className: "flex-1 bg-surface-800 border border-surface-700 rounded px-1 py-0.5 text-xs focus:outline-none",
44077
+ children: jpSiblingKeys.map((k2) => /* @__PURE__ */ jsxRuntimeExports.jsx("option", { value: k2, children: k2 }, k2))
44078
+ }
44079
+ )
44080
+ ] }),
44081
+ /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-center gap-1", children: [
44082
+ /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "text-[10px] text-surface-400 w-16 shrink-0", children: "equals" }),
44083
+ /* @__PURE__ */ jsxRuntimeExports.jsx(
44084
+ "input",
44085
+ {
44086
+ value: filterVal,
44087
+ onChange: (e) => setFilterVal(e.target.value),
44088
+ className: "flex-1 bg-surface-800 border border-surface-700 rounded px-1 py-0.5 text-xs font-mono focus:outline-none focus:border-blue-500"
44089
+ }
44090
+ )
44091
+ ] }),
44092
+ /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex gap-1 self-end", children: [
44093
+ /* @__PURE__ */ jsxRuntimeExports.jsx(
44094
+ "button",
44095
+ {
44096
+ disabled: !filterKey || !filterVal,
44097
+ onClick: () => {
44098
+ onConfirm(makeJsonPathSnippet(state.path, state.value, filterKey, filterVal));
44099
+ onClose();
44100
+ },
44101
+ className: "text-xs px-2 py-1 bg-blue-700 hover:bg-blue-600 disabled:opacity-40 rounded transition-colors",
44102
+ children: "Assert"
44103
+ }
44104
+ ),
44105
+ /* @__PURE__ */ jsxRuntimeExports.jsx(
44106
+ "button",
44107
+ {
44108
+ disabled: !filterKey || !filterVal,
44109
+ onClick: () => {
44110
+ onConfirm(makeJsonPathExtractSnippet(state.path, filterKey, filterVal, "variables"));
44111
+ onClose();
44112
+ },
44113
+ className: "text-xs px-2 py-1 bg-surface-700 hover:bg-surface-600 disabled:opacity-40 rounded transition-colors",
44114
+ children: "→ variable"
44115
+ }
44116
+ ),
44117
+ /* @__PURE__ */ jsxRuntimeExports.jsx(
44118
+ "button",
44119
+ {
44120
+ disabled: !filterKey || !filterVal,
44121
+ onClick: () => {
44122
+ onConfirm(makeJsonPathExtractSnippet(state.path, filterKey, filterVal, "environment"));
44123
+ onClose();
44124
+ },
44125
+ className: "text-xs px-2 py-1 bg-surface-700 hover:bg-surface-600 disabled:opacity-40 rounded transition-colors",
44126
+ children: "→ env"
44127
+ }
44128
+ )
44129
+ ] })
44130
+ ] })
44131
+ ] }),
44132
+ /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "mt-1 border-t border-surface-800 pt-1", children: [
44133
+ /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "text-[10px] text-surface-500 uppercase tracking-wider px-2 py-1", children: "Extract" }),
44134
+ state.type === "json" ? /* @__PURE__ */ jsxRuntimeExports.jsxs(jsxRuntimeExports.Fragment, { children: [
44135
+ /* @__PURE__ */ jsxRuntimeExports.jsx(
44136
+ "button",
44137
+ {
44138
+ onClick: () => {
44139
+ onConfirm(makeJsonExtractSnippet(state.path, "variables"));
44140
+ onClose();
44141
+ },
44142
+ className: "w-full text-left text-xs text-surface-300 hover:text-white hover:bg-surface-800 rounded px-2 py-1.5 transition-colors",
44143
+ children: "Save to variable"
44144
+ }
44145
+ ),
44146
+ /* @__PURE__ */ jsxRuntimeExports.jsx(
44147
+ "button",
44148
+ {
44149
+ onClick: () => {
44150
+ onConfirm(makeJsonExtractSnippet(state.path, "environment"));
44151
+ onClose();
44152
+ },
44153
+ className: "w-full text-left text-xs text-surface-300 hover:text-white hover:bg-surface-800 rounded px-2 py-1.5 transition-colors",
44154
+ children: "Save to environment"
44155
+ }
44156
+ )
44157
+ ] }) : /* @__PURE__ */ jsxRuntimeExports.jsxs(jsxRuntimeExports.Fragment, { children: [
44158
+ /* @__PURE__ */ jsxRuntimeExports.jsx(
44159
+ "button",
44160
+ {
44161
+ onClick: () => {
44162
+ onConfirm(makeXmlExtractSnippet(state.selector, "variables"));
44163
+ onClose();
44164
+ },
44165
+ className: "w-full text-left text-xs text-surface-300 hover:text-white hover:bg-surface-800 rounded px-2 py-1.5 transition-colors",
44166
+ children: "Save to variable"
44167
+ }
44168
+ ),
44169
+ /* @__PURE__ */ jsxRuntimeExports.jsx(
44170
+ "button",
44171
+ {
44172
+ onClick: () => {
44173
+ onConfirm(makeXmlExtractSnippet(state.selector, "environment"));
44174
+ onClose();
44175
+ },
44176
+ className: "w-full text-left text-xs text-surface-300 hover:text-white hover:bg-surface-800 rounded px-2 py-1.5 transition-colors",
44177
+ children: "Save to environment"
44178
+ }
44179
+ )
44180
+ ] })
44181
+ ] })
43899
44182
  ]
43900
44183
  }
43901
44184
  );
@@ -43924,7 +44207,7 @@ function JsonNode({
43924
44207
  onClick: (e) => onLeaf(e, path, value),
43925
44208
  className: "ml-auto opacity-0 group-hover:opacity-100 shrink-0 text-[10px] px-1.5 leading-4 py-0.5 text-blue-400 border border-blue-800 hover:border-blue-500 hover:text-blue-300 rounded transition-all",
43926
44209
  title: "Add assertion for this value",
43927
- children: "+ assert"
44210
+ children: "+ insert"
43928
44211
  }
43929
44212
  )
43930
44213
  ] });
@@ -43995,7 +44278,7 @@ function XmlNode({
43995
44278
  onClick: (e) => onLeaf(e, selector, text),
43996
44279
  className: "ml-auto opacity-0 group-hover:opacity-100 shrink-0 text-[10px] px-1.5 leading-4 py-0.5 text-blue-400 border border-blue-800 hover:border-blue-500 hover:text-blue-300 rounded transition-all",
43997
44280
  title: "Add assertion for this value",
43998
- children: "+ assert"
44281
+ children: "+ insert"
43999
44282
  }
44000
44283
  )
44001
44284
  ] });
@@ -44024,22 +44307,26 @@ function InteractiveBody({ body, contentType, onAssert }) {
44024
44307
  const [popover, setPopover] = reactExports.useState(null);
44025
44308
  const isJson = contentType.includes("json");
44026
44309
  const isXml = !isJson && (contentType.includes("xml") || contentType.includes("html"));
44310
+ let parsedJson = null;
44311
+ if (isJson) {
44312
+ try {
44313
+ parsedJson = JSON.parse(body);
44314
+ } catch {
44315
+ }
44316
+ }
44027
44317
  function handleJsonLeaf(e, path, value) {
44028
44318
  e.stopPropagation();
44029
- setPopover({ type: "json", path, value, x: e.clientX + 10, y: e.clientY + 10 });
44319
+ setPopover({ type: "json", path, value, root: parsedJson, x: e.clientX + 10, y: e.clientY + 10 });
44030
44320
  }
44031
44321
  function handleXmlLeaf(e, selector, value) {
44032
44322
  e.stopPropagation();
44033
44323
  setPopover({ type: "xml", selector, value, x: e.clientX + 10, y: e.clientY + 10 });
44034
44324
  }
44035
44325
  const treeContent = isJson ? (() => {
44036
- let parsed;
44037
- try {
44038
- parsed = JSON.parse(body);
44039
- } catch {
44326
+ if (parsedJson === null) {
44040
44327
  return /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "p-4 text-xs text-surface-600", children: "Unable to parse JSON response body" });
44041
44328
  }
44042
- return /* @__PURE__ */ jsxRuntimeExports.jsx(JsonNode, { nodeKey: null, value: parsed, path: [], depth: 0, onLeaf: handleJsonLeaf });
44329
+ return /* @__PURE__ */ jsxRuntimeExports.jsx(JsonNode, { nodeKey: null, value: parsedJson, path: [], depth: 0, onLeaf: handleJsonLeaf });
44043
44330
  })() : isXml ? (() => {
44044
44331
  const doc2 = new DOMParser().parseFromString(body, "text/xml");
44045
44332
  const root = doc2.documentElement;
@@ -44071,6 +44358,32 @@ function prettyJson(raw) {
44071
44358
  return raw;
44072
44359
  }
44073
44360
  }
44361
+ function prettyXml(raw) {
44362
+ try {
44363
+ const indent = " ";
44364
+ let result = "";
44365
+ let depth = 0;
44366
+ const tokens = raw.match(/<[^>]+>|[^<]+/g) ?? [];
44367
+ for (const token of tokens) {
44368
+ const text = token.trim();
44369
+ if (!text) continue;
44370
+ if (text.startsWith("<?") || text.startsWith("<!")) {
44371
+ result += indent.repeat(depth) + text + "\n";
44372
+ } else if (token.startsWith("</")) {
44373
+ depth = Math.max(0, depth - 1);
44374
+ result += indent.repeat(depth) + text + "\n";
44375
+ } else if (token.startsWith("<") && !token.endsWith("/>") && !token.includes("</")) {
44376
+ result += indent.repeat(depth) + text + "\n";
44377
+ depth++;
44378
+ } else {
44379
+ result += indent.repeat(depth) + text + "\n";
44380
+ }
44381
+ }
44382
+ return result.trimEnd();
44383
+ } catch {
44384
+ return raw;
44385
+ }
44386
+ }
44074
44387
  function SaveAsMockModal({ onClose }) {
44075
44388
  const mocks = useStore((s) => s.mocks);
44076
44389
  const addMock = useStore((s) => s.addMock);
@@ -44406,7 +44719,7 @@ function ResponseViewer() {
44406
44719
  const isJson = contentType.includes("json");
44407
44720
  const isXml = !isJson && (contentType.includes("xml") || contentType.includes("html"));
44408
44721
  const supportsTree = isJson || isXml;
44409
- const displayBody = isJson ? prettyJson(response.body) : response.body;
44722
+ const displayBody = isJson ? prettyJson(response.body) : isXml ? prettyXml(response.body) : response.body;
44410
44723
  const passedCount = scriptResult?.testResults.filter((t2) => t2.passed).length ?? 0;
44411
44724
  const totalCount = scriptResult?.testResults.length ?? 0;
44412
44725
  const consoleCount = scriptResult?.consoleOutput.length ?? 0;
@@ -44514,17 +44827,16 @@ function ResponseViewer() {
44514
44827
  contentType,
44515
44828
  onAssert: handleAssert
44516
44829
  }
44517
- ) : tab === "body" ? /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "flex-1 min-h-0 overflow-hidden", children: /* @__PURE__ */ jsxRuntimeExports.jsx(
44830
+ ) : tab === "body" ? /* @__PURE__ */ jsxRuntimeExports.jsx(
44518
44831
  ReactCodeMirror,
44519
44832
  {
44520
44833
  value: displayBody,
44521
- height: "100%",
44522
44834
  theme: oneDark,
44523
- extensions: isJson ? [json()] : [],
44835
+ extensions: isJson ? [json()] : isXml ? [xml()] : [],
44524
44836
  readOnly: true,
44525
44837
  basicSetup: { lineNumbers: true, foldGutter: true }
44526
44838
  }
44527
- ) }) : tab === "headers" ? /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "flex-1 min-h-0 overflow-y-auto", children: /* @__PURE__ */ jsxRuntimeExports.jsx("table", { className: "w-full text-xs px-4 py-2", children: /* @__PURE__ */ jsxRuntimeExports.jsx("tbody", { children: Object.entries(response.headers).map(([k2, v2]) => /* @__PURE__ */ jsxRuntimeExports.jsxs("tr", { className: "border-b border-surface-800", children: [
44839
+ ) : tab === "headers" ? /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "flex-1 min-h-0 overflow-y-auto", children: /* @__PURE__ */ jsxRuntimeExports.jsx("table", { className: "w-full text-xs px-4 py-2", children: /* @__PURE__ */ jsxRuntimeExports.jsx("tbody", { children: Object.entries(response.headers).map(([k2, v2]) => /* @__PURE__ */ jsxRuntimeExports.jsxs("tr", { className: "border-b border-surface-800", children: [
44528
44840
  /* @__PURE__ */ jsxRuntimeExports.jsx("td", { className: "py-1.5 px-4 text-surface-400 font-mono w-56 align-top", children: k2 }),
44529
44841
  /* @__PURE__ */ jsxRuntimeExports.jsx("td", { className: "py-1.5 px-4 text-white font-mono break-all", children: v2 })
44530
44842
  ] }, k2)) }) }) }) : tab === "tests" ? /* @__PURE__ */ jsxRuntimeExports.jsx(TestsPanel, { scriptResult }) : tab === "console" ? /* @__PURE__ */ jsxRuntimeExports.jsx(ConsolePanel, { scriptResult }) : tab === "request" ? /* @__PURE__ */ jsxRuntimeExports.jsx(RequestPanel, { sentRequest }) : null })
@@ -44934,7 +45246,7 @@ function MasterKeyModal({ onSuccess, onCancel }) {
44934
45246
  const [password, setPassword] = reactExports.useState("");
44935
45247
  const [error2, setError] = reactExports.useState("");
44936
45248
  const [copied, setCopied] = reactExports.useState(null);
44937
- async function confirm2() {
45249
+ async function confirm() {
44938
45250
  if (!password.trim()) {
44939
45251
  setError("Password cannot be empty.");
44940
45252
  return;
@@ -44980,7 +45292,7 @@ function MasterKeyModal({ onSuccess, onCancel }) {
44980
45292
  setPassword(e.target.value);
44981
45293
  setError("");
44982
45294
  },
44983
- onKeyDown: (e) => e.key === "Enter" && confirm2(),
45295
+ onKeyDown: (e) => e.key === "Enter" && confirm(),
44984
45296
  placeholder: "Enter master password…",
44985
45297
  className: "bg-surface-800 border border-surface-700 rounded px-3 py-1.5 text-sm font-mono focus:outline-none focus:border-blue-500"
44986
45298
  }
@@ -45016,7 +45328,7 @@ function MasterKeyModal({ onSuccess, onCancel }) {
45016
45328
  /* @__PURE__ */ jsxRuntimeExports.jsx(
45017
45329
  "button",
45018
45330
  {
45019
- onClick: confirm2,
45331
+ onClick: confirm,
45020
45332
  disabled: !password.trim(),
45021
45333
  className: "px-3 py-1.5 text-xs bg-blue-600 hover:bg-blue-500 disabled:opacity-40 rounded transition-colors",
45022
45334
  children: "Set Password & Continue"
@@ -45074,8 +45386,14 @@ function EnvironmentEditor({ onClose }) {
45074
45386
  const [savedIdx, setSavedIdx] = reactExports.useState(null);
45075
45387
  const [pendingEncryptIdx, setPendingEncryptIdx] = reactExports.useState(null);
45076
45388
  const [nameError, setNameError] = reactExports.useState(null);
45389
+ const deleteEnvironment = useStore((s) => s.deleteEnvironment);
45077
45390
  const envList = Object.values(environments);
45078
45391
  const env = selectedId ? environments[selectedId]?.data ?? null : null;
45392
+ function handleDelete(id2) {
45393
+ deleteEnvironment(id2);
45394
+ const remaining = Object.keys(environments).filter((k2) => k2 !== id2);
45395
+ setSelectedId(remaining[0] ?? "");
45396
+ }
45079
45397
  function updateVar(idx, patch) {
45080
45398
  if (!env) return;
45081
45399
  const vars = env.variables.map((v2, i) => i === idx ? { ...v2, ...patch } : v2);
@@ -45189,12 +45507,29 @@ function EnvironmentEditor({ onClose }) {
45189
45507
  children: [
45190
45508
  /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "w-44 border-r border-surface-800 flex flex-col flex-shrink-0", children: [
45191
45509
  /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "px-3 py-2 text-xs font-semibold text-surface-400 uppercase tracking-wider border-b border-surface-800", children: "Environments" }),
45192
- /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "flex-1 overflow-y-auto py-1", children: envList.map(({ data: e }) => /* @__PURE__ */ jsxRuntimeExports.jsx(
45193
- "button",
45510
+ /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "flex-1 overflow-y-auto py-1", children: envList.map(({ data: e }) => /* @__PURE__ */ jsxRuntimeExports.jsxs(
45511
+ "div",
45194
45512
  {
45195
- onClick: () => selectEnv(e.id),
45196
- className: `w-full text-left px-3 py-1.5 text-xs truncate transition-colors ${selectedId === e.id ? "bg-surface-800 text-white" : "text-surface-200 hover:bg-surface-800"}`,
45197
- children: e.name
45513
+ className: `group flex items-center pr-1 transition-colors ${selectedId === e.id ? "bg-surface-800" : "hover:bg-surface-800"}`,
45514
+ children: [
45515
+ /* @__PURE__ */ jsxRuntimeExports.jsx(
45516
+ "button",
45517
+ {
45518
+ onClick: () => selectEnv(e.id),
45519
+ className: `flex-1 text-left px-3 py-1.5 text-xs truncate ${selectedId === e.id ? "text-white" : "text-surface-200"}`,
45520
+ children: e.name
45521
+ }
45522
+ ),
45523
+ /* @__PURE__ */ jsxRuntimeExports.jsx(
45524
+ "button",
45525
+ {
45526
+ onClick: () => handleDelete(e.id),
45527
+ className: "opacity-0 group-hover:opacity-100 text-surface-400 hover:text-red-400 transition-all px-1 text-sm leading-none shrink-0",
45528
+ title: "Delete environment",
45529
+ children: "×"
45530
+ }
45531
+ )
45532
+ ]
45198
45533
  },
45199
45534
  e.id
45200
45535
  )) }),
@@ -45425,16 +45760,11 @@ function EnvironmentBar({ inline = false }) {
45425
45760
  const environments = useStore((s) => s.environments);
45426
45761
  const activeEnvironmentId = useStore((s) => s.activeEnvironmentId);
45427
45762
  const setActiveEnvironment = useStore((s) => s.setActiveEnvironment);
45428
- const addEnvironment = useStore((s) => s.addEnvironment);
45429
45763
  const [showEditor, setShowEditor] = reactExports.useState(false);
45430
45764
  const [pendingEnvId, setPendingEnvId] = reactExports.useState(null);
45431
45765
  const envList = Object.values(environments);
45432
45766
  const activeEnv = activeEnvironmentId ? environments[activeEnvironmentId]?.data : null;
45433
45767
  const varCount = activeEnv?.variables.filter((v2) => v2.enabled).length ?? 0;
45434
- function handleNew() {
45435
- addEnvironment();
45436
- setShowEditor(true);
45437
- }
45438
45768
  async function handleEnvChange(id2) {
45439
45769
  if (id2) {
45440
45770
  const hasSecrets = environments[id2]?.data.variables.some((v2) => v2.enabled && v2.secret);
@@ -45485,14 +45815,6 @@ function EnvironmentBar({ inline = false }) {
45485
45815
  children: activeEnv ? "Edit" : "Manage"
45486
45816
  }
45487
45817
  ),
45488
- /* @__PURE__ */ jsxRuntimeExports.jsx(
45489
- "button",
45490
- {
45491
- onClick: handleNew,
45492
- className: "text-surface-400 hover:text-white transition-colors text-xs",
45493
- children: "+ New"
45494
- }
45495
- ),
45496
45818
  showEditor && /* @__PURE__ */ jsxRuntimeExports.jsx(EnvironmentEditor, { onClose: () => setShowEditor(false) })
45497
45819
  ] });
45498
45820
  if (inline) return /* @__PURE__ */ jsxRuntimeExports.jsx(jsxRuntimeExports.Fragment, { children: controls });
@@ -48154,6 +48476,7 @@ function App() {
48154
48476
  const sidebarTab = useStore((s) => s.sidebarTab);
48155
48477
  const setSidebarTab = useStore((s) => s.setSidebarTab);
48156
48478
  const historyCount = useStore((s) => s.history.length);
48479
+ const addCollection = useStore((s) => s.addCollection);
48157
48480
  const addMockHit = useStore((s) => s.addMockHit);
48158
48481
  const activeMockId = useStore((s) => s.activeMockId);
48159
48482
  const theme2 = useStore((s) => s.theme);
@@ -48212,12 +48535,12 @@ function App() {
48212
48535
  /* @__PURE__ */ jsxRuntimeExports.jsx(RunnerModal, {}),
48213
48536
  /* @__PURE__ */ jsxRuntimeExports.jsx(CommandPalette, {}),
48214
48537
  docsModalOpen && /* @__PURE__ */ jsxRuntimeExports.jsx(DocsGeneratorModal, { onClose: () => setDocsModalOpen(false) }),
48215
- /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "drag-region flex-shrink-0 bg-surface-950 flex items-center justify-center", children: /* @__PURE__ */ jsxRuntimeExports.jsxs("span", { className: "no-drag text-[11px] font-medium tracking-widest select-none", style: { color: "var(--text-muted)" }, children: [
48538
+ window.electron.platform !== "win32" && /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "drag-region flex-shrink-0 bg-surface-950 flex items-center justify-center", children: /* @__PURE__ */ jsxRuntimeExports.jsxs("span", { className: "no-drag text-[11px] font-medium tracking-widest select-none", style: { color: "var(--text-muted)" }, children: [
48216
48539
  "api ",
48217
48540
  /* @__PURE__ */ jsxRuntimeExports.jsx("span", { style: { color: "#6aa3c8" }, children: "Spector" }),
48218
48541
  /* @__PURE__ */ jsxRuntimeExports.jsxs("span", { className: "ml-2 text-[10px] font-normal opacity-50", children: [
48219
48542
  "v",
48220
- "0.0.4"
48543
+ "0.0.5"
48221
48544
  ] })
48222
48545
  ] }) }),
48223
48546
  /* @__PURE__ */ jsxRuntimeExports.jsx(Toolbar, { onOpenDocs: () => setDocsModalOpen(true) }),
@@ -48266,6 +48589,15 @@ function App() {
48266
48589
  /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "text-[10px] font-semibold uppercase tracking-widest text-surface-600", children: sidebarTab === "collections" ? "Collections" : sidebarTab === "history" ? "History" : sidebarTab === "mocks" ? "Mocks" : "Contracts" }),
48267
48590
  /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-center gap-1.5", children: [
48268
48591
  sidebarTab === "history" && historyCount > 0 && /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "text-[10px] bg-surface-700 text-surface-400 rounded px-1.5 py-0.5", children: historyCount }),
48592
+ sidebarTab === "collections" && /* @__PURE__ */ jsxRuntimeExports.jsx(
48593
+ "button",
48594
+ {
48595
+ onClick: () => addCollection("New Collection"),
48596
+ title: "New collection",
48597
+ className: "text-surface-600 hover:text-surface-300 transition-colors text-sm leading-none px-0.5",
48598
+ children: "+"
48599
+ }
48600
+ ),
48269
48601
  /* @__PURE__ */ jsxRuntimeExports.jsx(
48270
48602
  "button",
48271
48603
  {