Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
4a5bc93
Refine light-mode sidebar surfaces
juliusmarminge Jul 22, 2026
a419786
Rework sidebar v2 light palette: zinc rail, white cards with hairline…
juliusmarminge Jul 22, 2026
fbaf6b7
Keep composer lower chrome opaque while the terminal drawer is open
juliusmarminge Jul 22, 2026
7acccd6
Lift mobile thread-list-v2 cards off the light screen as white surfaces
juliusmarminge Jul 22, 2026
b4fafe6
Drop card containers from mobile thread list v2: flat native rows
juliusmarminge Jul 22, 2026
9bd9f78
Align sidebar v2 status hues with the system-wide convention
juliusmarminge Jul 22, 2026
55bf99e
Show the host machine on mobile thread rows when several are connected
juliusmarminge Jul 22, 2026
fe57cfb
Render the PR badge on mobile thread-list-v2 rows
juliusmarminge Jul 22, 2026
de30f14
Pair the host machine with the project on thread-list-v2 rows
juliusmarminge Jul 22, 2026
9c9aa31
Bring Thread List v2 to the iPad split-view sidebar
juliusmarminge Jul 22, 2026
ab492a5
Restyle sidebar v2 rows to the split-view idiom
juliusmarminge Jul 22, 2026
2804406
Merge remote-tracking branch 'origin/main' into t3code/fix-lightmode-…
juliusmarminge Jul 22, 2026
6ddc391
Address v2 review findings: dead filter controls, pending tasks, stal…
juliusmarminge Jul 22, 2026
ddc8c71
Add project filtering to mobile thread navigation
juliusmarminge Jul 22, 2026
58f64c1
Fix mobile project filter feedback
juliusmarminge Jul 22, 2026
883d097
Use workspace test module for mobile filter test
juliusmarminge Jul 22, 2026
8baed8d
Scope mobile project empty states by environment
juliusmarminge Jul 22, 2026
0f4968c
Drop the v2 project chip row: the filter menu is the one filter surface
juliusmarminge Jul 22, 2026
abef74b
Move the host machine from the project line to the meta row
juliusmarminge Jul 22, 2026
c01c239
Merge branch 'main' into t3code/fix-lightmode-sidebar
juliusmarminge Jul 22, 2026
1c3fbd9
Strengthen light sidebar thread selection
juliusmarminge Jul 22, 2026
10f29ef
Unify Sidebar V2 surface tokens
juliusmarminge Jul 22, 2026
8c5b5b0
Fix light-mode sidebar styling
juliusmarminge Jul 22, 2026
4168fec
Fix light-mode sidebar surfaces and shortcuts
juliusmarminge Jul 22, 2026
89830a0
Fit swipe actions in compact thread rows
juliusmarminge Jul 22, 2026
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
149 changes: 119 additions & 30 deletions apps/mobile/src/features/home/HomeHeader.tsx
Comment thread
macroscopeapp[bot] marked this conversation as resolved.
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
Comment thread
juliusmarminge marked this conversation as resolved.
: 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}

Comment thread
cursor[bot] marked this conversation as resolved.
<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
Loading