Add a global New workspace picker (#1324)
* Add global New workspace project picker * Clean up New workspace picker state
This commit is contained in:
@@ -5,11 +5,12 @@ import { StyleSheet, useUnistyles } from "react-native-unistyles";
|
||||
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
||||
import { createNameId } from "mnemonic-id";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { Check, ChevronDown, GitBranch, GitPullRequest, X } from "lucide-react-native";
|
||||
import { Check, ChevronDown, Folder, GitBranch, GitPullRequest, X } from "lucide-react-native";
|
||||
import { Composer } from "@/composer";
|
||||
import { DraftAgentModeControl } from "@/composer/agent-controls/mode-control";
|
||||
import { splitComposerAttachmentsForSubmit } from "@/composer/attachments/submit";
|
||||
import { FileDropZone } from "@/components/file-drop-zone";
|
||||
import { ProjectIconView } from "@/components/project-icon-view";
|
||||
import { Combobox, ComboboxItem } from "@/components/ui/combobox";
|
||||
import type { ComboboxOption as ComboboxOptionType } from "@/components/ui/combobox";
|
||||
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip";
|
||||
@@ -21,15 +22,29 @@ import { useToast } from "@/contexts/toast-context";
|
||||
import { useAgentInputDraft } from "@/composer/draft/input-draft";
|
||||
import { useGithubSearchQuery } from "@/git/use-github-search-query";
|
||||
import { useHostRuntimeClient, useHostRuntimeIsConnected } from "@/runtime/host-runtime";
|
||||
import { navigateToWorkspace } from "@/stores/navigation-active-workspace-store";
|
||||
import {
|
||||
navigateToWorkspace,
|
||||
useLastWorkspaceSelection,
|
||||
} from "@/stores/navigation-active-workspace-store";
|
||||
import { normalizeWorkspaceDescriptor, useSessionStore } from "@/stores/session-store";
|
||||
import { useWorkspace } from "@/stores/session-store-hooks";
|
||||
import { generateDraftId } from "@/stores/draft-keys";
|
||||
import { useDraftStore } from "@/stores/draft-store";
|
||||
import { useCreateFlowStore } from "@/stores/create-flow-store";
|
||||
import { useWorkspaceDraftSubmissionStore } from "@/stores/workspace-draft-submission-store";
|
||||
import { generateMessageId } from "@/types/stream";
|
||||
import { toErrorMessage } from "@/utils/error-messages";
|
||||
import { projectIconPlaceholderLabelFromDisplayName } from "@/utils/project-display-name";
|
||||
import { navigateToPreparedWorkspaceTab } from "@/utils/workspace-navigation";
|
||||
import {
|
||||
hostProjectFromRoute,
|
||||
hostProjectFromWorkspace,
|
||||
resolveInitialWorktreeProject,
|
||||
resolveSelectedHostProject,
|
||||
useHostProjects,
|
||||
type HostProjectListItem,
|
||||
} from "@/projects/host-projects";
|
||||
import { useProjectIconDataByProjectKey } from "@/projects/project-icons";
|
||||
import type { ComposerAttachment, UserComposerAttachment } from "@/attachments/types";
|
||||
import type { ImageAttachment, MessagePayload } from "@/composer/types";
|
||||
import type { AgentAttachment, GitHubSearchItem } from "@getpaseo/protocol/messages";
|
||||
@@ -58,7 +73,7 @@ function resolveCheckoutRequest(
|
||||
|
||||
interface NewWorkspaceScreenProps {
|
||||
serverId: string;
|
||||
sourceDirectory: string;
|
||||
sourceDirectory?: string;
|
||||
projectId?: string;
|
||||
displayName?: string;
|
||||
}
|
||||
@@ -68,13 +83,38 @@ interface PickerOptionData {
|
||||
itemById: Map<string, PickerItem>;
|
||||
}
|
||||
|
||||
interface ProjectOptionData {
|
||||
options: ComboboxOptionType[];
|
||||
projectByOptionId: Map<string, HostProjectListItem>;
|
||||
}
|
||||
|
||||
interface PickerSelection {
|
||||
item: PickerItem;
|
||||
attachedPrNumber: number | null;
|
||||
}
|
||||
|
||||
interface NewWorkspaceProjectPickerInput {
|
||||
serverId: string;
|
||||
sourceDirectory?: string;
|
||||
projectId?: string;
|
||||
displayName?: string;
|
||||
}
|
||||
|
||||
interface NewWorkspaceProjectPickerState {
|
||||
projects: HostProjectListItem[];
|
||||
selectedProject: HostProjectListItem | null;
|
||||
selectedSourceDirectory: string | null;
|
||||
selectedDisplayName: string;
|
||||
projectPickerOptions: ComboboxOptionType[];
|
||||
projectByOptionId: Map<string, HostProjectListItem>;
|
||||
selectedProjectOptionId: string;
|
||||
projectTriggerLabel: string;
|
||||
handleSelectProjectOption: (id: string) => void;
|
||||
}
|
||||
|
||||
const BRANCH_OPTION_PREFIX = "branch:";
|
||||
const PR_OPTION_PREFIX = "github-pr:";
|
||||
const PROJECT_OPTION_PREFIX = "project:";
|
||||
|
||||
function RefPickerBadgeContent({
|
||||
selectedItem,
|
||||
@@ -150,6 +190,68 @@ function RefPickerTrigger({
|
||||
);
|
||||
}
|
||||
|
||||
function ProjectPickerTrigger({
|
||||
pickerAnchorRef,
|
||||
onPress,
|
||||
disabled,
|
||||
badgePressableStyle,
|
||||
label,
|
||||
projectKey,
|
||||
iconDataUri,
|
||||
iconColor,
|
||||
iconSize,
|
||||
}: {
|
||||
pickerAnchorRef: React.RefObject<View | null>;
|
||||
onPress: () => void;
|
||||
disabled: boolean;
|
||||
badgePressableStyle: React.ComponentProps<typeof Pressable>["style"];
|
||||
label: string;
|
||||
projectKey: string | null;
|
||||
iconDataUri: string | null;
|
||||
iconColor: string;
|
||||
iconSize: number;
|
||||
}) {
|
||||
const placeholderLabel = projectIconPlaceholderLabelFromDisplayName(label);
|
||||
const placeholderInitial = placeholderLabel.charAt(0).toUpperCase() || "?";
|
||||
return (
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild triggerRefProp="ref">
|
||||
<Pressable
|
||||
ref={pickerAnchorRef}
|
||||
testID="new-workspace-project-picker-trigger"
|
||||
onPress={onPress}
|
||||
disabled={disabled}
|
||||
style={badgePressableStyle}
|
||||
accessibilityRole="button"
|
||||
accessibilityLabel="Workspace project"
|
||||
>
|
||||
<View style={styles.badgeIconBox}>
|
||||
{projectKey ? (
|
||||
<ProjectIconView
|
||||
iconDataUri={iconDataUri}
|
||||
initial={placeholderInitial}
|
||||
projectKey={projectKey}
|
||||
imageStyle={styles.badgeProjectIcon}
|
||||
fallbackStyle={styles.badgeProjectIconFallback}
|
||||
textStyle={styles.badgeProjectIconFallbackText}
|
||||
/>
|
||||
) : (
|
||||
<Folder size={iconSize} color={iconColor} />
|
||||
)}
|
||||
</View>
|
||||
<Text style={styles.badgeText} numberOfLines={1}>
|
||||
{label}
|
||||
</Text>
|
||||
<ChevronDown size={iconSize} color={iconColor} />
|
||||
</Pressable>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="top" align="center" offset={8}>
|
||||
<Text style={styles.tooltipText}>Choose project</Text>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
|
||||
function CheckoutHintBadge({
|
||||
prNumber,
|
||||
onAccept,
|
||||
@@ -239,6 +341,59 @@ function PickerOptionItem({
|
||||
);
|
||||
}
|
||||
|
||||
function ProjectOptionItem({
|
||||
testID,
|
||||
projectKey,
|
||||
iconDataUri,
|
||||
label,
|
||||
description,
|
||||
selected,
|
||||
active,
|
||||
disabled,
|
||||
onPress,
|
||||
}: {
|
||||
testID: string;
|
||||
projectKey: string;
|
||||
iconDataUri: string | null;
|
||||
label: string;
|
||||
description: string | undefined;
|
||||
selected: boolean;
|
||||
active: boolean;
|
||||
disabled: boolean;
|
||||
onPress: () => void;
|
||||
}) {
|
||||
const placeholderLabel = projectIconPlaceholderLabelFromDisplayName(label);
|
||||
const placeholderInitial = placeholderLabel.charAt(0).toUpperCase() || "?";
|
||||
const leadingSlot = useMemo(
|
||||
() => (
|
||||
<View style={styles.rowIconBox}>
|
||||
<ProjectIconView
|
||||
iconDataUri={iconDataUri}
|
||||
initial={placeholderInitial}
|
||||
projectKey={projectKey}
|
||||
imageStyle={styles.projectOptionIcon}
|
||||
fallbackStyle={styles.projectOptionIconFallback}
|
||||
textStyle={styles.projectOptionIconFallbackText}
|
||||
/>
|
||||
</View>
|
||||
),
|
||||
[iconDataUri, placeholderInitial, projectKey],
|
||||
);
|
||||
|
||||
return (
|
||||
<ComboboxItem
|
||||
testID={testID}
|
||||
label={label}
|
||||
description={description}
|
||||
selected={selected}
|
||||
active={active}
|
||||
disabled={disabled}
|
||||
onPress={onPress}
|
||||
leadingSlot={leadingSlot}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function branchOptionId(name: string): string {
|
||||
return `${BRANCH_OPTION_PREFIX}${name}`;
|
||||
}
|
||||
@@ -247,6 +402,10 @@ function prOptionId(number: number): string {
|
||||
return `${PR_OPTION_PREFIX}${number}`;
|
||||
}
|
||||
|
||||
function projectOptionId(projectId: string): string {
|
||||
return `${PROJECT_OPTION_PREFIX}${projectId}`;
|
||||
}
|
||||
|
||||
function formatPrLabel(item: { number: number; title: string }): string {
|
||||
return `#${item.number} ${item.title}`;
|
||||
}
|
||||
@@ -256,7 +415,7 @@ function pickerItemLabel(item: PickerItem): string {
|
||||
}
|
||||
|
||||
function pickerItemTriggerLabel(item: PickerItem): string {
|
||||
return item.kind === "branch" ? item.name : formatPrLabel(item.item);
|
||||
return pickerItemLabel(item);
|
||||
}
|
||||
|
||||
function computePickerOptionData(
|
||||
@@ -292,6 +451,108 @@ function computePickerOptionData(
|
||||
return { options: timedOptions.map((t) => t.option), itemById: idMap };
|
||||
}
|
||||
|
||||
function computeProjectOptionData(projects: readonly HostProjectListItem[]): ProjectOptionData {
|
||||
const projectByOptionId = new Map<string, HostProjectListItem>();
|
||||
const options = projects.map((project) => {
|
||||
const id = projectOptionId(project.projectKey);
|
||||
projectByOptionId.set(id, project);
|
||||
return { id, label: project.projectName };
|
||||
});
|
||||
return { options, projectByOptionId };
|
||||
}
|
||||
|
||||
function useNewWorkspaceProjectPicker({
|
||||
serverId,
|
||||
sourceDirectory,
|
||||
projectId,
|
||||
displayName: displayNameProp,
|
||||
}: NewWorkspaceProjectPickerInput): NewWorkspaceProjectPickerState {
|
||||
const [manualProjectKey, setManualProjectKey] = useState<string | null>(null);
|
||||
const displayName = displayNameProp?.trim() ?? "";
|
||||
const projects = useHostProjects(serverId || null);
|
||||
const lastWorkspaceSelection = useLastWorkspaceSelection();
|
||||
const lastWorkspaceServerId = lastWorkspaceSelection?.serverId === serverId ? serverId : null;
|
||||
const lastWorkspaceId =
|
||||
lastWorkspaceSelection?.serverId === serverId ? lastWorkspaceSelection.workspaceId : null;
|
||||
const lastWorkspace = useWorkspace(lastWorkspaceServerId, lastWorkspaceId);
|
||||
const routeProject = useMemo(
|
||||
() =>
|
||||
hostProjectFromRoute({
|
||||
serverId,
|
||||
projectId,
|
||||
displayName,
|
||||
sourceDirectory,
|
||||
}),
|
||||
[displayName, projectId, serverId, sourceDirectory],
|
||||
);
|
||||
const lastActiveProject = useMemo(
|
||||
() => hostProjectFromWorkspace({ serverId, workspace: lastWorkspace }),
|
||||
[lastWorkspace, serverId],
|
||||
);
|
||||
const initialProject = useMemo(
|
||||
() =>
|
||||
resolveInitialWorktreeProject({
|
||||
routeProject,
|
||||
lastActiveProject,
|
||||
projects,
|
||||
}),
|
||||
[lastActiveProject, projects, routeProject],
|
||||
);
|
||||
const worktreeProjects = useMemo(
|
||||
() => projects.filter((project) => project.canCreateWorktree),
|
||||
[projects],
|
||||
);
|
||||
|
||||
const selectedProjectKey = manualProjectKey ?? initialProject?.projectKey ?? null;
|
||||
|
||||
const selectedProject = useMemo(
|
||||
() =>
|
||||
resolveSelectedHostProject({
|
||||
selectedProjectKey,
|
||||
projects,
|
||||
routeProject,
|
||||
lastActiveProject,
|
||||
}),
|
||||
[lastActiveProject, projects, routeProject, selectedProjectKey],
|
||||
);
|
||||
const { options: projectPickerOptions, projectByOptionId }: ProjectOptionData = useMemo(
|
||||
() => computeProjectOptionData(worktreeProjects),
|
||||
[worktreeProjects],
|
||||
);
|
||||
const handleSelectProjectOption = useCallback(
|
||||
(id: string) => {
|
||||
const project = projectByOptionId.get(id);
|
||||
if (!project?.canCreateWorktree) return;
|
||||
setManualProjectKey(project.projectKey);
|
||||
},
|
||||
[projectByOptionId],
|
||||
);
|
||||
|
||||
return {
|
||||
projects,
|
||||
selectedProject,
|
||||
selectedSourceDirectory: selectedProject?.iconWorkingDir ?? null,
|
||||
selectedDisplayName: selectedProject?.projectName ?? displayName,
|
||||
projectPickerOptions,
|
||||
projectByOptionId,
|
||||
selectedProjectOptionId: selectedProject ? projectOptionId(selectedProject.projectKey) : "",
|
||||
projectTriggerLabel: selectedProject?.projectName ?? "Choose project",
|
||||
handleSelectProjectOption,
|
||||
};
|
||||
}
|
||||
|
||||
function getContentStyle(input: { isCompact: boolean; insetBottom: number }) {
|
||||
if (input.isCompact) {
|
||||
return [styles.content, styles.contentCompact, { paddingBottom: input.insetBottom }];
|
||||
}
|
||||
return [styles.content, styles.contentCentered];
|
||||
}
|
||||
|
||||
function getSelectedPickerItem(selection: PickerSelection | null): PickerItem | null {
|
||||
if (!selection) return null;
|
||||
return selection.item;
|
||||
}
|
||||
|
||||
function normalizeBranchDetails(
|
||||
data:
|
||||
| { branchDetails?: Array<{ name: string; committerDate: number }>; branches?: string[] }
|
||||
@@ -378,32 +639,31 @@ function buildComposerConfig(input: {
|
||||
serverId: string;
|
||||
isConnected: boolean;
|
||||
workspaceDirectory: string | null;
|
||||
sourceDirectory: string;
|
||||
sourceDirectory: string | null;
|
||||
}): Parameters<typeof useAgentInputDraft>[0]["composer"] {
|
||||
const { serverId, isConnected, workspaceDirectory, sourceDirectory } = input;
|
||||
const workingDir = workspaceDirectory || sourceDirectory || undefined;
|
||||
return {
|
||||
initialServerId: serverId || null,
|
||||
initialValues:
|
||||
workspaceDirectory || sourceDirectory
|
||||
? { workingDir: workspaceDirectory || sourceDirectory }
|
||||
: undefined,
|
||||
initialValues: workingDir ? { workingDir } : undefined,
|
||||
isVisible: true,
|
||||
onlineServerIds: isConnected && serverId ? [serverId] : [],
|
||||
lockedWorkingDir: workspaceDirectory || sourceDirectory || undefined,
|
||||
lockedWorkingDir: workingDir,
|
||||
};
|
||||
}
|
||||
|
||||
function computeWorkspaceTitle(
|
||||
workspace: ReturnType<typeof normalizeWorkspaceDescriptor> | null,
|
||||
displayName: string,
|
||||
sourceDirectory: string,
|
||||
sourceDirectory: string | null,
|
||||
): string {
|
||||
const fallbackDirectoryName = sourceDirectory?.split(/[\\/]/).findLast(Boolean) ?? null;
|
||||
return (
|
||||
workspace?.name ||
|
||||
workspace?.projectDisplayName ||
|
||||
displayName ||
|
||||
sourceDirectory.split(/[\\/]/).findLast(Boolean) ||
|
||||
sourceDirectory
|
||||
fallbackDirectoryName ||
|
||||
"Choose project"
|
||||
);
|
||||
}
|
||||
|
||||
@@ -503,7 +763,7 @@ function submitWorkspaceDraft(input: SubmitDraftInput): void {
|
||||
|
||||
export function NewWorkspaceScreen({
|
||||
serverId,
|
||||
sourceDirectory,
|
||||
sourceDirectory: sourceDirectoryProp,
|
||||
projectId,
|
||||
displayName: displayNameProp,
|
||||
}: NewWorkspaceScreenProps) {
|
||||
@@ -519,9 +779,11 @@ export function NewWorkspaceScreen({
|
||||
const [pendingAction, setPendingAction] = useState<"chat" | "empty" | null>(null);
|
||||
const [manualPickerSelection, setManualPickerSelection] = useState<PickerSelection | null>(null);
|
||||
const [pickerOpen, setPickerOpen] = useState(false);
|
||||
const [projectPickerOpen, setProjectPickerOpen] = useState(false);
|
||||
const [pickerSearchQuery, setPickerSearchQuery] = useState("");
|
||||
const [debouncedPickerSearchQuery, setDebouncedPickerSearchQuery] = useState("");
|
||||
const pickerAnchorRef = useRef<View>(null);
|
||||
const projectPickerAnchorRef = useRef<View>(null);
|
||||
|
||||
useEffect(() => {
|
||||
const trimmed = pickerSearchQuery.trim();
|
||||
@@ -529,26 +791,42 @@ export function NewWorkspaceScreen({
|
||||
return () => clearTimeout(timer);
|
||||
}, [pickerSearchQuery]);
|
||||
|
||||
const displayName = displayNameProp?.trim() ?? "";
|
||||
const workspace = createdWorkspace;
|
||||
const isPending = pendingAction !== null;
|
||||
const client = useHostRuntimeClient(serverId);
|
||||
const isConnected = useHostRuntimeIsConnected(serverId);
|
||||
const draftKey = `new-workspace:${serverId}:${sourceDirectory}`;
|
||||
const {
|
||||
projects,
|
||||
selectedProject,
|
||||
selectedSourceDirectory,
|
||||
selectedDisplayName,
|
||||
projectPickerOptions,
|
||||
projectByOptionId,
|
||||
selectedProjectOptionId,
|
||||
projectTriggerLabel,
|
||||
handleSelectProjectOption: selectProjectOption,
|
||||
} = useNewWorkspaceProjectPicker({
|
||||
serverId,
|
||||
sourceDirectory: sourceDirectoryProp,
|
||||
projectId,
|
||||
displayName: displayNameProp,
|
||||
});
|
||||
const projectIconDataByProjectKey = useProjectIconDataByProjectKey({ serverId, projects });
|
||||
const draftKey = `new-workspace:${serverId}:${selectedSourceDirectory ?? "choose-project"}`;
|
||||
const chatDraft = useAgentInputDraft({
|
||||
draftKey,
|
||||
composer: buildComposerConfig({
|
||||
serverId,
|
||||
isConnected,
|
||||
workspaceDirectory: workspace?.workspaceDirectory ?? null,
|
||||
sourceDirectory,
|
||||
sourceDirectory: selectedSourceDirectory,
|
||||
}),
|
||||
});
|
||||
const composerState = chatDraft.composerState;
|
||||
const [dismissedCheckoutHintPrNumbers, setDismissedCheckoutHintPrNumbers] =
|
||||
useCheckoutHintDismissals(chatDraft.attachments);
|
||||
|
||||
const selectedItem = manualPickerSelection?.item ?? null;
|
||||
const selectedItem = getSelectedPickerItem(manualPickerSelection);
|
||||
|
||||
const withConnectedClient = useCallback(() => {
|
||||
if (!client || !isConnected) {
|
||||
@@ -558,15 +836,19 @@ export function NewWorkspaceScreen({
|
||||
}, [client, isConnected]);
|
||||
|
||||
const clientReady = isConnected && Boolean(client);
|
||||
const pickerQueryEnabled = pickerOpen && clientReady;
|
||||
const hasSelectedSourceDirectory = selectedSourceDirectory !== null;
|
||||
const pickerQueryEnabled = pickerOpen && clientReady && hasSelectedSourceDirectory;
|
||||
|
||||
const checkoutStatusQuery = useQuery({
|
||||
queryKey: ["checkout-status", serverId, sourceDirectory],
|
||||
queryKey: ["checkout-status", serverId, selectedSourceDirectory],
|
||||
queryFn: async () => {
|
||||
if (!selectedSourceDirectory) {
|
||||
throw new Error("Choose a project");
|
||||
}
|
||||
const connectedClient = withConnectedClient();
|
||||
return connectedClient.getCheckoutStatus(sourceDirectory);
|
||||
return connectedClient.getCheckoutStatus(selectedSourceDirectory);
|
||||
},
|
||||
enabled: clientReady,
|
||||
enabled: clientReady && hasSelectedSourceDirectory,
|
||||
staleTime: Infinity,
|
||||
refetchOnMount: false,
|
||||
refetchOnReconnect: false,
|
||||
@@ -576,11 +858,14 @@ export function NewWorkspaceScreen({
|
||||
const currentBranch = checkoutStatusQuery.data?.currentBranch ?? null;
|
||||
|
||||
const branchSuggestionsQuery = useQuery({
|
||||
queryKey: ["branch-suggestions", serverId, sourceDirectory, debouncedPickerSearchQuery],
|
||||
queryKey: ["branch-suggestions", serverId, selectedSourceDirectory, debouncedPickerSearchQuery],
|
||||
queryFn: async () => {
|
||||
if (!selectedSourceDirectory) {
|
||||
throw new Error("Choose a project");
|
||||
}
|
||||
const connectedClient = withConnectedClient();
|
||||
return connectedClient.getBranchSuggestions({
|
||||
cwd: sourceDirectory,
|
||||
cwd: selectedSourceDirectory,
|
||||
query: debouncedPickerSearchQuery || undefined,
|
||||
limit: 20,
|
||||
});
|
||||
@@ -592,7 +877,7 @@ export function NewWorkspaceScreen({
|
||||
const githubPrSearchQuery = useGithubSearchQuery({
|
||||
client,
|
||||
serverId,
|
||||
cwd: sourceDirectory,
|
||||
cwd: selectedSourceDirectory ?? "",
|
||||
query: debouncedPickerSearchQuery,
|
||||
kinds: ["github-pr"],
|
||||
enabled: pickerQueryEnabled,
|
||||
@@ -612,7 +897,6 @@ export function NewWorkspaceScreen({
|
||||
() => computePickerOptionData(branchDetails, prItems),
|
||||
[branchDetails, prItems],
|
||||
);
|
||||
|
||||
const triggerLabel = useMemo(() => {
|
||||
if (selectedItem) return pickerItemTriggerLabel(selectedItem);
|
||||
return currentBranch ?? "main";
|
||||
@@ -624,7 +908,6 @@ export function NewWorkspaceScreen({
|
||||
? branchOptionId(selectedItem.name)
|
||||
: prOptionId(selectedItem.item.number);
|
||||
}, [selectedItem]);
|
||||
|
||||
const selectPickerItem = useCallback(
|
||||
(item: PickerItem) => {
|
||||
const next = syncPickerPrAttachment({
|
||||
@@ -654,6 +937,17 @@ export function NewWorkspaceScreen({
|
||||
[itemById, selectPickerItem],
|
||||
);
|
||||
|
||||
const handleSelectProjectOption = useCallback(
|
||||
(id: string) => {
|
||||
const project = projectByOptionId.get(id);
|
||||
if (!project?.canCreateWorktree) return;
|
||||
selectProjectOption(id);
|
||||
setProjectPickerOpen(false);
|
||||
setManualPickerSelection(null);
|
||||
},
|
||||
[projectByOptionId, selectProjectOption],
|
||||
);
|
||||
|
||||
const checkoutHintPrAttachment = useMemo(
|
||||
() =>
|
||||
findCheckoutHintPrAttachment({
|
||||
@@ -684,6 +978,10 @@ export function NewWorkspaceScreen({
|
||||
setPickerOpen(true);
|
||||
}, []);
|
||||
|
||||
const openProjectPicker = useCallback(() => {
|
||||
setProjectPickerOpen(true);
|
||||
}, []);
|
||||
|
||||
const handleClearDraft = useCallback(() => {
|
||||
// No-op: screen navigates away on success, text should stay for retry on error
|
||||
}, []);
|
||||
@@ -705,19 +1003,26 @@ export function NewWorkspaceScreen({
|
||||
}
|
||||
}, []);
|
||||
|
||||
const handleProjectPickerOpenChange = useCallback((nextOpen: boolean) => {
|
||||
setProjectPickerOpen(nextOpen);
|
||||
}, []);
|
||||
|
||||
const buildCreateWorktreeInput = useCallback(
|
||||
(input: {
|
||||
cwd: string;
|
||||
prompt: string;
|
||||
attachments: AgentAttachment[];
|
||||
}): CreatePaseoWorktreeInput => {
|
||||
if (!selectedProject) {
|
||||
throw new Error("Choose a project");
|
||||
}
|
||||
const checkoutRequest = resolveCheckoutRequest(selectedItem, currentBranch);
|
||||
const trimmedPrompt = input.prompt.trim();
|
||||
const hasFirstAgentContext = trimmedPrompt.length > 0 || input.attachments.length > 0;
|
||||
|
||||
return {
|
||||
cwd: input.cwd,
|
||||
...(projectId ? { projectId } : {}),
|
||||
cwd: selectedProject.iconWorkingDir,
|
||||
projectId: selectedProject.projectKey,
|
||||
worktreeSlug: createNameId(),
|
||||
...(hasFirstAgentContext
|
||||
? {
|
||||
@@ -730,7 +1035,7 @@ export function NewWorkspaceScreen({
|
||||
...checkoutRequest,
|
||||
};
|
||||
},
|
||||
[currentBranch, projectId, selectedItem],
|
||||
[currentBranch, selectedItem, selectedProject],
|
||||
);
|
||||
|
||||
const ensureWorkspace = useCallback(
|
||||
@@ -783,7 +1088,11 @@ export function NewWorkspaceScreen({
|
||||
[composerState, draftKey, ensureWorkspace, serverId, toast],
|
||||
);
|
||||
|
||||
const workspaceTitle = computeWorkspaceTitle(workspace, displayName, sourceDirectory);
|
||||
const workspaceTitle = computeWorkspaceTitle(
|
||||
workspace,
|
||||
selectedDisplayName,
|
||||
selectedSourceDirectory,
|
||||
);
|
||||
|
||||
const addImagesRef = useRef<((images: ImageAttachment[]) => void) | null>(null);
|
||||
const handleAddImagesCallback = useCallback((addImages: (images: ImageAttachment[]) => void) => {
|
||||
@@ -835,12 +1144,40 @@ export function NewWorkspaceScreen({
|
||||
[isPending, itemById, theme.colors.foregroundMuted, theme.iconSize.sm],
|
||||
);
|
||||
|
||||
const renderProjectOption = useCallback(
|
||||
({
|
||||
option,
|
||||
selected,
|
||||
active,
|
||||
onPress,
|
||||
}: {
|
||||
option: ComboboxOptionType;
|
||||
selected: boolean;
|
||||
active: boolean;
|
||||
onPress: () => void;
|
||||
}) => {
|
||||
const project = projectByOptionId.get(option.id);
|
||||
if (!project) return <View key={option.id} />;
|
||||
|
||||
return (
|
||||
<ProjectOptionItem
|
||||
testID={`new-workspace-project-picker-option-${project.projectKey}`}
|
||||
projectKey={project.projectKey}
|
||||
iconDataUri={projectIconDataByProjectKey.get(project.projectKey) ?? null}
|
||||
label={project.projectName}
|
||||
description={project.iconWorkingDir}
|
||||
selected={selected}
|
||||
active={active}
|
||||
disabled={isPending || !project.canCreateWorktree}
|
||||
onPress={onPress}
|
||||
/>
|
||||
);
|
||||
},
|
||||
[isPending, projectByOptionId, projectIconDataByProjectKey],
|
||||
);
|
||||
|
||||
const contentStyle = useMemo(
|
||||
() => [
|
||||
styles.content,
|
||||
isCompact ? styles.contentCompact : styles.contentCentered,
|
||||
isCompact ? { paddingBottom: insets.bottom } : null,
|
||||
],
|
||||
() => getContentStyle({ isCompact, insetBottom: insets.bottom }),
|
||||
[isCompact, insets.bottom],
|
||||
);
|
||||
|
||||
@@ -863,11 +1200,42 @@ export function NewWorkspaceScreen({
|
||||
const composerFooter = useMemo(
|
||||
() => (
|
||||
<View testID="new-workspace-ref-picker-row" style={styles.optionsRow}>
|
||||
<View>
|
||||
<ProjectPickerTrigger
|
||||
pickerAnchorRef={projectPickerAnchorRef}
|
||||
onPress={openProjectPicker}
|
||||
disabled={isPending || projectPickerOptions.length === 0}
|
||||
badgePressableStyle={badgePressableStyle}
|
||||
label={projectTriggerLabel}
|
||||
projectKey={selectedProject?.projectKey ?? null}
|
||||
iconDataUri={
|
||||
selectedProject
|
||||
? (projectIconDataByProjectKey.get(selectedProject.projectKey) ?? null)
|
||||
: null
|
||||
}
|
||||
iconColor={theme.colors.foregroundMuted}
|
||||
iconSize={theme.iconSize.sm}
|
||||
/>
|
||||
<Combobox
|
||||
options={projectPickerOptions}
|
||||
value={selectedProjectOptionId}
|
||||
onSelect={handleSelectProjectOption}
|
||||
searchable
|
||||
searchPlaceholder="Search projects"
|
||||
title="Project"
|
||||
open={projectPickerOpen}
|
||||
onOpenChange={handleProjectPickerOpenChange}
|
||||
desktopPlacement="bottom-start"
|
||||
anchorRef={projectPickerAnchorRef}
|
||||
emptyText="No projects available."
|
||||
renderOption={renderProjectOption}
|
||||
/>
|
||||
</View>
|
||||
<View>
|
||||
<RefPickerTrigger
|
||||
pickerAnchorRef={pickerAnchorRef}
|
||||
onPress={openPicker}
|
||||
disabled={isPending}
|
||||
disabled={isPending || !selectedSourceDirectory}
|
||||
badgePressableStyle={badgePressableStyle}
|
||||
selectedItem={selectedItem}
|
||||
triggerLabel={triggerLabel}
|
||||
@@ -910,15 +1278,26 @@ export function NewWorkspaceScreen({
|
||||
checkoutHintPrAttachment,
|
||||
dismissCheckoutHint,
|
||||
handlePickerOpenChange,
|
||||
handleProjectPickerOpenChange,
|
||||
handleSelectOption,
|
||||
handleSelectProjectOption,
|
||||
isPending,
|
||||
openPicker,
|
||||
openProjectPicker,
|
||||
options,
|
||||
pickerEmptyText,
|
||||
pickerOpen,
|
||||
projectPickerOpen,
|
||||
projectPickerOptions,
|
||||
projectTriggerLabel,
|
||||
projectIconDataByProjectKey,
|
||||
renderPickerOption,
|
||||
renderProjectOption,
|
||||
selectedItem,
|
||||
selectedOptionId,
|
||||
selectedProject,
|
||||
selectedProjectOptionId,
|
||||
selectedSourceDirectory,
|
||||
setPickerSearchQuery,
|
||||
agentControlsWithDisabled,
|
||||
theme.colors.foregroundMuted,
|
||||
@@ -951,7 +1330,7 @@ export function NewWorkspaceScreen({
|
||||
<TitlebarDragRegion />
|
||||
<View style={styles.centered}>
|
||||
<Composer
|
||||
agentId={`new-workspace:${serverId}:${sourceDirectory}`}
|
||||
agentId={draftKey}
|
||||
serverId={serverId}
|
||||
isPaneFocused={true}
|
||||
onSubmitMessage={handleSubmitNewWorkspace}
|
||||
@@ -965,7 +1344,7 @@ export function NewWorkspaceScreen({
|
||||
onChangeText={chatDraft.setText}
|
||||
attachments={chatDraft.attachments}
|
||||
onChangeAttachments={chatDraft.setAttachments}
|
||||
cwd={sourceDirectory}
|
||||
cwd={selectedSourceDirectory ?? ""}
|
||||
clearDraft={handleClearDraft}
|
||||
autoFocus
|
||||
commandDraftConfig={composerState?.commandDraftConfig}
|
||||
@@ -1089,10 +1468,42 @@ const styles = StyleSheet.create((theme) => ({
|
||||
justifyContent: "center",
|
||||
flexShrink: 0,
|
||||
},
|
||||
badgeProjectIcon: {
|
||||
width: theme.iconSize.md,
|
||||
height: theme.iconSize.md,
|
||||
borderRadius: theme.borderRadius.sm,
|
||||
},
|
||||
badgeProjectIconFallback: {
|
||||
width: theme.iconSize.md,
|
||||
height: theme.iconSize.md,
|
||||
borderRadius: theme.borderRadius.sm,
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
},
|
||||
badgeProjectIconFallbackText: {
|
||||
fontSize: 10,
|
||||
fontWeight: "600",
|
||||
},
|
||||
rowIconBox: {
|
||||
width: theme.iconSize.md,
|
||||
height: theme.iconSize.md,
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
},
|
||||
projectOptionIcon: {
|
||||
width: theme.iconSize.md,
|
||||
height: theme.iconSize.md,
|
||||
borderRadius: theme.borderRadius.sm,
|
||||
},
|
||||
projectOptionIconFallback: {
|
||||
width: theme.iconSize.md,
|
||||
height: theme.iconSize.md,
|
||||
borderRadius: theme.borderRadius.sm,
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
},
|
||||
projectOptionIconFallbackText: {
|
||||
fontSize: 10,
|
||||
fontWeight: "600",
|
||||
},
|
||||
}));
|
||||
|
||||
Reference in New Issue
Block a user