|
|
|
@@ -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",
|
|
|
|
|
},
|
|
|
|
|
}));
|
|
|
|
|