diff --git a/packages/core/issues/stores/quick-create-store.ts b/packages/core/issues/stores/quick-create-store.ts index 090cd2b8fc..c36b52bc1f 100644 --- a/packages/core/issues/stores/quick-create-store.ts +++ b/packages/core/issues/stores/quick-create-store.ts @@ -15,6 +15,8 @@ import { defaultStorage } from "../../platform/storage"; interface QuickCreateState { lastAgentId: string | null; setLastAgentId: (id: string | null) => void; + keepOpen: boolean; + setKeepOpen: (v: boolean) => void; } export const useQuickCreateStore = create()( @@ -22,6 +24,8 @@ export const useQuickCreateStore = create()( (set) => ({ lastAgentId: null, setLastAgentId: (id) => set({ lastAgentId: id }), + keepOpen: false, + setKeepOpen: (v) => set({ keepOpen: v }), }), { name: "multica_quick_create", diff --git a/packages/views/agents/components/inspector/model-picker.tsx b/packages/views/agents/components/inspector/model-picker.tsx index 25dc610b03..7a89b6e07a 100644 --- a/packages/views/agents/components/inspector/model-picker.tsx +++ b/packages/views/agents/components/inspector/model-picker.tsx @@ -42,12 +42,11 @@ export function ModelPicker({ const supported = modelsQuery.data?.supported ?? true; // Memoise the model list so every downstream useMemo gets a stable // reference — `?? []` would mint a fresh array on every render and - // invalidate filters / defaultModel needlessly. + // invalidate `filtered` needlessly. const models = useMemo( () => modelsQuery.data?.models ?? [], [modelsQuery.data], ); - const defaultModel = useMemo(() => models.find((m) => m.default), [models]); const filtered = useMemo(() => { const s = search.trim().toLowerCase(); @@ -78,9 +77,7 @@ export function ModelPicker({ ); } - const triggerLabel = - value || - (defaultModel ? `Default — ${defaultModel.label}` : "Default"); + const triggerLabel = value || "Default"; const triggerTitle = `Model · ${triggerLabel}`; return ( diff --git a/packages/views/agents/components/model-dropdown.tsx b/packages/views/agents/components/model-dropdown.tsx index f85606caf9..5a3592538d 100644 --- a/packages/views/agents/components/model-dropdown.tsx +++ b/packages/views/agents/components/model-dropdown.tsx @@ -42,7 +42,6 @@ export function ModelDropdown({ const supported = modelsQuery.data?.supported ?? true; const models = modelsQuery.data?.models ?? []; - const defaultModel = useMemo(() => models.find((m) => m.default), [models]); const grouped = useMemo(() => groupByProvider(models), [models]); // When the selected runtime reports it doesn't support per-agent @@ -86,9 +85,7 @@ export function ModelDropdown({ (disabled ? "Select a runtime first" : runtimeOnline - ? defaultModel - ? `Default — ${defaultModel.label}` - : "Default (provider)" + ? "Default (provider)" : "Runtime offline — enter manually"); if (!supported && !modelsQuery.isLoading) { diff --git a/packages/views/inbox/components/inbox-detail-label.tsx b/packages/views/inbox/components/inbox-detail-label.tsx index 6ecf26d9b3..c643ef26aa 100644 --- a/packages/views/inbox/components/inbox-detail-label.tsx +++ b/packages/views/inbox/components/inbox-detail-label.tsx @@ -88,6 +88,11 @@ export function InboxDetailLabel({ item }: { item: InboxItem }) { if (emoji) return Reacted {emoji} to your comment; return {typeLabels[item.type]}; } + case "quick_create_done": { + const identifier = details.identifier; + if (identifier) return Created {identifier}; + return {typeLabels[item.type]}; + } default: return {typeLabels[item.type] ?? item.type}; } diff --git a/packages/views/issues/components/issue-detail.test.tsx b/packages/views/issues/components/issue-detail.test.tsx index 4bfa1c6d4e..a5652e9dda 100644 --- a/packages/views/issues/components/issue-detail.test.tsx +++ b/packages/views/issues/components/issue-detail.test.tsx @@ -399,14 +399,6 @@ describe("IssueDetail (shared)", () => { expect(screen.getByDisplayValue("Add JWT auth to the backend")).toBeInTheDocument(); }); - it("renders issue identifier in the breadcrumb", async () => { - renderIssueDetail(); - - await waitFor(() => { - expect(screen.getByText("TES-1")).toBeInTheDocument(); - }); - }); - it("renders workspace name as breadcrumb link", async () => { renderIssueDetail(); diff --git a/packages/views/issues/components/issue-detail.tsx b/packages/views/issues/components/issue-detail.tsx index ebde2092bf..f0b1c393ab 100644 --- a/packages/views/issues/components/issue-detail.tsx +++ b/packages/views/issues/components/issue-detail.tsx @@ -506,9 +506,6 @@ export function IssueDetail({ issueId, onDelete, defaultSidebarOpen = true, layo )} - - {issue.identifier} - {issue.title} diff --git a/packages/views/layout/app-sidebar.tsx b/packages/views/layout/app-sidebar.tsx index c1de3f170b..346194964d 100644 --- a/packages/views/layout/app-sidebar.tsx +++ b/packages/views/layout/app-sidebar.tsx @@ -560,7 +560,7 @@ export function AppSidebar({ topSlot, searchSlot, headerClassName, headerStyle } useModalStore.getState().open("create-issue")} + onClick={() => useModalStore.getState().open("quick-create-issue")} > diff --git a/packages/views/modals/quick-create-issue.tsx b/packages/views/modals/quick-create-issue.tsx index 0bfb4db93e..a0f6c7831a 100644 --- a/packages/views/modals/quick-create-issue.tsx +++ b/packages/views/modals/quick-create-issue.tsx @@ -1,7 +1,7 @@ "use client"; import { useCallback, useEffect, useMemo, useRef, useState } from "react"; -import { ArrowLeftRight, ChevronRight, X as XIcon } from "lucide-react"; +import { ArrowLeftRight, Check, ChevronRight, X as XIcon } from "lucide-react"; import { useQuery } from "@tanstack/react-query"; import { toast } from "sonner"; import { DialogTitle } from "@multica/ui/components/ui/dialog"; @@ -12,6 +12,7 @@ import { DropdownMenuTrigger, } from "@multica/ui/components/ui/dropdown-menu"; import { Button } from "@multica/ui/components/ui/button"; +import { Switch } from "@multica/ui/components/ui/switch"; import { api, ApiError } from "@multica/core/api"; import { useWorkspaceId } from "@multica/core/hooks"; import { useCurrentWorkspace } from "@multica/core/paths"; @@ -37,6 +38,7 @@ import { useFileDropZone, FileDropOverlay, } from "../editor"; +import { FileUploadButton } from "@multica/ui/components/common/file-upload-button"; // AgentCreatePanel — agent-mode body of the create-issue dialog. Renders // only the inner content; the surrounding `` AND `` @@ -78,6 +80,8 @@ export function AgentCreatePanel({ const lastAgentId = useQuickCreateStore((s) => s.lastAgentId); const setLastAgentId = useQuickCreateStore((s) => s.setLastAgentId); + const keepOpen = useQuickCreateStore((s) => s.keepOpen); + const setKeepOpen = useQuickCreateStore((s) => s.setKeepOpen); const setLastMode = useCreateModeStore((s) => s.setLastMode); const [agentId, setAgentId] = useState(() => { @@ -130,12 +134,14 @@ export function AgentCreatePanel({ const editorRef = useRef(null); const [hasContent, setHasContent] = useState(initialPrompt.trim().length > 0); const [submitting, setSubmitting] = useState(false); + const [justSent, setJustSent] = useState(false); + const [sentCount, setSentCount] = useState(0); const [error, setError] = useState(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 { uploadWithToast, uploading } = useFileUpload(api); const handleUploadFile = useCallback( (file: File) => uploadWithToast(file), [uploadWithToast], @@ -154,7 +160,7 @@ export function AgentCreatePanel({ const submit = async () => { const md = editorRef.current?.getMarkdown()?.trim() ?? ""; - if (!md || !agentId || submitting || versionBlocked) return; + if (!md || !agentId || submitting || versionBlocked || uploading) return; setSubmitting(true); setError(null); try { @@ -164,7 +170,18 @@ export function AgentCreatePanel({ toast.success("Sent to agent — you'll get an inbox notification when it's done", { duration: 4000, }); - onClose(); + if (keepOpen) { + // Stay open for continuous creation — clear the editor so the + // user can immediately type the next prompt. + editorRef.current?.clearContent(); + setHasContent(false); + setSentCount((c) => c + 1); + setJustSent(true); + setTimeout(() => setJustSent(false), 1500); + requestAnimationFrame(() => editorRef.current?.focus()); + } else { + onClose(); + } } catch (e) { // Server returns 422 with { code, ... } for the structured rejection // paths the modal cares about. Surface the reason in-modal so the @@ -334,7 +351,18 @@ export function AgentCreatePanel({ {/* Footer */}
- ⌘↵ to submit +
+ editorRef.current?.uploadFile(file)} + /> + + {keepOpen && sentCount > 0 && ( + {sentCount} sent · + )} + ⌘↵ to submit + +
+
diff --git a/packages/views/search/search-command.test.tsx b/packages/views/search/search-command.test.tsx index 80b8d71954..88fc92fd54 100644 --- a/packages/views/search/search-command.test.tsx +++ b/packages/views/search/search-command.test.tsx @@ -261,7 +261,7 @@ describe("SearchCommand", () => { ); await user.click(newIssue); - expect(mockOpenModal).toHaveBeenCalledWith("create-issue"); + expect(mockOpenModal).toHaveBeenCalledWith("quick-create-issue"); expect(useSearchStore.getState().open).toBe(false); }); diff --git a/packages/views/search/search-command.tsx b/packages/views/search/search-command.tsx index ba63b624ec..23129bdff5 100644 --- a/packages/views/search/search-command.tsx +++ b/packages/views/search/search-command.tsx @@ -202,7 +202,7 @@ export function SearchCommand() { icon: Plus, keywords: ["new", "issue", "create", "add"], onSelect: () => { - useModalStore.getState().open("create-issue"); + useModalStore.getState().open("quick-create-issue"); setOpen(false); }, }, diff --git a/server/internal/service/task.go b/server/internal/service/task.go index 4752e46b86..06518868a2 100644 --- a/server/internal/service/task.go +++ b/server/internal/service/task.go @@ -1436,7 +1436,7 @@ func (s *TaskService) notifyQuickCreateCompleted(ctx context.Context, task db.Ag Type: "quick_create_done", Severity: "info", IssueID: issue.ID, - Title: fmt.Sprintf("Created %s: %s", identifier, issue.Title), + Title: issue.Title, Body: pgtype.Text{}, ActorType: pgtype.Text{String: "agent", Valid: true}, ActorID: task.AgentID,