mirror of
https://github.com/multica-ai/multica.git
synced 2026-06-17 11:48:42 +02:00
Compare commits
2 Commits
agent/lamb
...
fix/quick-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
cffa429c4d | ||
|
|
e28749a318 |
@@ -89,6 +89,12 @@ export interface AgentTask {
|
||||
* or deleted.
|
||||
*/
|
||||
trigger_summary?: string;
|
||||
/**
|
||||
* Server-computed source discriminator used by the activity row to label
|
||||
* tasks that have no linked issue (so e.g. quick-create tasks render
|
||||
* with a meaningful title instead of falling through to "Untracked").
|
||||
*/
|
||||
kind?: "comment" | "autopilot" | "chat" | "quick_create" | "direct";
|
||||
}
|
||||
|
||||
export interface Agent {
|
||||
|
||||
@@ -5,11 +5,16 @@ import { Toaster as Sonner, type ToasterProps } from "sonner"
|
||||
import { CircleCheckIcon, InfoIcon, TriangleAlertIcon, OctagonXIcon, Loader2Icon } from "lucide-react"
|
||||
|
||||
const Toaster = ({ ...props }: ToasterProps) => {
|
||||
const { theme = "system" } = useTheme()
|
||||
// Use `resolvedTheme` (the concrete "light" / "dark" value) instead of
|
||||
// `theme` (which can be "system"). When we forward "system", sonner reads
|
||||
// `prefers-color-scheme` itself, and the Electron renderer's media query
|
||||
// can disagree with next-themes' `html.dark` class — that's why the toast
|
||||
// sometimes rendered light on a dark UI.
|
||||
const { resolvedTheme = "system" } = useTheme()
|
||||
|
||||
return (
|
||||
<Sonner
|
||||
theme={theme as ToasterProps["theme"]}
|
||||
theme={resolvedTheme as ToasterProps["theme"]}
|
||||
className="toaster group"
|
||||
icons={{
|
||||
success: (
|
||||
|
||||
@@ -392,12 +392,25 @@ function TaskRow({
|
||||
}
|
||||
};
|
||||
|
||||
// Terminal states never use active wording. The server links the new
|
||||
// issue back to a quick-create task on completion (so most successful
|
||||
// rows transition to kind=direct on next refetch), but rows whose link
|
||||
// write failed — or whose agent never created the issue at all — would
|
||||
// otherwise sit on "Creating issue" forever.
|
||||
const isTerminalStatus =
|
||||
task.status === "completed" ||
|
||||
task.status === "failed" ||
|
||||
task.status === "cancelled";
|
||||
const sourceFallback = !hasIssue
|
||||
? task.chat_session_id
|
||||
? "Chat session"
|
||||
: task.autopilot_run_id
|
||||
? "Autopilot run"
|
||||
: "Untracked"
|
||||
? task.kind === "quick_create"
|
||||
? isTerminalStatus
|
||||
? "Quick create"
|
||||
: "Creating issue"
|
||||
: task.chat_session_id
|
||||
? "Chat session"
|
||||
: task.autopilot_run_id
|
||||
? "Autopilot run"
|
||||
: "Untracked"
|
||||
: null;
|
||||
|
||||
// Origin marker — issue / chat / autopilot / untracked. The issue
|
||||
|
||||
@@ -264,7 +264,7 @@ export function AgentCreatePanel({
|
||||
<ContentEditor
|
||||
ref={editorRef}
|
||||
defaultValue={initialPrompt}
|
||||
placeholder='Describe the issue, e.g. "fix inbox loading slowness, assign to naiyuan, P1"'
|
||||
placeholder='Tell the agent what to do, e.g. "let Bohan fix the inbox loading slowness in the Web project"'
|
||||
onUpdate={(md) => setHasContent(md.trim().length > 0)}
|
||||
onUploadFile={handleUploadFile}
|
||||
onSubmit={submit}
|
||||
|
||||
@@ -156,6 +156,7 @@ type AgentTaskResponse struct {
|
||||
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
|
||||
@@ -204,9 +205,32 @@ func taskToResponse(t db.AgentTaskQueue) AgentTaskResponse {
|
||||
// with issue_id = "" once a task has no linked issue.
|
||||
ChatSessionID: uuidToString(t.ChatSessionID),
|
||||
AutopilotRunID: uuidToString(t.AutopilotRunID),
|
||||
Kind: computeTaskKind(t),
|
||||
}
|
||||
}
|
||||
|
||||
// computeTaskKind picks the source-discriminator string the activity UI uses
|
||||
// to choose how to render a task row. Computed from the existing FK shape so
|
||||
// no extra DB lookup is needed: chat / autopilot / comment-on-issue (any
|
||||
// triggered task with both an issue_id and trigger_comment_id) / quick_create
|
||||
// (no linked source — the agent is creating the issue itself) / direct
|
||||
// (assignee-driven task on an existing issue).
|
||||
func computeTaskKind(t db.AgentTaskQueue) string {
|
||||
if uuidToString(t.ChatSessionID) != "" {
|
||||
return "chat"
|
||||
}
|
||||
if uuidToString(t.AutopilotRunID) != "" {
|
||||
return "autopilot"
|
||||
}
|
||||
if uuidToString(t.IssueID) == "" {
|
||||
return "quick_create"
|
||||
}
|
||||
if uuidToString(t.TriggerCommentID) != "" {
|
||||
return "comment"
|
||||
}
|
||||
return "direct"
|
||||
}
|
||||
|
||||
func (h *Handler) ListAgents(w http.ResponseWriter, r *http.Request) {
|
||||
workspaceID := h.resolveWorkspaceID(r)
|
||||
member, ok := h.workspaceMember(w, r, workspaceID)
|
||||
|
||||
@@ -1347,6 +1347,22 @@ func (s *TaskService) notifyQuickCreateCompleted(ctx context.Context, task db.Ag
|
||||
s.notifyQuickCreateFailed(ctx, task, qc, "agent finished without creating an issue")
|
||||
return
|
||||
}
|
||||
|
||||
// Link the new issue back to this task so subsequent reads of the task
|
||||
// (Activity tab, Recent work, etc.) render it as a normal issue task
|
||||
// (kind = "direct") instead of staying on the "Creating issue" active-
|
||||
// wording label. Best-effort: a write failure here doesn't block the
|
||||
// inbox notification, which is the more important signal to the user.
|
||||
if err := s.Queries.LinkTaskToIssue(ctx, db.LinkTaskToIssueParams{
|
||||
ID: task.ID,
|
||||
IssueID: issue.ID,
|
||||
}); err != nil {
|
||||
slog.Warn("quick-create completion: link task→issue failed",
|
||||
"task_id", util.UUIDToString(task.ID),
|
||||
"issue_id", util.UUIDToString(issue.ID),
|
||||
"error", err,
|
||||
)
|
||||
}
|
||||
prefix := s.getIssuePrefix(workspaceID)
|
||||
identifier := fmt.Sprintf("%s-%d", prefix, issue.Number)
|
||||
details, _ := json.Marshal(map[string]any{
|
||||
|
||||
@@ -620,7 +620,7 @@ func (q *Queries) CreateAgentTask(ctx context.Context, arg CreateAgentTaskParams
|
||||
const createQuickCreateTask = `-- name: CreateQuickCreateTask :one
|
||||
INSERT INTO agent_task_queue (agent_id, runtime_id, issue_id, status, priority, context)
|
||||
VALUES ($1, $2, NULL, 'queued', $3, $4)
|
||||
RETURNING id, agent_id, issue_id, status, priority, dispatched_at, started_at, completed_at, result, error, created_at, context, runtime_id, session_id, work_dir, trigger_comment_id, chat_session_id, autopilot_run_id, attempt, max_attempts, parent_task_id, failure_reason, last_heartbeat_at
|
||||
RETURNING id, agent_id, issue_id, status, priority, dispatched_at, started_at, completed_at, result, error, created_at, context, runtime_id, session_id, work_dir, trigger_comment_id, chat_session_id, autopilot_run_id, attempt, max_attempts, parent_task_id, failure_reason, last_heartbeat_at, trigger_summary
|
||||
`
|
||||
|
||||
type CreateQuickCreateTaskParams struct {
|
||||
@@ -665,6 +665,7 @@ func (q *Queries) CreateQuickCreateTask(ctx context.Context, arg CreateQuickCrea
|
||||
&i.ParentTaskID,
|
||||
&i.FailureReason,
|
||||
&i.LastHeartbeatAt,
|
||||
&i.TriggerSummary,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
@@ -1144,6 +1145,27 @@ func (q *Queries) HasPendingTaskForIssueAndAgent(ctx context.Context, arg HasPen
|
||||
return has_pending, err
|
||||
}
|
||||
|
||||
const linkTaskToIssue = `-- name: LinkTaskToIssue :exec
|
||||
UPDATE agent_task_queue
|
||||
SET issue_id = $2
|
||||
WHERE id = $1 AND issue_id IS NULL
|
||||
`
|
||||
|
||||
type LinkTaskToIssueParams struct {
|
||||
ID pgtype.UUID `json:"id"`
|
||||
IssueID pgtype.UUID `json:"issue_id"`
|
||||
}
|
||||
|
||||
// Attaches the issue a quick-create task produced back to the task row, once
|
||||
// the agent has finished and the issue exists. Guarded by `issue_id IS NULL`
|
||||
// so this never overwrites an issue id that was set at task creation (only
|
||||
// quick-create tasks land here unset). Fixes the activity row staying on
|
||||
// "Creating issue" forever after completion.
|
||||
func (q *Queries) LinkTaskToIssue(ctx context.Context, arg LinkTaskToIssueParams) error {
|
||||
_, err := q.db.Exec(ctx, linkTaskToIssue, arg.ID, arg.IssueID)
|
||||
return err
|
||||
}
|
||||
|
||||
const listActiveTasksByIssue = `-- name: ListActiveTasksByIssue :many
|
||||
SELECT id, agent_id, issue_id, status, priority, dispatched_at, started_at, completed_at, result, error, created_at, context, runtime_id, session_id, work_dir, trigger_comment_id, chat_session_id, autopilot_run_id, attempt, max_attempts, parent_task_id, failure_reason, last_heartbeat_at, trigger_summary FROM agent_task_queue
|
||||
WHERE issue_id = $1 AND status IN ('dispatched', 'running')
|
||||
|
||||
@@ -77,6 +77,16 @@ INSERT INTO agent_task_queue (agent_id, runtime_id, issue_id, status, priority,
|
||||
VALUES ($1, $2, NULL, 'queued', $3, $4)
|
||||
RETURNING *;
|
||||
|
||||
-- name: LinkTaskToIssue :exec
|
||||
-- Attaches the issue a quick-create task produced back to the task row, once
|
||||
-- the agent has finished and the issue exists. Guarded by `issue_id IS NULL`
|
||||
-- so this never overwrites an issue id that was set at task creation (only
|
||||
-- quick-create tasks land here unset). Fixes the activity row staying on
|
||||
-- "Creating issue" forever after completion.
|
||||
UPDATE agent_task_queue
|
||||
SET issue_id = $2
|
||||
WHERE id = $1 AND issue_id IS NULL;
|
||||
|
||||
-- name: CreateRetryTask :one
|
||||
-- Clones a parent task into a fresh queued attempt. Carries forward the
|
||||
-- agent's resume context (session_id/work_dir) so the child can continue
|
||||
|
||||
Reference in New Issue
Block a user