@testsmith/api-spector 0.3.6 → 0.3.7

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.
@@ -13404,14 +13404,33 @@ const createWsSlice = (set2) => ({
13404
13404
  })
13405
13405
  });
13406
13406
  const HISTORY_CAP = 200;
13407
- const createHistorySlice = (set2) => ({
13407
+ let saveTimer = null;
13408
+ function persistIfEnabled(get2) {
13409
+ if (!get2().workspace?.settings?.persistHistory) return;
13410
+ if (saveTimer) clearTimeout(saveTimer);
13411
+ saveTimer = setTimeout(() => {
13412
+ window.electron.saveHistory(get2().history).catch((err) => {
13413
+ console.warn("persistHistory: could not save history.json", err);
13414
+ });
13415
+ }, 800);
13416
+ }
13417
+ const createHistorySlice = (set2, get2) => ({
13408
13418
  history: [],
13409
- addHistoryEntry: (entry) => set2((s) => {
13410
- s.history.unshift(entry);
13411
- if (s.history.length > HISTORY_CAP) s.history.length = HISTORY_CAP;
13412
- }),
13413
- clearHistory: () => set2((s) => {
13414
- s.history = [];
13419
+ addHistoryEntry: (entry) => {
13420
+ set2((s) => {
13421
+ s.history.unshift(entry);
13422
+ if (s.history.length > HISTORY_CAP) s.history.length = HISTORY_CAP;
13423
+ });
13424
+ persistIfEnabled(get2);
13425
+ },
13426
+ clearHistory: () => {
13427
+ set2((s) => {
13428
+ s.history = [];
13429
+ });
13430
+ persistIfEnabled(get2);
13431
+ },
13432
+ setHistory: (entries) => set2((s) => {
13433
+ s.history = entries.slice(0, HISTORY_CAP);
13415
13434
  })
13416
13435
  });
13417
13436
  const createRunnerSlice = (set2) => ({
@@ -13983,6 +14002,21 @@ const createEnvironmentsSlice = (set2, get2) => ({
13983
14002
  updateEnvironment: (id2, data) => set2((s) => {
13984
14003
  if (s.environments[id2]) s.environments[id2].data = data;
13985
14004
  }),
14005
+ upsertEnvVar: (envId, key, value) => {
14006
+ set2((s) => {
14007
+ const env = s.environments[envId]?.data;
14008
+ if (!env) return;
14009
+ const existing = env.variables.find((v) => v.key === key && !v.secret);
14010
+ if (existing) existing.value = value;
14011
+ else env.variables.push({ key, value, enabled: true });
14012
+ });
14013
+ const entry = get2().environments[envId];
14014
+ if (entry) {
14015
+ window.electron.saveEnvironment(entry.relPath, entry.data).catch((err) => {
14016
+ console.warn("upsertEnvVar: could not save environment", entry.relPath, err);
14017
+ });
14018
+ }
14019
+ },
13986
14020
  addEnvironment: () => set2((s) => {
13987
14021
  const existingNames = Object.values(s.environments).map((e) => e.data.name);
13988
14022
  const envName = uniqueName("New Environment", existingNames);
@@ -14152,7 +14186,7 @@ const useStore = create()(
14152
14186
  immer((set2, get2, api) => ({
14153
14187
  // ── Slice composition ─────────────────────────────────────────────────────
14154
14188
  ...createWsSlice(set2),
14155
- ...createHistorySlice(set2),
14189
+ ...createHistorySlice(set2, get2),
14156
14190
  ...createRunnerSlice(set2),
14157
14191
  ...createRecorderSlice(set2),
14158
14192
  ...createContractSlice(set2),
@@ -14244,7 +14278,7 @@ const useStore = create()(
14244
14278
  })
14245
14279
  }))
14246
14280
  );
14247
- const { electron: electron$r } = window;
14281
+ const { electron: electron$s } = window;
14248
14282
  function useAutoSave() {
14249
14283
  const collections = useStore((s) => s.collections);
14250
14284
  useStore((s) => s.environments);
@@ -14260,7 +14294,7 @@ function useAutoSave() {
14260
14294
  for (const { relPath, data, dirty } of dirtyCollections) {
14261
14295
  if (!dirty) continue;
14262
14296
  try {
14263
- await electron$r.saveCollection(relPath, data);
14297
+ await electron$s.saveCollection(relPath, data);
14264
14298
  markCollectionClean(data.id);
14265
14299
  } catch (e) {
14266
14300
  console.error("Auto-save failed for", relPath, e);
@@ -14276,7 +14310,7 @@ function useAutoSave() {
14276
14310
  if (wsTimerRef.current) clearTimeout(wsTimerRef.current);
14277
14311
  wsTimerRef.current = setTimeout(async () => {
14278
14312
  try {
14279
- await electron$r.saveWorkspace(workspace);
14313
+ await electron$s.saveWorkspace(workspace);
14280
14314
  } catch {
14281
14315
  }
14282
14316
  }, 300);
@@ -14285,7 +14319,7 @@ function useAutoSave() {
14285
14319
  };
14286
14320
  }, [workspace]);
14287
14321
  }
14288
- const { electron: electron$q } = window;
14322
+ const { electron: electron$r } = window;
14289
14323
  function useWorkspaceLoader() {
14290
14324
  const loadCollection = useStore((s) => s.loadCollection);
14291
14325
  const loadEnvironment = useStore((s) => s.loadEnvironment);
@@ -14307,16 +14341,25 @@ function useWorkspaceLoader() {
14307
14341
  });
14308
14342
  if (ws2.settings?.theme) setTheme(ws2.settings.theme);
14309
14343
  if (typeof ws2.settings?.zoom === "number") setZoom(ws2.settings.zoom);
14344
+ if (ws2.settings?.persistHistory) {
14345
+ try {
14346
+ const entries = await electron$r.loadHistory();
14347
+ useStore.getState().setHistory(entries);
14348
+ } catch {
14349
+ }
14350
+ } else {
14351
+ useStore.getState().setHistory([]);
14352
+ }
14310
14353
  for (const colPath of ws2.collections) {
14311
14354
  try {
14312
- const col = await electron$q.loadCollection(colPath);
14355
+ const col = await electron$r.loadCollection(colPath);
14313
14356
  loadCollection(colPath, col);
14314
14357
  } catch {
14315
14358
  }
14316
14359
  }
14317
14360
  for (const envPath of ws2.environments) {
14318
14361
  try {
14319
- const env = await electron$q.loadEnvironment(envPath);
14362
+ const env = await electron$r.loadEnvironment(envPath);
14320
14363
  loadEnvironment(envPath, env);
14321
14364
  } catch {
14322
14365
  }
@@ -14335,19 +14378,19 @@ function useWorkspaceLoader() {
14335
14378
  }
14336
14379
  for (const relPath of ws2.mocks ?? []) {
14337
14380
  try {
14338
- const mockData = await electron$q.loadMock(relPath);
14381
+ const mockData = await electron$r.loadMock(relPath);
14339
14382
  loadMock(relPath, mockData);
14340
14383
  } catch {
14341
14384
  }
14342
14385
  }
14343
14386
  try {
14344
- const snapshots = await electron$q.listContractSnapshots(ws2.contracts ?? []);
14387
+ const snapshots = await electron$r.listContractSnapshots(ws2.contracts ?? []);
14345
14388
  for (const { relPath, snapshot } of snapshots) loadContractSnapshot(relPath, snapshot);
14346
14389
  } catch {
14347
14390
  }
14348
14391
  if (ws2.collections.length > 0) {
14349
14392
  try {
14350
- const firstCol = await electron$q.loadCollection(ws2.collections[0]);
14393
+ const firstCol = await electron$r.loadCollection(ws2.collections[0]);
14351
14394
  setActiveCollection(firstCol.id);
14352
14395
  } catch {
14353
14396
  }
@@ -33137,8 +33180,8 @@ class CompletionTooltip {
33137
33180
  if (typeof section != "string" && section.header) {
33138
33181
  ul.appendChild(section.header(section));
33139
33182
  } else {
33140
- let header = ul.appendChild(document.createElement("completion-section"));
33141
- header.textContent = name2;
33183
+ let header2 = ul.appendChild(document.createElement("completion-section"));
33184
+ header2.textContent = name2;
33142
33185
  }
33143
33186
  }
33144
33187
  }
@@ -35948,7 +35991,7 @@ function CollectionSettingsModal({ collection, onClose }) {
35948
35991
  }
35949
35992
  );
35950
35993
  }
35951
- const { electron: electron$p } = window;
35994
+ const { electron: electron$q } = window;
35952
35995
  function normalisePath(url) {
35953
35996
  let path = url.replace(/^\{\{[^}]+\}\}/, "").replace(/^https?:\/\/[^/]+/, "");
35954
35997
  if (!path.startsWith("/")) path = "/" + path;
@@ -36035,7 +36078,7 @@ function SchemaSyncModal({
36035
36078
  setLoading(true);
36036
36079
  setError(null);
36037
36080
  try {
36038
- const entries = await electron$p.extractOpenApiSchemas();
36081
+ const entries = await electron$q.extractOpenApiSchemas();
36039
36082
  if (!entries) {
36040
36083
  setLoading(false);
36041
36084
  return;
@@ -36054,7 +36097,7 @@ function SchemaSyncModal({
36054
36097
  setLoading(true);
36055
36098
  setError(null);
36056
36099
  try {
36057
- const entries = await electron$p.extractOpenApiSchemasFromUrl(trimmed);
36100
+ const entries = await electron$q.extractOpenApiSchemasFromUrl(trimmed);
36058
36101
  setSpecEntries(entries);
36059
36102
  autoSelectChanged(entries);
36060
36103
  } catch (err) {
@@ -36087,7 +36130,7 @@ function SchemaSyncModal({
36087
36130
  }
36088
36131
  const entry = useStore.getState().collections[collectionId];
36089
36132
  if (entry) {
36090
- await electron$p.saveCollection(entry.relPath, entry.data);
36133
+ await electron$q.saveCollection(entry.relPath, entry.data);
36091
36134
  markCollectionClean(collectionId);
36092
36135
  }
36093
36136
  onClose();
@@ -55343,7 +55386,7 @@ function withContentType(headers, value) {
55343
55386
  if (idx === -1) return [...headers, next];
55344
55387
  return headers.map((h, i) => i === idx ? { ...h, value, enabled: true } : h);
55345
55388
  }
55346
- const { electron: electron$o } = window;
55389
+ const { electron: electron$p } = window;
55347
55390
  function ParamTree({ params, depth = 0 }) {
55348
55391
  if (params.length === 0) {
55349
55392
  return /* @__PURE__ */ jsxRuntimeExports.jsx("p", { className: "text-[10px] text-surface-600 italic", children: "No parameters declared in WSDL." });
@@ -55369,7 +55412,7 @@ function SoapEditor({ request, onChange }) {
55369
55412
  let cancelled = false;
55370
55413
  (async () => {
55371
55414
  try {
55372
- const result = await electron$o.wsdlFetch(url);
55415
+ const result = await electron$p.wsdlFetch(url);
55373
55416
  if (cancelled) return;
55374
55417
  setOperations(result.operations);
55375
55418
  setEndpoints(result.endpoints);
@@ -55406,7 +55449,7 @@ function SoapEditor({ request, onChange }) {
55406
55449
  setFetching(true);
55407
55450
  setFetchError(null);
55408
55451
  try {
55409
- const result = await electron$o.wsdlFetch(soap.wsdlUrl.trim());
55452
+ const result = await electron$p.wsdlFetch(soap.wsdlUrl.trim());
55410
55453
  setOperations(result.operations);
55411
55454
  setEndpoints(result.endpoints);
55412
55455
  setTargetNs(result.targetNamespace);
@@ -55676,7 +55719,7 @@ function BodyTab({ request, onChange }) {
55676
55719
  mode === "soap" && /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "flex-1 min-h-0", children: /* @__PURE__ */ jsxRuntimeExports.jsx(SoapEditor, { request, onChange }) })
55677
55720
  ] });
55678
55721
  }
55679
- const { electron: electron$n } = window;
55722
+ const { electron: electron$o } = window;
55680
55723
  const AUTH_TYPES = ["none", "bearer", "basic", "digest", "ntlm", "apikey", "oauth2"];
55681
55724
  function AuthTab({ request, onChange }) {
55682
55725
  const auth = request.auth;
@@ -55690,7 +55733,7 @@ function AuthTab({ request, onChange }) {
55690
55733
  }
55691
55734
  async function saveSecret(ref2) {
55692
55735
  if (!secretValue || !ref2) return;
55693
- await electron$n.setSecret(ref2, secretValue);
55736
+ await electron$o.setSecret(ref2, secretValue);
55694
55737
  setSaved(true);
55695
55738
  setSecretValue("");
55696
55739
  setTimeout(() => setSaved(false), 2e3);
@@ -55702,7 +55745,7 @@ function AuthTab({ request, onChange }) {
55702
55745
  setOauth2Error("");
55703
55746
  try {
55704
55747
  const vars = {};
55705
- const result = await electron$n.oauth2StartFlow(oauth2Auth, vars);
55748
+ const result = await electron$o.oauth2StartFlow(oauth2Auth, vars);
55706
55749
  setAuth({
55707
55750
  oauth2CachedToken: result.accessToken,
55708
55751
  oauth2TokenExpiry: result.expiresAt
@@ -55720,7 +55763,7 @@ function AuthTab({ request, onChange }) {
55720
55763
  setOauth2Status("fetching");
55721
55764
  setOauth2Error("");
55722
55765
  try {
55723
- const result = await electron$n.oauth2RefreshToken(oauth2Auth, {}, oauth2RefreshToken);
55766
+ const result = await electron$o.oauth2RefreshToken(oauth2Auth, {}, oauth2RefreshToken);
55724
55767
  setAuth({
55725
55768
  oauth2CachedToken: result.accessToken,
55726
55769
  oauth2TokenExpiry: result.expiresAt
@@ -63028,7 +63071,7 @@ function SchemaTab({ request, onChange }) {
63028
63071
  ] }) })
63029
63072
  ] });
63030
63073
  }
63031
- const { electron: electron$m } = window;
63074
+ const { electron: electron$n } = window;
63032
63075
  const EMPTY = { statusCode: 200, headers: [], bodySchema: "" };
63033
63076
  function ContractTab({ request, onChange }) {
63034
63077
  const activeTab = useStore((s) => s.tabs.find((t2) => t2.id === s.activeTabId));
@@ -63042,7 +63085,7 @@ function ContractTab({ request, onChange }) {
63042
63085
  if (!lastResponse?.body) return;
63043
63086
  setInferring(true);
63044
63087
  try {
63045
- const schema = await electron$m.inferContractSchema(lastResponse.body);
63088
+ const schema = await electron$n.inferContractSchema(lastResponse.body);
63046
63089
  if (schema) update({ bodySchema: schema });
63047
63090
  } finally {
63048
63091
  setInferring(false);
@@ -63168,7 +63211,7 @@ function ContractTab({ request, onChange }) {
63168
63211
  ] })
63169
63212
  ] });
63170
63213
  }
63171
- const { electron: electron$l } = window;
63214
+ const { electron: electron$m } = window;
63172
63215
  function formatTime$1(ts) {
63173
63216
  const d = new Date(ts);
63174
63217
  const hh = String(d.getHours()).padStart(2, "0");
@@ -63188,14 +63231,14 @@ function WebSocketPanel({ request }) {
63188
63231
  const [sendText, setSendText] = reactExports.useState("");
63189
63232
  const logEndRef = reactExports.useRef(null);
63190
63233
  reactExports.useEffect(() => {
63191
- electron$l.onWsMessage(({ requestId, message }) => {
63234
+ electron$m.onWsMessage(({ requestId, message }) => {
63192
63235
  addWsMessage(requestId, message);
63193
63236
  });
63194
- electron$l.onWsStatus(({ requestId, status, error: error2 }) => {
63237
+ electron$m.onWsStatus(({ requestId, status, error: error2 }) => {
63195
63238
  setWsStatus(requestId, status, error2);
63196
63239
  });
63197
63240
  return () => {
63198
- electron$l.offWsEvents();
63241
+ electron$m.offWsEvents();
63199
63242
  };
63200
63243
  }, [addWsMessage, setWsStatus]);
63201
63244
  reactExports.useEffect(() => {
@@ -63208,19 +63251,19 @@ function WebSocketPanel({ request }) {
63208
63251
  if (h.enabled && h.key) headers[h.key] = h.value;
63209
63252
  }
63210
63253
  try {
63211
- await electron$l.wsConnect(request.id, request.url, headers);
63254
+ await electron$m.wsConnect(request.id, request.url, headers);
63212
63255
  } catch (err) {
63213
63256
  setWsStatus(request.id, "error", err instanceof Error ? err.message : String(err));
63214
63257
  }
63215
63258
  }
63216
63259
  async function disconnect() {
63217
- await electron$l.wsDisconnect(request.id);
63260
+ await electron$m.wsDisconnect(request.id);
63218
63261
  }
63219
63262
  async function sendMessage() {
63220
63263
  const text = sendText.trim();
63221
63264
  if (!text || !isConnected) return;
63222
63265
  try {
63223
- await electron$l.wsSend(request.id, text);
63266
+ await electron$m.wsSend(request.id, text);
63224
63267
  const msg = {
63225
63268
  id: crypto.randomUUID(),
63226
63269
  direction: "sent",
@@ -63592,7 +63635,7 @@ function FuzzResultsPanel({ report, onClear }) {
63592
63635
  ] }) })
63593
63636
  ] });
63594
63637
  }
63595
- const { electron: electron$k } = window;
63638
+ const { electron: electron$l } = window;
63596
63639
  const WRITE_METHODS = /* @__PURE__ */ new Set(["POST", "PUT", "PATCH", "DELETE"]);
63597
63640
  function FuzzModal({ request, onClose }) {
63598
63641
  const environments = useStore((s) => s.environments);
@@ -63618,7 +63661,7 @@ function FuzzModal({ request, onClose }) {
63618
63661
  const env = resolveEnvironmentById(environments, activeEnvironmentId);
63619
63662
  const envVars = env ? Object.fromEntries(env.variables.filter((v) => v.enabled).map((v) => [v.key, v.value])) : {};
63620
63663
  const collectionVars = activeCollectionId ? collections[activeCollectionId]?.data.collectionVariables ?? {} : {};
63621
- const result = await electron$k.fuzzContracts({
63664
+ const result = await electron$l.fuzzContracts({
63622
63665
  requests: [request],
63623
63666
  envVars,
63624
63667
  collectionVars,
@@ -63710,7 +63753,7 @@ function FuzzModal({ request, onClose }) {
63710
63753
  }
63711
63754
  );
63712
63755
  }
63713
- const { electron: electron$j } = window;
63756
+ const { electron: electron$k } = window;
63714
63757
  const METHODS = ["GET", "POST", "PUT", "PATCH", "DELETE", "HEAD", "OPTIONS", "QUERY"];
63715
63758
  const METHOD_COLORS = {
63716
63759
  GET: "text-emerald-400",
@@ -63802,7 +63845,7 @@ function RequestBuilder({ request }) {
63802
63845
  activeEnvironmentId
63803
63846
  );
63804
63847
  const hookSessionVars = useStore.getState().sessionVars;
63805
- const r = await electron$j.sendRequest({
63848
+ const r = await electron$k.sendRequest({
63806
63849
  ...basePayload,
63807
63850
  environment: hookEnv,
63808
63851
  request: hook,
@@ -63846,7 +63889,7 @@ function RequestBuilder({ request }) {
63846
63889
  activeEnvironmentId
63847
63890
  );
63848
63891
  const freshSessionVars = useStore.getState().sessionVars;
63849
- const result = await electron$j.sendRequest({
63892
+ const result = await electron$k.sendRequest({
63850
63893
  ...basePayload,
63851
63894
  environment: freshEnv,
63852
63895
  request: mergedRequest,
@@ -63874,7 +63917,7 @@ function RequestBuilder({ request }) {
63874
63917
  activeEnvironmentId
63875
63918
  );
63876
63919
  const hookSessionVars = useStore.getState().sessionVars;
63877
- const r = await electron$j.sendRequest({
63920
+ const r = await electron$k.sendRequest({
63878
63921
  ...basePayload,
63879
63922
  environment: hookEnv,
63880
63923
  request: hook,
@@ -64650,7 +64693,7 @@ function HookResultsPanel({ results }) {
64650
64693
  }) })
64651
64694
  ] });
64652
64695
  }
64653
- const { electron: electron$i } = window;
64696
+ const { electron: electron$j } = window;
64654
64697
  function extractPath(url) {
64655
64698
  try {
64656
64699
  return new URL(url).pathname || "/";
@@ -64704,14 +64747,14 @@ function SaveAsMockModal({ onClose }) {
64704
64747
  const entry = state.mocks[serverId];
64705
64748
  const updated = { ...entry.data, name: newServerName, port: Number(newServerPort), routes: [route] };
64706
64749
  updateMock(serverId, updated);
64707
- await electron$i.saveMock(entry.relPath, updated);
64750
+ await electron$j.saveMock(entry.relPath, updated);
64708
64751
  const ws2 = useStore.getState().workspace;
64709
- if (ws2) await electron$i.saveWorkspace(ws2);
64752
+ if (ws2) await electron$j.saveWorkspace(ws2);
64710
64753
  } else {
64711
64754
  const entry = useStore.getState().mocks[serverId];
64712
64755
  const updated = { ...entry.data, routes: [...entry.data.routes, route] };
64713
64756
  updateMock(serverId, updated);
64714
- await electron$i.saveMock(entry.relPath, updated);
64757
+ await electron$j.saveMock(entry.relPath, updated);
64715
64758
  }
64716
64759
  onClose();
64717
64760
  } finally {
@@ -64994,7 +65037,7 @@ function RequestPanel({ sentRequest }) {
64994
65037
  if (!sentRequest) {
64995
65038
  return /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "flex items-center justify-center h-full text-surface-400 text-xs", children: "Send a request to see what was transmitted." });
64996
65039
  }
64997
- const hasBody = sentRequest.body !== void 0 && sentRequest.body !== "";
65040
+ const hasBody2 = sentRequest.body !== void 0 && sentRequest.body !== "";
64998
65041
  return /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex-1 min-h-0 overflow-y-auto text-xs font-mono", children: [
64999
65042
  /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "px-4 py-3 border-b border-surface-800 flex items-center gap-3", children: [
65000
65043
  /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "font-bold text-blue-400 shrink-0", children: sentRequest.method }),
@@ -65007,7 +65050,7 @@ function RequestPanel({ sentRequest }) {
65007
65050
  /* @__PURE__ */ jsxRuntimeExports.jsx("td", { className: "py-1 text-white break-all", children: v })
65008
65051
  ] }, k)) }) })
65009
65052
  ] }),
65010
- hasBody && /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "px-4 py-2", children: [
65053
+ hasBody2 && /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "px-4 py-2", children: [
65011
65054
  /* @__PURE__ */ jsxRuntimeExports.jsx("p", { className: "text-[10px] text-surface-400 uppercase tracking-wider font-medium mb-1.5", children: "Body" }),
65012
65055
  /* @__PURE__ */ jsxRuntimeExports.jsx("pre", { className: "text-white whitespace-pre-wrap break-all text-[11px]", children: sentRequest.body })
65013
65056
  ] })
@@ -65043,7 +65086,83 @@ function ConsolePanel({ scriptResult }) {
65043
65086
  )) })
65044
65087
  ] });
65045
65088
  }
65046
- const { electron: electron$h } = window;
65089
+ function header(headers, name2) {
65090
+ const lower = name2.toLowerCase();
65091
+ for (const [k, v] of Object.entries(headers)) {
65092
+ if (k.toLowerCase() === lower) return v;
65093
+ }
65094
+ return void 0;
65095
+ }
65096
+ function hasBody(res) {
65097
+ return (res.bodySize ?? res.body.length) > 0;
65098
+ }
65099
+ const REDIRECTS_NEEDING_LOCATION = /* @__PURE__ */ new Set([301, 302, 303, 307, 308]);
65100
+ function validateHttpSemantics(res) {
65101
+ if (!res.status || res.status === 0) return [];
65102
+ const f = [];
65103
+ const method = res.method.toUpperCase();
65104
+ const { status } = res;
65105
+ const body = hasBody(res);
65106
+ const contentType = header(res.headers, "content-type");
65107
+ const contentEncoding = header(res.headers, "content-encoding");
65108
+ if (status === 204 && body) {
65109
+ f.push({ rule: "no-body-204", severity: "error", message: "204 No Content must not include a message body.", ref: "RFC 9110 §15.3.5" });
65110
+ }
65111
+ if (status === 304 && body) {
65112
+ f.push({ rule: "no-body-304", severity: "error", message: "304 Not Modified must not include a message body.", ref: "RFC 9110 §15.4.5" });
65113
+ }
65114
+ if (status >= 100 && status < 200 && body) {
65115
+ f.push({ rule: "no-body-1xx", severity: "error", message: `${status} informational responses must not include a body.`, ref: "RFC 9110 §15.2" });
65116
+ }
65117
+ if (method === "HEAD" && body) {
65118
+ f.push({ rule: "no-body-head", severity: "error", message: "Response to a HEAD request must not include a body.", ref: "RFC 9110 §9.3.2" });
65119
+ }
65120
+ if (REDIRECTS_NEEDING_LOCATION.has(status) && !header(res.headers, "location")) {
65121
+ f.push({ rule: "redirect-no-location", severity: "error", message: `${status} redirect has no Location header, so the client cannot follow it.`, ref: "RFC 9110 §15.4" });
65122
+ }
65123
+ if (status === 401 && !header(res.headers, "www-authenticate")) {
65124
+ f.push({ rule: "401-no-www-authenticate", severity: "error", message: "401 Unauthorized must include a WWW-Authenticate header.", ref: "RFC 9110 §15.5.2" });
65125
+ }
65126
+ if (status === 405 && !header(res.headers, "allow")) {
65127
+ f.push({ rule: "405-no-allow", severity: "error", message: "405 Method Not Allowed must include an Allow header listing valid methods.", ref: "RFC 9110 §15.5.6" });
65128
+ }
65129
+ if (body && !contentType && status !== 204 && status !== 304) {
65130
+ f.push({ rule: "body-no-content-type", severity: "warning", message: "Response has a body but no Content-Type header; clients must guess how to parse it.", ref: "RFC 9110 §8.3" });
65131
+ }
65132
+ if (body && contentType && /application\/(json|.*\+json)/i.test(contentType)) {
65133
+ try {
65134
+ JSON.parse(res.body);
65135
+ } catch {
65136
+ f.push({ rule: "json-invalid", severity: "error", message: `Content-Type is "${contentType}" but the body is not valid JSON.`, ref: "RFC 8259" });
65137
+ }
65138
+ }
65139
+ if (contentType && /^text\//i.test(contentType) && !/charset=/i.test(contentType)) {
65140
+ f.push({ rule: "text-no-charset", severity: "hint", message: `"${contentType}" has no charset parameter; clients may misinterpret the encoding.`, ref: "RFC 9110 §8.3.2" });
65141
+ }
65142
+ const clRaw = header(res.headers, "content-length");
65143
+ if (clRaw !== void 0 && !contentEncoding && method !== "HEAD" && status !== 204 && status !== 304) {
65144
+ const cl = Number(clRaw);
65145
+ const actual = res.bodySize ?? res.body.length;
65146
+ if (Number.isFinite(cl) && cl !== actual) {
65147
+ f.push({ rule: "content-length-mismatch", severity: "error", message: `Content-Length is ${cl} but the body is ${actual} bytes.`, ref: "RFC 9110 §8.6" });
65148
+ }
65149
+ }
65150
+ if (status < 100 || status > 599) {
65151
+ f.push({ rule: "status-out-of-range", severity: "warning", message: `${status} is not a valid HTTP status code (must be 100-599).`, ref: "RFC 9110 §15" });
65152
+ }
65153
+ if (!header(res.headers, "date") && status >= 200) {
65154
+ f.push({ rule: "no-date", severity: "hint", message: "No Date header; origin servers are expected to send one.", ref: "RFC 9110 §6.6.1" });
65155
+ }
65156
+ if ((status === 429 || status === 503) && !header(res.headers, "retry-after")) {
65157
+ f.push({ rule: "no-retry-after", severity: "hint", message: `${status} should include a Retry-After header telling clients when to retry.`, ref: "RFC 9110 §10.2.3" });
65158
+ }
65159
+ if (status === 201 && !header(res.headers, "location")) {
65160
+ f.push({ rule: "201-no-location", severity: "warning", message: "201 Created should include a Location header pointing at the new resource.", ref: "RFC 9110 §15.3.2" });
65161
+ }
65162
+ const order = { error: 0, warning: 1, hint: 2 };
65163
+ return f.sort((a, b) => order[a.severity] - order[b.severity]);
65164
+ }
65165
+ const { electron: electron$i } = window;
65047
65166
  function ResponseViewer() {
65048
65167
  const activeTab = useStore((s) => s.tabs.find((t2) => t2.id === s.activeTabId));
65049
65168
  const activeTabId = useStore((s) => s.activeTabId);
@@ -65058,7 +65177,25 @@ function ResponseViewer() {
65058
65177
  const sentRequest = activeTab?.lastSentRequest ?? null;
65059
65178
  const hookResults = activeTab?.lastHookResults ?? null;
65060
65179
  const requestId = activeTab?.requestId ?? null;
65180
+ const setTabResponse = useStore((s) => s.setTabResponse);
65181
+ const history2 = useStore((s) => s.history);
65182
+ const activeEnvironmentId = useStore((s) => s.activeEnvironmentId);
65183
+ const environments = useStore((s) => s.environments);
65184
+ const upsertEnvVar = useStore((s) => s.upsertEnvVar);
65061
65185
  const [tab, setTab] = reactExports.useState("body");
65186
+ const requestHistory = requestId ? history2.filter((e) => e.request.id === requestId) : [];
65187
+ const httpFindings = response && !response.error ? validateHttpSemantics({
65188
+ method: sentRequest?.method ?? "GET",
65189
+ status: response.status,
65190
+ statusText: response.statusText,
65191
+ headers: response.headers,
65192
+ body: response.body,
65193
+ bodySize: response.bodySize
65194
+ }) : [];
65195
+ const httpErrors = httpFindings.filter((x) => x.severity === "error").length;
65196
+ const [headerMenu, setHeaderMenu] = reactExports.useState(null);
65197
+ const [varDialog, setVarDialog] = reactExports.useState(null);
65198
+ const activeEnvName = activeEnvironmentId ? environments[activeEnvironmentId]?.data.name : void 0;
65062
65199
  reactExports.useEffect(() => {
65063
65200
  if (scriptResult?.preScriptError || scriptResult?.postScriptError) {
65064
65201
  setTab("console");
@@ -65071,7 +65208,7 @@ function ResponseViewer() {
65071
65208
  const contractToast = useToast(2500);
65072
65209
  async function saveAsContract() {
65073
65210
  if (!response || !requestId || !activeTabId) return;
65074
- const schema = response.body ? await electron$h.inferContractSchema(response.body) : null;
65211
+ const schema = response.body ? await electron$i.inferContractSchema(response.body) : null;
65075
65212
  const contentType2 = response.headers["content-type"];
65076
65213
  const headers = contentType2 ? [{ key: "content-type", value: contentType2, required: true }] : [];
65077
65214
  updateRequest(requestId, {
@@ -65124,7 +65261,9 @@ function ResponseViewer() {
65124
65261
  { id: "body", label: "Body" },
65125
65262
  { id: "headers", label: "Headers" },
65126
65263
  { id: "tests", label: "Tests", badge: totalCount > 0 ? `${passedCount}/${totalCount}` : void 0 },
65127
- { id: "console", label: "Console", badge: hasScriptError ? "!" : consoleCount > 0 ? consoleCount : void 0, error: hasScriptError }
65264
+ { id: "console", label: "Console", badge: hasScriptError ? "!" : consoleCount > 0 ? consoleCount : void 0, error: hasScriptError },
65265
+ { id: "history", label: "History", badge: requestHistory.length > 0 ? requestHistory.length : void 0 },
65266
+ { id: "http", label: "HTTP", badge: httpFindings.length > 0 ? httpErrors > 0 ? "!" : httpFindings.length : void 0, error: httpErrors > 0 }
65128
65267
  ];
65129
65268
  return /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "h-full flex flex-col", children: [
65130
65269
  hookResults && hookResults.length > 0 && /* @__PURE__ */ jsxRuntimeExports.jsx(HookResultsPanel, { results: hookResults }),
@@ -65232,13 +65371,135 @@ function ResponseViewer() {
65232
65371
  readOnly: true,
65233
65372
  basicSetup: { lineNumbers: true, foldGutter: true }
65234
65373
  }
65235
- ) : 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(([k, v]) => /* @__PURE__ */ jsxRuntimeExports.jsxs("tr", { className: "border-b border-surface-800", children: [
65236
- /* @__PURE__ */ jsxRuntimeExports.jsx("td", { className: "py-1.5 px-4 text-surface-400 font-mono w-56 align-top", children: k }),
65237
- /* @__PURE__ */ jsxRuntimeExports.jsx("td", { className: "py-1.5 px-4 text-white font-mono break-all", children: v })
65238
- ] }, k)) }) }) }) : tab === "tests" ? /* @__PURE__ */ jsxRuntimeExports.jsx(TestsPanel, { scriptResult }) : tab === "console" ? /* @__PURE__ */ jsxRuntimeExports.jsx(ConsolePanel, { scriptResult }) : tab === "request" ? /* @__PURE__ */ jsxRuntimeExports.jsx(RequestPanel, { sentRequest }) : null })
65374
+ ) : 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(([k, v]) => /* @__PURE__ */ jsxRuntimeExports.jsxs(
65375
+ "tr",
65376
+ {
65377
+ className: "border-b border-surface-800 hover:bg-surface-800/40",
65378
+ onContextMenu: (e) => {
65379
+ e.preventDefault();
65380
+ setHeaderMenu({ x: e.clientX, y: e.clientY, key: k, value: v });
65381
+ },
65382
+ title: "Right-click to create an environment variable",
65383
+ children: [
65384
+ /* @__PURE__ */ jsxRuntimeExports.jsx("td", { className: "py-1.5 px-4 text-surface-400 font-mono w-56 align-top", children: k }),
65385
+ /* @__PURE__ */ jsxRuntimeExports.jsx("td", { className: "py-1.5 px-4 text-white font-mono break-all", children: v })
65386
+ ]
65387
+ },
65388
+ k
65389
+ )) }) }) }) : tab === "tests" ? /* @__PURE__ */ jsxRuntimeExports.jsx(TestsPanel, { scriptResult }) : tab === "console" ? /* @__PURE__ */ jsxRuntimeExports.jsx(ConsolePanel, { scriptResult }) : tab === "request" ? /* @__PURE__ */ jsxRuntimeExports.jsx(RequestPanel, { sentRequest }) : tab === "http" ? /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "flex-1 min-h-0 overflow-y-auto p-4", children: httpFindings.length === 0 ? /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex flex-col items-center justify-center h-full gap-2 text-center", children: [
65390
+ /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "text-2xl", children: "✓" }),
65391
+ /* @__PURE__ */ jsxRuntimeExports.jsx("p", { className: "text-sm text-emerald-400", children: "Conforms to HTTP semantics" }),
65392
+ /* @__PURE__ */ jsxRuntimeExports.jsx("p", { className: "text-xs text-surface-500 max-w-sm", children: "No violations of the HTTP specification (RFC 9110/9111) in this response. This check is automatic and needs no test or spec." })
65393
+ ] }) : /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "flex flex-col gap-2 max-w-3xl", children: httpFindings.map((find2, i) => {
65394
+ const tone = find2.severity === "error" ? "border-red-800/60 bg-red-950/20" : find2.severity === "warning" ? "border-amber-800/50 bg-amber-950/20" : "border-surface-700 bg-surface-800/40";
65395
+ const label = find2.severity === "error" ? "text-red-400" : find2.severity === "warning" ? "text-amber-400" : "text-surface-400";
65396
+ return /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: `border-l-2 rounded-r px-3 py-2 ${tone}`, children: [
65397
+ /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-center gap-2 flex-wrap", children: [
65398
+ /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: `text-[10px] font-bold uppercase tracking-wider ${label}`, children: find2.severity }),
65399
+ /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "text-[10px] font-mono text-surface-500 bg-surface-900 px-1.5 py-0.5 rounded", children: find2.rule }),
65400
+ find2.ref && /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "text-[10px] text-surface-600 ml-auto", children: find2.ref })
65401
+ ] }),
65402
+ /* @__PURE__ */ jsxRuntimeExports.jsx("p", { className: "text-xs text-surface-200 mt-1", children: find2.message })
65403
+ ] }, i);
65404
+ }) }) }) : 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.jsxs(
65405
+ "button",
65406
+ {
65407
+ onClick: () => {
65408
+ if (activeTabId) setTabResponse(activeTabId, entry.response, entry.scriptResult ?? null);
65409
+ },
65410
+ className: "flex items-center gap-3 px-4 py-2 border-b border-surface-800 hover:bg-surface-800/50 text-left transition-colors",
65411
+ children: [
65412
+ /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: `text-xs font-bold font-mono shrink-0 w-8 ${getStatusColor(entry.response.status)}`, children: entry.response.status || "ERR" }),
65413
+ /* @__PURE__ */ jsxRuntimeExports.jsxs("span", { className: "text-xs text-surface-400 shrink-0", children: [
65414
+ entry.response.durationMs,
65415
+ "ms"
65416
+ ] }),
65417
+ /* @__PURE__ */ jsxRuntimeExports.jsxs("span", { className: "text-[11px] text-surface-500 shrink-0", children: [
65418
+ (entry.response.bodySize / 1024).toFixed(1),
65419
+ " KB"
65420
+ ] }),
65421
+ 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 }),
65422
+ /* @__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" }) })
65423
+ ]
65424
+ },
65425
+ entry.id
65426
+ )) }) }) : null }),
65427
+ headerMenu && /* @__PURE__ */ jsxRuntimeExports.jsx(
65428
+ ContextMenu,
65429
+ {
65430
+ x: headerMenu.x,
65431
+ y: headerMenu.y,
65432
+ onClose: () => setHeaderMenu(null),
65433
+ items: [
65434
+ { type: "header", label: headerMenu.key },
65435
+ activeEnvironmentId ? {
65436
+ type: "item",
65437
+ label: `Create variable in "${activeEnvName}"`,
65438
+ onClick: () => {
65439
+ setVarDialog({ name: headerMenu.key.replace(/[^a-zA-Z0-9]+/g, "_").replace(/^_+|_+$/g, ""), value: headerMenu.value });
65440
+ setHeaderMenu(null);
65441
+ }
65442
+ } : { type: "header", label: "Select an environment first" }
65443
+ ]
65444
+ }
65445
+ ),
65446
+ varDialog && /* @__PURE__ */ jsxRuntimeExports.jsx(Modal, { onClose: () => setVarDialog(null), title: "Create environment variable", panelClassName: "bg-surface-900 border border-surface-800 rounded-lg shadow-2xl w-[420px]", children: /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex flex-col gap-3 p-4", children: [
65447
+ /* @__PURE__ */ jsxRuntimeExports.jsxs("label", { className: "flex flex-col gap-1", children: [
65448
+ /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "text-[10px] uppercase tracking-wider text-surface-600 font-medium", children: "Variable name" }),
65449
+ /* @__PURE__ */ jsxRuntimeExports.jsx(
65450
+ "input",
65451
+ {
65452
+ autoFocus: true,
65453
+ value: varDialog.name,
65454
+ onChange: (e) => setVarDialog((d) => d && { ...d, name: e.target.value }),
65455
+ className: "bg-surface-800 border border-surface-700 rounded px-2.5 py-1.5 text-sm font-mono focus:outline-none focus:border-blue-500"
65456
+ }
65457
+ )
65458
+ ] }),
65459
+ /* @__PURE__ */ jsxRuntimeExports.jsxs("label", { className: "flex flex-col gap-1", children: [
65460
+ /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "text-[10px] uppercase tracking-wider text-surface-600 font-medium", children: "Value" }),
65461
+ /* @__PURE__ */ jsxRuntimeExports.jsx(
65462
+ "input",
65463
+ {
65464
+ value: varDialog.value,
65465
+ onChange: (e) => setVarDialog((d) => d && { ...d, value: e.target.value }),
65466
+ className: "bg-surface-800 border border-surface-700 rounded px-2.5 py-1.5 text-sm font-mono focus:outline-none focus:border-blue-500"
65467
+ }
65468
+ )
65469
+ ] }),
65470
+ /* @__PURE__ */ jsxRuntimeExports.jsxs("p", { className: "text-[11px] text-surface-500", children: [
65471
+ "Saved to ",
65472
+ activeEnvName ? `"${activeEnvName}"` : "the active environment",
65473
+ ". Use it as ",
65474
+ /* @__PURE__ */ jsxRuntimeExports.jsxs("code", { className: "text-surface-300", children: [
65475
+ "{{",
65476
+ varDialog.name || "name",
65477
+ "}}"
65478
+ ] }),
65479
+ "."
65480
+ ] }),
65481
+ /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex justify-end gap-2 mt-1", children: [
65482
+ /* @__PURE__ */ jsxRuntimeExports.jsx("button", { onClick: () => setVarDialog(null), className: "px-3 py-1.5 text-xs text-surface-400 hover:text-surface-200 transition-colors", children: "Cancel" }),
65483
+ /* @__PURE__ */ jsxRuntimeExports.jsx(
65484
+ "button",
65485
+ {
65486
+ onClick: () => {
65487
+ if (activeEnvironmentId && varDialog.name.trim()) {
65488
+ upsertEnvVar(activeEnvironmentId, varDialog.name.trim(), varDialog.value);
65489
+ assertToast.show(`✓ Saved {{${varDialog.name.trim()}}}`, true);
65490
+ }
65491
+ setVarDialog(null);
65492
+ },
65493
+ disabled: !varDialog.name.trim(),
65494
+ className: "px-3 py-1.5 text-xs rounded bg-blue-600 hover:bg-blue-500 text-white font-semibold disabled:opacity-50 transition-colors",
65495
+ children: "Create"
65496
+ }
65497
+ )
65498
+ ] })
65499
+ ] }) })
65239
65500
  ] });
65240
65501
  }
65241
- const { electron: electron$g } = window;
65502
+ const { electron: electron$h } = window;
65242
65503
  const TARGETS = [
65243
65504
  { id: "robot_framework", label: "Robot Framework", description: "Python RequestsLibrary keywords + test suite" },
65244
65505
  { id: "playwright_ts", label: "Playwright TS", description: "TypeScript page-object API classes + spec files" },
@@ -65278,7 +65539,7 @@ function GeneratorPanel() {
65278
65539
  try {
65279
65540
  const col = collections[selectedCollectionId]?.data;
65280
65541
  const env = resolveEnvironmentById(environments, activeEnvironmentId);
65281
- const generated = await electron$g.generateCode({ collection: col, environment: env, target });
65542
+ const generated = await electron$h.generateCode({ collection: col, environment: env, target });
65282
65543
  setFiles(generated);
65283
65544
  setSelectedFile(generated[0]?.path ?? null);
65284
65545
  } catch (e) {
@@ -65290,7 +65551,7 @@ function GeneratorPanel() {
65290
65551
  async function saveZip() {
65291
65552
  if (files.length === 0) return;
65292
65553
  const col = collections[selectedCollectionId]?.data;
65293
- await electron$g.saveGeneratedFilesAsZip(files, col?.name ?? "api-tests", target);
65554
+ await electron$h.saveGeneratedFilesAsZip(files, col?.name ?? "api-tests", target);
65294
65555
  }
65295
65556
  const selectedContent = files.find((f) => f.path === selectedFile)?.content ?? "";
65296
65557
  const activeTarget = TARGETS.find((t2) => t2.id === target);
@@ -65389,6 +65650,92 @@ function GeneratorPanel() {
65389
65650
  files.length === 0 && !generating && /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "flex-1 flex items-center justify-center text-surface-400 text-xs text-center px-6", children: "Select a collection and hit Generate to preview the output code." })
65390
65651
  ] });
65391
65652
  }
65653
+ function enabledHeaders(headers) {
65654
+ return headers.filter((h) => h.enabled && h.key).map((h) => ({ name: h.key, value: h.value }));
65655
+ }
65656
+ function queryStringOf(url) {
65657
+ const q = url.indexOf("?");
65658
+ if (q === -1) return [];
65659
+ const out = [];
65660
+ for (const pair2 of url.slice(q + 1).split("&")) {
65661
+ if (!pair2) continue;
65662
+ const eq = pair2.indexOf("=");
65663
+ const name2 = eq === -1 ? pair2 : pair2.slice(0, eq);
65664
+ const value = eq === -1 ? "" : pair2.slice(eq + 1);
65665
+ try {
65666
+ out.push({ name: decodeURIComponent(name2), value: decodeURIComponent(value) });
65667
+ } catch {
65668
+ out.push({ name: name2, value });
65669
+ }
65670
+ }
65671
+ return out;
65672
+ }
65673
+ function postDataOf(body) {
65674
+ if (!body || body.mode === "none") return void 0;
65675
+ switch (body.mode) {
65676
+ case "json":
65677
+ return body.json ? { mimeType: "application/json", text: body.json } : void 0;
65678
+ case "raw":
65679
+ return body.raw ? { mimeType: body.rawContentType ?? "text/plain", text: body.raw } : void 0;
65680
+ case "graphql":
65681
+ return body.graphql ? { mimeType: "application/json", text: JSON.stringify(body.graphql) } : void 0;
65682
+ case "soap":
65683
+ return body.soap ? { mimeType: "text/xml", text: body.soap.envelope ?? "" } : void 0;
65684
+ case "form": {
65685
+ const text = (body.form ?? []).filter((p2) => p2.enabled && p2.key).map((p2) => `${encodeURIComponent(p2.key)}=${encodeURIComponent(p2.value)}`).join("&");
65686
+ return { mimeType: "application/x-www-form-urlencoded", text };
65687
+ }
65688
+ default:
65689
+ return void 0;
65690
+ }
65691
+ }
65692
+ function historyToHar(entries, creatorVersion = "1.0") {
65693
+ const harEntries = entries.map((e) => {
65694
+ const post = postDataOf(e.request.body);
65695
+ const contentType = e.response.headers["content-type"] ?? e.response.headers["Content-Type"] ?? "text/plain";
65696
+ return {
65697
+ startedDateTime: new Date(e.timestamp).toISOString(),
65698
+ time: e.response.durationMs,
65699
+ request: {
65700
+ method: e.request.method,
65701
+ url: e.resolvedUrl,
65702
+ httpVersion: "HTTP/1.1",
65703
+ cookies: [],
65704
+ headers: enabledHeaders(e.request.headers),
65705
+ queryString: queryStringOf(e.resolvedUrl),
65706
+ ...post ? { postData: post } : {},
65707
+ headersSize: -1,
65708
+ bodySize: post ? post.text.length : 0
65709
+ },
65710
+ response: {
65711
+ status: e.response.status,
65712
+ statusText: e.response.statusText,
65713
+ httpVersion: "HTTP/1.1",
65714
+ cookies: [],
65715
+ headers: Object.entries(e.response.headers).map(([name2, value]) => ({ name: name2, value })),
65716
+ content: {
65717
+ size: e.response.bodySize,
65718
+ mimeType: contentType,
65719
+ text: e.response.body
65720
+ },
65721
+ redirectURL: "",
65722
+ headersSize: -1,
65723
+ bodySize: e.response.bodySize
65724
+ },
65725
+ cache: {},
65726
+ timings: { send: 0, wait: e.response.durationMs, receive: 0 },
65727
+ ...e.environmentName ? { comment: `environment: ${e.environmentName}` } : {}
65728
+ };
65729
+ });
65730
+ return JSON.stringify({
65731
+ log: {
65732
+ version: "1.2",
65733
+ creator: { name: "API Spector", version: creatorVersion },
65734
+ entries: harEntries
65735
+ }
65736
+ }, null, 2);
65737
+ }
65738
+ const { electron: electron$g } = window;
65392
65739
  const STATUS_COLOR = {
65393
65740
  "2": "text-emerald-400",
65394
65741
  "3": "text-amber-400",
@@ -65416,6 +65763,8 @@ function HistoryPanel() {
65416
65763
  const clearHistory = useStore((s) => s.clearHistory);
65417
65764
  const activeTabId = useStore((s) => s.activeTabId);
65418
65765
  const setTabResponse = useStore((s) => s.setTabResponse);
65766
+ const setActiveRequest = useStore((s) => s.setActiveRequest);
65767
+ const collections = useStore((s) => s.collections);
65419
65768
  const [selected, setSelected] = reactExports.useState(null);
65420
65769
  const [search, setSearch] = reactExports.useState("");
65421
65770
  const filtered = search ? history2.filter(
@@ -65431,9 +65780,18 @@ function HistoryPanel() {
65431
65780
  groups.push({ label, entries: [entry] });
65432
65781
  }
65433
65782
  }
65783
+ async function downloadHar() {
65784
+ const stamp = (/* @__PURE__ */ new Date()).toISOString().slice(0, 19).replace(/[:T]/g, "-");
65785
+ await electron$g.saveResults(historyToHar(history2), `api-spector-history-${stamp}.har`);
65786
+ }
65434
65787
  function open(entry) {
65435
65788
  setSelected(entry);
65436
- if (activeTabId) {
65789
+ const stillExists = Object.values(collections).some((c) => entry.request.id in c.data.requests);
65790
+ if (stillExists) {
65791
+ setActiveRequest(entry.request.id);
65792
+ const tabId = useStore.getState().activeTabId;
65793
+ if (tabId) setTabResponse(tabId, entry.response, entry.scriptResult ?? null);
65794
+ } else if (activeTabId) {
65437
65795
  setTabResponse(activeTabId, entry.response, entry.scriptResult ?? null);
65438
65796
  }
65439
65797
  }
@@ -65448,18 +65806,29 @@ function HistoryPanel() {
65448
65806
  className: "flex-1 bg-surface-800 rounded px-2 py-1 text-xs focus:outline-none focus:ring-1 focus:ring-blue-500"
65449
65807
  }
65450
65808
  ),
65451
- history2.length > 0 && /* @__PURE__ */ jsxRuntimeExports.jsx(
65452
- "button",
65453
- {
65454
- onClick: () => {
65455
- clearHistory();
65456
- setSelected(null);
65457
- },
65458
- className: "text-xs text-surface-400 hover:text-red-400 transition-colors px-1",
65459
- title: "Clear all history",
65460
- children: "Clear"
65461
- }
65462
- )
65809
+ history2.length > 0 && /* @__PURE__ */ jsxRuntimeExports.jsxs(jsxRuntimeExports.Fragment, { children: [
65810
+ /* @__PURE__ */ jsxRuntimeExports.jsx(
65811
+ "button",
65812
+ {
65813
+ onClick: downloadHar,
65814
+ className: "text-xs text-surface-400 hover:text-surface-100 transition-colors px-1",
65815
+ title: "Download history as a HAR file",
65816
+ children: "HAR"
65817
+ }
65818
+ ),
65819
+ /* @__PURE__ */ jsxRuntimeExports.jsx(
65820
+ "button",
65821
+ {
65822
+ onClick: () => {
65823
+ clearHistory();
65824
+ setSelected(null);
65825
+ },
65826
+ className: "text-xs text-surface-400 hover:text-red-400 transition-colors px-1",
65827
+ title: "Clear all history",
65828
+ children: "Clear"
65829
+ }
65830
+ )
65831
+ ] })
65463
65832
  ] }),
65464
65833
  history2.length === 0 ? /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "flex-1 flex items-center justify-center text-xs text-surface-400 px-4 text-center", children: "No history yet. Send a request to start recording." }) : /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex-1 overflow-y-auto", children: [
65465
65834
  filtered.length === 0 && /* @__PURE__ */ jsxRuntimeExports.jsx("p", { className: "px-3 py-4 text-xs text-surface-400", children: "No matches." }),
@@ -66241,6 +66610,7 @@ function WorkspaceSettingsModal({ onClose }) {
66241
66610
  const existing = workspace?.settings ?? {};
66242
66611
  const [activeTab, setActiveTab] = reactExports.useState("general");
66243
66612
  const [defaultEnvironment, setDefaultEnvironment] = reactExports.useState(existing.defaultEnvironment ?? "");
66613
+ const [persistHistory, setPersistHistory] = reactExports.useState(existing.persistHistory ?? false);
66244
66614
  const [proxyUrl, setProxyUrl] = reactExports.useState(existing.proxy?.url ?? "");
66245
66615
  const [proxyUser, setProxyUser] = reactExports.useState(existing.proxy?.auth?.username ?? "");
66246
66616
  const [proxyPass, setProxyPass] = reactExports.useState(existing.proxy?.auth?.password ?? "");
@@ -66287,9 +66657,15 @@ function WorkspaceSettingsModal({ onClose }) {
66287
66657
  else delete settings.dashboardUrl;
66288
66658
  if (defaultEnvironment) settings.defaultEnvironment = defaultEnvironment;
66289
66659
  else delete settings.defaultEnvironment;
66660
+ if (persistHistory) settings.persistHistory = true;
66661
+ else delete settings.persistHistory;
66290
66662
  updateWorkspaceSettings(settings);
66291
66663
  const updated = useStore.getState().workspace;
66292
66664
  if (updated) await electron$b.saveWorkspace(updated);
66665
+ if (persistHistory) {
66666
+ await electron$b.saveHistory(useStore.getState().history).catch(() => {
66667
+ });
66668
+ }
66293
66669
  onClose();
66294
66670
  }
66295
66671
  function zoomStep(dir) {
@@ -66339,7 +66715,22 @@ function WorkspaceSettingsModal({ onClose }) {
66339
66715
  }
66340
66716
  )
66341
66717
  ] }),
66342
- /* @__PURE__ */ jsxRuntimeExports.jsx("p", { className: "text-surface-600 text-[11px]", children: "CLI runs without --environment use this environment, and the app selects it when no environment is active." })
66718
+ /* @__PURE__ */ jsxRuntimeExports.jsx("p", { className: "text-surface-600 text-[11px]", children: "CLI runs without --environment use this environment, and the app selects it when no environment is active." }),
66719
+ /* @__PURE__ */ jsxRuntimeExports.jsxs("label", { className: "flex items-start gap-2 mt-2 cursor-pointer", children: [
66720
+ /* @__PURE__ */ jsxRuntimeExports.jsx(
66721
+ "input",
66722
+ {
66723
+ type: "checkbox",
66724
+ checked: persistHistory,
66725
+ onChange: (e) => setPersistHistory(e.target.checked),
66726
+ className: "mt-0.5"
66727
+ }
66728
+ ),
66729
+ /* @__PURE__ */ jsxRuntimeExports.jsxs("span", { className: "flex flex-col gap-0.5", children: [
66730
+ /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "text-surface-200", children: "Persist request history" }),
66731
+ /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "text-surface-600 text-[11px]", children: "Save history to history.json in the workspace folder so it survives restarts. The file is gitignored. Off by default; history stays in memory otherwise." })
66732
+ ] })
66733
+ ] })
66343
66734
  ] }),
66344
66735
  activeTab === "appearance" && /* @__PURE__ */ jsxRuntimeExports.jsxs(jsxRuntimeExports.Fragment, { children: [
66345
66736
  /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex flex-col gap-1", children: [
@@ -71568,7 +71959,7 @@ function App() {
71568
71959
  /* @__PURE__ */ jsxRuntimeExports.jsx("span", { style: { color: "#6aa3c8" }, children: "Spector" }),
71569
71960
  /* @__PURE__ */ jsxRuntimeExports.jsxs("span", { className: "ml-2 text-[10px] font-normal opacity-50", children: [
71570
71961
  "v",
71571
- "0.3.6"
71962
+ "0.3.7"
71572
71963
  ] }),
71573
71964
  /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "mx-2 opacity-30", children: "·" }),
71574
71965
  /* @__PURE__ */ jsxRuntimeExports.jsx(