Compare commits

...

1 Commits

Author SHA1 Message Date
Jiayuan
8260e11c8a Revert "feat(quick-create): add preset issue fields (#2002)"
This reverts commit a039c4d803.
2026-05-03 19:45:43 +02:00
10 changed files with 119 additions and 429 deletions

View File

@@ -426,7 +426,7 @@ export class ApiClient {
});
}
async quickCreateIssue(data: { agent_id: string; prompt: string; priority?: string; due_date?: string; project_id?: string }): Promise<{ task_id: string }> {
async quickCreateIssue(data: { agent_id: string; prompt: string }): Promise<{ task_id: string }> {
return this.fetch("/api/issues/quick-create", {
method: "POST",
body: JSON.stringify(data),

View File

@@ -16,7 +16,7 @@ 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";
import { agentListOptions, memberListOptions } from "@multica/core/workspace/queries";
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";
@@ -27,12 +27,11 @@ import {
MIN_QUICK_CREATE_CLI_VERSION,
} from "@multica/core/runtimes";
import { useFileUpload } from "@multica/core/hooks/use-file-upload";
import type { Agent, IssuePriority } from "@multica/core/types";
import type { Agent } from "@multica/core/types";
import { ActorAvatar } from "../common/actor-avatar";
import { canAssignAgent } from "../issues/components/pickers/assignee-picker";
import { PriorityPicker, DueDatePicker } from "../issues/components";
import { ProjectPicker } from "../projects/components/project-picker";
import { useAuthStore } from "@multica/core/auth";
import { memberListOptions } from "@multica/core/workspace/queries";
import {
ContentEditor,
type ContentEditorRef,
@@ -40,7 +39,6 @@ import {
FileDropOverlay,
} from "../editor";
import { FileUploadButton } from "@multica/ui/components/common/file-upload-button";
import { PillButton } from "../common/pill-button";
// AgentCreatePanel — agent-mode body of the create-issue dialog. Renders
// only the inner content; the surrounding `<Dialog>` AND `<DialogContent>`
@@ -139,9 +137,6 @@ export function AgentCreatePanel({
const [justSent, setJustSent] = useState(false);
const [sentCount, setSentCount] = useState(0);
const [error, setError] = useState<string | null>(null);
const [priority, setPriority] = useState<IssuePriority>("none");
const [dueDate, setDueDate] = useState<string | null>(null);
const [projectId, setProjectId] = 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
@@ -169,13 +164,7 @@ export function AgentCreatePanel({
setSubmitting(true);
setError(null);
try {
await api.quickCreateIssue({
agent_id: agentId,
prompt: md,
...(priority !== "none" ? { priority } : {}),
...(dueDate ? { due_date: dueDate } : {}),
...(projectId ? { project_id: projectId } : {}),
});
await api.quickCreateIssue({ agent_id: agentId, prompt: md });
setLastAgentId(agentId);
setLastMode("agent");
toast.success("Sent to agent — you'll get an inbox notification when it's done", {
@@ -338,27 +327,6 @@ export function AgentCreatePanel({
</div>
)}
<div className="flex items-center gap-1.5 px-5 py-1.5 shrink-0 flex-wrap border-b">
<PriorityPicker
priority={priority}
onUpdate={(u) => u.priority && setPriority(u.priority)}
triggerRender={<PillButton />}
align="start"
/>
<DueDatePicker
dueDate={dueDate}
onUpdate={(u) => setDueDate(u.due_date ?? null)}
triggerRender={<PillButton />}
align="start"
/>
<ProjectPicker
projectId={projectId}
onUpdate={(u) => setProjectId(u.project_id ?? null)}
triggerRender={<PillButton />}
align="start"
/>
</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. */}

View File

@@ -61,11 +61,7 @@ func buildQuickCreatePrompt(task Task) string {
b.WriteString(" - Never echo the title in the description.\n\n")
// priority
if task.QuickCreatePriority != "" {
fmt.Fprintf(&b, "- **priority**: pass `--priority %s`.\n\n", task.QuickCreatePriority)
} else {
b.WriteString("- **priority**: one of `urgent`, `high`, `medium`, `low`, or omit. Map P0/P1 → urgent/high; \"asap\" → urgent. If unspecified, omit.\n\n")
}
b.WriteString("- **priority**: one of `urgent`, `high`, `medium`, `low`, or omit. Map P0/P1 → urgent/high; \"asap\" → urgent. If unspecified, omit.\n\n")
// assignee
b.WriteString("- **assignee**:\n")
@@ -80,17 +76,8 @@ func buildQuickCreatePrompt(task Task) string {
b.WriteString(" - When the user did NOT name an assignee, default to YOURSELF (the picker agent): pass `--assignee <your agent name>`. Never leave the issue unassigned.\n\n")
}
// due date
if task.QuickCreateDueDate != "" {
fmt.Fprintf(&b, "- **due-date**: pass `--due-date %s`.\n\n", task.QuickCreateDueDate)
}
// fields to omit
if task.QuickCreateProjectID != "" {
fmt.Fprintf(&b, "- **project**: pass `--project %s`.\n\n", task.QuickCreateProjectID)
} else {
b.WriteString("- **project**: omit unless the platform UI shows a specific project context. The platform will route the issue to the workspace default.\n")
}
b.WriteString("- **project**: omit. The platform will route the issue to the workspace default.\n")
b.WriteString("- **status**: omit (defaults to `todo`).\n")
b.WriteString("- **attachments**: do NOT pass `--attachment`. The flag only accepts LOCAL file paths. Any image URL in the user input is already markdown — keep it inline in `--description` instead.\n\n")

View File

@@ -1,65 +0,0 @@
package daemon
import (
"strings"
"testing"
)
func TestBuildQuickCreatePrompt_AllExplicitFields(t *testing.T) {
task := Task{
QuickCreatePrompt: "Fix the login button",
QuickCreatePriority: "high",
QuickCreateDueDate: "2025-06-01T00:00:00Z",
QuickCreateProjectID: "123e4567-e89b-12d3-a456-426614174000",
}
got := buildQuickCreatePrompt(task)
for _, want := range []string{
"`--priority high`",
"`--due-date 2025-06-01T00:00:00Z`",
"`--project 123e4567-e89b-12d3-a456-426614174000`",
"`--priority high`.\n\n",
"`--due-date 2025-06-01T00:00:00Z`.\n\n",
} {
if !strings.Contains(got, want) {
t.Fatalf("prompt missing %q:\n%s", want, got)
}
}
if strings.Contains(got, `\n`) {
t.Fatalf("prompt should contain real newlines, got literal \\n:\n%s", got)
}
if strings.Contains(got, "Map P0/P1") {
t.Fatalf("prompt should not include fallback priority guidance when explicit priority is set:\n%s", got)
}
}
func TestBuildQuickCreatePrompt_PriorityOnly(t *testing.T) {
task := Task{
QuickCreatePrompt: "Urgent: server is down",
QuickCreatePriority: "urgent",
}
got := buildQuickCreatePrompt(task)
if !strings.Contains(got, "`--priority urgent`") {
t.Fatalf("prompt missing explicit priority flag:\n%s", got)
}
if strings.Contains(got, "Map P0/P1") {
t.Fatalf("prompt should not include fallback priority guidance when explicit priority is set:\n%s", got)
}
if strings.Contains(got, "`--due-date") || strings.Contains(got, "`--project") {
t.Fatalf("prompt should not inject unset quick-create flags:\n%s", got)
}
}
func TestBuildQuickCreatePrompt_NoneSet(t *testing.T) {
task := Task{QuickCreatePrompt: "Something came up"}
got := buildQuickCreatePrompt(task)
if !strings.Contains(got, "Map P0/P1") {
t.Fatalf("prompt should include fallback priority guidance when no explicit priority is set:\n%s", got)
}
if strings.Contains(got, `\n`) {
t.Fatalf("prompt should contain real newlines, got literal \\n:\n%s", got)
}
}

View File

@@ -33,34 +33,31 @@ type ProjectResourceData struct {
// Task represents a claimed task from the server.
// Agent data (name, skills) is populated by the claim endpoint.
type Task struct {
ID string `json:"id"`
AgentID string `json:"agent_id"`
RuntimeID string `json:"runtime_id"`
IssueID string `json:"issue_id"`
WorkspaceID string `json:"workspace_id"`
Agent *AgentData `json:"agent,omitempty"`
ID string `json:"id"`
AgentID string `json:"agent_id"`
RuntimeID string `json:"runtime_id"`
IssueID string `json:"issue_id"`
WorkspaceID string `json:"workspace_id"`
Agent *AgentData `json:"agent,omitempty"`
Repos []RepoData `json:"repos,omitempty"`
ProjectID string `json:"project_id,omitempty"` // issue's project, when present
ProjectTitle string `json:"project_title,omitempty"` // human-readable project title for context injection
ProjectResources []ProjectResourceData `json:"project_resources,omitempty"` // project-scoped resources to expose to the agent
PriorSessionID string `json:"prior_session_id,omitempty"` // Claude session ID from a previous task on this issue
PriorWorkDir string `json:"prior_work_dir,omitempty"` // work_dir from a previous task on this issue
TriggerCommentID string `json:"trigger_comment_id,omitempty"` // comment that triggered this task
TriggerCommentContent string `json:"trigger_comment_content,omitempty"` // content of the triggering comment
TriggerAuthorType string `json:"trigger_author_type,omitempty"` // "agent" or "member" — author kind for the triggering comment
TriggerAuthorName string `json:"trigger_author_name,omitempty"` // display name of the triggering comment author
ChatSessionID string `json:"chat_session_id,omitempty"` // non-empty for chat tasks
ChatMessage string `json:"chat_message,omitempty"` // user message content for chat tasks
AutopilotRunID string `json:"autopilot_run_id,omitempty"` // non-empty for autopilot run_only tasks
AutopilotID string `json:"autopilot_id,omitempty"` // autopilot that spawned this run
AutopilotTitle string `json:"autopilot_title,omitempty"` // autopilot title used as task context
AutopilotDescription string `json:"autopilot_description,omitempty"` // autopilot description used as task prompt
AutopilotSource string `json:"autopilot_source,omitempty"` // manual, schedule, webhook, or api
AutopilotTriggerPayload json.RawMessage `json:"autopilot_trigger_payload,omitempty"` // optional trigger payload for webhook/api runs
QuickCreatePrompt string `json:"quick_create_prompt,omitempty"` // user's natural-language input for quick-create tasks
QuickCreatePriority string `json:"quick_create_priority,omitempty"` // priority explicitly selected in quick-create UI
QuickCreateDueDate string `json:"quick_create_due_date,omitempty"` // due date explicitly selected in quick-create UI
QuickCreateProjectID string `json:"quick_create_project_id,omitempty"` // project explicitly selected in quick-create UI
ProjectID string `json:"project_id,omitempty"` // issue's project, when present
ProjectTitle string `json:"project_title,omitempty"` // human-readable project title for context injection
ProjectResources []ProjectResourceData `json:"project_resources,omitempty"` // project-scoped resources to expose to the agent
PriorSessionID string `json:"prior_session_id,omitempty"` // Claude session ID from a previous task on this issue
PriorWorkDir string `json:"prior_work_dir,omitempty"` // work_dir from a previous task on this issue
TriggerCommentID string `json:"trigger_comment_id,omitempty"` // comment that triggered this task
TriggerCommentContent string `json:"trigger_comment_content,omitempty"` // content of the triggering comment
TriggerAuthorType string `json:"trigger_author_type,omitempty"` // "agent" or "member" — author kind for the triggering comment
TriggerAuthorName string `json:"trigger_author_name,omitempty"` // display name of the triggering comment author
ChatSessionID string `json:"chat_session_id,omitempty"` // non-empty for chat tasks
ChatMessage string `json:"chat_message,omitempty"` // user message content for chat tasks
AutopilotRunID string `json:"autopilot_run_id,omitempty"` // non-empty for autopilot run_only tasks
AutopilotID string `json:"autopilot_id,omitempty"` // autopilot that spawned this run
AutopilotTitle string `json:"autopilot_title,omitempty"` // autopilot title used as task context
AutopilotDescription string `json:"autopilot_description,omitempty"` // autopilot description used as task prompt
AutopilotSource string `json:"autopilot_source,omitempty"` // manual, schedule, webhook, or api
AutopilotTriggerPayload json.RawMessage `json:"autopilot_trigger_payload,omitempty"` // optional trigger payload for webhook/api runs
QuickCreatePrompt string `json:"quick_create_prompt,omitempty"` // user's natural-language input for quick-create tasks
}
// AgentData holds agent details returned by the claim endpoint.

View File

@@ -134,48 +134,45 @@ type ProjectResourceData struct {
}
type AgentTaskResponse struct {
ID string `json:"id"`
AgentID string `json:"agent_id"`
RuntimeID string `json:"runtime_id"`
IssueID string `json:"issue_id"`
WorkspaceID string `json:"workspace_id"`
Status string `json:"status"`
Priority int32 `json:"priority"`
DispatchedAt *string `json:"dispatched_at"`
StartedAt *string `json:"started_at"`
CompletedAt *string `json:"completed_at"`
Result any `json:"result"`
Error *string `json:"error"`
FailureReason string `json:"failure_reason,omitempty"` // see TaskService.MaybeRetryFailedTask
Attempt int32 `json:"attempt"`
MaxAttempts int32 `json:"max_attempts"`
ParentTaskID *string `json:"parent_task_id,omitempty"`
Agent *TaskAgentData `json:"agent,omitempty"`
ID string `json:"id"`
AgentID string `json:"agent_id"`
RuntimeID string `json:"runtime_id"`
IssueID string `json:"issue_id"`
WorkspaceID string `json:"workspace_id"`
Status string `json:"status"`
Priority int32 `json:"priority"`
DispatchedAt *string `json:"dispatched_at"`
StartedAt *string `json:"started_at"`
CompletedAt *string `json:"completed_at"`
Result any `json:"result"`
Error *string `json:"error"`
FailureReason string `json:"failure_reason,omitempty"` // see TaskService.MaybeRetryFailedTask
Attempt int32 `json:"attempt"`
MaxAttempts int32 `json:"max_attempts"`
ParentTaskID *string `json:"parent_task_id,omitempty"`
Agent *TaskAgentData `json:"agent,omitempty"`
Repos []RepoData `json:"repos,omitempty"`
ProjectID string `json:"project_id,omitempty"` // issue's project, when present
ProjectTitle string `json:"project_title,omitempty"` // for surfacing in agent context
ProjectResources []ProjectResourceData `json:"project_resources,omitempty"` // resources attached to the project
CreatedAt string `json:"created_at"`
PriorSessionID string `json:"prior_session_id,omitempty"` // session ID from a previous task on same issue
PriorWorkDir string `json:"prior_work_dir,omitempty"` // work_dir from a previous task on same issue
TriggerCommentID *string `json:"trigger_comment_id,omitempty"` // comment that triggered this task
TriggerCommentContent string `json:"trigger_comment_content,omitempty"` // content of the triggering comment
TriggerSummary *string `json:"trigger_summary,omitempty"` // canonical short description snapshot — comment text / autopilot title — taken at task creation; survives source edits/deletes
TriggerAuthorType string `json:"trigger_author_type,omitempty"` // "agent" or "member" — author kind of the triggering comment
TriggerAuthorName string `json:"trigger_author_name,omitempty"` // display name of the triggering comment author
ChatSessionID string `json:"chat_session_id,omitempty"` // non-empty for chat tasks
ChatMessage string `json:"chat_message,omitempty"` // user message for chat tasks
AutopilotRunID string `json:"autopilot_run_id,omitempty"` // non-empty for autopilot-spawned tasks
AutopilotID string `json:"autopilot_id,omitempty"` // autopilot that spawned this task
AutopilotTitle string `json:"autopilot_title,omitempty"` // autopilot title used as task context
AutopilotDescription string `json:"autopilot_description,omitempty"` // autopilot description used as task prompt
AutopilotSource string `json:"autopilot_source,omitempty"` // manual, schedule, webhook, or api
AutopilotTriggerPayload json.RawMessage `json:"autopilot_trigger_payload,omitempty"` // optional trigger payload for webhook/api runs
QuickCreatePrompt string `json:"quick_create_prompt,omitempty"` // user's natural-language input for quick-create tasks
QuickCreatePriority string `json:"quick_create_priority,omitempty"` // priority explicitly selected in quick-create UI
QuickCreateDueDate string `json:"quick_create_due_date,omitempty"` // due date explicitly selected in quick-create UI
QuickCreateProjectID string `json:"quick_create_project_id,omitempty"` // project explicitly selected in quick-create UI
Kind string `json:"kind"` // discriminator: "comment" | "autopilot" | "chat" | "quick_create" | "direct" — used by the activity row to label tasks that have no linked issue
ProjectID string `json:"project_id,omitempty"` // issue's project, when present
ProjectTitle string `json:"project_title,omitempty"` // for surfacing in agent context
ProjectResources []ProjectResourceData `json:"project_resources,omitempty"` // resources attached to the project
CreatedAt string `json:"created_at"`
PriorSessionID string `json:"prior_session_id,omitempty"` // session ID from a previous task on same issue
PriorWorkDir string `json:"prior_work_dir,omitempty"` // work_dir from a previous task on same issue
TriggerCommentID *string `json:"trigger_comment_id,omitempty"` // comment that triggered this task
TriggerCommentContent string `json:"trigger_comment_content,omitempty"` // content of the triggering comment
TriggerSummary *string `json:"trigger_summary,omitempty"` // canonical short description snapshot — comment text / autopilot title — taken at task creation; survives source edits/deletes
TriggerAuthorType string `json:"trigger_author_type,omitempty"` // "agent" or "member" — author kind of the triggering comment
TriggerAuthorName string `json:"trigger_author_name,omitempty"` // display name of the triggering comment author
ChatSessionID string `json:"chat_session_id,omitempty"` // non-empty for chat tasks
ChatMessage string `json:"chat_message,omitempty"` // user message for chat tasks
AutopilotRunID string `json:"autopilot_run_id,omitempty"` // non-empty for autopilot-spawned tasks
AutopilotID string `json:"autopilot_id,omitempty"` // autopilot that spawned this task
AutopilotTitle string `json:"autopilot_title,omitempty"` // autopilot title used as task context
AutopilotDescription string `json:"autopilot_description,omitempty"` // autopilot description used as task prompt
AutopilotSource string `json:"autopilot_source,omitempty"` // manual, schedule, webhook, or api
AutopilotTriggerPayload json.RawMessage `json:"autopilot_trigger_payload,omitempty"` // optional trigger payload for webhook/api runs
QuickCreatePrompt string `json:"quick_create_prompt,omitempty"` // user's natural-language input for quick-create tasks
Kind string `json:"kind"` // discriminator: "comment" | "autopilot" | "chat" | "quick_create" | "direct" — used by the activity row to label tasks that have no linked issue
}
// TaskAgentData holds agent info included in claim responses so the daemon

View File

@@ -1095,15 +1095,6 @@ func (h *Handler) ClaimTaskByRuntime(w http.ResponseWriter, r *http.Request) {
if json.Unmarshal(task.Context, &qc) == nil && qc.Type == service.QuickCreateContextType {
hasQuickCreate = true
resp.QuickCreatePrompt = qc.Prompt
if qc.Priority != nil {
resp.QuickCreatePriority = *qc.Priority
}
if qc.DueDate != nil {
resp.QuickCreateDueDate = *qc.DueDate
}
if qc.ProjectID != nil {
resp.QuickCreateProjectID = *qc.ProjectID
}
resp.WorkspaceID = qc.WorkspaceID
if ws, err := h.Queries.GetWorkspace(r.Context(), parseUUID(qc.WorkspaceID)); err == nil && ws.Repos != nil {
var repos []RepoData

View File

@@ -18,7 +18,6 @@ import (
"github.com/go-chi/chi/v5"
"github.com/jackc/pgx/v5/pgtype"
"github.com/multica-ai/multica/server/internal/logger"
"github.com/multica-ai/multica/server/internal/service"
"github.com/multica-ai/multica/server/internal/util"
"github.com/multica-ai/multica/server/pkg/agent"
db "github.com/multica-ai/multica/server/pkg/db/generated"
@@ -27,33 +26,33 @@ import (
// IssueResponse is the JSON response for an issue.
type IssueResponse struct {
ID string `json:"id"`
WorkspaceID string `json:"workspace_id"`
Number int32 `json:"number"`
Identifier string `json:"identifier"`
Title string `json:"title"`
Description *string `json:"description"`
Status string `json:"status"`
Priority string `json:"priority"`
AssigneeType *string `json:"assignee_type"`
AssigneeID *string `json:"assignee_id"`
CreatorType string `json:"creator_type"`
CreatorID string `json:"creator_id"`
ParentIssueID *string `json:"parent_issue_id"`
ProjectID *string `json:"project_id"`
Position float64 `json:"position"`
DueDate *string `json:"due_date"`
CreatedAt string `json:"created_at"`
UpdatedAt string `json:"updated_at"`
Reactions []IssueReactionResponse `json:"reactions,omitempty"`
Attachments []AttachmentResponse `json:"attachments,omitempty"`
ID string `json:"id"`
WorkspaceID string `json:"workspace_id"`
Number int32 `json:"number"`
Identifier string `json:"identifier"`
Title string `json:"title"`
Description *string `json:"description"`
Status string `json:"status"`
Priority string `json:"priority"`
AssigneeType *string `json:"assignee_type"`
AssigneeID *string `json:"assignee_id"`
CreatorType string `json:"creator_type"`
CreatorID string `json:"creator_id"`
ParentIssueID *string `json:"parent_issue_id"`
ProjectID *string `json:"project_id"`
Position float64 `json:"position"`
DueDate *string `json:"due_date"`
CreatedAt string `json:"created_at"`
UpdatedAt string `json:"updated_at"`
Reactions []IssueReactionResponse `json:"reactions,omitempty"`
Attachments []AttachmentResponse `json:"attachments,omitempty"`
// Labels are bulk-attached by list/detail endpoints so the client can render
// chips without an N+1 round-trip per row. Pointer + omitempty so paths that
// don't load labels (e.g. UpdateIssue, batch UpdateIssues, the issue:updated
// WS broadcast) emit no `labels` field at all — the client merge then
// preserves whatever labels are already in cache. nil pointer = "field
// absent, do not touch"; non-nil (incl. empty slice) = authoritative list.
Labels *[]LabelResponse `json:"labels,omitempty"`
Labels *[]LabelResponse `json:"labels,omitempty"`
}
func issueToResponse(i db.Issue, issuePrefix string) IssueResponse {
@@ -287,7 +286,7 @@ func buildSearchQuery(phrase string, terms []string, queryNum int, hasNum bool,
}
escapedPhrase := escapeLike(phrase)
phraseParam := nextArg(escapedPhrase) // $1
phraseParam := nextArg(escapedPhrase) // $1
phraseContains := "'%' || " + phraseParam + " || '%'"
phraseStartsWith := phraseParam + " || '%'"
@@ -859,11 +858,8 @@ func (h *Handler) ChildIssueProgress(w http.ResponseWriter, r *http.Request) {
// into a `multica issue create` invocation in the background; success and
// failure both surface as inbox notifications to the requester.
type QuickCreateIssueRequest struct {
AgentID string `json:"agent_id"`
Prompt string `json:"prompt"`
Priority *string `json:"priority,omitempty"`
DueDate *string `json:"due_date,omitempty"`
ProjectID *string `json:"project_id,omitempty"`
AgentID string `json:"agent_id"`
Prompt string `json:"prompt"`
}
// QuickCreateIssueResponse echoes the queued task id so the frontend can
@@ -887,43 +883,6 @@ func (h *Handler) QuickCreateIssue(w http.ResponseWriter, r *http.Request) {
if !ok {
return
}
if req.Priority != nil {
priority := strings.ToLower(strings.TrimSpace(*req.Priority))
if priority == "" {
req.Priority = nil
} else {
switch priority {
case "urgent", "high", "medium", "low":
req.Priority = &priority
default:
writeError(w, http.StatusBadRequest, "priority must be one of: urgent, high, medium, low")
return
}
}
}
if req.DueDate != nil {
dueDate := strings.TrimSpace(*req.DueDate)
if dueDate == "" {
req.DueDate = nil
} else {
if _, err := time.Parse(time.RFC3339, dueDate); err != nil {
writeError(w, http.StatusBadRequest, "invalid due_date format, expected RFC3339")
return
}
req.DueDate = &dueDate
}
}
if req.ProjectID != nil {
projectID := strings.TrimSpace(*req.ProjectID)
if projectID == "" {
req.ProjectID = nil
} else {
if _, ok := parseUUIDOrBadRequest(w, projectID, "project_id"); !ok {
return
}
req.ProjectID = &projectID
}
}
workspaceID := h.resolveWorkspaceID(r)
wsUUID, ok := parseUUIDOrBadRequest(w, workspaceID, "workspace_id")
@@ -984,11 +943,7 @@ func (h *Handler) QuickCreateIssue(w http.ResponseWriter, r *http.Request) {
return
}
task, err := h.TaskService.EnqueueQuickCreateTask(r.Context(), wsUUID, requesterUUID, agentUUID, prompt, func(qc *service.QuickCreateContext) {
qc.Priority = req.Priority
qc.DueDate = req.DueDate
qc.ProjectID = req.ProjectID
})
task, err := h.TaskService.EnqueueQuickCreateTask(r.Context(), wsUUID, requesterUUID, agentUUID, prompt)
if err != nil {
slog.Warn("quick-create enqueue failed", append(logger.RequestAttrs(r), "error", err)...)
writeError(w, http.StatusInternalServerError, "failed to enqueue quick-create task")
@@ -1087,16 +1042,16 @@ func readRuntimeCLIVersion(metadata []byte) string {
}
type CreateIssueRequest struct {
Title string `json:"title"`
Description *string `json:"description"`
Status string `json:"status"`
Priority string `json:"priority"`
AssigneeType *string `json:"assignee_type"`
AssigneeID *string `json:"assignee_id"`
ParentIssueID *string `json:"parent_issue_id"`
ProjectID *string `json:"project_id"`
DueDate *string `json:"due_date"`
AttachmentIDs []string `json:"attachment_ids,omitempty"`
Title string `json:"title"`
Description *string `json:"description"`
Status string `json:"status"`
Priority string `json:"priority"`
AssigneeType *string `json:"assignee_type"`
AssigneeID *string `json:"assignee_id"`
ParentIssueID *string `json:"parent_issue_id"`
ProjectID *string `json:"project_id"`
DueDate *string `json:"due_date"`
AttachmentIDs []string `json:"attachment_ids,omitempty"`
// OriginType / OriginID stamp the new issue with its provenance so
// platform-internal flows can deterministically locate it later. Only
// trusted callers should set these — currently the daemon CLI passes
@@ -1332,16 +1287,16 @@ func (h *Handler) CreateIssue(w http.ResponseWriter, r *http.Request) {
}
type UpdateIssueRequest struct {
Title *string `json:"title"`
Description *string `json:"description"`
Status *string `json:"status"`
Priority *string `json:"priority"`
AssigneeType *string `json:"assignee_type"`
AssigneeID *string `json:"assignee_id"`
Position *float64 `json:"position"`
DueDate *string `json:"due_date"`
ParentIssueID *string `json:"parent_issue_id"`
ProjectID *string `json:"project_id"`
Title *string `json:"title"`
Description *string `json:"description"`
Status *string `json:"status"`
Priority *string `json:"priority"`
AssigneeType *string `json:"assignee_type"`
AssigneeID *string `json:"assignee_id"`
Position *float64 `json:"position"`
DueDate *string `json:"due_date"`
ParentIssueID *string `json:"parent_issue_id"`
ProjectID *string `json:"project_id"`
}
func (h *Handler) UpdateIssue(w http.ResponseWriter, r *http.Request) {

View File

@@ -1,134 +0,0 @@
package handler
import (
"context"
"encoding/json"
"net/http"
"net/http/httptest"
"strings"
"testing"
"github.com/multica-ai/multica/server/internal/service"
)
func setHandlerTestRuntimeCLIVersion(t *testing.T, version string) {
t.Helper()
if _, err := testPool.Exec(context.Background(), `
UPDATE agent_runtime
SET metadata = jsonb_set(COALESCE(metadata, '{}'::jsonb), '{cli_version}', to_jsonb($2::text), true)
WHERE id = $1
`, handlerTestRuntimeID(t), version); err != nil {
t.Fatalf("set runtime cli_version: %v", err)
}
}
func TestQuickCreateIssue_StoresExplicitFields(t *testing.T) {
if testHandler == nil {
t.Skip("database not available")
}
ctx := context.Background()
setHandlerTestRuntimeCLIVersion(t, "0.2.24")
agentID := createHandlerTestAgent(t, "Quick Create Test Agent", nil)
const dueDate = "2025-06-01T00:00:00Z"
const projectID = "123e4567-e89b-12d3-a456-426614174000"
w := httptest.NewRecorder()
req := newRequest(http.MethodPost, "/api/issues/quick-create", map[string]any{
"agent_id": agentID,
"prompt": "Create a follow-up issue",
"priority": " HIGH ",
"due_date": " " + dueDate + " ",
"project_id": " " + projectID + " ",
})
testHandler.QuickCreateIssue(w, req)
if w.Code != http.StatusAccepted {
t.Fatalf("QuickCreateIssue: expected 202, got %d: %s", w.Code, w.Body.String())
}
var resp QuickCreateIssueResponse
if err := json.NewDecoder(w.Body).Decode(&resp); err != nil {
t.Fatalf("decode response: %v", err)
}
var contextJSON []byte
if err := testPool.QueryRow(ctx, `SELECT context FROM agent_task_queue WHERE id = $1`, resp.TaskID).Scan(&contextJSON); err != nil {
t.Fatalf("load queued task context: %v", err)
}
t.Cleanup(func() {
testPool.Exec(context.Background(), `DELETE FROM agent_task_queue WHERE id = $1`, resp.TaskID)
})
var qc service.QuickCreateContext
if err := json.Unmarshal(contextJSON, &qc); err != nil {
t.Fatalf("unmarshal quick-create context: %v", err)
}
if qc.Type != service.QuickCreateContextType {
t.Fatalf("context type = %q, want %q", qc.Type, service.QuickCreateContextType)
}
if qc.Priority == nil || *qc.Priority != "high" {
t.Fatalf("context priority = %v, want high", qc.Priority)
}
if qc.DueDate == nil || *qc.DueDate != dueDate {
t.Fatalf("context due_date = %v, want %s", qc.DueDate, dueDate)
}
if qc.ProjectID == nil || *qc.ProjectID != projectID {
t.Fatalf("context project_id = %v, want %s", qc.ProjectID, projectID)
}
}
func TestQuickCreateIssue_RejectsInvalidOptionalFields(t *testing.T) {
if testHandler == nil {
t.Skip("database not available")
}
setHandlerTestRuntimeCLIVersion(t, "0.2.24")
agentID := createHandlerTestAgent(t, "Quick Create Validation Agent", nil)
tests := []struct {
name string
body map[string]any
want string
}{
{
name: "invalid priority",
body: map[string]any{
"agent_id": agentID,
"prompt": "Create something",
"priority": "none",
},
want: "priority must be one of: urgent, high, medium, low",
},
{
name: "invalid due date",
body: map[string]any{
"agent_id": agentID,
"prompt": "Create something",
"due_date": "tomorrow",
},
want: "invalid due_date format, expected RFC3339",
},
{
name: "invalid project id",
body: map[string]any{
"agent_id": agentID,
"prompt": "Create something",
"project_id": "not-a-uuid",
},
want: "invalid project_id",
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
w := httptest.NewRecorder()
req := newRequest(http.MethodPost, "/api/issues/quick-create", tc.body)
testHandler.QuickCreateIssue(w, req)
if w.Code != http.StatusBadRequest {
t.Fatalf("QuickCreateIssue: expected 400, got %d: %s", w.Code, w.Body.String())
}
if body := w.Body.String(); body == "" || !strings.Contains(body, tc.want) {
t.Fatalf("response body %q does not contain %q", body, tc.want)
}
})
}
}

View File

@@ -208,13 +208,10 @@ func (s *TaskService) EnqueueTaskForMention(ctx context.Context, issue db.Issue,
// and switches to the quick-create prompt template; the completion path
// uses RequesterID + WorkspaceID to write the inbox notification.
type QuickCreateContext struct {
Type string `json:"type"`
Prompt string `json:"prompt"`
RequesterID string `json:"requester_id"`
WorkspaceID string `json:"workspace_id"`
Priority *string `json:"priority,omitempty"`
DueDate *string `json:"due_date,omitempty"`
ProjectID *string `json:"project_id,omitempty"`
Type string `json:"type"`
Prompt string `json:"prompt"`
RequesterID string `json:"requester_id"`
WorkspaceID string `json:"workspace_id"`
}
// QuickCreateContextType marks a task as a quick-create job.
@@ -226,7 +223,7 @@ const QuickCreateContextType = "quick_create"
// `multica issue create` call. Pre-validates that the agent is reachable
// (not archived, has a runtime) so the API can reject up-front rather than
// queue a task no one will ever claim.
func (s *TaskService) EnqueueQuickCreateTask(ctx context.Context, workspaceID, requesterID pgtype.UUID, agentID pgtype.UUID, prompt string, opts ...func(*QuickCreateContext)) (db.AgentTaskQueue, error) {
func (s *TaskService) EnqueueQuickCreateTask(ctx context.Context, workspaceID, requesterID pgtype.UUID, agentID pgtype.UUID, prompt string) (db.AgentTaskQueue, error) {
agent, err := s.Queries.GetAgent(ctx, agentID)
if err != nil {
return db.AgentTaskQueue{}, fmt.Errorf("load agent: %w", err)
@@ -244,9 +241,6 @@ func (s *TaskService) EnqueueQuickCreateTask(ctx context.Context, workspaceID, r
RequesterID: util.UUIDToString(requesterID),
WorkspaceID: util.UUIDToString(workspaceID),
}
for _, opt := range opts {
opt(&payload)
}
contextJSON, err := json.Marshal(payload)
if err != nil {
return db.AgentTaskQueue{}, fmt.Errorf("marshal quick-create context: %w", err)