From db5e2c48bf0ec9995d9b7f904b6bc1b2dfcf3cbc Mon Sep 17 00:00:00 2001 From: ScriptedAlchemy Date: Sat, 7 Nov 2026 20:37:19 -0800 Subject: [PATCH 1/3] fix(mobile): add offline environment thread filter --- .../src/features/home/HomeRouteScreen.tsx | 10 +++- apps/mobile/src/features/home/HomeScreen.tsx | 11 ++++ .../src/features/home/ThreadListFilters.tsx | 23 ++++++-- .../features/home/home-list-filter-menu.ts | 8 +++ .../features/home/home-list-options.test.tsx | 35 +++++++++++- .../src/features/home/home-list-options.ts | 12 +++- .../threads/ThreadNavigationSidebar.tsx | 18 ++++-- .../features/threads/useThreadListV2.test.tsx | 55 +++++++++++++++++++ .../src/features/threads/useThreadListV2.ts | 30 ++++++++-- .../src/persistence/mobile-preferences.ts | 1 + apps/mobile/src/state/presentation.ts | 9 +-- 11 files changed, 191 insertions(+), 21 deletions(-) diff --git a/apps/mobile/src/features/home/HomeRouteScreen.tsx b/apps/mobile/src/features/home/HomeRouteScreen.tsx index 99ede8afe7c4a..0b3a0e0ad9f4f 100644 --- a/apps/mobile/src/features/home/HomeRouteScreen.tsx +++ b/apps/mobile/src/features/home/HomeRouteScreen.tsx @@ -119,6 +119,7 @@ export function HomeRouteScreen() { options: listOptions, setSelectedEnvironmentId, setSelectedProjectKey, + setHideOfflineEnvironments, } = useHomeListOptions(availableEnvironmentIds); const selectedEnvironmentId = listOptions.selectedEnvironmentId; // A legacy per-environment scratch key resolves here on every render; the stored @@ -174,7 +175,10 @@ export function HomeRouteScreen() { accessibilityLabel: "Search threads", }} filter={{ - customized: selectedEnvironmentId !== null || selectedProjectKey !== null, + customized: + selectedEnvironmentId !== null || + selectedProjectKey !== null || + listOptions.hideOfflineEnvironments, items: buildHomeListFilterMenuItems({ environments, projects: projectFilterOptions, @@ -182,6 +186,8 @@ export function HomeRouteScreen() { selectedProjectKey, onEnvironmentChange: setSelectedEnvironmentId, onProjectChange: setSelectedProjectKey, + hideOfflineEnvironments: listOptions.hideOfflineEnvironments, + onHideOfflineEnvironmentsChange: setHideOfflineEnvironments, }), }} /> @@ -204,6 +210,8 @@ export function HomeRouteScreen() { onRegenerateThreadTitle={regenerateThreadTitle} onEnvironmentChange={setSelectedEnvironmentId} onProjectChange={setSelectedProjectKey} + hideOfflineEnvironments={listOptions.hideOfflineEnvironments} + onHideOfflineEnvironmentsChange={setHideOfflineEnvironments} onOpenSettings={() => selectTab("settings")} onSearchQueryChange={setSearchQuery} onSelectThread={handleSelectThread} diff --git a/apps/mobile/src/features/home/HomeScreen.tsx b/apps/mobile/src/features/home/HomeScreen.tsx index dac883341cc10..a5910a8cbbb0d 100644 --- a/apps/mobile/src/features/home/HomeScreen.tsx +++ b/apps/mobile/src/features/home/HomeScreen.tsx @@ -102,6 +102,8 @@ interface HomeScreenProps { readonly searchQuery: string; readonly selectedEnvironmentId: EnvironmentId | null; readonly selectedProjectKey: string | null; + readonly hideOfflineEnvironments: boolean; + readonly onHideOfflineEnvironmentsChange: (value: boolean) => void; readonly projectSortOrder: HomeProjectSortOrder; readonly projectGroupingMode: SidebarProjectGroupingMode; readonly onSearchQueryChange: (query: string) => void; @@ -465,6 +467,7 @@ export function HomeScreen(props: HomeScreenProps) { threads: props.threads, pendingTasks: props.pendingTasks, environmentId: props.selectedEnvironmentId, + hideOfflineEnvironments: props.hideOfflineEnvironments, projectRefs: v2ScopedProjectGroup === null ? null : v2ScopedProjectGroup.projectRefs, searchQuery: props.searchQuery, searchMatches: threadSearchMatchByKey, @@ -740,6 +743,8 @@ export function HomeScreen(props: HomeScreenProps) { selectedProjectKey={props.selectedProjectKey} onEnvironmentChange={props.onEnvironmentChange} onProjectChange={props.onProjectChange} + hideOfflineEnvironments={props.hideOfflineEnvironments} + onHideOfflineEnvironmentsChange={props.onHideOfflineEnvironmentsChange} /> ) : null} @@ -752,6 +757,12 @@ export function HomeScreen(props: HomeScreenProps) { detail={`No threads matching "${props.searchQuery}".`} variant={Platform.OS === "android" ? "plain" : undefined} /> + ) : props.hideOfflineEnvironments ? ( + ) : v2ScopedProjectGroup !== null ? ( ; readonly selectedEnvironmentId: EnvironmentId | null; readonly selectedProjectKey: string | null; + readonly hideOfflineEnvironments: boolean; + readonly onHideOfflineEnvironmentsChange: (value: boolean) => void; readonly onEnvironmentChange: (environmentId: EnvironmentId | null) => void; readonly onProjectChange: (projectKey: string | null) => void; /** List padding the scroller bleeds through so pills start where row content does. */ @@ -31,9 +33,12 @@ export function ThreadListFilters(props: { contentContainerStyle={{ paddingHorizontal: 20, paddingTop: 2, paddingBottom: 10, gap: 8 }} > { - if (id === "all") props.onEnvironmentChange(null); + if (id === "hide-offline") + props.onHideOfflineEnvironmentsChange(!props.hideOfflineEnvironments); + else if (id === "all") props.onEnvironmentChange(null); else { const environment = props.environments.find( (candidate) => JSON.stringify(candidate.environmentId) === id, @@ -85,7 +97,9 @@ export function ThreadListFilters(props: { } }} /> - {props.selectedEnvironmentId !== null || props.selectedProjectKey !== null ? ( + {props.selectedEnvironmentId !== null || + props.selectedProjectKey !== null || + props.hideOfflineEnvironments ? ( { props.onEnvironmentChange(null); props.onProjectChange(null); + props.onHideOfflineEnvironmentsChange(false); }} > diff --git a/apps/mobile/src/features/home/home-list-filter-menu.ts b/apps/mobile/src/features/home/home-list-filter-menu.ts index 617c3fa0988ef..35e3dc177cdcf 100644 --- a/apps/mobile/src/features/home/home-list-filter-menu.ts +++ b/apps/mobile/src/features/home/home-list-filter-menu.ts @@ -17,6 +17,8 @@ export function buildHomeListFilterMenuItems(props: { readonly projects: ReadonlyArray; readonly selectedEnvironmentId: EnvironmentId | null; readonly selectedProjectKey: string | null; + readonly hideOfflineEnvironments: boolean; + readonly onHideOfflineEnvironmentsChange: (value: boolean) => void; readonly onEnvironmentChange: (environmentId: EnvironmentId | null) => void; readonly onProjectChange: (projectKey: string | null) => void; }): AppBarMenuItem[] { @@ -64,5 +66,11 @@ export function buildHomeListFilterMenuItems(props: { }); } + items.push({ + id: "hide-offline-environments", + title: "Hide offline environments", + selected: props.hideOfflineEnvironments, + onPress: () => props.onHideOfflineEnvironmentsChange(!props.hideOfflineEnvironments), + }); return items; } diff --git a/apps/mobile/src/features/home/home-list-options.test.tsx b/apps/mobile/src/features/home/home-list-options.test.tsx index f25d4ff690851..8d366af409bc8 100644 --- a/apps/mobile/src/features/home/home-list-options.test.tsx +++ b/apps/mobile/src/features/home/home-list-options.test.tsx @@ -38,12 +38,45 @@ it("saves the scope from every update made in one event", async () => { act(() => { controls!.setSelectedEnvironmentId(ENVIRONMENT); controls!.setSelectedProjectKey("env-1:project-1"); + controls!.setHideOfflineEnvironments(true); }); expect(harness.saved.at(-1)).toEqual({ - threadListFilters: { environmentId: ENVIRONMENT, projectKey: "env-1:project-1" }, + threadListFilters: { + environmentId: ENVIRONMENT, + projectKey: "env-1:project-1", + hideOfflineEnvironments: true, + }, }); expect(controls!.options.selectedEnvironmentId).toBe(ENVIRONMENT); expect(controls!.options.selectedProjectKey).toBe("env-1:project-1"); + expect(controls!.options.hideOfflineEnvironments).toBe(true); + const savedFilters = controls!.options; act(() => root.unmount()); + const restored = createRoot(document.createElement("div")); + await act(async () => + restored.render( + + + , + ), + ); + expect(controls!.options.hideOfflineEnvironments).toBe(true); + act(() => controls!.setHideOfflineEnvironments(false)); + expect(controls!.options.hideOfflineEnvironments).toBe(false); + expect(harness.saved.at(-1)).toEqual({ + threadListFilters: { + environmentId: ENVIRONMENT, + projectKey: "env-1:project-1", + hideOfflineEnvironments: false, + }, + }); + act(() => restored.unmount()); }); diff --git a/apps/mobile/src/features/home/home-list-options.ts b/apps/mobile/src/features/home/home-list-options.ts index 09c8be2c9a7c1..e92b931293421 100644 --- a/apps/mobile/src/features/home/home-list-options.ts +++ b/apps/mobile/src/features/home/home-list-options.ts @@ -9,6 +9,7 @@ import { updateMobilePreferencesAtom } from "../../state/preferences"; import type { HomeProjectSortOrder } from "./homeThreadList"; export interface HomeListOptions { + readonly hideOfflineEnvironments: boolean; readonly selectedEnvironmentId: EnvironmentId | null; readonly selectedProjectKey: string | null; readonly projectSortOrder: HomeProjectSortOrder; @@ -20,6 +21,7 @@ export interface ResolvedHomeListOptions extends HomeListOptions { function defaultHomeListOptions(filters?: ThreadListFilters): HomeListOptions { return { + hideOfflineEnvironments: filters?.hideOfflineEnvironments ?? false, selectedEnvironmentId: filters?.environmentId ?? null, selectedProjectKey: filters?.projectKey ?? null, projectSortOrder: @@ -49,7 +51,7 @@ export function HomeListOptionsProvider({ const [optionsAtom] = useState(() => Atom.make(defaultHomeListOptions(savedFilters))); const options = useAtomValue(optionsAtom); const savePreferences = useAtomSet(updateMobilePreferencesAtom); - // Only the scope persists; the sort order is per session. Updates read the + // Only the filters persist; the sort order is per session. Updates read the // registry, so several in one event each build on the last and save it. const updateOptions = (patch: Partial) => { const current = registry.get(optionsAtom); @@ -57,12 +59,14 @@ export function HomeListOptionsProvider({ registry.set(optionsAtom, next); if ( next.selectedEnvironmentId !== current.selectedEnvironmentId || - next.selectedProjectKey !== current.selectedProjectKey + next.selectedProjectKey !== current.selectedProjectKey || + next.hideOfflineEnvironments !== current.hideOfflineEnvironments ) { savePreferences({ threadListFilters: { environmentId: next.selectedEnvironmentId, projectKey: next.selectedProjectKey, + hideOfflineEnvironments: next.hideOfflineEnvironments, }, }); } @@ -98,6 +102,9 @@ export function useHomeListOptions(availableEnvironmentIds: ReadonlySet { updateOptions({ selectedProjectKey: value }); }; + const setHideOfflineEnvironments = (value: boolean) => { + updateOptions({ hideOfflineEnvironments: value }); + }; const setProjectSortOrder = (value: HomeProjectSortOrder) => { updateOptions({ projectSortOrder: value }); }; @@ -106,5 +113,6 @@ export function useHomeListOptions(availableEnvironmentIds: ReadonlySet environment.environmentId), ); - const { options, setSelectedEnvironmentId, setSelectedProjectKey } = + const { options, setSelectedEnvironmentId, setSelectedProjectKey, setHideOfflineEnvironments } = useHomeListOptions(availableEnvironmentIds); const searchEnvironmentIds = options.selectedEnvironmentId === null @@ -268,6 +268,7 @@ function ThreadNavigationSidebarPane(props: ThreadNavigationSidebarProps) { threads, pendingTasks, environmentId: options.selectedEnvironmentId, + hideOfflineEnvironments: options.hideOfflineEnvironments, projectRefs: selectedProjectScope === null ? null : selectedProjectScope.projectRefs, searchQuery: props.searchQuery, searchMatches: threadSearchMatchByKey, @@ -535,7 +536,10 @@ function ThreadNavigationSidebarPane(props: ThreadNavigationSidebarProps) { accessibilityLabel: "Search threads", }} filter={{ - customized: options.selectedEnvironmentId !== null || selectedProjectKey !== null, + customized: + options.selectedEnvironmentId !== null || + selectedProjectKey !== null || + options.hideOfflineEnvironments, items: buildHomeListFilterMenuItems({ environments, projects: projectFilterOptions, @@ -543,6 +547,8 @@ function ThreadNavigationSidebarPane(props: ThreadNavigationSidebarProps) { selectedProjectKey, onEnvironmentChange: setSelectedEnvironmentId, onProjectChange: setSelectedProjectKey, + hideOfflineEnvironments: options.hideOfflineEnvironments, + onHideOfflineEnvironmentsChange: setHideOfflineEnvironments, }), }} sidebar={{ @@ -568,9 +574,11 @@ function ThreadNavigationSidebarPane(props: ThreadNavigationSidebarProps) { ? threadSearch.isPending ? "Searching thread messages…" : "No matching threads" - : selectedProjectScope !== null - ? `No threads in ${selectedProjectScope.title}` - : "No threads yet"} + : options.hideOfflineEnvironments + ? "No visible threads. Turn off Hide offline environments or choose another filter." + : selectedProjectScope !== null + ? `No threads in ${selectedProjectScope.title}` + : "No threads yet"} ); diff --git a/apps/mobile/src/features/threads/useThreadListV2.test.tsx b/apps/mobile/src/features/threads/useThreadListV2.test.tsx index 7eac8955efa14..48a3aefe408d6 100644 --- a/apps/mobile/src/features/threads/useThreadListV2.test.tsx +++ b/apps/mobile/src/features/threads/useThreadListV2.test.tsx @@ -8,6 +8,8 @@ import { EnvironmentId, ProjectId, ThreadId } from "@t3tools/contracts"; import { Atom, AtomRegistry } from "effect/reactivity"; import { expect, it, vi } from "vite-plus/test"; +import { offlineEnvironmentIdsAtom } from "../../state/presentation"; +import type { PendingNewTask } from "../../state/use-pending-new-tasks"; import { olderSettledThreads } from "../../state/threads"; import { makeRawThreadShell } from "../../test-fixtures"; import { useThreadListV2 } from "./useThreadListV2"; @@ -16,6 +18,10 @@ vi.mock("../../state/threads", () => ({ olderSettledThreads: { shelfAtom: vi.fn() }, threadFamilies: { mountRows: () => () => {} }, })); +vi.mock("../../state/presentation", async () => { + const { Atom } = await import("effect/reactivity"); + return { offlineEnvironmentIdsAtom: Atom.make>([]) }; +}); vi.mock("../../state/atom-registry", () => ({ appAtomRegistry: undefined })); it("reuses a large thread list across tab renders and updates when its inputs change", async () => { @@ -23,6 +29,7 @@ it("reuses a large thread list across tab renders and updates when its inputs ch using _globals = { [Symbol.dispose]: () => vi.unstubAllGlobals() }; const shelfAtom = Atom.make>(new Map()); vi.mocked(olderSettledThreads.shelfAtom).mockReturnValue(shelfAtom); + const offlineIds = offlineEnvironmentIdsAtom as Atom.Writable>; const registry = AtomRegistry.make(); using _registry = { [Symbol.dispose]: () => registry.dispose() }; const environmentId = EnvironmentId.make("environment-1"); @@ -43,6 +50,7 @@ it("reuses a large thread list across tab renders and updates when its inputs ch threads, pendingTasks: [], environmentId: null, + hideOfflineEnvironments: false, projectRefs: null, searchQuery: "", searchMatches: new Map(), @@ -109,6 +117,45 @@ it("reuses a large thread list across tab renders and updates when its inputs ch seen.at(-1)!.layout.items.every((item) => item.thread.environmentId === environmentId), ).toBe(true); + await act(async () => registry.set(offlineIds, [otherEnvironmentId])); + const pendingTasks: ReadonlyArray = [environmentId, otherEnvironmentId].map( + (id) => ({ + kind: "draft", + key: `draft-${id}`, + environmentId: id, + projectId: ProjectId.make("project-test"), + projectTitle: undefined, + projectCwd: undefined, + title: "Unsent draft", + createdAt: input.now, + draftKey: `draft-${id}`, + draft: { text: "Unsent draft", attachments: [] }, + }), + ); + const hiddenInput = { ...input, pendingTasks, hideOfflineEnvironments: true }; + await act(async () => root.render()); + expect(seen.at(-1)!.layout.items).toHaveLength(85); + expect(seen.at(-1)!.layout.settledCount).toBe(525); + expect( + seen + .at(-1)! + .items.filter((item) => item.type === "v2-pending") + .map((item) => item.pendingTask.environmentId), + ).toEqual([environmentId]); + expect( + seen.at(-1)!.layout.items.every((item) => item.thread.environmentId === environmentId), + ).toBe(true); + expect( + vi + .mocked(olderSettledThreads.shelfAtom) + .mock.lastCall?.[0].map(({ scope }) => scope.environmentId), + ).toEqual([environmentId]); + await act(async () => root.render()); + expect(seen.at(-1)!.layout.items).toHaveLength(160); + await act(async () => root.render()); + await act(async () => registry.set(offlineIds, [])); + expect(seen.at(-1)!.layout.items).toHaveLength(160); + const queuedThreadKeys = new Set([`${environmentId}:thread-150`]); await act(async () => root.render()); const queued = seen.at(-1)!; @@ -241,4 +288,12 @@ it("reuses a large thread list across tab renders and updates when its inputs ch expect(loaded.layout.items).toHaveLength(160); expect(loaded.olderSettledShelf.isListed(threads[150]!)).toBe(true); expect(loaded.items).not.toBe(loading.items); + await act(async () => registry.set(offlineIds, [environmentId])); + await act(async () => root.render()); + expect(seen.at(-1)!.layout.settledCount).toBe(525); + expect( + seen.at(-1)!.layout.items.every((item) => item.thread.environmentId === otherEnvironmentId), + ).toBe(true); + await act(async () => root.render()); + expect(seen.at(-1)!.layout.settledCount).toBe(1100); }); diff --git a/apps/mobile/src/features/threads/useThreadListV2.ts b/apps/mobile/src/features/threads/useThreadListV2.ts index fcf1048d2b6af..61c82bdbc9680 100644 --- a/apps/mobile/src/features/threads/useThreadListV2.ts +++ b/apps/mobile/src/features/threads/useThreadListV2.ts @@ -1,7 +1,9 @@ +import { useAtomValue } from "@effect/atom-react"; import type { OlderSettledScope } from "@t3tools/client-runtime/state/threads"; import type { EnvironmentId } from "@t3tools/contracts"; import { useMemo, useSyncExternalStore } from "react"; +import { offlineEnvironmentIdsAtom } from "../../state/presentation"; import { scopedProjectKey } from "../../lib/scopedEntities"; import { useOlderSettledShelf, useOlderSettledShelfGrowth } from "../../state/olderSettledShelf"; import type { PendingNewTask } from "../../state/use-pending-new-tasks"; @@ -46,6 +48,7 @@ function observeInbox( export function useThreadListV2(input: { readonly threads: LayoutInput["threads"]; readonly pendingTasks: ReadonlyArray; + readonly hideOfflineEnvironments: boolean; readonly environmentId: EnvironmentId | null; readonly projectRefs: LayoutInput["projectRefs"]; readonly searchQuery: string; @@ -66,7 +69,19 @@ export function useThreadListV2(input: { readonly settledShelfExpanded: boolean; readonly selectedThreadKey: string | null; }) { - const { threads, workingShelfEnabled } = input; + const { workingShelfEnabled } = input; + const offlineEnvironmentIds = useAtomValue(offlineEnvironmentIdsAtom); + const hiddenEnvironmentIds = useMemo( + () => new Set(input.hideOfflineEnvironments ? offlineEnvironmentIds : []), + [input.hideOfflineEnvironments, offlineEnvironmentIds], + ); + const threads = useMemo( + () => + hiddenEnvironmentIds.size === 0 + ? input.threads + : input.threads.filter((thread) => !hiddenEnvironmentIds.has(thread.environmentId)), + [input.threads, hiddenEnvironmentIds], + ); const layoutTime = useMemo( () => threadListV2LayoutTime({ threads, now: input.now, searchQuery: input.searchQuery }), [threads, input.now, input.searchQuery], @@ -121,8 +136,8 @@ export function useThreadListV2(input: { environmentId: input.environmentId, projectRefs: input.projectRefs, settlementEnvironmentIds: input.settlementEnvironmentIds, - }), - [input.environmentId, input.projectRefs, input.settlementEnvironmentIds], + }).filter((scope) => !hiddenEnvironmentIds.has(scope.environmentId)), + [input.environmentId, input.projectRefs, input.settlementEnvironmentIds, hiddenEnvironmentIds], ); const olderSettledShelf = useOlderSettledShelf({ scopes, @@ -201,6 +216,7 @@ export function useThreadListV2(input: { const searchQuery = input.searchQuery.trim().toLocaleLowerCase(); return input.pendingTasks.filter( (pendingTask) => + !hiddenEnvironmentIds.has(pendingTask.environmentId) && (input.environmentId === null || pendingTask.environmentId === input.environmentId) && (scopedProjectKeys === null || scopedProjectKeys.has( @@ -208,7 +224,13 @@ export function useThreadListV2(input: { )) && (searchQuery.length === 0 || pendingTask.title.toLocaleLowerCase().includes(searchQuery)), ); - }, [input.projectRefs, input.searchQuery, input.pendingTasks, input.environmentId]); + }, [ + input.projectRefs, + input.searchQuery, + input.pendingTasks, + input.environmentId, + hiddenEnvironmentIds, + ]); const items = useMemo( () => buildThreadListV2ListItems({ diff --git a/apps/mobile/src/persistence/mobile-preferences.ts b/apps/mobile/src/persistence/mobile-preferences.ts index 436f3b172cdae..80b884ea75776 100644 --- a/apps/mobile/src/persistence/mobile-preferences.ts +++ b/apps/mobile/src/persistence/mobile-preferences.ts @@ -39,6 +39,7 @@ export const PullRequestListControls = Schema.Struct({ export type PullRequestListControls = typeof PullRequestListControls.Type; const ThreadListFilters = Schema.Struct({ + hideOfflineEnvironments: Schema.optional(Schema.Boolean), environmentId: Schema.NullOr(EnvironmentId), projectKey: Schema.NullOr(Schema.String), }); diff --git a/apps/mobile/src/state/presentation.ts b/apps/mobile/src/state/presentation.ts index 68b2d278cd95b..93195a407a5d5 100644 --- a/apps/mobile/src/state/presentation.ts +++ b/apps/mobile/src/state/presentation.ts @@ -17,10 +17,11 @@ export const environmentPresentations = createEnvironmentPresentationAtoms({ serverConfigValueAtom: serverEnvironment.configValueAtom, }); -export const { connectedEnvironmentIdsAtom } = createEnvironmentSummaryAtoms({ - catalogValueAtom: environmentCatalog.catalogValueAtom, - presentationAtom: environmentPresentations.presentationAtom, -}); +export const { connectedEnvironmentIdsAtom, offlineEnvironmentIdsAtom } = + createEnvironmentSummaryAtoms({ + catalogValueAtom: environmentCatalog.catalogValueAtom, + presentationAtom: environmentPresentations.presentationAtom, + }); export const threadListEnvironmentsAtom = createThreadListEnvironmentsAtom( environmentServerConfigsAtom, From 080cb947605bc07431d42b2650b346fe961ebd26 Mon Sep 17 00:00:00 2001 From: ScriptedAlchemy Date: Sat, 7 Nov 2026 21:23:10 -0800 Subject: [PATCH 2/3] fix(mobile): move offline filter to Labs Zack section --- apps/mobile/src/features/home/HomeRouteScreen.tsx | 9 +-------- apps/mobile/src/features/home/HomeScreen.tsx | 4 +--- .../src/features/home/ThreadListFilters.tsx | 15 ++------------- .../src/features/home/home-list-filter-menu.ts | 8 -------- .../features/settings/ThreadSettingsSections.tsx | 14 ++++++++++++++ .../src/features/settings/settings-search.ts | 1 + .../features/threads/ThreadNavigationSidebar.tsx | 11 +++-------- 7 files changed, 22 insertions(+), 40 deletions(-) diff --git a/apps/mobile/src/features/home/HomeRouteScreen.tsx b/apps/mobile/src/features/home/HomeRouteScreen.tsx index 0b3a0e0ad9f4f..311d01f90dc1e 100644 --- a/apps/mobile/src/features/home/HomeRouteScreen.tsx +++ b/apps/mobile/src/features/home/HomeRouteScreen.tsx @@ -119,7 +119,6 @@ export function HomeRouteScreen() { options: listOptions, setSelectedEnvironmentId, setSelectedProjectKey, - setHideOfflineEnvironments, } = useHomeListOptions(availableEnvironmentIds); const selectedEnvironmentId = listOptions.selectedEnvironmentId; // A legacy per-environment scratch key resolves here on every render; the stored @@ -175,10 +174,7 @@ export function HomeRouteScreen() { accessibilityLabel: "Search threads", }} filter={{ - customized: - selectedEnvironmentId !== null || - selectedProjectKey !== null || - listOptions.hideOfflineEnvironments, + customized: selectedEnvironmentId !== null || selectedProjectKey !== null, items: buildHomeListFilterMenuItems({ environments, projects: projectFilterOptions, @@ -186,8 +182,6 @@ export function HomeRouteScreen() { selectedProjectKey, onEnvironmentChange: setSelectedEnvironmentId, onProjectChange: setSelectedProjectKey, - hideOfflineEnvironments: listOptions.hideOfflineEnvironments, - onHideOfflineEnvironmentsChange: setHideOfflineEnvironments, }), }} /> @@ -211,7 +205,6 @@ export function HomeRouteScreen() { onEnvironmentChange={setSelectedEnvironmentId} onProjectChange={setSelectedProjectKey} hideOfflineEnvironments={listOptions.hideOfflineEnvironments} - onHideOfflineEnvironmentsChange={setHideOfflineEnvironments} onOpenSettings={() => selectTab("settings")} onSearchQueryChange={setSearchQuery} onSelectThread={handleSelectThread} diff --git a/apps/mobile/src/features/home/HomeScreen.tsx b/apps/mobile/src/features/home/HomeScreen.tsx index a5910a8cbbb0d..23ac0b8394bb4 100644 --- a/apps/mobile/src/features/home/HomeScreen.tsx +++ b/apps/mobile/src/features/home/HomeScreen.tsx @@ -103,7 +103,6 @@ interface HomeScreenProps { readonly selectedEnvironmentId: EnvironmentId | null; readonly selectedProjectKey: string | null; readonly hideOfflineEnvironments: boolean; - readonly onHideOfflineEnvironmentsChange: (value: boolean) => void; readonly projectSortOrder: HomeProjectSortOrder; readonly projectGroupingMode: SidebarProjectGroupingMode; readonly onSearchQueryChange: (query: string) => void; @@ -744,7 +743,6 @@ export function HomeScreen(props: HomeScreenProps) { onEnvironmentChange={props.onEnvironmentChange} onProjectChange={props.onProjectChange} hideOfflineEnvironments={props.hideOfflineEnvironments} - onHideOfflineEnvironmentsChange={props.onHideOfflineEnvironmentsChange} /> ) : null} @@ -760,7 +758,7 @@ export function HomeScreen(props: HomeScreenProps) { ) : props.hideOfflineEnvironments ? ( ) : v2ScopedProjectGroup !== null ? ( diff --git a/apps/mobile/src/features/home/ThreadListFilters.tsx b/apps/mobile/src/features/home/ThreadListFilters.tsx index a06addea764f5..178dba15e23dc 100644 --- a/apps/mobile/src/features/home/ThreadListFilters.tsx +++ b/apps/mobile/src/features/home/ThreadListFilters.tsx @@ -13,7 +13,6 @@ export function ThreadListFilters(props: { readonly selectedEnvironmentId: EnvironmentId | null; readonly selectedProjectKey: string | null; readonly hideOfflineEnvironments: boolean; - readonly onHideOfflineEnvironmentsChange: (value: boolean) => void; readonly onEnvironmentChange: (environmentId: EnvironmentId | null) => void; readonly onProjectChange: (projectKey: string | null) => void; /** List padding the scroller bleeds through so pills start where row content does. */ @@ -53,16 +52,9 @@ export function ThreadListFilters(props: { ? ("on" as const) : ("off" as const), })), - { - id: "hide-offline", - title: "Hide offline environments", - state: props.hideOfflineEnvironments ? "on" : "off", - }, ]} onSelect={(id) => { - if (id === "hide-offline") - props.onHideOfflineEnvironmentsChange(!props.hideOfflineEnvironments); - else if (id === "all") props.onEnvironmentChange(null); + if (id === "all") props.onEnvironmentChange(null); else { const environment = props.environments.find( (candidate) => JSON.stringify(candidate.environmentId) === id, @@ -97,9 +89,7 @@ export function ThreadListFilters(props: { } }} /> - {props.selectedEnvironmentId !== null || - props.selectedProjectKey !== null || - props.hideOfflineEnvironments ? ( + {props.selectedEnvironmentId !== null || props.selectedProjectKey !== null ? ( { props.onEnvironmentChange(null); props.onProjectChange(null); - props.onHideOfflineEnvironmentsChange(false); }} > diff --git a/apps/mobile/src/features/home/home-list-filter-menu.ts b/apps/mobile/src/features/home/home-list-filter-menu.ts index 35e3dc177cdcf..617c3fa0988ef 100644 --- a/apps/mobile/src/features/home/home-list-filter-menu.ts +++ b/apps/mobile/src/features/home/home-list-filter-menu.ts @@ -17,8 +17,6 @@ export function buildHomeListFilterMenuItems(props: { readonly projects: ReadonlyArray; readonly selectedEnvironmentId: EnvironmentId | null; readonly selectedProjectKey: string | null; - readonly hideOfflineEnvironments: boolean; - readonly onHideOfflineEnvironmentsChange: (value: boolean) => void; readonly onEnvironmentChange: (environmentId: EnvironmentId | null) => void; readonly onProjectChange: (projectKey: string | null) => void; }): AppBarMenuItem[] { @@ -66,11 +64,5 @@ export function buildHomeListFilterMenuItems(props: { }); } - items.push({ - id: "hide-offline-environments", - title: "Hide offline environments", - selected: props.hideOfflineEnvironments, - onPress: () => props.onHideOfflineEnvironmentsChange(!props.hideOfflineEnvironments), - }); return items; } diff --git a/apps/mobile/src/features/settings/ThreadSettingsSections.tsx b/apps/mobile/src/features/settings/ThreadSettingsSections.tsx index 788df701e8bba..fe0f8f2c18bdb 100644 --- a/apps/mobile/src/features/settings/ThreadSettingsSections.tsx +++ b/apps/mobile/src/features/settings/ThreadSettingsSections.tsx @@ -1,3 +1,4 @@ +import { useHomeListOptions } from "../home/home-list-options"; import { Button } from "../../components/ui/Button"; import { AuthSettingsWriteScope, @@ -35,6 +36,10 @@ import { } from "./settings-scoped-server"; export function SettingsLabsRouteScreen() { + const { environments } = useEnvironments(); + const { options, setHideOfflineEnvironments } = useHomeListOptions( + new Set(environments.map((environment) => environment.environmentId)), + ); const insets = useSafeAreaInsets(); const navigation = useNavigation(); @@ -54,6 +59,15 @@ export function SettingsLabsRouteScreen() { value="Memory, summaries, skills, thread suggestions, evaluations" onPress={() => navigation.navigate("SettingsZack")} /> + + + diff --git a/apps/mobile/src/features/settings/settings-search.ts b/apps/mobile/src/features/settings/settings-search.ts index 3cf9665d4da8a..1d1b6af1a06d3 100644 --- a/apps/mobile/src/features/settings/settings-search.ts +++ b/apps/mobile/src/features/settings/settings-search.ts @@ -99,6 +99,7 @@ const SETTINGS_SEARCH_TERMS = { "start from remote", ], SettingsLabs: [ + "hide offline environments", "legacy", "plan mode", "beta", diff --git a/apps/mobile/src/features/threads/ThreadNavigationSidebar.tsx b/apps/mobile/src/features/threads/ThreadNavigationSidebar.tsx index c648c2a7aad43..92133890cf626 100644 --- a/apps/mobile/src/features/threads/ThreadNavigationSidebar.tsx +++ b/apps/mobile/src/features/threads/ThreadNavigationSidebar.tsx @@ -152,7 +152,7 @@ function ThreadNavigationSidebarPane(props: ThreadNavigationSidebarProps) { const availableEnvironmentIds = new Set( environments.map((environment) => environment.environmentId), ); - const { options, setSelectedEnvironmentId, setSelectedProjectKey, setHideOfflineEnvironments } = + const { options, setSelectedEnvironmentId, setSelectedProjectKey } = useHomeListOptions(availableEnvironmentIds); const searchEnvironmentIds = options.selectedEnvironmentId === null @@ -536,10 +536,7 @@ function ThreadNavigationSidebarPane(props: ThreadNavigationSidebarProps) { accessibilityLabel: "Search threads", }} filter={{ - customized: - options.selectedEnvironmentId !== null || - selectedProjectKey !== null || - options.hideOfflineEnvironments, + customized: options.selectedEnvironmentId !== null || selectedProjectKey !== null, items: buildHomeListFilterMenuItems({ environments, projects: projectFilterOptions, @@ -547,8 +544,6 @@ function ThreadNavigationSidebarPane(props: ThreadNavigationSidebarProps) { selectedProjectKey, onEnvironmentChange: setSelectedEnvironmentId, onProjectChange: setSelectedProjectKey, - hideOfflineEnvironments: options.hideOfflineEnvironments, - onHideOfflineEnvironmentsChange: setHideOfflineEnvironments, }), }} sidebar={{ @@ -575,7 +570,7 @@ function ThreadNavigationSidebarPane(props: ThreadNavigationSidebarProps) { ? "Searching thread messages…" : "No matching threads" : options.hideOfflineEnvironments - ? "No visible threads. Turn off Hide offline environments or choose another filter." + ? "No visible threads. Turn off Hide offline environments in Settings → Labs → Zack, or choose another filter." : selectedProjectScope !== null ? `No threads in ${selectedProjectScope.title}` : "No threads yet"} From efc68b6df64bc56b3014f3882dcd8ec95d222011 Mon Sep 17 00:00:00 2001 From: ScriptedAlchemy Date: Sat, 7 Nov 2026 21:53:13 -0800 Subject: [PATCH 3/3] fix(mobile): place offline preference inside Zack settings --- apps/mobile/src/features/home/HomeScreen.tsx | 2 +- .../SettingsServerControlsRouteScreen.tsx | 17 +++++++++++++++++ .../settings/ThreadSettingsSections.tsx | 14 -------------- .../threads/ThreadNavigationSidebar.tsx | 2 +- 4 files changed, 19 insertions(+), 16 deletions(-) diff --git a/apps/mobile/src/features/home/HomeScreen.tsx b/apps/mobile/src/features/home/HomeScreen.tsx index 23ac0b8394bb4..ccecf9ada32af 100644 --- a/apps/mobile/src/features/home/HomeScreen.tsx +++ b/apps/mobile/src/features/home/HomeScreen.tsx @@ -758,7 +758,7 @@ export function HomeScreen(props: HomeScreenProps) { ) : props.hideOfflineEnvironments ? ( ) : v2ScopedProjectGroup !== null ? ( diff --git a/apps/mobile/src/features/settings/SettingsServerControlsRouteScreen.tsx b/apps/mobile/src/features/settings/SettingsServerControlsRouteScreen.tsx index 44cef66bd3c4b..ddedee9d5154b 100644 --- a/apps/mobile/src/features/settings/SettingsServerControlsRouteScreen.tsx +++ b/apps/mobile/src/features/settings/SettingsServerControlsRouteScreen.tsx @@ -45,6 +45,8 @@ import { ModelEvaluationSettings } from "./ModelEvaluationSettings"; import { SkillsLibrarySettings } from "./SkillsLibrarySettings"; import { AutoSettleSettingsRows, ThreadSplitSettingsSection } from "./ThreadSettingsSections"; import { supportsSharedSettingsSync } from "@t3tools/client-runtime/state/shared-settings"; +import { useHomeListOptions } from "../home/home-list-options"; +import { useEnvironments } from "../../state/environments"; type SettingsPage = "threads" | "source-control" | "worktrees" | "maintenance" | "zack"; @@ -182,6 +184,10 @@ export function SettingsZackRouteScreen() { } function ServerSettingsDetail(props: { readonly page: SettingsPage }) { + const { environments } = useEnvironments(); + const { options, setHideOfflineEnvironments } = useHomeListOptions( + new Set(environments.map((environment) => environment.environmentId)), + ); const environmentMenu = useSettingsEnvironmentMenu(); const insets = useSafeAreaInsets(); const navigation = useNavigation(); @@ -310,6 +316,17 @@ function ServerSettingsDetail(props: { readonly page: SettingsPage }) { contentContainerClassName="gap-6 px-5 pt-4" contentContainerStyle={{ paddingBottom: Math.max(insets.bottom, 18) + 18 }} > + {props.page === "zack" ? ( + + + + ) : null} {!hasConnectedSelection || reference === null ? ( {projectSelected diff --git a/apps/mobile/src/features/settings/ThreadSettingsSections.tsx b/apps/mobile/src/features/settings/ThreadSettingsSections.tsx index fe0f8f2c18bdb..788df701e8bba 100644 --- a/apps/mobile/src/features/settings/ThreadSettingsSections.tsx +++ b/apps/mobile/src/features/settings/ThreadSettingsSections.tsx @@ -1,4 +1,3 @@ -import { useHomeListOptions } from "../home/home-list-options"; import { Button } from "../../components/ui/Button"; import { AuthSettingsWriteScope, @@ -36,10 +35,6 @@ import { } from "./settings-scoped-server"; export function SettingsLabsRouteScreen() { - const { environments } = useEnvironments(); - const { options, setHideOfflineEnvironments } = useHomeListOptions( - new Set(environments.map((environment) => environment.environmentId)), - ); const insets = useSafeAreaInsets(); const navigation = useNavigation(); @@ -59,15 +54,6 @@ export function SettingsLabsRouteScreen() { value="Memory, summaries, skills, thread suggestions, evaluations" onPress={() => navigation.navigate("SettingsZack")} /> - - - diff --git a/apps/mobile/src/features/threads/ThreadNavigationSidebar.tsx b/apps/mobile/src/features/threads/ThreadNavigationSidebar.tsx index 92133890cf626..a29cfb5885eaf 100644 --- a/apps/mobile/src/features/threads/ThreadNavigationSidebar.tsx +++ b/apps/mobile/src/features/threads/ThreadNavigationSidebar.tsx @@ -570,7 +570,7 @@ function ThreadNavigationSidebarPane(props: ThreadNavigationSidebarProps) { ? "Searching thread messages…" : "No matching threads" : options.hideOfflineEnvironments - ? "No visible threads. Turn off Hide offline environments in Settings → Labs → Zack, or choose another filter." + ? "No visible threads. Turn off Hide offline environments in Settings → Labs → Zack settings, or choose another filter." : selectedProjectScope !== null ? `No threads in ${selectedProjectScope.title}` : "No threads yet"}