Skip to content
Open
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
36 changes: 28 additions & 8 deletions apps/mobile/src/features/threads/NewTaskRouteScreen.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { NativeHeaderToolbar, NativeStackScreenOptions } from "../../native/StackHeader";
import { useIsFocused, useNavigation, type StaticScreenProps } from "@react-navigation/native";
import { SymbolView } from "../../components/AppSymbol";
import { buildProjectPickerDescription } from "@t3tools/client-runtime/state/project-grouping";
import type { EnvironmentId, ProjectId } from "@t3tools/contracts";
import { useEffect, useMemo, useRef } from "react";
import { ActivityIndicator, Alert, Platform, Pressable, ScrollView, View } from "react-native";
Expand All @@ -14,7 +15,11 @@ import { ProjectFavicon } from "../../components/ProjectFavicon";
import { useProjects, useThreadShells } from "../../state/entities";
import type { WorkspaceState } from "../../state/workspaceModel";
import { useWorkspaceState } from "../../state/workspace";
import { groupProjectsByRepository } from "../../lib/repositoryGroups";
import { useEnvironments } from "../../state/environments";
import {
expandRepositoryGroupProjects,
groupProjectsByRepository,
} from "../../lib/repositoryGroups";
import { useAdaptiveWorkspaceLayout } from "../layout/AdaptiveWorkspaceLayout";
import { useIncomingShare } from "../sharing/IncomingShareProvider";

Expand Down Expand Up @@ -81,6 +86,7 @@ export function NewTaskRouteScreen({ route }: StaticScreenProps<NewTaskRoutePara
const projects = useProjects();
const threads = useThreadShells();
const { state: catalogState } = useWorkspaceState();
const { environments } = useEnvironments();
const navigation = useNavigation();
const isFocused = useIsFocused();
const { layout } = useAdaptiveWorkspaceLayout();
Expand All @@ -104,29 +110,33 @@ export function NewTaskRouteScreen({ route }: StaticScreenProps<NewTaskRoutePara
() => groupProjectsByRepository({ projects, threads }),
[projects, threads],
);
const environmentLabelById = useMemo(
() =>
new Map(environments.map((environment) => [environment.environmentId, environment.label])),
[environments],
);
const showProjectEnvironmentLabels = environments.length > 1;
const items = useMemo(() => {
const nextItems: Array<{
readonly environmentId: EnvironmentId;
readonly id: ProjectId;
readonly key: string;
readonly title: string;
readonly workspaceRoot: string;
readonly environmentLabel: string | null;
}> = [];
for (const group of repositoryGroups) {
const project = group.projects[0]?.project;
if (!project) {
continue;
}
for (const { key, project } of expandRepositoryGroupProjects(repositoryGroups)) {
nextItems.push({
environmentId: project.environmentId,
id: project.id,
key: group.key,
key,
title: project.title,
workspaceRoot: project.workspaceRoot,
environmentLabel: environmentLabelById.get(project.environmentId) ?? null,
});
}
return nextItems;
}, [repositoryGroups]);
}, [environmentLabelById, repositoryGroups]);
Comment thread
coderabbitai[bot] marked this conversation as resolved.
const projectEmptyState = deriveProjectEmptyState(catalogState);
const resumedDestinationKeyRef = useRef<string | null>(null);
const reservedDestinationProject = incomingShare?.destination
Expand Down Expand Up @@ -313,6 +323,16 @@ export function NewTaskRouteScreen({ route }: StaticScreenProps<NewTaskRoutePara
</View>
<View className="flex-1">
<Text className="text-base leading-snug font-t3-bold">{item.title}</Text>
<Text
className="text-sm leading-snug text-foreground-muted"
numberOfLines={1}
>
{buildProjectPickerDescription({
workspaceRoot: item.workspaceRoot,
environmentLabel: item.environmentLabel,
showEnvironmentLabel: showProjectEnvironmentLabels,
})}
</Text>
</View>
<SymbolView
name="chevron.right"
Expand Down
25 changes: 5 additions & 20 deletions apps/mobile/src/features/threads/new-task-flow-provider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,10 @@ import {
groupByProvider,
resolveSelectableModelSelection,
} from "../../lib/modelOptions";
import { groupProjectsByRepository } from "../../lib/repositoryGroups";
import {
expandRepositoryGroupProjects,
groupProjectsByRepository,
} from "../../lib/repositoryGroups";
import { scopedProjectKey } from "../../lib/scopedEntities";
import { appAtomRegistry } from "../../state/atom-registry";
import {
Expand Down Expand Up @@ -181,25 +184,7 @@ export function NewTaskFlowProvider(props: React.PropsWithChildren) {
[projects, threads],
);
const logicalProjects = useMemo(
() =>
pipe(
repositoryGroups,
Arr.map((group) => {
const primaryProject = group.projects[0]?.project;
if (!primaryProject) {
return null;
}
return { key: group.key, project: primaryProject };
}),
Arr.filter(
(
entry,
): entry is {
readonly key: string;
readonly project: EnvironmentProject;
} => entry !== null,
),
),
() => expandRepositoryGroupProjects(repositoryGroups),
[repositoryGroups],
);

Expand Down
64 changes: 63 additions & 1 deletion apps/mobile/src/lib/repositoryGroups.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { describe, expect, it } from "vite-plus/test";

import { EnvironmentId, ProjectId, ProviderInstanceId, ThreadId } from "@t3tools/contracts";

import { groupProjectsByRepository } from "./repositoryGroups";
import { expandRepositoryGroupProjects, groupProjectsByRepository } from "./repositoryGroups";
import { EnvironmentProject, EnvironmentThreadShell } from "@t3tools/client-runtime/state/shell";

function makeProject(
Expand Down Expand Up @@ -44,6 +44,68 @@ function makeThread(
}

describe("groupProjectsByRepository", () => {
it("expands identical repository workspaces into scoped projects for every environment", () => {
const repositoryIdentity = {
canonicalKey: "github.com/t3tools/t3code",
locator: {
source: "git-remote" as const,
remoteName: "origin",
remoteUrl: "git@github.com:t3tools/t3code.git",
},
provider: "github",
owner: "t3tools",
name: "t3code",
displayName: "T3 Code",
};
const projects = [
makeProject({
environmentId: EnvironmentId.make("env-macbook"),
id: ProjectId.make("project-t3code"),
title: "T3 Code",
workspaceRoot: "/Users/henry/Desktop/t3code",
repositoryIdentity,
}),
makeProject({
environmentId: EnvironmentId.make("env-studio"),
id: ProjectId.make("project-t3code"),
title: "T3 Code",
workspaceRoot: "/Users/henry/Desktop/t3code",
repositoryIdentity,
}),
];

const expanded = expandRepositoryGroupProjects(
groupProjectsByRepository({ projects, threads: [] }),
);

expect(
expanded
.map(({ key, project }) => ({
key,
environmentId: project.environmentId,
projectId: project.id,
title: project.title,
workspaceRoot: project.workspaceRoot,
}))
.sort((left, right) => left.key.localeCompare(right.key)),
).toEqual([
{
key: "env-macbook:project-t3code",
environmentId: "env-macbook",
projectId: "project-t3code",
title: "T3 Code",
workspaceRoot: "/Users/henry/Desktop/t3code",
},
{
key: "env-studio:project-t3code",
environmentId: "env-studio",
projectId: "project-t3code",
title: "T3 Code",
workspaceRoot: "/Users/henry/Desktop/t3code",
},
]);
});

it("groups projects across environments by repository identity", () => {
const repoIdentity = {
canonicalKey: "github.com/t3tools/t3code",
Expand Down
6 changes: 6 additions & 0 deletions apps/mobile/src/lib/repositoryGroups.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,12 @@ export interface RepositoryGroup {
readonly projects: ReadonlyArray<RepositoryProjectGroup>;
}

export function expandRepositoryGroupProjects(
groups: ReadonlyArray<RepositoryGroup>,
): ReadonlyArray<RepositoryProjectGroup> {
return Arr.flatMap(groups, (group) => group.projects);
}

function compareIsoDateDescending(left: string, right: string): number {
return new Date(right).getTime() - new Date(left).getTime();
}
Expand Down
36 changes: 36 additions & 0 deletions apps/web/src/components/BranchToolbar.logic.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import {
resolvePreviousWorktreeLabel,
resolvePreviousWorktreeSeed,
shouldIncludeBranchPickerItem,
shouldShowComposerContextStrip,
shouldShowEnvironmentIndicator,
} from "./BranchToolbar.logic";

Expand Down Expand Up @@ -421,6 +422,41 @@ describe("shouldShowEnvironmentIndicator", () => {
});
});

describe("shouldShowComposerContextStrip", () => {
it("keeps machine identity visible for a remote non-git project", () => {
expect(
shouldShowComposerContextStrip({
hasActiveProject: true,
isGitRepo: false,
activeEnvironment: { isPrimary: false },
canPickEnvironment: false,
}),
).toBe(true);
});

it("keeps the existing git controls for a primary project", () => {
expect(
shouldShowComposerContextStrip({
hasActiveProject: true,
isGitRepo: true,
activeEnvironment: { isPrimary: true },
canPickEnvironment: false,
}),
).toBe(true);
});

it("hides the strip for a sole primary non-git project", () => {
expect(
shouldShowComposerContextStrip({
hasActiveProject: true,
isGitRepo: false,
activeEnvironment: { isPrimary: true },
canPickEnvironment: false,
}),
).toBe(false);
});
});

describe("resolveEffectiveEnvMode", () => {
it("treats draft threads already attached to a worktree as current-checkout mode", () => {
expect(
Expand Down
11 changes: 11 additions & 0 deletions apps/web/src/components/BranchToolbar.logic.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,17 @@ export function shouldShowEnvironmentIndicator(input: {
return input.activeEnvironment !== null && !input.activeEnvironment.isPrimary;
}

export function shouldShowComposerContextStrip(input: {
hasActiveProject: boolean;
isGitRepo: boolean;
activeEnvironment: Pick<EnvironmentOption, "isPrimary"> | null;
canPickEnvironment: boolean;
}): boolean {
if (!input.hasActiveProject) return false;
if (input.isGitRepo) return true;
return shouldShowEnvironmentIndicator(input);
}

export function resolveEnvModeLabel(mode: EnvMode): string {
return mode === "worktree" ? "New worktree" : "Current checkout";
}
Expand Down
49 changes: 33 additions & 16 deletions apps/web/src/components/BranchToolbar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ interface BranchToolbarProps {
onComposerFocusRequest?: () => void;
availableEnvironments?: readonly EnvironmentOption[];
onEnvironmentChange?: (environmentId: EnvironmentId) => void;
showWorkspaceControls?: boolean;
}

interface MobileRunContextSelectorProps {
Expand Down Expand Up @@ -229,6 +230,7 @@ export const BranchToolbar = memo(function BranchToolbar({
onComposerFocusRequest,
availableEnvironments,
onEnvironmentChange,
showWorkspaceControls = true,
}: BranchToolbarProps) {
const threadRef = useMemo(
() => scopeThreadRef(environmentId, threadId),
Expand Down Expand Up @@ -301,11 +303,24 @@ export const BranchToolbar = memo(function BranchToolbar({
});
const isMobile = useIsMobile();

if (!hasActiveThread || !activeProject) return null;
if (!hasActiveThread || !activeProject || (!showWorkspaceControls && !showEnvironmentIndicator)) {
return null;
}

return (
<div className="chat-composer-context-strip -mt-4 mx-auto flex w-[calc(100%-2.75rem)] max-w-[calc(48rem-2.75rem)] items-center gap-2 px-1 pt-5 pb-1">
{isMobile ? (
{!showWorkspaceControls ? (
<div className="flex min-w-0 flex-1 items-center">
{availableEnvironments ? (
<BranchToolbarEnvironmentSelector
envLocked={envLocked}
environmentId={environmentId}
availableEnvironments={availableEnvironments}
{...(showEnvironmentPicker && onEnvironmentChange ? { onEnvironmentChange } : {})}
/>
) : null}
</div>
) : isMobile ? (
<MobileRunContextSelector
envLocked={envLocked}
envModeLocked={envModeLocked}
Expand Down Expand Up @@ -344,20 +359,22 @@ export const BranchToolbar = memo(function BranchToolbar({
</div>
)}

<BranchToolbarBranchSelector
className="min-w-0 flex-1 justify-end md:ml-auto md:flex-none"
environmentId={environmentId}
threadId={threadId}
{...(draftId ? { draftId } : {})}
envLocked={envLocked}
{...(effectiveEnvModeOverride ? { effectiveEnvModeOverride } : {})}
{...(activeThreadBranchOverride !== undefined ? { activeThreadBranchOverride } : {})}
{...(onActiveThreadBranchOverrideChange ? { onActiveThreadBranchOverrideChange } : {})}
startFromOrigin={startFromOrigin}
onStartFromOriginChange={onStartFromOriginChange}
{...(onCheckoutPullRequestRequest ? { onCheckoutPullRequestRequest } : {})}
{...(onComposerFocusRequest ? { onComposerFocusRequest } : {})}
/>
{showWorkspaceControls ? (
<BranchToolbarBranchSelector
className="min-w-0 flex-1 justify-end md:ml-auto md:flex-none"
environmentId={environmentId}
threadId={threadId}
{...(draftId ? { draftId } : {})}
envLocked={envLocked}
{...(effectiveEnvModeOverride ? { effectiveEnvModeOverride } : {})}
{...(activeThreadBranchOverride !== undefined ? { activeThreadBranchOverride } : {})}
{...(onActiveThreadBranchOverrideChange ? { onActiveThreadBranchOverrideChange } : {})}
startFromOrigin={startFromOrigin}
onStartFromOriginChange={onStartFromOriginChange}
{...(onCheckoutPullRequestRequest ? { onCheckoutPullRequestRequest } : {})}
{...(onComposerFocusRequest ? { onComposerFocusRequest } : {})}
/>
) : null}
</div>
);
});
Loading
Loading