Skip to content

feat: embed videos inline in Slack responses#389

Merged
sweetmantech merged 3 commits intotestfrom
feature/slack-embedded-video
Apr 1, 2026
Merged

feat: embed videos inline in Slack responses#389
sweetmantech merged 3 commits intotestfrom
feature/slack-embedded-video

Conversation

@recoup-coding-agent
Copy link
Copy Markdown
Collaborator

@recoup-coding-agent recoup-coding-agent commented Apr 1, 2026

Summary

  • When the content agent finishes generating a video, it now downloads the video and uploads it as a file to Slack so it appears inline in the thread — instead of just posting a URL link
  • New downloadVideoBuffer utility fetches the video data from the callback URL
  • Falls back to the old URL-link behavior if the video download fails (network error, 404, etc.)
  • Each video is posted individually (one thread.post() per video) so Slack renders them as separate embedded media

Test plan

  • 4 new tests for downloadVideoBuffer (success, 404, network error, URL parsing)
  • 6 new tests for handleContentAgentCallback (file upload, fallback, multi-video, no caption, idempotency, partial failures)
  • All 57 content agent tests pass
  • All 1691 project tests pass (8 pre-existing failures in unrelated files)
  • Lint clean on all changed files
  • Manual test: trigger content generation in Slack and verify video appears inline

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Videos are now attached as downloadable files (with consistent filenames and optional "Video N" labels) and include captions when available.
    • If a download fails, the system posts the original video URL instead.
    • A final summary reports the number of failed runs.
  • Refactor

    • Video result posting logic has been centralized into a single helper for clearer flow and maintainability.

When the content agent finishes generating a video, download it and
post it as a file upload so it appears inline in the Slack thread.
Falls back to the old URL-link behavior if the download fails.

- New downloadVideoBuffer utility to fetch video data
- Updated handleContentAgentCallback to use thread.post({ files })
- 14 new tests (4 downloadVideoBuffer + 10 callback handler)

Co-Authored-By: Paperclip <noreply@paperclip.ing>
@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Apr 1, 2026

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: c59885cc-b391-47e1-9215-fd9137ac7ea1

📥 Commits

Reviewing files that changed from the base of the PR and between fb4f0fa and db93139.

⛔ Files ignored due to path filters (2)
  • lib/agents/content/__tests__/getFilenameFromUrl.test.ts is excluded by !**/*.test.*, !**/__tests__/** and included by lib/**
  • lib/agents/content/__tests__/handleContentAgentCallback.test.ts is excluded by !**/*.test.*, !**/__tests__/** and included by lib/**
📒 Files selected for processing (1)
  • lib/agents/content/postVideoResults.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • lib/agents/content/postVideoResults.ts

📝 Walkthrough

Walkthrough

Adds utilities to download video content and derive filenames, introduces postVideoResults to download and post videos (attachments or fallback links), and updates the content agent callback to delegate result posting to the new helper.

Changes

Cohort / File(s) Summary
Video download utility
lib/agents/content/downloadVideoBuffer.ts
New exported async function that fetches a URL, validates response.ok, converts the response to a Node.js Buffer, logs errors, and returns null on failure.
Filename helper
lib/agents/content/getFilenameFromUrl.ts
New exported helper that parses a URL pathname and returns the last path segment if it contains an extension, otherwise returns the fallback video.mp4.
Result posting helper
lib/agents/content/postVideoResults.ts
New exported function that concurrently downloads videos via downloadVideoBuffer, then posts results sequentially to preserve order: uploads video/mp4 attachments when downloads succeed (using derived filenames) and posts markdown fallback messages when downloads fail; posts a final failed-run summary when applicable.
Content agent callback update
lib/agents/content/handleContentAgentCallback.ts
Replaces inline video-result posting with a call to postVideoResults(thread, videos, failed.length), delegating download/attachment logic to the new helper.

Sequence Diagram

sequenceDiagram
    participant Agent as Content Agent Callback
    participant Poster as postVideoResults()
    participant Downloader as downloadVideoBuffer()
    participant URL as External URL
    participant Thread as Thread/Post API

    Agent->>Poster: postVideoResults(thread, videos, failedCount)
    Note over Poster,Downloader: Poster starts concurrent downloads
    Poster->>Downloader: downloadVideoBuffer(url) [concurrent]
    Downloader->>URL: fetch(url)
    URL-->>Downloader: HTTP response
    alt response.ok & buffer created
        Downloader-->>Poster: Buffer
        Poster->>Poster: getFilenameFromUrl(url)
        Poster->>Thread: upload attachment (buffer, filename, mimeType: video/mp4)
    else fetch or conversion failed
        Downloader-->>Poster: null
        Poster->>Thread: post markdown fallback (videoUrl + optional caption)
    end
    Poster->>Thread: if failedCount>0 post summary text
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Poem

🎬 A URL whispered, "Send me light,"
Buffers gathered through day and night,
Filenames chosen, attachments spun,
Fallback links when downloads run undone,
Posts arrive — the job is done. ✨

🚥 Pre-merge checks | ✅ 1
✅ Passed checks (1 passed)
Check name Status Explanation
Solid & Clean Code ✅ Passed PR demonstrates solid adherence to SOLID principles and clean code practices with focused utility functions, proper error handling, comprehensive test coverage, and self-documenting code.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feature/slack-embedded-video

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@vercel
Copy link
Copy Markdown
Contributor

vercel bot commented Apr 1, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
recoup-api Ready Ready Preview Apr 1, 2026 6:47pm

Request Review

* @param url - The video URL
* @returns The extracted filename
*/
function getFilenameFromUrl(url: string): string {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

SRP - new file for getFilenameFromUrl.

Comment on lines +83 to +109
for (let i = 0; i < videos.length; i++) {
const v = videos[i];
const videoBuffer = await downloadVideoBuffer(v.videoUrl!);

if (videoBuffer) {
const filename = getFilenameFromUrl(v.videoUrl!);
const label = videos.length > 1 ? `**Video ${i + 1}**` : "";
const caption = v.captionText ? `> ${v.captionText}` : "";
const markdown = [label, caption].filter(Boolean).join("\n");

await thread.post({
markdown: markdown || filename,
files: [
{
data: videoBuffer,
filename,
mimeType: "video/mp4",
},
],
});
} else {
// Fallback to URL link if download fails
const label = videos.length > 1 ? `**Video ${i + 1}:** ` : "";
const caption = v.captionText ? `\n> ${v.captionText}` : "";
await thread.post(`${label}${v.videoUrl}${caption}`);
}
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OCP - new file for loop / embed video logic.

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
lib/agents/content/handleContentAgentCallback.ts (1)

83-118: ⚠️ Potential issue | 🔴 Critical

The new per-video posts are not actually idempotent yet.

Lines 93-113 perform non-idempotent thread.post() calls while the thread stays running until Line 118. If an upload fails mid-loop, or a duplicate delivery arrives before the final state write, the callback will replay videos that were already posted. Persist per-video progress (URL/index/result ID) and skip already-sent items on retry.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lib/agents/content/handleContentAgentCallback.ts` around lines 83 - 118, The
loop that posts each video (using videos[], downloadVideoBuffer,
getFilenameFromUrl and thread.post) is not idempotent: if the callback retries
before thread.setState({ status: "completed" }) the same video posts can be
duplicated; persist per-video progress (e.g., by recording each video's index or
URL and final post result ID to the thread or a durable store as you
successfully post) and on start of the handler skip any videos already recorded
as posted, only attempting download/post for items not yet marked, and ensure
you write per-video success before continuing to the next item so retries resume
safely without duplicates.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@lib/agents/content/downloadVideoBuffer.ts`:
- Around line 11-13: The error log in downloadVideoBuffer.ts currently prints
the full signed URL (variable url) on failure; change the console.error in the
response.ok check to redact the query string by parsing url with the URL
constructor and logging only the origin and pathname (or host+pathname) plus the
HTTP status. Locate the failing-block that checks response.ok (references:
response, url, downloadVideoBuffer) and replace the message so it does not
include url.search/query parameters but still includes clear context and the
status code.
- Around line 9-17: The downloadVideoBuffer function currently uses fetch(url)
and response.arrayBuffer() without timeout or size limits and logs the full URL;
add an AbortController with a timeout (same pattern used in
lib/serpapi/searchGoogleImages.ts) and pass controller.signal into fetch(url, {
signal }), and enforce a MAX_BYTES limit (like
lib/youtube/getResizedImageBuffer.ts) by streaming response.body with a reader,
accumulating chunks until MAX_BYTES and aborting/returning null if exceeded;
replace the console.error that prints the full URL with a redacted message that
omits query params (log HTTP status and that the URL was redacted) and ensure
you clear the timeout on success or failure.

In `@lib/agents/content/handleContentAgentCallback.ts`:
- Around line 111-115: The failed-run summary (check of failed.length and
thread.post of `_${failed.length} run(s) failed._`) is currently inside the
"some videos succeeded" branch and gets skipped when there are zero successful
videos; move that check out of the success-video branch so it runs
unconditionally after handling the completed callback. In the
handleContentAgentCallback flow, ensure you still post either the success-video
message or the "Content generation finished but no videos were produced."
message, then always run the failed.length > 0 check and call thread.post with
the failure summary so failures are reported even when no videos were produced.

---

Outside diff comments:
In `@lib/agents/content/handleContentAgentCallback.ts`:
- Around line 83-118: The loop that posts each video (using videos[],
downloadVideoBuffer, getFilenameFromUrl and thread.post) is not idempotent: if
the callback retries before thread.setState({ status: "completed" }) the same
video posts can be duplicated; persist per-video progress (e.g., by recording
each video's index or URL and final post result ID to the thread or a durable
store as you successfully post) and on start of the handler skip any videos
already recorded as posted, only attempting download/post for items not yet
marked, and ensure you write per-video success before continuing to the next
item so retries resume safely without duplicates.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 5650f52d-55fe-4185-9632-317305beb5ef

📥 Commits

Reviewing files that changed from the base of the PR and between 6f9494b and 7b0f463.

⛔ Files ignored due to path filters (2)
  • lib/agents/content/__tests__/downloadVideoBuffer.test.ts is excluded by !**/*.test.*, !**/__tests__/** and included by lib/**
  • lib/agents/content/__tests__/handleContentAgentCallback.test.ts is excluded by !**/*.test.*, !**/__tests__/** and included by lib/**
📒 Files selected for processing (2)
  • lib/agents/content/downloadVideoBuffer.ts
  • lib/agents/content/handleContentAgentCallback.ts

Comment on lines +9 to +17
const response = await fetch(url);

if (!response.ok) {
console.error(`Failed to download video: HTTP ${response.status} from ${url}`);
return null;
}

const arrayBuffer = await response.arrayBuffer();
return Buffer.from(arrayBuffer);
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

# First, let's find and read the downloadVideoBuffer.ts file
fd downloadVideoBuffer.ts

Repository: recoupable/api

Length of output: 99


🏁 Script executed:

# Also check for the file that uses it
fd handleContentAgentCallback.ts

Repository: recoupable/api

Length of output: 106


🏁 Script executed:

# Get the file structure to understand the context better
git ls-files lib/agents/content/

Repository: recoupable/api

Length of output: 1105


🏁 Script executed:

cat -n lib/agents/content/downloadVideoBuffer.ts

Repository: recoupable/api

Length of output: 854


🏁 Script executed:

cat -n lib/agents/content/handleContentAgentCallback.ts

Repository: recoupable/api

Length of output: 5572


🏁 Script executed:

# Check if there are any existing timeout or size guards elsewhere in the codebase
rg "timeout|maxSize|max-size|MAX_SIZE" lib/agents/content/ -A 2 -B 2

Repository: recoupable/api

Length of output: 1861


🏁 Script executed:

# Check if there are any fetch configuration or timeout utilities elsewhere in the codebase
rg "AbortController|AbortSignal|timeout" lib/ -t ts -t tsx | head -20

Repository: recoupable/api

Length of output: 85


🏁 Script executed:

# Check if there's a configured fetch wrapper or utility function
fd -e ts -e tsx lib/networking/ lib/utils/ | head -20

Repository: recoupable/api

Length of output: 155


🏁 Script executed:

# Look for any environment variables or constants related to request timeouts
rg "TIMEOUT|timeout" lib/agents/content/ -t ts

Repository: recoupable/api

Length of output: 523


🏁 Script executed:

# Look for AbortController or timeout patterns in the broader codebase
rg "AbortController|AbortSignal" lib/ -t ts | head -20

Repository: recoupable/api

Length of output: 219


🏁 Script executed:

# Check if there's a networking utilities directory
fd -type d networking

Repository: recoupable/api

Length of output: 228


🏁 Script executed:

# Look for any fetch wrappers or similar patterns
rg "fetch\(" lib/ -t ts -B 2 -A 2 | head -40

Repository: recoupable/api

Length of output: 2324


🏁 Script executed:

# Check the test file to understand expected behavior
cat -n lib/agents/content/__tests__/downloadVideoBuffer.test.ts

Repository: recoupable/api

Length of output: 2072


🏁 Script executed:

# Check what URLs are being downloaded - look at the context that calls downloadVideoBuffer
rg "videoUrl" lib/agents/content/ -B 2 -A 2

Repository: recoupable/api

Length of output: 4883


🏁 Script executed:

# Check if there are other similar download functions in the codebase and how they handle this
rg "arrayBuffer\(\)" lib/ -B 5 -A 2

Repository: recoupable/api

Length of output: 3235


🏁 Script executed:

# Look at the examples of AbortController usage to see the pattern
cat -n lib/serpapi/searchGoogleImages.ts | grep -A 10 "AbortController"

Repository: recoupable/api

Length of output: 385


🏁 Script executed:

# Check if there are any environment constants for max sizes
rg "MAX_.*SIZE|maxSize" lib/ -t ts

Repository: recoupable/api

Length of output: 288


Add timeout and max-size guards to this download.

This runs on the callback request path and blocks the handler until complete. fetch() has no timeout and arrayBuffer() loads the entire response into memory without bounds. A slow or oversized video can stall the route and trigger Trigger.dev retries instead of gracefully falling back to the URL post. Add an AbortController timeout (following the pattern in lib/serpapi/searchGoogleImages.ts) and a max-size check (similar to lib/youtube/getResizedImageBuffer.ts).

Additionally, redact the URL from the error log at line 12 to avoid exposing signed query parameters.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lib/agents/content/downloadVideoBuffer.ts` around lines 9 - 17, The
downloadVideoBuffer function currently uses fetch(url) and
response.arrayBuffer() without timeout or size limits and logs the full URL; add
an AbortController with a timeout (same pattern used in
lib/serpapi/searchGoogleImages.ts) and pass controller.signal into fetch(url, {
signal }), and enforce a MAX_BYTES limit (like
lib/youtube/getResizedImageBuffer.ts) by streaming response.body with a reader,
accumulating chunks until MAX_BYTES and aborting/returning null if exceeded;
replace the console.error that prints the full URL with a redacted message that
omits query params (log HTTP status and that the URL was redacted) and ensure
you clear the timeout on success or failure.

Comment on lines +11 to +13
if (!response.ok) {
console.error(`Failed to download video: HTTP ${response.status} from ${url}`);
return null;
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Redact the asset URL in the error log.

Line 12 logs the full download URL. These asset URLs are commonly signed, so writing the query string to logs can leak temporary credentials. Log the status plus a redacted host/path instead.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lib/agents/content/downloadVideoBuffer.ts` around lines 11 - 13, The error
log in downloadVideoBuffer.ts currently prints the full signed URL (variable
url) on failure; change the console.error in the response.ok check to redact the
query string by parsing url with the URL constructor and logging only the origin
and pathname (or host+pathname) plus the HTTP status. Locate the failing-block
that checks response.ok (references: response, url, downloadVideoBuffer) and
replace the message so it does not include url.search/query parameters but still
includes clear context and the status code.

Comment on lines 111 to 115
if (failed.length > 0) {
lines.push(`\n_${failed.length} run(s) failed._`);
await thread.post(`_${failed.length} run(s) failed._`);
}

await thread.post(lines.join("\n\n"));
} else {
await thread.post("Content generation finished but no videos were produced.");
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Keep the failed-run summary outside the success-video branch.

Right now the failure count is only posted when at least one video succeeds. A completed callback with zero successful videos falls back to "no videos were produced" and drops the more actionable failure summary.

💡 Suggested change
-        if (failed.length > 0) {
-          await thread.post(`_${failed.length} run(s) failed._`);
-        }
       } else {
         await thread.post("Content generation finished but no videos were produced.");
       }
+
+      if (failed.length > 0) {
+        await thread.post(`_${failed.length} run(s) failed._`);
+      }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lib/agents/content/handleContentAgentCallback.ts` around lines 111 - 115, The
failed-run summary (check of failed.length and thread.post of `_${failed.length}
run(s) failed._`) is currently inside the "some videos succeeded" branch and
gets skipped when there are zero successful videos; move that check out of the
success-video branch so it runs unconditionally after handling the completed
callback. In the handleContentAgentCallback flow, ensure you still post either
the success-video message or the "Content generation finished but no videos were
produced." message, then always run the failed.length > 0 check and call
thread.post with the failure summary so failures are reported even when no
videos were produced.

…e downloads

- Extract getFilenameFromUrl into its own file with tests for edge
  cases (encoded chars, no extension, fal.ai URLs, query params)
- Extract video embed loop into postVideoResults (SRP/OCP)
- Download all videos in parallel with Promise.all instead of
  sequentially, then post to Slack in order
- Update handleContentAgentCallback tests to mock postVideoResults

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

♻️ Duplicate comments (1)
lib/agents/content/handleContentAgentCallback.ts (1)

65-69: ⚠️ Potential issue | 🟡 Minor

Failed-run summary is still skipped when no video succeeds.

When videos.length === 0, postVideoResults is not called, so _${failed.length} run(s) failed._ is never posted in completed callbacks with only failures.

Suggested fix
       if (videos.length > 0) {
         await postVideoResults(thread, videos, failed.length);
       } else {
         await thread.post("Content generation finished but no videos were produced.");
+        if (failed.length > 0) {
+          await thread.post(`_${failed.length} run(s) failed._`);
+        }
       }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lib/agents/content/handleContentAgentCallback.ts` around lines 65 - 69, The
code skips posting the failed-run summary when videos.length === 0; always call
postVideoResults so the completed callback includes the "_X run(s) failed._"
message even if no videos succeeded. Replace the conditional that only calls
postVideoResults when videos.length > 0 with a single unconditional await
postVideoResults(thread, videos, failed.length) (or otherwise ensure thread.post
is invoked to include failed.length) so the failure summary is always posted;
locate this change around the videos, failed, postVideoResults, and thread.post
usage in handleContentAgentCallback.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@lib/agents/content/postVideoResults.ts`:
- Around line 11-13: The Thread interface's post signature is incorrect: update
the Thread interface so post accepts the SDK union (string | PostableMessage |
CardJSXElement) and returns Promise<SentMessage> (or mirror SentMessage shape),
make markdown optional, include optional ast/card/raw fields, and broaden file
data to Buffer | Blob | ArrayBuffer; either import PostableMessage/SentMessage
from '@vercel/chat' and use them in the type for post, or inline an accurate
type matching those fields and file data variants for Thread.post.

---

Duplicate comments:
In `@lib/agents/content/handleContentAgentCallback.ts`:
- Around line 65-69: The code skips posting the failed-run summary when
videos.length === 0; always call postVideoResults so the completed callback
includes the "_X run(s) failed._" message even if no videos succeeded. Replace
the conditional that only calls postVideoResults when videos.length > 0 with a
single unconditional await postVideoResults(thread, videos, failed.length) (or
otherwise ensure thread.post is invoked to include failed.length) so the failure
summary is always posted; locate this change around the videos, failed,
postVideoResults, and thread.post usage in handleContentAgentCallback.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 9d5a62c4-3d45-4452-881e-0b3ffe04d348

📥 Commits

Reviewing files that changed from the base of the PR and between 7b0f463 and fb4f0fa.

⛔ Files ignored due to path filters (3)
  • lib/agents/content/__tests__/getFilenameFromUrl.test.ts is excluded by !**/*.test.*, !**/__tests__/** and included by lib/**
  • lib/agents/content/__tests__/handleContentAgentCallback.test.ts is excluded by !**/*.test.*, !**/__tests__/** and included by lib/**
  • lib/agents/content/__tests__/postVideoResults.test.ts is excluded by !**/*.test.*, !**/__tests__/** and included by lib/**
📒 Files selected for processing (3)
  • lib/agents/content/getFilenameFromUrl.ts
  • lib/agents/content/handleContentAgentCallback.ts
  • lib/agents/content/postVideoResults.ts

Comment on lines +11 to +13
interface Thread {
post: (message: string | { markdown: string; files: { data: Buffer; filename: string; mimeType: string }[] }) => Promise<void>;
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🌐 Web query:

For npm package "chat" (version 4.15.0), what is the exact TypeScript signature of ThreadImpl.post, especially for file uploads (field names/types for attachments)?

💡 Result:

The TypeScript signature for ThreadImpl.post in the "chat" npm package version 4.15.0 (Vercel Chat SDK) is: typescript post(message: string | PostableMessage | CardJSXElement): Promise; PostableMessage is a structured message type that supports file uploads via the files field: typescript type PostableMessage = { markdown?: string; ast?: Root; card?: CardElement; raw?: string; files?: FileUpload[]; // ... other fields }; For file uploads specifically, use the files array in any structured PostableMessage (e.g., { markdown: "...", files: [...] }). The FileUpload type is: typescript interface FileUpload { filename: string; data: Buffer | Blob | ArrayBuffer; mimeType?: string; } Example for file upload: typescript await thread.post({ markdown: "Here's the report:", files: [{ filename: "report.pdf", data: fileBuffer, mimeType: "application/pdf" }] }); ThreadImpl is the concrete implementation of the Thread interface, and post delegates to the adapter-specific posting logic after normalizing the message format. This applies to v4.15.0 as documented in the package files and official docs.

Citations:


Correct the Thread.post interface to match the Vercel Chat SDK contract.

The local Thread interface (lines 11-13) diverges from the actual ThreadImpl.post signature in Chat SDK v4.15.0. The SDK accepts string | PostableMessage | CardJSXElement and returns Promise<SentMessage>, but the local interface:

  • Makes markdown required instead of optional
  • Omits ast, card, raw fields in the message object
  • Ignores the CardJSXElement branch
  • Returns Promise<void> instead of Promise<SentMessage>
  • Restricts FileUpload.data to only Buffer (SDK also accepts Blob | ArrayBuffer)

This mismatch creates type drift that tests won't catch and reduces flexibility for future posting patterns.

Update interface to reflect SDK contract
// Replace lines 11-13 with the actual SDK type or import it
import { PostableMessage, SentMessage } from '@vercel/chat'; // if exported
interface Thread {
  post: (message: string | PostableMessage | CardJSXElement) => Promise<SentMessage>;
}

Or define it more accurately inline:

interface Thread {
  post: (message: string | { markdown?: string; ast?: any; card?: any; raw?: string; files?: Array<{ filename: string; data: Buffer | Blob | ArrayBuffer; mimeType?: string }> }) => Promise<{ messageId: string }>;
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lib/agents/content/postVideoResults.ts` around lines 11 - 13, The Thread
interface's post signature is incorrect: update the Thread interface so post
accepts the SDK union (string | PostableMessage | CardJSXElement) and returns
Promise<SentMessage> (or mirror SentMessage shape), make markdown optional,
include optional ast/card/raw fields, and broaden file data to Buffer | Blob |
ArrayBuffer; either import PostableMessage/SentMessage from '@vercel/chat' and
use them in the type for post, or inline an accurate type matching those fields
and file data variants for Thread.post.

Run prettier on new files. Change Thread.post return type from
Promise<void> to Promise<unknown> to match ThreadImpl signature.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@sweetmantech sweetmantech merged commit 237567d into test Apr 1, 2026
4 of 5 checks passed
@sweetmantech sweetmantech deleted the feature/slack-embedded-video branch April 1, 2026 20:21
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants