Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,8 @@ There's no public docs site yet, checkout the miscellaneous markdown files in [d
## Documentation

- [Getting started](./docs/getting-started/quick-start.md)
- [Remote access](./docs/user/remote-access.md)
- [Keeping T3 Code in sync](./docs/user/server-updates.md)
- [Architecture overview](./docs/architecture/overview.md)
- [Provider guides](./docs/providers/codex.md)
- [Operations](./docs/operations/ci.md)
Expand Down
2 changes: 1 addition & 1 deletion apps/mobile/src/connection/platform.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ import { clearComposerDraftsEnvironment } from "../state/use-composer-drafts";
import { connectionStorageLayer } from "./storage";

function networkStatus(state: Network.NetworkState): "unknown" | "offline" | "online" {
if (state.isConnected === false || state.isInternetReachable === false) {
if (state.isConnected === false) {
return "offline";
}
if (state.isConnected === true) {
Expand Down
149 changes: 119 additions & 30 deletions apps/mobile/src/features/home/HomeHeader.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ import type {
SidebarThreadSortOrder,
} from "@t3tools/contracts";
import type { MenuAction } from "@react-native-menu/menu";
import { useAtomValue } from "@effect/atom-react";
import { AsyncResult } from "effect/unstable/reactivity";
import { NativeHeaderToolbar, NativeStackScreenOptions } from "../../native/StackHeader";
import { useCallback, useMemo, useRef } from "react";
import { Platform, Pressable, Text as RNText, TextInput, View } from "react-native";
Expand All @@ -14,13 +16,15 @@ import { ControlPillMenu } from "../../components/ControlPill";
import { SymbolView } from "../../components/AppSymbol";
import { T3Wordmark } from "../../components/T3Wordmark";
import { useThemeColor } from "../../lib/useThemeColor";
import { mobilePreferencesAtom } from "../../state/preferences";
import { useHardwareKeyboardCommand } from "../keyboard/hardwareKeyboardCommands";
import { withNativeGlassHeaderItem } from "../layout/native-glass-header-items";
import { createNativeMailSearchToolbarItem } from "../layout/native-mail-search-toolbar";
import type { HomeProjectSortOrder } from "./homeThreadList";
import {
buildHomeListFilterMenu,
type HomeListFilterMenuEnvironment,
type HomeListFilterMenuProject,
} from "./home-list-filter-menu";
import {
hasCustomHomeListOptions,
Expand All @@ -33,13 +37,16 @@ export type HomeHeaderEnvironment = HomeListFilterMenuEnvironment;

export function HomeHeader(props: {
readonly environments: ReadonlyArray<HomeHeaderEnvironment>;
readonly projects: ReadonlyArray<HomeListFilterMenuProject>;
readonly searchQuery: string;
readonly selectedEnvironmentId: EnvironmentId | null;
readonly selectedProjectKey: string | null;
readonly projectSortOrder: HomeProjectSortOrder;
readonly threadSortOrder: SidebarThreadSortOrder;
readonly projectGroupingMode: SidebarProjectGroupingMode;
readonly onSearchQueryChange: (query: string) => void;
readonly onEnvironmentChange: (environmentId: EnvironmentId | null) => void;
readonly onProjectChange: (projectKey: string | null) => void;
readonly onProjectSortOrderChange: (sortOrder: HomeProjectSortOrder) => void;
readonly onThreadSortOrderChange: (sortOrder: SidebarThreadSortOrder) => void;
readonly onProjectGroupingModeChange: (mode: SidebarProjectGroupingMode) => void;
Expand All @@ -59,11 +66,24 @@ function checkedMenuState(checked: boolean) {
return checked ? ("on" as const) : undefined;
}

/** Thread List v2 lays the list out in fixed creation order, so the
sort/group filter controls would be silently ignored — hide them and
key the "customized" icon state off the environment filter alone. */
function useThreadListV2FilterGate() {
const preferencesResult = useAtomValue(mobilePreferencesAtom);
return (
AsyncResult.isSuccess(preferencesResult) && preferencesResult.value.threadListV2Enabled === true
);
}

function AndroidHomeHeader(props: HomeHeaderProps) {
const insets = useSafeAreaInsets();
const iconColor = useThemeColor("--color-icon");
const mutedColor = useThemeColor("--color-foreground-muted");
const hasCustomListOptions = hasCustomHomeListOptions(props);
const threadListV2Enabled = useThreadListV2FilterGate();
const hasCustomListOptions = threadListV2Enabled
? props.selectedEnvironmentId !== null || props.selectedProjectKey !== null
: hasCustomHomeListOptions(props);
const menuActions = useMemo<MenuAction[]>(
() => [
{
Expand All @@ -82,40 +102,67 @@ function AndroidHomeHeader(props: HomeHeaderProps) {
})),
],
},
{
id: "project-sort",
title: "Sort projects",
subactions: PROJECT_SORT_OPTIONS.map((option) => ({
id: `project-sort:${option.value}`,
title: option.label,
state: checkedMenuState(props.projectSortOrder === option.value),
})),
},
{
id: "thread-sort",
title: "Sort threads",
subactions: THREAD_SORT_OPTIONS.map((option) => ({
id: `thread-sort:${option.value}`,
title: option.label,
state: checkedMenuState(props.threadSortOrder === option.value),
})),
},
{
id: "project-grouping",
title: "Group projects",
subactions: PROJECT_GROUPING_OPTIONS.map((option) => ({
id: `project-grouping:${option.value}`,
title: option.label,
state: checkedMenuState(props.projectGroupingMode === option.value),
})),
},
...(props.projects.length === 0
? []
: ([
{
id: "project",
title: "Project",
subactions: [
{
id: "project:all",
title: "All projects",
state: checkedMenuState(props.selectedProjectKey === null),
},
...props.projects.map((project) => ({
id: `project:${project.key}`,
title: project.label,
state: checkedMenuState(props.selectedProjectKey === project.key),
})),
],
},
] satisfies MenuAction[])),
...(threadListV2Enabled
? []
: ([
{
id: "project-sort",
title: "Sort projects",
subactions: PROJECT_SORT_OPTIONS.map((option) => ({
id: `project-sort:${option.value}`,
title: option.label,
state: checkedMenuState(props.projectSortOrder === option.value),
})),
},
{
id: "thread-sort",
title: "Sort threads",
subactions: THREAD_SORT_OPTIONS.map((option) => ({
id: `thread-sort:${option.value}`,
title: option.label,
state: checkedMenuState(props.threadSortOrder === option.value),
})),
},
{
id: "project-grouping",
title: "Group projects",
subactions: PROJECT_GROUPING_OPTIONS.map((option) => ({
id: `project-grouping:${option.value}`,
title: option.label,
state: checkedMenuState(props.projectGroupingMode === option.value),
})),
},
] satisfies MenuAction[])),
],
[
props.environments,
props.projectGroupingMode,
props.projectSortOrder,
props.projects,
props.selectedEnvironmentId,
props.selectedProjectKey,
props.threadSortOrder,
threadListV2Enabled,
],
);
const handleMenuAction = useCallback(
Expand All @@ -137,6 +184,19 @@ function AndroidHomeHeader(props: HomeHeaderProps) {
return;
}

if (id === "project:all") {
props.onProjectChange(null);
return;
}

if (id.startsWith("project:")) {
const projectKey = id.slice("project:".length);
if (props.projects.some((project) => project.key === projectKey)) {
props.onProjectChange(projectKey);
}
return;
}

const projectSort = PROJECT_SORT_OPTIONS.find(
(option) => id === `project-sort:${option.value}`,
);
Expand Down Expand Up @@ -255,17 +315,24 @@ function AndroidHomeHeader(props: HomeHeaderProps) {
function IosHomeHeader(props: HomeHeaderProps) {
const searchBarRef = useRef<SearchBarCommands>(null);
const iconColor = useThemeColor("--color-icon");
const hasCustomListOptions = hasCustomHomeListOptions(props);
const threadListV2Enabled = useThreadListV2FilterGate();
const hasCustomListOptions = threadListV2Enabled
? props.selectedEnvironmentId !== null || props.selectedProjectKey !== null
: hasCustomHomeListOptions(props);
const focusSearch = useCallback(() => {
searchBarRef.current?.focus();
return searchBarRef.current !== null;
}, []);
useHardwareKeyboardCommand("focusSearch", focusSearch);
const filterMenu = buildHomeListFilterMenu(props);
const filterMenu = buildHomeListFilterMenu({
...props,
listOrganization: !threadListV2Enabled,
});

return (
<>
<NativeStackScreenOptions
optionsVersion={filterMenu.items}
options={{
// Static header config (glass, title, fonts) lives in Stack.tsx
// (GLASS_HEADER_OPTIONS). Only dynamic values are set here.
Expand Down Expand Up @@ -366,6 +433,28 @@ function IosHomeHeader(props: HomeHeaderProps) {
))}
</NativeHeaderToolbar.Menu>

{props.projects.length > 0 ? (
<NativeHeaderToolbar.Menu title="Project">
<NativeHeaderToolbar.Label>Project</NativeHeaderToolbar.Label>
<NativeHeaderToolbar.MenuAction
isOn={props.selectedProjectKey === null}
onPress={() => props.onProjectChange(null)}
subtitle="Show threads from every project"
>
<NativeHeaderToolbar.Label>All projects</NativeHeaderToolbar.Label>
</NativeHeaderToolbar.MenuAction>
{props.projects.map((project) => (
<NativeHeaderToolbar.MenuAction
key={project.key}
isOn={props.selectedProjectKey === project.key}
onPress={() => props.onProjectChange(project.key)}
>
<NativeHeaderToolbar.Label>{project.label}</NativeHeaderToolbar.Label>
</NativeHeaderToolbar.MenuAction>
))}
</NativeHeaderToolbar.Menu>
) : null}

<NativeHeaderToolbar.Menu title="Sort projects">
<NativeHeaderToolbar.Label>Sort projects</NativeHeaderToolbar.Label>
{PROJECT_SORT_OPTIONS.map((option) => (
Expand Down
30 changes: 29 additions & 1 deletion apps/mobile/src/features/home/HomeRouteScreen.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import * as Arr from "effect/Array";
import * as Order from "effect/Order";
import { useNavigation } from "@react-navigation/native";
import { useMemo, useState } from "react";
import { useEffect, useMemo, useState } from "react";

import { NativeHeaderToolbar, NativeStackScreenOptions } from "../../native/StackHeader";
import { scopedProjectKey } from "../../lib/scopedEntities";
import { useProjects, useThreadShells } from "../../state/entities";
import { usePendingNewTasks } from "../../state/use-pending-new-tasks";
import { useWorkspaceState } from "../../state/workspace";
Expand Down Expand Up @@ -58,6 +59,28 @@ export function HomeRouteScreen() {
setThreadSortOrder,
} = useHomeListOptions(availableEnvironmentIds);
const selectedEnvironmentId = listOptions.selectedEnvironmentId;
const [selectedProjectKey, setSelectedProjectKey] = useState<string | null>(null);
const projectFilterOptions = useMemo(
() =>
projects
.filter(
(project) =>
selectedEnvironmentId === null || project.environmentId === selectedEnvironmentId,
)
.map((project) => ({
key: scopedProjectKey(project.environmentId, project.id),
label: project.title,
})),
[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.
Expand Down Expand Up @@ -90,12 +113,15 @@ export function HomeRouteScreen() {
<NativeStackScreenOptions options={{ title: "Threads", headerTitle: "Threads" }} />
<HomeHeader
environments={environments}
projects={projectFilterOptions}
searchQuery={searchQuery}
selectedEnvironmentId={selectedEnvironmentId}
selectedProjectKey={selectedProjectKey}
projectSortOrder={listOptions.projectSortOrder}
threadSortOrder={listOptions.threadSortOrder}
projectGroupingMode={listOptions.projectGroupingMode}
onEnvironmentChange={setSelectedEnvironmentId}
onProjectChange={setSelectedProjectKey}
onOpenSettings={() => navigation.navigate("SettingsSheet", { screen: "Settings" })}
onProjectGroupingModeChange={setProjectGroupingMode}
onProjectSortOrderChange={setProjectSortOrder}
Expand All @@ -115,6 +141,7 @@ export function HomeRouteScreen() {
onSettleThread={settleThread}
onUnsettleThread={unsettleThread}
onEnvironmentChange={setSelectedEnvironmentId}
onProjectChange={setSelectedProjectKey}
onOpenEnvironments={() =>
navigation.navigate("SettingsSheet", { screen: "SettingsEnvironments" })
}
Expand Down Expand Up @@ -151,6 +178,7 @@ export function HomeRouteScreen() {
savedConnectionsById={savedConnectionsById}
searchQuery={searchQuery}
selectedEnvironmentId={selectedEnvironmentId}
selectedProjectKey={selectedProjectKey}
threads={threads}
threadSortOrder={listOptions.threadSortOrder}
/>
Expand Down
Loading