import * as Arr from "effect/Array";import * as Order from "effect/Order";import { useNavigation } from "@react-navigation/native";import { useEffect, useMemo, useState } from "react"; import { NativeHeaderToolbar, NativeStackScreenOptions } from "../../native/StackHeader";import { useProjects, useThreadShells } from "../../state/entities";import { usePendingNewTasks } from "../../state/use-pending-new-tasks";import { useWorkspaceState } from "../../state/workspace";import { useSavedRemoteConnections } from "../../state/use-remote-environment-registry";import { useAdaptiveWorkspaceLayout } from "../layout/AdaptiveWorkspaceLayout";import { WorkspaceEmptyDetail } from "../layout/WorkspaceEmptyDetail";import { WorkspaceSidebarToolbar } from "../layout/workspace-sidebar-toolbar";import { checkForAppUpdateOnLaunch, startAppUpdateForegroundRecheck } from "../updates/app-updates";import { AndroidHomeFabLayout } from "./AndroidHomeFab";import { HomeScreen } from "./HomeScreen";import { HomeHeader } from "./HomeHeader";import { useHomeListOptions } from "./home-list-options";import { buildHomeProjectScopes } from "./homeThreadList";import { usePendingTaskListActions } from "./usePendingTaskListActions";import { useThreadListActions } from "./useThreadListActions";import { getConnectionAwareBrandHeaderOptions } from "./WorkspaceConnectionTitle"; /* ─── Route screen ───────────────────────────────────────────────────── */ export function HomeRouteScreen() { const { layout } = useAdaptiveWorkspaceLayout(); const projects = useProjects(); const threads = useThreadShells(); const { environments: workspaceEnvironments, state: catalogState } = useWorkspaceState(); const { savedConnectionsById } = useSavedRemoteConnections(); const navigation = useNavigation(); const [searchQuery, setSearchQuery] = useState(""); useEffect(() => { void checkForAppUpdateOnLaunch(); startAppUpdateForegroundRecheck(); }, []); const { archiveThread, confirmDeleteThread, settleThread, snoozeThread, unsnoozeThread, pinThread, unpinThread, movePinnedThread, regenerateThreadTitle, unsettleThread, } = useThreadListActions(); const pendingTasks = usePendingNewTasks(); const { openPendingTask, confirmDeletePendingTask } = usePendingTaskListActions(); const environments = useMemo(() => { const connectionStateByEnvironmentId = new Map( workspaceEnvironments.map( (environment) => [environment.environmentId, environment.connectionState] as const, ), ); return Arr.sort( Object.values(savedConnectionsById).map((connection) => ({ environmentId: connection.environmentId, label: connection.environmentLabel, connectionState: connectionStateByEnvironmentId.get(connection.environmentId) ?? "available", })), Order.mapInput(Order.String, (environment: { readonly label: string }) => environment.label), ); }, [savedConnectionsById, workspaceEnvironments]); const availableEnvironmentIds = useMemo( () => new Set(environments.map((environment) => environment.environmentId)), [environments], ); const { options: listOptions, setSelectedEnvironmentId, setProjectSortOrder, setThreadSortOrder, } = useHomeListOptions(availableEnvironmentIds); const selectedEnvironmentId = listOptions.selectedEnvironmentId; const [selectedProjectKey, setSelectedProjectKey] = useState<string | null>(null); const projectFilterOptions = useMemo( () => buildHomeProjectScopes({ projects, environmentId: selectedEnvironmentId, projectGroupingMode: listOptions.projectGroupingMode, }).map((scope) => ({ key: scope.key, label: scope.title, })), [listOptions.projectGroupingMode, projects, selectedEnvironmentId], ); useEffect(() => { if ( selectedProjectKey !== null && !projectFilterOptions.some((project) => project.key === selectedProjectKey) ) { setSelectedProjectKey(null); } }, [projectFilterOptions, selectedProjectKey]); // In split layouts the persistent sidebar IS the thread list — Home becomes // an empty detail pane so selecting a thread never transitions layouts. if (layout.usesSplitView) { return ( <> <NativeStackScreenOptions options={{ title: "", headerTitle: "", unstable_headerLeftItems: () => [] }} /> <WorkspaceSidebarToolbar afterSidebarButton={ <NativeHeaderToolbar.Button accessibilityLabel="New task" icon="square.and.pencil" onPress={() => navigation.navigate("NewTaskSheet", { screen: "NewTask" })} /> } /> <WorkspaceEmptyDetail onStartNewTask={() => navigation.navigate("NewTaskSheet", { screen: "NewTask" })} /> </> ); } return ( <AndroidHomeFabLayout onStartNewTask={() => navigation.navigate("NewTaskSheet", { screen: "NewTask" })} > <> {/* Restore the compact title after the split branch blanks the detail header. The brand slot doubles as the connection status surface: while an environment reconnects, the lockup fades to a status label in place (no layout shift in the list below). */} <NativeStackScreenOptions options={getConnectionAwareBrandHeaderOptions({ onOpenEnvironments: () => navigation.navigate("SettingsSheet", { screen: "SettingsContent", params: { screen: "SettingsEnvironments" }, }), })} /> <HomeHeader environments={environments} projects={projectFilterOptions} searchQuery={searchQuery} selectedEnvironmentId={selectedEnvironmentId} selectedProjectKey={selectedProjectKey} projectSortOrder={listOptions.projectSortOrder} threadSortOrder={listOptions.threadSortOrder} onEnvironmentChange={setSelectedEnvironmentId} onProjectChange={setSelectedProjectKey} onOpenEnvironments={() => navigation.navigate("SettingsSheet", { screen: "SettingsContent", params: { screen: "SettingsEnvironments" }, }) } onOpenSettings={() => navigation.navigate("SettingsSheet", { screen: "SettingsContent", params: { screen: "Settings" }, }) } onProjectSortOrderChange={setProjectSortOrder} onSearchQueryChange={setSearchQuery} onStartNewTask={() => navigation.navigate("NewTaskSheet", { screen: "NewTask" })} onThreadSortOrderChange={setThreadSortOrder} /> <HomeScreen catalogState={catalogState} environments={environments} onAddConnection={() => navigation.navigate("SettingsSheet", { screen: "SettingsContent", params: { screen: "SettingsEnvironmentNew" }, }) } onArchiveThread={archiveThread} onDeleteThread={confirmDeleteThread} onSettleThread={settleThread} onSnoozeThread={snoozeThread} onUnsnoozeThread={unsnoozeThread} onUnsettleThread={unsettleThread} onPinThread={pinThread} onUnpinThread={unpinThread} onMovePinnedThread={movePinnedThread} onRegenerateThreadTitle={regenerateThreadTitle} onEnvironmentChange={setSelectedEnvironmentId} onProjectChange={setSelectedProjectKey} onOpenSettings={() => navigation.navigate("SettingsSheet", { screen: "SettingsContent", params: { screen: "Settings" }, }) } onProjectSortOrderChange={setProjectSortOrder} onSearchQueryChange={setSearchQuery} onSelectThread={(thread) => { // Settled threads are live shells: opening one is plain // navigation, and sending a message un-settles server-side. navigation.navigate("Thread", { environmentId: thread.environmentId, threadId: thread.id, }); }} onSelectPendingTask={openPendingTask} onDeletePendingTask={confirmDeletePendingTask} onNewThreadInProject={(project) => { navigation.navigate("NewTaskSheet", { screen: "NewTaskDraft", params: { environmentId: String(project.environmentId), projectId: String(project.id), title: project.title, }, }); }} onStartNewTask={() => navigation.navigate("NewTaskSheet", { screen: "NewTask" })} onThreadSortOrderChange={setThreadSortOrder} pendingTasks={pendingTasks} projectGroupingMode={listOptions.projectGroupingMode} projects={projects} projectSortOrder={listOptions.projectSortOrder} savedConnectionsById={savedConnectionsById} searchQuery={searchQuery} selectedEnvironmentId={selectedEnvironmentId} selectedProjectKey={selectedProjectKey} threads={threads} threadSortOrder={listOptions.threadSortOrder} /> </> </AndroidHomeFabLayout> );}
import * as Arr from "effect/Array";
import * as Order from "effect/Order";
import { useNavigation } from "@react-navigation/native";
import { useEffect, useMemo, useState } from "react";
import { NativeHeaderToolbar, NativeStackScreenOptions } from "../../native/StackHeader";
import { useProjects, useThreadShells } from "../../state/entities";
import { usePendingNewTasks } from "../../state/use-pending-new-tasks";
import { useWorkspaceState } from "../../state/workspace";
import { useSavedRemoteConnections } from "../../state/use-remote-environment-registry";
import { useAdaptiveWorkspaceLayout } from "../layout/AdaptiveWorkspaceLayout";
import { WorkspaceEmptyDetail } from "../layout/WorkspaceEmptyDetail";
import { WorkspaceSidebarToolbar } from "../layout/workspace-sidebar-toolbar";
import { checkForAppUpdateOnLaunch, startAppUpdateForegroundRecheck } from "../updates/app-updates";
import { AndroidHomeFabLayout } from "./AndroidHomeFab";
import { HomeScreen } from "./HomeScreen";
import { HomeHeader } from "./HomeHeader";
import { useHomeListOptions } from "./home-list-options";
import { buildHomeProjectScopes } from "./homeThreadList";
import { usePendingTaskListActions } from "./usePendingTaskListActions";
import { useThreadListActions } from "./useThreadListActions";
import { getConnectionAwareBrandHeaderOptions } from "./WorkspaceConnectionTitle";
/* ─── Route screen ───────────────────────────────────────────────────── */
export function HomeRouteScreen() {
const { layout } = useAdaptiveWorkspaceLayout();
const projects = useProjects();
const threads = useThreadShells();
const { environments: workspaceEnvironments, state: catalogState } = useWorkspaceState();
const { savedConnectionsById } = useSavedRemoteConnections();
const navigation = useNavigation();
const [searchQuery, setSearchQuery] = useState("");
useEffect(() => {
void checkForAppUpdateOnLaunch();
startAppUpdateForegroundRecheck();
}, []);
const {
archiveThread,
confirmDeleteThread,
settleThread,
snoozeThread,
unsnoozeThread,
pinThread,
unpinThread,
movePinnedThread,
regenerateThreadTitle,
unsettleThread,
} = useThreadListActions();
const pendingTasks = usePendingNewTasks();
const { openPendingTask, confirmDeletePendingTask } = usePendingTaskListActions();
const environments = useMemo(() => {
const connectionStateByEnvironmentId = new Map(
workspaceEnvironments.map(
(environment) => [environment.environmentId, environment.connectionState] as const,
),
);
return Arr.sort(
Object.values(savedConnectionsById).map((connection) => ({
environmentId: connection.environmentId,
label: connection.environmentLabel,
connectionState:
connectionStateByEnvironmentId.get(connection.environmentId) ?? "available",
})),
Order.mapInput(Order.String, (environment: { readonly label: string }) => environment.label),
);
}, [savedConnectionsById, workspaceEnvironments]);
const availableEnvironmentIds = useMemo(
() => new Set(environments.map((environment) => environment.environmentId)),
[environments],
);
const {
options: listOptions,
setSelectedEnvironmentId,
setProjectSortOrder,
setThreadSortOrder,
} = useHomeListOptions(availableEnvironmentIds);
const selectedEnvironmentId = listOptions.selectedEnvironmentId;
const [selectedProjectKey, setSelectedProjectKey] = useState<string | null>(null);
const projectFilterOptions = useMemo(
() =>
buildHomeProjectScopes({
projects,
environmentId: selectedEnvironmentId,
projectGroupingMode: listOptions.projectGroupingMode,
}).map((scope) => ({
key: scope.key,
label: scope.title,
})),
[listOptions.projectGroupingMode, projects, selectedEnvironmentId],
);
useEffect(() => {
if (
selectedProjectKey !== null &&
!projectFilterOptions.some((project) => project.key === selectedProjectKey)
) {
setSelectedProjectKey(null);
}
}, [projectFilterOptions, selectedProjectKey]);
// In split layouts the persistent sidebar IS the thread list — Home becomes
// an empty detail pane so selecting a thread never transitions layouts.
if (layout.usesSplitView) {
return (
<>
<NativeStackScreenOptions
options={{ title: "", headerTitle: "", unstable_headerLeftItems: () => [] }}
/>
<WorkspaceSidebarToolbar
afterSidebarButton={
<NativeHeaderToolbar.Button
accessibilityLabel="New task"
icon="square.and.pencil"
onPress={() => navigation.navigate("NewTaskSheet", { screen: "NewTask" })}
/>
}
/>
<WorkspaceEmptyDetail
onStartNewTask={() => navigation.navigate("NewTaskSheet", { screen: "NewTask" })}
/>
</>
);
}
return (
<AndroidHomeFabLayout
onStartNewTask={() => navigation.navigate("NewTaskSheet", { screen: "NewTask" })}
>
<>
{/* Restore the compact title after the split branch blanks the detail
header. The brand slot doubles as the connection status surface:
while an environment reconnects, the lockup fades to a status label
in place (no layout shift in the list below). */}
<NativeStackScreenOptions
options={getConnectionAwareBrandHeaderOptions({
onOpenEnvironments: () =>
navigation.navigate("SettingsSheet", {
screen: "SettingsContent",
params: { screen: "SettingsEnvironments" },
}),
})}
/>
<HomeHeader
environments={environments}
projects={projectFilterOptions}
searchQuery={searchQuery}
selectedEnvironmentId={selectedEnvironmentId}
selectedProjectKey={selectedProjectKey}
projectSortOrder={listOptions.projectSortOrder}
threadSortOrder={listOptions.threadSortOrder}
onEnvironmentChange={setSelectedEnvironmentId}
onProjectChange={setSelectedProjectKey}
onOpenEnvironments={() =>
navigation.navigate("SettingsSheet", {
screen: "SettingsContent",
params: { screen: "SettingsEnvironments" },
})
}
onOpenSettings={() =>
navigation.navigate("SettingsSheet", {
screen: "SettingsContent",
params: { screen: "Settings" },
})
}
onProjectSortOrderChange={setProjectSortOrder}
onSearchQueryChange={setSearchQuery}
onStartNewTask={() => navigation.navigate("NewTaskSheet", { screen: "NewTask" })}
onThreadSortOrderChange={setThreadSortOrder}
/>
<HomeScreen
catalogState={catalogState}
environments={environments}
onAddConnection={() =>
navigation.navigate("SettingsSheet", {
screen: "SettingsContent",
params: { screen: "SettingsEnvironmentNew" },
})
}
onArchiveThread={archiveThread}
onDeleteThread={confirmDeleteThread}
onSettleThread={settleThread}
onSnoozeThread={snoozeThread}
onUnsnoozeThread={unsnoozeThread}
onUnsettleThread={unsettleThread}
onPinThread={pinThread}
onUnpinThread={unpinThread}
onMovePinnedThread={movePinnedThread}
onRegenerateThreadTitle={regenerateThreadTitle}
onEnvironmentChange={setSelectedEnvironmentId}
onProjectChange={setSelectedProjectKey}
onOpenSettings={() =>
navigation.navigate("SettingsSheet", {
screen: "SettingsContent",
params: { screen: "Settings" },
})
}
onProjectSortOrderChange={setProjectSortOrder}
onSearchQueryChange={setSearchQuery}
onSelectThread={(thread) => {
// Settled threads are live shells: opening one is plain
// navigation, and sending a message un-settles server-side.
navigation.navigate("Thread", {
environmentId: thread.environmentId,
threadId: thread.id,
});
}}
onSelectPendingTask={openPendingTask}
onDeletePendingTask={confirmDeletePendingTask}
onNewThreadInProject={(project) => {
navigation.navigate("NewTaskSheet", {
screen: "NewTaskDraft",
params: {
environmentId: String(project.environmentId),
projectId: String(project.id),
title: project.title,
},
});
}}
onStartNewTask={() => navigation.navigate("NewTaskSheet", { screen: "NewTask" })}
onThreadSortOrderChange={setThreadSortOrder}
pendingTasks={pendingTasks}
projectGroupingMode={listOptions.projectGroupingMode}
projects={projects}
projectSortOrder={listOptions.projectSortOrder}
savedConnectionsById={savedConnectionsById}
searchQuery={searchQuery}
selectedEnvironmentId={selectedEnvironmentId}
selectedProjectKey={selectedProjectKey}
threads={threads}
threadSortOrder={listOptions.threadSortOrder}
/>
</>
</AndroidHomeFabLayout>
);
}