From 9c249f07705acdf7eb5d64b30a743354216ab9fb Mon Sep 17 00:00:00 2001 From: yushen Date: Wed, 1 Apr 2026 18:10:43 +0800 Subject: [PATCH] feat(server,cli): improve attachment support across issue and comment APIs - Add --attachment flag to `multica issue create` CLI command - Fix CreateComment response to include linked attachments instead of empty array - Include attachments inline in GetIssue API response (matching Jira/ClickUp pattern) Co-Authored-By: Claude Opus 4.6 (1M context) --- server/cmd/multica/cmd_issue.go | 22 +++++++++++++++++++++- server/internal/handler/comment.go | 4 +++- server/internal/handler/issue.go | 13 +++++++++++++ 3 files changed, 37 insertions(+), 2 deletions(-) diff --git a/server/cmd/multica/cmd_issue.go b/server/cmd/multica/cmd_issue.go index 9ce65c1e..9f341347 100644 --- a/server/cmd/multica/cmd_issue.go +++ b/server/cmd/multica/cmd_issue.go @@ -123,6 +123,7 @@ func init() { issueCreateCmd.Flags().String("parent", "", "Parent issue ID") issueCreateCmd.Flags().String("due-date", "", "Due date (RFC3339 format)") issueCreateCmd.Flags().String("output", "json", "Output format: table or json") + issueCreateCmd.Flags().StringSlice("attachment", nil, "File path(s) to attach (can be specified multiple times)") // issue update issueUpdateCmd.Flags().String("title", "", "New title") @@ -276,7 +277,13 @@ func runIssueCreate(cmd *cobra.Command, _ []string) error { return err } - ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second) + // Use a longer timeout when attachments are present (file uploads can be slow). + timeout := 15 * time.Second + attachments, _ := cmd.Flags().GetStringSlice("attachment") + if len(attachments) > 0 { + timeout = 60 * time.Second + } + ctx, cancel := context.WithTimeout(context.Background(), timeout) defer cancel() body := map[string]any{"title": title} @@ -309,6 +316,19 @@ func runIssueCreate(cmd *cobra.Command, _ []string) error { return fmt.Errorf("create issue: %w", err) } + // Upload attachments and link them to the newly created issue. + issueID := strVal(result, "id") + for _, filePath := range attachments { + data, readErr := os.ReadFile(filePath) + if readErr != nil { + return fmt.Errorf("read attachment %s: %w", filePath, readErr) + } + if _, uploadErr := client.UploadFile(ctx, data, filePath, issueID); uploadErr != nil { + return fmt.Errorf("upload attachment %s: %w", filePath, uploadErr) + } + fmt.Fprintf(os.Stderr, "Uploaded %s\n", filePath) + } + output, _ := cmd.Flags().GetString("output") if output == "table" { headers := []string{"ID", "TITLE", "STATUS", "PRIORITY"} diff --git a/server/internal/handler/comment.go b/server/internal/handler/comment.go index 4815304f..215fedf4 100644 --- a/server/internal/handler/comment.go +++ b/server/internal/handler/comment.go @@ -148,7 +148,9 @@ func (h *Handler) CreateComment(w http.ResponseWriter, r *http.Request) { h.linkAttachmentsByIDs(r.Context(), comment.ID, issue.ID, req.AttachmentIDs) } - resp := commentToResponse(comment, nil, nil) + // Fetch linked attachments so the response includes them. + groupedAtt := h.groupAttachments(r, []pgtype.UUID{comment.ID}) + resp := commentToResponse(comment, nil, groupedAtt[uuidToString(comment.ID)]) slog.Info("comment created", append(logger.RequestAttrs(r), "comment_id", uuidToString(comment.ID), "issue_id", issueID)...) h.publish(protocol.EventCommentCreated, uuidToString(issue.WorkspaceID), authorType, authorID, map[string]any{ "comment": resp, diff --git a/server/internal/handler/issue.go b/server/internal/handler/issue.go index 0cf7dc17..0c5a0d6a 100644 --- a/server/internal/handler/issue.go +++ b/server/internal/handler/issue.go @@ -36,6 +36,7 @@ type IssueResponse struct { CreatedAt string `json:"created_at"` UpdatedAt string `json:"updated_at"` Reactions []IssueReactionResponse `json:"reactions,omitempty"` + Attachments []AttachmentResponse `json:"attachments,omitempty"` } type agentTriggerSnapshot struct { @@ -142,6 +143,18 @@ func (h *Handler) GetIssue(w http.ResponseWriter, r *http.Request) { } } + // Fetch issue-level attachments. + attachments, err := h.Queries.ListAttachmentsByIssue(r.Context(), db.ListAttachmentsByIssueParams{ + IssueID: issue.ID, + WorkspaceID: issue.WorkspaceID, + }) + if err == nil && len(attachments) > 0 { + resp.Attachments = make([]AttachmentResponse, len(attachments)) + for i, a := range attachments { + resp.Attachments[i] = h.attachmentToResponse(a) + } + } + writeJSON(w, http.StatusOK, resp) }