Skip to content

feat: run-local-gateway toggle for pure-client desktop mode (#2909) - #2926

Closed
patrigao wants to merge 1 commit into
mainfrom
fix/local-gateway-toggle-2909
Closed

feat: run-local-gateway toggle for pure-client desktop mode (#2909)#2926
patrigao wants to merge 1 commit into
mainfrom
fix/local-gateway-toggle-2909

Conversation

@patrigao

@patrigao patrigao commented Aug 12, 2026

Copy link
Copy Markdown
Contributor

Problem

The desktop app has no way to say "do not run a gateway on this machine". A user who points the app at a remote gateway still gets a local Python backend spawned whenever the port probe finds nothing — startGateway() decides on probe outcome, not intent. That costs laptop memory and CPU for a backend nothing uses (a kiro-cli process per session plus sub-agents and shells), and when the remote is briefly unreachable at launch, a silently spawned local backend answers on the remote's port and the tab keeps wearing the remote's configured name.

Why it matters

Remote-gateway users pay a permanent resource tax on every launch, and the silent local spawn actively misleads: the tab says "my remote crew" while everything runs against a fresh local backend. There is no config key or environment variable to express pure-client intent.

Fix (symptoms → root cause → change)

Root cause: the spawn decision has no intent input. The change adds one, mirroring the existing zoomAPI pattern end to end:

  • Store key runLocalGateway (default true = today's behavior), beside the other defaults in website/electron/main.js.
  • Spawn gate: both startGateway() arms (probe-failed and conflict fall-through) route through spawnGatewayIfEnabled(), whose decision table lives in the new pure module website/electron/local-gateway-policy.js:
    • setting on → spawn (a configured remote alone never suppresses the spawn);
    • setting off + no remote configured → spawn anyway (refusing with nowhere else to connect would brick the launch — stated in a comment);
    • setting off + remote configured + this session already owns a spawned gateway → spawn (next-launch scoping: a mid-session flip must not strand the running session's recovery respawns);
    • setting off + remote configured, nothing owned → decline. Boot fail-fasts via gatewayStartFailure to the existing showGatewayErrorDialog with a plain "can't reach <host>" message; Retry re-probes through the same gate and never spawns. weSpawnedGateway / reusedLocalGateway stay untouched on this path, so recovery treats the remote like any external gateway (reconnect, never respawn).
  • IPC + bridge: local-gateway:get/set handlers beside the zoom handlers; localGatewayAPI in preload.js shaped exactly like zoomAPI. Absent in plain browsers and the PWA.
  • Renderer: useLocalGateway hook modeled on useZoom (supported = !!bridge), and a SettingsToggle in Settings → Developer that renders only when the bridge exists. Helper text states the change takes effect on next launch. i18n keys added across all 12 catalogs + regenerated en-XA; settings registry regenerated; the new module is registered in electron-builder build.files.

Deliberately out of scope (the issue defers both): applying the flip to an already-running local gateway, and any new Settings group — Developer is the interim home. The ownership booleans are untouched; #2282 proposes that refactor separately.

Tests

  • website/electron/test/local-gateway-policy.test.js (node:test, 7 cases): default-on, on-with-remote, the one declining combination (host carried for the dialog), off-with-no-remote spawns, only literal false disables (corrupted values degrade to spawn), owned-gateway recovery overrides off, and the override is ownership-scoped. The recovery-override test fails if the override is removed (mutation-checked).
  • website/src/test/useLocalGateway.test.ts (5 cases): unsupported without the bridge with safe no-op setter, default-on read, persisted-off read, set round-trip, and main-process-applied-value-wins reconciliation.
  • website/src/test/DeveloperPanel.test.tsx (3 new cases): toggle absent without the bridge, default-on render + next-launch helper text, flip round-trips set(false).

There is no harness for executing main.js itself; the repo's precedent for main-process logic is extraction into a pure module with node:test (gateway-recovery.js, gateway-wait.js), which this follows.

Manual verification

Rendered the real DeveloperPanel (real stylesheet, stubbed bridge) in headless Chromium and exercised both toggle states — screenshots below. Full desktop launch-path behavior (decline → dialog → retry) is covered by reading the existing waitForGateway fail-fast contract (getFailure() is checked before the timeout on the primary boot path) rather than a packaged-app run; the Electron binary cannot be installed in this environment.

Screenshots

Default (on) Turned off
Run Local Gateway toggle, on Run Local Gateway toggle, off

Closes #2909

@patrigao
patrigao requested a review from a team August 12, 2026 01:12
@patrigao
patrigao requested a review from a team as a code owner August 12, 2026 01:12
@github-actions github-actions Bot added the readiness: checking Automated validation is still running label Aug 12, 2026
@github-actions

github-actions Bot commented Aug 12, 2026

Copy link
Copy Markdown
Contributor

GPT 5.6 Review — ✅ no blocking findings

GPT 5.6 completed its review of 3e6ce46e1bf0fcf00d3012cbf42c861d7e61d16c and found no blocking issues.

This comment is updated in place on each push.

Review details

No findings.
[GPT-REVIEWED] 3e6ce46

False positive or not applicable? A repository writer can comment:
/ai-review override gpt 3e6ce46e1bf0fcf00d3012cbf42c861d7e61d16c: <one-sentence reason>

@github-actions

github-actions Bot commented Aug 12, 2026

Copy link
Copy Markdown
Contributor

Design Review (Fable 5) — ✅ PASS

Advisory design-level review of 3e6ce46e1bf0fcf00d3012cbf42c861d7e61d16c — updated in place on each push; does not block merge.

Design-Verdict: PASS

Real resource/misdirection harm, fixed at the actual decision point (intent input to the spawn gate), with anti-brick, recovery, and rollback semantics all thought through.

[DESIGN-REVIEWED] 3e6ce46

@github-actions

github-actions Bot commented Aug 12, 2026

Copy link
Copy Markdown
Contributor

Opus 4.8 Review — ✅ no blocking findings

Reviewed 3e6ce46e1bf0fcf00d3012cbf42c861d7e61d16c — this comment is updated in place on each push.

Review details

The candidate's scenario requires resolveGatewayConflict() to return "spawn" while a remote host is configured for PORT. I traced whether that can actually occur:

  • The drain-then-spawn return "spawn" (main.js:485) is inside if (readiness === "shutting-down"). But readiness is computed at main.js:444 as remoteHost ? "unknown" : await fetchGatewayReadiness() — with a remote host configured, readiness is forced to "unknown", so the "shutting-down" block is never entered and line 485 is unreachable when a remote is configured.
  • The takeover return "spawn" (main.js:538) requires decideGatewayActiontakeover-prompt, which requires localOwner === "kirocrew" (instance-guard.js:140). But when a remote host is configured, localOwner is set to "foreign" (main.js:429), so the decision is reuse, never takeover.

So whenever resolveGatewayConflict() yields "spawn", remoteHost is necessarily empty. spawnGatewayIfEnabled re-reads the same store value, gets "", and decideLocalSpawn returns {spawn:true, reason:"no-remote-configured"} — the misleading remoteUnreachable arm cannot be reached via the line-626 path. The .catch() arm (nothing answered the probe) is the only path that reaches the decline, and there the "remote not reachable" copy is accurate.

Condition (a) — a "spawn" verdict with a configured remote — does not occur in practice. The candidate is falsified.

No findings.

[OPUS-REVIEWED] 3e6ce46

Verdict parsed from the review's SHA-scoped output markers for commit 3e6ce46e1bf0fcf00d3012cbf42c861d7e61d16c.

False positive or not applicable? A repository writer can comment:
/ai-review override fable 3e6ce46e1bf0fcf00d3012cbf42c861d7e61d16c: <one-sentence reason>

@github-actions

github-actions Bot commented Aug 12, 2026

Copy link
Copy Markdown
Contributor

UX Review (Fable 5) — ✅ PASS

Advisory UX-level review of 3e6ce46e1bf0fcf00d3012cbf42c861d7e61d16c — updated in place on each push; does not block merge.

UX-Verdict: PASS

Honest, layered copy at every surface — toggle, unsupported row, and failure dialog each disclose the exact consequence — so a first-time user is never misled.

Suggestions

  • run_local_gateway_desc: "a configured remote gateway" never says where that configuration lives — name the surface (e.g. "…remote gateway (set in the tab's host settings)") so a user flipping this off can verify the precondition instead of discovering "ignored" on next launch.
  • The can't reach ${remoteUnreachable} dialog's only in-app path is retry-until-the-remote-heals; the stated escape ("run "kirocrew gateway" in a terminal") could be a dialog button ("Start local gateway instead") that spawns once without persisting the setting — the dialog is this PR's surface and the fix removes the terminal dependency at the exact moment the dashboard is unreachable.

[UX-REVIEWED] 3e6ce46

@github-actions github-actions Bot added readiness: passed Eligible automated validation passed for the current revision and removed readiness: checking Automated validation is still running labels Aug 12, 2026
@patrigao
patrigao force-pushed the fix/local-gateway-toggle-2909 branch from caaa86b to 2098fbf Compare August 12, 2026 01:34
@github-actions github-actions Bot added readiness: checking Automated validation is still running and removed readiness: passed Eligible automated validation passed for the current revision labels Aug 12, 2026
@patrigao

Copy link
Copy Markdown
Contributor Author

Dispositions for the UX Review (Fable 5) CONCERNS on caaa86b:

Watch 1 — circular recovery in the unreachable-remote dialog: fixed in 2098fbfb43eb29cbf058c8933aa025384565feb0. The dialog message now names the no-dashboard escape: "To use this machine instead, run kirocrew gateway in a terminal, then retry." A manually started gateway answers the next re-probe and is adopted by the existing reuse path, so Retry completes the loop without a reachable Settings page. A "Start local gateway anyway" button was considered and not taken in this PR: showGatewayErrorDialog's action set is shared by every caller, and adding a spawn-authorizing action to a shared dialog is a larger change than an advisory item should carry.

Watch 2 — off-with-no-remote is silently ignored: accepted-and-deferred to #2928. The override itself is deliberate and documented in local-gateway-policy.js (refusing to spawn with nowhere else to connect would brick the launch); reflecting it in the UI needs a new IPC read, a conditional helper line, catalog keys across all 12 locales, and test coverage — real surface that should not ride this PR.

Suggestion — lead the description with the off-decision: rebutted. The Developer panel's existing toggles describe the enabled behavior first (e.g. Developer Mode: "Show Developer page in sidebar…"), and this toggle defaults on, so sentence one states what the default does; sentence two states what turning it off is for. Reordering would make this row read differently from its siblings for no comprehension gain.

@github-actions github-actions Bot added readiness: passed Eligible automated validation passed for the current revision and removed readiness: checking Automated validation is still running labels Aug 12, 2026
@patrigao
patrigao force-pushed the fix/local-gateway-toggle-2909 branch from 2098fbf to 785d4de Compare August 12, 2026 02:07
@github-actions github-actions Bot added readiness: checking Automated validation is still running and removed readiness: passed Eligible automated validation passed for the current revision labels Aug 12, 2026
@patrigao

Copy link
Copy Markdown
Contributor Author

Dispositions for the UX Review (Fable 5) CONCERNS on 2098fbf, addressed in 785d4de60d4da05a24dc96ebd7b8212f773cb223:

Watch 1 — off can be silently ignored (no-remote case): fixed with the reviewer's proposed one-sentence remedy. run_local_gateway_desc now ends with "Ignored when no remote gateway is configured." in all 12 catalogs (+ regenerated en-XA). The dynamic variant (a conditional row that reflects the live override) remains tracked in #2928.

Watch 2 — command-palette dead end in browsers: fixed. DeveloperPanel now matches the Zoom Level precedent (DisplayPanel.tsx): without the bridge it renders the label plus an explanatory unsupported row ("Available in the desktop app only — a browser dashboard has no local gateway to control.", new key in all 12 catalogs) instead of nothing, so the developer.run-local-gateway palette entry lands on a real target. Test updated to pin label-present / switch-absent in browsers.

Suggestion — paragraph-wall dialog message: fixed. The unreachable-remote message is now three short paragraphs (what happened / Retry remedy / terminal escape), and the shared dialog's .msg style gained white-space: pre-line — a no-op for every existing single-paragraph caller.

@patrigao
patrigao force-pushed the fix/local-gateway-toggle-2909 branch from 785d4de to 889f5f7 Compare August 12, 2026 02:16
@github-actions github-actions Bot added readiness: action required A blocking check or review needs attention and removed readiness: checking Automated validation is still running labels Aug 12, 2026
@patrigao
patrigao force-pushed the fix/local-gateway-toggle-2909 branch from 889f5f7 to 8765eba Compare August 12, 2026 02:38
@github-actions github-actions Bot added readiness: checking Automated validation is still running readiness: action required A blocking check or review needs attention and removed readiness: action required A blocking check or review needs attention readiness: checking Automated validation is still running labels Aug 12, 2026
@github-actions github-actions Bot added readiness: checking Automated validation is still running readiness: passed Eligible automated validation passed for the current revision and removed readiness: checking Automated validation is still running labels Aug 12, 2026
@patrigao
patrigao force-pushed the fix/local-gateway-toggle-2909 branch from 8765eba to bcff975 Compare August 12, 2026 03:30
@github-actions github-actions Bot added readiness: checking Automated validation is still running and removed readiness: passed Eligible automated validation passed for the current revision labels Aug 12, 2026
@patrigao

Copy link
Copy Markdown
Contributor Author

Dispositions for the UX Review (Fable 5) CONCERNS on 8765eba, final content round bcff9758f342d7829ca43c83820f5588c4e7f2fa:

Suggestion — stale screenshots: fixed. Both temp-screenshots/local-gateway-toggle-2909/ images retaken against the current tree; they now show the shipped four-sentence helper including "Ignored when no remote gateway is configured."

Suggestion — trailing ellipsis on the decline status: fixed. sendStatus("Can't reach <host>") — the ellipsis is gone; the status no longer implies ongoing work on a terminal decline.

Watch 1 — no in-app recovery ("Start a local gateway instead" button): accepted-and-deferred. This escalates a concern already dispositioned on caaa86b: adding a spawn-authorizing action to showGatewayErrorDialog changes a dialog shared by every gateway-failure caller, and a one-shot override that spawns what the user explicitly turned off is a product decision about how strong the setting's promise is — not something an advisory round on this PR should decide. The terminal escape is now named in the dialog copy, and the in-app affordance is tracked with the rest of the off-state UX in #2928 (comment added there).

Watch 2 — toggle renders "off" while spawning (disable-when-no-remote): accepted-and-deferred, same grounds as its two prior dispositions: disabling the toggle requires the renderer to know whether a remote host is configured for the primary port — the exact new IPC read that #2928 scopes. The one-sentence disclosure shipped on 889f5f7 is this PR's proportionate mitigation.

Both deferred items are consolidated in #2928 so the off-state UX ships as one coherent change rather than piecemeal advisory patches.

@github-actions github-actions Bot added readiness: action required A blocking check or review needs attention and removed readiness: checking Automated validation is still running labels Aug 12, 2026
@patrigao
patrigao force-pushed the fix/local-gateway-toggle-2909 branch from bcff975 to a11e29e Compare August 12, 2026 20:19
@github-actions github-actions Bot added readiness: checking Automated validation is still running readiness: action required A blocking check or review needs attention and removed readiness: action required A blocking check or review needs attention readiness: checking Automated validation is still running labels Aug 12, 2026
The desktop app always spawns a local gateway when nothing answers its
port probe, even for users who point it at a remote gateway and want
this machine to act as a pure client. That costs laptop memory/CPU for
a backend nothing uses, and a silently spawned local backend can wear a
remote tab's name when the remote is briefly unreachable at launch.

Add one store-backed boolean, runLocalGateway (default on = today's
behavior), surfaced as a Settings > Developer toggle via an IPC bridge
shaped like zoomAPI. startGateway() routes every spawn arm through a
pure decision module: only the explicit opt-out PAIRED with a
configured remote host declines to spawn — off-with-no-remote still
spawns (refusing would brick the launch), and a gateway THIS session
already spawned keeps its recovery respawns regardless of the persisted
value (the setting gates launches, not the running session). When the
spawn is declined, boot fail-fasts to the existing gateway error dialog
with a plain "can't reach <host>" message; Retry re-probes through the
same gate and never spawns. Ownership booleans stay untouched on the
declined path. Next-launch scoped by design; the helper text says so.

Closes #2909
@patrigao
patrigao force-pushed the fix/local-gateway-toggle-2909 branch from a11e29e to 3e6ce46 Compare August 12, 2026 21:16
@github-actions github-actions Bot added readiness: checking Automated validation is still running readiness: action required A blocking check or review needs attention readiness: passed Eligible automated validation passed for the current revision merge conflict Branch has merge conflicts with its base — author must resolve before merge and removed readiness: action required A blocking check or review needs attention readiness: checking Automated validation is still running labels Aug 12, 2026
@patrigao

Copy link
Copy Markdown
Contributor Author

🤖 Kiro Crew Auto-Pipeline [operator: patrigao] — closing as superseded: #2917 (merged 2026-08-13T02:19Z, commit 5d64560) landed the same run-local-gateway Developer setting and closed #2909. The both-added conflicts on this branch are against that implementation, so a rebase would just duplicate shipped code.

@patrigao patrigao closed this Aug 13, 2026
@github-actions github-actions Bot removed the readiness: passed Eligible automated validation passed for the current revision label Aug 13, 2026
@bolichen97
bolichen97 deleted the fix/local-gateway-toggle-2909 branch August 15, 2026 01:32
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

merge conflict Branch has merge conflicts with its base — author must resolve before merge

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Setting to turn off the local gateway when you only use a remote one

1 participant