1import { use } from "react";
2import { NativePrimaryColumnContext } from "../../native/v5-workspace-context";
3import * as Arr from "effect/Array";
4import * as Order from "effect/Order";
5import { useNavigation } from "@react-navigation/native";
6import { useCallback, useEffect, useMemo, useState } from "react";
7import { Platform, useWindowDimensions } from "react-native";
8
9import type { EnvironmentThreadShell } from "@t3tools/client-runtime/state/shell";
10import { resolveScratchProjectScopeKey } from "@t3tools/client-runtime/state/project-grouping";
11
12import { NativeHeaderToolbar, NativeStackScreenOptions } from "../../native/StackHeader";
13import { useProjects } from "../../state/entities";
14import { environmentThreadShells } from "../../state/threads";
15import { usePendingNewTasks } from "../../state/use-pending-new-tasks";
16import { useWorkspaceState } from "../../state/workspace";
17import { useSavedRemoteConnections } from "../../state/use-remote-environment-registry";
18import { useAdaptiveWorkspaceLayout } from "../layout/AdaptiveWorkspaceLayout";
19import { useNativeColumnLayoutMetrics } from "../../native/native-layout-metrics";
20import { WorkspaceEmptyDetail } from "../layout/WorkspaceEmptyDetail";
21import { AndroidScreenHeader } from "../../components/AndroidScreenHeader";
22import { checkForAppUpdateOnLaunch, startAppUpdateForegroundRecheck } from "../updates/app-updates";
23import { AndroidHomeFabLayout } from "./AndroidHomeFab";
24import { HomeScreen } from "./HomeScreen";
25import { HomeHeader } from "./HomeHeader";
26import { useHomeListOptions } from "./home-list-options";
27import { useAtomValueWhileVisible, useHomeRouteVisible } from "./home-route-visibility";
28import { useHomeThreadSelection } from "./home-thread-navigation";
29import { buildHomeProjectScopes } from "./homeThreadList";
30import { usePendingTaskListActions } from "./usePendingTaskListActions";
31import { useThreadListActions } from "./useThreadListActions";
32import { getConnectionAwareBrandHeaderOptions } from "./WorkspaceConnectionTitle";
33import { useWorkspaceTabs } from "../workspace/WorkspaceTabsContext";
34
35/* ─── Route screen ───────────────────────────────────────────────────── */
36
37export function HomeRouteScreen() {
38 const { width: windowWidth } = useWindowDimensions();
39 const nativePrimaryColumn = use(NativePrimaryColumnContext);
40 const columnMetrics = useNativeColumnLayoutMetrics();
41 const headerWidth = nativePrimaryColumn ? (columnMetrics?.width ?? windowWidth) : windowWidth;
42 const { layout, panes, primarySidebarSearchQuery, setPrimarySidebarSearchQuery } =
43 useAdaptiveWorkspaceLayout();
44 const projects = useProjects();
45 // Streaming turns rewrite thread shells many times a second. While a Thread
46 // covers Home, rebuilding this list is invisible work.
47 const visible = useHomeRouteVisible();
48 const threads = useAtomValueWhileVisible(
49 environmentThreadShells.navigationThreadShellsAtom,
50 visible,
51 );
52 const { environments: workspaceEnvironments, state: catalogState } = useWorkspaceState();
53 const { savedConnectionsById } = useSavedRemoteConnections();
54 const navigation = useNavigation();
55 const workspace = useWorkspaceTabs();
56 const [localSearchQuery, setLocalSearchQuery] = useState("");
57 const searchQuery =
58 Platform.OS === "android"
59 ? primarySidebarSearchQuery
60 : (workspace?.state.tabs.threads.query ?? localSearchQuery);
61 const setSearchQuery = (query: string) => {
62 if (Platform.OS === "android") setPrimarySidebarSearchQuery(query);
63 else if (workspace) workspace.dispatch({ type: "query", query, tab: "threads" });
64 else setLocalSearchQuery(query);
65 };
66 const handleSelectThread = useHomeThreadSelection();
67 const handleNewThreadOnBranch = useCallback(
68 (thread: EnvironmentThreadShell) => {
69 navigation.navigate("NewTaskSheet", {
70 screen: "NewTaskDraft",
71 params: {
72 environmentId: String(thread.environmentId),
73 projectId: String(thread.projectId),
74 branch: thread.branch,
75 worktreePath: thread.worktreePath,
76 },
77 });
78 },
79 [navigation],
80 );
81
82 useEffect(() => {
83 void checkForAppUpdateOnLaunch();
84 startAppUpdateForegroundRecheck();
85 }, []);
86
87 const {
88 archiveThread,
89 confirmDeleteThread,
90 settleThread,
91 snoozeThread,
92 unsnoozeThread,
93 pinThread,
94 unpinThread,
95 setThreadAutoSettle,
96 moveThread,
97 renameThread,
98 regenerateThreadTitle,
99 unsettleThread,
100 } = useThreadListActions();
101 const pendingTasks = usePendingNewTasks();
102 const { openPendingTask, confirmDeletePendingTask } = usePendingTaskListActions();
103 const environments = useMemo(() => {
104 const connectionStateByEnvironmentId = new Map(
105 workspaceEnvironments.map(
106 (environment) => [environment.environmentId, environment.connectionState] as const,
107 ),
108 );
109 return Arr.sort(
110 Object.values(savedConnectionsById).map((connection) => ({
111 environmentId: connection.environmentId,
112 label: connection.environmentLabel,
113 connectionState:
114 connectionStateByEnvironmentId.get(connection.environmentId) ?? "available",
115 })),
116 Order.mapInput(Order.String, (environment: { readonly label: string }) => environment.label),
117 );
118 }, [savedConnectionsById, workspaceEnvironments]);
119 const availableEnvironmentIds = useMemo(
120 () => new Set(environments.map((environment) => environment.environmentId)),
121 [environments],
122 );
123 const {
124 options: listOptions,
125 setSelectedEnvironmentId,
126 setSelectedProjectKey,
127 } = useHomeListOptions(availableEnvironmentIds);
128 const selectedEnvironmentId = listOptions.selectedEnvironmentId;
129 const projectScopeKey = listOptions.selectedProjectKey;
130 const resolvedProjectKey = resolveScratchProjectScopeKey(projectScopeKey, projects, {
131 sidebarProjectGroupingMode: listOptions.projectGroupingMode,
132 sidebarProjectGroupingOverrides: {},
133 });
134 useEffect(() => {
135 if (resolvedProjectKey !== projectScopeKey) {
136 setSelectedProjectKey(resolvedProjectKey);
137 }
138 }, [projectScopeKey, resolvedProjectKey, setSelectedProjectKey]);
139 const projectFilterOptions = useMemo(
140 () =>
141 buildHomeProjectScopes({
142 projects,
143 environmentId: selectedEnvironmentId,
144 projectGroupingMode: listOptions.projectGroupingMode,
145 }).map((scope) => ({
146 key: scope.key,
147 label: scope.title,
148 })),
149 [listOptions.projectGroupingMode, projects, selectedEnvironmentId],
150 );
151 // A saved project stays selected while its environment is offline or filtered out.
152 const selectedProjectKey = projectFilterOptions.some(
153 (project) => project.key === resolvedProjectKey,
154 )
155 ? resolvedProjectKey
156 : null;
157
158 // In split layouts the persistent sidebar IS the thread list — Home becomes
159 // an empty detail pane so selecting a thread never transitions layouts.
160 if (layout.usesSplitView && !nativePrimaryColumn) {
161 return (
162 <>
163 <NativeStackScreenOptions
164 options={
165 Platform.OS === "android"
166 ? { headerShown: false }
167 : {
168 title: "",
169 headerTitle: "",
170 unstable_headerLeftItems: () => [],
171 // Options survive layout changes; search and settings now belong to the sidebar.
172 headerSearchBarOptions: undefined,
173 unstable_headerRightItems: () => [],
174 unstable_headerToolbarItems: () => [],
175 }
176 }
177 />
178 {Platform.OS === "ios" ? (
179 <NativeHeaderToolbar placement="left">
180 <NativeHeaderToolbar.Button
181 accessibilityLabel="New task"
182 icon="square.and.pencil"
183 onPress={() => navigation.navigate("NewTaskSheet", { screen: "NewTask" })}
184 />
185 </NativeHeaderToolbar>
186 ) : null}
187 {Platform.OS === "android" ? <AndroidScreenHeader title="Threads" /> : null}
188 <WorkspaceEmptyDetail
189 onAddConnection={
190 Platform.OS === "android" && !catalogState.hasConnections
191 ? () =>
192 navigation.navigate("SettingsSheet", {
193 screen: "SettingsContent",
194 params: { screen: "SettingsEnvironmentNew" },
195 })
196 : undefined
197 }
198 onStartNewTask={
199 Platform.OS === "android" && panes.primarySidebarVisible
200 ? undefined
201 : () => navigation.navigate("NewTaskSheet", { screen: "NewTask" })
202 }
203 />
204 </>
205 );
206 }
207
208 return (
209 <AndroidHomeFabLayout
210 disabled={workspace !== null && Platform.OS !== "android"}
211 onStartNewTask={() => navigation.navigate("NewTaskSheet", { screen: "NewTask" })}
212 >
213 <>
214 {/* Restore the header after leaving split view; screen options are
215 shallow-merged. The brand slot also doubles as the connection
216 status surface while an environment reconnects. */}
217 {workspace !== null && Platform.OS === "ios" ? null : (
218 <NativeStackScreenOptions
219 optionsVersion={headerWidth}
220 options={{
221 ...getConnectionAwareBrandHeaderOptions({
222 headerWidth,
223 trailingItemCount:
224 nativePrimaryColumn && Platform.OS === "ios" && Platform.isPad ? 2 : 1,
225 onOpenEnvironments: () =>
226 navigation.navigate("SettingsSheet", {
227 screen: "SettingsContent",
228 params: { screen: "SettingsEnvironments" },
229 }),
230 }),
231 headerShown: Platform.OS !== "android",
232 }}
233 />
234 )}
235 <HomeHeader
236 usesWorkspaceToolbar={workspace !== null}
237 environments={environments}
238 projects={projectFilterOptions}
239 searchQuery={searchQuery}
240 selectedEnvironmentId={selectedEnvironmentId}
241 selectedProjectKey={selectedProjectKey}
242 onEnvironmentChange={setSelectedEnvironmentId}
243 onProjectChange={setSelectedProjectKey}
244 onOpenEnvironments={() =>
245 navigation.navigate("SettingsSheet", {
246 screen: "SettingsContent",
247 params: { screen: "SettingsEnvironments" },
248 })
249 }
250 onOpenSettings={() =>
251 navigation.navigate("SettingsSheet", {
252 screen: "SettingsContent",
253 params: { screen: "Settings" },
254 })
255 }
256 onSearchQueryChange={setSearchQuery}
257 onStartNewTask={() => navigation.navigate("NewTaskSheet", { screen: "NewTask" })}
258 />
259
260 <HomeScreen
261 catalogState={catalogState}
262 environments={environments}
263 onAddConnection={() =>
264 navigation.navigate("SettingsSheet", {
265 screen: "SettingsContent",
266 params: { screen: "SettingsEnvironmentNew" },
267 })
268 }
269 onArchiveThread={archiveThread}
270 onDeleteThread={confirmDeleteThread}
271 onSettleThread={settleThread}
272 onSnoozeThread={snoozeThread}
273 onUnsnoozeThread={unsnoozeThread}
274 onUnsettleThread={unsettleThread}
275 onPinThread={pinThread}
276 onUnpinThread={unpinThread}
277 onSetThreadAutoSettle={setThreadAutoSettle}
278 onMoveThread={moveThread}
279 onRenameThread={renameThread}
280 onRegenerateThreadTitle={regenerateThreadTitle}
281 onEnvironmentChange={setSelectedEnvironmentId}
282 onProjectChange={setSelectedProjectKey}
283 onOpenSettings={() =>
284 navigation.navigate("SettingsSheet", {
285 screen: "SettingsContent",
286 params: { screen: "Settings" },
287 })
288 }
289 onSearchQueryChange={setSearchQuery}
290 onSelectThread={handleSelectThread}
291 onSelectPendingTask={openPendingTask}
292 onDeletePendingTask={confirmDeletePendingTask}
293 onNewThreadOnBranch={handleNewThreadOnBranch}
294 onNewThreadInProject={(project) => {
295 navigation.navigate("NewTaskSheet", {
296 screen: "NewTaskDraft",
297 params: {
298 environmentId: String(project.environmentId),
299 projectId: String(project.id),
300 title: project.title,
301 },
302 });
303 }}
304 onStartNewTask={() => navigation.navigate("NewTaskSheet", { screen: "NewTask" })}
305 pendingTasks={pendingTasks}
306 projectGroupingMode={listOptions.projectGroupingMode}
307 projects={projects}
308 projectSortOrder={listOptions.projectSortOrder}
309 savedConnectionsById={savedConnectionsById}
310 searchQuery={searchQuery}
311 selectedEnvironmentId={selectedEnvironmentId}
312 selectedProjectKey={selectedProjectKey}
313 threads={threads}
314 />
315 </>
316 </AndroidHomeFabLayout>
317 );
318}