From 70ecb046c14874da6448c8741492b8c6646ff2b3 Mon Sep 17 00:00:00 2001 From: Ben Sabic Date: Tue, 14 Apr 2026 15:04:10 +1000 Subject: [PATCH 1/2] feat(chat): add chat.thread() method for creating thread handles Allows constructing a Thread handle from a thread ID outside of webhook contexts, enabling proactive messaging to existing threads. Closes #148 --- .changeset/thread-handle.md | 5 +++++ packages/chat/src/chat.test.ts | 27 +++++++++++++++++++++++++++ packages/chat/src/chat.ts | 34 ++++++++++++++++++++++++++++++++++ 3 files changed, 66 insertions(+) create mode 100644 .changeset/thread-handle.md diff --git a/.changeset/thread-handle.md b/.changeset/thread-handle.md new file mode 100644 index 00000000..cdfed15d --- /dev/null +++ b/.changeset/thread-handle.md @@ -0,0 +1,5 @@ +--- +"chat": minor +--- + +Add `chat.thread(threadId)` method to create Thread handles outside of webhook contexts diff --git a/packages/chat/src/chat.test.ts b/packages/chat/src/chat.test.ts index a1878c67..e02dcfab 100644 --- a/packages/chat/src/chat.test.ts +++ b/packages/chat/src/chat.test.ts @@ -1423,6 +1423,33 @@ describe("Chat", () => { }); }); + describe("thread", () => { + it("should return a Thread handle for a valid thread ID", () => { + const thread = chat.thread("slack:C123:1234.5678"); + expect(thread).toBeDefined(); + expect(thread.id).toBe("slack:C123:1234.5678"); + }); + + it("should allow posting to a thread handle", async () => { + const thread = chat.thread("slack:C123:1234.5678"); + await thread.post("Hello from outside a webhook!"); + expect(mockAdapter.postMessage).toHaveBeenCalledWith( + "slack:C123:1234.5678", + "Hello from outside a webhook!" + ); + }); + + it("should throw for an invalid thread ID", () => { + expect(() => chat.thread("")).toThrow("Invalid thread ID"); + }); + + it("should throw for an unknown adapter prefix", () => { + expect(() => chat.thread("unknown:C123:1234.5678")).toThrow( + 'Adapter "unknown" not found' + ); + }); + }); + describe("isDM", () => { it("should return true for DM threads", async () => { const thread = await chat.openDM("U123456"); diff --git a/packages/chat/src/chat.ts b/packages/chat/src/chat.ts index 230a3436..5cac0b93 100644 --- a/packages/chat/src/chat.ts +++ b/packages/chat/src/chat.ts @@ -1575,6 +1575,40 @@ export class Chat< }); } + /** + * Get a Thread handle by its thread ID. + * + * The adapter is automatically inferred from the thread ID prefix. + * + * @param threadId - Full thread ID (e.g., "slack:C123ABC:1234567890.123456") + * @returns A Thread that can be used to post messages, subscribe, etc. + * + * @example + * ```typescript + * const thread = chat.thread("slack:C123ABC:1234567890.123456"); + * await thread.post("Hello from outside a webhook!"); + * ``` + */ + thread(threadId: string): Thread { + const adapterName = threadId.split(":")[0]; + if (!adapterName) { + throw new ChatError( + `Invalid thread ID: ${threadId}`, + "INVALID_THREAD_ID" + ); + } + + const adapter = this.adapters.get(adapterName); + if (!adapter) { + throw new ChatError( + `Adapter "${adapterName}" not found for thread ID "${threadId}"`, + "ADAPTER_NOT_FOUND" + ); + } + + return this.createThread(adapter, threadId, {} as Message, false); + } + /** * Infer which adapter to use based on the userId format. */ From 8f8308b27c7f604072ff3c04d307e5266036866c Mon Sep 17 00:00:00 2001 From: Ben Sabic Date: Tue, 14 Apr 2026 15:07:54 +1000 Subject: [PATCH 2/2] docs: add chat.thread() to API reference --- apps/docs/content/docs/api/chat.mdx | 9 +++++++++ apps/docs/content/docs/api/thread.mdx | 2 +- 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/apps/docs/content/docs/api/chat.mdx b/apps/docs/content/docs/api/chat.mdx index 79a0ae21..977225dd 100644 --- a/apps/docs/content/docs/api/chat.mdx +++ b/apps/docs/content/docs/api/chat.mdx @@ -443,6 +443,15 @@ await dm.post("Hello via DM!"); const dm = await bot.openDM(message.author); ``` +### thread + +Get a Thread handle by its thread ID. Useful for posting to threads outside of webhook contexts (e.g. cron jobs, external triggers). + +```typescript +const thread = bot.thread("slack:C123ABC:1234567890.123456"); +await thread.post("Hello from a cron job!"); +``` + ### channel Get a Channel by its channel ID. diff --git a/apps/docs/content/docs/api/thread.mdx b/apps/docs/content/docs/api/thread.mdx index 2e9de901..188d86ea 100644 --- a/apps/docs/content/docs/api/thread.mdx +++ b/apps/docs/content/docs/api/thread.mdx @@ -4,7 +4,7 @@ description: Represents a conversation thread with methods for posting, subscrib type: reference --- -A `Thread` is provided to your event handlers and represents a conversation thread on any platform. You don't create threads directly — they come from handler callbacks or `chat.openDM()`. +A `Thread` is provided to your event handlers and represents a conversation thread on any platform. You can also create thread handles directly using `chat.thread()` or `chat.openDM()`. ## Properties