From 0e28c0b45e963817f38ec8a8bafbaf8f35709d1a Mon Sep 17 00:00:00 2001 From: Algis Dumbris Date: Tue, 24 Mar 2026 13:00:28 +0200 Subject: [PATCH] =?UTF-8?q?feat:=20fix=20attachment=20handling=20=E2=80=94?= =?UTF-8?q?=20display=20in=20DMs,=20enrich=20in=20MCP,=20allow=20all=20fil?= =?UTF-8?q?e=20types?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Show attachment previews on DM messages (was missing, only channels had it) - Add file upload button to DM compose bar with paperclip icon - Enrich messages with attachment data in all MCP bridge functions (read_inbox, claim_messages, search, channel_messages, list_by_state) - Remove file type restrictions — allow any file type, keep 50MB size limit - Rebuild web dist Co-Authored-By: Claude Opus 4.6 (1M context) --- internal/attachments/mime.go | 24 +----- internal/attachments/mime_test.go | 8 +- internal/attachments/service.go | 5 -- internal/attachments/service_test.go | 8 +- internal/mcp/bridge.go | 51 +++++++++---- internal/web/dist/index.html | 22 +++--- web/src/lib/components/ComposeForm.svelte | 3 - web/src/routes/channels/[name]/+page.svelte | 1 - web/src/routes/dm/[name]/+page.svelte | 85 ++++++++++++++++++++- 9 files changed, 141 insertions(+), 66 deletions(-) diff --git a/internal/attachments/mime.go b/internal/attachments/mime.go index 90f7539..ed42dd1 100644 --- a/internal/attachments/mime.go +++ b/internal/attachments/mime.go @@ -119,26 +119,8 @@ func IsImageType(mimeType string) bool { return imageTypes[mimeType] } -// IsAllowedType returns true if the MIME type is allowed for upload. -// Allowed: image/*, application/pdf, text/*. +// IsAllowedType returns true for all MIME types. Any file type is allowed; +// only size is restricted (50 MB max). func IsAllowedType(mimeType string) bool { - // Normalize: strip parameters like "; charset=utf-8". - base := mimeType - if idx := strings.Index(mimeType, ";"); idx >= 0 { - base = strings.TrimSpace(mimeType[:idx]) - } - if strings.HasPrefix(base, "image/") { - return true - } - if base == "application/pdf" { - return true - } - if strings.HasPrefix(base, "text/") { - return true - } - // Also allow JSON and XML which may be detected as application/* - if base == "application/json" || base == "application/xml" { - return true - } - return false + return true } diff --git a/internal/attachments/mime_test.go b/internal/attachments/mime_test.go index 142e674..25c2b06 100644 --- a/internal/attachments/mime_test.go +++ b/internal/attachments/mime_test.go @@ -167,10 +167,10 @@ func TestIsAllowedType(t *testing.T) { {"text/csv", true}, {"text/plain; charset=utf-8", true}, {"application/json", true}, - {"application/octet-stream", false}, - {"application/zip", false}, - {"application/x-executable", false}, - {"video/mp4", false}, + {"application/octet-stream", true}, + {"application/zip", true}, + {"application/x-executable", true}, + {"video/mp4", true}, } for _, tt := range tests { diff --git a/internal/attachments/service.go b/internal/attachments/service.go index 1d64773..6866f86 100644 --- a/internal/attachments/service.go +++ b/internal/attachments/service.go @@ -60,11 +60,6 @@ func (s *Service) Upload(ctx context.Context, req UploadRequest) (*UploadResult, mimeType = DetectMIMEType(sniffBuf, req.Filename) } - // Validate file type against allowlist. - if !IsAllowedType(mimeType) { - return nil, ErrUnsupportedType - } - // Assign default filename if missing. filename := req.Filename if filename == "" { diff --git a/internal/attachments/service_test.go b/internal/attachments/service_test.go index b5dd1df..2dcb1b6 100644 --- a/internal/attachments/service_test.go +++ b/internal/attachments/service_test.go @@ -236,18 +236,18 @@ func TestService_Upload_FileTypeValidation(t *testing.T) { wantErr: nil, }, { - name: "invalid type zip rejected", + name: "zip upload allowed", content: []byte("not real zip content"), filename: "archive.zip", mimeType: "application/zip", - wantErr: ErrUnsupportedType, + wantErr: nil, }, { - name: "invalid type executable rejected", + name: "executable upload allowed", content: []byte{0x7f, 0x45, 0x4c, 0x46}, filename: "program.exe", mimeType: "application/x-executable", - wantErr: ErrUnsupportedType, + wantErr: nil, }, } diff --git a/internal/mcp/bridge.go b/internal/mcp/bridge.go index 2683aa7..7ccd095 100644 --- a/internal/mcp/bridge.go +++ b/internal/mcp/bridge.go @@ -216,6 +216,8 @@ func (b *ServiceBridge) callReadInbox(ctx context.Context, args map[string]any) return nil, err } + b.msgService.EnrichMessages(ctx, page.Messages) + return map[string]any{ "messages": page.Messages, "count": len(page.Messages), @@ -233,6 +235,8 @@ func (b *ServiceBridge) callClaimMessages(ctx context.Context, args map[string]a return nil, err } + b.msgService.EnrichMessages(ctx, messages) + return map[string]any{ "messages": messages, "count": len(messages), @@ -287,6 +291,15 @@ func (b *ServiceBridge) callSearchMessages(ctx context.Context, args map[string] return nil, err } + // Enrich messages with attachments + searchMsgs := make([]*messaging.Message, 0, len(resp.Results)) + for _, r := range resp.Results { + if r.Message != nil { + searchMsgs = append(searchMsgs, r.Message) + } + } + b.msgService.EnrichMessages(ctx, searchMsgs) + resultMsgs := make([]map[string]any, len(resp.Results)) for i, r := range resp.Results { entry := map[string]any{ @@ -326,6 +339,8 @@ func (b *ServiceBridge) callSearchMessages(ctx context.Context, args map[string] return nil, err } + b.msgService.EnrichMessages(ctx, page.Messages) + return map[string]any{ "messages": page.Messages, "count": len(page.Messages), @@ -553,15 +568,18 @@ func (b *ServiceBridge) callGetChannelMessages(ctx context.Context, args map[str return nil, err } + b.msgService.EnrichMessages(ctx, page.Messages) + result := make([]map[string]any, len(page.Messages)) for i, msg := range page.Messages { result[i] = map[string]any{ - "id": msg.ID, - "from": msg.FromAgent, - "body": msg.Body, - "priority": msg.Priority, - "status": msg.Status, - "created_at": msg.CreatedAt, + "id": msg.ID, + "from": msg.FromAgent, + "body": msg.Body, + "priority": msg.Priority, + "status": msg.Status, + "created_at": msg.CreatedAt, + "attachments": msg.Attachments, } if len(msg.Metadata) > 0 { result[i]["metadata"] = msg.Metadata @@ -1119,23 +1137,30 @@ func (b *ServiceBridge) callListByState(ctx context.Context, args map[string]any if maxBodyLen <= 0 { maxBodyLen = 500 } - var messages []map[string]any + var msgSlice []*messaging.Message for _, id := range pageIDs { msg, err := b.msgService.GetMessageByID(ctx, id) if err != nil { continue } + msgSlice = append(msgSlice, msg) + } + b.msgService.EnrichMessages(ctx, msgSlice) + + var messages []map[string]any + for _, msg := range msgSlice { body := msg.Body if len(body) > maxBodyLen { body = body[:maxBodyLen] + "..." } messages = append(messages, map[string]any{ - "id": msg.ID, - "from_agent": msg.FromAgent, - "body": body, - "priority": msg.Priority, - "created_at": msg.CreatedAt, - "reply_to": msg.ReplyTo, + "id": msg.ID, + "from_agent": msg.FromAgent, + "body": body, + "priority": msg.Priority, + "created_at": msg.CreatedAt, + "reply_to": msg.ReplyTo, + "attachments": msg.Attachments, }) } resp["messages"] = messages diff --git a/internal/web/dist/index.html b/internal/web/dist/index.html index 789bdd7..2d8b7fe 100644 --- a/internal/web/dist/index.html +++ b/internal/web/dist/index.html @@ -11,30 +11,30 @@ - - + + - - - - - - + + + + + +
@@ -248,6 +285,13 @@ {/if}
+ {#if msg.attachments?.length > 0} +
+ {#each msg.attachments as att (att.hash)} + + {/each} +
+ {/if} {#if msg.workflow_state} {/if} @@ -286,11 +330,34 @@ {#if sendError}
{sendError}
{/if} + {#if uploadError} +
{uploadError}
+ {/if} {#if ownAgents.length === 0}
Register an agent to send messages
{:else} + + + {#if uploadedAttachments.length > 0} +
+ {#each uploadedAttachments as att (att.hash)} + + + {att.original_filename} + ({formatFileSize(att.size)}) + + + {/each} +
+ {/if} + {#if uploading} +
+ + Uploading... +
+ {/if}
+