@testsmith/api-spector 0.4.9 → 0.5.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -13403,6 +13403,38 @@ const createWsSlice = (set2) => ({
13403
13403
  if (s.wsConnections[requestId]) s.wsConnections[requestId].messages = [];
13404
13404
  })
13405
13405
  });
13406
+ const GRPC_MESSAGE_CAP = 1e3;
13407
+ const blank = () => ({ status: "idle", messages: [], services: [] });
13408
+ const createGrpcSlice = (set2) => ({
13409
+ grpcCalls: {},
13410
+ setGrpcStatus: (requestId, status, extra) => set2((s) => {
13411
+ const c = s.grpcCalls[requestId] ??= blank();
13412
+ c.status = status;
13413
+ c.code = extra?.code;
13414
+ c.codeName = extra?.codeName;
13415
+ c.error = extra?.error;
13416
+ }),
13417
+ addGrpcMessage: (requestId, message) => set2((s) => {
13418
+ const c = s.grpcCalls[requestId] ??= blank();
13419
+ c.messages.push(message);
13420
+ if (c.messages.length > GRPC_MESSAGE_CAP) {
13421
+ c.messages.splice(0, c.messages.length - GRPC_MESSAGE_CAP);
13422
+ }
13423
+ }),
13424
+ clearGrpcMessages: (requestId) => set2((s) => {
13425
+ if (s.grpcCalls[requestId]) s.grpcCalls[requestId].messages = [];
13426
+ }),
13427
+ setGrpcServices: (requestId, services) => set2((s) => {
13428
+ const c = s.grpcCalls[requestId] ??= blank();
13429
+ c.services = services;
13430
+ c.protoError = void 0;
13431
+ }),
13432
+ setGrpcProtoError: (requestId, error2) => set2((s) => {
13433
+ const c = s.grpcCalls[requestId] ??= blank();
13434
+ c.protoError = error2;
13435
+ if (error2) c.services = [];
13436
+ })
13437
+ });
13406
13438
  const LIVE_EVENT_CAP = 5e3;
13407
13439
  const createStreamSlice = (set2) => ({
13408
13440
  liveStream: null,
@@ -13706,6 +13738,19 @@ const createCollectionsSlice = (set2, get2) => ({
13706
13738
  s.activeCollectionId = col.id;
13707
13739
  if (s.workspace) s.workspace.collections.push(relPath);
13708
13740
  }),
13741
+ addCollectionObject: (data) => {
13742
+ let id2 = data.id;
13743
+ set2((s) => {
13744
+ const existingNames = Object.values(s.collections).map((c) => c.data.name);
13745
+ data.name = uniqueName(data.name, existingNames);
13746
+ const relPath = colRelPath(data.name, data.id);
13747
+ s.collections[data.id] = { relPath, data, dirty: true };
13748
+ s.activeCollectionId = data.id;
13749
+ if (s.workspace) s.workspace.collections.push(relPath);
13750
+ id2 = data.id;
13751
+ });
13752
+ return id2;
13753
+ },
13709
13754
  renameCollection: (id2, name2) => set2((s) => {
13710
13755
  if (!s.collections[id2]) return;
13711
13756
  const oldRelPath = s.collections[id2].relPath;
@@ -14278,6 +14323,8 @@ const createUiSlice = (set2) => ({
14278
14323
  quickInsertsOpen: true,
14279
14324
  sendSignal: 0,
14280
14325
  collectionPanelOpen: false,
14326
+ coverageOpen: false,
14327
+ compareOpen: false,
14281
14328
  setShowGeneratorPanel: (v) => set2((s) => {
14282
14329
  s.showGeneratorPanel = v;
14283
14330
  }),
@@ -14331,12 +14378,27 @@ const createUiSlice = (set2) => ({
14331
14378
  }),
14332
14379
  setCollectionPanelOpen: (open) => set2((s) => {
14333
14380
  s.collectionPanelOpen = open;
14381
+ }),
14382
+ setCoverageOpen: (open) => set2((s) => {
14383
+ s.coverageOpen = open;
14384
+ }),
14385
+ setCompareOpen: (open) => set2((s) => {
14386
+ s.compareOpen = open;
14387
+ }),
14388
+ // The OpenAPI spec used for coverage travels with the workspace (persisted on
14389
+ // save), mirroring how theme/zoom are stored.
14390
+ setCoverageSpec: (spec) => set2((s) => {
14391
+ if (s.workspace) {
14392
+ if (!s.workspace.settings) s.workspace.settings = {};
14393
+ s.workspace.settings.coverageSpec = spec;
14394
+ }
14334
14395
  })
14335
14396
  });
14336
14397
  const useStore = create()(
14337
14398
  immer((set2, get2, api) => ({
14338
14399
  // ── Slice composition ─────────────────────────────────────────────────────
14339
14400
  ...createWsSlice(set2),
14401
+ ...createGrpcSlice(set2),
14340
14402
  ...createStreamSlice(set2),
14341
14403
  ...createHistorySlice(set2, get2),
14342
14404
  ...createRunnerSlice(set2),
@@ -14430,7 +14492,7 @@ const useStore = create()(
14430
14492
  })
14431
14493
  }))
14432
14494
  );
14433
- const { electron: electron$v } = window;
14495
+ const { electron: electron$y } = window;
14434
14496
  function useAutoSave() {
14435
14497
  const collections = useStore((s) => s.collections);
14436
14498
  useStore((s) => s.environments);
@@ -14446,7 +14508,7 @@ function useAutoSave() {
14446
14508
  for (const { relPath, data, dirty } of dirtyCollections) {
14447
14509
  if (!dirty) continue;
14448
14510
  try {
14449
- await electron$v.saveCollection(relPath, data);
14511
+ await electron$y.saveCollection(relPath, data);
14450
14512
  markCollectionClean(data.id);
14451
14513
  } catch (e) {
14452
14514
  console.error("Auto-save failed for", relPath, e);
@@ -14462,7 +14524,7 @@ function useAutoSave() {
14462
14524
  if (wsTimerRef.current) clearTimeout(wsTimerRef.current);
14463
14525
  wsTimerRef.current = setTimeout(async () => {
14464
14526
  try {
14465
- await electron$v.saveWorkspace(workspace);
14527
+ await electron$y.saveWorkspace(workspace);
14466
14528
  } catch {
14467
14529
  }
14468
14530
  }, 300);
@@ -14471,7 +14533,7 @@ function useAutoSave() {
14471
14533
  };
14472
14534
  }, [workspace]);
14473
14535
  }
14474
- const { electron: electron$u } = window;
14536
+ const { electron: electron$x } = window;
14475
14537
  function useWorkspaceLoader() {
14476
14538
  const loadCollection = useStore((s) => s.loadCollection);
14477
14539
  const loadEnvironment = useStore((s) => s.loadEnvironment);
@@ -14495,7 +14557,7 @@ function useWorkspaceLoader() {
14495
14557
  if (typeof ws2.settings?.zoom === "number") setZoom(ws2.settings.zoom);
14496
14558
  if (ws2.settings?.persistHistory) {
14497
14559
  try {
14498
- const entries = await electron$u.loadHistory();
14560
+ const entries = await electron$x.loadHistory();
14499
14561
  useStore.getState().setHistory(entries);
14500
14562
  } catch {
14501
14563
  }
@@ -14504,14 +14566,14 @@ function useWorkspaceLoader() {
14504
14566
  }
14505
14567
  for (const colPath of ws2.collections ?? []) {
14506
14568
  try {
14507
- const col = await electron$u.loadCollection(colPath);
14569
+ const col = await electron$x.loadCollection(colPath);
14508
14570
  loadCollection(colPath, col);
14509
14571
  } catch {
14510
14572
  }
14511
14573
  }
14512
14574
  for (const envPath of ws2.environments ?? []) {
14513
14575
  try {
14514
- const env = await electron$u.loadEnvironment(envPath);
14576
+ const env = await electron$x.loadEnvironment(envPath);
14515
14577
  loadEnvironment(envPath, env);
14516
14578
  } catch {
14517
14579
  }
@@ -14530,19 +14592,19 @@ function useWorkspaceLoader() {
14530
14592
  }
14531
14593
  for (const relPath of ws2.mocks ?? []) {
14532
14594
  try {
14533
- const mockData = await electron$u.loadMock(relPath);
14595
+ const mockData = await electron$x.loadMock(relPath);
14534
14596
  loadMock(relPath, mockData);
14535
14597
  } catch {
14536
14598
  }
14537
14599
  }
14538
14600
  try {
14539
- const snapshots = await electron$u.listContractSnapshots(ws2.contracts ?? []);
14601
+ const snapshots = await electron$x.listContractSnapshots(ws2.contracts ?? []);
14540
14602
  for (const { relPath, snapshot } of snapshots) loadContractSnapshot(relPath, snapshot);
14541
14603
  } catch {
14542
14604
  }
14543
14605
  if ((ws2.collections ?? []).length > 0) {
14544
14606
  try {
14545
- const firstCol = await electron$u.loadCollection(ws2.collections[0]);
14607
+ const firstCol = await electron$x.loadCollection(ws2.collections[0]);
14546
14608
  setActiveCollection(firstCol.id);
14547
14609
  } catch {
14548
14610
  }
@@ -36428,7 +36490,7 @@ function CollectionSettingsModal({ collection, onClose }) {
36428
36490
  }
36429
36491
  );
36430
36492
  }
36431
- const { electron: electron$t } = window;
36493
+ const { electron: electron$w } = window;
36432
36494
  function normalisePath(url) {
36433
36495
  let path = url.replace(/^\{\{[^}]+\}\}/, "").replace(/^https?:\/\/[^/]+/, "");
36434
36496
  if (!path.startsWith("/")) path = "/" + path;
@@ -36515,7 +36577,7 @@ function SchemaSyncModal({
36515
36577
  setLoading(true);
36516
36578
  setError(null);
36517
36579
  try {
36518
- const entries = await electron$t.extractOpenApiSchemas();
36580
+ const entries = await electron$w.extractOpenApiSchemas();
36519
36581
  if (!entries) {
36520
36582
  setLoading(false);
36521
36583
  return;
@@ -36534,7 +36596,7 @@ function SchemaSyncModal({
36534
36596
  setLoading(true);
36535
36597
  setError(null);
36536
36598
  try {
36537
- const entries = await electron$t.extractOpenApiSchemasFromUrl(trimmed);
36599
+ const entries = await electron$w.extractOpenApiSchemasFromUrl(trimmed);
36538
36600
  setSpecEntries(entries);
36539
36601
  autoSelectChanged(entries);
36540
36602
  } catch (err) {
@@ -36567,7 +36629,7 @@ function SchemaSyncModal({
36567
36629
  }
36568
36630
  const entry = useStore.getState().collections[collectionId];
36569
36631
  if (entry) {
36570
- await electron$t.saveCollection(entry.relPath, entry.data);
36632
+ await electron$w.saveCollection(entry.relPath, entry.data);
36571
36633
  markCollectionClean(collectionId);
36572
36634
  }
36573
36635
  onClose();
@@ -36852,14 +36914,14 @@ function collectTagged(folder, requests, collectionVars, filterTags, parentPath
36852
36914
  }
36853
36915
  function collectAllTags(folder, requests) {
36854
36916
  const tags2 = /* @__PURE__ */ new Set();
36855
- function walk(f) {
36917
+ function walk2(f) {
36856
36918
  (f.tags ?? []).forEach((t2) => tags2.add(t2));
36857
36919
  for (const reqId of f.requestIds) {
36858
36920
  (requests[reqId]?.meta?.tags ?? []).forEach((t2) => tags2.add(t2));
36859
36921
  }
36860
- f.folders.forEach(walk);
36922
+ f.folders.forEach(walk2);
36861
36923
  }
36862
- walk(folder);
36924
+ walk2(folder);
36863
36925
  return Array.from(tags2).sort();
36864
36926
  }
36865
36927
  function folderPathTo(root2, requestId) {
@@ -36982,7 +37044,7 @@ function hasContract(r) {
36982
37044
  const c = r.contract;
36983
37045
  return !!c && (c.statusCode != null || !!c.bodySchema || !!c.bodyMatcher || (c.headers?.length ?? 0) > 0 || (c.providerStates?.length ?? 0) > 0);
36984
37046
  }
36985
- const { electron: electron$s } = window;
37047
+ const { electron: electron$v } = window;
36986
37048
  function cloudEnabled() {
36987
37049
  return Boolean(useStore.getState().workspace?.settings?.cloud?.enabled);
36988
37050
  }
@@ -36991,7 +37053,7 @@ function assertEnabled() {
36991
37053
  }
36992
37054
  async function pushContractToCloud(requests, opts) {
36993
37055
  assertEnabled();
36994
- return electron$s.cloudPushPact({
37056
+ return electron$v.cloudPushPact({
36995
37057
  consumer: opts.consumer,
36996
37058
  provider: opts.provider,
36997
37059
  consumerVersion: opts.version,
@@ -37000,15 +37062,15 @@ async function pushContractToCloud(requests, opts) {
37000
37062
  }
37001
37063
  async function pushProviderSpecToCloud(opts) {
37002
37064
  assertEnabled();
37003
- return electron$s.cloudPushSpec(opts);
37065
+ return electron$v.cloudPushSpec(opts);
37004
37066
  }
37005
37067
  function openCloudMatrix() {
37006
- void electron$s.cloudOpenMatrix();
37068
+ void electron$v.cloudOpenMatrix();
37007
37069
  }
37008
37070
  async function getCloudMockRoutes(name2) {
37009
37071
  if (!cloudEnabled()) return null;
37010
37072
  try {
37011
- const res = await electron$s.cloudGetMock(name2);
37073
+ const res = await electron$v.cloudGetMock(name2);
37012
37074
  return res.exists ? res.routes ?? [] : null;
37013
37075
  } catch {
37014
37076
  return null;
@@ -37017,7 +37079,7 @@ async function getCloudMockRoutes(name2) {
37017
37079
  async function pushMockToCloud(server, routeIds) {
37018
37080
  assertEnabled();
37019
37081
  const routes = routeIds ? (server.routes ?? []).filter((r) => routeIds.includes(r.id)) : server.routes ?? [];
37020
- return electron$s.cloudPushMock({ ...server, routes });
37082
+ return electron$v.cloudPushMock({ ...server, routes });
37021
37083
  }
37022
37084
  async function pushRequestAsMonitor(request, opts) {
37023
37085
  assertEnabled();
@@ -37038,7 +37100,7 @@ async function pushRequestAsMonitor(request, opts) {
37038
37100
  ...request.headers
37039
37101
  ];
37040
37102
  const mergedRequest = { ...request, auth: mergedAuth, headers: mergedHeaders };
37041
- return electron$s.cloudPushMonitor(
37103
+ return electron$v.cloudPushMonitor(
37042
37104
  {
37043
37105
  request: mergedRequest,
37044
37106
  setup,
@@ -37796,16 +37858,16 @@ function methodColor$2(method) {
37796
37858
  }
37797
37859
  function collectMatches(col, q) {
37798
37860
  const out = [];
37799
- const walk = (folder, path) => {
37861
+ const walk2 = (folder, path) => {
37800
37862
  for (const reqId of folder.requestIds) {
37801
37863
  const req = col.requests[reqId];
37802
37864
  if (!req) continue;
37803
37865
  const hay = [req.name, req.url, req.method, ...req.meta?.tags ?? []].join(" ").toLowerCase();
37804
37866
  if (hay.includes(q)) out.push({ collectionId: col.id, collectionName: col.name, req, path });
37805
37867
  }
37806
- for (const sub of folder.folders) walk(sub, [...path, sub.name]);
37868
+ for (const sub of folder.folders) walk2(sub, [...path, sub.name]);
37807
37869
  };
37808
- walk(col.rootFolder, []);
37870
+ walk2(col.rootFolder, []);
37809
37871
  return out;
37810
37872
  }
37811
37873
  function CollectionTree() {
@@ -56448,7 +56510,7 @@ function withContentType(headers, value) {
56448
56510
  if (idx === -1) return [...headers, next];
56449
56511
  return headers.map((h, i) => i === idx ? { ...h, value, enabled: true } : h);
56450
56512
  }
56451
- const { electron: electron$r } = window;
56513
+ const { electron: electron$u } = window;
56452
56514
  function ParamTree({ params, depth = 0 }) {
56453
56515
  if (params.length === 0) {
56454
56516
  return /* @__PURE__ */ jsxRuntimeExports.jsx("p", { className: "text-[10px] text-surface-600 italic", children: "No parameters declared in WSDL." });
@@ -56462,7 +56524,7 @@ function ParamTree({ params, depth = 0 }) {
56462
56524
  }
56463
56525
  function SoapEditor({ request, onChange }) {
56464
56526
  const soap = request.body.soap ?? { wsdlUrl: "", envelope: "" };
56465
- const [operations, setOperations] = reactExports.useState([]);
56527
+ const [operations2, setOperations] = reactExports.useState([]);
56466
56528
  const [endpoints, setEndpoints] = reactExports.useState([]);
56467
56529
  const [targetNs, setTargetNs] = reactExports.useState("");
56468
56530
  const [fetchError, setFetchError] = reactExports.useState(null);
@@ -56474,7 +56536,7 @@ function SoapEditor({ request, onChange }) {
56474
56536
  let cancelled = false;
56475
56537
  (async () => {
56476
56538
  try {
56477
- const result = await electron$r.wsdlFetch(url);
56539
+ const result = await electron$u.wsdlFetch(url);
56478
56540
  if (cancelled) return;
56479
56541
  setOperations(result.operations);
56480
56542
  setEndpoints(result.endpoints);
@@ -56511,7 +56573,7 @@ function SoapEditor({ request, onChange }) {
56511
56573
  setFetching(true);
56512
56574
  setFetchError(null);
56513
56575
  try {
56514
- const result = await electron$r.wsdlFetch(soap.wsdlUrl.trim());
56576
+ const result = await electron$u.wsdlFetch(soap.wsdlUrl.trim());
56515
56577
  setOperations(result.operations);
56516
56578
  setEndpoints(result.endpoints);
56517
56579
  setTargetNs(result.targetNamespace);
@@ -56527,11 +56589,11 @@ function SoapEditor({ request, onChange }) {
56527
56589
  function selectOperation(op) {
56528
56590
  applyOperation(op);
56529
56591
  }
56530
- const selected = operations.find((o) => o.name === soap.operationName) ?? operations[0];
56592
+ const selected = operations2.find((o) => o.name === soap.operationName) ?? operations2[0];
56531
56593
  const primaryEndpoint = endpoints[0]?.address;
56532
- const versions = Array.from(new Set(operations.map((o) => o.soapVersion))).sort();
56594
+ const versions = Array.from(new Set(operations2.map((o) => o.soapVersion))).sort();
56533
56595
  const hasSavedSoap = Boolean(soap.envelope?.trim() || soap.operationName || soap.wsdlUrl?.trim());
56534
- const showFullUi = operations.length > 0;
56596
+ const showFullUi = operations2.length > 0;
56535
56597
  const showFallback = !showFullUi && hasSavedSoap;
56536
56598
  const showEmpty = !showFullUi && !showFallback;
56537
56599
  return /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex flex-col gap-3 h-full min-h-0", children: [
@@ -56555,12 +56617,12 @@ function SoapEditor({ request, onChange }) {
56555
56617
  onClick: fetchWsdl,
56556
56618
  disabled: fetching || !soap.wsdlUrl.trim(),
56557
56619
  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 whitespace-nowrap",
56558
- children: fetching ? "Fetching…" : operations.length > 0 ? "Refresh" : "Fetch WSDL"
56620
+ children: fetching ? "Fetching…" : operations2.length > 0 ? "Refresh" : "Fetch WSDL"
56559
56621
  }
56560
56622
  )
56561
56623
  ] }),
56562
56624
  fetchError && /* @__PURE__ */ jsxRuntimeExports.jsx("p", { className: "text-xs text-red-400 bg-red-950/30 border border-red-900 rounded px-2.5 py-1.5", children: fetchError }),
56563
- operations.length > 0 && /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "bg-surface-900/40 border border-surface-800 rounded-md px-3 py-2 text-[11px] flex flex-col gap-1", children: [
56625
+ operations2.length > 0 && /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "bg-surface-900/40 border border-surface-800 rounded-md px-3 py-2 text-[11px] flex flex-col gap-1", children: [
56564
56626
  /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-center gap-2 text-surface-500", children: [
56565
56627
  /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "uppercase tracking-wider text-[9px] font-semibold text-surface-600 w-20 shrink-0", children: "Endpoint" }),
56566
56628
  /* @__PURE__ */ jsxRuntimeExports.jsx("code", { className: "text-surface-200 font-mono truncate", children: primaryEndpoint ?? "(not declared)" })
@@ -56572,7 +56634,7 @@ function SoapEditor({ request, onChange }) {
56572
56634
  /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-center gap-2 text-surface-500", children: [
56573
56635
  /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "uppercase tracking-wider text-[9px] font-semibold text-surface-600 w-20 shrink-0", children: "Operations" }),
56574
56636
  /* @__PURE__ */ jsxRuntimeExports.jsxs("span", { className: "text-surface-300", children: [
56575
- operations.length,
56637
+ operations2.length,
56576
56638
  /* @__PURE__ */ jsxRuntimeExports.jsxs("span", { className: "text-surface-600", children: [
56577
56639
  " · SOAP ",
56578
56640
  versions.join(", ")
@@ -56583,7 +56645,7 @@ function SoapEditor({ request, onChange }) {
56583
56645
  showFullUi && /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex gap-3 min-h-0 flex-1", children: [
56584
56646
  /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "w-44 shrink-0 flex flex-col bg-surface-900/40 border border-surface-800 rounded-md overflow-hidden", children: [
56585
56647
  /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "px-2.5 py-1.5 text-[9px] uppercase tracking-wider font-semibold text-surface-600 border-b border-surface-800", children: "Operations" }),
56586
- /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "overflow-y-auto flex-1", children: operations.map((op) => {
56648
+ /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "overflow-y-auto flex-1", children: operations2.map((op) => {
56587
56649
  const active = selected && op.name === selected.name && op.soapVersion === selected.soapVersion;
56588
56650
  return /* @__PURE__ */ jsxRuntimeExports.jsxs(
56589
56651
  "button",
@@ -56781,7 +56843,7 @@ function BodyTab({ request, onChange }) {
56781
56843
  mode === "soap" && /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "flex-1 min-h-0", children: /* @__PURE__ */ jsxRuntimeExports.jsx(SoapEditor, { request, onChange }) })
56782
56844
  ] });
56783
56845
  }
56784
- const { electron: electron$q } = window;
56846
+ const { electron: electron$t } = window;
56785
56847
  const AUTH_TYPES = ["none", "bearer", "basic", "digest", "ntlm", "apikey", "oauth2"];
56786
56848
  function AuthTab({ request, onChange }) {
56787
56849
  const auth = request.auth;
@@ -56795,7 +56857,7 @@ function AuthTab({ request, onChange }) {
56795
56857
  }
56796
56858
  async function saveSecret(ref2) {
56797
56859
  if (!secretValue || !ref2) return;
56798
- await electron$q.setSecret(ref2, secretValue);
56860
+ await electron$t.setSecret(ref2, secretValue);
56799
56861
  setSaved(true);
56800
56862
  setSecretValue("");
56801
56863
  setTimeout(() => setSaved(false), 2e3);
@@ -56807,7 +56869,7 @@ function AuthTab({ request, onChange }) {
56807
56869
  setOauth2Error("");
56808
56870
  try {
56809
56871
  const vars = {};
56810
- const result = await electron$q.oauth2StartFlow(oauth2Auth, vars);
56872
+ const result = await electron$t.oauth2StartFlow(oauth2Auth, vars);
56811
56873
  setAuth({
56812
56874
  oauth2CachedToken: result.accessToken,
56813
56875
  oauth2TokenExpiry: result.expiresAt
@@ -56825,7 +56887,7 @@ function AuthTab({ request, onChange }) {
56825
56887
  setOauth2Status("fetching");
56826
56888
  setOauth2Error("");
56827
56889
  try {
56828
- const result = await electron$q.oauth2RefreshToken(oauth2Auth, {}, oauth2RefreshToken);
56890
+ const result = await electron$t.oauth2RefreshToken(oauth2Auth, {}, oauth2RefreshToken);
56829
56891
  setAuth({
56830
56892
  oauth2CachedToken: result.accessToken,
56831
56893
  oauth2TokenExpiry: result.expiresAt
@@ -60546,8 +60608,8 @@ function getData($data, { dataLevel, dataNames, dataPathArr }) {
60546
60608
  return data;
60547
60609
  }
60548
60610
  let expr = data;
60549
- const segments = jsonPointer.split("/");
60550
- for (const segment of segments) {
60611
+ const segments2 = jsonPointer.split("/");
60612
+ for (const segment of segments2) {
60551
60613
  if (segment) {
60552
60614
  data = (0, codegen_1$n._)`${data}${(0, codegen_1$n.getProperty)((0, util_1$m.unescapeJsonPointer)(segment))}`;
60553
60615
  expr = (0, codegen_1$n._)`${expr} && ${data}`;
@@ -60695,7 +60757,7 @@ function compileSchema(sch) {
60695
60757
  }
60696
60758
  }
60697
60759
  compile.compileSchema = compileSchema;
60698
- function resolveRef(root2, baseId, ref2) {
60760
+ function resolveRef$3(root2, baseId, ref2) {
60699
60761
  var _a2;
60700
60762
  ref2 = (0, resolve_1.resolveUrl)(this.opts.uriResolver, baseId, ref2);
60701
60763
  const schOrFunc = root2.refs[ref2];
@@ -60712,7 +60774,7 @@ function resolveRef(root2, baseId, ref2) {
60712
60774
  return;
60713
60775
  return root2.refs[ref2] = inlineOrCompile.call(this, _sch);
60714
60776
  }
60715
- compile.resolveRef = resolveRef;
60777
+ compile.resolveRef = resolveRef$3;
60716
60778
  function inlineOrCompile(sch) {
60717
60779
  if ((0, resolve_1.inlineRef)(sch.schema, this.opts.inlineRefs))
60718
60780
  return sch.schema;
@@ -61909,9 +61971,9 @@ uri$1.default = uri;
61909
61971
  const rules2 = this.RULES.all;
61910
61972
  metaSchema = JSON.parse(JSON.stringify(metaSchema));
61911
61973
  for (const jsonPointer of keywordsJsonPointers) {
61912
- const segments = jsonPointer.split("/").slice(1);
61974
+ const segments2 = jsonPointer.split("/").slice(1);
61913
61975
  let keywords2 = metaSchema;
61914
- for (const seg of segments)
61976
+ for (const seg of segments2)
61915
61977
  keywords2 = keywords2[seg];
61916
61978
  for (const key in rules2) {
61917
61979
  const rule = rules2[key];
@@ -64148,7 +64210,7 @@ function SchemaTab({ request, onChange }) {
64148
64210
  ] }) })
64149
64211
  ] });
64150
64212
  }
64151
- const { electron: electron$p } = window;
64213
+ const { electron: electron$s } = window;
64152
64214
  const EMPTY = { statusCode: 200, headers: [], bodySchema: "" };
64153
64215
  function ContractTab({ request, onChange }) {
64154
64216
  const activeTab = useStore((s) => s.tabs.find((t2) => t2.id === s.activeTabId));
@@ -64162,7 +64224,7 @@ function ContractTab({ request, onChange }) {
64162
64224
  if (!lastResponse?.body) return;
64163
64225
  setInferring(true);
64164
64226
  try {
64165
- const schema = await electron$p.inferContractSchema(lastResponse.body);
64227
+ const schema = await electron$s.inferContractSchema(lastResponse.body);
64166
64228
  if (schema) update({ bodySchema: schema });
64167
64229
  } finally {
64168
64230
  setInferring(false);
@@ -64360,8 +64422,8 @@ function StreamTab({ request, onChange }) {
64360
64422
  ] })
64361
64423
  ] });
64362
64424
  }
64363
- const { electron: electron$o } = window;
64364
- function formatTime$1(ts) {
64425
+ const { electron: electron$r } = window;
64426
+ function formatTime$2(ts) {
64365
64427
  const d = new Date(ts);
64366
64428
  const hh = String(d.getHours()).padStart(2, "0");
64367
64429
  const mm = String(d.getMinutes()).padStart(2, "0");
@@ -64380,14 +64442,14 @@ function WebSocketPanel({ request }) {
64380
64442
  const [sendText, setSendText] = reactExports.useState("");
64381
64443
  const logEndRef = reactExports.useRef(null);
64382
64444
  reactExports.useEffect(() => {
64383
- electron$o.onWsMessage(({ requestId, message }) => {
64445
+ electron$r.onWsMessage(({ requestId, message }) => {
64384
64446
  addWsMessage(requestId, message);
64385
64447
  });
64386
- electron$o.onWsStatus(({ requestId, status, error: error2 }) => {
64448
+ electron$r.onWsStatus(({ requestId, status, error: error2 }) => {
64387
64449
  setWsStatus(requestId, status, error2);
64388
64450
  });
64389
64451
  return () => {
64390
- electron$o.offWsEvents();
64452
+ electron$r.offWsEvents();
64391
64453
  };
64392
64454
  }, [addWsMessage, setWsStatus]);
64393
64455
  reactExports.useEffect(() => {
@@ -64400,19 +64462,19 @@ function WebSocketPanel({ request }) {
64400
64462
  if (h.enabled && h.key) headers[h.key] = h.value;
64401
64463
  }
64402
64464
  try {
64403
- await electron$o.wsConnect(request.id, request.url, headers);
64465
+ await electron$r.wsConnect(request.id, request.url, headers);
64404
64466
  } catch (err) {
64405
64467
  setWsStatus(request.id, "error", err instanceof Error ? err.message : String(err));
64406
64468
  }
64407
64469
  }
64408
64470
  async function disconnect() {
64409
- await electron$o.wsDisconnect(request.id);
64471
+ await electron$r.wsDisconnect(request.id);
64410
64472
  }
64411
64473
  async function sendMessage() {
64412
64474
  const text = sendText.trim();
64413
64475
  if (!text || !isConnected) return;
64414
64476
  try {
64415
- await electron$o.wsSend(request.id, text);
64477
+ await electron$r.wsSend(request.id, text);
64416
64478
  const msg = {
64417
64479
  id: crypto.randomUUID(),
64418
64480
  direction: "sent",
@@ -64475,7 +64537,7 @@ function WebSocketPanel({ request }) {
64475
64537
  children: [
64476
64538
  /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: `flex-shrink-0 font-mono font-bold ${msg.direction === "sent" ? "text-blue-400" : "text-emerald-400"}`, children: msg.direction === "sent" ? "→" : "←" }),
64477
64539
  /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "flex-1 font-mono text-surface-300 whitespace-pre-wrap break-all", children: msg.data }),
64478
- /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "flex-shrink-0 text-surface-400 font-mono text-[10px] mt-px", children: formatTime$1(msg.timestamp) })
64540
+ /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "flex-shrink-0 text-surface-400 font-mono text-[10px] mt-px", children: formatTime$2(msg.timestamp) })
64479
64541
  ]
64480
64542
  },
64481
64543
  msg.id
@@ -64515,6 +64577,209 @@ function WebSocketPanel({ request }) {
64515
64577
  ] })
64516
64578
  ] });
64517
64579
  }
64580
+ const { electron: electron$q } = window;
64581
+ function formatTime$1(ts) {
64582
+ const d = new Date(ts);
64583
+ const p2 = (n, w = 2) => String(n).padStart(w, "0");
64584
+ return `${p2(d.getHours())}:${p2(d.getMinutes())}:${p2(d.getSeconds())}.${p2(d.getMilliseconds(), 3)}`;
64585
+ }
64586
+ const DEFAULT_GRPC = { message: "{}", metadata: [], plaintext: false };
64587
+ function metaToText(meta2) {
64588
+ return (meta2 ?? []).map((m) => `${m.key}: ${m.value}`).join("\n");
64589
+ }
64590
+ function textToMeta(text) {
64591
+ return text.split("\n").map((line) => {
64592
+ const i = line.indexOf(":");
64593
+ if (i === -1) return null;
64594
+ const key = line.slice(0, i).trim();
64595
+ if (!key) return null;
64596
+ return { key, value: line.slice(i + 1).trim(), enabled: true };
64597
+ }).filter((x) => x !== null);
64598
+ }
64599
+ function GrpcPanel({ request, onChange }) {
64600
+ const grpc = { ...DEFAULT_GRPC, ...request.body.grpc ?? {} };
64601
+ const grpcCalls = useStore((s) => s.grpcCalls);
64602
+ const setGrpcStatus = useStore((s) => s.setGrpcStatus);
64603
+ const addGrpcMessage = useStore((s) => s.addGrpcMessage);
64604
+ const clearGrpcMessages = useStore((s) => s.clearGrpcMessages);
64605
+ const setGrpcServices = useStore((s) => s.setGrpcServices);
64606
+ const setGrpcProtoError = useStore((s) => s.setGrpcProtoError);
64607
+ const call = grpcCalls[request.id] ?? { status: "idle", messages: [], services: [] };
64608
+ const isRunning = call.status === "running";
64609
+ const [loadingProto, setLoadingProto] = reactExports.useState(false);
64610
+ const logEndRef = reactExports.useRef(null);
64611
+ function patchGrpc(changes) {
64612
+ onChange({ body: { ...request.body, mode: "grpc", grpc: { ...grpc, ...changes } } });
64613
+ }
64614
+ reactExports.useEffect(() => {
64615
+ electron$q.onGrpcMessage(({ requestId, message }) => {
64616
+ addGrpcMessage(requestId, message);
64617
+ });
64618
+ electron$q.onGrpcStatus(({ requestId, status, code: code2, codeName, error: error2 }) => {
64619
+ setGrpcStatus(requestId, status, { code: code2, codeName, error: error2 });
64620
+ });
64621
+ return () => electron$q.offGrpcEvents();
64622
+ }, [addGrpcMessage, setGrpcStatus]);
64623
+ reactExports.useEffect(() => {
64624
+ logEndRef.current?.scrollIntoView({ behavior: "smooth" });
64625
+ }, [call.messages.length]);
64626
+ const selectedService = reactExports.useMemo(
64627
+ () => call.services.find((s) => s.name === grpc.serviceName),
64628
+ [call.services, grpc.serviceName]
64629
+ );
64630
+ async function loadProto() {
64631
+ if (!grpc.protoSource?.trim() && !grpc.protoPath?.trim()) return;
64632
+ setLoadingProto(true);
64633
+ try {
64634
+ const result = await electron$q.grpcLoadProto({ protoSource: grpc.protoSource, protoPath: grpc.protoPath });
64635
+ const services = result.services;
64636
+ setGrpcServices(request.id, services);
64637
+ if (services.length && !services.some((s) => s.name === grpc.serviceName)) {
64638
+ patchGrpc({ serviceName: services[0].name, methodName: services[0].methods[0]?.name });
64639
+ }
64640
+ } catch (err) {
64641
+ setGrpcProtoError(request.id, err instanceof Error ? err.message : String(err));
64642
+ } finally {
64643
+ setLoadingProto(false);
64644
+ }
64645
+ }
64646
+ async function invoke() {
64647
+ if (!request.url || !grpc.serviceName || !grpc.methodName) return;
64648
+ clearGrpcMessages(request.id);
64649
+ addGrpcMessage(request.id, { id: crypto.randomUUID(), direction: "sent", data: grpc.message || "{}", timestamp: Date.now() });
64650
+ const metadata2 = {};
64651
+ for (const m of grpc.metadata ?? []) if (m.enabled && m.key) metadata2[m.key] = m.value;
64652
+ try {
64653
+ await electron$q.grpcInvoke(request.id, {
64654
+ target: request.url,
64655
+ serviceName: grpc.serviceName,
64656
+ methodName: grpc.methodName,
64657
+ message: grpc.message || "{}",
64658
+ metadata: metadata2,
64659
+ plaintext: grpc.plaintext,
64660
+ protoSource: grpc.protoSource,
64661
+ protoPath: grpc.protoPath
64662
+ });
64663
+ } catch (err) {
64664
+ setGrpcStatus(request.id, "error", { error: err instanceof Error ? err.message : String(err) });
64665
+ }
64666
+ }
64667
+ function cancel() {
64668
+ electron$q.grpcCancel(request.id);
64669
+ }
64670
+ const statusColors = {
64671
+ running: "bg-amber-400",
64672
+ completed: "bg-emerald-500",
64673
+ error: "bg-red-500",
64674
+ idle: "bg-surface-600"
64675
+ };
64676
+ const inputCls = "bg-surface-800 border border-surface-700 rounded px-2 py-1.5 text-xs focus:outline-none focus:border-blue-500";
64677
+ return /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex flex-col h-full min-h-0", children: [
64678
+ /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "px-4 pt-2 pb-1 flex-shrink-0 flex items-center gap-2", children: [
64679
+ /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "text-xs font-bold text-violet-400 bg-surface-800 border border-surface-700 rounded px-2 py-1", children: "gRPC" }),
64680
+ /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "text-[10px] text-surface-600", children: "Set the target (host:port) in the URL field above." }),
64681
+ /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: `ml-auto w-2 h-2 rounded-full ${statusColors[call.status] ?? "bg-surface-600"}`, title: call.status }),
64682
+ call.codeName && /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "text-[10px] font-mono text-surface-400", children: call.codeName })
64683
+ ] }),
64684
+ /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex-1 min-h-0 overflow-y-auto px-4 pb-2 flex flex-col gap-3", children: [
64685
+ /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { children: [
64686
+ /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-center justify-between mb-1", children: [
64687
+ /* @__PURE__ */ jsxRuntimeExports.jsx("label", { className: "text-[11px] text-surface-400", children: "Proto (paste .proto source, or a file path below)" }),
64688
+ /* @__PURE__ */ jsxRuntimeExports.jsx("button", { onClick: loadProto, disabled: loadingProto, className: "text-[10px] px-2 py-0.5 rounded border border-surface-700 text-surface-300 hover:border-violet-500 hover:text-violet-400 disabled:opacity-40", children: loadingProto ? "Loading…" : "Load proto" })
64689
+ ] }),
64690
+ /* @__PURE__ */ jsxRuntimeExports.jsx(
64691
+ "textarea",
64692
+ {
64693
+ value: grpc.protoSource ?? "",
64694
+ onChange: (e) => patchGrpc({ protoSource: e.target.value }),
64695
+ rows: 4,
64696
+ spellCheck: false,
64697
+ placeholder: 'syntax = "proto3";\npackage helloworld;\nservice Greeter { rpc SayHello (HelloRequest) returns (HelloReply); }',
64698
+ className: "w-full resize-y bg-surface-950 border border-surface-800 rounded px-3 py-2 text-xs font-mono focus:outline-none focus:border-violet-500 placeholder-surface-700"
64699
+ }
64700
+ ),
64701
+ /* @__PURE__ */ jsxRuntimeExports.jsx(
64702
+ "input",
64703
+ {
64704
+ value: grpc.protoPath ?? "",
64705
+ onChange: (e) => patchGrpc({ protoPath: e.target.value }),
64706
+ placeholder: "/path/to/service.proto (optional; used if source is empty)",
64707
+ className: `${inputCls} w-full font-mono mt-1`
64708
+ }
64709
+ ),
64710
+ call.protoError && /* @__PURE__ */ jsxRuntimeExports.jsx("p", { className: "text-[10px] text-red-400 mt-1", children: call.protoError })
64711
+ ] }),
64712
+ /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "grid grid-cols-[1fr_1fr_auto] gap-2 items-end", children: [
64713
+ /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { children: [
64714
+ /* @__PURE__ */ jsxRuntimeExports.jsx("label", { className: "block text-[11px] text-surface-400 mb-1", children: "Service" }),
64715
+ /* @__PURE__ */ jsxRuntimeExports.jsxs("select", { value: grpc.serviceName ?? "", onChange: (e) => patchGrpc({ serviceName: e.target.value, methodName: call.services.find((s) => s.name === e.target.value)?.methods[0]?.name }), className: `${inputCls} w-full`, children: [
64716
+ /* @__PURE__ */ jsxRuntimeExports.jsx("option", { value: "", children: call.services.length ? "Select…" : "Load a proto first" }),
64717
+ call.services.map((s) => /* @__PURE__ */ jsxRuntimeExports.jsx("option", { value: s.name, children: s.name }, s.name))
64718
+ ] })
64719
+ ] }),
64720
+ /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { children: [
64721
+ /* @__PURE__ */ jsxRuntimeExports.jsx("label", { className: "block text-[11px] text-surface-400 mb-1", children: "Method" }),
64722
+ /* @__PURE__ */ jsxRuntimeExports.jsxs("select", { value: grpc.methodName ?? "", onChange: (e) => patchGrpc({ methodName: e.target.value }), className: `${inputCls} w-full`, children: [
64723
+ /* @__PURE__ */ jsxRuntimeExports.jsx("option", { value: "", children: "Select…" }),
64724
+ selectedService?.methods.map((m) => /* @__PURE__ */ jsxRuntimeExports.jsxs("option", { value: m.name, children: [
64725
+ m.name,
64726
+ m.responseStream ? " (stream)" : ""
64727
+ ] }, m.name))
64728
+ ] })
64729
+ ] }),
64730
+ /* @__PURE__ */ jsxRuntimeExports.jsxs("label", { className: "flex items-center gap-1.5 text-xs text-surface-300 pb-1.5", children: [
64731
+ /* @__PURE__ */ jsxRuntimeExports.jsx("input", { type: "checkbox", checked: !!grpc.plaintext, onChange: (e) => patchGrpc({ plaintext: e.target.checked }) }),
64732
+ "Plaintext"
64733
+ ] })
64734
+ ] }),
64735
+ /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { children: [
64736
+ /* @__PURE__ */ jsxRuntimeExports.jsx("label", { className: "block text-[11px] text-surface-400 mb-1", children: "Request message (JSON)" }),
64737
+ /* @__PURE__ */ jsxRuntimeExports.jsx(
64738
+ "textarea",
64739
+ {
64740
+ value: grpc.message,
64741
+ onChange: (e) => patchGrpc({ message: e.target.value }),
64742
+ rows: 4,
64743
+ spellCheck: false,
64744
+ placeholder: '{ "name": "world" }',
64745
+ className: "w-full resize-y bg-surface-950 border border-surface-800 rounded px-3 py-2 text-xs font-mono focus:outline-none focus:border-violet-500 placeholder-surface-700"
64746
+ }
64747
+ )
64748
+ ] }),
64749
+ /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { children: [
64750
+ /* @__PURE__ */ jsxRuntimeExports.jsxs("label", { className: "block text-[11px] text-surface-400 mb-1", children: [
64751
+ "Metadata (one ",
64752
+ /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "font-mono", children: "key: value" }),
64753
+ " per line)"
64754
+ ] }),
64755
+ /* @__PURE__ */ jsxRuntimeExports.jsx(
64756
+ "textarea",
64757
+ {
64758
+ value: metaToText(grpc.metadata),
64759
+ onChange: (e) => patchGrpc({ metadata: textToMeta(e.target.value) }),
64760
+ rows: 2,
64761
+ spellCheck: false,
64762
+ placeholder: "authorization: Bearer {{TOKEN}}",
64763
+ className: "w-full resize-y bg-surface-950 border border-surface-800 rounded px-3 py-2 text-xs font-mono focus:outline-none focus:border-violet-500 placeholder-surface-700"
64764
+ }
64765
+ )
64766
+ ] }),
64767
+ /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-center gap-2", children: [
64768
+ isRunning ? /* @__PURE__ */ jsxRuntimeExports.jsx("button", { onClick: cancel, className: "px-4 py-1.5 bg-red-700 hover:bg-red-600 rounded text-sm font-medium", children: "Cancel" }) : /* @__PURE__ */ jsxRuntimeExports.jsx("button", { onClick: invoke, disabled: !request.url || !grpc.serviceName || !grpc.methodName, className: "px-4 py-1.5 bg-violet-600 hover:bg-violet-500 disabled:bg-surface-800 disabled:text-surface-500 rounded text-sm font-medium", children: "Invoke" }),
64769
+ call.messages.length > 0 && /* @__PURE__ */ jsxRuntimeExports.jsx("button", { onClick: () => clearGrpcMessages(request.id), className: "text-[10px] text-surface-600 hover:text-surface-400 ml-auto", children: "Clear" })
64770
+ ] }),
64771
+ call.status === "error" && call.error && /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "px-3 py-1.5 bg-red-900/40 border border-red-700/50 rounded text-xs text-red-400", children: call.error }),
64772
+ /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "border border-surface-800 rounded bg-surface-950 min-h-[120px]", children: call.messages.length === 0 ? /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "p-4 text-center text-xs text-surface-500", children: "No messages yet" }) : /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "p-2 flex flex-col gap-1", children: [
64773
+ call.messages.map((msg) => /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: `flex gap-2 items-start text-xs rounded px-2 py-1.5 ${msg.direction === "sent" ? "bg-blue-900/30 border border-blue-800/40" : "bg-emerald-900/20 border border-emerald-800/30"}`, children: [
64774
+ /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: `flex-shrink-0 font-mono font-bold ${msg.direction === "sent" ? "text-blue-400" : "text-emerald-400"}`, children: msg.direction === "sent" ? "→" : "←" }),
64775
+ /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "flex-1 font-mono text-surface-300 whitespace-pre-wrap break-all", children: msg.data }),
64776
+ /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "flex-shrink-0 text-surface-400 font-mono text-[10px] mt-px", children: formatTime$1(msg.timestamp) })
64777
+ ] }, msg.id)),
64778
+ /* @__PURE__ */ jsxRuntimeExports.jsx("div", { ref: logEndRef })
64779
+ ] }) })
64780
+ ] })
64781
+ ] });
64782
+ }
64518
64783
  function statusColor$3(code2) {
64519
64784
  const d = String(code2)[0];
64520
64785
  return d === "2" ? "text-emerald-400" : d === "3" ? "text-amber-400" : "text-red-400";
@@ -64770,7 +65035,7 @@ function FuzzResultsPanel({ report, onClear }) {
64770
65035
  ] }) })
64771
65036
  ] });
64772
65037
  }
64773
- const { electron: electron$n } = window;
65038
+ const { electron: electron$p } = window;
64774
65039
  const WRITE_METHODS = /* @__PURE__ */ new Set(["POST", "PUT", "PATCH", "DELETE"]);
64775
65040
  function FuzzModal({ request, onClose }) {
64776
65041
  const environments = useStore((s) => s.environments);
@@ -64796,7 +65061,7 @@ function FuzzModal({ request, onClose }) {
64796
65061
  const env = resolveEnvironmentById(environments, activeEnvironmentId);
64797
65062
  const envVars = env ? Object.fromEntries(env.variables.filter((v) => v.enabled).map((v) => [v.key, v.value])) : {};
64798
65063
  const collectionVars = activeCollectionId ? collections[activeCollectionId]?.data.collectionVariables ?? {} : {};
64799
- const result = await electron$n.fuzzContracts({
65064
+ const result = await electron$p.fuzzContracts({
64800
65065
  requests: [request],
64801
65066
  envVars,
64802
65067
  collectionVars,
@@ -64888,7 +65153,7 @@ function FuzzModal({ request, onClose }) {
64888
65153
  }
64889
65154
  );
64890
65155
  }
64891
- const { electron: electron$m } = window;
65156
+ const { electron: electron$o } = window;
64892
65157
  const METHODS = ["GET", "POST", "PUT", "PATCH", "DELETE", "HEAD", "OPTIONS", "QUERY"];
64893
65158
  const METHOD_COLORS = {
64894
65159
  GET: "text-emerald-400",
@@ -65013,7 +65278,7 @@ function RequestBuilder({ request }) {
65013
65278
  activeEnvironmentId
65014
65279
  );
65015
65280
  const hookSessionVars = useStore.getState().sessionVars;
65016
- const r = await electron$m.sendRequest({
65281
+ const r = await electron$o.sendRequest({
65017
65282
  ...basePayload,
65018
65283
  environment: hookEnv,
65019
65284
  request: hook,
@@ -65061,7 +65326,7 @@ function RequestBuilder({ request }) {
65061
65326
  startLiveStream(activeTabId, streamId);
65062
65327
  let result;
65063
65328
  try {
65064
- result = await electron$m.sendRequest({
65329
+ result = await electron$o.sendRequest({
65065
65330
  ...basePayload,
65066
65331
  environment: freshEnv,
65067
65332
  request: mergedRequest,
@@ -65093,7 +65358,7 @@ function RequestBuilder({ request }) {
65093
65358
  activeEnvironmentId
65094
65359
  );
65095
65360
  const hookSessionVars = useStore.getState().sessionVars;
65096
- const r = await electron$m.sendRequest({
65361
+ const r = await electron$o.sendRequest({
65097
65362
  ...basePayload,
65098
65363
  environment: hookEnv,
65099
65364
  request: hook,
@@ -65143,6 +65408,7 @@ function RequestBuilder({ request }) {
65143
65408
  const hasPostScript = Boolean(request.postRequestScript?.trim());
65144
65409
  const isWs = request.protocol === "websocket";
65145
65410
  const isSoap = request.protocol === "soap";
65411
+ const isGrpc = request.protocol === "grpc";
65146
65412
  const isExample = !!activeAppTab?.exampleId;
65147
65413
  const tabs = [
65148
65414
  // SOAP collapses Params + Body into a single "SOAP" tab — the WSDL drives both.
@@ -65190,7 +65456,7 @@ function RequestBuilder({ request }) {
65190
65456
  "button",
65191
65457
  {
65192
65458
  onClick: () => update({ protocol: "http" }),
65193
- className: `px-2 py-1.5 transition-colors ${request.protocol !== "websocket" && request.protocol !== "soap" ? "bg-blue-600 text-white" : "text-surface-500 hover:text-white"}`,
65459
+ className: `px-2 py-1.5 transition-colors ${!isWs && !isSoap && !isGrpc ? "bg-blue-600 text-white" : "text-surface-500 hover:text-white"}`,
65194
65460
  title: "HTTP request",
65195
65461
  children: "HTTP"
65196
65462
  }
@@ -65219,9 +65485,21 @@ function RequestBuilder({ request }) {
65219
65485
  title: "SOAP - endpoint and method are derived from the WSDL",
65220
65486
  children: "SOAP"
65221
65487
  }
65488
+ ),
65489
+ /* @__PURE__ */ jsxRuntimeExports.jsx(
65490
+ "button",
65491
+ {
65492
+ onClick: () => update({
65493
+ protocol: "grpc",
65494
+ body: request.body.mode === "grpc" ? request.body : { ...request.body, mode: "grpc", grpc: request.body.grpc ?? { message: "{}", metadata: [], plaintext: false } }
65495
+ }),
65496
+ className: `px-2 py-1.5 transition-colors ${isGrpc ? "bg-violet-700 text-violet-100" : "text-surface-500 hover:text-white"}`,
65497
+ title: "gRPC - proto-defined services over HTTP/2",
65498
+ children: "gRPC"
65499
+ }
65222
65500
  )
65223
65501
  ] }),
65224
- !isWs && !isSoap && (customVerb ? /* @__PURE__ */ jsxRuntimeExports.jsx(
65502
+ !isWs && !isSoap && !isGrpc && (customVerb ? /* @__PURE__ */ jsxRuntimeExports.jsx(
65225
65503
  "input",
65226
65504
  {
65227
65505
  autoFocus: true,
@@ -65262,18 +65540,26 @@ function RequestBuilder({ request }) {
65262
65540
  children: "POST"
65263
65541
  }
65264
65542
  ),
65543
+ isGrpc && /* @__PURE__ */ jsxRuntimeExports.jsx(
65544
+ "span",
65545
+ {
65546
+ className: "bg-surface-900 border border-surface-700 rounded px-2 py-1.5 text-xs font-bold text-violet-400 select-none",
65547
+ title: "gRPC target is host:port",
65548
+ children: "gRPC"
65549
+ }
65550
+ ),
65265
65551
  /* @__PURE__ */ jsxRuntimeExports.jsx(
65266
65552
  VarInput,
65267
65553
  {
65268
65554
  value: request.url,
65269
65555
  onChange: (url) => update({ url }),
65270
65556
  onPaste: handleUrlPaste,
65271
- placeholder: isWs ? "ws://example.com/socket" : isSoap ? "Endpoint (auto-filled from WSDL <soap:address>)" : "https://api.example.com/endpoint",
65557
+ placeholder: isWs ? "ws://example.com/socket" : isSoap ? "Endpoint (auto-filled from WSDL <soap:address>)" : isGrpc ? "localhost:50051" : "https://api.example.com/endpoint",
65272
65558
  wrapperClassName: "flex-1",
65273
65559
  className: `border rounded px-3 py-1.5 text-sm focus:outline-none focus:border-blue-500 font-mono placeholder-surface-700 ${isSoap ? "bg-surface-900 border-amber-900/50 text-surface-300" : "bg-surface-800 border-surface-700"}`
65274
65560
  }
65275
65561
  ),
65276
- !isWs && /* @__PURE__ */ jsxRuntimeExports.jsxs(jsxRuntimeExports.Fragment, { children: [
65562
+ !isWs && !isGrpc && /* @__PURE__ */ jsxRuntimeExports.jsxs(jsxRuntimeExports.Fragment, { children: [
65277
65563
  /* @__PURE__ */ jsxRuntimeExports.jsx(
65278
65564
  "button",
65279
65565
  {
@@ -65295,7 +65581,7 @@ function RequestBuilder({ request }) {
65295
65581
  ] })
65296
65582
  ] }),
65297
65583
  showFuzz && /* @__PURE__ */ jsxRuntimeExports.jsx(FuzzModal, { request, onClose: () => setShowFuzz(false) }),
65298
- isWs ? /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "flex-1 min-h-0 overflow-hidden", children: /* @__PURE__ */ jsxRuntimeExports.jsx(WebSocketPanel, { request }) }) : /* @__PURE__ */ jsxRuntimeExports.jsxs(jsxRuntimeExports.Fragment, { children: [
65584
+ isWs ? /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "flex-1 min-h-0 overflow-hidden", children: /* @__PURE__ */ jsxRuntimeExports.jsx(WebSocketPanel, { request }) }) : isGrpc ? /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "flex-1 min-h-0 overflow-hidden", children: /* @__PURE__ */ jsxRuntimeExports.jsx(GrpcPanel, { request, onChange: update }) }) : /* @__PURE__ */ jsxRuntimeExports.jsxs(jsxRuntimeExports.Fragment, { children: [
65299
65585
  /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex border-b border-surface-800 px-4 gap-0 flex-shrink-0", children: [
65300
65586
  tabs.map((tab) => /* @__PURE__ */ jsxRuntimeExports.jsxs(
65301
65587
  "button",
@@ -65828,7 +66114,263 @@ function InteractiveBody({ body, contentType, onAssert }) {
65828
66114
  treeContent
65829
66115
  ] });
65830
66116
  }
65831
- const { electron: electron$l } = window;
66117
+ function getByPath(data, path) {
66118
+ if (!path) return data;
66119
+ const tokens = path.match(/[^.[\]]+|\[\d+\]/g) ?? [];
66120
+ let cur2 = data;
66121
+ for (const tok of tokens) {
66122
+ if (cur2 == null || typeof cur2 !== "object") return void 0;
66123
+ cur2 = tok.startsWith("[") ? cur2[Number(tok.slice(1, -1))] : cur2[tok];
66124
+ }
66125
+ return cur2;
66126
+ }
66127
+ function joinPath(base2, index2, key) {
66128
+ return `${base2}[${index2}]${key ? `.${key}` : ""}`;
66129
+ }
66130
+ const PREFERRED = /^(data|items|results|records|rows|list|content|entries|values|payload|elements)$/i;
66131
+ function findPrimaryArray(data) {
66132
+ if (Array.isArray(data)) return { path: "", array: data };
66133
+ const found = [];
66134
+ const queue = [{ node: data, path: "", depth: 0 }];
66135
+ while (queue.length) {
66136
+ const { node, path, depth } = queue.shift();
66137
+ if (depth > 6 || node === null || typeof node !== "object") continue;
66138
+ if (Array.isArray(node)) {
66139
+ const key = (path.split(".").pop() ?? "").replace(/\[\d+\]/g, "");
66140
+ const objs = node.filter((x) => x && typeof x === "object" && !Array.isArray(x)).length;
66141
+ found.push({ path, array: node, pref: PREFERRED.test(key) ? 1 : 0, objs, depth });
66142
+ } else {
66143
+ for (const [k, v] of Object.entries(node)) {
66144
+ queue.push({ node: v, path: path ? `${path}.${k}` : k, depth: depth + 1 });
66145
+ }
66146
+ }
66147
+ }
66148
+ if (!found.length) return null;
66149
+ found.sort(
66150
+ (a, b) => b.pref - a.pref || Math.sign(b.objs) - Math.sign(a.objs) || b.array.length - a.array.length || a.depth - b.depth
66151
+ );
66152
+ return { path: found[0].path, array: found[0].array };
66153
+ }
66154
+ function tableColumns(rows, cap = 60) {
66155
+ const cols = [];
66156
+ const seen = /* @__PURE__ */ new Set();
66157
+ for (const row of rows) {
66158
+ if (row && typeof row === "object" && !Array.isArray(row)) {
66159
+ for (const k of Object.keys(row)) {
66160
+ if (!seen.has(k)) {
66161
+ seen.add(k);
66162
+ cols.push(k);
66163
+ if (cols.length >= cap) return cols;
66164
+ }
66165
+ }
66166
+ }
66167
+ }
66168
+ return cols;
66169
+ }
66170
+ function classify(v) {
66171
+ if (v === null || v === void 0) return "null";
66172
+ if (Array.isArray(v)) return "array";
66173
+ if (typeof v === "object") return "object";
66174
+ return "primitive";
66175
+ }
66176
+ function cellPreview(v) {
66177
+ switch (classify(v)) {
66178
+ case "null":
66179
+ return "";
66180
+ case "array":
66181
+ return `[${v.length}]`;
66182
+ case "object": {
66183
+ const s = JSON.stringify(v);
66184
+ return s.length > 60 ? s.slice(0, 57) + "…" : s;
66185
+ }
66186
+ default:
66187
+ return String(v);
66188
+ }
66189
+ }
66190
+ function compareValues(x, y) {
66191
+ if (x == null && y == null) return 0;
66192
+ if (x == null) return 1;
66193
+ if (y == null) return -1;
66194
+ const nx = Number(x), ny = Number(y);
66195
+ if (!Number.isNaN(nx) && !Number.isNaN(ny) && String(x).trim() !== "" && String(y).trim() !== "") return nx - ny;
66196
+ return String(x).localeCompare(String(y));
66197
+ }
66198
+ function cellAt(row, col) {
66199
+ return col === "$value" ? row : row && typeof row === "object" ? row[col] : void 0;
66200
+ }
66201
+ function sortedIndices(rows, col, dir) {
66202
+ const idx = rows.map((_, i) => i);
66203
+ idx.sort((a, b) => compareValues(cellAt(rows[a], col), cellAt(rows[b], col)));
66204
+ return dir === "desc" ? idx.reverse() : idx;
66205
+ }
66206
+ const MAX_ROWS = 1e3;
66207
+ function xmlToJson(node) {
66208
+ const out = {};
66209
+ for (const attr of Array.from(node.attributes)) out[`@${attr.name}`] = attr.value;
66210
+ const childEls = Array.from(node.children);
66211
+ if (childEls.length === 0) {
66212
+ const text = node.textContent?.trim() ?? "";
66213
+ return node.attributes.length ? { ...out, "#text": text } : text;
66214
+ }
66215
+ for (const child of childEls) {
66216
+ const val = xmlToJson(child);
66217
+ if (child.tagName in out) {
66218
+ const existing = out[child.tagName];
66219
+ if (Array.isArray(existing)) existing.push(val);
66220
+ else out[child.tagName] = [existing, val];
66221
+ } else {
66222
+ out[child.tagName] = val;
66223
+ }
66224
+ }
66225
+ return out;
66226
+ }
66227
+ function parseBody(body, contentType) {
66228
+ const isXml = !contentType.includes("json") && (contentType.includes("xml") || body.trim().startsWith("<"));
66229
+ try {
66230
+ if (isXml) {
66231
+ const doc2 = new DOMParser().parseFromString(body, "application/xml");
66232
+ if (doc2.querySelector("parsererror")) throw new Error("Malformed XML");
66233
+ return { ok: true, value: doc2.documentElement ? xmlToJson(doc2.documentElement) : {} };
66234
+ }
66235
+ return { ok: true, value: JSON.parse(body) };
66236
+ } catch (e) {
66237
+ return { ok: false, error: e instanceof Error ? e.message : String(e) };
66238
+ }
66239
+ }
66240
+ function bodyHasArray(body, contentType) {
66241
+ const parsed = parseBody(body, contentType);
66242
+ return parsed.ok && findPrimaryArray(parsed.value) !== null;
66243
+ }
66244
+ function ResponseTable({ body, contentType }) {
66245
+ const parsed = reactExports.useMemo(() => parseBody(body, contentType), [body, contentType]);
66246
+ const detected = reactExports.useMemo(() => parsed.ok ? findPrimaryArray(parsed.value) : null, [parsed]);
66247
+ const [path, setPath] = reactExports.useState("");
66248
+ const [sort, setSort] = reactExports.useState(null);
66249
+ reactExports.useEffect(() => {
66250
+ setPath(detected?.path ?? "");
66251
+ setSort(null);
66252
+ }, [detected]);
66253
+ if (!parsed.ok) return /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "p-4 text-xs text-red-400", children: [
66254
+ "Could not parse body: ",
66255
+ parsed.error
66256
+ ] });
66257
+ const target = getByPath(parsed.value, path);
66258
+ const rows = Array.isArray(target) ? target : null;
66259
+ const columns = rows ? tableColumns(rows) : [];
66260
+ const isPrimitiveRows = rows !== null && columns.length === 0;
66261
+ const sortCol = sort?.col ?? null;
66262
+ const order = !rows ? [] : (sort ? sortedIndices(rows, sort.col, sort.dir) : rows.map((_, i) => i)).slice(0, MAX_ROWS);
66263
+ function toggleSort(col) {
66264
+ setSort((s) => s?.col === col ? s.dir === "asc" ? { col, dir: "desc" } : null : { col, dir: "asc" });
66265
+ }
66266
+ function navigate(p2) {
66267
+ setPath(p2);
66268
+ setSort(null);
66269
+ }
66270
+ function drill(originalIndex, col, value) {
66271
+ if (classify(value) === "array" || classify(value) === "object") {
66272
+ navigate(joinPath(path, originalIndex, col ?? void 0));
66273
+ }
66274
+ }
66275
+ function drillKey(key, value) {
66276
+ if (classify(value) === "array" || classify(value) === "object") {
66277
+ navigate(path ? `${path}.${key}` : key);
66278
+ }
66279
+ }
66280
+ const crumbs = (path.match(/[^.[\]]+|\[\d+\]/g) ?? []).reduce((acc, tok) => {
66281
+ const prev = acc.length ? acc[acc.length - 1].path : "";
66282
+ const next = tok.startsWith("[") ? prev + tok : prev ? `${prev}.${tok}` : tok;
66283
+ acc.push({ label: tok, path: next });
66284
+ return acc;
66285
+ }, []);
66286
+ const parentPath = crumbs.length > 1 ? crumbs[crumbs.length - 2].path : "";
66287
+ const th = "px-3 py-1.5 text-left font-medium text-surface-300 border-b border-surface-800 cursor-pointer select-none whitespace-nowrap hover:text-white";
66288
+ const td = "px-3 py-1 border-b border-surface-800/50 align-top";
66289
+ const arrow = (col) => sortCol === col ? sort.dir === "asc" ? " ▲" : " ▼" : "";
66290
+ return /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex flex-col h-full min-h-0", children: [
66291
+ /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "border-b border-surface-800 flex-shrink-0", children: [
66292
+ path && /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-center gap-1 px-3 pt-2 text-xs flex-wrap", children: [
66293
+ /* @__PURE__ */ jsxRuntimeExports.jsx("button", { onClick: () => navigate(parentPath), title: "Up one level", className: "px-1.5 py-0.5 rounded bg-surface-800 hover:bg-surface-700 text-surface-300 mr-1", children: "↑ up" }),
66294
+ /* @__PURE__ */ jsxRuntimeExports.jsx("button", { onClick: () => navigate(""), className: "text-surface-400 hover:text-white", children: "root" }),
66295
+ crumbs.map((c, i) => /* @__PURE__ */ jsxRuntimeExports.jsxs(React$2.Fragment, { children: [
66296
+ /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "text-surface-600", children: "›" }),
66297
+ /* @__PURE__ */ jsxRuntimeExports.jsx("button", { onClick: () => navigate(c.path), className: `font-mono hover:text-white ${i === crumbs.length - 1 ? "text-white font-semibold" : "text-surface-400"}`, children: c.label })
66298
+ ] }, c.path))
66299
+ ] }),
66300
+ /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-center gap-2 px-3 py-2 text-xs", children: [
66301
+ /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "text-surface-500", children: "Path" }),
66302
+ /* @__PURE__ */ jsxRuntimeExports.jsx(
66303
+ "input",
66304
+ {
66305
+ value: path,
66306
+ onChange: (e) => navigate(e.target.value),
66307
+ placeholder: "(root) e.g. data.items",
66308
+ className: "flex-1 bg-surface-800 border border-surface-700 rounded px-2 py-1 font-mono text-[11px] focus:outline-none focus:border-blue-500"
66309
+ }
66310
+ ),
66311
+ /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "text-surface-500 shrink-0", children: rows ? `${rows.length} rows${columns.length ? ` · ${columns.length} cols` : ""}` : classify(target) === "object" ? `${Object.keys(target).length} fields` : "" })
66312
+ ] })
66313
+ ] }),
66314
+ /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex-1 min-h-0 overflow-auto", children: [
66315
+ rows ? /* @__PURE__ */ jsxRuntimeExports.jsxs("table", { className: "text-[11px] font-mono w-full border-collapse", children: [
66316
+ /* @__PURE__ */ jsxRuntimeExports.jsx("thead", { className: "sticky top-0 bg-surface-900", children: /* @__PURE__ */ jsxRuntimeExports.jsxs("tr", { children: [
66317
+ /* @__PURE__ */ jsxRuntimeExports.jsx("th", { className: "px-3 py-1.5 text-right text-surface-600 border-b border-surface-800 w-10", children: "#" }),
66318
+ isPrimitiveRows ? /* @__PURE__ */ jsxRuntimeExports.jsxs("th", { className: th, onClick: () => toggleSort("$value"), children: [
66319
+ "value",
66320
+ arrow("$value")
66321
+ ] }) : columns.map((col) => /* @__PURE__ */ jsxRuntimeExports.jsxs("th", { className: th, onClick: () => toggleSort(col), children: [
66322
+ col,
66323
+ arrow(col)
66324
+ ] }, col))
66325
+ ] }) }),
66326
+ /* @__PURE__ */ jsxRuntimeExports.jsx("tbody", { children: order.map((i) => {
66327
+ const row = rows[i];
66328
+ return /* @__PURE__ */ jsxRuntimeExports.jsxs("tr", { className: "hover:bg-surface-800/40", children: [
66329
+ /* @__PURE__ */ jsxRuntimeExports.jsx("td", { className: "px-3 py-1 text-right text-surface-600 border-b border-surface-800/50", children: i }),
66330
+ isPrimitiveRows ? /* @__PURE__ */ jsxRuntimeExports.jsx("td", { className: td, children: /* @__PURE__ */ jsxRuntimeExports.jsx(Cell, { value: row, onDrill: () => drill(i, null, row) }) }) : columns.map((col) => {
66331
+ const v = row && typeof row === "object" ? row[col] : void 0;
66332
+ return /* @__PURE__ */ jsxRuntimeExports.jsx("td", { className: td, children: /* @__PURE__ */ jsxRuntimeExports.jsx(Cell, { value: v, onDrill: () => drill(i, col, v) }) }, col);
66333
+ })
66334
+ ] }, i);
66335
+ }) })
66336
+ ] }) : classify(target) === "object" ? (
66337
+ // The path points at an object, not an array: show it as fields.
66338
+ /* @__PURE__ */ jsxRuntimeExports.jsx("table", { className: "text-[11px] font-mono w-full border-collapse", children: /* @__PURE__ */ jsxRuntimeExports.jsx("tbody", { children: Object.entries(target).map(([k, v]) => /* @__PURE__ */ jsxRuntimeExports.jsxs("tr", { className: "hover:bg-surface-800/40", children: [
66339
+ /* @__PURE__ */ jsxRuntimeExports.jsx("td", { className: "px-3 py-1 text-surface-400 border-b border-surface-800/50 align-top whitespace-nowrap", children: k }),
66340
+ /* @__PURE__ */ jsxRuntimeExports.jsx("td", { className: td, children: /* @__PURE__ */ jsxRuntimeExports.jsx(Cell, { value: v, onDrill: () => drillKey(k, v) }) })
66341
+ ] }, k)) }) })
66342
+ ) : target !== void 0 ? (
66343
+ // A primitive (or null): just show the value.
66344
+ /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "p-4 text-xs font-mono text-surface-300 whitespace-pre-wrap break-all", children: String(target) })
66345
+ ) : /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "p-4 text-xs text-surface-500", children: [
66346
+ "Nothing at ",
66347
+ /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "font-mono text-surface-400", children: path || "(root)" }),
66348
+ ".",
66349
+ detected ? /* @__PURE__ */ jsxRuntimeExports.jsxs(jsxRuntimeExports.Fragment, { children: [
66350
+ " Detected array: ",
66351
+ /* @__PURE__ */ jsxRuntimeExports.jsx("button", { className: "font-mono text-blue-400", onClick: () => navigate(detected.path), children: detected.path || "(root)" })
66352
+ ] }) : ""
66353
+ ] }),
66354
+ rows && rows.length > MAX_ROWS && /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "p-2 text-[10px] text-surface-500", children: [
66355
+ "Showing first ",
66356
+ MAX_ROWS,
66357
+ " of ",
66358
+ rows.length,
66359
+ " rows."
66360
+ ] })
66361
+ ] })
66362
+ ] });
66363
+ }
66364
+ function Cell({ value, onDrill }) {
66365
+ const kind = classify(value);
66366
+ if (kind === "array" || kind === "object") {
66367
+ return /* @__PURE__ */ jsxRuntimeExports.jsx("button", { onClick: onDrill, title: "Open this nested value", className: "text-blue-400 hover:underline", children: kind === "array" ? cellPreview(value) : "{…}" });
66368
+ }
66369
+ if (kind === "null") return /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "text-surface-600", children: "null" });
66370
+ const text = String(value);
66371
+ return /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "inline-block max-w-[26rem] truncate align-bottom text-surface-300", title: text.length > 40 ? text : void 0, children: text });
66372
+ }
66373
+ const { electron: electron$n } = window;
65832
66374
  const RENDER_TAIL = 500;
65833
66375
  const CLOSE_LABEL = {
65834
66376
  complete: "closed",
@@ -65869,7 +66411,7 @@ function StreamView({ events, streaming, streamId, streamClose, firstEventMs })
65869
66411
  if (!streamId) return;
65870
66412
  setStopping(true);
65871
66413
  try {
65872
- await electron$l.stopStream(streamId);
66414
+ await electron$n.stopStream(streamId);
65873
66415
  } catch {
65874
66416
  }
65875
66417
  }
@@ -66031,7 +66573,7 @@ function HookResultsPanel({ results }) {
66031
66573
  }) })
66032
66574
  ] });
66033
66575
  }
66034
- const { electron: electron$k } = window;
66576
+ const { electron: electron$m } = window;
66035
66577
  function extractPath(url) {
66036
66578
  try {
66037
66579
  return new URL(url).pathname || "/";
@@ -66085,14 +66627,14 @@ function SaveAsMockModal({ onClose }) {
66085
66627
  const entry = state.mocks[serverId];
66086
66628
  const updated = { ...entry.data, name: newServerName, port: Number(newServerPort), routes: [route] };
66087
66629
  updateMock(serverId, updated);
66088
- await electron$k.saveMock(entry.relPath, updated);
66630
+ await electron$m.saveMock(entry.relPath, updated);
66089
66631
  const ws2 = useStore.getState().workspace;
66090
- if (ws2) await electron$k.saveWorkspace(ws2);
66632
+ if (ws2) await electron$m.saveWorkspace(ws2);
66091
66633
  } else {
66092
66634
  const entry = useStore.getState().mocks[serverId];
66093
66635
  const updated = { ...entry.data, routes: [...entry.data.routes, route] };
66094
66636
  updateMock(serverId, updated);
66095
- await electron$k.saveMock(entry.relPath, updated);
66637
+ await electron$m.saveMock(entry.relPath, updated);
66096
66638
  }
66097
66639
  onClose();
66098
66640
  } finally {
@@ -66519,7 +67061,7 @@ function validateHttpSemantics(res, opts = {}) {
66519
67061
  const order = { error: 0, warning: 1, hint: 2 };
66520
67062
  return f.sort((a, b) => order[a.severity] - order[b.severity]);
66521
67063
  }
66522
- const { electron: electron$j } = window;
67064
+ const { electron: electron$l } = window;
66523
67065
  function xmlWellFormed(body) {
66524
67066
  try {
66525
67067
  const doc2 = new DOMParser().parseFromString(body, "application/xml");
@@ -66538,6 +67080,8 @@ function requestBodyText(body) {
66538
67080
  return body.graphql?.query ?? "";
66539
67081
  case "soap":
66540
67082
  return body.soap?.envelope ?? "";
67083
+ case "grpc":
67084
+ return body.grpc?.message ?? "";
66541
67085
  case "form":
66542
67086
  return (body.form ?? []).filter((p2) => p2.enabled && p2.key).map((p2) => `${p2.key}=${p2.value}`).join("\n");
66543
67087
  default:
@@ -66657,7 +67201,7 @@ function ResponseViewer() {
66657
67201
  const contractToast = useToast(2500);
66658
67202
  async function saveAsContract() {
66659
67203
  if (!response || !requestId || !activeTabId) return;
66660
- const schema = response.body ? await electron$j.inferContractSchema(response.body) : null;
67204
+ const schema = response.body ? await electron$l.inferContractSchema(response.body) : null;
66661
67205
  const contentType2 = response.headers["content-type"];
66662
67206
  const headers = contentType2 ? [{ key: "content-type", value: contentType2, required: true }] : [];
66663
67207
  updateRequest(requestId, {
@@ -66705,6 +67249,7 @@ function ResponseViewer() {
66705
67249
  const isXml = !isJson && (contentType.includes("xml") || contentType.includes("html"));
66706
67250
  const supportsTree = isJson || isXml;
66707
67251
  const displayBody = isJson ? prettyJson(response.body) : isXml ? prettyXml(response.body) : response.body;
67252
+ const showTable = supportsTree && !response.streamed && bodyHasArray(response.body, contentType);
66708
67253
  const bodyParseError = response.body.trim().length > 0 && (isJson && (() => {
66709
67254
  try {
66710
67255
  JSON.parse(response.body);
@@ -66791,6 +67336,15 @@ function ResponseViewer() {
66791
67336
  title: "Raw body view",
66792
67337
  children: "Raw"
66793
67338
  }
67339
+ ),
67340
+ showTable && /* @__PURE__ */ jsxRuntimeExports.jsx(
67341
+ "button",
67342
+ {
67343
+ onClick: () => setBodyView("table"),
67344
+ className: `px-2 py-0.5 text-[10px] transition-colors ${bodyView === "table" ? "bg-surface-700 text-white" : "text-surface-600 hover:text-white"}`,
67345
+ title: "Show an array in the response as a sortable table",
67346
+ children: "Table"
67347
+ }
66794
67348
  )
66795
67349
  ] }),
66796
67350
  /* @__PURE__ */ jsxRuntimeExports.jsx(
@@ -66843,7 +67397,7 @@ function ResponseViewer() {
66843
67397
  streamClose: response.streamClose,
66844
67398
  firstEventMs: response.firstEventMs
66845
67399
  }
66846
- ) : tab === "body" && supportsTree && bodyView === "tree" ? /* @__PURE__ */ jsxRuntimeExports.jsx(
67400
+ ) : tab === "body" && showTable && bodyView === "table" ? /* @__PURE__ */ jsxRuntimeExports.jsx(ResponseTable, { body: response.body, contentType }) : tab === "body" && supportsTree && bodyView === "tree" ? /* @__PURE__ */ jsxRuntimeExports.jsx(
66847
67401
  InteractiveBody,
66848
67402
  {
66849
67403
  body: response.body,
@@ -66965,7 +67519,7 @@ function ResponseViewer() {
66965
67519
  ] }) })
66966
67520
  ] });
66967
67521
  }
66968
- const { electron: electron$i } = window;
67522
+ const { electron: electron$k } = window;
66969
67523
  const TARGETS = [
66970
67524
  { id: "robot_framework", label: "Robot Framework", description: "Python RequestsLibrary keywords + test suite" },
66971
67525
  { id: "playwright_ts", label: "Playwright TS", description: "TypeScript page-object API classes + spec files" },
@@ -67006,7 +67560,7 @@ function GeneratorPanel() {
67006
67560
  try {
67007
67561
  const col = collections[selectedCollectionId]?.data;
67008
67562
  const env = resolveEnvironmentById(environments, activeEnvironmentId);
67009
- const generated = await electron$i.generateCode({ collection: col, environment: env, target });
67563
+ const generated = await electron$k.generateCode({ collection: col, environment: env, target });
67010
67564
  setFiles(generated);
67011
67565
  setSelectedFile(generated[0]?.path ?? null);
67012
67566
  } catch (e) {
@@ -67018,7 +67572,7 @@ function GeneratorPanel() {
67018
67572
  async function saveZip() {
67019
67573
  if (files.length === 0) return;
67020
67574
  const col = collections[selectedCollectionId]?.data;
67021
- await electron$i.saveGeneratedFilesAsZip(files, col?.name ?? "api-tests", target);
67575
+ await electron$k.saveGeneratedFilesAsZip(files, col?.name ?? "api-tests", target);
67022
67576
  }
67023
67577
  const selectedContent = files.find((f) => f.path === selectedFile)?.content ?? "";
67024
67578
  const activeTarget = TARGETS.find((t2) => t2.id === target);
@@ -67202,7 +67756,7 @@ function historyToHar(entries, creatorVersion = "1.0") {
67202
67756
  }
67203
67757
  }, null, 2);
67204
67758
  }
67205
- const { electron: electron$h } = window;
67759
+ const { electron: electron$j } = window;
67206
67760
  const STATUS_COLOR = {
67207
67761
  "2": "text-emerald-400",
67208
67762
  "3": "text-amber-400",
@@ -67250,7 +67804,7 @@ function HistoryPanel() {
67250
67804
  }
67251
67805
  async function downloadHar() {
67252
67806
  const stamp = (/* @__PURE__ */ new Date()).toISOString().slice(0, 19).replace(/[:T]/g, "-");
67253
- await electron$h.saveResults(historyToHar(history2), `api-spector-history-${stamp}.har`);
67807
+ await electron$j.saveResults(historyToHar(history2), `api-spector-history-${stamp}.har`);
67254
67808
  }
67255
67809
  function open(entry) {
67256
67810
  setSelected(entry);
@@ -67381,30 +67935,30 @@ function HistoryRow({
67381
67935
  }
67382
67936
  );
67383
67937
  }
67384
- const { electron: electron$g } = window;
67938
+ const { electron: electron$i } = window;
67385
67939
  function WelcomeScreen() {
67386
67940
  const { applyWorkspace } = useWorkspaceLoader();
67387
67941
  const [recents, setRecents] = reactExports.useState([]);
67388
67942
  const [update, setUpdate] = reactExports.useState(null);
67389
67943
  reactExports.useEffect(() => {
67390
- electron$g.getRecentWorkspaces().then(setRecents).catch(() => setRecents([]));
67391
- electron$g.checkForUpdate().then((info) => {
67944
+ electron$i.getRecentWorkspaces().then(setRecents).catch(() => setRecents([]));
67945
+ electron$i.checkForUpdate().then((info) => {
67392
67946
  if (info?.updateAvailable) setUpdate(info);
67393
67947
  }).catch(() => {
67394
67948
  });
67395
67949
  }, []);
67396
67950
  async function openWorkspace() {
67397
- const result = await electron$g.openWorkspace();
67951
+ const result = await electron$i.openWorkspace();
67398
67952
  if (!result) return;
67399
67953
  await applyWorkspace(result.workspace, result.workspacePath);
67400
67954
  }
67401
67955
  async function newWorkspace() {
67402
- const result = await electron$g.newWorkspace();
67956
+ const result = await electron$i.newWorkspace();
67403
67957
  if (!result) return;
67404
67958
  await applyWorkspace(result.workspace, result.workspacePath);
67405
67959
  }
67406
67960
  async function openRecent(path) {
67407
- const result = await electron$g.openWorkspacePath(path);
67961
+ const result = await electron$i.openWorkspacePath(path);
67408
67962
  if (!result) {
67409
67963
  setRecents((prev) => prev.filter((r) => r.path !== path));
67410
67964
  return;
@@ -67444,7 +67998,7 @@ function WelcomeScreen() {
67444
67998
  /* @__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." }),
67445
67999
  /* @__PURE__ */ jsxRuntimeExports.jsxs("p", { className: "text-[11px] mt-3", style: { color: "var(--text-muted)" }, children: [
67446
68000
  "version ",
67447
- "0.4.9"
68001
+ "0.5.1"
67448
68002
  ] })
67449
68003
  ] }),
67450
68004
  /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex flex-col gap-3 w-64", children: [
@@ -67488,7 +68042,7 @@ function WelcomeScreen() {
67488
68042
  ] })
67489
68043
  ] });
67490
68044
  }
67491
- const { electron: electron$f } = window;
68045
+ const { electron: electron$h } = window;
67492
68046
  const EXAMPLES = [
67493
68047
  {
67494
68048
  label: "macOS / Linux (~/.zshrc or ~/.bashrc)",
@@ -67512,7 +68066,7 @@ function MasterKeyModal({ onSuccess, onCancel }) {
67512
68066
  setError("Password cannot be empty.");
67513
68067
  return;
67514
68068
  }
67515
- await electron$f.setMasterKey(password);
68069
+ await electron$h.setMasterKey(password);
67516
68070
  onSuccess(password);
67517
68071
  }
67518
68072
  function copy(idx, text) {
@@ -67596,7 +68150,7 @@ function MasterKeyModal({ onSuccess, onCancel }) {
67596
68150
  }
67597
68151
  );
67598
68152
  }
67599
- const { electron: electron$e } = window;
68153
+ const { electron: electron$g } = window;
67600
68154
  async function shortHash(value) {
67601
68155
  const buf = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(value));
67602
68156
  return Array.from(new Uint8Array(buf)).map((b) => b.toString(16).padStart(2, "0")).join("").slice(0, 8);
@@ -67723,7 +68277,7 @@ function EnvironmentEditor({ onClose }) {
67723
68277
  async function saveEncrypted(idx) {
67724
68278
  const plaintext = secretInputs[idx] ?? "";
67725
68279
  if (!plaintext) return;
67726
- const { set: set2 } = await electron$e.checkMasterKey();
68280
+ const { set: set2 } = await electron$g.checkMasterKey();
67727
68281
  if (!set2) {
67728
68282
  setPendingEncryptIdx(idx);
67729
68283
  return;
@@ -67760,9 +68314,9 @@ function EnvironmentEditor({ onClose }) {
67760
68314
  } : state.workspace
67761
68315
  }));
67762
68316
  const ws2 = useStore.getState().workspace;
67763
- if (ws2) await electron$e.saveWorkspace(ws2);
68317
+ if (ws2) await electron$g.saveWorkspace(ws2);
67764
68318
  }
67765
- await electron$e.saveEnvironment(newRelPath, env);
68319
+ await electron$g.saveEnvironment(newRelPath, env);
67766
68320
  }
67767
68321
  return /* @__PURE__ */ jsxRuntimeExports.jsxs(jsxRuntimeExports.Fragment, { children: [
67768
68322
  pendingEncryptIdx !== null && /* @__PURE__ */ jsxRuntimeExports.jsx(
@@ -68072,7 +68626,7 @@ function EnvironmentEditor({ onClose }) {
68072
68626
  )
68073
68627
  ] });
68074
68628
  }
68075
- const { electron: electron$d } = window;
68629
+ const { electron: electron$f } = window;
68076
68630
  function EnvironmentBar({ inline = false }) {
68077
68631
  const environments = useStore((s) => s.environments);
68078
68632
  const activeEnvironmentId = useStore((s) => s.activeEnvironmentId);
@@ -68087,7 +68641,7 @@ function EnvironmentBar({ inline = false }) {
68087
68641
  if (id2) {
68088
68642
  const hasSecrets = environments[id2]?.data.variables.some((v) => v.enabled && v.secret);
68089
68643
  if (hasSecrets) {
68090
- const { set: set2 } = await electron$d.checkMasterKey();
68644
+ const { set: set2 } = await electron$f.checkMasterKey();
68091
68645
  if (!set2) {
68092
68646
  setPendingEnvId(id2);
68093
68647
  return;
@@ -68141,7 +68695,7 @@ function EnvironmentBar({ inline = false }) {
68141
68695
  controls
68142
68696
  ] });
68143
68697
  }
68144
- const { electron: electron$c } = window;
68698
+ const { electron: electron$e } = window;
68145
68699
  const DEFAULT_PII_PATTERNS = ["authorization", "password", "token", "secret", "api-key", "x-api-key"];
68146
68700
  const ZOOM_STEPS = [0.75, 0.9, 1, 1.1, 1.25, 1.5];
68147
68701
  function WorkspaceSettingsModal({ onClose }) {
@@ -68173,8 +68727,8 @@ function WorkspaceSettingsModal({ onClose }) {
68173
68727
  async function testCloud() {
68174
68728
  setCloudTest({ status: "testing" });
68175
68729
  try {
68176
- if (cloudToken.trim()) await electron$c.setSecret("cloud:token", cloudToken.trim());
68177
- const me = await electron$c.cloudTest();
68730
+ if (cloudToken.trim()) await electron$e.setSecret("cloud:token", cloudToken.trim());
68731
+ const me = await electron$e.cloudTest();
68178
68732
  setCloudTest({ status: "ok", msg: `Connected as ${me.email} · ${me.organization} (${me.plan})` });
68179
68733
  } catch (e) {
68180
68734
  setCloudTest({ status: "err", msg: e.message });
@@ -68198,7 +68752,7 @@ function WorkspaceSettingsModal({ onClose }) {
68198
68752
  async function vaultOidcLogin() {
68199
68753
  setOidc({ status: "busy", msg: "Complete the sign-in in your browser…" });
68200
68754
  try {
68201
- const r = await electron$c.vaultOidcLogin({
68755
+ const r = await electron$e.vaultOidcLogin({
68202
68756
  address: vAddress.trim(),
68203
68757
  mount: vLoginMount.trim() || void 0,
68204
68758
  role: vJwtRole.trim() || void 0,
@@ -68250,7 +68804,7 @@ function WorkspaceSettingsModal({ onClose }) {
68250
68804
  } else {
68251
68805
  delete settings.cloud;
68252
68806
  }
68253
- if (cloudToken.trim()) await electron$c.setSecret("cloud:token", cloudToken.trim());
68807
+ if (cloudToken.trim()) await electron$e.setSecret("cloud:token", cloudToken.trim());
68254
68808
  if (defaultEnvironment) settings.defaultEnvironment = defaultEnvironment;
68255
68809
  else delete settings.defaultEnvironment;
68256
68810
  if (persistHistory) settings.persistHistory = true;
@@ -68277,9 +68831,9 @@ function WorkspaceSettingsModal({ onClose }) {
68277
68831
  else delete settings.secrets;
68278
68832
  updateWorkspaceSettings(settings);
68279
68833
  const updated = useStore.getState().workspace;
68280
- if (updated) await electron$c.saveWorkspace(updated);
68834
+ if (updated) await electron$e.saveWorkspace(updated);
68281
68835
  if (persistHistory) {
68282
- await electron$c.saveHistory(useStore.getState().history).catch(() => {
68836
+ await electron$e.saveHistory(useStore.getState().history).catch(() => {
68283
68837
  });
68284
68838
  }
68285
68839
  onClose();
@@ -68800,7 +69354,7 @@ function WorkspaceSettingsModal({ onClose }) {
68800
69354
  }
68801
69355
  );
68802
69356
  }
68803
- const { electron: electron$b } = window;
69357
+ const { electron: electron$d } = window;
68804
69358
  function DocsGeneratorModal({ onClose }) {
68805
69359
  const collections = useStore((s) => s.collections);
68806
69360
  const collectionList = Object.values(collections);
@@ -68840,9 +69394,9 @@ function DocsGeneratorModal({ onClose }) {
68840
69394
  setGenerating(true);
68841
69395
  setError(null);
68842
69396
  try {
68843
- const content2 = await electron$b.generateDocs(buildPayload());
69397
+ const content2 = await electron$d.generateDocs(buildPayload());
68844
69398
  const filename = format2 === "html" ? "api-docs.html" : "api-docs.md";
68845
- await electron$b.saveResults(content2, filename);
69399
+ await electron$d.saveResults(content2, filename);
68846
69400
  } catch (err) {
68847
69401
  setError(err instanceof Error ? err.message : String(err));
68848
69402
  } finally {
@@ -68853,7 +69407,7 @@ function DocsGeneratorModal({ onClose }) {
68853
69407
  setGenerating(true);
68854
69408
  setError(null);
68855
69409
  try {
68856
- const content2 = await electron$b.generateDocs(buildPayload());
69410
+ const content2 = await electron$d.generateDocs(buildPayload());
68857
69411
  setPreview(content2);
68858
69412
  } catch (err) {
68859
69413
  setError(err instanceof Error ? err.message : String(err));
@@ -69202,7 +69756,7 @@ function parseCurl(command2) {
69202
69756
  }
69203
69757
  return { name: name2, method: resolvedMethod, url, headers, params: [], auth, body };
69204
69758
  }
69205
- const { electron: electron$a } = window;
69759
+ const { electron: electron$c } = window;
69206
69760
  const OPTIONS = [
69207
69761
  { id: "postman", label: "Postman", description: "Collection v2.1 JSON" },
69208
69762
  { id: "openapi", label: "OpenAPI", description: "JSON or YAML (v3.x)", supportsUrl: true },
@@ -69214,16 +69768,16 @@ const OPTIONS = [
69214
69768
  ];
69215
69769
  function listEndpoints(col) {
69216
69770
  const result = [];
69217
- function walk(folder, path) {
69771
+ function walk2(folder, path) {
69218
69772
  for (const id2 of folder.requestIds) {
69219
69773
  const req = col.requests[id2];
69220
69774
  if (req) result.push({ request: req, folderPath: path });
69221
69775
  }
69222
69776
  for (const sub of folder.folders) {
69223
- walk(sub, [...path, sub.name]);
69777
+ walk2(sub, [...path, sub.name]);
69224
69778
  }
69225
69779
  }
69226
- walk(col.rootFolder, []);
69780
+ walk2(col.rootFolder, []);
69227
69781
  return result;
69228
69782
  }
69229
69783
  function ImportModal({ onImport, onClose }) {
@@ -69277,12 +69831,12 @@ function ImportModal({ onImport, onClose }) {
69277
69831
  setError(null);
69278
69832
  try {
69279
69833
  let col = null;
69280
- if (opt2.id === "postman") col = await electron$a.importPostman();
69281
- if (opt2.id === "openapi") col = await electron$a.importOpenApi();
69282
- if (opt2.id === "insomnia") col = await electron$a.importInsomnia();
69283
- if (opt2.id === "bruno") col = await electron$a.importBruno();
69284
- if (opt2.id === "http") col = await electron$a.importHttpFile();
69285
- if (opt2.id === "spector") col = await electron$a.importSpectorCollection();
69834
+ if (opt2.id === "postman") col = await electron$c.importPostman();
69835
+ if (opt2.id === "openapi") col = await electron$c.importOpenApi();
69836
+ if (opt2.id === "insomnia") col = await electron$c.importInsomnia();
69837
+ if (opt2.id === "bruno") col = await electron$c.importBruno();
69838
+ if (opt2.id === "http") col = await electron$c.importHttpFile();
69839
+ if (opt2.id === "spector") col = await electron$c.importSpectorCollection();
69286
69840
  if (!col) {
69287
69841
  setLoading(false);
69288
69842
  return;
@@ -69305,7 +69859,7 @@ function ImportModal({ onImport, onClose }) {
69305
69859
  setLoading(true);
69306
69860
  setError(null);
69307
69861
  try {
69308
- const col = await electron$a.importOpenApiFromUrl(trimmed);
69862
+ const col = await electron$c.importOpenApiFromUrl(trimmed);
69309
69863
  if (col) enterPreview(col);
69310
69864
  } catch (err) {
69311
69865
  setError(err instanceof Error ? err.message : String(err));
@@ -69412,7 +69966,7 @@ function ImportModal({ onImport, onClose }) {
69412
69966
  mergeIntoCollection(target, prunedRoot, prunedRequests);
69413
69967
  const entry = useStore.getState().collections[target];
69414
69968
  if (entry) {
69415
- await electron$a.saveCollection(entry.relPath, entry.data);
69969
+ await electron$c.saveCollection(entry.relPath, entry.data);
69416
69970
  markCollectionClean(target);
69417
69971
  }
69418
69972
  setActiveCollection(target);
@@ -69439,7 +69993,7 @@ function ImportModal({ onImport, onClose }) {
69439
69993
  variables: [{ key: name2, value, enabled: true }]
69440
69994
  };
69441
69995
  const relPath = envRelPath(finalName, envId);
69442
- await electron$a.saveEnvironment(relPath, env);
69996
+ await electron$c.saveEnvironment(relPath, env);
69443
69997
  useStore.setState((s) => {
69444
69998
  s.environments[envId] = { relPath, data: env };
69445
69999
  if (!s.activeEnvironmentId) s.activeEnvironmentId = envId;
@@ -69449,7 +70003,7 @@ function ImportModal({ onImport, onClose }) {
69449
70003
  return s;
69450
70004
  });
69451
70005
  const ws2 = useStore.getState().workspace;
69452
- if (ws2) await electron$a.saveWorkspace(ws2);
70006
+ if (ws2) await electron$c.saveWorkspace(ws2);
69453
70007
  } else {
69454
70008
  const entry = state.environments[envTarget];
69455
70009
  if (!entry) throw new Error("Target environment not found");
@@ -69464,7 +70018,7 @@ function ImportModal({ onImport, onClose }) {
69464
70018
  } else {
69465
70019
  updated.variables = [...updated.variables, { key: name2, value, enabled: true }];
69466
70020
  }
69467
- await electron$a.saveEnvironment(entry.relPath, updated);
70021
+ await electron$c.saveEnvironment(entry.relPath, updated);
69468
70022
  useStore.getState().updateEnvironment(envTarget, updated);
69469
70023
  }
69470
70024
  }
@@ -69831,16 +70385,16 @@ function pruneFolder(src, _col, keep, _out) {
69831
70385
  }
69832
70386
  function collectRequestsByFolder(folder, src) {
69833
70387
  const out = {};
69834
- function walk(f) {
70388
+ function walk2(f) {
69835
70389
  for (const id2 of f.requestIds) {
69836
70390
  if (src.requests[id2]) out[id2] = src.requests[id2];
69837
70391
  }
69838
- for (const sub of f.folders) walk(sub);
70392
+ for (const sub of f.folders) walk2(sub);
69839
70393
  }
69840
- walk(folder);
70394
+ walk2(folder);
69841
70395
  return out;
69842
70396
  }
69843
- const { electron: electron$9 } = window;
70397
+ const { electron: electron$b } = window;
69844
70398
  function Toolbar({ onOpenDocs: _onOpenDocs }) {
69845
70399
  const { applyWorkspace } = useWorkspaceLoader();
69846
70400
  const workspace = useStore((s) => s.workspace);
@@ -69863,13 +70417,13 @@ function Toolbar({ onOpenDocs: _onOpenDocs }) {
69863
70417
  try {
69864
70418
  for (const { relPath, data, dirty } of Object.values(collections)) {
69865
70419
  if (!dirty) continue;
69866
- await electron$9.saveCollection(relPath, data);
70420
+ await electron$b.saveCollection(relPath, data);
69867
70421
  markCollectionClean(data.id);
69868
70422
  }
69869
70423
  for (const { relPath, data } of Object.values(environments)) {
69870
- await electron$9.saveEnvironment(relPath, data);
70424
+ await electron$b.saveEnvironment(relPath, data);
69871
70425
  }
69872
- if (workspace) await electron$9.saveWorkspace(workspace);
70426
+ if (workspace) await electron$b.saveWorkspace(workspace);
69873
70427
  } finally {
69874
70428
  setSaving(false);
69875
70429
  }
@@ -69877,14 +70431,14 @@ function Toolbar({ onOpenDocs: _onOpenDocs }) {
69877
70431
  async function afterImport(col) {
69878
70432
  if (!col) return;
69879
70433
  const relPath = colRelPath(col.name, col.id);
69880
- await electron$9.saveCollection(relPath, col);
70434
+ await electron$b.saveCollection(relPath, col);
69881
70435
  loadCollection(relPath, col);
69882
70436
  setActiveCollection(col.id);
69883
70437
  const ws2 = useStore.getState().workspace;
69884
70438
  if (ws2 && !ws2.collections.includes(relPath)) {
69885
70439
  const updated = { ...ws2, collections: [...ws2.collections, relPath] };
69886
70440
  useStore.setState({ workspace: updated });
69887
- await electron$9.saveWorkspace(updated);
70441
+ await electron$b.saveWorkspace(updated);
69888
70442
  }
69889
70443
  }
69890
70444
  if (!workspace) return null;
@@ -69973,7 +70527,7 @@ function Toolbar({ onOpenDocs: _onOpenDocs }) {
69973
70527
  "button",
69974
70528
  {
69975
70529
  onClick: async () => {
69976
- const result = await electron$9.openWorkspace();
70530
+ const result = await electron$b.openWorkspace();
69977
70531
  if (result) await applyWorkspace(result.workspace, result.workspacePath);
69978
70532
  },
69979
70533
  className: "px-2.5 py-1 text-xs bg-surface-800 hover:bg-surface-700 rounded transition-colors",
@@ -69985,7 +70539,7 @@ function Toolbar({ onOpenDocs: _onOpenDocs }) {
69985
70539
  "button",
69986
70540
  {
69987
70541
  onClick: async () => {
69988
- const result = await electron$9.newWorkspace();
70542
+ const result = await electron$b.newWorkspace();
69989
70543
  if (result) await applyWorkspace(result.workspace, result.workspacePath);
69990
70544
  },
69991
70545
  className: "px-2.5 py-1 text-xs bg-surface-800 hover:bg-surface-700 rounded transition-colors",
@@ -69997,7 +70551,7 @@ function Toolbar({ onOpenDocs: _onOpenDocs }) {
69997
70551
  "button",
69998
70552
  {
69999
70553
  onClick: async () => {
70000
- await electron$9.closeWorkspace();
70554
+ await electron$b.closeWorkspace();
70001
70555
  closeWorkspace();
70002
70556
  },
70003
70557
  className: "px-2.5 py-1 text-xs bg-surface-800 hover:bg-surface-700 rounded transition-colors",
@@ -70448,7 +71002,7 @@ api-tests:
70448
71002
  function EmptyState({ message }) {
70449
71003
  return /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "flex items-center justify-center h-24 text-surface-400 text-xs", children: message });
70450
71004
  }
70451
- const { electron: electron$8 } = window;
71005
+ const { electron: electron$a } = window;
70452
71006
  const HOOK_BADGE = {
70453
71007
  beforeAll: { label: "BEFORE ALL", cls: "bg-violet-700 text-white" },
70454
71008
  before: { label: "BEFORE", cls: "bg-violet-600 text-white" },
@@ -70568,13 +71122,13 @@ function RunnerModal() {
70568
71122
  setSummary(null);
70569
71123
  setRunnerRunning(true);
70570
71124
  progressIdxRef.current = 0;
70571
- electron$8.onRunProgress((result) => {
71125
+ electron$a.onRunProgress((result) => {
70572
71126
  const idx = progressIdxRef.current;
70573
71127
  patchRunnerResult(idx, result);
70574
71128
  if (result.status !== "running") progressIdxRef.current++;
70575
71129
  });
70576
71130
  try {
70577
- const s = await electron$8.runCollection({
71131
+ const s = await electron$a.runCollection({
70578
71132
  items: items2,
70579
71133
  environment: env,
70580
71134
  globals,
@@ -70585,7 +71139,7 @@ function RunnerModal() {
70585
71139
  });
70586
71140
  setSummary(s);
70587
71141
  } finally {
70588
- electron$8.offRunProgress();
71142
+ electron$a.offRunProgress();
70589
71143
  setRunnerRunning(false);
70590
71144
  }
70591
71145
  }, [collectionId, folderId, filterTags, selectedEnvId, environments, globals, colEntry, requestDelay, workspaceSettings, setRunnerResults, patchRunnerResult, setRunnerRunning]);
@@ -70811,7 +71365,7 @@ function RunnerModal() {
70811
71365
  };
70812
71366
  const content2 = exportFormat === "junit" ? buildJUnitReport(runnerResults, summary, meta2) : exportFormat === "html" ? buildHtmlReport(runnerResults, summary, meta2) : buildJsonReport(runnerResults, summary, meta2);
70813
71367
  const ext = exportFormat === "junit" ? "xml" : exportFormat === "html" ? "html" : "json";
70814
- electron$8.saveResults(content2, `spector-results.${ext}`);
71368
+ electron$a.saveResults(content2, `spector-results.${ext}`);
70815
71369
  },
70816
71370
  className: "px-2.5 py-0.5 bg-surface-800 hover:bg-surface-700 rounded transition-colors text-[11px] whitespace-nowrap",
70817
71371
  children: "Export results"
@@ -70823,11 +71377,843 @@ function RunnerModal() {
70823
71377
  }
70824
71378
  );
70825
71379
  }
71380
+ const HTTP_METHODS$2 = ["get", "put", "post", "delete", "options", "head", "patch", "trace"];
71381
+ function normalizePath(url) {
71382
+ let u = (url || "").trim();
71383
+ u = u.split("#")[0].split("?")[0];
71384
+ u = u.replace(/\{\{[^}]*\}\}/g, "");
71385
+ u = u.replace(/^[a-z0-9+.-]+:\/\/[^/]*/i, "");
71386
+ u = u.replace(/\/{2,}/g, "/");
71387
+ if (!u.startsWith("/")) u = "/" + u;
71388
+ u = u.replace(/\/+$/, "");
71389
+ return u === "" ? "/" : u;
71390
+ }
71391
+ function segments(path) {
71392
+ return path.split("/").filter(Boolean);
71393
+ }
71394
+ function isParam(seg) {
71395
+ return seg.startsWith("{") && seg.endsWith("}");
71396
+ }
71397
+ function pathMatches(requestUrl, template) {
71398
+ const reqSegs = segments(normalizePath(requestUrl));
71399
+ const tplSegs = segments(template);
71400
+ if (reqSegs.length < tplSegs.length) return false;
71401
+ const tail = reqSegs.slice(reqSegs.length - tplSegs.length);
71402
+ return tplSegs.every((seg, i) => isParam(seg) ? tail[i] !== void 0 && tail[i] !== "" : tail[i] === seg);
71403
+ }
71404
+ function resolveRef$2(spec, ref2) {
71405
+ const parts = ref2.replace(/^#\//, "").split("/");
71406
+ return parts.reduce((o, k) => o?.[decodeURIComponent(k.replace(/~1/g, "/").replace(/~0/g, "~"))], spec);
71407
+ }
71408
+ function deref$2(spec, node, depth = 0, seen = /* @__PURE__ */ new Set()) {
71409
+ if (!node || typeof node !== "object" || depth > 8) return node;
71410
+ if (Array.isArray(node)) return node.map((n) => deref$2(spec, n, depth + 1, seen));
71411
+ if ("$ref" in node) {
71412
+ const target = resolveRef$2(spec, node.$ref);
71413
+ if (!target || seen.has(target)) return {};
71414
+ return deref$2(spec, target, depth + 1, /* @__PURE__ */ new Set([...seen, target]));
71415
+ }
71416
+ const out = {};
71417
+ for (const [k, v] of Object.entries(node)) out[k] = deref$2(spec, v, depth + 1, seen);
71418
+ return out;
71419
+ }
71420
+ function flattenSchemaPaths(schema, prefix2 = "", depth = 0) {
71421
+ if (!schema || typeof schema !== "object" || depth > 8) return [];
71422
+ const type2 = Array.isArray(schema.type) ? schema.type[0] : schema.type;
71423
+ const out = [];
71424
+ if (type2 === "object" || schema.properties) {
71425
+ for (const [name2, sub] of Object.entries(schema.properties ?? {})) {
71426
+ const path = prefix2 ? `${prefix2}.${name2}` : name2;
71427
+ out.push(path);
71428
+ out.push(...flattenSchemaPaths(sub, path, depth + 1));
71429
+ }
71430
+ } else if (type2 === "array" && schema.items) {
71431
+ out.push(...flattenSchemaPaths(schema.items, `${prefix2}[]`, depth + 1));
71432
+ }
71433
+ return out;
71434
+ }
71435
+ function flattenValuePaths(value, prefix2 = "", depth = 0) {
71436
+ if (value == null || typeof value !== "object" || depth > 8) return [];
71437
+ const out = [];
71438
+ if (Array.isArray(value)) {
71439
+ for (const el of value) out.push(...flattenValuePaths(el, `${prefix2}[]`, depth + 1));
71440
+ } else {
71441
+ for (const [k, v] of Object.entries(value)) {
71442
+ const path = prefix2 ? `${prefix2}.${k}` : k;
71443
+ out.push(path);
71444
+ out.push(...flattenValuePaths(v, path, depth + 1));
71445
+ }
71446
+ }
71447
+ return [...new Set(out)];
71448
+ }
71449
+ function successResponseSchema$1(spec, responses) {
71450
+ const code2 = Object.keys(responses ?? {}).filter((c) => /^2\d\d$/.test(c)).sort()[0];
71451
+ if (!code2) return void 0;
71452
+ const schema = responses[code2]?.content?.["application/json"]?.schema ?? responses[code2]?.content?.["application/json;charset=utf-8"]?.schema;
71453
+ return schema ? deref$2(spec, schema) : void 0;
71454
+ }
71455
+ function enumerateOperations(spec) {
71456
+ const doc2 = spec ?? {};
71457
+ const ops2 = [];
71458
+ for (const [path, item] of Object.entries(doc2.paths ?? {})) {
71459
+ if (!item || typeof item !== "object") continue;
71460
+ for (const [method, op] of Object.entries(item)) {
71461
+ if (!HTTP_METHODS$2.includes(method.toLowerCase())) continue;
71462
+ if (!op || typeof op !== "object") continue;
71463
+ const declaredStatuses = Object.keys(op.responses ?? {}).filter((c) => /^\d{3}$/.test(c));
71464
+ ops2.push({ method: method.toUpperCase(), path, operationId: op.operationId, declaredStatuses, responses: op.responses });
71465
+ }
71466
+ }
71467
+ return ops2;
71468
+ }
71469
+ function round(n) {
71470
+ return Math.round(n * 10) / 10;
71471
+ }
71472
+ function computeCoverage(spec, requests, observations = []) {
71473
+ const doc2 = spec ?? {};
71474
+ const operations2 = enumerateOperations(spec).map((op) => {
71475
+ const mapped = requests.filter((r) => r.method.toUpperCase() === op.method && pathMatches(r.url, op.path));
71476
+ const obs = observations.filter((o) => o.method.toUpperCase() === op.method && pathMatches(o.url, op.path));
71477
+ const asserted = mapped.map((r) => r.expectedStatus).filter((s) => typeof s === "number");
71478
+ const seen = obs.map((o) => o.status);
71479
+ const covered = /* @__PURE__ */ new Set([...asserted, ...seen]);
71480
+ const coveredStatuses2 = op.declaredStatuses.filter((code2) => covered.has(Number(code2)));
71481
+ const hasNegativeTest = [...covered].some((s) => s >= 400);
71482
+ const declaredProperties2 = flattenSchemaPaths(successResponseSchema$1(doc2, op.responses ?? {}));
71483
+ const observedPaths = new Set(obs.flatMap((o) => o.responsePaths ?? []));
71484
+ const coveredProperties2 = declaredProperties2.filter((p2) => observedPaths.has(p2));
71485
+ return {
71486
+ method: op.method,
71487
+ path: op.path,
71488
+ operationId: op.operationId,
71489
+ tested: mapped.length > 0 || obs.length > 0,
71490
+ requests: mapped.map((r) => r.name),
71491
+ declaredStatuses: op.declaredStatuses,
71492
+ coveredStatuses: coveredStatuses2,
71493
+ hasNegativeTest,
71494
+ declaredProperties: declaredProperties2,
71495
+ coveredProperties: coveredProperties2
71496
+ };
71497
+ });
71498
+ const tested = operations2.filter((o) => o.tested).length;
71499
+ const declaredStatuses = operations2.reduce((n, o) => n + o.declaredStatuses.length, 0);
71500
+ const coveredStatuses = operations2.reduce((n, o) => n + o.coveredStatuses.length, 0);
71501
+ const declaredProperties = operations2.reduce((n, o) => n + o.declaredProperties.length, 0);
71502
+ const coveredProperties = operations2.reduce((n, o) => n + o.coveredProperties.length, 0);
71503
+ const withoutNegativeTest = operations2.filter((o) => o.tested && !o.hasNegativeTest).length;
71504
+ return {
71505
+ spec: { title: doc2.info?.title, version: doc2.info?.version },
71506
+ totals: {
71507
+ operations: operations2.length,
71508
+ tested,
71509
+ untested: operations2.length - tested,
71510
+ operationPct: operations2.length ? round(tested / operations2.length * 100) : 0,
71511
+ declaredStatuses,
71512
+ coveredStatuses,
71513
+ statusPct: declaredStatuses ? round(coveredStatuses / declaredStatuses * 100) : 0,
71514
+ withoutNegativeTest,
71515
+ declaredProperties,
71516
+ coveredProperties,
71517
+ propertyPct: declaredProperties ? round(coveredProperties / declaredProperties * 100) : 0
71518
+ },
71519
+ operations: operations2
71520
+ };
71521
+ }
71522
+ const HTTP_METHODS$1 = ["get", "put", "post", "delete", "options", "head", "patch", "trace"];
71523
+ function resolveRef$1(spec, ref2) {
71524
+ const parts = ref2.replace(/^#\//, "").split("/");
71525
+ return parts.reduce((o, k) => o?.[decodeURIComponent(k.replace(/~1/g, "/").replace(/~0/g, "~"))], spec);
71526
+ }
71527
+ function deref$1(spec, node, depth = 0, seen = /* @__PURE__ */ new Set()) {
71528
+ if (!node || typeof node !== "object" || depth > 8) return node;
71529
+ if (Array.isArray(node)) return node.map((n) => deref$1(spec, n, depth + 1, seen));
71530
+ if ("$ref" in node) {
71531
+ const target = resolveRef$1(spec, node.$ref);
71532
+ if (!target || seen.has(target)) return {};
71533
+ return deref$1(spec, target, depth + 1, /* @__PURE__ */ new Set([...seen, target]));
71534
+ }
71535
+ const out = {};
71536
+ for (const [k, v] of Object.entries(node)) out[k] = deref$1(spec, v, depth + 1, seen);
71537
+ return out;
71538
+ }
71539
+ function jsonSchemaFor(spec, responses, code2) {
71540
+ const resp = responses?.[code2];
71541
+ const schema = resp?.content?.["application/json"]?.schema ?? resp?.content?.["application/json;charset=utf-8"]?.schema;
71542
+ return schema ? deref$1(spec, schema) : void 0;
71543
+ }
71544
+ function sampleValue(schema) {
71545
+ if (!schema || typeof schema !== "object") return "string";
71546
+ if (schema.example !== void 0) return schema.example;
71547
+ if (schema.default !== void 0) return schema.default;
71548
+ if (Array.isArray(schema.enum) && schema.enum.length) return schema.enum[0];
71549
+ const type2 = Array.isArray(schema.type) ? schema.type[0] : schema.type;
71550
+ switch (type2) {
71551
+ case "integer":
71552
+ case "number": {
71553
+ const min = schema.minimum ?? (schema.exclusiveMinimum != null ? schema.exclusiveMinimum + 1 : void 0);
71554
+ const max = schema.maximum ?? (schema.exclusiveMaximum != null ? schema.exclusiveMaximum - 1 : void 0);
71555
+ if (min != null) return min;
71556
+ if (max != null) return max;
71557
+ return type2 === "integer" ? 1 : 1.5;
71558
+ }
71559
+ case "boolean":
71560
+ return true;
71561
+ case "array":
71562
+ return [sampleValue(schema.items ?? {})];
71563
+ case "object": {
71564
+ const out = {};
71565
+ const props = schema.properties ?? {};
71566
+ const required2 = schema.required ?? [];
71567
+ for (const [name2, propSchema] of Object.entries(props)) out[name2] = sampleValue(propSchema);
71568
+ for (const name2 of required2) if (!(name2 in out)) out[name2] = "string";
71569
+ return out;
71570
+ }
71571
+ case "string":
71572
+ default:
71573
+ return sampleString(schema);
71574
+ }
71575
+ }
71576
+ function sampleString(schema) {
71577
+ switch (schema.format) {
71578
+ case "email":
71579
+ return "user@example.com";
71580
+ case "uuid":
71581
+ return "00000000-0000-0000-0000-000000000000";
71582
+ case "date":
71583
+ return "2020-01-01";
71584
+ case "date-time":
71585
+ return "2020-01-01T00:00:00Z";
71586
+ case "uri":
71587
+ case "url":
71588
+ return "https://example.com";
71589
+ case "hostname":
71590
+ return "example.com";
71591
+ case "ipv4":
71592
+ return "127.0.0.1";
71593
+ default: {
71594
+ const min = schema.minLength ?? 0;
71595
+ let s = "string";
71596
+ if (min > s.length) s = s.padEnd(min, "x");
71597
+ if (schema.maxLength != null && s.length > schema.maxLength) s = s.slice(0, schema.maxLength);
71598
+ return s;
71599
+ }
71600
+ }
71601
+ }
71602
+ function collectParams(spec, pathItem, op) {
71603
+ const raw = [...pathItem?.parameters ?? [], ...op?.parameters ?? []].map((p2) => deref$1(spec, p2));
71604
+ const byKey = /* @__PURE__ */ new Map();
71605
+ for (const p2 of raw) if (p2?.name && p2?.in) byKey.set(`${p2.in}:${p2.name}`, p2);
71606
+ return [...byKey.values()];
71607
+ }
71608
+ function successCode(responses) {
71609
+ const codes = Object.keys(responses ?? {}).filter((c) => /^2\d\d$/.test(c)).map(Number).sort((a, b) => a - b);
71610
+ return codes[0] ?? 200;
71611
+ }
71612
+ function negativeCode(responses) {
71613
+ const codes = Object.keys(responses ?? {}).filter((c) => /^4\d\d$/.test(c)).map(Number).sort((a, b) => a - b);
71614
+ return codes[0] ?? 400;
71615
+ }
71616
+ function baseTest(method, path, op, params) {
71617
+ const pathParams = {};
71618
+ for (const p2 of params.filter((p22) => p22.in === "path")) pathParams[p2.name] = sampleValue(p2.schema ?? {});
71619
+ const query = params.filter((p2) => p2.in === "query" && p2.required).map((p2) => ({ key: p2.name, value: String(sampleValue(p2.schema ?? {})) }));
71620
+ const headers = params.filter((p2) => p2.in === "header" && p2.required).map((p2) => ({ key: p2.name, value: String(sampleValue(p2.schema ?? {})) }));
71621
+ return { operationId: op?.operationId, method, path, pathParams, query, headers };
71622
+ }
71623
+ function requestBodySchema(spec, op) {
71624
+ const rb = deref$1(spec, op?.requestBody);
71625
+ const schema = rb?.content?.["application/json"]?.schema;
71626
+ return schema;
71627
+ }
71628
+ function generateForOperation(spec, method, path, pathItem, op, opts) {
71629
+ const tests = [];
71630
+ const params = collectParams(spec, pathItem, op);
71631
+ const responses = op?.responses ?? {};
71632
+ const okCode = successCode(responses);
71633
+ const badCode = negativeCode(responses);
71634
+ const bodySchema = requestBodySchema(spec, op);
71635
+ const validBody = bodySchema ? sampleValue(bodySchema) : void 0;
71636
+ const base2 = baseTest(method, path, op, params);
71637
+ const label = `${method} ${path}`;
71638
+ tests.push({
71639
+ ...base2,
71640
+ name: `${label} - happy path`,
71641
+ category: "happy",
71642
+ body: validBody !== void 0 ? JSON.stringify(validBody, null, 2) : void 0,
71643
+ expectedStatus: okCode,
71644
+ responseSchema: jsonSchemaFor(spec, responses, String(okCode)) ? JSON.stringify(jsonSchemaFor(spec, responses, String(okCode)), null, 2) : void 0
71645
+ });
71646
+ const props = bodySchema?.properties ?? {};
71647
+ const required2 = bodySchema?.required ?? [];
71648
+ if (opts.includeNegative && validBody && typeof validBody === "object") {
71649
+ let n = 0;
71650
+ for (const field of required2) {
71651
+ if (n >= opts.maxNegativePerOp) break;
71652
+ const mutated = { ...validBody };
71653
+ delete mutated[field];
71654
+ tests.push({ ...base2, name: `${label} - missing ${field}`, category: "negative", body: JSON.stringify(mutated, null, 2), expectedStatus: badCode });
71655
+ n++;
71656
+ }
71657
+ for (const [field, ps] of Object.entries(props)) {
71658
+ if (n >= opts.maxNegativePerOp) break;
71659
+ const t2 = Array.isArray(ps.type) ? ps.type[0] : ps.type;
71660
+ if (t2 !== "string" && t2 !== "integer" && t2 !== "number" && t2 !== "boolean") continue;
71661
+ const wrong = t2 === "string" ? 12345 : "not-a-valid-value";
71662
+ tests.push({ ...base2, name: `${label} - ${field} wrong type`, category: "negative", body: JSON.stringify({ ...validBody, [field]: wrong }, null, 2), expectedStatus: badCode });
71663
+ n++;
71664
+ }
71665
+ }
71666
+ if (opts.includeBoundary && validBody && typeof validBody === "object") {
71667
+ let n = 0;
71668
+ for (const [field, ps] of Object.entries(props)) {
71669
+ if (n >= opts.maxBoundaryPerOp) break;
71670
+ const t2 = Array.isArray(ps.type) ? ps.type[0] : ps.type;
71671
+ if ((t2 === "integer" || t2 === "number") && ps.minimum != null) {
71672
+ tests.push({ ...base2, name: `${label} - ${field} below minimum`, category: "boundary", body: JSON.stringify({ ...validBody, [field]: ps.minimum - 1 }, null, 2), expectedStatus: badCode });
71673
+ n++;
71674
+ } else if ((t2 === "integer" || t2 === "number") && ps.maximum != null) {
71675
+ tests.push({ ...base2, name: `${label} - ${field} above maximum`, category: "boundary", body: JSON.stringify({ ...validBody, [field]: ps.maximum + 1 }, null, 2), expectedStatus: badCode });
71676
+ n++;
71677
+ } else if (t2 === "string" && ps.maxLength != null) {
71678
+ tests.push({ ...base2, name: `${label} - ${field} too long`, category: "boundary", body: JSON.stringify({ ...validBody, [field]: "x".repeat(ps.maxLength + 1) }, null, 2), expectedStatus: badCode });
71679
+ n++;
71680
+ }
71681
+ }
71682
+ }
71683
+ return tests;
71684
+ }
71685
+ function generateTests(spec, options = {}) {
71686
+ const opts = {
71687
+ only: options.only ?? /* @__PURE__ */ new Set(),
71688
+ includeNegative: options.includeNegative ?? true,
71689
+ includeBoundary: options.includeBoundary ?? true,
71690
+ maxNegativePerOp: options.maxNegativePerOp ?? 4,
71691
+ maxBoundaryPerOp: options.maxBoundaryPerOp ?? 4
71692
+ };
71693
+ const doc2 = spec ?? {};
71694
+ const out = [];
71695
+ for (const [path, item] of Object.entries(doc2.paths ?? {})) {
71696
+ if (!item || typeof item !== "object") continue;
71697
+ for (const [method, op] of Object.entries(item)) {
71698
+ if (!HTTP_METHODS$1.includes(method.toLowerCase())) continue;
71699
+ if (!op || typeof op !== "object") continue;
71700
+ if (opts.only.size && !opts.only.has(`${method.toUpperCase()} ${path}`)) continue;
71701
+ out.push(...generateForOperation(doc2, method.toUpperCase(), path, item, op, opts));
71702
+ }
71703
+ }
71704
+ return out;
71705
+ }
71706
+ function testUrl(test, baseVar = "{{baseUrl}}") {
71707
+ let path = test.path;
71708
+ for (const [name2, value] of Object.entries(test.pathParams)) {
71709
+ path = path.replace(new RegExp(`\\{${name2}\\}`, "g"), encodeURIComponent(String(value)));
71710
+ }
71711
+ const qs = test.query.filter((q) => q.key).map((q) => `${encodeURIComponent(q.key)}=${encodeURIComponent(q.value)}`).join("&");
71712
+ return `${baseVar}${path}${qs ? `?${qs}` : ""}`;
71713
+ }
71714
+ function toApiRequest(test, id2) {
71715
+ const headers = test.headers.map((h) => ({ key: h.key, value: h.value, enabled: true }));
71716
+ return {
71717
+ id: id2,
71718
+ name: test.name,
71719
+ method: test.method,
71720
+ url: testUrl(test),
71721
+ headers,
71722
+ params: [],
71723
+ auth: { type: "none" },
71724
+ body: test.body !== void 0 ? { mode: "json", json: test.body } : { mode: "none" },
71725
+ contract: {
71726
+ statusCode: test.expectedStatus,
71727
+ ...test.responseSchema ? { bodySchema: test.responseSchema } : {}
71728
+ },
71729
+ meta: { tags: [test.category] }
71730
+ };
71731
+ }
71732
+ const { electron: electron$9 } = window;
71733
+ function Bar({ pct }) {
71734
+ const color = pct >= 80 ? "bg-emerald-500" : pct >= 50 ? "bg-amber-500" : "bg-red-500";
71735
+ return /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "h-2 w-full rounded bg-surface-800 overflow-hidden", children: /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: `h-full ${color}`, style: { width: `${pct}%` } }) });
71736
+ }
71737
+ function Stat({ value, label }) {
71738
+ return /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex-1", children: [
71739
+ /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "text-2xl font-bold text-white", children: value }),
71740
+ /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "text-[11px] text-surface-400", children: label })
71741
+ ] });
71742
+ }
71743
+ function CoverageModal() {
71744
+ const open = useStore((s) => s.coverageOpen);
71745
+ const setOpen = useStore((s) => s.setCoverageOpen);
71746
+ const collections = useStore((s) => s.collections);
71747
+ const history2 = useStore((s) => s.history);
71748
+ const savedSpec = useStore((s) => s.workspace?.settings?.coverageSpec);
71749
+ const setCoverageSpec = useStore((s) => s.setCoverageSpec);
71750
+ const addCollectionObject = useStore((s) => s.addCollectionObject);
71751
+ const [source, setSource] = reactExports.useState(savedSpec ?? "");
71752
+ const [pasted, setPasted] = reactExports.useState("");
71753
+ const [report, setReport] = reactExports.useState(null);
71754
+ const [spec, setSpec] = reactExports.useState(null);
71755
+ const [error2, setError] = reactExports.useState(null);
71756
+ const [busy, setBusy] = reactExports.useState(false);
71757
+ const [onlyGaps, setOnlyGaps] = reactExports.useState(false);
71758
+ const [generated, setGenerated] = reactExports.useState(null);
71759
+ const requests = reactExports.useMemo(() => {
71760
+ const out = [];
71761
+ for (const entry of Object.values(collections)) {
71762
+ for (const req of Object.values(entry.data.requests)) {
71763
+ if (req.disabled) continue;
71764
+ out.push({ name: `${entry.data.name} / ${req.name}`, method: req.method, url: req.url, expectedStatus: req.contract?.statusCode });
71765
+ }
71766
+ }
71767
+ return out;
71768
+ }, [collections]);
71769
+ const observations = reactExports.useMemo(() => {
71770
+ return history2.map((h) => {
71771
+ let responsePaths;
71772
+ try {
71773
+ if (h.response?.body) responsePaths = flattenValuePaths(JSON.parse(h.response.body));
71774
+ } catch {
71775
+ }
71776
+ return { method: h.request.method, url: h.resolvedUrl, status: h.response?.status ?? 0, responsePaths };
71777
+ });
71778
+ }, [history2]);
71779
+ async function compute() {
71780
+ setBusy(true);
71781
+ setError(null);
71782
+ try {
71783
+ const isUrl = /^https?:\/\//i.test(source.trim());
71784
+ const spec2 = await electron$9.coverageLoadSpec(
71785
+ pasted.trim() ? { text: pasted } : isUrl ? { url: source.trim() } : { path: source.trim() }
71786
+ );
71787
+ if (source.trim() && !pasted.trim()) setCoverageSpec(source.trim());
71788
+ setSpec(spec2);
71789
+ setReport(computeCoverage(spec2, requests, observations));
71790
+ setGenerated(null);
71791
+ } catch (err) {
71792
+ setError(err instanceof Error ? err.message : String(err));
71793
+ setReport(null);
71794
+ } finally {
71795
+ setBusy(false);
71796
+ }
71797
+ }
71798
+ function generateForGaps() {
71799
+ if (!spec || !report) return;
71800
+ const only = new Set(report.operations.filter((o) => !o.tested).map((o) => `${o.method} ${o.path}`));
71801
+ if (only.size === 0) return;
71802
+ const tests = generateTests(spec, { only });
71803
+ const requestIds = [];
71804
+ const requestMap = {};
71805
+ for (const t22 of tests) {
71806
+ const id2 = crypto.randomUUID();
71807
+ requestMap[id2] = toApiRequest(t22, id2);
71808
+ requestIds.push(id2);
71809
+ }
71810
+ const name2 = `${report.spec.title ?? "API"} tests (generated)`;
71811
+ const collection = {
71812
+ version: "1.0",
71813
+ id: crypto.randomUUID(),
71814
+ name: name2,
71815
+ description: "Generated for untested operations from the OpenAPI spec.",
71816
+ rootFolder: { id: crypto.randomUUID(), name: "root", description: "", folders: [], requestIds },
71817
+ requests: requestMap
71818
+ };
71819
+ addCollectionObject(collection);
71820
+ setGenerated(`Added ${tests.length} tests for ${only.size} untested operations as "${collection.name}".`);
71821
+ }
71822
+ if (!open) return null;
71823
+ const t2 = report?.totals;
71824
+ const shownOps = report?.operations.filter((o) => !onlyGaps || !o.tested || !o.hasNegativeTest) ?? [];
71825
+ return /* @__PURE__ */ jsxRuntimeExports.jsxs(
71826
+ Modal,
71827
+ {
71828
+ onClose: () => setOpen(false),
71829
+ overlayClassName: "bg-black/50 z-50 flex items-start justify-center pt-16",
71830
+ panelClassName: "bg-surface-900 border border-surface-800 rounded-lg shadow-2xl flex flex-col w-[760px] max-h-[82vh]",
71831
+ title: "API test coverage",
71832
+ subtitle: "How much of an OpenAPI contract this workspace tests",
71833
+ children: [
71834
+ /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "px-4 py-3 border-b border-surface-800 flex flex-col gap-2 flex-shrink-0", children: [
71835
+ /* @__PURE__ */ jsxRuntimeExports.jsx("label", { className: "text-[11px] text-surface-400", children: "OpenAPI spec (file path or URL)" }),
71836
+ /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex gap-2", children: [
71837
+ /* @__PURE__ */ jsxRuntimeExports.jsx(
71838
+ "input",
71839
+ {
71840
+ value: source,
71841
+ onChange: (e) => setSource(e.target.value),
71842
+ placeholder: "./openapi.yaml or https://api.example.com/openapi.json",
71843
+ className: "flex-1 bg-surface-800 border border-surface-700 rounded px-3 py-1.5 text-sm font-mono focus:outline-none focus:border-blue-500"
71844
+ }
71845
+ ),
71846
+ /* @__PURE__ */ jsxRuntimeExports.jsx(
71847
+ "button",
71848
+ {
71849
+ onClick: compute,
71850
+ disabled: busy || !source.trim() && !pasted.trim(),
71851
+ className: "px-4 py-1.5 bg-blue-600 hover:bg-blue-500 disabled:bg-surface-800 disabled:text-surface-500 rounded text-sm font-medium",
71852
+ children: busy ? "…" : "Measure"
71853
+ }
71854
+ )
71855
+ ] }),
71856
+ /* @__PURE__ */ jsxRuntimeExports.jsxs("details", { className: "text-[11px] text-surface-500", children: [
71857
+ /* @__PURE__ */ jsxRuntimeExports.jsx("summary", { className: "cursor-pointer hover:text-surface-300", children: "or paste spec" }),
71858
+ /* @__PURE__ */ jsxRuntimeExports.jsx(
71859
+ "textarea",
71860
+ {
71861
+ value: pasted,
71862
+ onChange: (e) => setPasted(e.target.value),
71863
+ rows: 4,
71864
+ placeholder: "Paste OpenAPI JSON or YAML here",
71865
+ className: "mt-1 w-full resize-y bg-surface-950 border border-surface-800 rounded px-3 py-2 text-xs font-mono focus:outline-none focus:border-blue-500"
71866
+ }
71867
+ )
71868
+ ] }),
71869
+ error2 && /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "text-xs text-red-400", children: error2 })
71870
+ ] }),
71871
+ /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "px-4 py-3 flex-1 overflow-y-auto min-h-0", children: !report ? /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "text-sm text-surface-500 text-center py-10", children: [
71872
+ "Point at your OpenAPI spec and choose ",
71873
+ /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "text-surface-300", children: "Measure" }),
71874
+ " to see which operations are tested."
71875
+ ] }) : /* @__PURE__ */ jsxRuntimeExports.jsxs(jsxRuntimeExports.Fragment, { children: [
71876
+ /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "mb-3", children: [
71877
+ /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex justify-between text-xs mb-1", children: [
71878
+ /* @__PURE__ */ jsxRuntimeExports.jsxs("span", { className: "text-surface-300", children: [
71879
+ report.spec.title ?? "API",
71880
+ report.spec.version ? ` v${report.spec.version}` : ""
71881
+ ] }),
71882
+ /* @__PURE__ */ jsxRuntimeExports.jsxs("span", { className: "text-surface-400", children: [
71883
+ t2.operationPct,
71884
+ "% operations tested"
71885
+ ] })
71886
+ ] }),
71887
+ /* @__PURE__ */ jsxRuntimeExports.jsx(Bar, { pct: t2.operationPct })
71888
+ ] }),
71889
+ /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex gap-4 mb-4 border-b border-surface-800 pb-3", children: [
71890
+ /* @__PURE__ */ jsxRuntimeExports.jsx(Stat, { value: `${t2.tested}/${t2.operations}`, label: "operations tested" }),
71891
+ /* @__PURE__ */ jsxRuntimeExports.jsx(Stat, { value: `${t2.coveredStatuses}/${t2.declaredStatuses}`, label: "response codes covered" }),
71892
+ t2.declaredProperties > 0 && /* @__PURE__ */ jsxRuntimeExports.jsx(Stat, { value: `${t2.propertyPct}%`, label: "response shape seen in runs" }),
71893
+ /* @__PURE__ */ jsxRuntimeExports.jsx(Stat, { value: /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: t2.untested ? "text-amber-400" : "text-emerald-400", children: t2.untested }), label: "never tested" }),
71894
+ /* @__PURE__ */ jsxRuntimeExports.jsx(Stat, { value: /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: t2.withoutNegativeTest ? "text-amber-400" : "text-emerald-400", children: t2.withoutNegativeTest }), label: "no negative test" })
71895
+ ] }),
71896
+ /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-center justify-between mb-2 gap-3", children: [
71897
+ /* @__PURE__ */ jsxRuntimeExports.jsxs("label", { className: "flex items-center gap-2 text-xs text-surface-400", children: [
71898
+ /* @__PURE__ */ jsxRuntimeExports.jsx("input", { type: "checkbox", checked: onlyGaps, onChange: (e) => setOnlyGaps(e.target.checked) }),
71899
+ "Show only gaps (untested or missing a negative test)"
71900
+ ] }),
71901
+ t2.untested > 0 && /* @__PURE__ */ jsxRuntimeExports.jsxs(
71902
+ "button",
71903
+ {
71904
+ onClick: generateForGaps,
71905
+ title: "Generate happy-path, negative, and boundary tests for the untested operations",
71906
+ className: "px-3 py-1 text-xs bg-violet-600 hover:bg-violet-500 rounded font-medium shrink-0",
71907
+ children: [
71908
+ "Generate tests for ",
71909
+ t2.untested,
71910
+ " gap",
71911
+ t2.untested !== 1 ? "s" : ""
71912
+ ]
71913
+ }
71914
+ )
71915
+ ] }),
71916
+ generated && /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "mb-2 px-3 py-1.5 rounded bg-emerald-900/20 border border-emerald-800/40 text-xs text-emerald-300", children: [
71917
+ generated,
71918
+ " Re-measure to see the coverage rise."
71919
+ ] }),
71920
+ /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "flex flex-col gap-0.5", children: shownOps.map((op, i) => /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-center gap-2 text-xs py-1 border-b border-surface-800/50", children: [
71921
+ /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: op.tested ? "text-emerald-400" : "text-red-400", children: op.tested ? "✓" : "✗" }),
71922
+ /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "font-mono font-bold text-surface-300 w-14 shrink-0", children: op.method }),
71923
+ /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: `font-mono flex-1 ${op.tested ? "text-white" : "text-surface-500"}`, children: op.path }),
71924
+ op.declaredStatuses.length > 0 && /* @__PURE__ */ jsxRuntimeExports.jsxs("span", { className: "text-surface-500 shrink-0", children: [
71925
+ op.coveredStatuses.length,
71926
+ "/",
71927
+ op.declaredStatuses.length,
71928
+ " codes"
71929
+ ] }),
71930
+ op.tested && !op.hasNegativeTest && /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "text-amber-400 shrink-0", children: "no negative" })
71931
+ ] }, `${op.method}-${op.path}-${i}`)) })
71932
+ ] }) })
71933
+ ]
71934
+ }
71935
+ );
71936
+ }
71937
+ const HTTP_METHODS = ["get", "put", "post", "delete", "options", "head", "patch", "trace"];
71938
+ function resolveRef(spec, ref2) {
71939
+ const parts = ref2.replace(/^#\//, "").split("/");
71940
+ return parts.reduce((o, k) => o?.[decodeURIComponent(k.replace(/~1/g, "/").replace(/~0/g, "~"))], spec);
71941
+ }
71942
+ function deref(spec, node, depth = 0, seen = /* @__PURE__ */ new Set()) {
71943
+ if (!node || typeof node !== "object" || depth > 8) return node;
71944
+ if (Array.isArray(node)) return node.map((n) => deref(spec, n, depth + 1, seen));
71945
+ if ("$ref" in node) {
71946
+ const target = resolveRef(spec, node.$ref);
71947
+ if (!target || seen.has(target)) return {};
71948
+ return deref(spec, target, depth + 1, /* @__PURE__ */ new Set([...seen, target]));
71949
+ }
71950
+ const out = {};
71951
+ for (const [k, v] of Object.entries(node)) out[k] = deref(spec, v, depth + 1, seen);
71952
+ return out;
71953
+ }
71954
+ function walk(schema, prefix2, depth, types2, required2) {
71955
+ if (!schema || typeof schema !== "object" || depth > 8) return;
71956
+ const type2 = Array.isArray(schema.type) ? schema.type[0] : schema.type;
71957
+ if (type2 === "object" || schema.properties) {
71958
+ const req = schema.required ?? [];
71959
+ for (const [name2, sub] of Object.entries(schema.properties ?? {})) {
71960
+ const p2 = prefix2 ? `${prefix2}.${name2}` : name2;
71961
+ const subType = (Array.isArray(sub?.type) ? sub.type[0] : sub?.type) ?? (sub?.properties ? "object" : "any");
71962
+ types2.set(p2, subType);
71963
+ if (req.includes(name2)) required2.add(p2);
71964
+ walk(sub, p2, depth + 1, types2, required2);
71965
+ }
71966
+ } else if (type2 === "array" && schema.items) {
71967
+ walk(schema.items, `${prefix2}[]`, depth + 1, types2, required2);
71968
+ }
71969
+ }
71970
+ function schemaInfo(spec, schema) {
71971
+ const types2 = /* @__PURE__ */ new Map();
71972
+ const required2 = /* @__PURE__ */ new Set();
71973
+ if (schema) walk(deref(spec, schema), "", 0, types2, required2);
71974
+ return { types: types2, required: required2 };
71975
+ }
71976
+ function requestSchema(spec, op) {
71977
+ return deref(spec, op?.requestBody)?.content?.["application/json"]?.schema;
71978
+ }
71979
+ function successResponseSchema(spec, op) {
71980
+ const responses = op?.responses ?? {};
71981
+ const code2 = Object.keys(responses).filter((c) => /^2\d\d$/.test(c)).sort()[0];
71982
+ return code2 ? deref(spec, responses[code2])?.content?.["application/json"]?.schema : void 0;
71983
+ }
71984
+ function paramRequired(spec, pathItem, op) {
71985
+ const raw = [...pathItem?.parameters ?? [], ...op?.parameters ?? []].map((p2) => deref(spec, p2));
71986
+ const m = /* @__PURE__ */ new Map();
71987
+ for (const p2 of raw) if (p2?.name && p2?.in) m.set(`${p2.in}:${p2.name}`, !!p2.required);
71988
+ return m;
71989
+ }
71990
+ function operations(spec) {
71991
+ const map = /* @__PURE__ */ new Map();
71992
+ for (const [path, item] of Object.entries(spec?.paths ?? {})) {
71993
+ if (!item || typeof item !== "object") continue;
71994
+ for (const [method, op] of Object.entries(item)) {
71995
+ if (!HTTP_METHODS.includes(method.toLowerCase())) continue;
71996
+ map.set(`${method.toUpperCase()} ${path}`, { method: method.toUpperCase(), path, pathItem: item, op });
71997
+ }
71998
+ }
71999
+ return map;
72000
+ }
72001
+ function diffSpecs(oldSpec, newSpec) {
72002
+ const oldOps = operations(oldSpec);
72003
+ const newOps = operations(newSpec);
72004
+ const changes = [];
72005
+ for (const [key, o] of oldOps) {
72006
+ if (!newOps.has(key)) {
72007
+ changes.push({ kind: "operation-removed", breaking: true, method: o.method, path: o.path, detail: `Operation ${key} was removed` });
72008
+ }
72009
+ }
72010
+ for (const [key, n] of newOps) {
72011
+ if (!oldOps.has(key)) {
72012
+ changes.push({ kind: "operation-added", breaking: false, method: n.method, path: n.path, detail: `Operation ${key} was added` });
72013
+ continue;
72014
+ }
72015
+ const o = oldOps.get(key);
72016
+ const label = `${n.method} ${n.path}`;
72017
+ const oReq = schemaInfo(oldSpec, requestSchema(oldSpec, o.op));
72018
+ const nReq = schemaInfo(newSpec, requestSchema(newSpec, n.op));
72019
+ for (const p2 of nReq.required) {
72020
+ if (!oReq.required.has(p2)) {
72021
+ changes.push({ kind: "request-required-added", breaking: true, method: n.method, path: n.path, detail: `${label}: request field "${p2}" is now required` });
72022
+ }
72023
+ }
72024
+ for (const [p2, t2] of nReq.types) {
72025
+ const ot = oReq.types.get(p2);
72026
+ if (ot && ot !== t2) {
72027
+ changes.push({ kind: "request-type-changed", breaking: true, method: n.method, path: n.path, detail: `${label}: request field "${p2}" type ${ot} -> ${t2}` });
72028
+ }
72029
+ }
72030
+ const oRes = schemaInfo(oldSpec, successResponseSchema(oldSpec, o.op));
72031
+ const nRes = schemaInfo(newSpec, successResponseSchema(newSpec, n.op));
72032
+ for (const [p2, t2] of oRes.types) {
72033
+ if (!nRes.types.has(p2)) {
72034
+ changes.push({ kind: "response-removed", breaking: true, method: n.method, path: n.path, detail: `${label}: response field "${p2}" was removed` });
72035
+ } else if (nRes.types.get(p2) !== t2) {
72036
+ changes.push({ kind: "response-type-changed", breaking: true, method: n.method, path: n.path, detail: `${label}: response field "${p2}" type ${t2} -> ${nRes.types.get(p2)}` });
72037
+ }
72038
+ }
72039
+ const oParams = paramRequired(oldSpec, o.pathItem, o.op);
72040
+ const nParams = paramRequired(newSpec, n.pathItem, n.op);
72041
+ for (const [k, req] of nParams) {
72042
+ if (req && !oParams.get(k)) {
72043
+ changes.push({ kind: "param-required-added", breaking: true, method: n.method, path: n.path, detail: `${label}: parameter "${k}" is now required` });
72044
+ }
72045
+ }
72046
+ const oCodes = Object.keys(o.op?.responses ?? {}).filter((c) => /^2\d\d$/.test(c));
72047
+ const nCodes = new Set(Object.keys(n.op?.responses ?? {}));
72048
+ for (const c of oCodes) {
72049
+ if (!nCodes.has(c)) {
72050
+ changes.push({ kind: "success-code-removed", breaking: true, method: n.method, path: n.path, detail: `${label}: success response ${c} was removed` });
72051
+ }
72052
+ }
72053
+ }
72054
+ return changes;
72055
+ }
72056
+ function summarizeDiff(changes) {
72057
+ return {
72058
+ breaking: changes.filter((c) => c.breaking).length,
72059
+ nonBreaking: changes.filter((c) => !c.breaking).length
72060
+ };
72061
+ }
72062
+ const { electron: electron$8 } = window;
72063
+ function SpecField({ label, value, onSource, onText }) {
72064
+ return /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex-1", children: [
72065
+ /* @__PURE__ */ jsxRuntimeExports.jsx("label", { className: "text-[11px] text-surface-400", children: label }),
72066
+ /* @__PURE__ */ jsxRuntimeExports.jsx(
72067
+ "input",
72068
+ {
72069
+ value: value.source,
72070
+ onChange: (e) => onSource(e.target.value),
72071
+ placeholder: "./openapi.yaml or URL",
72072
+ className: "mt-1 w-full bg-surface-800 border border-surface-700 rounded px-2 py-1.5 text-xs font-mono focus:outline-none focus:border-blue-500"
72073
+ }
72074
+ ),
72075
+ /* @__PURE__ */ jsxRuntimeExports.jsxs("details", { className: "text-[10px] text-surface-500 mt-1", children: [
72076
+ /* @__PURE__ */ jsxRuntimeExports.jsx("summary", { className: "cursor-pointer hover:text-surface-300", children: "or paste" }),
72077
+ /* @__PURE__ */ jsxRuntimeExports.jsx(
72078
+ "textarea",
72079
+ {
72080
+ value: value.text,
72081
+ onChange: (e) => onText(e.target.value),
72082
+ rows: 3,
72083
+ className: "mt-1 w-full resize-y bg-surface-950 border border-surface-800 rounded px-2 py-1.5 text-[11px] font-mono focus:outline-none focus:border-blue-500"
72084
+ }
72085
+ )
72086
+ ] })
72087
+ ] });
72088
+ }
72089
+ function CompareModal() {
72090
+ const open = useStore((s) => s.compareOpen);
72091
+ const setOpen = useStore((s) => s.setCompareOpen);
72092
+ const collections = useStore((s) => s.collections);
72093
+ const savedSpec = useStore((s) => s.workspace?.settings?.coverageSpec);
72094
+ const [oldIn, setOldIn] = reactExports.useState({ source: "", text: "" });
72095
+ const [newIn, setNewIn] = reactExports.useState({ source: savedSpec ?? "", text: "" });
72096
+ const [changes, setChanges] = reactExports.useState(null);
72097
+ const [error2, setError] = reactExports.useState(null);
72098
+ const [busy, setBusy] = reactExports.useState(false);
72099
+ const requests = reactExports.useMemo(() => {
72100
+ const out = [];
72101
+ for (const entry of Object.values(collections)) {
72102
+ for (const req of Object.values(entry.data.requests)) {
72103
+ if (req.disabled) continue;
72104
+ out.push({ name: `${entry.data.name} / ${req.name}`, method: req.method, url: req.url });
72105
+ }
72106
+ }
72107
+ return out;
72108
+ }, [collections]);
72109
+ const load = (i) => electron$8.coverageLoadSpec(i.text.trim() ? { text: i.text } : /^https?:\/\//i.test(i.source.trim()) ? { url: i.source.trim() } : { path: i.source.trim() });
72110
+ async function run() {
72111
+ setBusy(true);
72112
+ setError(null);
72113
+ try {
72114
+ const [oldSpec, newSpec] = await Promise.all([load(oldIn), load(newIn)]);
72115
+ setChanges(diffSpecs(oldSpec, newSpec));
72116
+ } catch (err) {
72117
+ setError(err instanceof Error ? err.message : String(err));
72118
+ setChanges(null);
72119
+ } finally {
72120
+ setBusy(false);
72121
+ }
72122
+ }
72123
+ const impact = reactExports.useMemo(() => {
72124
+ if (!changes) return null;
72125
+ const affected = changes.filter((c) => c.breaking && c.method).map((change) => ({
72126
+ change,
72127
+ tests: requests.filter((r) => r.method.toUpperCase() === change.method && pathMatches(r.url, change.path)).map((r) => r.name)
72128
+ }));
72129
+ const hitTests = new Set(affected.flatMap((a) => a.tests));
72130
+ return { affected, hitTests: hitTests.size };
72131
+ }, [changes, requests]);
72132
+ if (!open) return null;
72133
+ const sum = changes ? summarizeDiff(changes) : null;
72134
+ const canRun = (oldIn.source.trim() || oldIn.text.trim()) && (newIn.source.trim() || newIn.text.trim());
72135
+ return /* @__PURE__ */ jsxRuntimeExports.jsxs(
72136
+ Modal,
72137
+ {
72138
+ onClose: () => setOpen(false),
72139
+ overlayClassName: "bg-black/50 z-50 flex items-start justify-center pt-16",
72140
+ panelClassName: "bg-surface-900 border border-surface-800 rounded-lg shadow-2xl flex flex-col w-[760px] max-h-[82vh]",
72141
+ title: "Compare API specs",
72142
+ subtitle: "Breaking-change detection and impact on your tests",
72143
+ children: [
72144
+ /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "px-4 py-3 border-b border-surface-800 flex-shrink-0", children: [
72145
+ /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex gap-3 items-start", children: [
72146
+ /* @__PURE__ */ jsxRuntimeExports.jsx(SpecField, { label: "Baseline (old)", value: oldIn, onSource: (s) => setOldIn({ ...oldIn, source: s }), onText: (t2) => setOldIn({ ...oldIn, text: t2 }) }),
72147
+ /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "text-surface-500 pt-6", children: "->" }),
72148
+ /* @__PURE__ */ jsxRuntimeExports.jsx(SpecField, { label: "Candidate (new)", value: newIn, onSource: (s) => setNewIn({ ...newIn, source: s }), onText: (t2) => setNewIn({ ...newIn, text: t2 }) })
72149
+ ] }),
72150
+ /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "flex justify-end mt-2", children: /* @__PURE__ */ jsxRuntimeExports.jsx(
72151
+ "button",
72152
+ {
72153
+ onClick: run,
72154
+ disabled: busy || !canRun,
72155
+ className: "px-4 py-1.5 bg-blue-600 hover:bg-blue-500 disabled:bg-surface-800 disabled:text-surface-500 rounded text-sm font-medium",
72156
+ children: busy ? "…" : "Compare"
72157
+ }
72158
+ ) }),
72159
+ error2 && /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "text-xs text-red-400 mt-2", children: error2 })
72160
+ ] }),
72161
+ /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "px-4 py-3 flex-1 overflow-y-auto min-h-0 text-xs", children: !changes ? /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "text-surface-500 text-center py-10", children: [
72162
+ "Point at two spec versions and choose ",
72163
+ /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "text-surface-300", children: "Compare" }),
72164
+ "."
72165
+ ] }) : changes.length === 0 ? /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "text-emerald-400 text-center py-10", children: "No differences between the specs." }) : /* @__PURE__ */ jsxRuntimeExports.jsxs(jsxRuntimeExports.Fragment, { children: [
72166
+ sum.breaking > 0 && /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "mb-3", children: [
72167
+ /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "text-red-400 font-bold uppercase tracking-wide text-[11px] mb-1", children: "Breaking changes" }),
72168
+ changes.filter((c) => c.breaking).map((c, i) => /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex gap-2 text-surface-300 py-0.5", children: [
72169
+ /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "text-red-400", children: "✗" }),
72170
+ c.detail
72171
+ ] }, i))
72172
+ ] }),
72173
+ sum.nonBreaking > 0 && /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "mb-3", children: [
72174
+ /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "text-emerald-400 font-bold uppercase tracking-wide text-[11px] mb-1", children: "Non-breaking" }),
72175
+ changes.filter((c) => !c.breaking).map((c, i) => /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex gap-2 text-surface-400 py-0.5", children: [
72176
+ /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "text-emerald-400", children: "✓" }),
72177
+ c.detail
72178
+ ] }, i))
72179
+ ] }),
72180
+ /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "mt-3 pt-3 border-t border-surface-800", children: sum.breaking === 0 ? /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "text-emerald-400 font-medium", children: "No breaking changes. Safe to deploy." }) : impact.hitTests === 0 ? /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "text-amber-400", children: [
72181
+ sum.breaking,
72182
+ " breaking change",
72183
+ sum.breaking !== 1 ? "s" : "",
72184
+ ", but no test in this workspace exercises the affected operations. Add tests, then re-check."
72185
+ ] }) : /* @__PURE__ */ jsxRuntimeExports.jsxs(jsxRuntimeExports.Fragment, { children: [
72186
+ impact.affected.filter((a) => a.tests.length).map((a, i) => /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "mb-1", children: [
72187
+ /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "text-amber-300", children: a.change.detail }),
72188
+ a.tests.map((t2) => /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "text-surface-500 pl-4", children: [
72189
+ "- ",
72190
+ t2
72191
+ ] }, t2))
72192
+ ] }, i)),
72193
+ /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "mt-2 text-red-400 font-bold", children: [
72194
+ "Block deployment: ",
72195
+ sum.breaking,
72196
+ " breaking change",
72197
+ sum.breaking !== 1 ? "s" : "",
72198
+ " affect ",
72199
+ impact.hitTests,
72200
+ " test",
72201
+ impact.hitTests !== 1 ? "s" : "",
72202
+ "."
72203
+ ] })
72204
+ ] }) })
72205
+ ] }) })
72206
+ ]
72207
+ }
72208
+ );
72209
+ }
70826
72210
  function CollectionPanel() {
70827
72211
  const activeCollectionId = useStore((s) => s.activeCollectionId);
70828
72212
  const collections = useStore((s) => s.collections);
70829
72213
  const updateCollectionDataSet = useStore((s) => s.updateCollectionDataSet);
70830
72214
  const openRunner = useStore((s) => s.openRunner);
72215
+ const setCoverageOpen = useStore((s) => s.setCoverageOpen);
72216
+ const setCompareOpen = useStore((s) => s.setCompareOpen);
70831
72217
  const [activeTab, setActiveTab] = reactExports.useState("data");
70832
72218
  if (!activeCollectionId) {
70833
72219
  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" });
@@ -70849,17 +72235,37 @@ function CollectionPanel() {
70849
72235
  iterCount > 0 ? ` · ${iterCount} data row${iterCount !== 1 ? "s" : ""}` : ""
70850
72236
  ] })
70851
72237
  ] }),
70852
- /* @__PURE__ */ jsxRuntimeExports.jsxs(
70853
- "button",
70854
- {
70855
- onClick: () => openRunner(activeCollectionId),
70856
- className: "px-3 py-1.5 text-xs bg-emerald-700 hover:bg-emerald-600 rounded font-medium transition-colors flex items-center gap-1.5",
70857
- children: [
70858
- /* @__PURE__ */ jsxRuntimeExports.jsx("svg", { className: "w-3.5 h-3.5", viewBox: "0 0 20 20", fill: "currentColor", children: /* @__PURE__ */ jsxRuntimeExports.jsx("path", { fillRule: "evenodd", d: "M10 18a8 8 0 100-16 8 8 0 000 16zM9.555 7.168A1 1 0 008 8v4a1 1 0 001.555.832l3-2a1 1 0 000-1.664l-3-2z", clipRule: "evenodd" }) }),
70859
- "Run collection"
70860
- ]
70861
- }
70862
- )
72238
+ /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-center gap-2", children: [
72239
+ /* @__PURE__ */ jsxRuntimeExports.jsx(
72240
+ "button",
72241
+ {
72242
+ onClick: () => setCoverageOpen(true),
72243
+ title: "Measure how much of an OpenAPI spec this workspace tests",
72244
+ className: "px-3 py-1.5 text-xs border border-surface-700 text-surface-300 hover:text-white hover:border-surface-500 rounded font-medium transition-colors",
72245
+ children: "Coverage"
72246
+ }
72247
+ ),
72248
+ /* @__PURE__ */ jsxRuntimeExports.jsx(
72249
+ "button",
72250
+ {
72251
+ onClick: () => setCompareOpen(true),
72252
+ title: "Diff two OpenAPI versions: breaking changes and which tests they affect",
72253
+ className: "px-3 py-1.5 text-xs border border-surface-700 text-surface-300 hover:text-white hover:border-surface-500 rounded font-medium transition-colors",
72254
+ children: "Compare"
72255
+ }
72256
+ ),
72257
+ /* @__PURE__ */ jsxRuntimeExports.jsxs(
72258
+ "button",
72259
+ {
72260
+ onClick: () => openRunner(activeCollectionId),
72261
+ className: "px-3 py-1.5 text-xs bg-emerald-700 hover:bg-emerald-600 rounded font-medium transition-colors flex items-center gap-1.5",
72262
+ children: [
72263
+ /* @__PURE__ */ jsxRuntimeExports.jsx("svg", { className: "w-3.5 h-3.5", viewBox: "0 0 20 20", fill: "currentColor", children: /* @__PURE__ */ jsxRuntimeExports.jsx("path", { fillRule: "evenodd", d: "M10 18a8 8 0 100-16 8 8 0 000 16zM9.555 7.168A1 1 0 008 8v4a1 1 0 001.555.832l3-2a1 1 0 000-1.664l-3-2z", clipRule: "evenodd" }) }),
72264
+ "Run collection"
72265
+ ]
72266
+ }
72267
+ )
72268
+ ] })
70863
72269
  ] }),
70864
72270
  /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "flex border-b border-surface-800 px-6 flex-shrink-0", children: [
70865
72271
  { id: "data", label: "Data", badge: iterCount > 0 ? iterCount : 0 },
@@ -73276,18 +74682,70 @@ function detectPlatform(remotes) {
73276
74682
  return "unknown";
73277
74683
  }
73278
74684
  const NODE_LTS = "lts/*";
73279
- function generateCiContent(platform, envName, tags2, secretVars) {
74685
+ const PROVIDER_ENV = {
74686
+ vault: ["VAULT_ADDR", "VAULT_ROLE_ID", "VAULT_SECRET_ID"],
74687
+ aws: ["AWS_REGION", "AWS_ACCESS_KEY_ID", "AWS_SECRET_ACCESS_KEY"],
74688
+ azure: ["AZURE_TENANT_ID", "AZURE_CLIENT_ID", "AZURE_CLIENT_SECRET"],
74689
+ op: ["OP_CONNECT_HOST", "OP_CONNECT_TOKEN"]
74690
+ };
74691
+ function secretManagerOf(ref2) {
74692
+ if (ref2.startsWith("op://")) return "op";
74693
+ const scheme = ref2.split(":", 1)[0];
74694
+ if (scheme === "vault" || scheme === "aws" || scheme === "azure") return scheme;
74695
+ return null;
74696
+ }
74697
+ const INLINE_REF_RE = /\{\{\s*(vault:|aws:|azure:|op:\/\/)/g;
74698
+ function inlineSecretManagers(text) {
74699
+ if (!text) return [];
74700
+ const kinds = /* @__PURE__ */ new Set();
74701
+ for (const m of text.matchAll(INLINE_REF_RE)) {
74702
+ kinds.add(m[1] === "op://" ? "op" : m[1].slice(0, -1));
74703
+ }
74704
+ return [...kinds];
74705
+ }
74706
+ function requestSecretManagers(req) {
74707
+ return inlineSecretManagers(JSON.stringify(req));
74708
+ }
74709
+ function providerEnvVars(managers) {
74710
+ return [...new Set(managers.flatMap((k) => PROVIDER_ENV[k]))];
74711
+ }
74712
+ function generateCiContent(platform, envName, tags2, secretVars, secretManagers = []) {
73280
74713
  const runCmd = [
73281
74714
  "api-spector run --workspace .",
73282
74715
  envName ? `--environment "${envName}"` : "",
73283
74716
  tags2 ? `--tags "${tags2}"` : "",
73284
74717
  "--output results.html"
73285
74718
  ].filter(Boolean).join(" ");
73286
- const allSecretVars = secretVars.length ? ["API_SPECTOR_MASTER_KEY", ...secretVars] : [];
74719
+ const managers = [...new Set(secretManagers)];
74720
+ const usesVault = managers.includes("vault");
74721
+ const encryptedVars = secretVars.length ? ["API_SPECTOR_MASTER_KEY", ...secretVars] : [];
73287
74722
  if (platform === "github") {
73288
- const secretHint = allSecretVars.length ? ` # Add these secrets in: Settings → Secrets and variables → Actions
73289
- ` + allSecretVars.map((v) => ` # ${v}`).join("\n") + "\n" : "";
73290
- const envBlock = allSecretVars.length ? "\n env:\n" + allSecretVars.map((v) => ` ${v}: \${{ secrets.${v} }}`).join("\n") : "";
74723
+ const ghProviderEnv = providerEnvVars(managers).filter((v) => v !== "VAULT_ROLE_ID" && v !== "VAULT_SECRET_ID");
74724
+ const allSecretVars = [...encryptedVars, ...ghProviderEnv.filter((v) => v !== "VAULT_ADDR")];
74725
+ const hintLines = [
74726
+ ...allSecretVars.map((v) => ` # ${v}`),
74727
+ ...usesVault ? [" # VAULT_ADDR (as a repository variable, not a secret)"] : []
74728
+ ];
74729
+ const secretHint = hintLines.length ? ` # ⚠ Add these in: Settings → Secrets and variables → Actions
74730
+ ${hintLines.join("\n")}
74731
+ ` : "";
74732
+ const vaultStep = usesVault ? ` - name: Authenticate to Vault (OIDC)
74733
+ uses: hashicorp/vault-action@v3
74734
+ with:
74735
+ url: \${{ vars.VAULT_ADDR }}
74736
+ method: jwt
74737
+ role: apispector-ci # your Vault JWT/OIDC role
74738
+ exportToken: true # sets VAULT_TOKEN for later steps
74739
+ ` : "";
74740
+ const permissions = usesVault ? ` permissions:
74741
+ contents: read
74742
+ id-token: write # required for Vault OIDC
74743
+ ` : "";
74744
+ const envEntries = [
74745
+ ...allSecretVars.map((v) => ` ${v}: \${{ secrets.${v} }}`),
74746
+ ...usesVault ? [" VAULT_ADDR: ${{ vars.VAULT_ADDR }}"] : []
74747
+ ];
74748
+ const envBlock = envEntries.length ? "\n env:\n" + envEntries.join("\n") : "";
73291
74749
  return `name: API Tests
73292
74750
 
73293
74751
  on:
@@ -73298,13 +74756,13 @@ on:
73298
74756
  jobs:
73299
74757
  api-tests:
73300
74758
  runs-on: ubuntu-latest
73301
- steps:
74759
+ ${permissions} steps:
73302
74760
  - uses: actions/checkout@v4
73303
74761
  - uses: actions/setup-node@v4
73304
74762
  with:
73305
74763
  node-version: '${NODE_LTS}'
73306
74764
  - run: npm install -g @testsmith/api-spector
73307
- ${secretHint} - name: Run API tests
74765
+ ${vaultStep}${secretHint} - name: Run API tests
73308
74766
  run: ${runCmd}${envBlock}
73309
74767
  - name: Upload test results
73310
74768
  if: always()
@@ -73315,6 +74773,7 @@ ${secretHint} - name: Run API tests
73315
74773
  `;
73316
74774
  }
73317
74775
  if (platform === "gitlab") {
74776
+ const allSecretVars = [...encryptedVars, ...providerEnvVars(managers)];
73318
74777
  const secretHint = allSecretVars.length ? ` # ⚠ Add these in: Settings → CI/CD → Variables
73319
74778
  ` + allSecretVars.map((v) => ` # ${v}`).join("\n") + "\n" : "";
73320
74779
  const envBlock = allSecretVars.length ? "\n variables:\n" + allSecretVars.map((v) => ` ${v}: $${v}`).join("\n") : "";
@@ -73333,6 +74792,7 @@ ${secretHint} script:
73333
74792
  `;
73334
74793
  }
73335
74794
  if (platform === "azure") {
74795
+ const allSecretVars = [...encryptedVars, ...providerEnvVars(managers)];
73336
74796
  const secretHint = allSecretVars.length ? ` # ⚠ Add these in: Pipelines → Library → Variable groups (mark as secret)
73337
74797
  ` + allSecretVars.map((v) => ` # ${v}`).join("\n") + "\n" : "";
73338
74798
  const envBlock = allSecretVars.length ? "\n env:\n" + allSecretVars.map((v) => ` ${v}: $(${v})`).join("\n") : "";
@@ -74026,6 +75486,7 @@ function BranchesTab({ onRefresh }) {
74026
75486
  function CiTab() {
74027
75487
  const environments = useStore((s) => s.environments);
74028
75488
  const envList = Object.values(environments);
75489
+ const collections = useStore((s) => s.collections);
74029
75490
  const [remotes, setRemotes] = reactExports.useState([]);
74030
75491
  const [platform, setPlatform] = reactExports.useState("unknown");
74031
75492
  const [envId, setEnvId] = reactExports.useState("");
@@ -74045,10 +75506,13 @@ function CiTab() {
74045
75506
  reactExports.useEffect(() => {
74046
75507
  const env = envList.find((e) => e.data.id === envId);
74047
75508
  const secretVars = env ? env.data.variables.filter((v) => v.secret && v.enabled).map((v) => v.key) : [];
75509
+ const envRefManagers = env ? env.data.variables.filter((v) => v.enabled && v.secretRef).map((v) => secretManagerOf(v.secretRef)).filter((k) => k !== null) : [];
75510
+ const inlineManagers = Object.values(collections).flatMap((c) => Object.values(c.data.requests).flatMap(requestSecretManagers));
75511
+ const secretManagers = [.../* @__PURE__ */ new Set([...envRefManagers, ...inlineManagers])];
74048
75512
  const envName = env?.data.name ?? "";
74049
- setPreview(generateCiContent(platform, envName, tags2, secretVars));
75513
+ setPreview(generateCiContent(platform, envName, tags2, secretVars, secretManagers));
74050
75514
  setWritten(false);
74051
- }, [platform, envId, tags2, environments]);
75515
+ }, [platform, envId, tags2, environments, collections]);
74052
75516
  async function write() {
74053
75517
  try {
74054
75518
  setError(null);
@@ -74633,6 +76097,8 @@ function App() {
74633
76097
  }
74634
76098
  return /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex flex-col h-screen bg-surface-950", style: { color: "var(--text-primary)" }, children: [
74635
76099
  /* @__PURE__ */ jsxRuntimeExports.jsx(RunnerModal, {}),
76100
+ /* @__PURE__ */ jsxRuntimeExports.jsx(CoverageModal, {}),
76101
+ /* @__PURE__ */ jsxRuntimeExports.jsx(CompareModal, {}),
74636
76102
  /* @__PURE__ */ jsxRuntimeExports.jsx(CommandPalette, {}),
74637
76103
  docsModalOpen && /* @__PURE__ */ jsxRuntimeExports.jsx(DocsGeneratorModal, { onClose: () => setDocsModalOpen(false) }),
74638
76104
  window.electron.platform !== "win32" && /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "drag-region flex-shrink-0 bg-surface-950 flex items-center justify-center", children: /* @__PURE__ */ jsxRuntimeExports.jsxs("span", { className: "no-drag text-[11px] font-medium tracking-widest select-none", style: { color: "var(--text-muted)" }, children: [
@@ -74641,7 +76107,7 @@ function App() {
74641
76107
  /* @__PURE__ */ jsxRuntimeExports.jsx("span", { style: { color: "#6aa3c8" }, children: "Spector" }),
74642
76108
  /* @__PURE__ */ jsxRuntimeExports.jsxs("span", { className: "ml-2 text-[10px] font-normal opacity-50", children: [
74643
76109
  "v",
74644
- "0.4.9"
76110
+ "0.5.1"
74645
76111
  ] }),
74646
76112
  /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "mx-2 opacity-30", children: "·" }),
74647
76113
  /* @__PURE__ */ jsxRuntimeExports.jsx(