Compare commits

...

2 Commits

Author SHA1 Message Date
Jiang Bohan
cffa429c4d fix(quick-create): link new issue back to task on completion
Addresses the review on PR #1831: completed quick-create tasks were
left with issue_id=NULL forever, so the activity row stayed on
"Creating issue" instead of transitioning to the normal MUL-XXX +
title rendering once the agent finished.

- Server: notifyQuickCreateCompleted now writes the resolved issue id
  back to agent_task_queue.issue_id via a new LinkTaskToIssue query
  (guarded by `issue_id IS NULL` so it only ever fills the unset
  quick-create case). Best-effort: a write failure logs but doesn't
  block the inbox notification.
- Frontend: defensive wording fallback — kind=quick_create rows in
  terminal status (completed/failed/cancelled) now render as
  "Quick create" instead of the active "Creating issue" label,
  covering rows whose link write failed or whose agent never
  produced an issue at all.
2026-04-29 15:37:15 +08:00
Jiang Bohan
e28749a318 fix: polish quick-create UX (kind labeling, dark toast, placeholder)
Three small fixes shaken out from using the agent-create flow:

- AgentTaskResponse now carries a `kind` discriminator
  ("comment" | "autopilot" | "chat" | "quick_create" | "direct"), computed
  from the existing FK shape with no extra DB access. The Activity row
  uses it to label quick-create tasks as "Creating issue" instead of
  falling through to the generic "Untracked" — once the agent finishes
  and the new issue is linked, the row transitions to the normal
  identifier+title display.

- Sonner Toaster reads `resolvedTheme` instead of `theme`, so toasts
  follow the actual dark/light state. Forwarding "system" let sonner
  pick its own answer from `prefers-color-scheme`, which in the Electron
  renderer can disagree with next-themes' `html.dark` class — the toast
  rendered light on a dark UI.

- Agent-create placeholder rephrased to a more conversational example
  with a project reference: "let Bohan fix the inbox loading slowness
  in the Web project". Drops the priority hint (priority isn't widely
  used) and matches how people actually instruct the agent.
2026-04-29 15:27:21 +08:00
8 changed files with 105 additions and 9 deletions

View File

@@ -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 {

View File

@@ -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: (

View File

@@ -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

View File

@@ -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}

View File

@@ -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)

View File

@@ -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{

View File

@@ -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')

View File

@@ -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