Skip to content
Merged
4 changes: 4 additions & 0 deletions packages/core/issues/stores/quick-create-store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,13 +15,17 @@ 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<QuickCreateState>()(
persist(
(set) => ({
lastAgentId: null,
setLastAgentId: (id) => set({ lastAgentId: id }),
keepOpen: false,
setKeepOpen: (v) => set({ keepOpen: v }),
}),
{
name: "multica_quick_create",
Expand Down
7 changes: 2 additions & 5 deletions packages/views/agents/components/inspector/model-picker.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down Expand Up @@ -78,9 +77,7 @@ export function ModelPicker({
);
}

const triggerLabel =
value ||
(defaultModel ? `Default — ${defaultModel.label}` : "Default");
const triggerLabel = value || "Default";
const triggerTitle = `Model · ${triggerLabel}`;

return (
Expand Down
5 changes: 1 addition & 4 deletions packages/views/agents/components/model-dropdown.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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) {
Expand Down
5 changes: 5 additions & 0 deletions packages/views/inbox/components/inbox-detail-label.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,11 @@ export function InboxDetailLabel({ item }: { item: InboxItem }) {
if (emoji) return <span>Reacted {emoji} to your comment</span>;
return <span>{typeLabels[item.type]}</span>;
}
case "quick_create_done": {
const identifier = details.identifier;
if (identifier) return <span>Created {identifier}</span>;
return <span>{typeLabels[item.type]}</span>;
}
default:
return <span>{typeLabels[item.type] ?? item.type}</span>;
}
Expand Down
8 changes: 0 additions & 8 deletions packages/views/issues/components/issue-detail.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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();

Expand Down
3 changes: 0 additions & 3 deletions packages/views/issues/components/issue-detail.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -506,9 +506,6 @@ export function IssueDetail({ issueId, onDelete, defaultSidebarOpen = true, layo
<ChevronRight className="h-3 w-3 text-muted-foreground/50 shrink-0" />
</>
)}
<span className="shrink-0 text-muted-foreground">
{issue.identifier}
</span>
<span className="truncate font-medium text-foreground">
{issue.title}
</span>
Expand Down
2 changes: 1 addition & 1 deletion packages/views/layout/app-sidebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -560,7 +560,7 @@ export function AppSidebar({ topSlot, searchSlot, headerClassName, headerStyle }
<SidebarMenuItem>
<SidebarMenuButton
className="text-muted-foreground"
onClick={() => useModalStore.getState().open("create-issue")}
onClick={() => useModalStore.getState().open("quick-create-issue")}
>
<span className="relative">
<SquarePen />
Expand Down
53 changes: 46 additions & 7 deletions packages/views/modals/quick-create-issue.tsx
Original file line number Diff line number Diff line change
@@ -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";
Expand All @@ -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";
Expand All @@ -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 `<Dialog>` AND `<DialogContent>`
Expand Down Expand Up @@ -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<string | undefined>(() => {
Expand Down Expand Up @@ -130,12 +134,14 @@ export function AgentCreatePanel({
const editorRef = useRef<ContentEditorRef>(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<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 { uploadWithToast, uploading } = useFileUpload(api);
const handleUploadFile = useCallback(
(file: File) => uploadWithToast(file),
[uploadWithToast],
Expand All @@ -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 {
Expand All @@ -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
Expand Down Expand Up @@ -334,7 +351,18 @@ 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-1.5">
<FileUploadButton
size="sm"
onSelect={(file) => editorRef.current?.uploadFile(file)}
/>
<span className="text-xs text-muted-foreground">
{keepOpen && sentCount > 0 && (
<span className="text-emerald-600 dark:text-emerald-400">{sentCount} sent · </span>
)}
⌘↵ to submit
</span>
</div>
<div className="flex items-center gap-2">
<button
type="button"
Expand All @@ -345,17 +373,28 @@ export function AgentCreatePanel({
<ArrowLeftRight className="size-3.5" />
Switch to manual
</button>
<label className="flex items-center gap-1.5 text-xs text-muted-foreground cursor-pointer select-none">
<Switch
size="sm"
checked={keepOpen}
onCheckedChange={setKeepOpen}
/>
Create another
</label>
<Button
size="sm"
onClick={submit}
disabled={!hasContent || !agentId || submitting || versionBlocked}
disabled={!hasContent || !agentId || submitting || versionBlocked || uploading}
title={
versionBlocked
? `Daemon CLI must be ≥ ${versionCheck.min}`
: undefined
}
className={justSent ? "!bg-emerald-600 !text-white" : undefined}
>
{submitting ? "Sending…" : "Create"}
{submitting ? "Sending…" : uploading ? "Uploading…" : justSent ? (
<span className="flex items-center gap-1"><Check className="size-3.5" />Sent</span>
) : "Create"}
</Button>
</div>
</div>
Expand Down
2 changes: 1 addition & 1 deletion packages/views/search/search-command.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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);
});

Expand Down
2 changes: 1 addition & 1 deletion packages/views/search/search-command.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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);
},
},
Expand Down
2 changes: 1 addition & 1 deletion server/internal/service/task.go
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down