Skip to content

API Client: offline Monaco, data-loss and hang fixes, render perf, folder collections, Insomnia/Bruno import - #310

Open
itsmeakhil wants to merge 9 commits into
mainfrom
feat/api-client-phase0
Open

API Client: offline Monaco, data-loss and hang fixes, render perf, folder collections, Insomnia/Bruno import#310
itsmeakhil wants to merge 9 commits into
mainfrom
feat/api-client-phase0

Conversation

@itsmeakhil

Copy link
Copy Markdown
Collaborator

What does this change?

API Client work in four parts: fix the things that lose data or break offline, stop the per-keystroke re-render of the whole editor, land the git-friendly folder collections that were stranded on an unmerged branch, and add Insomnia + Bruno + Postman-environment import.

It started as a gap analysis against Bruno v4.1 and turned into the first two phases of the plan that came out of it.

Closes #

Why?

Three of these were quietly serious:

  1. Monaco was loaded from jsdelivr at runtime. There is no loader.config anywhere in apps/desktop-ui and csp is null, so @monaco-editor/loader used its CDN default. With no network every code editor in the app — request body, scripts, response, and the ~15 other tools using CodeEditor — sat on "Loading editor..." forever. monaco-editor was already in package.json and never imported. "Fully offline" was not true.
  2. One saved example could wipe every open tab. Saved-example bodies were persisted into the api-client-tabs localStorage key; a 2 MB response blew the 5 MB quota and the catch deleted the whole key.
  3. A top-level while (true) in a pre-request script hung the tab permanently. The 3s script budget was only checked inside pm.test(), and Cancel aborted the fetch, not the worker.

Plus: git-friendly folder-backed collections (de5aa0a3) had been sitting on api-tester-improvements, unmerged, since July — that feature is our answer to Bruno's core pitch.

Type of change

  • Bug fix
  • New tool
  • Improvement to an existing tool
  • Documentation
  • Translation
  • Chore (deps, config, tooling)

What's in it, commit by commit

c50bd6e7 — trust blockers

  • Monaco served from the app bundle: scripts/copy-monaco.mjs copies monaco-editor/min/vspublic/monaco/vs (gitignored, runs from pre{dev,build,dev:tauri}), lib/monaco-loader.ts points the loader at it. monaco-editor bumped 0.53 → 0.55.1 — the 0.53 AMD build throws Property description must be an object in language/css/monaco.contribution.js.
  • Script runner: 5s hard timeout, worker terminated and respawned so the next send works.
  • Tabs: graphqlSchema (refetchable) always stripped; on quota overflow, retry once with example bodies and uploaded file bytes stripped instead of dropping every tab.
  • History: file bytes stripped before the POST too (not just localStorage); ORDER BY … LIMIT in SQL instead of loading every row and sorting in Rust; clear/delete now hard-delete (sync is gone, tombstones only accumulated).
  • Rust proxy: pooled reqwest clients (was one per request — a TLS handshake every send), 50 MB response cap with an error envelope instead of unbounded buffering.
  • Dropped the NEXT_PUBLIC_FIREBASE_API_KEY hard-fail from build-tauri.mjs — leftover from the removed auth, it blocked builds for a feature that no longer exists.

26282f74 — render performance

  • RequestTabs and ResponsePanel memoized; the nine inline (v) => updateActiveTab({...}) props replaced with one memoized setter map; stable onMount/onChange/onSaveExample.
  • handleSend and handleCopyCurl go through latest-value refs, so RequestPanel's memo holds and the window keydown listener stops being removed/re-added on every keystroke.
  • Response body parsed once per body instead of three times per render (tryParseJson was called inline in three eagerly-evaluated JSX props).
  • Monaco no longer remounts on Send: the request panel stays in one ResizablePanelGroup and the response panel joins it conditionally; body/pre-request/tests tabs are forceMount + hidden.
  • History rows and TabBar's measure effect no longer invalidate on every tab-array identity change.
  • sendProxyRequest() returns the Rust envelope directly instead of stringify → fake Response.json() (two extra full passes over the body on the UI thread).
  • Secret scan covers {params, headers, auth, body} instead of JSON.stringify(activeTab) — it was dragging the previous response and every saved example through a regex on each send.
  • next/dynamic for GrpcPanel (protobufjs), WebSocketPanel, ImportDialog (js-yaml), runner, metrics, cookie jar; exporters imported on click.
  • JSON tree/table views cap at 200 children / 500 rows (unvirtualised — a 10k-element array mounted ~100k DOM nodes on tab open).

711f3bd4 — merge api-tester-improvements
Folder-of-YAML collections (file-store.ts, collection-files.ts, move-to-vault.ts, Rust fs/dialog/persisted-scope plugins). Conflict resolution keeps main's auth removal — the branch's deep-link plugin, await_browser_auth command and cookie_store deps stay deleted — and main's reqwest 0.13 / base64 0.23. The "Open folder as collection" button moved into ToolSidebarActions, where main had relocated the sidebar header controls.

58aaf5bc — marketing copy
The Postman and Insomnia comparison pages were still conceding mock servers, collections and test automation to Postman — all of which ship. Rewritten to what's real, conceding only team/cloud workspaces, a CLI runner and Windows/Linux builds. Added /compare/mydevtools-vs-bruno and /compare/bruno-alternative.

5ce3275a, ab1b1d6b — imports

  • The Postman environment importer already existed and had no caller. Wired into the dialog; added a matching per-environment export button.
  • New Insomnia importer: v4 JSON resources and v5 YAML, nested groups, all body/auth types we support, {{ _.VAR }}{{VAR}}, scripts, environments. Round-trips our own Insomnia v4 exporter.
  • New Bruno importer: the legacy .bru language and v4 OpenCollection YAML — folder trees with folder.bru defaults, seq ordering, ~key disabled rows, json/text/xml/form/multipart (@file(...))/graphql bodies, basic/bearer/apikey/oauth2, scripts, environments/*.
  • Imports that carry environments now create them instead of dropping them.

How did you verify it?

  • cd apps/desktop-ui && pnpm test135 suites, 1267 passed, 2 skipped. 31 of those tests are new (tab slimming, Postman env round-trip, 13 Insomnia, 9 Bruno, 2 detection).
  • ./node_modules/.bin/tsc --noEmit -p . in both apps/desktop-ui and apps/web → clean.
  • cargo check and cargo test in apps/desktop/src-tauri → 54 passed. One existing test (history_limit_and_clear) started failing on the new SQL ordering because three rows land in the same millisecond; fixed with a rowid DESC tiebreak and the test now also asserts that clear actually empties.
  • Monaco checked in a real browser, not just by reading code: loaded /app/format-converter, confirmed the editor renders and that the network log shows /monaco/vs/loader.js, /monaco/vs/editor/editor.main.js, … and zero jsdelivr requests.
  • Drove the API Client in the running app: switched between Params/Body/Auth/Pre-request/Tests (editors keep state, inactive tabs hidden), typed a JSON body, sent with ⌘↵, confirmed the split layout appears and the body survives the send.

Screenshots

Not attached — the visible UI is unchanged apart from the sidebar's "Open folder as collection" button and an export icon in the environment manager. The perf work is deliberately invisible.

Checklist

  • pnpm lint passes — see note below
  • Tests pass (cd apps/desktop-ui && pnpm test), and logic changes have a test
  • User-visible strings go through useTranslations — the one new string (ApiClient.environmentManager.exportPostman) is in all 27 locales
  • New/changed keys added to messages/en.json (inserted into all 27 locales; JSON re-validated)
  • Async mutations that can fail are wrapped in try/catch with toast.error(message)
  • No new mandatory network call, sign-in, sync or backend — this PR removes the app's only unconditional outbound request
  • No secrets, tokens or personal data in the diff

On lint: the repo has ~360 pre-existing ESLint errors and lint is deliberately out of CI. I compared the touched files before and after: same three pre-existing errors (two react-hooks/purity on Date.now() in render, one set-state-in-effect in tabs-context), no new ones. I did fix the two I introduced mid-work (ref writes during render → effects) rather than leave them.

Anything reviewers should know?

  • Worth a close look: the ResizablePanelGroup change in api-client.tsx. Panels now get explicit id/order because the group's membership is conditional; if the response panel's saved size behaves oddly on your machine, that's where it is.
  • The Monaco version bump is load-bearing, not housekeeping. 0.53's prebuilt AMD bundle throws on load; don't pin it back without re-testing offline.
  • Bruno import is paste-a-file only for now. Picking a whole Bruno directory needs readCollectionFiles in lib/desktop/collection-files.ts to read .bru/.yml/.json — it is .yaml-only today. importBrunoFolder already takes the flat {path, text}[] that pass would produce.
  • git log is not linear711f3bd4 merges a five-week-old branch. Review with git diff main...HEAD rather than commit-by-commit if the merge noise gets in the way.
  • The gap analysis this came from also flagged features that are fully built and wired to nothing: the SSE relay in proxy.rs, the GraphQL subscriptions client, the OpenAPI spec watcher, cookie-jar export, and the backend's drag-and-drop move op. Not in this PR — worth their own.
  • I did not touch the deliberately-deferred items: NTLM/SPNEGO stay 501 on desktop, no CLI runner, no git UI.

🤖 Generated with Claude Code

https://claude.ai/code/session_01H3GuKxASDXLwFCLA7o3GGH

itsmeakhil and others added 9 commits July 21, 2026 22:46
Open a folder as a collection: requests stored as YAML files
(NNN- prefix = order, dirs = folders, collection.yaml manifest),
so collections can be committed, diffed and PR-reviewed. Filesystem
is the store — mutations diff the serialized tree and write only
changed files; window focus re-reads external edits. Secrets stay
out of files ({{vault.*}} tokens pass through; environments never
serialized). Desktop only: tauri fs/dialog/persisted-scope plugins
plus a recursive fs-scope grant command for picked dirs.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
… loss

- Serve Monaco from the app bundle (scripts/copy-monaco.mjs → public/monaco/vs,
  loader.config in lib/monaco-loader.ts) instead of jsdelivr; every editor was
  stuck on "Loading editor..." offline. Bump monaco-editor to 0.55.1 — the
  0.53 AMD build throws in language/css/monaco.contribution.js.
- Drop the stale Firebase-key gate from build-tauri.mjs (auth was removed).
- Script runner: 5 s hard timeout with worker terminate + respawn; a top-level
  infinite loop no longer hangs the tab forever.
- Tabs persistence: strip refetchable graphqlSchema always; on quota overflow
  retry with example bodies + file bytes stripped instead of wiping all tabs.
- History: strip uploaded file bytes before POST; SQL ORDER BY/LIMIT instead of
  loading every row; clear/delete hard-delete rows (sync is gone, tombstones
  only accumulated).
- Rust proxy: pooled reqwest clients (TLS/keep-alive reuse) and a 50 MB
  response body cap with an error envelope instead of unbounded buffering.
- generate-code.ts: replace raw NUL byte in Go escaper regex with \0.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01H3GuKxASDXLwFCLA7o3GGH
…ors mounted

- RequestTabs + ResponsePanel are React.memo; the nine inline
  `(v) => updateActiveTab({...})` setters become one memoized setter map,
  onSaveExample/onMount/onChange handlers are stable.
- handleSend/handleCopyCurl go through latest-value refs so RequestPanel's memo
  holds and the keydown listener stops re-registering on every edit;
  handleLoadRequest/handleCancel/handleCurlPaste/example handlers memoized.
- Response JSON parsed once (useMemo) instead of three times per render for
  the tree/table/geo views; those views cap at 200 children / 500 rows.
- Desktop layout keeps the request panel in one ResizablePanelGroup and adds
  the response panel conditionally (id/order) — Send no longer remounts Monaco.
  Body / pre-request / tests editor tabs are forceMount + hidden when inactive.
- History list row renderer memoized so VirtualHistoryList's memo holds;
  TabBar's layout effect keys on tab id/kind/method/name, not the tabs array.
- sendProxyRequest() returns the Rust envelope directly instead of
  stringify → fake Response → json(); typed ProxyEnvelope.
- Secret-variable scan covers params/headers/auth/body only, not the whole tab
  (previous response + example bodies).
- Scripts worker caches pm.response.json() per run.
- next/dynamic for GrpcPanel (protobufjs), WebSocketPanel, ImportDialog
  (js-yaml), CookieJarDialog, MetricsDialog, CollectionRunnerDialog; exporters
  are imported on click.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01H3GuKxASDXLwFCLA7o3GGH
…llections

Brings the folder-of-YAML collections (file-store.ts, collection-files.ts,
move-to-vault.ts, Rust fs/dialog/persisted-scope plugins, registry commands)
onto the current main. Conflict resolution keeps main's auth removal — the
branch's deep-link plugin, auth_server command and cookie_store deps stay
deleted — and main's reqwest 0.13 / base64 0.23. The "Open folder as
collection" button moves into ToolSidebarActions, where main relocated the
sidebar header controls.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01H3GuKxASDXLwFCLA7o3GGH
Postman and Insomnia comparison pages stopped conceding mock servers,
collections, scripting and the runner — all shipped. They now list REST,
GraphQL, WebSocket, native gRPC, the local mock server, runner, vault-backed
secrets and import/export formats, and concede only team/cloud workspaces,
a CLI runner and Windows/Linux builds. New /compare/mydevtools-vs-bruno and
/compare/bruno-alternative pages; api-client metadata description/keywords
and the landing capability blurb describe the real scope.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01H3GuKxASDXLwFCLA7o3GGH
The Postman environment importer existed but nothing called it. The import
dialog now detects `{ name, values[] }` payloads and creates the environment;
the environment manager gets a per-environment Postman export button.
i18n key ApiClient.environmentManager.exportPostman in all 27 locales.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01H3GuKxASDXLwFCLA7o3GGH
…in palette entry

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01H3GuKxASDXLwFCLA7o3GGH
Two importers for the switcher path, both pure libs with tests:

- Insomnia (`lib/import/insomnia.ts`): v4 JSON resources and v5 YAML, nested
  groups, every body/auth type we support, `{{ _.VAR }}` → `{{VAR}}`,
  pre-request/after-response scripts, environments. Round-trips our own
  Insomnia v4 exporter.
- Bruno (`lib/import/bruno.ts`): the legacy `.bru` language and v4
  OpenCollection YAML. Folder trees with `folder.bru` defaults, `seq`
  ordering, `~key` disabled rows, json/text/xml/form/multipart (`@file(...)`)
  /graphql bodies, basic/bearer/apikey/oauth2 auth, scripts, and
  `environments/*` (secret var names come in with empty values).

Detection covers both, and the import dialog now creates the environments an
import brings with it instead of dropping them. 22 new tests.

Still folder-only from a paste of a single file: picking a whole Bruno
directory needs a desktop folder-read pass (readCollectionFiles is .yaml-only).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01H3GuKxASDXLwFCLA7o3GGH
@vercel

vercel Bot commented Aug 22, 2026

Copy link
Copy Markdown
Contributor

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
mydevtools-tech Ready Ready Preview Aug 22, 2026 4:07pm

export function importInsomniaWithMeta(text: string): InsomniaImportResult {
const t = text.trim()
// yaml.load also parses JSON, so v4 YAML exports come for free.
const doc = (t.startsWith("{") ? JSON.parse(t) : yaml.load(t)) as
@github-actions

Copy link
Copy Markdown

ThreatCrush Security Scan

1 finding(s)

MEDIUM: 1

Severity Rule Location
MEDIUM js-unsafe-yaml-load apps/desktop-ui/src/lib/import/insomnia.ts:290

Snippets are redacted; ThreatCrush never prints matched credential material.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants