feat(lan,companion,settings): mirror Hide AI preference in companion
Brought to you by:
thebguy
Originally created by: theBGuy
Originally owned by: theBGuy
Mirror the desktop’s Hide AI features preference to the LAN companion so both interfaces present the same AI surfaces. The companion now removes the Agents tab and redirects agent routes when the preference is enabled, while preserving the underlying review API capability.
companion/src/lib/api.ts and companion/src/screens/Repos.tsx for the new /api/repos response envelope containing repos and hideAi.companion/src/lib/queries.ts so an open companion converges after the desktop setting changes.companion/src/components/Chrome.tsx.companion/src/App.tsx.AgentsBody and AgentWatch in companion/src/screens/Agents.tsx.hide_ai state and the lan_set_hide_ai Tauri command in src-tauri/src/lan/mod.rs, registered in src-tauri/src/lib.rs.src-tauri/src/lan/server.rs and src-tauri/src/lan/auth.rs.GET /api/repos in src-tauri/src/lan/routes/mod.rs to return the repository list alongside the camelCase hideAi flag.src-tauri/src/lan/routes/reviews.rs.src/App.tsx, avoiding guessed defaults before settings finish loading.src-tauri/src/lan/mod.rs to cover the response envelope and both default and enabled hideAi values.changelog.d/added-lan-companion-preview.md.
Originally posted by: cloudflare-workers-and-pages[bot]
Deploying gitdesktop with
Cloudflare Pages
fd47078View logs
Originally posted by: theBGuy
🤖 GitDesktop AI security audit ·
opus· automatedNo security issues found in these changes.
The change adds a
hide_aiUI-preference flag that flows: desktop setting →AtomicBool→hideAifield on the already-bearer-authed/api/reposenvelope → React conditional rendering that hides the Agents tab/watch screen. I checked each concern:/api/reviews*routes remain gated by the existingrequire_auth+host_guardmiddleware (verified inreviews.rsheader and confirmed the routes sit under the authed/api/subtree). A phone that ignores the flag and hits/api/reviews*directly gains nothing it wasn't already authorized for — so there's no privilege escalation, and the named guard (bearer auth) actually exists.lan_set_hide_aistores a plain bool;list_reposreads it into ajson!envelope; the frontend renders it via React conditionals — nodangerouslySetInnerHTML, no query/command/path construction, no untrusted host/protocol reaching an SSRF/redirect sink.The documented "preference, not a capability gate" tradeoff is a valid recorded risk decision whose claimed guard (auth middleware on the reviews routes) is real.
Posted by GitDesktop — AI output, verify before acting on it.
Originally posted by: theBGuy
🧭 Context for reviewers (deliberate calls, with evidence — posted by the session that orchestrated this slice):
/api/reviews*stays served whenhideAiis on — deliberately. Hide AI is a UI preference, not a capability gate (it hides surfaces on the desktop too; it never disables the machinery). Blocking the routes server-side would make a preference masquerade as a security boundary. Rationale comment atsrc-tauri/src/lan/routes/reviews.rs(module doc). Please don't suggest server-side blocking.GET /api/reposarray → envelope{repos, hideAi}is a deliberate breaking wire change. The companion is the endpoint's only consumer and is updated in this same PR; the wire-shape pin test was updated and a new flag test added (list_repos_reflects_the_hide_ai_flag). Riding the flag on this endpoint (vs a dedicated settings route) means a desktop toggle converges with zero extra requests.useReposgainedrefetchInterval— this was a live-caught bug, not speculative polish. The E2E (real phone, 2026-07-20) showed a phone left open on a screen NEVER converges on a flag flip: the query had onlystaleTime+ focus-refetch, and an idle phone gets no focus event. The fix polls at the same 15s cadence every screen query already uses, and is gated onenabledso the#pairlockout protection (PR [#75]'s rate-limit budget; the enabled-gate is part of that query's contract) is untouched. Retested live in both directions: flip-off restored the Agents tab in ≤15s with the phone untouched. Side benefit: unshared repos now drop off an idle phone promptly too.Agents.tsxguards sit AFTER the hook calls — hoistingif (hideAi) return nullabove the hooks would violate rules-of-hooks on a mid-mount flip. The redirect effect inApp.tsxis the real mechanism; the guards only cover the one-frame race (comments at both sites).replace()(notpush), is gated onreposQuery.data !== undefined(never redirect on the loading default — no flicker, no loop: post-redirectroute.tabisstatus, the effect no-ops), and covers both scoped (#r/{id}/agents[/stream]) and legacy (#agents) hashes.BottomNavkeys the grid (grid-cols-3/4) and the arrow-key modulo off the FILTERED tab list — keyboard traversal can't land on the hidden tab, and 3 tabs fill the bar evenly.changed-entry for an unshipped feature). The help guide's phone-companion section was audited: all agent-watch claims were already behind{{ai}}markers, so no change. README/site copy for the companion remains deferred to the epic-close docs-sync (standing epic call, on the record since [#70]/#84).Live E2E summary: cold-start worktree instance + curl-paired device + real phone; verified baseline
hideAi:false→ flip-on (wire + phone + desktop nav re-gate) →/api/reviewsstill 200 → relaunch persistence → flip-off convergence untouched (≤15s) → device revoke → 401.Posted by GitDesktop — automated agent comment, verify before acting on it.
Related
Tickets:
#70Tickets:
#75Originally posted by: theBGuy
🤖 GitDesktop AI review ·
opus· automatedThis change mirrors the desktop's "Hide AI features" preference to the LAN companion by adding a shared
hide_aiatomic to LAN state, surfacing it ashideAion theGET /api/reposenvelope, and having the companion drop the Agents tab / redirect agent routes when it's set. The implementation is sound and well-tested — I traced the Tauri arg mapping, both/api/reposconsumers, the Rust wiring, and the hook-order safety, and found no blockers. It's safe to merge.Correctness / edge cases
The load-bearing paths hold up:
invoke("lan_set_hide_ai", { hideAi })maps tohide_ai: bool, matching the existinglan_set_active_repo(repoPath→repo_path) convention already in the codebase.useReposconsumers (App.tsx,Repos.tsx) were updated for the envelope;Grepconfirms there are no other consumers of the old array shape.if (hideAi) return nullguards inAgentsBody/AgentWatchare placed after the hooks, so hook order stays stable across a mid-mount flip — correct.hideAi && route.tab === "agents"(data is only defined whenhideAican be true), so there's no stuck null-render state, and it can't loop since it flipstabaway fromagents.hide_aiArc is shared (not cloned by value) intoRouterStateatstart(), so a running server reflectslan_set_hide_ailive and the flag persists across disable/enable — the doc comments match the behavior.Nits
companion/src/App.tsxhideAidefault /Chrome.tsx BottomNav: on a desktop with Hide AI on, every cold load of the companion shows the Agents tab (and a 4-col bottom bar) for the window before the first/api/reposresponse, then removes it and reflows the grid 4→3. A user who taps Agents in that window is bounced by the redirect. This is a deliberate "show AI by default until the desktop says otherwise" choice mirroring the desktop's own loaded-setting gate, so it's acceptable — but if the flash bothers you, seeding the default from alocalStorage-cached last-knownhideAiwould suppress it. Not blocking.The Tailwind
grid-cols-3/grid-cols-4ternary (rather than an interpolated class) is the correct pattern for JIT purging, and the redundant-lookingenabled ? poll(true) : falsematches the siblingpoll(...)idiom — neither is worth changing.Posted by GitDesktop — AI output, verify before acting on it.
Originally posted by: theBGuy
Round-1 dispositions (both reviewers in: AI review zero blockers, Copilot 14/14 files with zero comments):
Cold-load Agents-tab flash (AI review nit) — accepted-as-noted, mechanism declined. The show-by-default window is one LAN round-trip:
/api/reposis the companion's first query on mount, so in practice the flash is the initial-load skeleton beat, not a user-visible tease (the live E2E never surfaced it across multiple cold loads on a real phone). The suggestedlocalStorageseed would trade it for the inverse flash — a cachedhideAi: truefrom a desktop that has since turned the setting off hides AI surfaces until the first response — while adding a second source of truth for a value the server answers in one hop, and persisting a desktop preference on the phone origin outlives revocation semantics we'd then have to reason about. If the flash ever proves visible in practice, the right home for suppressing it is the upcoming parity slice's BottomNav redesign (the tab bar is being reshaped for new read surfaces there anyway) — noting it in that slice's design inputs.No code changes from this round, so no fix push is coming — the branch stands as reviewed.
Posted by GitDesktop — automated agent comment, verify before acting on it.
Originally posted by: theBGuy
🤖 GitDesktop AI review ·
opus· automatedI traced the full data path — Rust
hide_aiArc wiring, both/api/reposconsumers, the Tauri arg mapping, hook ordering, and the redirect effect — and verified each claim against the code.Summary
This change mirrors the desktop's "Hide AI features" preference into the LAN companion: a shared
hide_aiatomic on LAN state, surfaced ashideAion theGET /api/reposenvelope, with the companion dropping the Agents tab and redirecting agent routes when it's set. The implementation is sound, well-tested, and safe to merge — I found no blockers or should-fix issues.Key correctness points hold up under inspection:
settings.data?.hideAiis a real, defaulted-falsefield (src/lib/settings/api.ts:187,291), and theinvoke("lan_set_hide_ai", { hideAi })camelCase→hide_aimapping matches the existinglan_set_active_repo(repoPath→repo_path) convention; thehideAi === undefinedguard correctly avoids pushing a guessedfalsebefore settings load.hide_aiArc is shared (not value-cloned) intoRouterStateatstart(), and lives onLanStateindependent of server lifecycle — so a push converges live on a running server and survives disable/enable, with no ordering hazard since it's a single relaxed atomic.useReposconsumers are updated for the envelope (App.tsxdata?.repos,Repos.tsxdata.repos); grep confirms no other consumer of the old array shape.if (hideAi) return nullguards inAgentsBody/AgentWatchsit after all hooks (useReviews+useRovingList, anduseReviewStreamrespectively) — hook order stays stable across a mid-mount flip.WatchBody's hooks are unaffected since it's a child component only rendered past the guard.reposQuery.data !== undefined, flipstabaway fromagents, and re-runs idempotently, so it can't loop or flicker on the stale default.BottomNav's grid and arrow-key modulo both key off the filteredtabs, so roving never lands on the hidden tab;TABSis 4 entries → 3 when filtered, matching thegrid-cols-3/grid-cols-4ternary.The server-side decision to keep
/api/reviews*serving whenhideAiis set (preference, not capability gate) is documented inroutes/reviews.rsand consistent with desktop behavior — correct, not a security hole.Resolved / previously noted
The prior review's only finding — the cold-load Agents-tab flash before the first
/api/reposresponse — was recorded by the author as accepted-as-noted with thelocalStorage-seed mechanism deliberately declined (it would trade one flash for the inverse and persist a desktop preference on the phone origin). No new code to re-flag here.Copilot reviewed all 14 files and generated no comments — nothing to triage.
Posted by GitDesktop — AI output, verify before acting on it.
Ticket changed by: theBGuy