@testsmith/api-spector 0.7.4 → 0.7.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,4 +1,4 @@
1
- const __vite__mapDeps=(i,m=__vite__mapDeps,d=(m.f||(m.f=["./GraphQLEditor-Ih9tb7XI.js","./codemirror-F6m3Hm1M.js","./ajv-2MAg6kGF.js","./SoapEditor-CwrFQmAx.js"])))=>i.map(i=>d[i]);
1
+ const __vite__mapDeps=(i,m=__vite__mapDeps,d=(m.f||(m.f=["./GraphQLEditor-CWJMEqny.js","./codemirror-F6m3Hm1M.js","./ajv-2MAg6kGF.js","./SoapEditor-C01AJQ38.js"])))=>i.map(i=>d[i]);
2
2
  import { r as reactExports, g as getDefaultExportFromCjs, R as React$4, a as autocompletion, h as hoverTooltip, j as jsxRuntimeExports, k as keymap, t as toggleComment, x as xml, b as json, c as ReactCodeMirror, o as oneDark, d as javascript } from "./codemirror-F6m3Hm1M.js";
3
3
  import { A as Ajv } from "./ajv-2MAg6kGF.js";
4
4
  var client = { exports: {} };
@@ -13883,7 +13883,8 @@ const STORAGE_KEYS = {
13883
13883
  runHooks: "runHooks",
13884
13884
  gitPullRebase: "apiSpector.git.pullRebase",
13885
13885
  ai: "apiSpector.ai",
13886
- updateSkipped: "apiSpectorUpdateSkipped"
13886
+ updateSkipped: "apiSpectorUpdateSkipped",
13887
+ collapsedCollections: "apiSpector.sidebar.collapsedCollections"
13887
13888
  };
13888
13889
  function envSlugRelPath(name) {
13889
13890
  return `environments/${name.toLowerCase().replaceAll(/[^a-z0-9]+/g, "-")}.env.json`;
@@ -24364,9 +24365,9 @@ function VarInput({ value: value2, onChange, className, wrapperClassName, static
24364
24365
  }
24365
24366
  if (staticSuggestions) {
24366
24367
  const q = val.toLowerCase();
24367
- const filtered2 = q ? staticSuggestions.filter((s) => s.toLowerCase().includes(q)).sort((a, b) => (a.toLowerCase().startsWith(q) ? 0 : 1) - (b.toLowerCase().startsWith(q) ? 0 : 1)).slice(0, 20) : staticSuggestions.slice(0, 20);
24368
+ const filtered = q ? staticSuggestions.filter((s) => s.toLowerCase().includes(q)).sort((a, b) => (a.toLowerCase().startsWith(q) ? 0 : 1) - (b.toLowerCase().startsWith(q) ? 0 : 1)).slice(0, 20) : staticSuggestions.slice(0, 20);
24368
24369
  partialLen.current = 0;
24369
- setSuggestions(filtered2.map((s) => ({ label: s, insert: s, type: "variable" })));
24370
+ setSuggestions(filtered.map((s) => ({ label: s, insert: s, type: "variable" })));
24370
24371
  setSuggestionMode("static");
24371
24372
  setActiveIndex(-1);
24372
24373
  return;
@@ -26810,6 +26811,30 @@ function ExampleRow({ name, indent, isActive, onOpen, onRename, onDelete, onDupl
26810
26811
  }
26811
26812
  );
26812
26813
  }
26814
+ function readSet() {
26815
+ try {
26816
+ const raw = localStorage.getItem(STORAGE_KEYS.collapsedCollections);
26817
+ const arr = raw ? JSON.parse(raw) : [];
26818
+ return new Set(Array.isArray(arr) ? arr.filter((x) => typeof x === "string") : []);
26819
+ } catch {
26820
+ return /* @__PURE__ */ new Set();
26821
+ }
26822
+ }
26823
+ function writeSet(set2) {
26824
+ try {
26825
+ localStorage.setItem(STORAGE_KEYS.collapsedCollections, JSON.stringify([...set2]));
26826
+ } catch {
26827
+ }
26828
+ }
26829
+ function isCollectionCollapsed(id2) {
26830
+ return readSet().has(id2);
26831
+ }
26832
+ function setCollectionCollapsed(id2, collapsed) {
26833
+ const set2 = readSet();
26834
+ if (collapsed) set2.add(id2);
26835
+ else set2.delete(id2);
26836
+ writeSet(set2);
26837
+ }
26813
26838
  function ConfirmDialog({ message, onConfirm, onCancel }) {
26814
26839
  const t = useT();
26815
26840
  return /* @__PURE__ */ jsxRuntimeExports.jsxs(
@@ -27038,6 +27063,7 @@ function CollectionTree() {
27038
27063
  }
27039
27064
  const [newRequestId, setNewRequestId] = reactExports.useState(null);
27040
27065
  const [dragging, setDragging] = reactExports.useState(null);
27066
+ const [colExpand, setColExpand] = reactExports.useState({ value: true, seq: 0 });
27041
27067
  function confirmThen(message, action) {
27042
27068
  setPendingConfirm({ message, onConfirm: () => {
27043
27069
  action();
@@ -27074,29 +27100,41 @@ function CollectionTree() {
27074
27100
  onCancel: () => setPendingConfirm(null)
27075
27101
  }
27076
27102
  ),
27077
- colList.length > 0 && /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "px-2 py-1.5 border-b border-surface-800 shrink-0", children: /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "relative", children: [
27103
+ colList.length > 0 && /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "px-2 py-1.5 border-b border-surface-800 shrink-0 flex items-center gap-1", children: [
27104
+ /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "relative flex-1 min-w-0", children: [
27105
+ /* @__PURE__ */ jsxRuntimeExports.jsx(
27106
+ "input",
27107
+ {
27108
+ value: query,
27109
+ onChange: (e) => setQuery(e.target.value),
27110
+ onKeyDown: (e) => {
27111
+ if (e.key === "Escape") setQuery("");
27112
+ },
27113
+ placeholder: t("Search requests…"),
27114
+ className: "w-full text-xs bg-surface-800 border border-surface-700 rounded pl-2 pr-6 py-1 focus:outline-none focus:border-blue-500 placeholder-surface-500"
27115
+ }
27116
+ ),
27117
+ query && /* @__PURE__ */ jsxRuntimeExports.jsx(
27118
+ "button",
27119
+ {
27120
+ onClick: () => setQuery(""),
27121
+ title: t("Clear (Esc)"),
27122
+ className: "absolute right-1 top-1/2 -translate-y-1/2 w-4 h-4 flex items-center justify-center text-surface-500 hover:text-surface-200 leading-none",
27123
+ children: "×"
27124
+ }
27125
+ )
27126
+ ] }),
27078
27127
  /* @__PURE__ */ jsxRuntimeExports.jsx(
27079
- "input",
27080
- {
27081
- value: query,
27082
- onChange: (e) => setQuery(e.target.value),
27083
- onKeyDown: (e) => {
27084
- if (e.key === "Escape") setQuery("");
27085
- },
27086
- placeholder: t("Search requests…"),
27087
- className: "w-full text-xs bg-surface-800 border border-surface-700 rounded pl-2 pr-6 py-1 focus:outline-none focus:border-blue-500 placeholder-surface-500"
27088
- }
27089
- ),
27090
- query && /* @__PURE__ */ jsxRuntimeExports.jsx(
27091
27128
  "button",
27092
27129
  {
27093
- onClick: () => setQuery(""),
27094
- title: t("Clear (Esc)"),
27095
- className: "absolute right-1 top-1/2 -translate-y-1/2 w-4 h-4 flex items-center justify-center text-surface-500 hover:text-surface-200 leading-none",
27096
- children: "×"
27130
+ onClick: () => setColExpand((c) => ({ value: !c.value, seq: c.seq + 1 })),
27131
+ title: colExpand.value ? t("Collapse all collections") : t("Expand all collections"),
27132
+ "aria-label": colExpand.value ? t("Collapse all collections") : t("Expand all collections"),
27133
+ className: "shrink-0 w-6 h-6 flex items-center justify-center rounded text-surface-400 hover:text-surface-100 hover:bg-surface-800 transition-colors",
27134
+ children: colExpand.value ? /* @__PURE__ */ jsxRuntimeExports.jsx(CollapseAllIcon, {}) : /* @__PURE__ */ jsxRuntimeExports.jsx(ExpandAllIcon, {})
27097
27135
  }
27098
27136
  )
27099
- ] }) }),
27137
+ ] }),
27100
27138
  /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "flex-1 overflow-y-auto", children: q ? matches.length === 0 ? /* @__PURE__ */ jsxRuntimeExports.jsx("p", { className: "px-3 py-3 text-xs text-surface-500", children: t("No requests match “:query”.", { query: query.trim() }) }) : /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "py-1", children: [
27101
27139
  /* @__PURE__ */ jsxRuntimeExports.jsx("p", { className: "px-3 pb-1 text-[10px] uppercase tracking-wider text-surface-500", children: t(":count match|:count matches", { count: matches.length }) }),
27102
27140
  matches.map(({ collectionId, collectionName, req, path }) => /* @__PURE__ */ jsxRuntimeExports.jsxs(
@@ -27121,6 +27159,7 @@ function CollectionTree() {
27121
27159
  col,
27122
27160
  isActive: col.id === activeCollectionId,
27123
27161
  activeRequestId,
27162
+ collapseSignal: colExpand,
27124
27163
  existingCollectionNames: colList.map((c) => c.data.name),
27125
27164
  onSelectCollection: () => {
27126
27165
  setActiveCollection(col.id);
@@ -27225,6 +27264,7 @@ function CollectionNode({
27225
27264
  col,
27226
27265
  isActive,
27227
27266
  activeRequestId,
27267
+ collapseSignal,
27228
27268
  existingCollectionNames,
27229
27269
  newRequestId,
27230
27270
  onSelectCollection,
@@ -27250,7 +27290,13 @@ function CollectionNode({
27250
27290
  onRunFolder
27251
27291
  }) {
27252
27292
  const t = useT();
27253
- const [expanded, setExpanded] = reactExports.useState(true);
27293
+ const [expanded, setExpanded] = reactExports.useState(() => !isCollectionCollapsed(col.id));
27294
+ reactExports.useEffect(() => {
27295
+ if (collapseSignal.seq > 0) setExpanded(collapseSignal.value);
27296
+ }, [collapseSignal.seq, collapseSignal.value]);
27297
+ reactExports.useEffect(() => {
27298
+ setCollectionCollapsed(col.id, !expanded);
27299
+ }, [col.id, expanded]);
27254
27300
  const [renaming, setRenaming] = reactExports.useState(false);
27255
27301
  const [showSettings, setShowSettings] = reactExports.useState(false);
27256
27302
  const [showSchemaSync, setShowSchemaSync] = reactExports.useState(false);
@@ -27830,8 +27876,8 @@ function jsonWithComments() {
27830
27876
  function xmlWithComments() {
27831
27877
  return [xml(), commentKeymap];
27832
27878
  }
27833
- const GraphQLEditor = reactExports.lazy(() => __vitePreload(() => import("./GraphQLEditor-Ih9tb7XI.js"), true ? __vite__mapDeps([0,1,2]) : void 0, import.meta.url).then((m) => ({ default: m.GraphQLEditor })));
27834
- const SoapEditor = reactExports.lazy(() => __vitePreload(() => import("./SoapEditor-CwrFQmAx.js"), true ? __vite__mapDeps([3,1,2]) : void 0, import.meta.url).then((m) => ({ default: m.SoapEditor })));
27879
+ const GraphQLEditor = reactExports.lazy(() => __vitePreload(() => import("./GraphQLEditor-CWJMEqny.js"), true ? __vite__mapDeps([0,1,2]) : void 0, import.meta.url).then((m) => ({ default: m.GraphQLEditor })));
27880
+ const SoapEditor = reactExports.lazy(() => __vitePreload(() => import("./SoapEditor-C01AJQ38.js"), true ? __vite__mapDeps([3,1,2]) : void 0, import.meta.url).then((m) => ({ default: m.SoapEditor })));
27835
27881
  function BodyTab({ request: request2, onChange }) {
27836
27882
  const t = useT();
27837
27883
  const editorFallback = /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "p-4 text-xs text-surface-500", children: t("Loading editor…") });
@@ -29039,6 +29085,109 @@ function StreamTab({ request: request2, onChange }) {
29039
29085
  ] })
29040
29086
  ] });
29041
29087
  }
29088
+ function isDynamicToken(name) {
29089
+ return name.startsWith("$") || name.includes("faker.") || name.includes("dayjs") || name.includes("(");
29090
+ }
29091
+ function collectUsedTokens(req) {
29092
+ const parts = [req.url];
29093
+ for (const p of req.params ?? []) parts.push(p.key, p.value);
29094
+ for (const h of req.headers ?? []) parts.push(h.key, h.value);
29095
+ if (req.auth) parts.push(JSON.stringify(req.auth));
29096
+ const b = req.body;
29097
+ if (b) {
29098
+ parts.push(b.json, b.raw);
29099
+ if (b.graphql) parts.push(b.graphql.query, b.graphql.variables);
29100
+ if (b.soap) parts.push(b.soap.envelope);
29101
+ for (const f of b.form ?? []) parts.push(f.key, f.value);
29102
+ }
29103
+ const found = /* @__PURE__ */ new Set();
29104
+ for (const s of parts) {
29105
+ if (!s) continue;
29106
+ for (const m of s.matchAll(/\{\{([^}]+)\}\}/g)) found.add(m[1].trim());
29107
+ }
29108
+ return [...found];
29109
+ }
29110
+ const SCOPE_BADGE = {
29111
+ environment: "text-emerald-400 border-emerald-500/30",
29112
+ collection: "text-blue-400 border-blue-500/30",
29113
+ global: "text-amber-400 border-amber-500/30"
29114
+ };
29115
+ function VariablesTab({ request: request2 }) {
29116
+ const t = useT();
29117
+ const activeCollectionId = useStore$1((s) => s.activeCollectionId);
29118
+ const collections = useStore$1((s) => s.collections);
29119
+ const globals = useStore$1((s) => s.globals);
29120
+ const activeEnv = useActiveEnvironment();
29121
+ const values = useVarValues();
29122
+ const { sourceOf, inScope } = reactExports.useMemo(() => {
29123
+ const envKeys = /* @__PURE__ */ new Set();
29124
+ for (const v of activeEnv?.variables ?? []) if (v.enabled && v.key) envKeys.add(v.key);
29125
+ const colKeys = new Set(Object.keys(
29126
+ activeCollectionId ? collections[activeCollectionId]?.data.collectionVariables ?? {} : {}
29127
+ ));
29128
+ const globalKeys = new Set(Object.keys(globals));
29129
+ const all2 = /* @__PURE__ */ new Set([...envKeys, ...colKeys, ...globalKeys]);
29130
+ const sourceOf2 = (name) => envKeys.has(name) ? "environment" : colKeys.has(name) ? "collection" : "global";
29131
+ return { sourceOf: sourceOf2, inScope: all2 };
29132
+ }, [activeEnv, activeCollectionId, collections, globals]);
29133
+ const used = reactExports.useMemo(() => collectUsedTokens(request2), [request2]);
29134
+ const usedSet = reactExports.useMemo(() => new Set(used), [used]);
29135
+ const allNames = reactExports.useMemo(
29136
+ () => [...inScope].sort((a, b) => a.localeCompare(b)),
29137
+ [inScope]
29138
+ );
29139
+ const SCOPE_LABEL = {
29140
+ environment: t("environment"),
29141
+ collection: t("collection"),
29142
+ global: t("global")
29143
+ };
29144
+ return /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "space-y-5 text-xs", children: [
29145
+ /* @__PURE__ */ jsxRuntimeExports.jsxs("section", { children: [
29146
+ /* @__PURE__ */ jsxRuntimeExports.jsxs("h3", { className: "text-[11px] uppercase tracking-wider text-surface-400 mb-2", children: [
29147
+ t("Referenced by this request"),
29148
+ used.length > 0 && /* @__PURE__ */ jsxRuntimeExports.jsxs("span", { className: "ml-1.5 text-surface-500", children: [
29149
+ "(",
29150
+ used.length,
29151
+ ")"
29152
+ ] })
29153
+ ] }),
29154
+ used.length === 0 ? /* @__PURE__ */ jsxRuntimeExports.jsx("p", { className: "text-surface-500", children: t("No {{variables}} are referenced in the URL, params, headers, auth, or body.") }) : /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "rounded border border-surface-800 overflow-hidden", children: used.map((name) => {
29155
+ const defined = inScope.has(name);
29156
+ const dynamic = !defined && isDynamicToken(name);
29157
+ return /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-center gap-2 px-3 py-1.5 border-b border-surface-800 last:border-b-0", children: [
29158
+ /* @__PURE__ */ jsxRuntimeExports.jsx("code", { className: "font-mono text-surface-200 shrink-0", children: `{{${name}}}` }),
29159
+ /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "flex-1 min-w-0 truncate text-surface-400 font-mono", children: defined ? values[name] || /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "italic text-surface-600", children: t("(empty)") }) : dynamic ? /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "text-surface-500", children: t("generated at send time") }) : /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "text-red-400", children: t("not defined") }) }),
29160
+ defined && /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: `shrink-0 text-[9px] uppercase tracking-wider border rounded px-1 py-px ${SCOPE_BADGE[sourceOf(name)]}`, children: SCOPE_LABEL[sourceOf(name)] }),
29161
+ !defined && !dynamic && /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "shrink-0 text-[9px] uppercase tracking-wider text-red-400 border border-red-500/30 rounded px-1 py-px", children: t("missing") })
29162
+ ] }, name);
29163
+ }) })
29164
+ ] }),
29165
+ /* @__PURE__ */ jsxRuntimeExports.jsxs("section", { children: [
29166
+ /* @__PURE__ */ jsxRuntimeExports.jsxs("h3", { className: "text-[11px] uppercase tracking-wider text-surface-400 mb-2", children: [
29167
+ t("All variables in scope"),
29168
+ allNames.length > 0 && /* @__PURE__ */ jsxRuntimeExports.jsxs("span", { className: "ml-1.5 text-surface-500", children: [
29169
+ "(",
29170
+ allNames.length,
29171
+ ")"
29172
+ ] })
29173
+ ] }),
29174
+ allNames.length === 0 ? /* @__PURE__ */ jsxRuntimeExports.jsx("p", { className: "text-surface-500", children: t("No variables are defined in the active environment, collection, or globals.") }) : /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "rounded border border-surface-800 overflow-hidden", children: allNames.map((name) => /* @__PURE__ */ jsxRuntimeExports.jsxs(
29175
+ "div",
29176
+ {
29177
+ className: `flex items-center gap-2 px-3 py-1.5 border-b border-surface-800 last:border-b-0 ${usedSet.has(name) ? "bg-surface-800/40" : ""}`,
29178
+ children: [
29179
+ /* @__PURE__ */ jsxRuntimeExports.jsx("code", { className: "font-mono text-surface-200 shrink-0", children: name }),
29180
+ usedSet.has(name) && /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "shrink-0 text-[9px] uppercase tracking-wider text-surface-400 border border-surface-700 rounded px-1 py-px", title: t("Referenced by this request"), children: t("used") }),
29181
+ /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "flex-1 min-w-0 truncate text-surface-400 font-mono text-right", children: values[name] || /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "italic text-surface-600", children: t("(empty)") }) }),
29182
+ /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: `shrink-0 text-[9px] uppercase tracking-wider border rounded px-1 py-px ${SCOPE_BADGE[sourceOf(name)]}`, children: SCOPE_LABEL[sourceOf(name)] })
29183
+ ]
29184
+ },
29185
+ name
29186
+ )) })
29187
+ ] }),
29188
+ /* @__PURE__ */ jsxRuntimeExports.jsx("p", { className: "text-[10px] text-surface-600 leading-relaxed", children: t("Precedence: environment overrides collection overrides global. Secrets show as dots; environment-reference variables show as $NAME.") })
29189
+ ] });
29190
+ }
29042
29191
  const { electron: electron$u } = window;
29043
29192
  function formatTime$2(ts) {
29044
29193
  const d = new Date(ts);
@@ -29769,7 +29918,8 @@ function deriveHookStatus(r) {
29769
29918
  }
29770
29919
  const TAB_HINTS = {
29771
29920
  schema: "Schema: validate the response body against a JSON Schema, synced from your OpenAPI spec or created from a response. Checked automatically on every send.",
29772
- stream: "Stream: idle and total timeouts for streamed responses (SSE / NDJSON / chunked)."
29921
+ stream: "Stream: idle and total timeouts for streamed responses (SSE / NDJSON / chunked).",
29922
+ variables: "Variables: every variable in scope for this request (environment, collection, globals), their resolved values, and which ones the request references."
29773
29923
  };
29774
29924
  function RequestBuilder({ request: request2 }) {
29775
29925
  const t = useT();
@@ -30034,6 +30184,7 @@ function RequestBuilder({ request: request2 }) {
30034
30184
  ...!isWs ? [
30035
30185
  { id: "body", label: isSoap ? "SOAP" : "Body", count: request2.body.mode !== "none" ? 1 : 0 },
30036
30186
  { id: "auth", label: "Auth", count: request2.auth.type !== "none" ? 1 : 0 },
30187
+ { id: "variables", label: "Variables", count: 0 },
30037
30188
  ...!isExample ? [
30038
30189
  { id: "scripts", label: "Scripts", count: (hasPreScript ? 1 : 0) + (hasPostScript ? 1 : 0) },
30039
30190
  { id: "schema", label: "Schema", count: request2.schema?.trim() ? 1 : 0 },
@@ -30254,6 +30405,7 @@ function RequestBuilder({ request: request2 }) {
30254
30405
  shown === "headers" && /* @__PURE__ */ jsxRuntimeExports.jsx(HeadersTab, { request: request2, onChange: update }),
30255
30406
  shown === "body" && /* @__PURE__ */ jsxRuntimeExports.jsx(BodyTab, { request: request2, onChange: update }),
30256
30407
  shown === "auth" && /* @__PURE__ */ jsxRuntimeExports.jsx(AuthTab, { request: request2, onChange: update }),
30408
+ shown === "variables" && /* @__PURE__ */ jsxRuntimeExports.jsx(VariablesTab, { request: request2 }),
30257
30409
  !isExample && shown === "scripts" && /* @__PURE__ */ jsxRuntimeExports.jsx(ScriptsTab, { request: request2, onChange: update }),
30258
30410
  !isExample && shown === "schema" && /* @__PURE__ */ jsxRuntimeExports.jsx(SchemaTab, { request: request2, onChange: update }),
30259
30411
  !isExample && shown === "docs" && /* @__PURE__ */ jsxRuntimeExports.jsx(DocsTab, { request: request2, onChange: update }),
@@ -30849,6 +31001,41 @@ function sortedIndices(rows, col, dir) {
30849
31001
  idx.sort((a, b) => compareValues(cellAt(rows[a], col), cellAt(rows[b], col)));
30850
31002
  return dir === "desc" ? idx.reverse() : idx;
30851
31003
  }
31004
+ function sortRows(rows, col, dir) {
31005
+ return sortedIndices(rows, col, dir).map((i) => rows[i]);
31006
+ }
31007
+ function exportCell(value2) {
31008
+ if (value2 == null) return "";
31009
+ if (typeof value2 === "object") return JSON.stringify(value2);
31010
+ return String(value2);
31011
+ }
31012
+ function tableMatrix(rows, columns) {
31013
+ if (columns.length === 0) return { headers: ["value"], rows: rows.map((r) => [exportCell(r)]) };
31014
+ return { headers: columns, rows: rows.map((r) => columns.map((c) => exportCell(cellAt(r, c)))) };
31015
+ }
31016
+ function tableToCsv(headers, rows) {
31017
+ const esc2 = (s) => /[",\r\n]/.test(s) ? `"${s.replaceAll('"', '""')}"` : s;
31018
+ return [headers, ...rows].map((r) => r.map(esc2).join(",")).join("\r\n");
31019
+ }
31020
+ function tableToTsv(headers, rows) {
31021
+ const clean = (s) => s.replace(/[\t\r\n]+/g, " ");
31022
+ return [headers, ...rows].map((r) => r.map(clean).join(" ")).join("\n");
31023
+ }
31024
+ const COPY_FEEDBACK_MS = 2e3;
31025
+ function useCopyFeedback(resetMs = COPY_FEEDBACK_MS) {
31026
+ const [copied, setCopied] = reactExports.useState(null);
31027
+ const timer2 = reactExports.useRef(null);
31028
+ const copy2 = reactExports.useCallback(async (text, key) => {
31029
+ try {
31030
+ await navigator.clipboard.writeText(text);
31031
+ setCopied(key ?? true);
31032
+ if (timer2.current) clearTimeout(timer2.current);
31033
+ timer2.current = setTimeout(() => setCopied(null), resetMs);
31034
+ } catch {
31035
+ }
31036
+ }, [resetMs]);
31037
+ return { copied, copy: copy2 };
31038
+ }
30852
31039
  const MAX_ROWS = 1e3;
30853
31040
  function xmlToJson(node) {
30854
31041
  const out = {};
@@ -30893,6 +31080,7 @@ function ResponseTable({ body, contentType }) {
30893
31080
  const detected = reactExports.useMemo(() => parsed.ok ? findPrimaryArray(parsed.value) : null, [parsed]);
30894
31081
  const [path, setPath] = reactExports.useState("");
30895
31082
  const [sort, setSort] = reactExports.useState(null);
31083
+ const { copied, copy: copy2 } = useCopyFeedback();
30896
31084
  reactExports.useEffect(() => {
30897
31085
  setPath(detected?.path ?? "");
30898
31086
  setSort(null);
@@ -30907,6 +31095,29 @@ function ResponseTable({ body, contentType }) {
30907
31095
  function toggleSort(col) {
30908
31096
  setSort((s) => s?.col === col ? s.dir === "asc" ? { col, dir: "desc" } : null : { col, dir: "asc" });
30909
31097
  }
31098
+ const hasTable = rows !== null || classify(target) === "object";
31099
+ function exportMatrix() {
31100
+ if (rows) return tableMatrix(sort ? sortRows(rows, sort.col, sort.dir) : rows, columns);
31101
+ if (classify(target) === "object") {
31102
+ return { headers: ["field", "value"], rows: Object.entries(target).map(([k, v]) => [k, exportCell(v)]) };
31103
+ }
31104
+ return null;
31105
+ }
31106
+ function copyTable() {
31107
+ const m = exportMatrix();
31108
+ if (m) copy2(tableToTsv(m.headers, m.rows));
31109
+ }
31110
+ function downloadCsv() {
31111
+ const m = exportMatrix();
31112
+ if (!m) return;
31113
+ const blob = new Blob([tableToCsv(m.headers, m.rows)], { type: "text/csv;charset=utf-8" });
31114
+ const url = URL.createObjectURL(blob);
31115
+ const a = document.createElement("a");
31116
+ a.href = url;
31117
+ a.download = `response${path ? "-" + path.replace(/[^\w.-]+/g, "_") : ""}.csv`;
31118
+ a.click();
31119
+ URL.revokeObjectURL(url);
31120
+ }
30910
31121
  function navigate2(p) {
30911
31122
  setPath(p);
30912
31123
  setSort(null);
@@ -30952,7 +31163,27 @@ function ResponseTable({ body, contentType }) {
30952
31163
  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"
30953
31164
  }
30954
31165
  ),
30955
- /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "text-surface-500 shrink-0", children: rows ? `${t(":count row|:count rows", { count: rows.length })}${columns.length ? ` · ${t(":count col|:count cols", { count: columns.length })}` : ""}` : classify(target) === "object" ? t(":count field|:count fields", { count: Object.keys(target).length }) : "" })
31166
+ /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "text-surface-500 shrink-0", children: rows ? `${t(":count row|:count rows", { count: rows.length })}${columns.length ? ` · ${t(":count col|:count cols", { count: columns.length })}` : ""}` : classify(target) === "object" ? t(":count field|:count fields", { count: Object.keys(target).length }) : "" }),
31167
+ hasTable && /* @__PURE__ */ jsxRuntimeExports.jsxs(jsxRuntimeExports.Fragment, { children: [
31168
+ /* @__PURE__ */ jsxRuntimeExports.jsx(
31169
+ "button",
31170
+ {
31171
+ onClick: copyTable,
31172
+ title: t("Copy as tab-separated values (paste into Excel or Google Sheets)"),
31173
+ className: "shrink-0 px-2 py-0.5 rounded border border-surface-700 text-surface-300 hover:text-white hover:border-surface-500 transition-colors",
31174
+ children: copied ? t("Copied") : t("Copy")
31175
+ }
31176
+ ),
31177
+ /* @__PURE__ */ jsxRuntimeExports.jsx(
31178
+ "button",
31179
+ {
31180
+ onClick: downloadCsv,
31181
+ title: t("Download as CSV"),
31182
+ className: "shrink-0 px-2 py-0.5 rounded border border-surface-700 text-surface-300 hover:text-white hover:border-surface-500 transition-colors",
31183
+ children: t("CSV")
31184
+ }
31185
+ )
31186
+ ] })
30956
31187
  ] })
30957
31188
  ] }),
30958
31189
  /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex-1 min-h-0 overflow-auto", children: [
@@ -32774,9 +33005,9 @@ function HistoryPanel() {
32774
33005
  }, [collections]);
32775
33006
  const groups = reactExports.useMemo(() => {
32776
33007
  const q = search.toLowerCase();
32777
- const filtered2 = q ? history.filter((e) => e.request.name.toLowerCase().includes(q) || e.resolvedUrl.toLowerCase().includes(q) || e.request.method.toLowerCase().includes(q)) : history;
33008
+ const filtered = q ? history.filter((e) => e.request.name.toLowerCase().includes(q) || e.resolvedUrl.toLowerCase().includes(q) || e.request.method.toLowerCase().includes(q)) : history;
32778
33009
  const out = [];
32779
- for (const entry of filtered2) {
33010
+ for (const entry of filtered) {
32780
33011
  const label = formatDate(entry.timestamp, t);
32781
33012
  const last = out.at(-1);
32782
33013
  if (last?.label === label) last.entries.push(entry);
@@ -32842,7 +33073,7 @@ function HistoryPanel() {
32842
33073
  ] })
32843
33074
  ] }),
32844
33075
  history.length === 0 ? /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "flex-1 flex items-center justify-center text-xs text-surface-400 px-4 text-center", children: t("No history yet. Send a request to start recording.") }) : /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex-1 overflow-y-auto", children: [
32845
- filtered.length === 0 && /* @__PURE__ */ jsxRuntimeExports.jsx("p", { className: "px-3 py-4 text-xs text-surface-400", children: t("No matches.") }),
33076
+ groups.length === 0 && /* @__PURE__ */ jsxRuntimeExports.jsx("p", { className: "px-3 py-4 text-xs text-surface-400", children: t("No matches.") }),
32846
33077
  groups.map((group) => /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { children: [
32847
33078
  /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "px-3 py-1 text-[10px] font-semibold text-surface-400 uppercase tracking-wider bg-surface-950/50 sticky top-0", children: group.label }),
32848
33079
  group.entries.map((entry) => /* @__PURE__ */ jsxRuntimeExports.jsx(
@@ -33018,7 +33249,7 @@ function WelcomeScreen() {
33018
33249
  )
33019
33250
  ] }),
33020
33251
  /* @__PURE__ */ jsxRuntimeExports.jsx("p", { className: "text-surface-400 text-sm max-w-sm", children: t("Local-first API testing with Robot Framework & Playwright code generation. Secrets stay on your machine.") }),
33021
- /* @__PURE__ */ jsxRuntimeExports.jsx("p", { className: "text-[11px] mt-3", style: { color: "var(--text-muted)" }, children: t("version :version", { version: "0.7.4" }) })
33252
+ /* @__PURE__ */ jsxRuntimeExports.jsx("p", { className: "text-[11px] mt-3", style: { color: "var(--text-muted)" }, children: t("version :version", { version: "0.7.5" }) })
33022
33253
  ] }),
33023
33254
  /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex flex-col gap-3 w-64", children: [
33024
33255
  /* @__PURE__ */ jsxRuntimeExports.jsx(
@@ -33100,21 +33331,6 @@ function WelcomeScreen() {
33100
33331
  ] })
33101
33332
  ] });
33102
33333
  }
33103
- const COPY_FEEDBACK_MS = 2e3;
33104
- function useCopyFeedback(resetMs = COPY_FEEDBACK_MS) {
33105
- const [copied, setCopied] = reactExports.useState(null);
33106
- const timer2 = reactExports.useRef(null);
33107
- const copy2 = reactExports.useCallback(async (text, key) => {
33108
- try {
33109
- await navigator.clipboard.writeText(text);
33110
- setCopied(key ?? true);
33111
- if (timer2.current) clearTimeout(timer2.current);
33112
- timer2.current = setTimeout(() => setCopied(null), resetMs);
33113
- } catch {
33114
- }
33115
- }, [resetMs]);
33116
- return { copied, copy: copy2 };
33117
- }
33118
33334
  const { electron: electron$k } = window;
33119
33335
  const EXAMPLES = [
33120
33336
  {
@@ -33994,6 +34210,7 @@ function WorkspaceSettingsModal({ onClose }) {
33994
34210
  const [azTenantId, setAzTenantId] = reactExports.useState(sec.azure?.tenantId ?? "");
33995
34211
  const [azClientId, setAzClientId] = reactExports.useState(sec.azure?.clientId ?? "");
33996
34212
  const [opConnectHost, setOpConnectHost] = reactExports.useState(sec.onePassword?.connectHost ?? "");
34213
+ const [gcpProject, setGcpProject] = reactExports.useState(sec.gcp?.project ?? "");
33997
34214
  const [oidc, setOidc] = reactExports.useState({ status: "idle" });
33998
34215
  async function vaultOidcLogin() {
33999
34216
  setOidc({ status: "busy", msg: t("Complete the sign-in in your browser…") });
@@ -34078,6 +34295,7 @@ function WorkspaceSettingsModal({ onClose }) {
34078
34295
  if (azClientId.trim()) azure.clientId = azClientId.trim();
34079
34296
  if (Object.keys(azure).length) secrets.azure = azure;
34080
34297
  if (opConnectHost.trim()) secrets.onePassword = { connectHost: opConnectHost.trim() };
34298
+ if (gcpProject.trim()) secrets.gcp = { project: gcpProject.trim() };
34081
34299
  if (Object.keys(secrets).length) settings.secrets = secrets;
34082
34300
  else delete settings.secrets;
34083
34301
  updateWorkspaceSettings(settings);
@@ -34647,6 +34865,22 @@ function WorkspaceSettingsModal({ onClose }) {
34647
34865
  className: `${inputCls} placeholder-surface-600`
34648
34866
  }
34649
34867
  )
34868
+ ] }),
34869
+ /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex flex-col gap-1", children: [
34870
+ /* @__PURE__ */ jsxRuntimeExports.jsxs("label", { className: labelCls, children: [
34871
+ t("GCP project"),
34872
+ " ",
34873
+ /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "text-surface-600 normal-case", children: t("(credentials from GOOGLE_APPLICATION_CREDENTIALS or GCP_ACCESS_TOKEN)") })
34874
+ ] }),
34875
+ /* @__PURE__ */ jsxRuntimeExports.jsx(
34876
+ "input",
34877
+ {
34878
+ value: gcpProject,
34879
+ onChange: (e) => setGcpProject(e.target.value),
34880
+ placeholder: "my-gcp-project",
34881
+ className: `${inputCls} placeholder-surface-600`
34882
+ }
34883
+ )
34650
34884
  ] })
34651
34885
  ] })
34652
34886
  ] }),
@@ -35265,14 +35499,14 @@ function ImportModal({ onImport, onClose }) {
35265
35499
  await persistBaseUrlVariable(trimmedVarName, trimmedBaseUrl);
35266
35500
  }
35267
35501
  if (target === "__new__") {
35268
- const filtered2 = {
35502
+ const filtered = {
35269
35503
  ...previewCol,
35270
35504
  name: newColName.trim() || previewCol.name,
35271
35505
  requests: {},
35272
35506
  rootFolder: pruneFolder(previewCol.rootFolder, previewCol, chosenIds, {})
35273
35507
  };
35274
- filtered2.requests = collectRequestsByFolder(filtered2.rootFolder, previewCol);
35275
- onImport(filtered2);
35508
+ filtered.requests = collectRequestsByFolder(filtered.rootFolder, previewCol);
35509
+ onImport(filtered);
35276
35510
  onClose();
35277
35511
  } else {
35278
35512
  const targetCol = collections[target]?.data;
@@ -40293,15 +40527,16 @@ const PROVIDER_ENV = {
40293
40527
  vault: ["VAULT_ADDR", "VAULT_ROLE_ID", "VAULT_SECRET_ID"],
40294
40528
  aws: ["AWS_REGION", "AWS_ACCESS_KEY_ID", "AWS_SECRET_ACCESS_KEY"],
40295
40529
  azure: ["AZURE_TENANT_ID", "AZURE_CLIENT_ID", "AZURE_CLIENT_SECRET"],
40530
+ gcp: ["GCP_PROJECT", "GOOGLE_APPLICATION_CREDENTIALS_JSON"],
40296
40531
  op: ["OP_CONNECT_HOST", "OP_CONNECT_TOKEN"]
40297
40532
  };
40298
40533
  function secretManagerOf(ref) {
40299
40534
  if (ref.startsWith("op://")) return "op";
40300
40535
  const scheme = ref.split(":", 1)[0];
40301
- if (scheme === "vault" || scheme === "aws" || scheme === "azure") return scheme;
40536
+ if (scheme === "vault" || scheme === "aws" || scheme === "azure" || scheme === "gcp") return scheme;
40302
40537
  return null;
40303
40538
  }
40304
- const INLINE_REF_RE = /\{\{\s*(vault:|aws:|azure:|op:\/\/)/g;
40539
+ const INLINE_REF_RE = /\{\{\s*(vault:|aws:|azure:|gcp:|op:\/\/)/g;
40305
40540
  function inlineSecretManagers(text) {
40306
40541
  if (!text) return [];
40307
40542
  const kinds = /* @__PURE__ */ new Set();
@@ -52121,7 +52356,7 @@ function CommandPalette() {
52121
52356
  }
52122
52357
  return results;
52123
52358
  }, [collections]);
52124
- const filtered2 = reactExports.useMemo(() => {
52359
+ const filtered = reactExports.useMemo(() => {
52125
52360
  if (!query.trim()) return allRequests.slice(0, 30);
52126
52361
  const q = query.toLowerCase();
52127
52362
  return allRequests.filter(
@@ -52129,18 +52364,18 @@ function CommandPalette() {
52129
52364
  ).slice(0, 30);
52130
52365
  }, [allRequests, query]);
52131
52366
  reactExports.useEffect(() => {
52132
- setActiveIdx((prev) => Math.min(prev, Math.max(0, filtered2.length - 1)));
52133
- }, [filtered2.length]);
52367
+ setActiveIdx((prev) => Math.min(prev, Math.max(0, filtered.length - 1)));
52368
+ }, [filtered.length]);
52134
52369
  function handleKeyDown(e) {
52135
52370
  if (e.key === "ArrowDown") {
52136
52371
  e.preventDefault();
52137
- setActiveIdx((i) => Math.min(i + 1, filtered2.length - 1));
52372
+ setActiveIdx((i) => Math.min(i + 1, filtered.length - 1));
52138
52373
  } else if (e.key === "ArrowUp") {
52139
52374
  e.preventDefault();
52140
52375
  setActiveIdx((i) => Math.max(i - 1, 0));
52141
52376
  } else if (e.key === "Enter") {
52142
52377
  e.preventDefault();
52143
- const item = filtered2[activeIdx];
52378
+ const item = filtered[activeIdx];
52144
52379
  if (item) {
52145
52380
  openInTab(item.requestId, item.collectionId);
52146
52381
  setOpen(false);
@@ -52176,7 +52411,7 @@ function CommandPalette() {
52176
52411
  ),
52177
52412
  /* @__PURE__ */ jsxRuntimeExports.jsx("kbd", { className: "text-[10px] text-surface-600 bg-surface-800 border border-surface-700 rounded px-1.5 py-0.5", children: "Esc" })
52178
52413
  ] }),
52179
- /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "max-h-80 overflow-y-auto", children: filtered2.length === 0 ? /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "px-4 py-6 text-center text-sm text-surface-600", children: t('No requests match ":query"', { query }) }) : filtered2.map((item, idx) => /* @__PURE__ */ jsxRuntimeExports.jsxs(
52414
+ /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "max-h-80 overflow-y-auto", children: filtered.length === 0 ? /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "px-4 py-6 text-center text-sm text-surface-600", children: t('No requests match ":query"', { query }) }) : filtered.map((item, idx) => /* @__PURE__ */ jsxRuntimeExports.jsxs(
52180
52415
  "div",
52181
52416
  {
52182
52417
  role: "button",
@@ -52202,7 +52437,7 @@ function CommandPalette() {
52202
52437
  },
52203
52438
  item.requestId
52204
52439
  )) }),
52205
- filtered2.length > 0 && /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "px-4 py-2 border-t border-surface-800 flex items-center gap-3 text-[10px] text-surface-600", children: [
52440
+ filtered.length > 0 && /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "px-4 py-2 border-t border-surface-800 flex items-center gap-3 text-[10px] text-surface-600", children: [
52206
52441
  /* @__PURE__ */ jsxRuntimeExports.jsxs("span", { children: [
52207
52442
  /* @__PURE__ */ jsxRuntimeExports.jsx("kbd", { className: "bg-surface-800 border border-surface-700 rounded px-1", children: "↑↓" }),
52208
52443
  " ",
@@ -52639,7 +52874,7 @@ function App() {
52639
52874
  /* @__PURE__ */ jsxRuntimeExports.jsx("span", { style: { color: "#6aa3c8" }, children: "Spector" }),
52640
52875
  /* @__PURE__ */ jsxRuntimeExports.jsxs("span", { className: "ml-2 text-[10px] font-normal opacity-50", children: [
52641
52876
  "v",
52642
- "0.7.4"
52877
+ "0.7.5"
52643
52878
  ] }),
52644
52879
  /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "mx-2 opacity-30", children: "·" }),
52645
52880
  /* @__PURE__ */ jsxRuntimeExports.jsx(