Menu

#92 feat(lan,companion,settings): mirror Hide AI preference in companion

closed
nobody
2026-07-20
2026-07-20
Anonymous
No

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 UI

  • Updates companion/src/lib/api.ts and companion/src/screens/Repos.tsx for the new /api/repos response envelope containing repos and hideAi.
  • Polls the preference in companion/src/lib/queries.ts so an open companion converges after the desktop setting changes.
  • Filters the Agents tab and adjusts keyboard navigation and grid layout in companion/src/components/Chrome.tsx.
  • Redirects hidden Agents routes to the status view in companion/src/App.tsx.
  • Adds defensive rendering guards to AgentsBody and AgentWatch in companion/src/screens/Agents.tsx.

LAN preference and API

  • Adds shared 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.
  • Passes the preference into the LAN router through src-tauri/src/lan/server.rs and src-tauri/src/lan/auth.rs.
  • Changes GET /api/repos in src-tauri/src/lan/routes/mod.rs to return the repository list alongside the camelCase hideAi flag.
  • Keeps the review endpoints available regardless of the preference, as documented in src-tauri/src/lan/routes/reviews.rs.

Desktop integration and coverage

  • Pushes the loaded desktop setting to LAN state from src/App.tsx, avoiding guessed defaults before settings finish loading.
  • Updates LAN route tests in src-tauri/src/lan/mod.rs to cover the response envelope and both default and enabled hideAi values.
  • Documents the companion behavior in changelog.d/added-lan-companion-preview.md.

Discussion

  • Anonymous

    Anonymous - 2026-07-20
     
  • Anonymous

    Anonymous - 2026-07-20

    Originally posted by: theBGuy

    🤖 GitDesktop AI security audit · opus · automated


    No security issues found in these changes.

    The change adds a hide_ai UI-preference flag that flows: desktop setting → AtomicBoolhideAi field on the already-bearer-authed /api/repos envelope → React conditional rendering that hides the Agents tab/watch screen. I checked each concern:

    • Not an auth boundary bypass. The author explicitly documents (and the code confirms) that Hide AI is a client-side UI preference, not a capability gate. The /api/reviews* routes remain gated by the existing require_auth + host_guard middleware (verified in reviews.rs header 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.
    • No injection/sink. lan_set_hide_ai stores a plain bool; list_repos reads it into a json! envelope; the frontend renders it via React conditionals — no dangerouslySetInnerHTML, no query/command/path construction, no untrusted host/protocol reaching an SSRF/redirect sink.
    • No secrets/PII, no crypto, no supply-chain ref, no prompt-injection flow — the flag never reaches an LLM prompt or a security decision.

    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.

     
  • Anonymous

    Anonymous - 2026-07-20

    Originally posted by: theBGuy

    🧭 Context for reviewers (deliberate calls, with evidence — posted by the session that orchestrated this slice):

    1. /api/reviews* stays served when hideAi is 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 at src-tauri/src/lan/routes/reviews.rs (module doc). Please don't suggest server-side blocking.
    2. GET /api/repos array → 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.
    3. useRepos gained refetchInterval — 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 only staleTime + 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 on enabled so the #pair lockout 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.
    4. Agents.tsx guards sit AFTER the hook calls — hoisting if (hideAi) return null above the hooks would violate rules-of-hooks on a mid-mount flip. The redirect effect in App.tsx is the real mechanism; the guards only cover the one-frame race (comments at both sites).
    5. The agents-route redirect uses replace() (not push), is gated on reposQuery.data !== undefined (never redirect on the loading default — no flicker, no loop: post-redirect route.tab is status, the effect no-ops), and covers both scoped (#r/{id}/agents[/stream]) and legacy (#agents) hashes.
    6. The desktop push effect skips while settings are undefined (never pushes a guessed default that would briefly un-hide AI on the phone) and re-fires when settings load — so the flag survives app relaunches. Verified live: after a dev-app relaunch the server reported the persisted value with no interaction.
    7. BottomNav keys 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.
    8. Docs: the existing unreleased changelog fragment was amended in place (one feature, one fragment — no separate 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/reviews still 200 → relaunch persistence → flip-off convergence untouched (≤15s) → device revoke → 401.


    Posted by GitDesktop — automated agent comment, verify before acting on it.

     

    Related

    Tickets: #70
    Tickets: #75

  • Anonymous

    Anonymous - 2026-07-20

    Originally posted by: theBGuy

    🤖 GitDesktop AI review · opus · automated


    This change mirrors the desktop's "Hide AI features" preference to the LAN companion by adding a shared hide_ai atomic to LAN state, surfacing it as hideAi on the GET /api/repos envelope, 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/repos consumers, 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:

    • The camelCase→snake_case arg mapping is correct — invoke("lan_set_hide_ai", { hideAi }) maps to hide_ai: bool, matching the existing lan_set_active_repo (repoPathrepo_path) convention already in the codebase.
    • Both useRepos consumers (App.tsx, Repos.tsx) were updated for the envelope; Grep confirms there are no other consumers of the old array shape.
    • The if (hideAi) return null guards in AgentsBody/AgentWatch are placed after the hooks, so hook order stays stable across a mid-mount flip — correct.
    • The redirect effect always fires when hideAi && route.tab === "agents" (data is only defined when hideAi can be true), so there's no stuck null-render state, and it can't loop since it flips tab away from agents.
    • The hide_ai Arc is shared (not cloned by value) into RouterState at start(), so a running server reflects lan_set_hide_ai live and the flag persists across disable/enable — the doc comments match the behavior.

    Nits

    • nitcompanion/src/App.tsx hideAi default / 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/repos response, 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 a localStorage-cached last-known hideAi would suppress it. Not blocking.

    The Tailwind grid-cols-3/grid-cols-4 ternary (rather than an interpolated class) is the correct pattern for JIT purging, and the redundant-looking enabled ? poll(true) : false matches the sibling poll(...) idiom — neither is worth changing.


    Posted by GitDesktop — AI output, verify before acting on it.

     
  • Anonymous

    Anonymous - 2026-07-20

    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/repos is 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 suggested localStorage seed would trade it for the inverse flash — a cached hideAi: true from 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.

     
  • Anonymous

    Anonymous - 2026-07-20

    Originally posted by: theBGuy

    🤖 GitDesktop AI review · opus · automated


    I traced the full data path — Rust hide_ai Arc wiring, both /api/repos consumers, 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_ai atomic on LAN state, surfaced as hideAi on the GET /api/repos envelope, 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?.hideAi is a real, defaulted-false field (src/lib/settings/api.ts:187,291), and the invoke("lan_set_hide_ai", { hideAi }) camelCase→hide_ai mapping matches the existing lan_set_active_repo (repoPathrepo_path) convention; the hideAi === undefined guard correctly avoids pushing a guessed false before settings load.
    • The hide_ai Arc is shared (not value-cloned) into RouterState at start(), and lives on LanState independent 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.
    • Both useRepos consumers are updated for the envelope (App.tsx data?.repos, Repos.tsx data.repos); grep confirms no other consumer of the old array shape.
    • The if (hideAi) return null guards in AgentsBody/AgentWatch sit after all hooks (useReviews+useRovingList, and useReviewStream respectively) — 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.
    • The redirect effect gates on reposQuery.data !== undefined, flips tab away from agents, 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 filtered tabs, so roving never lands on the hidden tab; TABS is 4 entries → 3 when filtered, matching the grid-cols-3/grid-cols-4 ternary.

    The server-side decision to keep /api/reviews* serving when hideAi is set (preference, not capability gate) is documented in routes/reviews.rs and 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/repos response — was recorded by the author as accepted-as-noted with the localStorage-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.

     
  • Anonymous

    Anonymous - 2026-07-20

    Ticket changed by: theBGuy

    • status: open --> closed
     

Log in to post a comment.