diff --git a/apps/desktop/plans/20260405-1945-v1-workspace-ux-into-v2-modal.md b/apps/desktop/plans/20260405-1945-v1-workspace-ux-into-v2-modal.md new file mode 100644 index 00000000000..4849e417868 --- /dev/null +++ b/apps/desktop/plans/20260405-1945-v1-workspace-ux-into-v2-modal.md @@ -0,0 +1,164 @@ +# V1 Create Workspace Port On V2 Hosts + +This doc replaces the earlier split plan and API draft. + +## Goal + +Match the V1 create-workspace experience on the V2 stack, with one intentional addition: explicit host-target selection. + +1. V1 composer UX and semantics +2. V2 routes, collections, sidebar, and workspace rows +3. host-service as the semantic backend +4. `@superset/workspace-client` as the only host transport +5. the unified `/events` bus as the live-state channel + +## Boundaries + +### Renderer + +Owns: + +1. modal draft state +2. V1 composer UI plus host-target selection +3. picking one `WorkspaceHostTarget` +4. optimistic UI and navigation + +Does not own: + +1. branch/worktree/open/adopt decisions +2. repo scanning +3. PR-specific create behavior +4. setup or agent execution +5. a separate websocket or polling layer + +### `@superset/workspace-client` + +Owns: + +1. one tRPC client per host URL +2. one `/events` connection per host URL +3. auth, reconnect, ref-counting, subscriptions + +Does not own: + +1. create semantics +2. repo/worktree logic + +### Host-service + +Owns: + +1. `workspaceCreation.*` APIs +2. repo clone/ensure +3. branch generation and base-branch handling +4. PR/issue/worktree resolution +5. open vs create vs adopt behavior +6. setup/init execution +7. agent launch handoff + +### Cloud/shared APIs + +Stay thin: + +1. hosts +2. workspace rows +3. project metadata +4. shared PR/issue/task data if proxied by host-service + +## Target UX + +Keep the V1 surface, plus explicit host target selection: + +1. single composer +2. workspace name +3. branch name +4. prompt +5. attachments +6. linked internal issues +7. linked GitHub issues +8. linked PR +9. agent picker +10. setup toggle +11. inline compare-base/worktree picker +12. host target selection +13. auto-open/navigate after create + +Do not keep the current V2 tabbed modal. Keep host target selection available without changing the core V1 composer flow. + +## Target Host API + +```ts +workspaceCreation.getContext({ projectId }) +workspaceCreation.searchBranches({ projectId, query, filter, limit }) +workspaceCreation.searchPullRequests({ projectId, query, limit }) +workspaceCreation.searchInternalIssues({ projectId, query, limit }) +workspaceCreation.searchGitHubIssues({ projectId, query, limit }) +workspaceCreation.prepareAttachmentUpload(...) +workspaceCreation.commitAttachmentUpload(...) +workspaceCreation.create(...) + +workspace.get({ id }) +workspace.gitStatus({ id }) +workspace.delete({ id }) +``` + +Core create shape: + +```ts +workspaceCreation.create({ + projectId, + source, + names: { workspaceName, branchName }, + composer: { prompt, compareBaseBranch, runSetupScript }, + linkedContext: { + internalIssueIds, + githubIssueUrls, + linkedPrUrl, + attachments, + }, + launch: { agentId, autoRun }, + behavior: { onExistingWorkspace, onExistingWorktree }, +}) +``` + +Create returns: + +1. outcome: `created_workspace | opened_existing_workspace | opened_worktree | adopted_external_worktree` +2. workspace row +3. warnings + +The call blocks until the worktree and cloud row are fully created. The renderer awaits it and shows a loading state via the pending-workspace store. No event bus extension or init-state polling needed — worktree creation is fast (<60s) and setup-script progress is visible once the workspace opens. + +## Event Bus + +Use the existing host `/events` bus. No new event types for Phase 1. + +Keep: + +1. `git:changed` +2. `fs:events` + +## Phases + +### Phase 1 + +1. Replace the V2 modal UI with the V1 composer plus explicit host target selection +2. Expand the V2 draft/store to hold full V1 state +3. Add `workspaceCreation.getContext` +4. Add `workspaceCreation.searchBranches` +5. Add semantic `workspaceCreation.create` with full V1 outcome resolution (`created_workspace`, `opened_existing_workspace`, `opened_worktree`, `adopted_external_worktree`) + +### Phase 2 + +1. Move PR and issue linking behind host-service +2. Move attachments to upload refs +3. Remove remaining V2-only modal shell pieces + +## Decisions Locked + +1. V1 composer UX and semantics win over preserving the current V2 modal structure. +2. Host-service is the only semantic backend boundary for modal behavior. +3. `@superset/workspace-client` is the only host transport boundary. +4. Create blocks until done; renderer shows loading via pending-workspace store. No event bus extension needed for Phase 1. +5. Visible host selection is intentionally part of the first-pass UX. +6. Phase 1 `workspaceCreation.create` includes full V1 create/open/adopt semantics. diff --git a/apps/desktop/src/renderer/routes/_authenticated/components/DashboardNewWorkspaceModal/DashboardNewWorkspaceDraftContext.tsx b/apps/desktop/src/renderer/routes/_authenticated/components/DashboardNewWorkspaceModal/DashboardNewWorkspaceDraftContext.tsx index 07d4bc41f1f..4b3c13600f0 100644 --- a/apps/desktop/src/renderer/routes/_authenticated/components/DashboardNewWorkspaceModal/DashboardNewWorkspaceDraftContext.tsx +++ b/apps/desktop/src/renderer/routes/_authenticated/components/DashboardNewWorkspaceModal/DashboardNewWorkspaceDraftContext.tsx @@ -9,50 +9,65 @@ import { } from "react"; import type { WorkspaceHostTarget } from "renderer/routes/_authenticated/components/DashboardNewWorkspaceModal/components/DashboardNewWorkspaceForm/components/DevicePicker"; -export type DashboardNewWorkspaceTab = - | "prompt" - | "issues" - | "pull-requests" - | "branches"; +export type LinkedIssue = { + slug: string; + title: string; + source?: "github" | "internal"; + url?: string; + taskId?: string; + number?: number; + state?: "open" | "closed"; +}; + +export type LinkedPR = { + prNumber: number; + title: string; + url: string; + state: string; +}; export interface DashboardNewWorkspaceDraft { - activeTab: DashboardNewWorkspaceTab; selectedProjectId: string | null; hostTarget: WorkspaceHostTarget; prompt: string; + workspaceName: string; + workspaceNameEdited: boolean; branchName: string; branchNameEdited: boolean; compareBaseBranch: string | null; showAdvanced: boolean; branchSearch: string; - issuesQuery: string; - pullRequestsQuery: string; - branchesQuery: string; + runSetupScript: boolean; + linkedIssues: LinkedIssue[]; + linkedPR: LinkedPR | null; } interface DashboardNewWorkspaceDraftState extends DashboardNewWorkspaceDraft { draftVersion: number; + resetKey: number; } const initialDraft: DashboardNewWorkspaceDraft = { - activeTab: "prompt", selectedProjectId: null, hostTarget: { kind: "local" }, prompt: "", + workspaceName: "", + workspaceNameEdited: false, branchName: "", branchNameEdited: false, compareBaseBranch: null, showAdvanced: false, branchSearch: "", - issuesQuery: "", - pullRequestsQuery: "", - branchesQuery: "", + runSetupScript: true, + linkedIssues: [], + linkedPR: null, }; function buildInitialDraftState(): DashboardNewWorkspaceDraftState { return { ...initialDraft, draftVersion: 0, + resetKey: 0, }; } @@ -69,6 +84,7 @@ interface DashboardNewWorkspaceActionOptions { interface DashboardNewWorkspaceDraftContextValue { draft: DashboardNewWorkspaceDraft; draftVersion: number; + resetKey: number; closeModal: () => void; closeAndResetDraft: () => void; runAsyncAction: ( @@ -117,6 +133,7 @@ export function DashboardNewWorkspaceDraftProvider({ setState((state) => ({ ...initialDraft, draftVersion: state.draftVersion + 1, + resetKey: state.resetKey + 1, })); }, []); @@ -148,20 +165,22 @@ export function DashboardNewWorkspaceDraftProvider({ const value = useMemo( () => ({ draft: { - activeTab: state.activeTab, selectedProjectId: state.selectedProjectId, hostTarget: state.hostTarget, prompt: state.prompt, + workspaceName: state.workspaceName, + workspaceNameEdited: state.workspaceNameEdited, branchName: state.branchName, branchNameEdited: state.branchNameEdited, compareBaseBranch: state.compareBaseBranch, showAdvanced: state.showAdvanced, branchSearch: state.branchSearch, - issuesQuery: state.issuesQuery, - pullRequestsQuery: state.pullRequestsQuery, - branchesQuery: state.branchesQuery, + runSetupScript: state.runSetupScript, + linkedIssues: state.linkedIssues, + linkedPR: state.linkedPR, }, draftVersion: state.draftVersion, + resetKey: state.resetKey, closeModal: onClose, closeAndResetDraft, runAsyncAction, diff --git a/apps/desktop/src/renderer/routes/_authenticated/components/DashboardNewWorkspaceModal/DashboardNewWorkspaceModal.tsx b/apps/desktop/src/renderer/routes/_authenticated/components/DashboardNewWorkspaceModal/DashboardNewWorkspaceModal.tsx index 12bf45f75db..c952aa8fc8f 100644 --- a/apps/desktop/src/renderer/routes/_authenticated/components/DashboardNewWorkspaceModal/DashboardNewWorkspaceModal.tsx +++ b/apps/desktop/src/renderer/routes/_authenticated/components/DashboardNewWorkspaceModal/DashboardNewWorkspaceModal.tsx @@ -1,3 +1,7 @@ +import { + PromptInputProvider, + usePromptInputController, +} from "@superset/ui/ai-elements/prompt-input"; import { Dialog, DialogContent, @@ -5,38 +9,67 @@ import { DialogHeader, DialogTitle, } from "@superset/ui/dialog"; +import { useEffect, useRef } from "react"; +import { electronTrpc } from "renderer/lib/electron-trpc"; import { useCloseNewWorkspaceModal, useNewWorkspaceModalOpen, usePreSelectedProjectId, } from "renderer/stores/new-workspace-modal"; import { DashboardNewWorkspaceForm } from "./components/DashboardNewWorkspaceForm"; -import { DashboardNewWorkspaceDraftProvider } from "./DashboardNewWorkspaceDraftContext"; +import { + DashboardNewWorkspaceDraftProvider, + useDashboardNewWorkspaceDraft, +} from "./DashboardNewWorkspaceDraftContext"; + +/** Clears the PromptInputProvider text & attachments when the draft resets. */ +function PromptInputResetSync() { + const { resetKey } = useDashboardNewWorkspaceDraft(); + const { textInput, attachments } = usePromptInputController(); + const prevResetKeyRef = useRef(resetKey); + + useEffect(() => { + if (resetKey !== prevResetKeyRef.current) { + prevResetKeyRef.current = resetKey; + textInput.clear(); + attachments.clear(); + } + }, [resetKey, textInput.clear, attachments.clear]); + + return null; +} export function DashboardNewWorkspaceModal() { const isOpen = useNewWorkspaceModalOpen(); const closeModal = useCloseNewWorkspaceModal(); const preSelectedProjectId = usePreSelectedProjectId(); + // Prevents AgentSelect from flashing "No agent" while presets load after refresh. + electronTrpc.settings.getAgentPresets.useQuery(); + return ( - !open && closeModal()}> - - New Workspace - - Create a new workspace from a PR, branch, issue, or prompt. - - - - - - + + + !open && closeModal()}> + + New Workspace + + Create a new workspace from a prompt, PR, branch, or issue. + + + e.preventDefault()} + className="bg-popover text-popover-foreground sm:max-w-[560px] max-h-[min(70vh,600px)] !top-[calc(50%-min(35vh,300px))] !-translate-y-0 flex flex-col overflow-hidden p-0" + > + + + + ); } diff --git a/apps/desktop/src/renderer/routes/_authenticated/components/DashboardNewWorkspaceModal/components/DashboardNewWorkspaceForm/DashboardNewWorkspaceForm.tsx b/apps/desktop/src/renderer/routes/_authenticated/components/DashboardNewWorkspaceModal/components/DashboardNewWorkspaceForm/DashboardNewWorkspaceForm.tsx index fe5b9b3ab0c..1696bbd1aeb 100644 --- a/apps/desktop/src/renderer/routes/_authenticated/components/DashboardNewWorkspaceModal/components/DashboardNewWorkspaceForm/DashboardNewWorkspaceForm.tsx +++ b/apps/desktop/src/renderer/routes/_authenticated/components/DashboardNewWorkspaceModal/components/DashboardNewWorkspaceForm/DashboardNewWorkspaceForm.tsx @@ -1,90 +1,612 @@ -import { useCallback } from "react"; -import { useDashboardNewWorkspaceDraft } from "../../DashboardNewWorkspaceDraftContext"; -import { DashboardNewWorkspaceFormHeader } from "./components/DashboardNewWorkspaceFormHeader"; -import { DashboardNewWorkspaceListTabContent } from "./components/DashboardNewWorkspaceListTabContent"; -import { DashboardNewWorkspacePromptTabContent } from "./components/DashboardNewWorkspacePromptTabContent"; +import { + PromptInput, + PromptInputAttachment, + PromptInputAttachments, + PromptInputButton, + PromptInputFooter, + PromptInputSubmit, + PromptInputTextarea, + PromptInputTools, + usePromptInputAttachments, + useProviderAttachments, +} from "@superset/ui/ai-elements/prompt-input"; +import { + Command, + CommandEmpty, + CommandInput, + CommandItem, + CommandList, +} from "@superset/ui/command"; +import { Input } from "@superset/ui/input"; +import { Popover, PopoverContent, PopoverTrigger } from "@superset/ui/popover"; +import { toast } from "@superset/ui/sonner"; +import { Tooltip, TooltipContent, TooltipTrigger } from "@superset/ui/tooltip"; +import { cn } from "@superset/ui/utils"; +import { AnimatePresence, motion } from "framer-motion"; +import { ArrowUpIcon, PaperclipIcon } from "lucide-react"; +import { useCallback, useMemo, useRef, useState } from "react"; +import { GoGitBranch, GoIssueOpened } from "react-icons/go"; +import { HiCheck, HiChevronUpDown } from "react-icons/hi2"; +import { LuGitPullRequest } from "react-icons/lu"; +import { SiLinear } from "react-icons/si"; +import { AgentSelect } from "renderer/components/AgentSelect"; +import { LinkedIssuePill } from "renderer/components/Chat/ChatInterface/components/ChatInputFooter/components/LinkedIssuePill"; +import { IssueLinkCommand } from "renderer/components/Chat/ChatInterface/components/IssueLinkCommand"; +import { GitHubIssueLinkCommand } from "renderer/components/NewWorkspaceModal/components/PromptGroup/components/GitHubIssueLinkCommand"; +import { LinkedGitHubIssuePill } from "renderer/components/NewWorkspaceModal/components/PromptGroup/components/LinkedGitHubIssuePill"; +import { LinkedPRPill } from "renderer/components/NewWorkspaceModal/components/PromptGroup/components/LinkedPRPill"; +import { PRLinkCommand } from "renderer/components/NewWorkspaceModal/components/PromptGroup/components/PRLinkCommand"; +import { useAgentLaunchPreferences } from "renderer/hooks/useAgentLaunchPreferences"; +import { PLATFORM } from "renderer/hotkeys"; +import { electronTrpc } from "renderer/lib/electron-trpc"; +import { + type AgentDefinitionId, + getEnabledAgentConfigs, +} from "shared/utils/agent-settings"; +import { sanitizeBranchNameWithMaxLength } from "shared/utils/branch"; +import { + type LinkedPR, + useDashboardNewWorkspaceDraft, +} from "../../DashboardNewWorkspaceDraftContext"; +import { useCreateDashboardWorkspace } from "../../hooks/useCreateDashboardWorkspace"; +import { DevicePicker } from "./components/DevicePicker"; +import { ProjectSelector } from "./components/ProjectSelector"; +import { useBranchContext } from "./hooks/useBranchContext"; import { useDashboardNewWorkspaceProjectSelection } from "./hooks/useDashboardNewWorkspaceProjectSelection"; -import { useResolvedLocalProject } from "./hooks/useResolvedLocalProject"; + +type WorkspaceCreateAgent = AgentDefinitionId | "none"; +type LinkCommand = "issue" | "github-issue" | "pr"; + +const AGENT_STORAGE_KEY = "lastSelectedWorkspaceCreateAgent"; +const PILL_BUTTON_CLASS = + "!h-[22px] min-h-0 rounded-md border-[0.5px] border-border bg-foreground/[0.04] shadow-none text-[11px]"; + +// ── Small sub-components ───────────────────────────────────────────── + +function AttachmentButtons({ + anchorRef, + onOpen, +}: { + anchorRef: React.RefObject; + onOpen: (cmd: LinkCommand) => void; +}) { + const attachments = usePromptInputAttachments(); + return ( +
+ + + attachments.openFileDialog()} + > + + + + Add attachment + + {( + [ + ["issue", SiLinear, "Link issue"], + ["github-issue", GoIssueOpened, "Link GitHub issue"], + ["pr", LuGitPullRequest, "Link pull request"], + ] as const + ).map(([cmd, Icon, label]) => ( + + + onOpen(cmd)} + > + + + + {label} + + ))} +
+ ); +} + +function CompareBaseBranchPicker({ + effectiveBranch, + defaultBranch, + isLoading, + isError, + branches, + onSelect, +}: { + effectiveBranch: string | null; + defaultBranch?: string | null; + isLoading: boolean; + isError: boolean; + branches: Array<{ name: string }>; + onSelect: (name: string) => void; +}) { + const [open, setOpen] = useState(false); + const [search, setSearch] = useState(""); + + const filtered = useMemo(() => { + if (!search) return branches; + const q = search.toLowerCase(); + return branches.filter((b) => b.name.toLowerCase().includes(q)); + }, [branches, search]); + + if (isError) { + return ( + Failed to load branches + ); + } + + return ( + { + setOpen(v); + if (!v) setSearch(""); + }} + > + + + + e.stopPropagation()} + > + + + + No branches found + {filtered.map((branch) => ( + { + onSelect(branch.name); + setOpen(false); + }} + className="flex items-center justify-between" + > + + + + {branch.name} + + {branch.name === defaultBranch && ( + + default + + )} + + {effectiveBranch === branch.name && ( + + )} + + ))} + + + + + ); +} + +// ── Main Form ──────────────────────────────────────────────────────── interface DashboardNewWorkspaceFormProps { isOpen: boolean; preSelectedProjectId: string | null; } -/** Main form for the new workspace modal with collection-based project selection. */ export function DashboardNewWorkspaceForm({ isOpen, preSelectedProjectId, }: DashboardNewWorkspaceFormProps) { - const { draft, updateDraft } = useDashboardNewWorkspaceDraft(); + const modKey = PLATFORM === "mac" ? "⌘" : "Ctrl"; + const { closeAndResetDraft, closeModal, draft, updateDraft } = + useDashboardNewWorkspaceDraft(); + const attachments = useProviderAttachments(); + const plusMenuRef = useRef(null); + const [activeLinkCommand, setActiveLinkCommand] = + useState(null); + + const { + compareBaseBranch, + prompt, + workspaceName, + workspaceNameEdited, + branchName, + branchNameEdited, + linkedIssues, + linkedPR, + hostTarget, + runSetupScript, + } = draft; + + // ── Project selection ──────────────────────────────────────────── const handleSelectProject = useCallback( - (selectedProjectId: string | null) => { - updateDraft({ selectedProjectId }); + (projectId: string | null) => { + updateDraft({ selectedProjectId: projectId, compareBaseBranch: null }); }, [updateDraft], ); - const { githubRepository, githubRepositoryId } = - useDashboardNewWorkspaceProjectSelection({ - isOpen, - preSelectedProjectId, - selectedProjectId: draft.selectedProjectId, - onSelectProject: handleSelectProject, + useDashboardNewWorkspaceProjectSelection({ + isOpen, + preSelectedProjectId, + selectedProjectId: draft.selectedProjectId, + onSelectProject: handleSelectProject, + }); + + // ── Agent presets ──────────────────────────────────────────────── + const agentPresetsQuery = electronTrpc.settings.getAgentPresets.useQuery(); + const enabledAgentPresets = useMemo( + () => getEnabledAgentConfigs(agentPresetsQuery.data ?? []), + [agentPresetsQuery.data], + ); + const selectableAgentIds = useMemo( + () => enabledAgentPresets.map((p) => p.id), + [enabledAgentPresets], + ); + const { selectedAgent, setSelectedAgent } = + useAgentLaunchPreferences({ + agentStorageKey: AGENT_STORAGE_KEY, + defaultAgent: "claude", + fallbackAgent: "none", + validAgents: ["none", ...selectableAgentIds], + agentsReady: agentPresetsQuery.isFetched, }); - const resolvedLocalProjectId = useResolvedLocalProject(githubRepository); - - const listTab = draft.activeTab === "prompt" ? null : draft.activeTab; - const isListTab = listTab !== null; - const listQuery = - draft.activeTab === "issues" - ? draft.issuesQuery - : draft.activeTab === "branches" - ? draft.branchesQuery - : draft.pullRequestsQuery; - - const handleListQueryChange = (value: string) => { - switch (draft.activeTab) { - case "issues": - updateDraft({ issuesQuery: value }); - return; - case "branches": - updateDraft({ branchesQuery: value }); - return; - case "pull-requests": - updateDraft({ pullRequestsQuery: value }); - return; - default: - return; + + // ── Branch data (via host-service) ─────────────────────────────── + const { + data: branchData, + isLoading: isBranchesLoading, + isError: isBranchesError, + } = useBranchContext(draft.selectedProjectId, hostTarget); + + const effectiveCompareBaseBranch = + compareBaseBranch || branchData?.defaultBranch || null; + + // ── Create workspace ───────────────────────────────────────────── + const { createWorkspace, isPending } = useCreateDashboardWorkspace(); + + const handleCreate = useCallback(() => { + if (!draft.selectedProjectId) { + toast.error("Select a project first"); + return; } + const detachedFiles = attachments.takeFiles(); + closeAndResetDraft(); + + void createWorkspace({ + projectId: draft.selectedProjectId, + hostTarget, + prompt, + workspaceName: workspaceNameEdited ? workspaceName : undefined, + branchName, + branchNameEdited, + compareBaseBranch: compareBaseBranch || undefined, + runSetupScript, + linkedPR, + linkedIssues, + attachmentFiles: detachedFiles, + }); + }, [ + attachments, + branchName, + branchNameEdited, + closeAndResetDraft, + compareBaseBranch, + createWorkspace, + draft.selectedProjectId, + hostTarget, + linkedIssues, + linkedPR, + prompt, + runSetupScript, + workspaceName, + workspaceNameEdited, + ]); + + // ── Issue / PR linking ─────────────────────────────────────────── + + const addLinkedIssue = ( + slug: string, + title: string, + taskId: string | undefined, + url?: string, + ) => { + if (linkedIssues.some((i) => i.slug === slug)) return; + updateDraft({ + linkedIssues: [ + ...linkedIssues, + { slug, title, source: "internal", taskId, url }, + ], + }); }; + const addLinkedGitHubIssue = ( + issueNumber: number, + title: string, + url: string, + state: string, + ) => { + if (linkedIssues.some((i) => i.url === url)) return; + updateDraft({ + linkedIssues: [ + ...linkedIssues, + { + slug: `#${issueNumber}`, + title, + source: "github" as const, + url, + number: issueNumber, + state: state.toLowerCase() === "closed" ? "closed" : "open", + }, + ], + }); + }; + + const removeLinkedIssue = (slug: string) => + updateDraft({ + linkedIssues: linkedIssues.filter((i) => i.slug !== slug), + }); + + const setLinkedPR = (pr: LinkedPR) => updateDraft({ linkedPR: pr }); + const removeLinkedPR = () => updateDraft({ linkedPR: null }); + + const openLinkCommand = (cmd: LinkCommand) => + requestAnimationFrame(() => setActiveLinkCommand(cmd)); + + // ── Render ──────────────────────────────────────────────────────── + return ( - <> - updateDraft({ activeTab })} - onSelectHostTarget={(hostTarget) => updateDraft({ hostTarget })} - onSelectProject={handleSelectProject} - /> - - {isListTab ? ( - + {/* Workspace name + branch name */} +
+ + updateDraft({ + workspaceName: e.target.value, + workspaceNameEdited: true, + }) + } + onBlur={() => { + if (!workspaceName.trim()) + updateDraft({ workspaceName: "", workspaceNameEdited: false }); + }} /> - ) : ( - + + updateDraft({ + branchName: e.target.value.replace(/\s+/g, "-"), + branchNameEdited: true, + }) + } + onBlur={() => { + const sanitized = sanitizeBranchNameWithMaxLength( + branchName.trim(), + undefined, + { preserveCase: true }, + ); + if (!sanitized) + updateDraft({ branchName: "", branchNameEdited: false }); + else updateDraft({ branchName: sanitized }); + }} + /> +
+ + + {/* Rich prompt input */} + + {(linkedPR || + linkedIssues.length > 0 || + attachments.files.length > 0) && ( +
+ + {linkedPR && ( + + + + )} + {linkedIssues.map((issue) => ( + + {issue.source === "github" ? ( + removeLinkedIssue(issue.slug)} + /> + ) : ( + removeLinkedIssue(issue.slug)} + /> + )} + + ))} + + + {(file) => } + +
+ )} + updateDraft({ prompt: e.target.value })} /> - )} - + + + + agents={enabledAgentPresets} + value={selectedAgent} + placeholder="No agent" + onValueChange={setSelectedAgent} + onBeforeConfigureAgents={closeModal} + triggerClassName={`${PILL_BUTTON_CLASS} px-1.5 gap-1 text-foreground w-auto max-w-[160px]`} + iconClassName="size-3 object-contain" + allowNone + noneLabel="No agent" + noneValue="none" + /> + +
+ + !open && setActiveLinkCommand(null)} + onSelect={addLinkedIssue} + /> + !open && setActiveLinkCommand(null)} + onSelect={(issue) => + addLinkedGitHubIssue( + issue.issueNumber, + issue.title, + issue.url, + issue.state, + ) + } + projectId={null} + anchorRef={plusMenuRef} + /> + !open && setActiveLinkCommand(null)} + onSelect={setLinkedPR} + projectId={null} + githubOwner={null} + repoName={null} + anchorRef={plusMenuRef} + /> + { + e.preventDefault(); + handleCreate(); + }} + > + + +
+
+
+ + {/* Bottom bar */} +
+
+ + + {linkedPR ? ( + + + based off PR #{linkedPR.prNumber} + + ) : branchData ? ( + + + updateDraft({ compareBaseBranch: branch }) + } + /> + + ) : null} + +
+
+ updateDraft({ hostTarget: t })} + /> + + {modKey}↵ + +
+
+ ); } diff --git a/apps/desktop/src/renderer/routes/_authenticated/components/DashboardNewWorkspaceModal/components/DashboardNewWorkspaceForm/components/BranchesGroup/BranchesGroup.tsx b/apps/desktop/src/renderer/routes/_authenticated/components/DashboardNewWorkspaceModal/components/DashboardNewWorkspaceForm/components/BranchesGroup/BranchesGroup.tsx deleted file mode 100644 index 455999262f6..00000000000 --- a/apps/desktop/src/renderer/routes/_authenticated/components/DashboardNewWorkspaceModal/components/DashboardNewWorkspaceForm/components/BranchesGroup/BranchesGroup.tsx +++ /dev/null @@ -1,206 +0,0 @@ -import { Button } from "@superset/ui/button"; -import { CommandEmpty, CommandGroup, CommandItem } from "@superset/ui/command"; -import { eq } from "@tanstack/db"; -import { useLiveQuery } from "@tanstack/react-db"; -import { useNavigate } from "@tanstack/react-router"; -import Fuse from "fuse.js"; -import { useCallback, useMemo } from "react"; -import { GoArrowUpRight, GoGitBranch, GoGlobe } from "react-icons/go"; -import { useDebouncedValue } from "renderer/hooks/useDebouncedValue"; -import { electronTrpc } from "renderer/lib/electron-trpc"; -import { navigateToV2Workspace } from "renderer/routes/_authenticated/_dashboard/utils/workspace-navigation"; -import type { WorkspaceHostTarget } from "renderer/routes/_authenticated/components/DashboardNewWorkspaceModal/components/DashboardNewWorkspaceForm/components/DevicePicker"; -import { useCollections } from "renderer/routes/_authenticated/providers/CollectionsProvider"; -import { useDashboardNewWorkspaceDraft } from "../../../../DashboardNewWorkspaceDraftContext"; -import { useCreateDashboardWorkspace } from "../../../../hooks/useCreateDashboardWorkspace"; - -interface BranchesGroupProps { - projectId: string | null; - localProjectId: string | null; - hostTarget: WorkspaceHostTarget; -} - -export function BranchesGroup({ - projectId, - localProjectId, - hostTarget, -}: BranchesGroupProps) { - const navigate = useNavigate(); - const collections = useCollections(); - const { createWorkspace } = useCreateDashboardWorkspace(); - const { draft, closeAndResetDraft, runAsyncAction } = - useDashboardNewWorkspaceDraft(); - - const hasLocalProject = !!localProjectId; - - const { data: localData, isLoading: isLocalLoading } = - electronTrpc.projects.getBranchesLocal.useQuery( - { projectId: localProjectId ?? "" }, - { enabled: hasLocalProject }, - ); - - const { data: remoteData } = electronTrpc.projects.getBranches.useQuery( - { projectId: localProjectId ?? "" }, - { enabled: hasLocalProject }, - ); - - const data = remoteData ?? localData; - - // Check v2Workspaces for existing workspaces by branch - const { data: v2WorkspacesData } = useLiveQuery( - (q) => - q - .from({ ws: collections.v2Workspaces }) - .where(({ ws }) => eq(ws.projectId, projectId ?? "")) - .select(({ ws }) => ({ id: ws.id, branch: ws.branch })), - [collections, projectId], - ); - - const workspaceByBranch = useMemo(() => { - const map = new Map(); - for (const w of v2WorkspacesData ?? []) { - map.set(w.branch, w.id); - } - return map; - }, [v2WorkspacesData]); - - const defaultBranch = data?.defaultBranch ?? "main"; - - const branches = (data?.branches ?? []).sort((a, b) => { - if (a.name === defaultBranch) return -1; - if (b.name === defaultBranch) return 1; - if (a.isLocal !== b.isLocal) return a.isLocal ? -1 : 1; - return a.name.localeCompare(b.name); - }); - - const branchRows = useMemo(() => { - return branches.map((branch) => ({ - branch, - existingWorkspaceId: workspaceByBranch.get(branch.name), - })); - }, [branches, workspaceByBranch]); - - const debouncedQuery = useDebouncedValue(draft.branchesQuery, 150); - - const branchFuse = useMemo( - () => - new Fuse(branchRows, { - keys: ["branch.name"], - threshold: 0.3, - includeScore: true, - ignoreLocation: true, - }), - [branchRows], - ); - - const visibleBranchRows = useMemo(() => { - const query = debouncedQuery.trim(); - if (!query) { - return branchRows.slice(0, 100); - } - return branchFuse - .search(query) - .slice(0, 100) - .map((result) => result.item); - }, [debouncedQuery, branchRows, branchFuse]); - - const handleCreate = useCallback( - (branchName: string) => { - if (!projectId) return; - void runAsyncAction( - createWorkspace({ - projectId, - name: branchName, - branch: branchName, - hostTarget, - }), - { - loading: "Creating workspace from branch...", - success: "Workspace created", - error: (err) => - err instanceof Error ? err.message : "Failed to create workspace", - }, - ); - }, - [createWorkspace, hostTarget, projectId, runAsyncAction], - ); - - const handleOpen = useCallback( - (workspaceId: string) => { - closeAndResetDraft(); - navigateToV2Workspace(workspaceId, navigate); - }, - [closeAndResetDraft, navigate], - ); - - const handleBranchAction = useCallback( - (branchName: string) => { - const existingId = workspaceByBranch.get(branchName); - if (existingId) { - handleOpen(existingId); - return; - } - handleCreate(branchName); - }, - [handleCreate, handleOpen, workspaceByBranch], - ); - - if (!projectId) { - return ( - - Select a project to view branches. - - ); - } - - if (!hasLocalProject) { - return ( - - No local repository linked to this project. - - ); - } - - if (isLocalLoading) { - return ( - - Loading branches... - - ); - } - - return ( - - No branches found. - {visibleBranchRows.map(({ branch, existingWorkspaceId }) => { - const buttonLabel = existingWorkspaceId ? "Open" : "Create"; - return ( - handleBranchAction(branch.name)} - className="group h-12" - > - {existingWorkspaceId ? ( - - ) : branch.isLocal ? ( - - ) : ( - - )} - {branch.name} - - - ); - })} - - ); -} diff --git a/apps/desktop/src/renderer/routes/_authenticated/components/DashboardNewWorkspaceModal/components/DashboardNewWorkspaceForm/components/BranchesGroup/index.ts b/apps/desktop/src/renderer/routes/_authenticated/components/DashboardNewWorkspaceModal/components/DashboardNewWorkspaceForm/components/BranchesGroup/index.ts deleted file mode 100644 index 75953e3d249..00000000000 --- a/apps/desktop/src/renderer/routes/_authenticated/components/DashboardNewWorkspaceModal/components/DashboardNewWorkspaceForm/components/BranchesGroup/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { BranchesGroup } from "./BranchesGroup"; diff --git a/apps/desktop/src/renderer/routes/_authenticated/components/DashboardNewWorkspaceModal/components/DashboardNewWorkspaceForm/components/DashboardNewWorkspaceFormHeader/DashboardNewWorkspaceFormHeader.tsx b/apps/desktop/src/renderer/routes/_authenticated/components/DashboardNewWorkspaceModal/components/DashboardNewWorkspaceForm/components/DashboardNewWorkspaceFormHeader/DashboardNewWorkspaceFormHeader.tsx deleted file mode 100644 index cd712d1bbd6..00000000000 --- a/apps/desktop/src/renderer/routes/_authenticated/components/DashboardNewWorkspaceModal/components/DashboardNewWorkspaceForm/components/DashboardNewWorkspaceFormHeader/DashboardNewWorkspaceFormHeader.tsx +++ /dev/null @@ -1,52 +0,0 @@ -import { Tabs, TabsList, TabsTrigger } from "@superset/ui/tabs"; -import type { WorkspaceHostTarget } from "renderer/routes/_authenticated/components/DashboardNewWorkspaceModal/components/DashboardNewWorkspaceForm/components/DevicePicker"; -import type { DashboardNewWorkspaceTab } from "../../../../DashboardNewWorkspaceDraftContext"; -import { DevicePicker } from "../DevicePicker"; -import { ProjectSelector } from "../ProjectSelector"; - -interface DashboardNewWorkspaceFormHeaderProps { - activeTab: DashboardNewWorkspaceTab; - hostTarget: WorkspaceHostTarget; - selectedProjectId: string | null; - onSelectTab: (tab: DashboardNewWorkspaceTab) => void; - onSelectHostTarget: (hostTarget: WorkspaceHostTarget) => void; - onSelectProject: (projectId: string | null) => void; -} - -export function DashboardNewWorkspaceFormHeader({ - activeTab, - hostTarget, - selectedProjectId, - onSelectTab, - onSelectHostTarget, - onSelectProject, -}: DashboardNewWorkspaceFormHeaderProps) { - return ( -
- - onSelectTab(value as DashboardNewWorkspaceTab) - } - > - - Prompt - Issues - Pull requests - Branches - - -
- -
- -
-
- ); -} diff --git a/apps/desktop/src/renderer/routes/_authenticated/components/DashboardNewWorkspaceModal/components/DashboardNewWorkspaceForm/components/DashboardNewWorkspaceFormHeader/index.ts b/apps/desktop/src/renderer/routes/_authenticated/components/DashboardNewWorkspaceModal/components/DashboardNewWorkspaceForm/components/DashboardNewWorkspaceFormHeader/index.ts deleted file mode 100644 index f4469410524..00000000000 --- a/apps/desktop/src/renderer/routes/_authenticated/components/DashboardNewWorkspaceModal/components/DashboardNewWorkspaceForm/components/DashboardNewWorkspaceFormHeader/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { DashboardNewWorkspaceFormHeader } from "./DashboardNewWorkspaceFormHeader"; diff --git a/apps/desktop/src/renderer/routes/_authenticated/components/DashboardNewWorkspaceModal/components/DashboardNewWorkspaceForm/components/DashboardNewWorkspaceListTabContent/DashboardNewWorkspaceListTabContent.tsx b/apps/desktop/src/renderer/routes/_authenticated/components/DashboardNewWorkspaceModal/components/DashboardNewWorkspaceForm/components/DashboardNewWorkspaceListTabContent/DashboardNewWorkspaceListTabContent.tsx deleted file mode 100644 index aa01a2dd257..00000000000 --- a/apps/desktop/src/renderer/routes/_authenticated/components/DashboardNewWorkspaceModal/components/DashboardNewWorkspaceForm/components/DashboardNewWorkspaceListTabContent/DashboardNewWorkspaceListTabContent.tsx +++ /dev/null @@ -1,65 +0,0 @@ -import { Command, CommandInput, CommandList } from "@superset/ui/command"; -import type { WorkspaceHostTarget } from "renderer/routes/_authenticated/components/DashboardNewWorkspaceModal/components/DashboardNewWorkspaceForm/components/DevicePicker"; -import type { DashboardNewWorkspaceTab } from "../../../../DashboardNewWorkspaceDraftContext"; -import { BranchesGroup } from "../BranchesGroup"; -import { IssuesGroup } from "../IssuesGroup"; -import { PullRequestsGroup } from "../PullRequestsGroup"; - -const COMMAND_CLASS_NAME = - "[&_[cmdk-group-heading]]:text-muted-foreground **:data-[slot=command-input-wrapper]:h-12 [&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group]]:px-2 [&_[cmdk-group]:not([hidden])_~[cmdk-group]]:pt-0 [&_[cmdk-input-wrapper]_svg]:h-5 [&_[cmdk-input-wrapper]_svg]:w-5 [&_[cmdk-input]]:h-12 [&_[cmdk-item]]:px-2 [&_[cmdk-item]]:py-3 [&_[cmdk-item]_svg]:h-5 [&_[cmdk-item]_svg]:w-5 flex h-full w-full flex-1 flex-col overflow-hidden rounded-none"; - -interface DashboardNewWorkspaceListTabContentProps { - activeTab: Exclude; - projectId: string | null; - githubRepositoryId: string | null; - hostTarget: WorkspaceHostTarget; - localProjectId: string | null; - query: string; - onQueryChange: (value: string) => void; -} - -export function DashboardNewWorkspaceListTabContent({ - activeTab, - projectId, - githubRepositoryId, - hostTarget, - localProjectId, - query, - onQueryChange, -}: DashboardNewWorkspaceListTabContentProps) { - return ( - - - - - {activeTab === "pull-requests" && ( - - )} - {activeTab === "branches" && ( - - )} - {activeTab === "issues" && ( - - )} - - - ); -} diff --git a/apps/desktop/src/renderer/routes/_authenticated/components/DashboardNewWorkspaceModal/components/DashboardNewWorkspaceForm/components/DashboardNewWorkspaceListTabContent/index.ts b/apps/desktop/src/renderer/routes/_authenticated/components/DashboardNewWorkspaceModal/components/DashboardNewWorkspaceForm/components/DashboardNewWorkspaceListTabContent/index.ts deleted file mode 100644 index af9feb38a8c..00000000000 --- a/apps/desktop/src/renderer/routes/_authenticated/components/DashboardNewWorkspaceModal/components/DashboardNewWorkspaceForm/components/DashboardNewWorkspaceListTabContent/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { DashboardNewWorkspaceListTabContent } from "./DashboardNewWorkspaceListTabContent"; diff --git a/apps/desktop/src/renderer/routes/_authenticated/components/DashboardNewWorkspaceModal/components/DashboardNewWorkspaceForm/components/DashboardNewWorkspacePromptTabContent/DashboardNewWorkspacePromptTabContent.tsx b/apps/desktop/src/renderer/routes/_authenticated/components/DashboardNewWorkspaceModal/components/DashboardNewWorkspaceForm/components/DashboardNewWorkspacePromptTabContent/DashboardNewWorkspacePromptTabContent.tsx deleted file mode 100644 index 801c7311f06..00000000000 --- a/apps/desktop/src/renderer/routes/_authenticated/components/DashboardNewWorkspaceModal/components/DashboardNewWorkspaceForm/components/DashboardNewWorkspacePromptTabContent/DashboardNewWorkspacePromptTabContent.tsx +++ /dev/null @@ -1,24 +0,0 @@ -import type { WorkspaceHostTarget } from "renderer/routes/_authenticated/components/DashboardNewWorkspaceModal/components/DashboardNewWorkspaceForm/components/DevicePicker"; -import { PromptGroup } from "../PromptGroup"; - -interface DashboardNewWorkspacePromptTabContentProps { - projectId: string | null; - localProjectId: string | null; - hostTarget: WorkspaceHostTarget; -} - -export function DashboardNewWorkspacePromptTabContent({ - projectId, - localProjectId, - hostTarget, -}: DashboardNewWorkspacePromptTabContentProps) { - return ( -
- -
- ); -} diff --git a/apps/desktop/src/renderer/routes/_authenticated/components/DashboardNewWorkspaceModal/components/DashboardNewWorkspaceForm/components/DashboardNewWorkspacePromptTabContent/index.ts b/apps/desktop/src/renderer/routes/_authenticated/components/DashboardNewWorkspaceModal/components/DashboardNewWorkspaceForm/components/DashboardNewWorkspacePromptTabContent/index.ts deleted file mode 100644 index 0dd4c4cbf1a..00000000000 --- a/apps/desktop/src/renderer/routes/_authenticated/components/DashboardNewWorkspaceModal/components/DashboardNewWorkspaceForm/components/DashboardNewWorkspacePromptTabContent/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { DashboardNewWorkspacePromptTabContent } from "./DashboardNewWorkspacePromptTabContent"; diff --git a/apps/desktop/src/renderer/routes/_authenticated/components/DashboardNewWorkspaceModal/components/DashboardNewWorkspaceForm/components/IssuesGroup/IssuesGroup.tsx b/apps/desktop/src/renderer/routes/_authenticated/components/DashboardNewWorkspaceModal/components/DashboardNewWorkspaceForm/components/IssuesGroup/IssuesGroup.tsx deleted file mode 100644 index d31ef1e646c..00000000000 --- a/apps/desktop/src/renderer/routes/_authenticated/components/DashboardNewWorkspaceModal/components/DashboardNewWorkspaceForm/components/IssuesGroup/IssuesGroup.tsx +++ /dev/null @@ -1,211 +0,0 @@ -import { Avatar } from "@superset/ui/atoms/Avatar"; -import { Button } from "@superset/ui/button"; -import { CommandEmpty, CommandGroup, CommandItem } from "@superset/ui/command"; -import { toast } from "@superset/ui/sonner"; -import { eq, isNull } from "@tanstack/db"; -import { useLiveQuery } from "@tanstack/react-db"; -import { useNavigate } from "@tanstack/react-router"; -import { useMemo } from "react"; -import { GoArrowUpRight } from "react-icons/go"; -import { HiOutlineUserCircle } from "react-icons/hi2"; -import { SiLinear } from "react-icons/si"; -import { GATED_FEATURES, usePaywall } from "renderer/components/Paywall"; -import { useDebouncedValue } from "renderer/hooks/useDebouncedValue"; -import { getSlugColumnWidth } from "renderer/lib/slug-width"; -import { - StatusIcon, - type StatusType, -} from "renderer/routes/_authenticated/_dashboard/tasks/components/TasksView/components/shared/StatusIcon"; -import { useHybridSearch } from "renderer/routes/_authenticated/_dashboard/tasks/components/TasksView/hooks/useHybridSearch"; -import { compareTasks } from "renderer/routes/_authenticated/_dashboard/tasks/components/TasksView/utils/sorting"; -import { navigateToV2Workspace } from "renderer/routes/_authenticated/_dashboard/utils/workspace-navigation"; -import type { WorkspaceHostTarget } from "renderer/routes/_authenticated/components/DashboardNewWorkspaceModal/components/DashboardNewWorkspaceForm/components/DevicePicker"; -import { useCollections } from "renderer/routes/_authenticated/providers/CollectionsProvider"; -import { useDashboardNewWorkspaceDraft } from "../../../../DashboardNewWorkspaceDraftContext"; -import { useCreateDashboardWorkspace } from "../../../../hooks/useCreateDashboardWorkspace"; - -interface IssuesGroupProps { - projectId: string | null; - hostTarget: WorkspaceHostTarget; -} - -export function IssuesGroup({ projectId, hostTarget }: IssuesGroupProps) { - const collections = useCollections(); - const navigate = useNavigate(); - const { gateFeature } = usePaywall(); - const { createWorkspace } = useCreateDashboardWorkspace(); - const { draft, closeAndResetDraft, runAsyncAction } = - useDashboardNewWorkspaceDraft(); - - const { data: integrations } = useLiveQuery( - (q) => - q - .from({ - integrationConnections: collections.integrationConnections, - }) - .select(({ integrationConnections }) => ({ - ...integrationConnections, - })), - [collections], - ); - - const isLinearConnected = - integrations?.some((i) => i.provider === "linear") ?? false; - - const { data } = useLiveQuery( - (q) => - q - .from({ tasks: collections.tasks }) - .innerJoin({ status: collections.taskStatuses }, ({ tasks, status }) => - eq(tasks.statusId, status.id), - ) - .leftJoin({ assignee: collections.users }, ({ tasks, assignee }) => - eq(tasks.assigneeId, assignee.id), - ) - .select(({ tasks, status, assignee }) => ({ - ...tasks, - status, - assignee: assignee ?? null, - })) - .where(({ tasks }) => isNull(tasks.deletedAt)), - [collections], - ); - - // Check v2Workspaces for existing workspaces by branch - const { data: v2WorkspacesData } = useLiveQuery( - (q) => - q - .from({ ws: collections.v2Workspaces }) - .where(({ ws }) => eq(ws.projectId, projectId ?? "")) - .select(({ ws }) => ({ id: ws.id, branch: ws.branch })), - [collections, projectId], - ); - - const workspaceByBranch = useMemo(() => { - const map = new Map(); - for (const w of v2WorkspacesData ?? []) { - map.set(w.branch, w.id); - } - return map; - }, [v2WorkspacesData]); - - const tasks = useMemo(() => data ?? [], [data]); - const sortedTasks = useMemo(() => [...tasks].sort(compareTasks), [tasks]); - - const debouncedQuery = useDebouncedValue(draft.issuesQuery, 150); - const { search } = useHybridSearch(sortedTasks); - - const visibleTasks = useMemo(() => { - const query = debouncedQuery.trim(); - if (!query) { - return sortedTasks.slice(0, 100); - } - return search(query) - .slice(0, 100) - .map((result) => result.item); - }, [debouncedQuery, sortedTasks, search]); - - const slugWidth = useMemo( - () => getSlugColumnWidth(visibleTasks.map((t) => t.slug)), - [visibleTasks], - ); - - if (!isLinearConnected) { - return ( -
- -
-

Connect Linear

-

- Sync issues from Linear to create workspaces -

-
- -
- ); - } - - return ( - - No issues found. - {visibleTasks.map((task) => ( - { - if (!projectId) { - toast.error("Select a project first"); - return; - } - const existingId = workspaceByBranch.get(task.slug.toLowerCase()); - if (existingId) { - closeAndResetDraft(); - navigateToV2Workspace(existingId, navigate); - return; - } - void runAsyncAction( - createWorkspace({ - projectId, - name: task.title, - branch: task.slug.toLowerCase(), - hostTarget, - }), - { - loading: "Creating workspace...", - success: "Workspace created", - error: (err) => - err instanceof Error - ? err.message - : "Failed to create workspace", - }, - ); - }} - className="group h-12" - > - {workspaceByBranch.has(task.slug.toLowerCase()) ? ( - - ) : ( - - )} - - {task.slug} - - {task.title} - - {task.assignee ? ( - - ) : ( - - )} - - - {workspaceByBranch.has(task.slug.toLowerCase()) ? "Open" : "Create"}{" "} - ↵ - - - ))} - - ); -} diff --git a/apps/desktop/src/renderer/routes/_authenticated/components/DashboardNewWorkspaceModal/components/DashboardNewWorkspaceForm/components/IssuesGroup/index.ts b/apps/desktop/src/renderer/routes/_authenticated/components/DashboardNewWorkspaceModal/components/DashboardNewWorkspaceForm/components/IssuesGroup/index.ts deleted file mode 100644 index c0762c8495d..00000000000 --- a/apps/desktop/src/renderer/routes/_authenticated/components/DashboardNewWorkspaceModal/components/DashboardNewWorkspaceForm/components/IssuesGroup/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { IssuesGroup } from "./IssuesGroup"; diff --git a/apps/desktop/src/renderer/routes/_authenticated/components/DashboardNewWorkspaceModal/components/DashboardNewWorkspaceForm/components/PromptGroup/PromptGroup.tsx b/apps/desktop/src/renderer/routes/_authenticated/components/DashboardNewWorkspaceModal/components/DashboardNewWorkspaceForm/components/PromptGroup/PromptGroup.tsx deleted file mode 100644 index 40d1d127402..00000000000 --- a/apps/desktop/src/renderer/routes/_authenticated/components/DashboardNewWorkspaceModal/components/DashboardNewWorkspaceForm/components/PromptGroup/PromptGroup.tsx +++ /dev/null @@ -1,239 +0,0 @@ -import { Button } from "@superset/ui/button"; -import { Kbd, KbdGroup } from "@superset/ui/kbd"; -import { toast } from "@superset/ui/sonner"; -import { Textarea } from "@superset/ui/textarea"; -import { useNavigate } from "@tanstack/react-router"; -import { useEffect, useMemo, useRef, useState } from "react"; -import { PLATFORM } from "renderer/hotkeys"; -import { electronTrpc } from "renderer/lib/electron-trpc"; -import { resolveEffectiveWorkspaceBaseBranch } from "renderer/lib/workspaceBaseBranch"; -import type { WorkspaceHostTarget } from "renderer/routes/_authenticated/components/DashboardNewWorkspaceModal/components/DashboardNewWorkspaceForm/components/DevicePicker"; -import { - resolveBranchPrefix, - sanitizeBranchNameWithMaxLength, -} from "shared/utils/branch"; -import { useDashboardNewWorkspaceDraft } from "../../../../DashboardNewWorkspaceDraftContext"; -import { useCreateDashboardWorkspace } from "../../../../hooks/useCreateDashboardWorkspace"; -import { PromptGroupAdvancedOptions } from "./components/PromptGroupAdvancedOptions"; - -interface PromptGroupProps { - projectId: string | null; - localProjectId: string | null; - hostTarget: WorkspaceHostTarget; -} - -export function PromptGroup({ - projectId, - localProjectId, - hostTarget, -}: PromptGroupProps) { - const navigate = useNavigate(); - const modKey = PLATFORM === "mac" ? "⌘" : "Ctrl"; - const textareaRef = useRef(null); - const { closeModal, draft, runAsyncAction, updateDraft } = - useDashboardNewWorkspaceDraft(); - const [compareBaseBranchOpen, setCompareBaseBranchOpen] = useState(false); - const { - compareBaseBranch, - branchName, - branchNameEdited, - branchSearch, - prompt, - showAdvanced, - } = draft; - const { createWorkspace, isPending } = useCreateDashboardWorkspace(); - - const trimmedPrompt = prompt.trim(); - - const hasLocalProject = !!localProjectId; - - const { data: project } = electronTrpc.projects.get.useQuery( - { id: localProjectId ?? "" }, - { enabled: hasLocalProject }, - ); - const { - data: localBranchData, - isLoading: isBranchesLoading, - isError: isBranchesError, - } = electronTrpc.projects.getBranchesLocal.useQuery( - { projectId: localProjectId ?? "" }, - { enabled: hasLocalProject }, - ); - const { data: remoteBranchData } = electronTrpc.projects.getBranches.useQuery( - { projectId: localProjectId ?? "" }, - { enabled: hasLocalProject }, - ); - const branchData = remoteBranchData ?? localBranchData; - const { data: gitAuthor } = electronTrpc.projects.getGitAuthor.useQuery( - { id: localProjectId ?? "" }, - { enabled: hasLocalProject }, - ); - const { data: globalBranchPrefix } = - electronTrpc.settings.getBranchPrefix.useQuery(); - const { data: gitInfo } = electronTrpc.settings.getGitInfo.useQuery(); - - const resolvedPrefix = useMemo(() => { - const projectOverrides = project?.branchPrefixMode != null; - return resolveBranchPrefix({ - mode: projectOverrides - ? project?.branchPrefixMode - : (globalBranchPrefix?.mode ?? "none"), - customPrefix: projectOverrides - ? project?.branchPrefixCustom - : globalBranchPrefix?.customPrefix, - authorPrefix: gitAuthor?.prefix, - githubUsername: gitInfo?.githubUsername, - }); - }, [project, globalBranchPrefix, gitAuthor, gitInfo]); - - const filteredBranches = useMemo(() => { - if (!branchData?.branches) return []; - if (!branchSearch) return branchData.branches; - const searchLower = branchSearch.toLowerCase(); - return branchData.branches.filter((branch) => - branch.name.toLowerCase().includes(searchLower), - ); - }, [branchData?.branches, branchSearch]); - - const effectiveCompareBaseBranch = resolveEffectiveWorkspaceBaseBranch({ - explicitBaseBranch: compareBaseBranch, - workspaceBaseBranch: project?.workspaceBaseBranch, - defaultBranch: branchData?.defaultBranch, - branches: branchData?.branches, - }); - - const branchSlug = branchNameEdited - ? sanitizeBranchNameWithMaxLength(branchName, undefined, { - preserveFirstSegmentCase: true, - }) - : sanitizeBranchNameWithMaxLength(trimmedPrompt); - - const applyPrefix = !branchNameEdited; - - const branchPreview = - branchSlug && applyPrefix && resolvedPrefix - ? sanitizeBranchNameWithMaxLength(`${resolvedPrefix}/${branchSlug}`) - : branchSlug; - - const previousProjectIdRef = useRef(localProjectId); - - useEffect(() => { - if (previousProjectIdRef.current === localProjectId) { - return; - } - previousProjectIdRef.current = localProjectId; - updateDraft({ - compareBaseBranch: null, - branchSearch: "", - }); - setCompareBaseBranchOpen(false); - }, [localProjectId, updateDraft]); - - const handleCreate = () => { - if (!projectId) { - toast.error("Select a project first"); - return; - } - const name = branchSlug || trimmedPrompt || "workspace"; - const branch = branchPreview || "workspace"; - void runAsyncAction( - createWorkspace({ - projectId, - name, - branch, - hostTarget, - }), - { - loading: "Creating workspace...", - success: "Workspace created", - error: (err) => - err instanceof Error ? err.message : "Failed to create workspace", - }, - ); - }; - - const handleBranchNameChange = (value: string) => { - updateDraft({ - branchName: value, - branchNameEdited: true, - }); - }; - - const handleBranchNameBlur = () => { - if (!branchName.trim()) { - updateDraft({ - branchName: "", - branchNameEdited: false, - }); - } - }; - - const handleCompareBaseBranchSelect = (selectedBaseBranch: string) => { - updateDraft({ - compareBaseBranch: selectedBaseBranch, - branchSearch: "", - }); - setCompareBaseBranchOpen(false); - }; - - return ( -
-