@testsmith/api-spector 0.4.0 → 0.4.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.
@@ -13535,6 +13535,7 @@ const createTabsSlice = (set2, get2) => ({
13535
13535
  activeTabId: null,
13536
13536
  activeCollectionId: null,
13537
13537
  openInTab: (requestId, collectionId) => set2((s) => {
13538
+ s.collectionPanelOpen = false;
13538
13539
  const existing = s.tabs.find((t2) => t2.requestId === requestId);
13539
13540
  if (existing) {
13540
13541
  s.activeTabId = existing.id;
@@ -13568,6 +13569,7 @@ const createTabsSlice = (set2, get2) => ({
13568
13569
  }),
13569
13570
  setActiveTabId: (id2) => set2((s) => {
13570
13571
  s.activeTabId = id2;
13572
+ s.collectionPanelOpen = false;
13571
13573
  const tab = s.tabs.find((t2) => t2.id === id2);
13572
13574
  if (tab) s.activeCollectionId = tab.collectionId;
13573
13575
  }),
@@ -13983,6 +13985,16 @@ const createCollectionsSlice = (set2, get2) => ({
13983
13985
  if (folder.headers?.length) inheritedHeaders = [...inheritedHeaders, ...folder.headers];
13984
13986
  }
13985
13987
  return { auth: inheritedAuth, headers: inheritedHeaders };
13988
+ },
13989
+ getInheritedVariables: (requestId) => {
13990
+ const state = get2();
13991
+ const colEntry = Object.values(state.collections).find((c) => c.data.requests[requestId]);
13992
+ if (!colEntry) return {};
13993
+ const merged = {};
13994
+ for (const folder of findFolderPath(colEntry.data.rootFolder, requestId)) {
13995
+ if (folder.variables) Object.assign(merged, folder.variables);
13996
+ }
13997
+ return merged;
13986
13998
  }
13987
13999
  });
13988
14000
  function envSlugRelPath(name2) {
@@ -14133,6 +14145,8 @@ const createUiSlice = (set2) => ({
14133
14145
  pinnedResponse: null,
14134
14146
  activeGitDiff: null,
14135
14147
  quickInsertsOpen: true,
14148
+ sendSignal: 0,
14149
+ collectionPanelOpen: false,
14136
14150
  setShowGeneratorPanel: (v) => set2((s) => {
14137
14151
  s.showGeneratorPanel = v;
14138
14152
  }),
@@ -14180,6 +14194,12 @@ const createUiSlice = (set2) => ({
14180
14194
  }),
14181
14195
  setQuickInsertsOpen: (open) => set2((s) => {
14182
14196
  s.quickInsertsOpen = open;
14197
+ }),
14198
+ requestSend: () => set2((s) => {
14199
+ s.sendSignal += 1;
14200
+ }),
14201
+ setCollectionPanelOpen: (open) => set2((s) => {
14202
+ s.collectionPanelOpen = open;
14183
14203
  })
14184
14204
  });
14185
14205
  const useStore = create()(
@@ -35812,16 +35832,162 @@ function BearerPanel({
35812
35832
  ] })
35813
35833
  ] });
35814
35834
  }
35835
+ function parseCSV(text) {
35836
+ const lines = text.trim().split(/\r?\n/).filter(Boolean);
35837
+ if (lines.length === 0) return { columns: [], rows: [] };
35838
+ function splitRow(line) {
35839
+ const cells = [];
35840
+ let cur2 = "";
35841
+ let inQuote = false;
35842
+ for (let i = 0; i < line.length; i++) {
35843
+ const ch = line[i];
35844
+ if (ch === '"') {
35845
+ inQuote = !inQuote;
35846
+ } else if (ch === "," && !inQuote) {
35847
+ cells.push(cur2.trim());
35848
+ cur2 = "";
35849
+ } else {
35850
+ cur2 += ch;
35851
+ }
35852
+ }
35853
+ cells.push(cur2.trim());
35854
+ return cells;
35855
+ }
35856
+ const columns = splitRow(lines[0]);
35857
+ const rows = lines.slice(1).map(splitRow);
35858
+ return { columns, rows };
35859
+ }
35860
+ function toCSV(ds) {
35861
+ const escape2 = (s) => s.includes(",") || s.includes('"') ? `"${s.replace(/"/g, '""')}"` : s;
35862
+ return [ds.columns, ...ds.rows].map((row) => row.map(escape2).join(",")).join("\n");
35863
+ }
35864
+ function DataSetEditor({ ds, onChange, exportName, scopeLabel = "collection" }) {
35865
+ const csvFileRef = reactExports.useRef(null);
35866
+ function addColumn() {
35867
+ const name2 = `var${ds.columns.length + 1}`;
35868
+ onChange({ columns: [...ds.columns, name2], rows: ds.rows.map((r) => [...r, ""]) });
35869
+ }
35870
+ function renameColumn(ci, name2) {
35871
+ onChange({ ...ds, columns: ds.columns.map((c, i) => i === ci ? name2 : c) });
35872
+ }
35873
+ function removeColumn(ci) {
35874
+ onChange({ columns: ds.columns.filter((_, i) => i !== ci), rows: ds.rows.map((r) => r.filter((_, i) => i !== ci)) });
35875
+ }
35876
+ function addRow() {
35877
+ onChange({ ...ds, rows: [...ds.rows, ds.columns.map(() => "")] });
35878
+ }
35879
+ function setCell(ri, ci, v) {
35880
+ onChange({ ...ds, rows: ds.rows.map((row, i) => i === ri ? row.map((c, j) => j === ci ? v : c) : row) });
35881
+ }
35882
+ function removeRow(ri) {
35883
+ onChange({ ...ds, rows: ds.rows.filter((_, i) => i !== ri) });
35884
+ }
35885
+ function importCSV(e) {
35886
+ const file = e.target.files?.[0];
35887
+ if (!file) return;
35888
+ const reader = new FileReader();
35889
+ reader.onload = (ev) => onChange(parseCSV(ev.target?.result));
35890
+ reader.readAsText(file);
35891
+ e.target.value = "";
35892
+ }
35893
+ function exportCSV() {
35894
+ const blob = new Blob([toCSV(ds)], { type: "text/csv" });
35895
+ const url = URL.createObjectURL(blob);
35896
+ const a = document.createElement("a");
35897
+ a.href = url;
35898
+ a.download = `${exportName.replace(/\s+/g, "_")}_data.csv`;
35899
+ a.click();
35900
+ URL.revokeObjectURL(url);
35901
+ }
35902
+ const hasColumns = ds.columns.length > 0;
35903
+ const iterCount = ds.rows.length;
35904
+ return /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex flex-col gap-3 text-xs", children: [
35905
+ /* @__PURE__ */ jsxRuntimeExports.jsxs("p", { className: "text-surface-600 text-[11px]", children: [
35906
+ "Define variables here - each row runs the entire ",
35907
+ scopeLabel,
35908
+ " once with those values injected. Columns become ",
35909
+ /* @__PURE__ */ jsxRuntimeExports.jsx("code", { className: "text-surface-500", children: "{{variable}}" }),
35910
+ " placeholders."
35911
+ ] }),
35912
+ /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-center gap-2 flex-wrap", children: [
35913
+ /* @__PURE__ */ jsxRuntimeExports.jsx("button", { onClick: addColumn, className: "px-2.5 py-1 bg-surface-700 hover:bg-surface-600 rounded transition-colors", children: "+ Column" }),
35914
+ /* @__PURE__ */ jsxRuntimeExports.jsx("button", { onClick: addRow, disabled: !hasColumns, className: "px-2.5 py-1 bg-surface-700 hover:bg-surface-600 disabled:opacity-40 rounded transition-colors", children: "+ Row" }),
35915
+ /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "flex-1" }),
35916
+ /* @__PURE__ */ jsxRuntimeExports.jsx(
35917
+ "button",
35918
+ {
35919
+ onClick: () => csvFileRef.current?.click(),
35920
+ className: "px-2.5 py-1 bg-surface-700 hover:bg-surface-600 rounded transition-colors",
35921
+ title: "Import CSV - first row is column headers",
35922
+ children: "↑ Import CSV"
35923
+ }
35924
+ ),
35925
+ hasColumns && iterCount > 0 && /* @__PURE__ */ jsxRuntimeExports.jsx("button", { onClick: exportCSV, className: "px-2.5 py-1 bg-surface-700 hover:bg-surface-600 rounded transition-colors", children: "↓ Export CSV" }),
35926
+ /* @__PURE__ */ jsxRuntimeExports.jsx("input", { ref: csvFileRef, type: "file", accept: ".csv,text/csv", className: "hidden", onChange: importCSV })
35927
+ ] }),
35928
+ hasColumns && /* @__PURE__ */ jsxRuntimeExports.jsx("p", { className: "text-surface-500", children: iterCount === 0 ? "No rows yet - add rows or import a CSV." : `${iterCount} iteration${iterCount !== 1 ? "s" : ""} · columns: ${ds.columns.join(", ")}` }),
35929
+ hasColumns ? /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "overflow-x-auto", children: /* @__PURE__ */ jsxRuntimeExports.jsxs("table", { className: "w-full border-collapse text-xs", children: [
35930
+ /* @__PURE__ */ jsxRuntimeExports.jsx("thead", { children: /* @__PURE__ */ jsxRuntimeExports.jsxs("tr", { className: "border-b border-surface-700", children: [
35931
+ /* @__PURE__ */ jsxRuntimeExports.jsx("th", { className: "w-8 px-2 py-1 text-surface-600 font-normal text-left", children: "#" }),
35932
+ ds.columns.map((col, ci) => /* @__PURE__ */ jsxRuntimeExports.jsx("th", { className: "px-1 py-1 font-normal text-left min-w-[120px]", children: /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-center gap-1", children: [
35933
+ /* @__PURE__ */ jsxRuntimeExports.jsx(
35934
+ "input",
35935
+ {
35936
+ value: col,
35937
+ onChange: (e) => renameColumn(ci, e.target.value),
35938
+ className: "flex-1 bg-surface-800 border border-surface-700 rounded px-1.5 py-0.5 font-mono text-blue-400 focus:outline-none focus:border-blue-500",
35939
+ title: "Variable name"
35940
+ }
35941
+ ),
35942
+ /* @__PURE__ */ jsxRuntimeExports.jsx("button", { onClick: () => removeColumn(ci), className: "text-surface-400 hover:text-red-400 transition-colors shrink-0", children: "×" })
35943
+ ] }) }, ci)),
35944
+ /* @__PURE__ */ jsxRuntimeExports.jsx("th", { className: "w-6" })
35945
+ ] }) }),
35946
+ /* @__PURE__ */ jsxRuntimeExports.jsxs("tbody", { children: [
35947
+ ds.rows.map((row, ri) => /* @__PURE__ */ jsxRuntimeExports.jsxs("tr", { className: "group border-b border-surface-800/60 hover:bg-surface-800/30", children: [
35948
+ /* @__PURE__ */ jsxRuntimeExports.jsx("td", { className: "px-2 py-1 text-surface-600", children: ri + 1 }),
35949
+ ds.columns.map((_, ci) => /* @__PURE__ */ jsxRuntimeExports.jsx("td", { className: "px-1 py-1", children: /* @__PURE__ */ jsxRuntimeExports.jsx(
35950
+ "input",
35951
+ {
35952
+ value: row[ci] ?? "",
35953
+ onChange: (e) => setCell(ri, ci, e.target.value),
35954
+ className: "w-full bg-surface-800 border border-transparent rounded px-1.5 py-0.5 font-mono focus:outline-none focus:border-blue-500 hover:border-surface-600"
35955
+ }
35956
+ ) }, ci)),
35957
+ /* @__PURE__ */ jsxRuntimeExports.jsx("td", { className: "px-1 py-1", children: /* @__PURE__ */ jsxRuntimeExports.jsx("button", { onClick: () => removeRow(ri), className: "text-surface-400 hover:text-red-400 opacity-0 group-hover:opacity-100 transition-all", children: "×" }) })
35958
+ ] }, ri)),
35959
+ iterCount === 0 && /* @__PURE__ */ jsxRuntimeExports.jsx("tr", { children: /* @__PURE__ */ jsxRuntimeExports.jsx("td", { colSpan: ds.columns.length + 2, className: "px-2 py-3 text-surface-600 text-center", children: 'No rows - click "+ Row" or import a CSV' }) })
35960
+ ] })
35961
+ ] }) }) : /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex flex-col items-center justify-center gap-2 py-8 text-surface-600", children: [
35962
+ /* @__PURE__ */ jsxRuntimeExports.jsx("p", { children: "No columns defined." }),
35963
+ /* @__PURE__ */ jsxRuntimeExports.jsxs("p", { className: "text-[10px]", children: [
35964
+ "Click ",
35965
+ /* @__PURE__ */ jsxRuntimeExports.jsx("strong", { children: "+ Column" }),
35966
+ " to add a variable, or ",
35967
+ /* @__PURE__ */ jsxRuntimeExports.jsx("strong", { children: "↑ Import CSV" }),
35968
+ " to load from a file."
35969
+ ] })
35970
+ ] })
35971
+ ] });
35972
+ }
35815
35973
  function FolderSettingsModal({ collectionId, folder, onClose }) {
35816
35974
  const updateFolder = useStore((s) => s.updateFolder);
35817
35975
  const [activeTab, setActiveTab] = reactExports.useState("auth");
35818
35976
  const [auth, setAuth] = reactExports.useState(folder.auth ?? { type: "none" });
35819
35977
  const [headers, setHeaders] = reactExports.useState(folder.headers ?? []);
35978
+ const [varRows, setVarRows] = reactExports.useState(
35979
+ Object.entries(folder.variables ?? {}).map(([key, value]) => ({ key, value, enabled: true }))
35980
+ );
35981
+ const [dataSet, setDataSet] = reactExports.useState(folder.dataSet ?? { columns: [], rows: [] });
35820
35982
  function patchAuth(patch) {
35821
35983
  setAuth((prev) => ({ ...prev, ...patch }));
35822
35984
  }
35823
35985
  function save() {
35824
- updateFolder(collectionId, folder.id, { auth, headers });
35986
+ const variables = Object.fromEntries(
35987
+ varRows.filter((r) => r.key.trim()).map((r) => [r.key.trim(), r.value])
35988
+ );
35989
+ const cleanData = dataSet.columns.length > 0 ? dataSet : void 0;
35990
+ updateFolder(collectionId, folder.id, { auth, headers, variables, dataSet: cleanData });
35825
35991
  onClose();
35826
35992
  }
35827
35993
  return /* @__PURE__ */ jsxRuntimeExports.jsxs(
@@ -35836,12 +36002,12 @@ function FolderSettingsModal({ collectionId, folder, onClose }) {
35836
36002
  /* @__PURE__ */ jsxRuntimeExports.jsx("h2", { className: "text-sm font-semibold", children: "Folder settings" }),
35837
36003
  /* @__PURE__ */ jsxRuntimeExports.jsxs("p", { className: "text-[10px] text-surface-600 mt-0.5", children: [
35838
36004
  folder.name,
35839
- " - auth and headers inherited by all requests in this folder"
36005
+ " - auth, headers and variables inherited by all requests in this folder"
35840
36006
  ] })
35841
36007
  ] }),
35842
36008
  /* @__PURE__ */ jsxRuntimeExports.jsx("button", { onClick: onClose, className: "text-surface-400 hover:text-white text-lg leading-none", children: "×" })
35843
36009
  ] }),
35844
- /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "flex border-b border-surface-800 px-4 shrink-0", children: ["auth", "headers"].map((t2) => /* @__PURE__ */ jsxRuntimeExports.jsx(
36010
+ /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "flex border-b border-surface-800 px-4 shrink-0", children: ["auth", "headers", "variables", "data"].map((t2) => /* @__PURE__ */ jsxRuntimeExports.jsx(
35845
36011
  "button",
35846
36012
  {
35847
36013
  onClick: () => setActiveTab(t2),
@@ -35868,7 +36034,24 @@ function FolderSettingsModal({ collectionId, folder, onClose }) {
35868
36034
  valuePlaceholder: "value",
35869
36035
  headerMode: true
35870
36036
  }
35871
- )
36037
+ ),
36038
+ activeTab === "variables" && /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex flex-col gap-2", children: [
36039
+ /* @__PURE__ */ jsxRuntimeExports.jsxs("p", { className: "text-[10px] text-surface-600", children: [
36040
+ "Variables scoped to this folder. They override collection variables and are overridden by an inner folder, the active environment, and script-set values. Reference them anywhere with ",
36041
+ "{{name}}",
36042
+ "."
36043
+ ] }),
36044
+ /* @__PURE__ */ jsxRuntimeExports.jsx(
36045
+ KVTable,
36046
+ {
36047
+ rows: varRows,
36048
+ onChange: setVarRows,
36049
+ keyPlaceholder: "VARIABLE_NAME",
36050
+ valuePlaceholder: "value"
36051
+ }
36052
+ )
36053
+ ] }),
36054
+ activeTab === "data" && /* @__PURE__ */ jsxRuntimeExports.jsx(DataSetEditor, { ds: dataSet, onChange: setDataSet, exportName: folder.name, scopeLabel: "folder" })
35872
36055
  ] }),
35873
36056
  /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex gap-2 px-4 py-3 border-t border-surface-800 shrink-0", children: [
35874
36057
  /* @__PURE__ */ jsxRuntimeExports.jsx(
@@ -36855,6 +37038,7 @@ function CollectionTree() {
36855
37038
  const tabs = useStore((s) => s.tabs);
36856
37039
  const openInTab = useStore((s) => s.openInTab);
36857
37040
  const setActiveCollection = useStore((s) => s.setActiveCollection);
37041
+ const setCollectionPanelOpen = useStore((s) => s.setCollectionPanelOpen);
36858
37042
  const activeRequestId = tabs.find((t2) => t2.id === activeTabId)?.requestId ?? null;
36859
37043
  const addCollection = useStore((s) => s.addCollection);
36860
37044
  const addRequest = useStore((s) => s.addRequest);
@@ -36912,7 +37096,10 @@ function CollectionTree() {
36912
37096
  isActive: col.id === activeCollectionId,
36913
37097
  activeRequestId,
36914
37098
  existingCollectionNames: colList.map((c) => c.data.name),
36915
- onSelectCollection: () => setActiveCollection(col.id),
37099
+ onSelectCollection: () => {
37100
+ setActiveCollection(col.id);
37101
+ setCollectionPanelOpen(true);
37102
+ },
36916
37103
  onSelectRequest: (reqId) => openInTab(reqId, col.id),
36917
37104
  newRequestId,
36918
37105
  onAddRequest: (folderId) => setNewRequestId(addRequest(col.id, folderId)),
@@ -63874,6 +64061,7 @@ function RequestBuilder({ request }) {
63874
64061
  }
63875
64062
  const [editingName, setEditingName] = reactExports.useState(false);
63876
64063
  const [showFuzz, setShowFuzz] = reactExports.useState(false);
64064
+ const [customVerb, setCustomVerb] = reactExports.useState(false);
63877
64065
  const [runHooks, setRunHooks] = reactExports.useState(() => localStorage.getItem("runHooks") !== "false");
63878
64066
  function toggleRunHooks() {
63879
64067
  setRunHooks((prev) => {
@@ -63885,6 +64073,14 @@ function RequestBuilder({ request }) {
63885
64073
  function update(patch) {
63886
64074
  updateRequest(request.id, patch);
63887
64075
  }
64076
+ const sendSignal = useStore((s) => s.sendSignal);
64077
+ const lastSendSignal = reactExports.useRef(sendSignal);
64078
+ reactExports.useEffect(() => {
64079
+ if (sendSignal !== lastSendSignal.current) {
64080
+ lastSendSignal.current = sendSignal;
64081
+ void sendRequest();
64082
+ }
64083
+ }, [sendSignal]);
63888
64084
  async function sendRequest() {
63889
64085
  if (!activeTabId) return;
63890
64086
  setTabSending(activeTabId, true);
@@ -63903,6 +64099,8 @@ function RequestBuilder({ request }) {
63903
64099
  };
63904
64100
  let collectionVars = {
63905
64101
  ...activeCollectionId ? collections[activeCollectionId]?.data.collectionVariables ?? {} : {},
64102
+ // Folder-chain variables sit above collection vars and below session/local.
64103
+ ...useStore.getState().getInheritedVariables(request.id),
63906
64104
  ...sessionVars
63907
64105
  };
63908
64106
  let liveGlobals = { ...globals };
@@ -64113,15 +64311,39 @@ function RequestBuilder({ request }) {
64113
64311
  }
64114
64312
  )
64115
64313
  ] }),
64116
- !isWs && !isSoap && /* @__PURE__ */ jsxRuntimeExports.jsx(
64117
- "select",
64314
+ !isWs && !isSoap && (customVerb ? /* @__PURE__ */ jsxRuntimeExports.jsx(
64315
+ "input",
64118
64316
  {
64317
+ autoFocus: true,
64119
64318
  value: request.method,
64120
- onChange: (e) => update({ method: e.target.value }),
64121
- className: `bg-surface-800 border border-surface-700 rounded px-2 py-1.5 text-xs font-bold focus:outline-none focus:border-blue-500 ${METHOD_COLORS[request.method]}`,
64122
- children: METHODS.map((m) => /* @__PURE__ */ jsxRuntimeExports.jsx("option", { value: m, className: "text-white", children: m }, m))
64319
+ onChange: (e) => update({ method: e.target.value.toUpperCase() }),
64320
+ onBlur: () => setCustomVerb(false),
64321
+ onKeyDown: (e) => {
64322
+ if (e.key === "Enter") {
64323
+ e.preventDefault();
64324
+ setCustomVerb(false);
64325
+ }
64326
+ },
64327
+ placeholder: "VERB",
64328
+ title: "Type any HTTP method",
64329
+ className: "w-24 bg-surface-800 border border-blue-500 rounded px-2 py-1.5 text-xs font-bold uppercase focus:outline-none text-fuchsia-400 placeholder-surface-600"
64123
64330
  }
64124
- ),
64331
+ ) : /* @__PURE__ */ jsxRuntimeExports.jsxs(
64332
+ "select",
64333
+ {
64334
+ value: METHODS.includes(request.method) ? request.method : "__current__",
64335
+ onChange: (e) => {
64336
+ if (e.target.value === "__custom__") setCustomVerb(true);
64337
+ else if (e.target.value !== "__current__") update({ method: e.target.value });
64338
+ },
64339
+ className: `bg-surface-800 border border-surface-700 rounded px-2 py-1.5 text-xs font-bold focus:outline-none focus:border-blue-500 ${METHOD_COLORS[request.method] ?? "text-fuchsia-400"}`,
64340
+ children: [
64341
+ METHODS.map((m) => /* @__PURE__ */ jsxRuntimeExports.jsx("option", { value: m, className: "text-white", children: m }, m)),
64342
+ !METHODS.includes(request.method) && /* @__PURE__ */ jsxRuntimeExports.jsx("option", { value: "__current__", className: "text-white", children: request.method }),
64343
+ /* @__PURE__ */ jsxRuntimeExports.jsx("option", { value: "__custom__", className: "text-white", children: "Custom…" })
64344
+ ]
64345
+ }
64346
+ )),
64125
64347
  isSoap && /* @__PURE__ */ jsxRuntimeExports.jsx(
64126
64348
  "span",
64127
64349
  {
@@ -65297,7 +65519,7 @@ function KVBlock({ label, rows }) {
65297
65519
  ] }, k))
65298
65520
  ] });
65299
65521
  }
65300
- function HistoryTabRow({ entry, onLoad }) {
65522
+ function HistoryTabRow({ entry, onLoad, onResend }) {
65301
65523
  const [open, setOpen] = reactExports.useState(false);
65302
65524
  const reqBody = requestBodyText(entry.request.body);
65303
65525
  return /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "border-b border-surface-800", children: [
@@ -65315,7 +65537,8 @@ function HistoryTabRow({ entry, onLoad }) {
65315
65537
  ] }),
65316
65538
  entry.environmentName && /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "text-[10px] bg-surface-800 text-surface-400 px-1.5 py-0.5 rounded shrink-0", children: entry.environmentName }),
65317
65539
  /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "text-[11px] text-surface-500 ml-auto shrink-0", children: new Date(entry.timestamp).toLocaleString([], { month: "short", day: "numeric", hour: "2-digit", minute: "2-digit", second: "2-digit" }) }),
65318
- /* @__PURE__ */ jsxRuntimeExports.jsx("button", { onClick: onLoad, title: "Load this response into the viewer", className: "text-[10px] text-blue-400 hover:text-blue-300 shrink-0", children: "load" })
65540
+ /* @__PURE__ */ jsxRuntimeExports.jsx("button", { onClick: onLoad, title: "Load this response into the viewer", className: "text-[10px] text-blue-400 hover:text-blue-300 shrink-0", children: "load" }),
65541
+ onResend && /* @__PURE__ */ jsxRuntimeExports.jsx("button", { onClick: onResend, title: "Send this request again", className: "text-[10px] text-emerald-400 hover:text-emerald-300 shrink-0", children: "resend" })
65319
65542
  ] }),
65320
65543
  open && /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "px-4 pb-3 pt-1 flex flex-col gap-3 bg-surface-950/40", children: [
65321
65544
  /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex flex-col gap-1.5", children: [
@@ -65356,6 +65579,7 @@ function ResponseViewer() {
65356
65579
  const hookResults = activeTab?.lastHookResults ?? null;
65357
65580
  const requestId = activeTab?.requestId ?? null;
65358
65581
  const setTabResponse = useStore((s) => s.setTabResponse);
65582
+ const requestSend = useStore((s) => s.requestSend);
65359
65583
  const history2 = useStore((s) => s.history);
65360
65584
  const activeEnvironmentId = useStore((s) => s.activeEnvironmentId);
65361
65585
  const environments = useStore((s) => s.environments);
@@ -65379,6 +65603,13 @@ function ResponseViewer() {
65379
65603
  setTab("console");
65380
65604
  }
65381
65605
  }, [scriptResult?.preScriptError, scriptResult?.postScriptError]);
65606
+ reactExports.useEffect(() => {
65607
+ if (response?.error) {
65608
+ if (tab !== "request" && tab !== "history") setTab("error");
65609
+ } else if (tab === "error") {
65610
+ setTab("body");
65611
+ }
65612
+ }, [response]);
65382
65613
  const [diffMode, setDiffMode] = reactExports.useState(false);
65383
65614
  const [showMockModal, setShowMockModal] = reactExports.useState(false);
65384
65615
  const [bodyView, setBodyView] = reactExports.useState("raw");
@@ -65419,12 +65650,6 @@ function ResponseViewer() {
65419
65650
  if (!response) {
65420
65651
  return /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "h-full flex items-center justify-center text-surface-400 text-sm", children: "Hit Send to see the response" });
65421
65652
  }
65422
- if (response.error) {
65423
- return /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "h-full flex flex-col p-4 gap-2", children: [
65424
- /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "text-red-400 text-sm font-medium", children: "Request failed" }),
65425
- /* @__PURE__ */ jsxRuntimeExports.jsx("pre", { className: "text-xs text-red-300 whitespace-pre-wrap", children: response.error })
65426
- ] });
65427
- }
65428
65653
  const contentType = response.headers["content-type"] ?? "";
65429
65654
  const isJson = contentType.includes("json");
65430
65655
  const isXml = !isJson && (contentType.includes("xml") || contentType.includes("html"));
@@ -65442,15 +65667,31 @@ function ResponseViewer() {
65442
65667
  const totalCount = scriptResult?.testResults.length ?? 0;
65443
65668
  const consoleCount = scriptResult?.consoleOutput.length ?? 0;
65444
65669
  const hasScriptError = !!(scriptResult?.preScriptError || scriptResult?.postScriptError);
65445
- const tabList = [
65670
+ const historyBadge = requestHistory.length > 0 ? requestHistory.length : void 0;
65671
+ const tabList = response.error ? [
65672
+ { id: "error", label: "Error", error: true },
65673
+ { id: "request", label: "Request" },
65674
+ { id: "history", label: "History", badge: historyBadge }
65675
+ ] : [
65446
65676
  { id: "request", label: "Request" },
65447
65677
  { id: "body", label: "Body", badge: bodyParseError ? "!" : void 0, error: bodyParseError },
65448
65678
  { id: "headers", label: "Headers" },
65449
65679
  { id: "tests", label: "Tests", badge: totalCount > 0 ? `${passedCount}/${totalCount}` : void 0 },
65450
65680
  { id: "console", label: "Console", badge: hasScriptError ? "!" : consoleCount > 0 ? consoleCount : void 0, error: hasScriptError },
65451
- { id: "history", label: "History", badge: requestHistory.length > 0 ? requestHistory.length : void 0 },
65681
+ { id: "history", label: "History", badge: historyBadge },
65452
65682
  { id: "http", label: "HTTP", badge: httpFindings.length > 0 ? httpErrors > 0 ? "!" : httpFindings.length : void 0, error: httpErrors > 0 }
65453
65683
  ];
65684
+ const historyContent = requestHistory.length === 0 ? /* @__PURE__ */ jsxRuntimeExports.jsx("p", { className: "text-xs text-surface-500 text-center p-8", children: "No past responses for this request yet. Each send is recorded here." }) : /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "flex flex-col", children: requestHistory.map((entry) => /* @__PURE__ */ jsxRuntimeExports.jsx(
65685
+ HistoryTabRow,
65686
+ {
65687
+ entry,
65688
+ onLoad: () => {
65689
+ if (activeTabId) setTabResponse(activeTabId, entry.response, entry.scriptResult ?? null);
65690
+ },
65691
+ onResend: requestSend
65692
+ },
65693
+ entry.id
65694
+ )) });
65454
65695
  return /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "h-full flex flex-col", children: [
65455
65696
  hookResults && hookResults.length > 0 && /* @__PURE__ */ jsxRuntimeExports.jsx(HookResultsPanel, { results: hookResults }),
65456
65697
  /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-center gap-4 px-4 py-1.5 border-b border-surface-800 flex-shrink-0 overflow-x-auto", children: [
@@ -65479,7 +65720,7 @@ function ResponseViewer() {
65479
65720
  },
65480
65721
  t2.id
65481
65722
  )) }),
65482
- /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "ml-auto flex items-center gap-1 shrink-0", children: [
65723
+ !response.error && /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "ml-auto flex items-center gap-1 shrink-0", children: [
65483
65724
  assertToast.toast && /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "text-[10px] text-emerald-400 font-medium px-1", children: assertToast.toast.msg }),
65484
65725
  contractToast.toast && /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "text-[10px] text-blue-400 font-medium px-1", children: contractToast.toast.msg }),
65485
65726
  tab === "body" && supportsTree && /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex rounded overflow-hidden border border-surface-800 mr-1", children: [
@@ -65541,7 +65782,10 @@ function ResponseViewer() {
65541
65782
  ] })
65542
65783
  ] }),
65543
65784
  showMockModal && /* @__PURE__ */ jsxRuntimeExports.jsx(SaveAsMockModal, { onClose: () => setShowMockModal(false) }),
65544
- /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "flex-1 min-h-0 flex flex-col overflow-y-auto", children: diffMode && pinnedResponse ? /* @__PURE__ */ jsxRuntimeExports.jsx(DiffView, { pinned: pinnedResponse, current: response }) : tab === "body" && supportsTree && bodyView === "tree" ? /* @__PURE__ */ jsxRuntimeExports.jsx(
65785
+ /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "flex-1 min-h-0 flex flex-col overflow-y-auto", children: response.error ? tab === "request" ? /* @__PURE__ */ jsxRuntimeExports.jsx(RequestPanel, { sentRequest }) : tab === "history" ? /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "flex-1 min-h-0 overflow-y-auto", children: historyContent }) : /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex flex-col p-4 gap-2", children: [
65786
+ /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "text-red-400 text-sm font-medium", children: "Request failed" }),
65787
+ /* @__PURE__ */ jsxRuntimeExports.jsx("pre", { className: "text-xs text-red-300 whitespace-pre-wrap", children: response.error })
65788
+ ] }) : diffMode && pinnedResponse ? /* @__PURE__ */ jsxRuntimeExports.jsx(DiffView, { pinned: pinnedResponse, current: response }) : tab === "body" && supportsTree && bodyView === "tree" ? /* @__PURE__ */ jsxRuntimeExports.jsx(
65545
65789
  InteractiveBody,
65546
65790
  {
65547
65791
  body: response.body,
@@ -65587,16 +65831,7 @@ function ResponseViewer() {
65587
65831
  ] }),
65588
65832
  /* @__PURE__ */ jsxRuntimeExports.jsx("p", { className: "text-xs text-surface-200 mt-1", children: find2.message })
65589
65833
  ] }, i);
65590
- }) }) }) : tab === "history" ? /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "flex-1 min-h-0 overflow-y-auto", children: requestHistory.length === 0 ? /* @__PURE__ */ jsxRuntimeExports.jsx("p", { className: "text-xs text-surface-500 text-center p-8", children: "No past responses for this request yet. Each send is recorded here." }) : /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "flex flex-col", children: requestHistory.map((entry) => /* @__PURE__ */ jsxRuntimeExports.jsx(
65591
- HistoryTabRow,
65592
- {
65593
- entry,
65594
- onLoad: () => {
65595
- if (activeTabId) setTabResponse(activeTabId, entry.response, entry.scriptResult ?? null);
65596
- }
65597
- },
65598
- entry.id
65599
- )) }) }) : null }),
65834
+ }) }) }) : tab === "history" ? /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "flex-1 min-h-0 overflow-y-auto", children: historyContent }) : null }),
65600
65835
  headerMenu && /* @__PURE__ */ jsxRuntimeExports.jsx(
65601
65836
  ContextMenu,
65602
65837
  {
@@ -65681,7 +65916,8 @@ const TARGETS = [
65681
65916
  { id: "supertest_js", label: "Supertest JS", description: "Jest + Supertest JavaScript tests" },
65682
65917
  { id: "rest_assured", label: "REST Assured", description: "Java + JUnit 5 + Maven pom.xml" },
65683
65918
  { id: "karate", label: "Karate", description: "Karate feature files + JUnit 5 runner + Maven" },
65684
- { id: "http_file", label: "HTTP file", description: ".http / .rest file (VSCode REST Client / IntelliJ)" }
65919
+ { id: "http_file", label: "HTTP file", description: ".http / .rest file (VSCode REST Client / IntelliJ)" },
65920
+ { id: "curl", label: "cURL", description: "Runnable shell script, one curl command per request" }
65685
65921
  ];
65686
65922
  function GeneratorPanel() {
65687
65923
  const setShowGeneratorPanel = useStore((s) => s.setShowGeneratorPanel);
@@ -65937,6 +66173,7 @@ function HistoryPanel() {
65937
66173
  const activeTabId = useStore((s) => s.activeTabId);
65938
66174
  const setTabResponse = useStore((s) => s.setTabResponse);
65939
66175
  const setActiveRequest = useStore((s) => s.setActiveRequest);
66176
+ const requestSend = useStore((s) => s.requestSend);
65940
66177
  const collections = useStore((s) => s.collections);
65941
66178
  const [selected, setSelected] = reactExports.useState(null);
65942
66179
  const [search, setSearch] = reactExports.useState("");
@@ -65959,8 +66196,8 @@ function HistoryPanel() {
65959
66196
  }
65960
66197
  function open(entry) {
65961
66198
  setSelected(entry);
65962
- const stillExists = Object.values(collections).some((c) => entry.request.id in c.data.requests);
65963
- if (stillExists) {
66199
+ const stillExists2 = Object.values(collections).some((c) => entry.request.id in c.data.requests);
66200
+ if (stillExists2) {
65964
66201
  setActiveRequest(entry.request.id);
65965
66202
  const tabId = useStore.getState().activeTabId;
65966
66203
  if (tabId) setTabResponse(tabId, entry.response, entry.scriptResult ?? null);
@@ -65968,6 +66205,14 @@ function HistoryPanel() {
65968
66205
  setTabResponse(activeTabId, entry.response, entry.scriptResult ?? null);
65969
66206
  }
65970
66207
  }
66208
+ function replay(entry) {
66209
+ setSelected(entry);
66210
+ setActiveRequest(entry.request.id);
66211
+ requestSend();
66212
+ }
66213
+ function stillExists(entry) {
66214
+ return Object.values(collections).some((c) => entry.request.id in c.data.requests);
66215
+ }
65971
66216
  return /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex flex-col flex-1 min-h-0", children: [
65972
66217
  /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "px-2 py-2 border-b border-surface-800 flex gap-1.5 flex-shrink-0", children: [
65973
66218
  /* @__PURE__ */ jsxRuntimeExports.jsx(
@@ -66012,7 +66257,8 @@ function HistoryPanel() {
66012
66257
  {
66013
66258
  entry,
66014
66259
  isSelected: selected?.id === entry.id,
66015
- onSelect: () => open(entry)
66260
+ onSelect: () => open(entry),
66261
+ onResend: stillExists(entry) ? () => replay(entry) : void 0
66016
66262
  },
66017
66263
  entry.id
66018
66264
  ))
@@ -66023,19 +66269,40 @@ function HistoryPanel() {
66023
66269
  function HistoryRow({
66024
66270
  entry,
66025
66271
  isSelected,
66026
- onSelect
66272
+ onSelect,
66273
+ onResend
66027
66274
  }) {
66028
66275
  const status = entry.response.status;
66029
66276
  const hasError = !!entry.response.error;
66030
66277
  return /* @__PURE__ */ jsxRuntimeExports.jsxs(
66031
- "button",
66278
+ "div",
66032
66279
  {
66280
+ role: "button",
66281
+ tabIndex: 0,
66033
66282
  onClick: onSelect,
66034
- className: `w-full text-left px-3 py-2 border-b border-surface-800/50 transition-colors ${isSelected ? "bg-surface-800" : "hover:bg-surface-800/50"}`,
66283
+ onKeyDown: (e) => {
66284
+ if (e.key === "Enter" || e.key === " ") {
66285
+ e.preventDefault();
66286
+ onSelect();
66287
+ }
66288
+ },
66289
+ className: `group w-full text-left px-3 py-2 border-b border-surface-800/50 transition-colors cursor-pointer ${isSelected ? "bg-surface-800" : "hover:bg-surface-800/50"}`,
66035
66290
  children: [
66036
66291
  /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-center gap-2 min-w-0", children: [
66037
66292
  /* @__PURE__ */ jsxRuntimeExports.jsx(MethodBadge, { method: entry.request.method, size: "xs" }),
66038
66293
  /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "flex-1 text-xs truncate text-white", children: entry.request.name }),
66294
+ onResend && /* @__PURE__ */ jsxRuntimeExports.jsx(
66295
+ "button",
66296
+ {
66297
+ onClick: (e) => {
66298
+ e.stopPropagation();
66299
+ onResend();
66300
+ },
66301
+ title: "Send this request again",
66302
+ className: "text-[10px] text-emerald-400 hover:text-emerald-300 shrink-0 opacity-0 group-hover:opacity-100 transition-opacity",
66303
+ children: "resend"
66304
+ }
66305
+ ),
66039
66306
  hasError ? /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "text-red-400 text-[10px] font-medium shrink-0", children: "ERR" }) : /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: `text-[10px] font-bold shrink-0 ${statusColor$2(status)}`, children: status })
66040
66307
  ] }),
66041
66308
  /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-center gap-2 mt-0.5 min-w-0", children: [
@@ -66059,6 +66326,15 @@ function HistoryRow({
66059
66326
  const { electron: electron$f } = window;
66060
66327
  function WelcomeScreen() {
66061
66328
  const { applyWorkspace } = useWorkspaceLoader();
66329
+ const [recents, setRecents] = reactExports.useState([]);
66330
+ const [update, setUpdate] = reactExports.useState(null);
66331
+ reactExports.useEffect(() => {
66332
+ electron$f.getRecentWorkspaces().then(setRecents).catch(() => setRecents([]));
66333
+ electron$f.checkForUpdate().then((info) => {
66334
+ if (info?.updateAvailable) setUpdate(info);
66335
+ }).catch(() => {
66336
+ });
66337
+ }, []);
66062
66338
  async function openWorkspace() {
66063
66339
  const result = await electron$f.openWorkspace();
66064
66340
  if (!result) return;
@@ -66069,7 +66345,25 @@ function WelcomeScreen() {
66069
66345
  if (!result) return;
66070
66346
  await applyWorkspace(result.workspace, result.workspacePath);
66071
66347
  }
66348
+ async function openRecent(path) {
66349
+ const result = await electron$f.openWorkspacePath(path);
66350
+ if (!result) {
66351
+ setRecents((prev) => prev.filter((r) => r.path !== path));
66352
+ return;
66353
+ }
66354
+ await applyWorkspace(result.workspace, result.workspacePath);
66355
+ }
66072
66356
  return /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex-1 flex flex-col items-center justify-center gap-6 text-center p-8", children: [
66357
+ update && /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex flex-col items-center gap-1 px-4 py-2 rounded-lg border border-blue-800 bg-blue-950/40 text-xs", children: [
66358
+ /* @__PURE__ */ jsxRuntimeExports.jsxs("span", { className: "text-blue-200", children: [
66359
+ "New version available (v",
66360
+ update.latest,
66361
+ ", you have v",
66362
+ update.current,
66363
+ ")"
66364
+ ] }),
66365
+ /* @__PURE__ */ jsxRuntimeExports.jsx("code", { className: "text-[11px] text-blue-300 bg-surface-900 px-2 py-0.5 rounded select-all", children: update.command })
66366
+ ] }),
66073
66367
  /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { children: [
66074
66368
  /* @__PURE__ */ jsxRuntimeExports.jsxs("h1", { className: "text-2xl font-semibold mb-1", children: [
66075
66369
  /* @__PURE__ */ jsxRuntimeExports.jsx("span", { style: { color: "var(--wordmark-muted)" }, children: "API" }),
@@ -66089,7 +66383,11 @@ function WelcomeScreen() {
66089
66383
  }
66090
66384
  )
66091
66385
  ] }),
66092
- /* @__PURE__ */ jsxRuntimeExports.jsx("p", { className: "text-surface-400 text-sm max-w-sm", children: "Local-first API testing with Robot Framework & Playwright code generation. Secrets stay on your machine." })
66386
+ /* @__PURE__ */ jsxRuntimeExports.jsx("p", { className: "text-surface-400 text-sm max-w-sm", children: "Local-first API testing with Robot Framework & Playwright code generation. Secrets stay on your machine." }),
66387
+ /* @__PURE__ */ jsxRuntimeExports.jsxs("p", { className: "text-[11px] mt-3", style: { color: "var(--text-muted)" }, children: [
66388
+ "version ",
66389
+ "0.4.2"
66390
+ ] })
66093
66391
  ] }),
66094
66392
  /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex flex-col gap-3 w-64", children: [
66095
66393
  /* @__PURE__ */ jsxRuntimeExports.jsx(
@@ -66109,6 +66407,22 @@ function WelcomeScreen() {
66109
66407
  }
66110
66408
  )
66111
66409
  ] }),
66410
+ recents.length > 0 && /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex flex-col gap-1 w-72 text-left", children: [
66411
+ /* @__PURE__ */ jsxRuntimeExports.jsx("p", { className: "text-[10px] uppercase tracking-wider font-semibold px-1", style: { color: "var(--text-muted)" }, children: "Recent workspaces" }),
66412
+ recents.map((r) => /* @__PURE__ */ jsxRuntimeExports.jsxs(
66413
+ "button",
66414
+ {
66415
+ onClick: () => openRecent(r.path),
66416
+ title: r.path,
66417
+ className: "flex flex-col items-start px-2 py-1.5 rounded hover:bg-surface-800 transition-colors text-left group",
66418
+ children: [
66419
+ /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "text-xs text-surface-200 group-hover:text-white truncate max-w-full", children: r.name }),
66420
+ /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "text-[10px] text-surface-500 truncate max-w-full", children: r.path })
66421
+ ]
66422
+ },
66423
+ r.path
66424
+ ))
66425
+ ] }),
66112
66426
  /* @__PURE__ */ jsxRuntimeExports.jsxs("p", { className: "text-surface-400 text-xs max-w-xs", children: [
66113
66427
  "A workspace is a ",
66114
66428
  /* @__PURE__ */ jsxRuntimeExports.jsx("code", { className: "text-surface-500", children: ".spector" }),
@@ -67305,6 +67619,230 @@ function DocsGeneratorModal({ onClose }) {
67305
67619
  }
67306
67620
  );
67307
67621
  }
67622
+ const SKIP_WITH_ARG = /* @__PURE__ */ new Set([
67623
+ "--connect-timeout",
67624
+ "--max-time",
67625
+ "-m",
67626
+ "--retry",
67627
+ "--resolve",
67628
+ "--cacert",
67629
+ "--cert",
67630
+ "--key",
67631
+ "--proxy",
67632
+ "-x",
67633
+ "--output",
67634
+ "-o",
67635
+ "--write-out",
67636
+ "-w"
67637
+ ]);
67638
+ function tokenize(input) {
67639
+ const tokens = [];
67640
+ let cur2 = "";
67641
+ let inTok = false;
67642
+ let i = 0;
67643
+ const n = input.length;
67644
+ while (i < n) {
67645
+ const c = input[i];
67646
+ if (c === "\\" && (input[i + 1] === "\n" || input[i + 1] === "\r")) {
67647
+ i += input[i + 1] === "\r" && input[i + 2] === "\n" ? 3 : 2;
67648
+ continue;
67649
+ }
67650
+ if (c === "'") {
67651
+ inTok = true;
67652
+ i++;
67653
+ while (i < n && input[i] !== "'") cur2 += input[i++];
67654
+ i++;
67655
+ continue;
67656
+ }
67657
+ if (c === '"') {
67658
+ inTok = true;
67659
+ i++;
67660
+ while (i < n && input[i] !== '"') {
67661
+ if (input[i] === "\\" && i + 1 < n && `"\\$\``.includes(input[i + 1])) {
67662
+ cur2 += input[i + 1];
67663
+ i += 2;
67664
+ } else cur2 += input[i++];
67665
+ }
67666
+ i++;
67667
+ continue;
67668
+ }
67669
+ if (/\s/.test(c)) {
67670
+ if (inTok) {
67671
+ tokens.push(cur2);
67672
+ cur2 = "";
67673
+ inTok = false;
67674
+ }
67675
+ i++;
67676
+ continue;
67677
+ }
67678
+ if (c === "\\" && i + 1 < n) {
67679
+ cur2 += input[i + 1];
67680
+ i += 2;
67681
+ inTok = true;
67682
+ continue;
67683
+ }
67684
+ cur2 += c;
67685
+ inTok = true;
67686
+ i++;
67687
+ }
67688
+ if (inTok) tokens.push(cur2);
67689
+ return tokens;
67690
+ }
67691
+ function looksJson(s) {
67692
+ const t2 = s.trim();
67693
+ if (!(t2.startsWith("{") || t2.startsWith("["))) return false;
67694
+ try {
67695
+ JSON.parse(t2);
67696
+ return true;
67697
+ } catch {
67698
+ return false;
67699
+ }
67700
+ }
67701
+ function decodeBasic(b642) {
67702
+ try {
67703
+ const decoded = typeof atob === "function" ? atob(b642) : Buffer.from(b642, "base64").toString("utf8");
67704
+ const idx = decoded.indexOf(":");
67705
+ if (idx === -1) return null;
67706
+ return { username: decoded.slice(0, idx), password: decoded.slice(idx + 1) };
67707
+ } catch {
67708
+ return null;
67709
+ }
67710
+ }
67711
+ function parseCurl(command2) {
67712
+ const tokens = tokenize(command2.trim());
67713
+ if (tokens[0] === "curl") tokens.shift();
67714
+ let method = null;
67715
+ let url = "";
67716
+ let user = null;
67717
+ let forceGet = false;
67718
+ const headers = [];
67719
+ const dataParts = [];
67720
+ const formParts = [];
67721
+ const next = (i) => tokens[i + 1] ?? "";
67722
+ for (let i = 0; i < tokens.length; i++) {
67723
+ const t2 = tokens[i];
67724
+ switch (t2) {
67725
+ case "-X":
67726
+ case "--request":
67727
+ method = next(i);
67728
+ i++;
67729
+ break;
67730
+ case "-H":
67731
+ case "--header": {
67732
+ const raw = next(i);
67733
+ i++;
67734
+ const idx = raw.indexOf(":");
67735
+ if (idx > 0) headers.push({ key: raw.slice(0, idx).trim(), value: raw.slice(idx + 1).trim(), enabled: true });
67736
+ break;
67737
+ }
67738
+ case "-u":
67739
+ case "--user":
67740
+ user = next(i);
67741
+ i++;
67742
+ break;
67743
+ case "-d":
67744
+ case "--data":
67745
+ case "--data-raw":
67746
+ case "--data-ascii":
67747
+ case "--data-binary":
67748
+ dataParts.push(next(i));
67749
+ i++;
67750
+ break;
67751
+ case "--data-urlencode":
67752
+ dataParts.push(next(i));
67753
+ i++;
67754
+ break;
67755
+ case "-F":
67756
+ case "--form":
67757
+ formParts.push(next(i));
67758
+ i++;
67759
+ break;
67760
+ case "-b":
67761
+ case "--cookie":
67762
+ headers.push({ key: "Cookie", value: next(i), enabled: true });
67763
+ i++;
67764
+ break;
67765
+ case "-A":
67766
+ case "--user-agent":
67767
+ headers.push({ key: "User-Agent", value: next(i), enabled: true });
67768
+ i++;
67769
+ break;
67770
+ case "-e":
67771
+ case "--referer":
67772
+ headers.push({ key: "Referer", value: next(i), enabled: true });
67773
+ i++;
67774
+ break;
67775
+ case "--url":
67776
+ url = next(i);
67777
+ i++;
67778
+ break;
67779
+ case "-G":
67780
+ case "--get":
67781
+ forceGet = true;
67782
+ break;
67783
+ case "-I":
67784
+ case "--head":
67785
+ method = method ?? "HEAD";
67786
+ break;
67787
+ default:
67788
+ if (SKIP_WITH_ARG.has(t2)) {
67789
+ i++;
67790
+ break;
67791
+ }
67792
+ if (t2.startsWith("-")) break;
67793
+ if (!url) url = t2;
67794
+ break;
67795
+ }
67796
+ }
67797
+ if (!url) throw new Error("No URL found in the curl command.");
67798
+ const hasData = dataParts.length > 0;
67799
+ const isForm = formParts.length > 0;
67800
+ const resolvedMethod = (forceGet ? "GET" : method ?? (hasData || isForm ? "POST" : "GET")).toUpperCase();
67801
+ let auth = { type: "none" };
67802
+ if (user) {
67803
+ const idx = user.indexOf(":");
67804
+ auth = { type: "basic", username: idx === -1 ? user : user.slice(0, idx), password: idx === -1 ? "" : user.slice(idx + 1) };
67805
+ } else {
67806
+ const authIdx = headers.findIndex((h) => h.key.toLowerCase() === "authorization");
67807
+ if (authIdx !== -1) {
67808
+ const v = headers[authIdx].value;
67809
+ if (/^Bearer\s+/i.test(v)) {
67810
+ auth = { type: "bearer", token: v.replace(/^Bearer\s+/i, "") };
67811
+ headers.splice(authIdx, 1);
67812
+ } else if (/^Basic\s+/i.test(v)) {
67813
+ const creds = decodeBasic(v.replace(/^Basic\s+/i, "").trim());
67814
+ if (creds) {
67815
+ auth = { type: "basic", ...creds };
67816
+ headers.splice(authIdx, 1);
67817
+ }
67818
+ }
67819
+ }
67820
+ }
67821
+ let body = { mode: "none" };
67822
+ if (isForm) {
67823
+ body = {
67824
+ mode: "form",
67825
+ form: formParts.map((kv) => {
67826
+ const idx = kv.indexOf("=");
67827
+ return { key: idx === -1 ? kv : kv.slice(0, idx), value: idx === -1 ? "" : kv.slice(idx + 1), enabled: true };
67828
+ })
67829
+ };
67830
+ } else if (hasData) {
67831
+ const data = dataParts.join("&");
67832
+ const ct = headers.find((h) => h.key.toLowerCase() === "content-type")?.value;
67833
+ if (ct && ct.includes("json") || looksJson(data)) body = { mode: "json", json: data };
67834
+ else body = { mode: "raw", raw: data, rawContentType: ct ?? "application/x-www-form-urlencoded" };
67835
+ }
67836
+ let name2;
67837
+ try {
67838
+ const u = new URL(url.replace(/\{\{[^}]+\}\}/g, "x"));
67839
+ name2 = `${resolvedMethod} ${u.pathname === "/" ? u.hostname : u.pathname}`;
67840
+ } catch {
67841
+ const cleaned = url.replace(/^[a-z]+:\/\//i, "").replace(/\?.*$/, "");
67842
+ name2 = `${resolvedMethod} ${cleaned}`.trim();
67843
+ }
67844
+ return { name: name2, method: resolvedMethod, url, headers, params: [], auth, body };
67845
+ }
67308
67846
  const { electron: electron$9 } = window;
67309
67847
  const OPTIONS = [
67310
67848
  { id: "postman", label: "Postman", description: "Collection v2.1 JSON" },
@@ -67312,7 +67850,8 @@ const OPTIONS = [
67312
67850
  { id: "insomnia", label: "Insomnia", description: "Export v4 JSON" },
67313
67851
  { id: "bruno", label: "Bruno", description: "bruno.json collection file" },
67314
67852
  { id: "http", label: "HTTP file", description: ".http / .rest (REST Client)" },
67315
- { id: "spector", label: "API Spector", description: "An existing .spector / .json collection" }
67853
+ { id: "spector", label: "API Spector", description: "An existing .spector / .json collection" },
67854
+ { id: "curl", label: "cURL", description: "Paste a curl command" }
67316
67855
  ];
67317
67856
  function listEndpoints(col) {
67318
67857
  const result = [];
@@ -67336,6 +67875,7 @@ function ImportModal({ onImport, onClose }) {
67336
67875
  const markCollectionClean = useStore((s) => s.markCollectionClean);
67337
67876
  const [selected, setSelected] = reactExports.useState(null);
67338
67877
  const [url, setUrl] = reactExports.useState("");
67878
+ const [curlText, setCurlText] = reactExports.useState("");
67339
67879
  const [loading, setLoading] = reactExports.useState(false);
67340
67880
  const [error2, setError] = reactExports.useState(null);
67341
67881
  const urlInputRef = reactExports.useRef(null);
@@ -67414,6 +67954,37 @@ function ImportModal({ onImport, onClose }) {
67414
67954
  setLoading(false);
67415
67955
  }
67416
67956
  }
67957
+ function importCurl() {
67958
+ const text = curlText.trim();
67959
+ if (!text) return;
67960
+ setError(null);
67961
+ try {
67962
+ const parsed = parseCurl(text);
67963
+ const reqId = v4();
67964
+ const col = {
67965
+ version: "1.0",
67966
+ id: v4(),
67967
+ name: parsed.name || "Imported request",
67968
+ description: "",
67969
+ rootFolder: { id: v4(), name: "root", description: "", folders: [], requestIds: [reqId] },
67970
+ requests: {
67971
+ [reqId]: {
67972
+ id: reqId,
67973
+ name: parsed.name,
67974
+ method: parsed.method,
67975
+ url: parsed.url,
67976
+ headers: parsed.headers,
67977
+ params: parsed.params,
67978
+ auth: parsed.auth,
67979
+ body: parsed.body
67980
+ }
67981
+ }
67982
+ };
67983
+ enterPreview(col);
67984
+ } catch (err) {
67985
+ setError(err instanceof Error ? err.message : String(err));
67986
+ }
67987
+ }
67417
67988
  function toggleId(id2) {
67418
67989
  setChosenIds((prev) => {
67419
67990
  const next = new Set(prev);
@@ -67548,7 +68119,7 @@ function ImportModal({ onImport, onClose }) {
67548
68119
  children: [
67549
68120
  /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-center justify-between", children: [
67550
68121
  /* @__PURE__ */ jsxRuntimeExports.jsxs("h2", { className: "text-sm font-semibold text-surface-100", children: [
67551
- "Import OpenAPI - ",
68122
+ "Import - ",
67552
68123
  previewCol.name
67553
68124
  ] }),
67554
68125
  /* @__PURE__ */ jsxRuntimeExports.jsx(
@@ -67800,6 +68371,24 @@ function ImportModal({ onImport, onClose }) {
67800
68371
  )
67801
68372
  ] })
67802
68373
  ] }),
68374
+ selected === "curl" && /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex flex-col gap-2", children: [
68375
+ /* @__PURE__ */ jsxRuntimeExports.jsx("p", { className: "text-[10px] text-surface-500 uppercase tracking-wider font-medium", children: "Paste a curl command" }),
68376
+ /* @__PURE__ */ jsxRuntimeExports.jsx(
68377
+ "textarea",
68378
+ {
68379
+ value: curlText,
68380
+ onChange: (e) => {
68381
+ setCurlText(e.target.value);
68382
+ setError(null);
68383
+ },
68384
+ placeholder: `curl -X POST https://api.example.com/users \\
68385
+ -H 'Content-Type: application/json' \\
68386
+ -d '{"name":"Ada"}'`,
68387
+ rows: 6,
68388
+ className: "w-full text-xs bg-surface-800 border border-surface-700 rounded px-2.5 py-1.5 focus:outline-none focus:border-blue-500 placeholder-surface-600 font-mono resize-y"
68389
+ }
68390
+ )
68391
+ ] }),
67803
68392
  error2 && /* @__PURE__ */ jsxRuntimeExports.jsx("p", { className: "text-[11px] text-red-400", children: error2 }),
67804
68393
  /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex justify-end gap-2 pt-1", children: [
67805
68394
  /* @__PURE__ */ jsxRuntimeExports.jsx(
@@ -67810,7 +68399,15 @@ function ImportModal({ onImport, onClose }) {
67810
68399
  children: "Cancel"
67811
68400
  }
67812
68401
  ),
67813
- /* @__PURE__ */ jsxRuntimeExports.jsx(
68402
+ selected === "curl" ? /* @__PURE__ */ jsxRuntimeExports.jsx(
68403
+ "button",
68404
+ {
68405
+ disabled: !curlText.trim() || loading,
68406
+ onClick: importCurl,
68407
+ className: "px-3 py-1.5 text-xs bg-blue-700 hover:bg-blue-600 disabled:bg-surface-800 disabled:text-surface-600 rounded transition-colors",
68408
+ children: "Parse"
68409
+ }
68410
+ ) : /* @__PURE__ */ jsxRuntimeExports.jsx(
67814
68411
  "button",
67815
68412
  {
67816
68413
  disabled: !selected || loading,
@@ -68546,7 +69143,7 @@ function RunnerModal() {
68546
69143
  const colEntry = collectionId ? collections[collectionId] : null;
68547
69144
  const colName = colEntry?.data.name ?? "Collection";
68548
69145
  const folderName = folderId && colEntry ? findFolder$1(colEntry.data.rootFolder, folderId)?.name ?? "Folder" : null;
68549
- const dataSet = colEntry?.data.dataSet ?? { columns: [], rows: [] };
69146
+ const dataSet = (folderId && colEntry ? findFolder$1(colEntry.data.rootFolder, folderId)?.dataSet : null) ?? colEntry?.data.dataSet ?? { columns: [], rows: [] };
68550
69147
  const iterCount = dataSet.rows.length;
68551
69148
  const availableTags = collectionId ? allTagsIn(collectionId, folderId) : [];
68552
69149
  const progressIdxRef = reactExports.useRef(0);
@@ -68562,7 +69159,7 @@ function RunnerModal() {
68562
69159
  }, [runnerModal.open]);
68563
69160
  const toggleTag = (tag) => setFilterTags((prev) => prev.includes(tag) ? prev.filter((t2) => t2 !== tag) : [...prev, tag]);
68564
69161
  const run = reactExports.useCallback(async () => {
68565
- const ds = colEntry?.data.dataSet ?? { columns: [], rows: [] };
69162
+ const ds = (folderId && colEntry ? findFolder$1(colEntry.data.rootFolder, folderId)?.dataSet : null) ?? colEntry?.data.dataSet ?? { columns: [], rows: [] };
68566
69163
  const baseItems = collectionId ? collectRequests(collectionId, folderId, filterTags) : [];
68567
69164
  if (baseItems.length === 0) return;
68568
69165
  let items2;
@@ -68867,42 +69464,12 @@ function RunnerModal() {
68867
69464
  }
68868
69465
  );
68869
69466
  }
68870
- function parseCSV(text) {
68871
- const lines = text.trim().split(/\r?\n/).filter(Boolean);
68872
- if (lines.length === 0) return { columns: [], rows: [] };
68873
- function splitRow(line) {
68874
- const cells = [];
68875
- let cur2 = "";
68876
- let inQuote = false;
68877
- for (let i = 0; i < line.length; i++) {
68878
- const ch = line[i];
68879
- if (ch === '"') {
68880
- inQuote = !inQuote;
68881
- } else if (ch === "," && !inQuote) {
68882
- cells.push(cur2.trim());
68883
- cur2 = "";
68884
- } else {
68885
- cur2 += ch;
68886
- }
68887
- }
68888
- cells.push(cur2.trim());
68889
- return cells;
68890
- }
68891
- const columns = splitRow(lines[0]);
68892
- const rows = lines.slice(1).map(splitRow);
68893
- return { columns, rows };
68894
- }
68895
- function toCSV(ds) {
68896
- const escape2 = (s) => s.includes(",") || s.includes('"') ? `"${s.replace(/"/g, '""')}"` : s;
68897
- return [ds.columns, ...ds.rows].map((row) => row.map(escape2).join(",")).join("\n");
68898
- }
68899
69467
  function CollectionPanel() {
68900
69468
  const activeCollectionId = useStore((s) => s.activeCollectionId);
68901
69469
  const collections = useStore((s) => s.collections);
68902
69470
  const updateCollectionDataSet = useStore((s) => s.updateCollectionDataSet);
68903
69471
  const openRunner = useStore((s) => s.openRunner);
68904
69472
  const [activeTab, setActiveTab] = reactExports.useState("data");
68905
- const csvFileRef = reactExports.useRef(null);
68906
69473
  if (!activeCollectionId) {
68907
69474
  return /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "flex items-center justify-center h-full text-surface-400 text-sm", children: "Select a request from the sidebar" });
68908
69475
  }
@@ -68912,43 +69479,6 @@ function CollectionPanel() {
68912
69479
  function setDs(next) {
68913
69480
  updateCollectionDataSet(activeCollectionId, next);
68914
69481
  }
68915
- function addColumn() {
68916
- const name2 = `var${ds.columns.length + 1}`;
68917
- setDs({ columns: [...ds.columns, name2], rows: ds.rows.map((r) => [...r, ""]) });
68918
- }
68919
- function renameColumn(ci, name2) {
68920
- setDs({ ...ds, columns: ds.columns.map((c, i) => i === ci ? name2 : c) });
68921
- }
68922
- function removeColumn(ci) {
68923
- setDs({ columns: ds.columns.filter((_, i) => i !== ci), rows: ds.rows.map((r) => r.filter((_, i) => i !== ci)) });
68924
- }
68925
- function addRow() {
68926
- setDs({ ...ds, rows: [...ds.rows, ds.columns.map(() => "")] });
68927
- }
68928
- function setCell(ri, ci, v) {
68929
- setDs({ ...ds, rows: ds.rows.map((row, i) => i === ri ? row.map((c, j) => j === ci ? v : c) : row) });
68930
- }
68931
- function removeRow(ri) {
68932
- setDs({ ...ds, rows: ds.rows.filter((_, i) => i !== ri) });
68933
- }
68934
- function importCSV(e) {
68935
- const file = e.target.files?.[0];
68936
- if (!file) return;
68937
- const reader = new FileReader();
68938
- reader.onload = (ev) => setDs(parseCSV(ev.target?.result));
68939
- reader.readAsText(file);
68940
- e.target.value = "";
68941
- }
68942
- function exportCSV() {
68943
- const blob = new Blob([toCSV(ds)], { type: "text/csv" });
68944
- const url = URL.createObjectURL(blob);
68945
- const a = document.createElement("a");
68946
- a.href = url;
68947
- a.download = `${col.name.replace(/\s+/g, "_")}_data.csv`;
68948
- a.click();
68949
- URL.revokeObjectURL(url);
68950
- }
68951
- const hasColumns = ds.columns.length > 0;
68952
69482
  const iterCount = ds.rows.length;
68953
69483
  return /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex flex-col h-full", children: [
68954
69484
  /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "px-6 py-4 border-b border-surface-800 flex-shrink-0 flex items-center justify-between", children: [
@@ -68988,72 +69518,7 @@ function CollectionPanel() {
68988
69518
  tab.id
68989
69519
  )) }),
68990
69520
  /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex-1 overflow-y-auto px-6 py-4", children: [
68991
- activeTab === "data" && /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex flex-col gap-3 text-xs", children: [
68992
- /* @__PURE__ */ jsxRuntimeExports.jsxs("p", { className: "text-surface-600 text-[11px]", children: [
68993
- "Define variables here - each row runs the entire collection once with those values injected. Columns become ",
68994
- /* @__PURE__ */ jsxRuntimeExports.jsx("code", { className: "text-surface-500", children: "{{variable}}" }),
68995
- " placeholders."
68996
- ] }),
68997
- /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-center gap-2 flex-wrap", children: [
68998
- /* @__PURE__ */ jsxRuntimeExports.jsx("button", { onClick: addColumn, className: "px-2.5 py-1 bg-surface-700 hover:bg-surface-600 rounded transition-colors", children: "+ Column" }),
68999
- /* @__PURE__ */ jsxRuntimeExports.jsx("button", { onClick: addRow, disabled: !hasColumns, className: "px-2.5 py-1 bg-surface-700 hover:bg-surface-600 disabled:opacity-40 rounded transition-colors", children: "+ Row" }),
69000
- /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "flex-1" }),
69001
- /* @__PURE__ */ jsxRuntimeExports.jsx(
69002
- "button",
69003
- {
69004
- onClick: () => csvFileRef.current?.click(),
69005
- className: "px-2.5 py-1 bg-surface-700 hover:bg-surface-600 rounded transition-colors",
69006
- title: "Import CSV - first row is column headers",
69007
- children: "↑ Import CSV"
69008
- }
69009
- ),
69010
- hasColumns && iterCount > 0 && /* @__PURE__ */ jsxRuntimeExports.jsx("button", { onClick: exportCSV, className: "px-2.5 py-1 bg-surface-700 hover:bg-surface-600 rounded transition-colors", children: "↓ Export CSV" }),
69011
- /* @__PURE__ */ jsxRuntimeExports.jsx("input", { ref: csvFileRef, type: "file", accept: ".csv,text/csv", className: "hidden", onChange: importCSV })
69012
- ] }),
69013
- hasColumns && /* @__PURE__ */ jsxRuntimeExports.jsx("p", { className: "text-surface-500", children: iterCount === 0 ? "No rows yet - add rows or import a CSV." : `${iterCount} iteration${iterCount !== 1 ? "s" : ""} · columns: ${ds.columns.join(", ")}` }),
69014
- hasColumns ? /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "overflow-x-auto", children: /* @__PURE__ */ jsxRuntimeExports.jsxs("table", { className: "w-full border-collapse text-xs", children: [
69015
- /* @__PURE__ */ jsxRuntimeExports.jsx("thead", { children: /* @__PURE__ */ jsxRuntimeExports.jsxs("tr", { className: "border-b border-surface-700", children: [
69016
- /* @__PURE__ */ jsxRuntimeExports.jsx("th", { className: "w-8 px-2 py-1 text-surface-600 font-normal text-left", children: "#" }),
69017
- ds.columns.map((col2, ci) => /* @__PURE__ */ jsxRuntimeExports.jsx("th", { className: "px-1 py-1 font-normal text-left min-w-[120px]", children: /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-center gap-1", children: [
69018
- /* @__PURE__ */ jsxRuntimeExports.jsx(
69019
- "input",
69020
- {
69021
- value: col2,
69022
- onChange: (e) => renameColumn(ci, e.target.value),
69023
- className: "flex-1 bg-surface-800 border border-surface-700 rounded px-1.5 py-0.5 font-mono text-blue-400 focus:outline-none focus:border-blue-500",
69024
- title: "Variable name"
69025
- }
69026
- ),
69027
- /* @__PURE__ */ jsxRuntimeExports.jsx("button", { onClick: () => removeColumn(ci), className: "text-surface-400 hover:text-red-400 transition-colors shrink-0", children: "×" })
69028
- ] }) }, ci)),
69029
- /* @__PURE__ */ jsxRuntimeExports.jsx("th", { className: "w-6" })
69030
- ] }) }),
69031
- /* @__PURE__ */ jsxRuntimeExports.jsxs("tbody", { children: [
69032
- ds.rows.map((row, ri) => /* @__PURE__ */ jsxRuntimeExports.jsxs("tr", { className: "group border-b border-surface-800/60 hover:bg-surface-800/30", children: [
69033
- /* @__PURE__ */ jsxRuntimeExports.jsx("td", { className: "px-2 py-1 text-surface-600", children: ri + 1 }),
69034
- ds.columns.map((_, ci) => /* @__PURE__ */ jsxRuntimeExports.jsx("td", { className: "px-1 py-1", children: /* @__PURE__ */ jsxRuntimeExports.jsx(
69035
- "input",
69036
- {
69037
- value: row[ci] ?? "",
69038
- onChange: (e) => setCell(ri, ci, e.target.value),
69039
- className: "w-full bg-surface-800 border border-transparent rounded px-1.5 py-0.5 font-mono focus:outline-none focus:border-blue-500 hover:border-surface-600"
69040
- }
69041
- ) }, ci)),
69042
- /* @__PURE__ */ jsxRuntimeExports.jsx("td", { className: "px-1 py-1", children: /* @__PURE__ */ jsxRuntimeExports.jsx("button", { onClick: () => removeRow(ri), className: "text-surface-400 hover:text-red-400 opacity-0 group-hover:opacity-100 transition-all", children: "×" }) })
69043
- ] }, ri)),
69044
- iterCount === 0 && /* @__PURE__ */ jsxRuntimeExports.jsx("tr", { children: /* @__PURE__ */ jsxRuntimeExports.jsx("td", { colSpan: ds.columns.length + 2, className: "px-2 py-3 text-surface-600 text-center", children: 'No rows - click "+ Row" or import a CSV' }) })
69045
- ] })
69046
- ] }) }) : /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex flex-col items-center justify-center gap-2 py-8 text-surface-600", children: [
69047
- /* @__PURE__ */ jsxRuntimeExports.jsx("p", { children: "No columns defined." }),
69048
- /* @__PURE__ */ jsxRuntimeExports.jsxs("p", { className: "text-[10px]", children: [
69049
- "Click ",
69050
- /* @__PURE__ */ jsxRuntimeExports.jsx("strong", { children: "+ Column" }),
69051
- " to add a variable, or ",
69052
- /* @__PURE__ */ jsxRuntimeExports.jsx("strong", { children: "↑ Import CSV" }),
69053
- " to load from a file."
69054
- ] })
69055
- ] })
69056
- ] }),
69521
+ activeTab === "data" && /* @__PURE__ */ jsxRuntimeExports.jsx(DataSetEditor, { ds, onChange: setDs, exportName: col.name, scopeLabel: "collection" }),
69057
69522
  activeTab === "variables" && /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "flex flex-col gap-2 text-xs text-surface-600", children: Object.keys(col.collectionVariables ?? {}).length === 0 ? /* @__PURE__ */ jsxRuntimeExports.jsxs("p", { children: [
69058
69523
  "No collection variables. Scripts can set them via ",
69059
69524
  /* @__PURE__ */ jsxRuntimeExports.jsx("code", { className: "text-surface-500", children: "sp.collectionVariables.set(…)" }),
@@ -72017,6 +72482,7 @@ function App() {
72017
72482
  const [tabContextMenu, setTabContextMenu] = reactExports.useState(null);
72018
72483
  const showGeneratorPanel = useStore((s) => s.showGeneratorPanel);
72019
72484
  const sidebarTab = useStore((s) => s.sidebarTab);
72485
+ const collectionPanelOpen = useStore((s) => s.collectionPanelOpen);
72020
72486
  const setSidebarTab = useStore((s) => s.setSidebarTab);
72021
72487
  const historyCount = useStore((s) => s.history.length);
72022
72488
  const addCollection = useStore((s) => s.addCollection);
@@ -72134,7 +72600,7 @@ function App() {
72134
72600
  /* @__PURE__ */ jsxRuntimeExports.jsx("span", { style: { color: "#6aa3c8" }, children: "Spector" }),
72135
72601
  /* @__PURE__ */ jsxRuntimeExports.jsxs("span", { className: "ml-2 text-[10px] font-normal opacity-50", children: [
72136
72602
  "v",
72137
- "0.4.0"
72603
+ "0.4.2"
72138
72604
  ] }),
72139
72605
  /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "mx-2 opacity-30", children: "·" }),
72140
72606
  /* @__PURE__ */ jsxRuntimeExports.jsx(
@@ -72365,7 +72831,7 @@ function App() {
72365
72831
  }
72366
72832
  }
72367
72833
  }
72368
- ) }) : sidebarTab === "mocks" && activeMockId ? /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "flex-1 min-h-0", children: /* @__PURE__ */ jsxRuntimeExports.jsx(MockDetailPanel, { mockId: activeMockId }) }) : activeRequest ? /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex-1 flex min-h-0", children: [
72834
+ ) }) : sidebarTab === "mocks" && activeMockId ? /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "flex-1 min-h-0", children: /* @__PURE__ */ jsxRuntimeExports.jsx(MockDetailPanel, { mockId: activeMockId }) }) : collectionPanelOpen ? /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "flex-1 min-h-0", children: /* @__PURE__ */ jsxRuntimeExports.jsx(CollectionPanel, {}) }) : activeRequest ? /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex-1 flex min-h-0", children: [
72369
72835
  /* @__PURE__ */ jsxRuntimeExports.jsx(
72370
72836
  "div",
72371
72837
  {