Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
6d1ee77
fix(mobile): preserve grouped project workspaces
shivamhwp Jul 27, 2026
5a38989
docs: add Android grouping verification screenshots
shivamhwp Jul 27, 2026
5b89b51
refactor(mobile): simplify project grouping settings
shivamhwp Jul 27, 2026
4f75888
refactor(mobile): use global project grouping only
shivamhwp Jul 27, 2026
72bf9c6
Merge branch 'main' into agent/fix-mobile-project-grouping
shivamhwp Jul 27, 2026
7cb3c13
fix mobile project grouping review issues
shivamhwp Jul 28, 2026
89ee06f
fix stale mobile draft project selection
shivamhwp Jul 28, 2026
da8bccd
Merge remote-tracking branch 'upstream/main' into agent/fix-mobile-pr…
shivamhwp Jul 30, 2026
69f4be5
Merge branch 'main' into agent/fix-mobile-project-grouping
shivamhwp Jul 31, 2026
e6a6e85
fix(mobile): gate grouping settings on hydration
shivamhwp Jul 31, 2026
a4b234f
Merge remote-tracking branch 'upstream/main' into agent/fix-mobile-pr…
shivamhwp Jul 31, 2026
b3810f5
chore(mobile): remove verification artifacts
shivamhwp Jul 31, 2026
b64c036
fix(mobile): preserve grouped project workspaces
shivamhwp Jul 27, 2026
5d5b5a8
docs: add Android grouping verification screenshots
shivamhwp Jul 27, 2026
b8380d1
refactor(mobile): simplify project grouping settings
shivamhwp Jul 27, 2026
2c2022d
refactor(mobile): use global project grouping only
shivamhwp Jul 27, 2026
9ffd3c4
fix mobile project grouping review issues
shivamhwp Jul 28, 2026
0ecedae
fix stale mobile draft project selection
shivamhwp Jul 28, 2026
9c6fd58
fix(mobile): gate grouping settings on hydration
shivamhwp Jul 31, 2026
e39c2ee
chore(mobile): remove verification artifacts
shivamhwp Jul 31, 2026
7e1cd27
docs(mobile): refresh grouping verification screenshots
Jul 31, 2026
20b7f06
chore(mobile): remove verification artifacts
Jul 31, 2026
fd21fbc
fix(mobile): polish project selection UI
Jul 31, 2026
4737831
docs(mobile): refresh grouping screenshots
Jul 31, 2026
166040d
chore(mobile): remove screenshot artifacts
Jul 31, 2026
6147257
Merge branch 'main' into agent/fix-mobile-project-grouping
shivamhwp Aug 4, 2026
68550f9
Merge remote-tracking branch 'origin/agent/fix-mobile-project-groupin…
shivamhwp Aug 4, 2026
9ebf027
Merge branch 'main' into agent/fix-mobile-project-grouping
shivamhwp Aug 5, 2026
98d00c3
Merge branch 'main' into agent/fix-mobile-project-grouping
shivamhwp Aug 5, 2026
db0db83
Merge branch 'main' into agent/fix-mobile-project-grouping
shivamhwp Aug 5, 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
8 changes: 8 additions & 0 deletions apps/mobile/src/Stack.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ import { SettingsClientStorageRouteScreen } from "./features/settings/SettingsCl
import { SettingsAuthRouteScreen } from "./features/settings/SettingsAuthRouteScreen";
import { SettingsEnvironmentsRouteScreen } from "./features/settings/SettingsEnvironmentsRouteScreen";
import { SettingsLegalRouteScreen } from "./features/settings/SettingsLegalRouteScreen";
import { SettingsProjectGroupingRouteScreen } from "./features/settings/SettingsProjectGroupingRouteScreen";
import { SettingsRouteScreen } from "./features/settings/SettingsRouteScreen";
import { SettingsWaitlistRouteScreen } from "./features/settings/SettingsWaitlistRouteScreen";
import { ShowcaseCaptureCoordinator } from "./features/showcase/ShowcaseCaptureCoordinator";
Expand Down Expand Up @@ -175,6 +176,13 @@ const SettingsSheetStack = createNativeStackNavigator({
title: "Appearance",
},
}),
SettingsProjectGrouping: createNativeStackScreen({
screen: SettingsProjectGroupingRouteScreen,
linking: "project-grouping",
options: {
title: "Project Grouping",
},
}),
SettingsClientStorage: createNativeStackScreen({
screen: SettingsClientStorageRouteScreen,
linking: "client-storage",
Expand Down
9 changes: 8 additions & 1 deletion apps/mobile/src/features/home/HomeRouteScreen.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -65,11 +65,17 @@ export function HomeRouteScreen() {
projects,
environmentId: selectedEnvironmentId,
projectGroupingMode: listOptions.projectGroupingMode,
projectGroupingOverrides: listOptions.projectGroupingOverrides,
}).map((scope) => ({
key: scope.key,
label: scope.title,
})),
[listOptions.projectGroupingMode, projects, selectedEnvironmentId],
[
listOptions.projectGroupingMode,
listOptions.projectGroupingOverrides,
projects,
selectedEnvironmentId,
],
);
useEffect(() => {
if (
Expand Down Expand Up @@ -168,6 +174,7 @@ export function HomeRouteScreen() {
onThreadSortOrderChange={setThreadSortOrder}
pendingTasks={pendingTasks}
projectGroupingMode={listOptions.projectGroupingMode}
projectGroupingOverrides={listOptions.projectGroupingOverrides}
projects={projects}
projectSortOrder={listOptions.projectSortOrder}
savedConnectionsById={savedConnectionsById}
Expand Down
11 changes: 10 additions & 1 deletion apps/mobile/src/features/home/HomeScreen.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@ interface HomeScreenProps {
readonly projectSortOrder: HomeProjectSortOrder;
readonly threadSortOrder: SidebarThreadSortOrder;
readonly projectGroupingMode: SidebarProjectGroupingMode;
readonly projectGroupingOverrides: Record<string, SidebarProjectGroupingMode>;
readonly onSearchQueryChange: (query: string) => void;
readonly onEnvironmentChange: (environmentId: EnvironmentId | null) => void;
readonly onProjectChange: (projectKey: string | null) => void;
Expand Down Expand Up @@ -251,8 +252,14 @@ export function HomeScreen(props: HomeScreenProps) {
projects: props.projects,
environmentId: props.selectedEnvironmentId,
projectGroupingMode: props.projectGroupingMode,
projectGroupingOverrides: props.projectGroupingOverrides,
}),
[props.projectGroupingMode, props.projects, props.selectedEnvironmentId],
[
props.projectGroupingMode,
props.projectGroupingOverrides,
props.projects,
props.selectedEnvironmentId,
],
);
const selectedProjectScope = useMemo(
() =>
Expand Down Expand Up @@ -321,9 +328,11 @@ export function HomeScreen(props: HomeScreenProps) {
projectSortOrder: props.projectSortOrder,
threadSortOrder: props.threadSortOrder,
projectGroupingMode: props.projectGroupingMode,
projectGroupingOverrides: props.projectGroupingOverrides,
}),
[
props.projectGroupingMode,
props.projectGroupingOverrides,
props.projectSortOrder,
props.searchQuery,
props.selectedEnvironmentId,
Expand Down
13 changes: 10 additions & 3 deletions apps/mobile/src/features/home/home-list-options.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ export interface HomeListOptions {

export interface ResolvedHomeListOptions extends HomeListOptions {
readonly projectGroupingMode: SidebarProjectGroupingMode;
readonly projectGroupingOverrides: Record<string, SidebarProjectGroupingMode>;
}

export function resolveProjectGroupingMode(
Expand Down Expand Up @@ -68,6 +69,7 @@ interface HomeListOptionsContextValue {
readonly options: HomeListOptions;
readonly setOptions: Dispatch<SetStateAction<HomeListOptions>>;
readonly projectGroupingMode: SidebarProjectGroupingMode;
readonly projectGroupingOverrides: Record<string, SidebarProjectGroupingMode>;
}

const HomeListOptionsContext = createContext<HomeListOptionsContextValue | null>(null);
Expand All @@ -76,11 +78,15 @@ const HomeListOptionsContext = createContext<HomeListOptionsContextValue | null>
export function HomeListOptionsProvider({
children,
projectGroupingMode,
}: PropsWithChildren<{ readonly projectGroupingMode: SidebarProjectGroupingMode }>) {
projectGroupingOverrides,
}: PropsWithChildren<{
Comment thread
cursor[bot] marked this conversation as resolved.
readonly projectGroupingMode: SidebarProjectGroupingMode;
readonly projectGroupingOverrides: Record<string, SidebarProjectGroupingMode>;
}>) {
const [options, setOptions] = useState<HomeListOptions>(defaultHomeListOptions);
const value = useMemo(
() => ({ options, setOptions, projectGroupingMode }),
[options, projectGroupingMode],
() => ({ options, setOptions, projectGroupingMode, projectGroupingOverrides }),
[options, projectGroupingMode, projectGroupingOverrides],
);
return createElement(HomeListOptionsContext, { value }, children);
}
Expand Down Expand Up @@ -119,6 +125,7 @@ export function useHomeListOptions(availableEnvironmentIds: ReadonlySet<Environm
const resolvedOptions: ResolvedHomeListOptions = {
...availableOptions,
projectGroupingMode: shared?.projectGroupingMode ?? "repository",
projectGroupingOverrides: shared?.projectGroupingOverrides ?? {},
};

const setSelectedEnvironmentId = useCallback((value: EnvironmentId | null) => {
Expand Down
20 changes: 13 additions & 7 deletions apps/mobile/src/features/home/homeThreadList.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -374,7 +374,7 @@ describe("buildHomeThreadGroups", () => {
).toHaveLength(2);
});

it("uses the repository label for a singleton repository scope", () => {
it("uses the physical project title for a singleton scope", () => {
const project = makeProject({
environmentId: EnvironmentId.make("environment-1"),
id: ProjectId.make("project-1"),
Expand Down Expand Up @@ -407,8 +407,8 @@ describe("buildHomeThreadGroups", () => {
],
);

expect(scopes[0]?.title).toBe("codething-mvp");
expect(groups[0]?.title).toBe("codething-mvp");
expect(scopes[0]?.title).toBe("local-worktree-name");
expect(groups[0]?.title).toBe("local-worktree-name");
});

it("sorts the newest thread first regardless of snapshot order", () => {
Expand Down Expand Up @@ -564,10 +564,16 @@ describe("buildHomeThreadGroups", () => {
);

expect(buildGroups(projects, threads, { projectGroupingMode: "repository" })).toHaveLength(1);
expect(buildGroups(projects, threads, { projectGroupingMode: "repository_path" })).toHaveLength(
2,
);
expect(buildGroups(projects, threads, { projectGroupingMode: "separate" })).toHaveLength(2);
expect(
buildGroups(projects, threads, { projectGroupingMode: "repository_path" }).map(
(group) => group.title,
),
).toEqual(["Mobile", "Web"]);
expect(
buildGroups(projects, threads, { projectGroupingMode: "separate" }).map(
(group) => group.title,
),
).toEqual(["Mobile", "Web"]);
});

it("default view shows only threads from the last 5 days", () => {
Expand Down
83 changes: 20 additions & 63 deletions apps/mobile/src/features/home/homeThreadList.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import {
deriveLogicalProjectKey,
buildProjectGroups,
derivePhysicalProjectKey,
deriveProjectGroupLabel,
} from "@t3tools/client-runtime/state/project-grouping";
Expand Down Expand Up @@ -36,10 +36,6 @@ export interface HomeProjectScope {
readonly projectRefs: ReadonlyArray<ScopedProjectRef>;
}

function getProjectFreshnessTimestamp(project: EnvironmentProject): number {
return toSortableTimestamp(project.updatedAt) ?? toSortableTimestamp(project.createdAt) ?? 0;
}

function getProjectSortTimestamp(
project: EnvironmentProject,
sortOrder: HomeProjectSortOrder,
Expand All @@ -55,68 +51,24 @@ export function buildHomeProjectScopes(input: {
readonly projects: ReadonlyArray<EnvironmentProject>;
readonly environmentId: EnvironmentId | null;
readonly projectGroupingMode: SidebarProjectGroupingMode;
readonly projectGroupingOverrides?: Record<string, SidebarProjectGroupingMode>;
}): ReadonlyArray<HomeProjectScope> {
const projects = input.projects.filter(
(project) => input.environmentId === null || project.environmentId === input.environmentId,
);
const projectsByPhysicalKey = new Map<string, EnvironmentProject[]>();
for (const project of projects) {
const physicalKey = derivePhysicalProjectKey(project);
const existing = projectsByPhysicalKey.get(physicalKey);
if (existing) existing.push(project);
else projectsByPhysicalKey.set(physicalKey, [project]);
}

const winnersByPhysicalKey = new Map<
string,
{ readonly key: string; readonly project: EnvironmentProject }
>();
for (const [physicalKey, members] of projectsByPhysicalKey) {
const project = members.reduce((winner, candidate) => {
const freshnessDelta =
getProjectFreshnessTimestamp(candidate) - getProjectFreshnessTimestamp(winner);
return freshnessDelta > 0 || (freshnessDelta === 0 && candidate.id > winner.id)
? candidate
: winner;
});
const identitySource = members.find((member) => member.repositoryIdentity !== null) ?? project;
winnersByPhysicalKey.set(physicalKey, {
key: deriveLogicalProjectKey(identitySource, { groupingMode: input.projectGroupingMode }),
project,
});
}

const groups = new Map<string, EnvironmentProject[]>();
for (const { key, project } of winnersByPhysicalKey.values()) {
const existing = groups.get(key);
if (existing) existing.push(project);
else groups.set(key, [project]);
}

const projectRefsByGroup = new Map<string, ScopedProjectRef[]>();
const seenProjectRefs = new Set<string>();
for (const project of projects) {
const refKey = scopedProjectKey(project.environmentId, project.id);
if (seenProjectRefs.has(refKey)) continue;
seenProjectRefs.add(refKey);

const key =
winnersByPhysicalKey.get(derivePhysicalProjectKey(project))?.key ??
deriveLogicalProjectKey(project, { groupingMode: input.projectGroupingMode });
const refs = projectRefsByGroup.get(key);
const projectRef = { environmentId: project.environmentId, projectId: project.id };
if (refs) refs.push(projectRef);
else projectRefsByGroup.set(key, [projectRef]);
}

return Array.from(groups, ([key, projects]) => {
const representative = projects[0]!;
return buildProjectGroups({
projects,
settings: {
sidebarProjectGroupingMode: input.projectGroupingMode,
sidebarProjectGroupingOverrides: input.projectGroupingOverrides ?? {},
},
}).map((group) => {
return {
key,
title: deriveProjectGroupLabel({ representative, members: projects }),
representative,
projects,
projectRefs: projectRefsByGroup.get(key) ?? [],
key: group.key,
title: group.label,
representative: group.representative,
projects: group.members.map((member) => member.project),
projectRefs: group.memberProjectRefs,
};
});
}
Expand Down Expand Up @@ -257,14 +209,17 @@ export function buildHomeThreadGroups(input: {
readonly projectSortOrder: HomeProjectSortOrder;
readonly threadSortOrder: SidebarThreadSortOrder;
readonly projectGroupingMode: SidebarProjectGroupingMode;
readonly projectGroupingOverrides?: Record<string, SidebarProjectGroupingMode>;
/** Current time used for the recency window; defaults to now. Injectable for tests. */
readonly now?: number;
}): ReadonlyArray<HomeThreadGroup> {
const now = input.now ?? Date.now();
const groups = new Map<string, MutableHomeThreadGroup>();
const groupTitleByKey = new Map<string, string>();
const groupKeyByProjectKey = new Map<string, string>();

for (const scope of buildHomeProjectScopes(input)) {
groupTitleByKey.set(scope.key, scope.title);
groups.set(scope.key, {
key: scope.key,
projects: [...scope.projects],
Expand Down Expand Up @@ -343,7 +298,9 @@ export function buildHomeThreadGroups(input: {
continue;
}

const title = deriveProjectGroupLabel({ representative, members: group.projects });
const title =
groupTitleByKey.get(group.key) ??
deriveProjectGroupLabel({ representative, members: group.projects });
const groupMatches =
query.length === 0 ||
title.toLocaleLowerCase().includes(query) ||
Expand Down
26 changes: 20 additions & 6 deletions apps/mobile/src/features/layout/AdaptiveWorkspaceLayout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -37,11 +37,15 @@ import {
import { resolveThreadSelectionNavigationAction } from "../../lib/adaptive-navigation";
import { scopedThreadKey } from "../../lib/scopedEntities";
import { mobilePreferencesAtom } from "../../state/preferences";
import {
DEFAULT_MOBILE_PROJECT_GROUPING_SETTINGS,
resolveMobileProjectGroupingSettings,
} from "../../state/project-grouping";
import {
parseActiveThreadPath,
useHardwareKeyboardCommand,
} from "../keyboard/hardwareKeyboardCommands";
import { HomeListOptionsProvider, resolveProjectGroupingMode } from "../home/home-list-options";
import { HomeListOptionsProvider } from "../home/home-list-options";
import { ThreadNavigationSidebar } from "../threads/ThreadNavigationSidebar";
import { WORKSPACE_PANE_TIMING } from "./workspace-pane-animation";
import { WorkspaceInspectorPane } from "./workspace-inspector-pane";
Expand Down Expand Up @@ -190,15 +194,21 @@ export function AdaptiveWorkspaceLayout(props: {
const preferencesResult = useAtomValue(mobilePreferencesAtom);
if (!AsyncResult.isSuccess(preferencesResult)) {
return AsyncResult.isFailure(preferencesResult) ? (
<AdaptiveWorkspaceLayoutContent {...props} projectGroupingMode="repository" />
<AdaptiveWorkspaceLayoutContent
{...props}
projectGroupingMode={DEFAULT_MOBILE_PROJECT_GROUPING_SETTINGS.sidebarProjectGroupingMode}
projectGroupingOverrides={
DEFAULT_MOBILE_PROJECT_GROUPING_SETTINGS.sidebarProjectGroupingOverrides
}
/>
) : null;
}
const groupingSettings = resolveMobileProjectGroupingSettings(preferencesResult.value);
return (
<AdaptiveWorkspaceLayoutContent
{...props}
projectGroupingMode={resolveProjectGroupingMode(
preferencesResult.value.projectGroupingEnabled,
)}
projectGroupingMode={groupingSettings.sidebarProjectGroupingMode}
projectGroupingOverrides={groupingSettings.sidebarProjectGroupingOverrides}
/>
);
}
Expand All @@ -209,6 +219,7 @@ function AdaptiveWorkspaceLayoutContent(
readonly pathname: string;
} & {
readonly projectGroupingMode: SidebarProjectGroupingMode;
readonly projectGroupingOverrides: Record<string, SidebarProjectGroupingMode>;
},
) {
const projectGroupingMode = props.projectGroupingMode;
Expand Down Expand Up @@ -502,7 +513,10 @@ function AdaptiveWorkspaceLayoutContent(
);

return (
<HomeListOptionsProvider projectGroupingMode={projectGroupingMode}>
<HomeListOptionsProvider
projectGroupingMode={projectGroupingMode}
projectGroupingOverrides={props.projectGroupingOverrides}
>
<AdaptiveWorkspaceContext.Provider value={contextValue}>
<View testID="adaptive-workspace-layout" className="flex-1 flex-row">
{shouldRenderPrimarySidebar && layout.listPaneWidth !== null ? (
Expand Down
Loading
Loading