Compare commits

..

1 Commits

Author SHA1 Message Date
Jiang Bohan
859a4ea222 fix(views): drop disableHoverCard from QuickCreate modal ActorAvatars
The ActorAvatar prop was renamed in #1794 (split presence into
availability + last-task) — `disableHoverCard` is now `enableHoverCard`
with inverted semantics. The QuickCreate modal landed against the old
API and broke main's frontend typecheck. The two avatars in the modal
already want the default (no hover card), so just drop the prop
instead of opting in.
2026-04-29 14:12:53 +08:00
7 changed files with 142 additions and 370 deletions

View File

@@ -1,36 +0,0 @@
"use client";
import { create } from "zustand";
import { createJSONStorage, persist } from "zustand/middleware";
import { defaultStorage } from "../../platform/storage";
/**
* Last create-issue mode the user landed on. Drives the global `c` shortcut
* and the in-modal mode switch — pressing `c` opens whichever modal the user
* used last, and the switch button in either modal updates this so the
* preference sticks.
*
* Workspace-agnostic on purpose: the user's mental preference for "how do I
* file an issue" doesn't change per workspace, so this lives in plain
* localStorage rather than the workspace-aware StateStorage that scopes
* per-workspace stores like quick-create-store / draft-store.
*/
export type CreateMode = "agent" | "manual";
interface CreateModeState {
lastMode: CreateMode;
setLastMode: (mode: CreateMode) => void;
}
export const useCreateModeStore = create<CreateModeState>()(
persist(
(set) => ({
lastMode: "agent",
setLastMode: (mode) => set({ lastMode: mode }),
}),
{
name: "multica_create_mode",
storage: createJSONStorage(() => defaultStorage),
},
),
);

View File

@@ -1,5 +1,4 @@
export { useIssueSelectionStore } from "./selection-store";
export { useCreateModeStore, type CreateMode } from "./create-mode-store";
export { useIssueDraftStore } from "./draft-store";
export { useRecentIssuesStore, type RecentIssueEntry } from "./recent-issues-store";
export {

View File

@@ -38,7 +38,6 @@ import { Tooltip, TooltipTrigger, TooltipContent } from "@multica/ui/components/
import { Collapsible, CollapsibleTrigger, CollapsibleContent } from "@multica/ui/components/ui/collapsible";
import { StatusIcon } from "../issues/components/status-icon";
import { useIssueDraftStore } from "@multica/core/issues/stores/draft-store";
import { useCreateModeStore } from "@multica/core/issues/stores/create-mode-store";
import {
Sidebar,
SidebarContent,
@@ -396,14 +395,13 @@ export function AppSidebar({ topSlot, searchSlot, headerClassName, headerStyle }
},
});
// Global "C" shortcut: opens whichever create mode the user landed on last
// (agent vs manual), persisted in useCreateModeStore. The mode switch lives
// inside both modal footers so users can flip without remembering which
// shortcut goes where — `c` always means "open the create flow I prefer".
// Global "C" shortcut: opens the quick-create modal by default; Shift+C
// jumps straight to the legacy advanced form for users who want every
// field. Both branches honor the same focus / open-modal guards.
useEffect(() => {
const handleKeyDown = (e: KeyboardEvent) => {
if (e.key !== "c" && e.key !== "C") return;
if (e.metaKey || e.ctrlKey || e.altKey || e.shiftKey) return;
if (e.metaKey || e.ctrlKey || e.altKey) return;
const tag = (e.target as HTMLElement)?.tagName;
const isEditable =
tag === "INPUT" ||
@@ -413,11 +411,10 @@ export function AppSidebar({ topSlot, searchSlot, headerClassName, headerStyle }
if (isEditable) return;
if (useModalStore.getState().modal) return;
e.preventDefault();
const lastMode = useCreateModeStore.getState().lastMode;
if (lastMode === "manual") {
// Auto-fill project when on a project detail page (manual form only —
// agent mode lets the agent infer project from the prompt).
const projectMatch = pathname.match(/^\/[^/]+\/projects\/([^/]+)$/);
// Auto-fill project when on a project detail page (advanced form only —
// quick-create lets the agent infer project from prompt).
const projectMatch = pathname.match(/^\/[^/]+\/projects\/([^/]+)$/);
if (e.shiftKey) {
const data = projectMatch ? { project_id: projectMatch[1] } : undefined;
useModalStore.getState().open("create-issue", data);
} else {

View File

@@ -1,94 +0,0 @@
"use client";
import { useState } from "react";
import { cn } from "@multica/ui/lib/utils";
import { Dialog, DialogContent } from "@multica/ui/components/ui/dialog";
import {
useCreateModeStore,
type CreateMode,
} from "@multica/core/issues/stores/create-mode-store";
import { AgentCreatePanel } from "./quick-create-issue";
import { ManualCreatePanel, manualDialogContentClass } from "./create-issue";
/**
* Shell that owns the single `<Dialog>` AND `<DialogContent>` for the
* create-issue flow. Mode switching unmounts/mounts only the inner panel
* body — the Portal, Backdrop, and Popup all stay in the DOM, so Base UI
* never replays the open animation. That's what makes the switch feel
* instant; an earlier version put `<DialogContent>` inside each panel and
* the close→open animation cycle still fired on every toggle.
*
* `initialMode` comes from the modal registry (`quick-create-issue` →
* agent, `create-issue` → manual). Subsequent switches are local state
* only and never round-trip through the modal store.
*
* Carry payload: when a panel switches mode it can hand a payload up via
* `onSwitchMode`; the shell stores it as the next panel's `data` so seeding
* works exactly like a fresh open.
*
* Manual-mode `isExpanded` / `backlogHintIssueId` are lifted up because they
* drive `DialogContent`'s className — the className lives here in the shell
* since the Popup is here, but the toggles for those states live in the
* manual panel body.
*/
export function CreateIssueDialog({
onClose,
initialMode,
data,
}: {
onClose: () => void;
initialMode: CreateMode;
data?: Record<string, unknown> | null;
}) {
const setLastMode = useCreateModeStore((s) => s.setLastMode);
const [mode, setMode] = useState<CreateMode>(initialMode);
const [panelData, setPanelData] = useState(data ?? null);
const [isExpanded, setIsExpanded] = useState(false);
const [backlogHintIssueId, setBacklogHintIssueId] = useState<string | null>(null);
const switchTo = (next: CreateMode) => (carry?: Record<string, unknown> | null) => {
setLastMode(next);
setPanelData(carry ?? null);
setMode(next);
};
const className =
mode === "agent"
? cn(
"p-0 gap-0 flex flex-col overflow-hidden",
"!top-1/2 !left-1/2 !-translate-x-1/2 !-translate-y-1/2",
"!max-w-xl !w-full",
// Smooth size transition when switching modes — the manual mode
// uses the same easing.
"!transition-all !duration-300 !ease-out",
)
: manualDialogContentClass(isExpanded, backlogHintIssueId);
return (
<Dialog open onOpenChange={(v) => { if (!v) onClose(); }}>
<DialogContent
finalFocus={false}
showCloseButton={false}
className={className}
>
{mode === "agent" ? (
<AgentCreatePanel
onClose={onClose}
onSwitchMode={switchTo("manual")}
data={panelData}
/>
) : (
<ManualCreatePanel
onClose={onClose}
onSwitchMode={switchTo("agent")}
data={panelData}
isExpanded={isExpanded}
setIsExpanded={setIsExpanded}
backlogHintIssueId={backlogHintIssueId}
setBacklogHintIssueId={setBacklogHintIssueId}
/>
)}
</DialogContent>
</Dialog>
);
}

View File

@@ -5,7 +5,6 @@ import { useQuery } from "@tanstack/react-query";
import { useNavigation } from "../navigation";
import {
ArrowDown,
ArrowLeftRight,
ArrowUp,
Check,
ChevronRight,
@@ -18,6 +17,7 @@ import { cn } from "@multica/ui/lib/utils";
import { toast } from "sonner";
import type { Issue, IssueStatus, IssuePriority, IssueAssigneeType } from "@multica/core/types";
import {
Dialog,
DialogContent,
DialogTitle,
} from "@multica/ui/components/ui/dialog";
@@ -37,7 +37,6 @@ import { ProjectPicker } from "../projects/components/project-picker";
import { useCurrentWorkspace, useWorkspacePaths } from "@multica/core/paths";
import { useWorkspaceId } from "@multica/core/hooks";
import { useIssueDraftStore } from "@multica/core/issues/stores/draft-store";
import { useCreateModeStore } from "@multica/core/issues/stores/create-mode-store";
import { issueDetailOptions } from "@multica/core/issues/queries";
import { useCreateIssue, useUpdateIssue } from "@multica/core/issues/mutations";
import { useFileUpload } from "@multica/core/hooks/use-file-upload";
@@ -47,34 +46,10 @@ import { PillButton } from "../common/pill-button";
import { IssuePickerModal } from "./issue-picker-modal";
// ---------------------------------------------------------------------------
// ManualCreatePanel — manual-mode body of the create-issue dialog. Renders
// DialogContent + everything inside; the surrounding `<Dialog>` is owned by
// CreateIssueDialog so mode switching swaps only the inner panel without
// remounting the Dialog Root (no overlay flash). `onSwitchMode` flips the
// shell's local mode state.
// CreateIssueModal
// ---------------------------------------------------------------------------
export function ManualCreatePanel({
onClose,
onSwitchMode,
data,
isExpanded,
setIsExpanded,
backlogHintIssueId,
setBacklogHintIssueId,
}: {
onClose: () => void;
/** Called with the carry payload to seed the agent panel after switch. */
onSwitchMode?: (carry?: Record<string, unknown> | null) => void;
data?: Record<string, unknown> | null;
/** Lifted to the shell so DialogContent's mode-aware className can react
* without the body itself having to live inside DialogContent (which would
* re-mount the Portal on mode swap and replay the open animation). */
isExpanded: boolean;
setIsExpanded: (v: boolean) => void;
backlogHintIssueId: string | null;
setBacklogHintIssueId: (id: string | null) => void;
}) {
export function CreateIssueModal({ onClose, data }: { onClose: () => void; data?: Record<string, unknown> | null }) {
const router = useNavigation();
const p = useWorkspacePaths();
const workspaceName = useCurrentWorkspace()?.name;
@@ -83,7 +58,6 @@ export function ManualCreatePanel({
const setDraft = useIssueDraftStore((s) => s.setDraft);
const clearDraft = useIssueDraftStore((s) => s.clearDraft);
const setLastAssignee = useIssueDraftStore((s) => s.setLastAssignee);
const setLastMode = useCreateModeStore((s) => s.setLastMode);
const [title, setTitle] = useState(draft.title);
const descEditorRef = useRef<ContentEditorRef>(null);
@@ -107,6 +81,9 @@ export function ManualCreatePanel({
// object, and we never need to hydrate from an ID the way we do for parent.
const [childIssues, setChildIssues] = useState<Issue[]>([]);
const [childPickerOpen, setChildPickerOpen] = useState(false);
const [isExpanded, setIsExpanded] = useState(false);
const [backlogHintIssueId, setBacklogHintIssueId] = useState<string | null>(null);
// Fetch parent issue details for the chip (status/identifier/title).
// List cache usually has it already, so this resolves synchronously.
const wsId = useWorkspaceId();
@@ -178,7 +155,6 @@ export function ManualCreatePanel({
}
setLastAssignee(assigneeType, assigneeId);
setLastMode("manual");
clearDraft();
const shouldShowBacklogHint =
status === "backlog" && assigneeType === "agent" && assigneeId &&
@@ -223,25 +199,32 @@ export function ManualCreatePanel({
}
};
// Switch to agent mode. Hand the typed text up to the shell as the carry
// payload; the shell stores it as the next panel's `data` so the agent
// panel reads `data.prompt` on mount. Concatenate title + description so
// nothing the user typed is lost — the agent derives a fresh title from
// the combined text. Persist the mode flip so the next `c` lands in agent.
const switchToAgent = () => {
const desc = descEditorRef.current?.getMarkdown()?.trim() ?? "";
const prompt = [title.trim(), desc].filter(Boolean).join("\n\n");
setLastMode("agent");
onSwitchMode?.({
prompt,
...(assigneeType === "agent" && assigneeId
? { agent_id: assigneeId }
: {}),
});
};
return (
<>
<Dialog
open
onOpenChange={(v) => {
if (!v) {
setBacklogHintIssueId(null);
onClose();
}
}}
>
<DialogContent
finalFocus={false}
showCloseButton={false}
className={cn(
"p-0 gap-0 flex flex-col overflow-hidden",
"!top-1/2 !left-1/2 !-translate-x-1/2",
backlogHintIssueId
? "!max-w-[480px] !w-[calc(100vw-2rem)] !h-auto !-translate-y-1/2 !transition-none !duration-0"
: "!transition-all !duration-300 !ease-out",
!backlogHintIssueId && isExpanded
? "!max-w-4xl !w-full !h-5/6 !-translate-y-1/2"
: !backlogHintIssueId
? "!max-w-2xl !w-full !h-96 !-translate-y-1/2"
: "",
)}
>
{backlogHintIssueId ? (
<BacklogAgentHintContent
onKeepInBacklog={() => {
@@ -269,7 +252,7 @@ export function ManualCreatePanel({
<div className="flex items-center gap-1.5 text-xs">
<span className="text-muted-foreground">{workspaceName}</span>
<ChevronRight className="size-3 text-muted-foreground/50" />
<span className="font-medium">Create manually</span>
<span className="font-medium">New issue</span>
</div>
<div className="flex items-center gap-1">
<Tooltip>
@@ -498,74 +481,13 @@ export function ManualCreatePanel({
<FileUploadButton
onSelect={(file) => descEditorRef.current?.uploadFile(file)}
/>
<div className="flex items-center gap-2">
<button
type="button"
onClick={switchToAgent}
title="Switch to create with agent — describe in one line and let the agent file it"
className="flex items-center gap-1.5 text-xs px-2 py-1 rounded-sm text-muted-foreground hover:text-foreground hover:bg-accent/60 transition-colors cursor-pointer"
>
<ArrowLeftRight className="size-3.5" />
Switch to agent
</button>
<Button size="sm" onClick={handleSubmit} disabled={!title.trim() || submitting}>
{submitting ? "Creating..." : "Create Issue"}
</Button>
</div>
<Button size="sm" onClick={handleSubmit} disabled={!title.trim() || submitting}>
{submitting ? "Creating..." : "Create Issue"}
</Button>
</div>
</>
)}
</>
);
}
/** className for DialogContent in manual mode — depends on isExpanded and the
* backlog-hint sub-state. Exported so the shell (which now owns the
* DialogContent) can apply the same visual treatment without duplicating it. */
export function manualDialogContentClass(
isExpanded: boolean,
backlogHintIssueId: string | null,
) {
return cn(
"p-0 gap-0 flex flex-col overflow-hidden",
"!top-1/2 !left-1/2 !-translate-x-1/2",
backlogHintIssueId
? "!max-w-[480px] !w-[calc(100vw-2rem)] !h-auto !-translate-y-1/2 !transition-none !duration-0"
: "!transition-all !duration-300 !ease-out",
!backlogHintIssueId && isExpanded
? "!max-w-4xl !w-full !h-5/6 !-translate-y-1/2"
: !backlogHintIssueId
? "!max-w-2xl !w-full !h-96 !-translate-y-1/2"
: "",
);
}
// Thin Dialog-wrapping export — registry mounts the panel directly under the
// shell's shared Dialog, but a few legacy callers (and the test suite) still
// import this module's modal version. Equivalent runtime behavior to the
// pre-refactor component when used standalone.
import { Dialog as DialogRoot } from "@multica/ui/components/ui/dialog";
export function CreateIssueModal(props: {
onClose: () => void;
data?: Record<string, unknown> | null;
}) {
const [isExpanded, setIsExpanded] = useState(false);
const [backlogHintIssueId, setBacklogHintIssueId] = useState<string | null>(null);
return (
<DialogRoot open onOpenChange={(v) => { if (!v) props.onClose(); }}>
<DialogContent
finalFocus={false}
showCloseButton={false}
className={manualDialogContentClass(isExpanded, backlogHintIssueId)}
>
<ManualCreatePanel
{...props}
isExpanded={isExpanded}
setIsExpanded={setIsExpanded}
backlogHintIssueId={backlogHintIssueId}
setBacklogHintIssueId={setBacklogHintIssueId}
/>
</DialogContent>
</DialogRoot>
</Dialog>
);
}

View File

@@ -1,16 +1,22 @@
"use client";
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import { ArrowLeftRight, ChevronRight, Sparkles, X as XIcon } from "lucide-react";
import { useEffect, useMemo, useRef, useState } from "react";
import { ChevronRight, Sparkles, X as XIcon } from "lucide-react";
import { useQuery } from "@tanstack/react-query";
import { toast } from "sonner";
import { DialogTitle } from "@multica/ui/components/ui/dialog";
import { cn } from "@multica/ui/lib/utils";
import {
Dialog,
DialogContent,
DialogTitle,
} from "@multica/ui/components/ui/dialog";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from "@multica/ui/components/ui/dropdown-menu";
import { Tooltip, TooltipContent, TooltipTrigger } from "@multica/ui/components/ui/tooltip";
import { Button } from "@multica/ui/components/ui/button";
import { api, ApiError } from "@multica/core/api";
import { useWorkspaceId } from "@multica/core/hooks";
@@ -18,36 +24,22 @@ import { useCurrentWorkspace } from "@multica/core/paths";
import { agentListOptions } from "@multica/core/workspace/queries";
import { useQuickCreateStore } from "@multica/core/issues/stores/quick-create-store";
import { useIssueDraftStore } from "@multica/core/issues/stores/draft-store";
import { useCreateModeStore } from "@multica/core/issues/stores/create-mode-store";
import { useFileUpload } from "@multica/core/hooks/use-file-upload";
import { useModalStore } from "@multica/core/modals";
import type { Agent } from "@multica/core/types";
import { ActorAvatar } from "../common/actor-avatar";
import { canAssignAgent } from "../issues/components/pickers/assignee-picker";
import { useAuthStore } from "@multica/core/auth";
import { memberListOptions } from "@multica/core/workspace/queries";
import {
ContentEditor,
type ContentEditorRef,
useFileDropZone,
FileDropOverlay,
} from "../editor";
// AgentCreatePanel — agent-mode body of the create-issue dialog. Renders
// only the inner content; the surrounding `<Dialog>` AND `<DialogContent>`
// (Portal + Overlay + Popup) are owned by CreateIssueDialog so mode-switching
// swaps only this body. Lifting the Portal is what eliminates the close→open
// animation flash — Base UI replays Popup enter/exit when DialogContent is
// remounted, even inside a still-open Dialog Root.
//
// `onSwitchMode` is wired by the shell — the panel calls it (no payload from
// agent → manual; the shared draft store carries description + agent).
export function AgentCreatePanel({
// QuickCreateIssueModal — a streamlined create-issue UI: pick an agent, type
// one line, submit. The agent translates the line into a `multica issue
// create` call asynchronously; the modal closes immediately and the user is
// notified via inbox when the agent finishes.
export function QuickCreateIssueModal({
onClose,
onSwitchMode,
data,
}: {
onClose: () => void;
onSwitchMode?: () => void;
data?: Record<string, unknown> | null;
}) {
const workspaceName = useCurrentWorkspace()?.name;
@@ -72,7 +64,6 @@ export function AgentCreatePanel({
const lastAgentId = useQuickCreateStore((s) => s.lastAgentId);
const setLastAgentId = useQuickCreateStore((s) => s.setLastAgentId);
const setLastMode = useCreateModeStore((s) => s.setLastMode);
const [agentId, setAgentId] = useState<string | undefined>(() => {
const seed = (data?.agent_id as string) || lastAgentId || undefined;
@@ -98,43 +89,22 @@ export function AgentCreatePanel({
);
const initialPrompt = (data?.prompt as string) || "";
// The editor is uncontrolled — we read the latest markdown via the ref at
// submit/switch time. `hasContent` mirrors emptiness so the Create button
// can disable correctly without a controlled-input rerender on every keystroke.
const editorRef = useRef<ContentEditorRef>(null);
const [hasContent, setHasContent] = useState(initialPrompt.trim().length > 0);
const [prompt, setPrompt] = useState(initialPrompt);
const textareaRef = useRef<HTMLTextAreaElement>(null);
const [submitting, setSubmitting] = useState(false);
const [error, setError] = useState<string | null>(null);
// Image paste/drop support: route uploads through the same helper Advanced
// uses, so users can paste screenshots straight into the prompt and the
// agent receives them as embedded markdown image URLs in the prompt.
const { uploadWithToast } = useFileUpload(api);
const handleUploadFile = useCallback(
(file: File) => uploadWithToast(file),
[uploadWithToast],
);
const { isDragOver, dropZoneProps } = useFileDropZone({
onDrop: (files) => files.forEach((f) => editorRef.current?.uploadFile(f)),
});
useEffect(() => {
// Defer focus so it lands after the dialog's focus trap has settled —
// otherwise the trap can bounce focus back to the first focusable header
// button on the next tick.
const id = requestAnimationFrame(() => editorRef.current?.focus());
return () => cancelAnimationFrame(id);
textareaRef.current?.focus();
}, []);
const submit = async () => {
const md = editorRef.current?.getMarkdown()?.trim() ?? "";
if (!md || !agentId || submitting) return;
if (!prompt.trim() || !agentId || submitting) return;
setSubmitting(true);
setError(null);
try {
await api.quickCreateIssue({ agent_id: agentId, prompt: md });
await api.quickCreateIssue({ agent_id: agentId, prompt: prompt.trim() });
setLastAgentId(agentId);
setLastMode("agent");
toast.success("Sent to agent — you'll get an inbox notification when it's done", {
duration: 4000,
});
@@ -157,26 +127,39 @@ export function AgentCreatePanel({
}
};
// Switch to the manual form, carrying what the user typed over as the
// description (markdown, including any pasted images) so they don't lose
// their work. The picked agent becomes the default assignee candidate
// (still editable). We seed the shared issue-draft store directly because
// the manual panel reads its initial values from there. Persist the mode
// flip so the next `c` lands in manual.
const switchToManual = () => {
const md = editorRef.current?.getMarkdown() ?? "";
// Switch to the legacy advanced form, carrying the prompt over as the
// description so the user doesn't lose what they typed. The picked agent
// becomes the default assignee candidate (still editable). We seed the
// shared issue-draft store directly because the legacy modal reads its
// initial values from there rather than from `data`.
const switchToAdvanced = () => {
useIssueDraftStore.getState().setDraft({
description: md,
description: prompt,
...(agentId
? { assigneeType: "agent" as const, assigneeId: agentId }
: {}),
});
setLastMode("manual");
onSwitchMode?.();
useModalStore.getState().open("create-issue");
};
const handleKeyDown = (e: React.KeyboardEvent<HTMLTextAreaElement>) => {
if (e.key === "Enter" && (e.metaKey || e.ctrlKey)) {
e.preventDefault();
submit();
}
};
return (
<>
<Dialog open onOpenChange={(v) => { if (!v) onClose(); }}>
<DialogContent
finalFocus={false}
showCloseButton={false}
className={cn(
"p-0 gap-0 flex flex-col overflow-hidden",
"!top-1/2 !left-1/2 !-translate-x-1/2 !-translate-y-1/2",
"!max-w-xl !w-full",
)}
>
<DialogTitle className="sr-only">Quick create issue</DialogTitle>
{/* Header */}
@@ -184,20 +167,38 @@ export function AgentCreatePanel({
<div className="flex items-center gap-1.5 text-xs">
<span className="text-muted-foreground">{workspaceName}</span>
<ChevronRight className="size-3 text-muted-foreground/50" />
<span className="font-medium">Create with agent</span>
<span className="font-medium">Quick create</span>
</div>
<div className="flex items-center gap-1">
<Tooltip>
<TooltipTrigger
render={
<button
onClick={switchToAdvanced}
className="text-xs px-2 py-1 rounded-sm opacity-70 hover:opacity-100 hover:bg-accent/60 transition-all cursor-pointer"
>
Advanced
</button>
}
/>
<TooltipContent side="bottom">
Open the full form with all fields
</TooltipContent>
</Tooltip>
<Tooltip>
<TooltipTrigger
render={
<button
onClick={onClose}
className="rounded-sm p-1.5 opacity-70 hover:opacity-100 hover:bg-accent/60 transition-all cursor-pointer"
>
<XIcon className="size-4" />
</button>
}
/>
<TooltipContent side="bottom">Close</TooltipContent>
</Tooltip>
</div>
{/* Native `title` instead of Base UI Tooltip — Tooltip opens on
keyboard focus, and the dialog's focus trap briefly lands focus
on the first focusable element on mount, causing the tooltip to
auto-pop every open. */}
<button
onClick={onClose}
title="Close"
aria-label="Close"
className="rounded-sm p-1.5 opacity-70 hover:opacity-100 hover:bg-accent/60 transition-all cursor-pointer"
>
<XIcon className="size-4" />
</button>
</div>
{/* Agent picker */}
@@ -254,23 +255,17 @@ export function AgentCreatePanel({
</DropdownMenu>
</div>
{/* Prompt — same rich editor Advanced uses, so paste/drop images,
mentions, and formatting all work. The dropZone wrapper enables
drag-and-drop file uploads alongside paste. */}
<div
{...dropZoneProps}
className="relative px-5 pb-3 min-h-[140px]"
>
<ContentEditor
ref={editorRef}
defaultValue={initialPrompt}
{/* Prompt textarea */}
<div className="px-5 pb-3">
<textarea
ref={textareaRef}
value={prompt}
onChange={(e) => setPrompt(e.target.value)}
onKeyDown={handleKeyDown}
placeholder='Describe the issue, e.g. "fix inbox loading slowness, assign to naiyuan, P1"'
onUpdate={(md) => setHasContent(md.trim().length > 0)}
onUploadFile={handleUploadFile}
onSubmit={submit}
debounceMs={150}
rows={5}
className="w-full resize-none bg-transparent text-sm placeholder:text-muted-foreground focus:outline-none"
/>
{isDragOver && <FileDropOverlay />}
</div>
{error && (
@@ -280,25 +275,15 @@ export function AgentCreatePanel({
{/* Footer */}
<div className="flex items-center justify-between px-4 py-3 border-t shrink-0">
<span className="text-xs text-muted-foreground"> to submit</span>
<div className="flex items-center gap-2">
<button
type="button"
onClick={switchToManual}
title="Switch to manual create — fill the fields yourself"
className="flex items-center gap-1.5 text-xs px-2 py-1 rounded-sm text-muted-foreground hover:text-foreground hover:bg-accent/60 transition-colors cursor-pointer"
>
<ArrowLeftRight className="size-3.5" />
Switch to manual
</button>
<Button
size="sm"
onClick={submit}
disabled={!hasContent || !agentId || submitting}
>
{submitting ? "Sending…" : "Create"}
</Button>
</div>
<Button
size="sm"
onClick={submit}
disabled={!prompt.trim() || !agentId || submitting}
>
{submitting ? "Sending…" : "Create"}
</Button>
</div>
</>
</DialogContent>
</Dialog>
);
}

View File

@@ -2,7 +2,8 @@
import { useModalStore } from "@multica/core/modals";
import { CreateWorkspaceModal } from "./create-workspace";
import { CreateIssueDialog } from "./create-issue-dialog";
import { CreateIssueModal } from "./create-issue";
import { QuickCreateIssueModal } from "./quick-create-issue";
import { CreateProjectModal } from "./create-project";
import { FeedbackModal } from "./feedback";
import { SetParentIssueModal } from "./set-parent-issue";
@@ -18,12 +19,10 @@ export function ModalRegistry() {
switch (modal) {
case "create-workspace":
return <CreateWorkspaceModal onClose={close} />;
// Both modal types open the same shell so the in-modal mode switch is
// instant — only the inner panel swaps, the Dialog Root stays mounted.
case "create-issue":
return <CreateIssueDialog onClose={close} initialMode="manual" data={data} />;
return <CreateIssueModal onClose={close} data={data} />;
case "quick-create-issue":
return <CreateIssueDialog onClose={close} initialMode="agent" data={data} />;
return <QuickCreateIssueModal onClose={close} data={data} />;
case "create-project":
return <CreateProjectModal onClose={close} />;
case "feedback":