From f831ca3dac6674e0e62847d7d8570ca0ac065978 Mon Sep 17 00:00:00 2001 From: v0 Date: Tue, 24 Mar 2026 23:01:42 +0000 Subject: [PATCH 1/2] feat: add channel visibility feature to SlackAdapter and ChannelImpl Slack-Thread: https://vercel.slack.com/archives/C0ACY4D798Q/p1771867586825769?thread_ts=1771867586.825769&cid=C0ACY4D798Q Co-authored-by: Kavin Valli <41034356+kavinvalli@users.noreply.github.com> --- packages/adapter-slack/src/index.ts | 62 +++++++++++++++++++++++++ packages/chat/src/channel.test.ts | 25 ++++++++-- packages/chat/src/channel.ts | 31 ++++++++++++- packages/chat/src/chat.ts | 8 ++-- packages/chat/src/index.ts | 1 + packages/chat/src/mock-adapter.ts | 1 + packages/chat/src/serialization.test.ts | 53 ++++++++++++++++++--- packages/chat/src/thread.ts | 33 +++++++++++-- packages/chat/src/types.ts | 47 +++++++++++++++++-- 9 files changed, 241 insertions(+), 20 deletions(-) diff --git a/packages/adapter-slack/src/index.ts b/packages/adapter-slack/src/index.ts index 78331df7..a1ca2e1f 100644 --- a/packages/adapter-slack/src/index.ts +++ b/packages/adapter-slack/src/index.ts @@ -15,6 +15,7 @@ import type { AdapterPostableMessage, Attachment, ChannelInfo, + ChannelVisibility, ChatInstance, EmojiValue, EphemeralMessage, @@ -2399,6 +2400,7 @@ export class SlackAdapter implements Adapter { | { name?: string; is_ext_shared?: boolean; + is_private?: boolean; } | undefined; @@ -2412,10 +2414,22 @@ export class SlackAdapter implements Adapter { ok: result.ok, }); + // Determine channel visibility + let channelVisibility: ChannelVisibility = "unknown"; + if (channelInfo?.is_ext_shared) { + channelVisibility = "external"; + } else if (channelInfo?.is_private || channel.startsWith("D")) { + channelVisibility = "private"; + } else if (channel.startsWith("C")) { + channelVisibility = "workspace"; + } + return { id: threadId, channelId: channel, channelName: channelInfo?.name, + channelVisibility, + // Keep isExternalChannel for backwards compatibility isExternalChannel: channelInfo?.is_ext_shared ?? false, metadata: { threadTs, @@ -2476,12 +2490,42 @@ export class SlackAdapter implements Adapter { * Check if a thread is in an external/shared channel (Slack Connect). * Uses the `is_ext_shared_channel` flag from incoming webhook payloads, * cached per channel ID. + * @deprecated Use `getChannelVisibility` instead */ isExternalChannel(threadId: string): boolean { const { channel } = this.decodeThreadId(threadId); return this._externalChannels.has(channel); } + /** + * Get the visibility scope of a channel containing the thread. + * + * - `external`: Slack Connect channel shared with external organizations + * - `private`: Private channel (starts with G) or DM (starts with D) + * - `workspace`: Public channel visible to all workspace members + * - `unknown`: Visibility cannot be determined (not yet cached) + */ + getChannelVisibility(threadId: string): ChannelVisibility { + const { channel } = this.decodeThreadId(threadId); + + // Check for external channel first (Slack Connect) + if (this._externalChannels.has(channel)) { + return "external"; + } + + // Private channels start with G, DMs start with D + if (channel.startsWith("G") || channel.startsWith("D")) { + return "private"; + } + + // Public channels start with C + if (channel.startsWith("C")) { + return "workspace"; + } + + return "unknown"; + } + decodeThreadId(threadId: string): SlackThreadId { const parts = threadId.split(":"); if (parts.length < 2 || parts.length > 3 || parts[0] !== "slack") { @@ -2793,6 +2837,7 @@ export class SlackAdapter implements Adapter { name?: string; is_im?: boolean; is_mpim?: boolean; + is_private?: boolean; is_ext_shared?: boolean; num_members?: number; purpose?: { value?: string }; @@ -2804,10 +2849,27 @@ export class SlackAdapter implements Adapter { this._externalChannels.add(channel); } + // Determine channel visibility + let channelVisibility: ChannelVisibility = "unknown"; + if (info?.is_ext_shared) { + channelVisibility = "external"; + } else if ( + info?.is_im || + info?.is_mpim || + info?.is_private || + channel.startsWith("D") + ) { + channelVisibility = "private"; + } else if (channel.startsWith("C")) { + channelVisibility = "workspace"; + } + return { id: channelId, name: info?.name ? `#${info.name}` : undefined, isDM: Boolean(info?.is_im || info?.is_mpim), + channelVisibility, + // Keep isExternalChannel for backwards compatibility isExternalChannel: info?.is_ext_shared ?? false, memberCount: info?.num_members, metadata: { diff --git a/packages/chat/src/channel.test.ts b/packages/chat/src/channel.test.ts index e13c44c5..a43eb72d 100644 --- a/packages/chat/src/channel.test.ts +++ b/packages/chat/src/channel.test.ts @@ -465,6 +465,7 @@ describe("ChannelImpl", () => { _type: "chat:Channel", id: "slack:C123", adapterName: "slack", + channelVisibility: "unknown", isDM: false, }); }); @@ -566,7 +567,7 @@ describe("thread.channel", () => { expect(thread.channel.isDM).toBe(true); }); - it("should inherit isExternalChannel from thread", () => { + it("should inherit channelVisibility from thread", () => { const mockAdapter = createMockAdapter(); const mockState = createMockState(); @@ -575,13 +576,14 @@ describe("thread.channel", () => { adapter: mockAdapter, channelId: "C123", stateAdapter: mockState, - isExternalChannel: true, + channelVisibility: "external", }); + expect(thread.channel.channelVisibility).toBe("external"); expect(thread.channel.isExternalChannel).toBe(true); }); - it("should default isExternalChannel to false", () => { + it("should default channelVisibility to unknown", () => { const mockAdapter = createMockAdapter(); const mockState = createMockState(); @@ -592,6 +594,23 @@ describe("thread.channel", () => { stateAdapter: mockState, }); + expect(thread.channel.channelVisibility).toBe("unknown"); + expect(thread.channel.isExternalChannel).toBe(false); + }); + + it("should support private channel visibility", () => { + const mockAdapter = createMockAdapter(); + const mockState = createMockState(); + + const thread = new ThreadImpl({ + id: "slack:G123:1234.5678", + adapter: mockAdapter, + channelId: "G123", + stateAdapter: mockState, + channelVisibility: "private", + }); + + expect(thread.channel.channelVisibility).toBe("private"); expect(thread.channel.isExternalChannel).toBe(false); }); }); diff --git a/packages/chat/src/channel.ts b/packages/chat/src/channel.ts index 16ac7562..7b59ad28 100644 --- a/packages/chat/src/channel.ts +++ b/packages/chat/src/channel.ts @@ -16,6 +16,7 @@ import type { Author, Channel, ChannelInfo, + ChannelVisibility, EphemeralMessage, PostableMessage, PostEphemeralOptions, @@ -34,8 +35,12 @@ const CHANNEL_STATE_KEY_PREFIX = "channel-state:"; export interface SerializedChannel { _type: "chat:Channel"; adapterName: string; + channelVisibility?: ChannelVisibility; id: string; isDM: boolean; + /** + * @deprecated Use `channelVisibility` instead + */ isExternalChannel?: boolean; } @@ -44,8 +49,12 @@ export interface SerializedChannel { */ interface ChannelImplConfigWithAdapter { adapter: Adapter; + channelVisibility?: ChannelVisibility; id: string; isDM?: boolean; + /** + * @deprecated Use `channelVisibility` instead + */ isExternalChannel?: boolean; stateAdapter: StateAdapter; } @@ -55,8 +64,12 @@ interface ChannelImplConfigWithAdapter { */ interface ChannelImplConfigLazy { adapterName: string; + channelVisibility?: ChannelVisibility; id: string; isDM?: boolean; + /** + * @deprecated Use `channelVisibility` instead + */ isExternalChannel?: boolean; } @@ -82,7 +95,14 @@ export class ChannelImpl> { readonly id: string; readonly isDM: boolean; - readonly isExternalChannel: boolean; + readonly channelVisibility: ChannelVisibility; + + /** + * @deprecated Use `channelVisibility === 'external'` instead + */ + get isExternalChannel(): boolean { + return this.channelVisibility === "external"; + } private _adapter?: Adapter; private readonly _adapterName?: string; @@ -92,7 +112,10 @@ export class ChannelImpl> constructor(config: ChannelImplConfig) { this.id = config.id; this.isDM = config.isDM ?? false; - this.isExternalChannel = config.isExternalChannel ?? false; + // Support both new channelVisibility and deprecated isExternalChannel + this.channelVisibility = + config.channelVisibility ?? + (config.isExternalChannel ? "external" : "unknown"); if (isLazyConfig(config)) { this._adapterName = config.adapterName; @@ -337,7 +360,9 @@ export class ChannelImpl> _type: "chat:Channel", id: this.id, adapterName: this.adapter.name, + channelVisibility: this.channelVisibility, isDM: this.isDM, + // Keep isExternalChannel for backwards compatibility ...(this.isExternalChannel ? { isExternalChannel: true } : {}), }; } @@ -349,6 +374,8 @@ export class ChannelImpl> const channel = new ChannelImpl({ id: json.id, adapterName: json.adapterName, + // Prefer channelVisibility, fall back to isExternalChannel for backwards compat + channelVisibility: json.channelVisibility, isDM: json.isDM, isExternalChannel: json.isExternalChannel, }); diff --git a/packages/chat/src/chat.ts b/packages/chat/src/chat.ts index 356ebb2f..4bf16634 100644 --- a/packages/chat/src/chat.ts +++ b/packages/chat/src/chat.ts @@ -1576,8 +1576,10 @@ export class Chat< // Check if this is a DM const isDM = adapter.isDM?.(threadId) ?? false; - // Check if this is an external/shared channel (e.g., Slack Connect) - const isExternalChannel = adapter.isExternalChannel?.(threadId) ?? false; + // Get channel visibility (prefer new API, fall back to deprecated isExternalChannel) + const channelVisibility = + adapter.getChannelVisibility?.(threadId) ?? + (adapter.isExternalChannel?.(threadId) ? "external" : "unknown"); return new ThreadImpl({ id: threadId, @@ -1587,7 +1589,7 @@ export class Chat< initialMessage, isSubscribedContext, isDM, - isExternalChannel, + channelVisibility, currentMessage: initialMessage, streamingUpdateIntervalMs: this._streamingUpdateIntervalMs, }); diff --git a/packages/chat/src/index.ts b/packages/chat/src/index.ts index 40a6f919..82c5e12b 100644 --- a/packages/chat/src/index.ts +++ b/packages/chat/src/index.ts @@ -194,6 +194,7 @@ export type { Author, Channel, ChannelInfo, + ChannelVisibility, ChatConfig, ChatInstance, CustomEmojiMap, diff --git a/packages/chat/src/mock-adapter.ts b/packages/chat/src/mock-adapter.ts index a32e601a..5e09935a 100644 --- a/packages/chat/src/mock-adapter.ts +++ b/packages/chat/src/mock-adapter.ts @@ -69,6 +69,7 @@ export function createMockAdapter(name = "slack"): Adapter { .fn() .mockImplementation((threadId: string) => threadId.includes(":D")), isExternalChannel: vi.fn().mockReturnValue(false), + getChannelVisibility: vi.fn().mockReturnValue("unknown"), openModal: vi.fn().mockResolvedValue({ viewId: "V123" }), channelIdFromThreadId: vi .fn() diff --git a/packages/chat/src/serialization.test.ts b/packages/chat/src/serialization.test.ts index 2020fcec..80f9b8f5 100644 --- a/packages/chat/src/serialization.test.ts +++ b/packages/chat/src/serialization.test.ts @@ -30,6 +30,7 @@ describe("Serialization", () => { _type: "chat:Thread", id: "slack:C123:1234.5678", channelId: "C123", + channelVisibility: "unknown", currentMessage: undefined, isDM: false, adapterName: "slack", @@ -63,16 +64,17 @@ describe("Serialization", () => { adapter: mockAdapter, channelId: "C123", stateAdapter: mockState, - isExternalChannel: true, + channelVisibility: "external", }); const json = thread.toJSON(); expect(json._type).toBe("chat:Thread"); + expect(json.channelVisibility).toBe("external"); expect(json.isExternalChannel).toBe(true); }); - it("should omit isExternalChannel when false", () => { + it("should serialize private channel thread correctly", () => { const mockAdapter = createMockAdapter("slack"); const mockState = createMockState(); @@ -81,14 +83,34 @@ describe("Serialization", () => { adapter: mockAdapter, channelId: "C123", stateAdapter: mockState, - isExternalChannel: false, + channelVisibility: "private", }); const json = thread.toJSON(); + expect(json._type).toBe("chat:Thread"); + expect(json.channelVisibility).toBe("private"); expect(json.isExternalChannel).toBeUndefined(); }); + it("should omit isExternalChannel when not external", () => { + const mockAdapter = createMockAdapter("slack"); + const mockState = createMockState(); + + const thread = new ThreadImpl({ + id: "slack:C123:1234.5678", + adapter: mockAdapter, + channelId: "C123", + stateAdapter: mockState, + channelVisibility: "workspace", + }); + + const json = thread.toJSON(); + + expect(json.isExternalChannel).toBeUndefined(); + expect(json.channelVisibility).toBe("workspace"); + }); + it("should produce JSON-serializable output", () => { const mockAdapter = createMockAdapter("teams"); const mockState = createMockState(); @@ -198,7 +220,7 @@ describe("Serialization", () => { expect(restored.adapter.name).toBe(original.adapter.name); }); - it("should round-trip isExternalChannel correctly", () => { + it("should round-trip channelVisibility correctly", () => { const mockAdapter = createMockAdapter("slack"); const original = new ThreadImpl({ @@ -206,16 +228,17 @@ describe("Serialization", () => { adapter: mockAdapter, channelId: "C123", stateAdapter: mockState, - isExternalChannel: true, + channelVisibility: "external", }); const json = original.toJSON(); const restored = ThreadImpl.fromJSON(json); + expect(restored.channelVisibility).toBe("external"); expect(restored.isExternalChannel).toBe(true); }); - it("should default isExternalChannel to false when missing from JSON", () => { + it("should default channelVisibility to unknown when missing from JSON", () => { const json: SerializedThread = { _type: "chat:Thread", id: "slack:C123:1234.5678", @@ -226,9 +249,26 @@ describe("Serialization", () => { const thread = ThreadImpl.fromJSON(json); + expect(thread.channelVisibility).toBe("unknown"); expect(thread.isExternalChannel).toBe(false); }); + it("should migrate legacy isExternalChannel to channelVisibility", () => { + const json: SerializedThread = { + _type: "chat:Thread", + id: "slack:C123:1234.5678", + channelId: "C123", + isDM: false, + adapterName: "slack", + isExternalChannel: true, + }; + + const thread = ThreadImpl.fromJSON(json); + + expect(thread.channelVisibility).toBe("external"); + expect(thread.isExternalChannel).toBe(true); + }); + it("should serialize currentMessage", () => { const mockAdapter = createMockAdapter("slack"); const currentMessage = createTestMessage("msg-1", "Hello", { @@ -735,6 +775,7 @@ describe("Serialization", () => { _type: "chat:Thread", id: "slack:C123:1234.5678", channelId: "C123", + channelVisibility: "unknown", currentMessage: undefined, isDM: false, adapterName: "slack", diff --git a/packages/chat/src/thread.ts b/packages/chat/src/thread.ts index 54064f9a..d0bb91fa 100644 --- a/packages/chat/src/thread.ts +++ b/packages/chat/src/thread.ts @@ -18,6 +18,7 @@ import type { Attachment, Author, Channel, + ChannelVisibility, EphemeralMessage, PostableMessage, PostEphemeralOptions, @@ -35,9 +36,13 @@ export interface SerializedThread { _type: "chat:Thread"; adapterName: string; channelId: string; + channelVisibility?: ChannelVisibility; currentMessage?: SerializedMessage; id: string; isDM: boolean; + /** + * @deprecated Use `channelVisibility` instead + */ isExternalChannel?: boolean; } @@ -47,10 +52,14 @@ export interface SerializedThread { interface ThreadImplConfigWithAdapter { adapter: Adapter; channelId: string; + channelVisibility?: ChannelVisibility; currentMessage?: Message; id: string; initialMessage?: Message; isDM?: boolean; + /** + * @deprecated Use `channelVisibility` instead + */ isExternalChannel?: boolean; isSubscribedContext?: boolean; stateAdapter: StateAdapter; @@ -64,10 +73,14 @@ interface ThreadImplConfigWithAdapter { interface ThreadImplConfigLazy { adapterName: string; channelId: string; + channelVisibility?: ChannelVisibility; currentMessage?: Message; id: string; initialMessage?: Message; isDM?: boolean; + /** + * @deprecated Use `channelVisibility` instead + */ isExternalChannel?: boolean; isSubscribedContext?: boolean; streamingUpdateIntervalMs?: number; @@ -99,7 +112,14 @@ export class ThreadImpl> readonly id: string; readonly channelId: string; readonly isDM: boolean; - readonly isExternalChannel: boolean; + readonly channelVisibility: ChannelVisibility; + + /** + * @deprecated Use `channelVisibility === 'external'` instead + */ + get isExternalChannel(): boolean { + return this.channelVisibility === "external"; + } /** Direct adapter instance (if provided) */ private _adapter?: Adapter; @@ -120,7 +140,10 @@ export class ThreadImpl> this.id = config.id; this.channelId = config.channelId; this.isDM = config.isDM ?? false; - this.isExternalChannel = config.isExternalChannel ?? false; + // Support both new channelVisibility and deprecated isExternalChannel + this.channelVisibility = + config.channelVisibility ?? + (config.isExternalChannel ? "external" : "unknown"); this._isSubscribedContext = config.isSubscribedContext ?? false; this._currentMessage = config.currentMessage; this._streamingUpdateIntervalMs = config.streamingUpdateIntervalMs ?? 500; @@ -232,7 +255,7 @@ export class ThreadImpl> adapter: this.adapter, stateAdapter: this._stateAdapter, isDM: this.isDM, - isExternalChannel: this.isExternalChannel, + channelVisibility: this.channelVisibility, }); } return this._channel; @@ -556,8 +579,10 @@ export class ThreadImpl> _type: "chat:Thread", id: this.id, channelId: this.channelId, + channelVisibility: this.channelVisibility, currentMessage: this._currentMessage?.toJSON(), isDM: this.isDM, + // Keep isExternalChannel for backwards compatibility ...(this.isExternalChannel ? { isExternalChannel: true } : {}), adapterName: this.adapter.name, }; @@ -585,6 +610,8 @@ export class ThreadImpl> id: json.id, adapterName: json.adapterName, channelId: json.channelId, + // Prefer channelVisibility, fall back to isExternalChannel for backwards compat + channelVisibility: json.channelVisibility, currentMessage: json.currentMessage ? Message.fromJSON(json.currentMessage) : undefined, diff --git a/packages/chat/src/types.ts b/packages/chat/src/types.ts index 0370362a..be6c01c7 100644 --- a/packages/chat/src/types.ts +++ b/packages/chat/src/types.ts @@ -9,6 +9,20 @@ import type { Logger, LogLevel } from "./logger"; import type { Message } from "./message"; import type { ModalElement } from "./modals"; +// ============================================================================= +// Channel Visibility +// ============================================================================= + +/** + * Represents the visibility scope of a channel. + * + * - `private`: Channel is only visible to invited members (e.g., private Slack channels) + * - `workspace`: Channel is visible to all workspace members (e.g., public Slack channels) + * - `external`: Channel is shared with external organizations (e.g., Slack Connect) + * - `unknown`: Visibility cannot be determined + */ +export type ChannelVisibility = "private" | "workspace" | "external" | "unknown"; + // ============================================================================= // Re-exports from extracted modules // ============================================================================= @@ -203,9 +217,21 @@ export interface Adapter { * * @param threadId - The thread ID to check * @returns True if the thread is in an external channel, false otherwise + * @deprecated Use `getChannelVisibility` instead for more granular visibility control */ isExternalChannel?(threadId: string): boolean; + /** + * Get the visibility scope of a channel containing the thread. + * + * This provides more granular control than `isExternalChannel` by distinguishing + * between private channels, workspace-visible channels, and externally shared channels. + * + * @param threadId - The thread ID to check + * @returns The channel visibility scope + */ + getChannelVisibility?(threadId: string): ChannelVisibility; + /** * List threads in a channel. */ @@ -517,8 +543,13 @@ export interface Postable< readonly id: string; /** Whether this is a direct message conversation */ readonly isDM: boolean; - /** Whether this is an external/shared channel (e.g., Slack Connect) */ + /** + * Whether this is an external/shared channel (e.g., Slack Connect). + * @deprecated Use `channelVisibility` instead for more granular visibility control + */ readonly isExternalChannel: boolean; + /** The visibility scope of this channel */ + readonly channelVisibility: ChannelVisibility; /** * Get a platform-specific mention string for a user. @@ -613,8 +644,13 @@ export interface ThreadSummary { export interface ChannelInfo { id: string; isDM?: boolean; - /** Whether this is an external/shared channel (e.g., Slack Connect) */ + /** + * Whether this is an external/shared channel (e.g., Slack Connect). + * @deprecated Use `channelVisibility` instead for more granular visibility control + */ isExternalChannel?: boolean; + /** The visibility scope of this channel */ + channelVisibility?: ChannelVisibility; memberCount?: number; metadata: Record; name?: string; @@ -812,8 +848,13 @@ export interface ThreadInfo { id: string; /** Whether this is a direct message conversation */ isDM?: boolean; - /** Whether this is an external/shared channel (e.g., Slack Connect) */ + /** + * Whether this is an external/shared channel (e.g., Slack Connect). + * @deprecated Use `channelVisibility` instead for more granular visibility control + */ isExternalChannel?: boolean; + /** The visibility scope of this channel */ + channelVisibility?: ChannelVisibility; /** Platform-specific metadata */ metadata: Record; } From b346eebb52eb026e225e0e6df0e9b7a6e80634d8 Mon Sep 17 00:00:00 2001 From: v0 Date: Wed, 25 Mar 2026 00:14:06 +0000 Subject: [PATCH 2/2] refactor: remove deprecated isExternalChannel logic and usage Slack-Thread: https://vercel.slack.com/archives/C0ACY4D798Q/p1771867586825769?thread_ts=1771867586.825769&cid=C0ACY4D798Q Co-authored-by: Kavin Valli <41034356+kavinvalli@users.noreply.github.com> --- packages/adapter-slack/src/index.ts | 15 ------------ packages/chat/src/channel.test.ts | 3 --- packages/chat/src/channel.ts | 28 +--------------------- packages/chat/src/chat.ts | 5 ++-- packages/chat/src/mock-adapter.ts | 1 - packages/chat/src/serialization.test.ts | 23 +----------------- packages/chat/src/thread.ts | 28 +--------------------- packages/chat/src/types.ts | 31 ++----------------------- 8 files changed, 7 insertions(+), 127 deletions(-) diff --git a/packages/adapter-slack/src/index.ts b/packages/adapter-slack/src/index.ts index a1ca2e1f..c3f86aa5 100644 --- a/packages/adapter-slack/src/index.ts +++ b/packages/adapter-slack/src/index.ts @@ -2429,8 +2429,6 @@ export class SlackAdapter implements Adapter { channelId: channel, channelName: channelInfo?.name, channelVisibility, - // Keep isExternalChannel for backwards compatibility - isExternalChannel: channelInfo?.is_ext_shared ?? false, metadata: { threadTs, channel: result.channel, @@ -2486,17 +2484,6 @@ export class SlackAdapter implements Adapter { return channel.startsWith("D"); } - /** - * Check if a thread is in an external/shared channel (Slack Connect). - * Uses the `is_ext_shared_channel` flag from incoming webhook payloads, - * cached per channel ID. - * @deprecated Use `getChannelVisibility` instead - */ - isExternalChannel(threadId: string): boolean { - const { channel } = this.decodeThreadId(threadId); - return this._externalChannels.has(channel); - } - /** * Get the visibility scope of a channel containing the thread. * @@ -2869,8 +2856,6 @@ export class SlackAdapter implements Adapter { name: info?.name ? `#${info.name}` : undefined, isDM: Boolean(info?.is_im || info?.is_mpim), channelVisibility, - // Keep isExternalChannel for backwards compatibility - isExternalChannel: info?.is_ext_shared ?? false, memberCount: info?.num_members, metadata: { purpose: info?.purpose?.value, diff --git a/packages/chat/src/channel.test.ts b/packages/chat/src/channel.test.ts index a43eb72d..ea7e965f 100644 --- a/packages/chat/src/channel.test.ts +++ b/packages/chat/src/channel.test.ts @@ -580,7 +580,6 @@ describe("thread.channel", () => { }); expect(thread.channel.channelVisibility).toBe("external"); - expect(thread.channel.isExternalChannel).toBe(true); }); it("should default channelVisibility to unknown", () => { @@ -595,7 +594,6 @@ describe("thread.channel", () => { }); expect(thread.channel.channelVisibility).toBe("unknown"); - expect(thread.channel.isExternalChannel).toBe(false); }); it("should support private channel visibility", () => { @@ -611,7 +609,6 @@ describe("thread.channel", () => { }); expect(thread.channel.channelVisibility).toBe("private"); - expect(thread.channel.isExternalChannel).toBe(false); }); }); diff --git a/packages/chat/src/channel.ts b/packages/chat/src/channel.ts index 7b59ad28..f306abd6 100644 --- a/packages/chat/src/channel.ts +++ b/packages/chat/src/channel.ts @@ -38,10 +38,6 @@ export interface SerializedChannel { channelVisibility?: ChannelVisibility; id: string; isDM: boolean; - /** - * @deprecated Use `channelVisibility` instead - */ - isExternalChannel?: boolean; } /** @@ -52,10 +48,6 @@ interface ChannelImplConfigWithAdapter { channelVisibility?: ChannelVisibility; id: string; isDM?: boolean; - /** - * @deprecated Use `channelVisibility` instead - */ - isExternalChannel?: boolean; stateAdapter: StateAdapter; } @@ -67,10 +59,6 @@ interface ChannelImplConfigLazy { channelVisibility?: ChannelVisibility; id: string; isDM?: boolean; - /** - * @deprecated Use `channelVisibility` instead - */ - isExternalChannel?: boolean; } type ChannelImplConfig = ChannelImplConfigWithAdapter | ChannelImplConfigLazy; @@ -97,13 +85,6 @@ export class ChannelImpl> readonly isDM: boolean; readonly channelVisibility: ChannelVisibility; - /** - * @deprecated Use `channelVisibility === 'external'` instead - */ - get isExternalChannel(): boolean { - return this.channelVisibility === "external"; - } - private _adapter?: Adapter; private readonly _adapterName?: string; private _stateAdapterInstance?: StateAdapter; @@ -112,10 +93,7 @@ export class ChannelImpl> constructor(config: ChannelImplConfig) { this.id = config.id; this.isDM = config.isDM ?? false; - // Support both new channelVisibility and deprecated isExternalChannel - this.channelVisibility = - config.channelVisibility ?? - (config.isExternalChannel ? "external" : "unknown"); + this.channelVisibility = config.channelVisibility ?? "unknown"; if (isLazyConfig(config)) { this._adapterName = config.adapterName; @@ -362,8 +340,6 @@ export class ChannelImpl> adapterName: this.adapter.name, channelVisibility: this.channelVisibility, isDM: this.isDM, - // Keep isExternalChannel for backwards compatibility - ...(this.isExternalChannel ? { isExternalChannel: true } : {}), }; } @@ -374,10 +350,8 @@ export class ChannelImpl> const channel = new ChannelImpl({ id: json.id, adapterName: json.adapterName, - // Prefer channelVisibility, fall back to isExternalChannel for backwards compat channelVisibility: json.channelVisibility, isDM: json.isDM, - isExternalChannel: json.isExternalChannel, }); if (adapter) { channel._adapter = adapter; diff --git a/packages/chat/src/chat.ts b/packages/chat/src/chat.ts index 4bf16634..c0b0afd1 100644 --- a/packages/chat/src/chat.ts +++ b/packages/chat/src/chat.ts @@ -1576,10 +1576,9 @@ export class Chat< // Check if this is a DM const isDM = adapter.isDM?.(threadId) ?? false; - // Get channel visibility (prefer new API, fall back to deprecated isExternalChannel) + // Get channel visibility const channelVisibility = - adapter.getChannelVisibility?.(threadId) ?? - (adapter.isExternalChannel?.(threadId) ? "external" : "unknown"); + adapter.getChannelVisibility?.(threadId) ?? "unknown"; return new ThreadImpl({ id: threadId, diff --git a/packages/chat/src/mock-adapter.ts b/packages/chat/src/mock-adapter.ts index 5e09935a..d1edfc2a 100644 --- a/packages/chat/src/mock-adapter.ts +++ b/packages/chat/src/mock-adapter.ts @@ -68,7 +68,6 @@ export function createMockAdapter(name = "slack"): Adapter { isDM: vi .fn() .mockImplementation((threadId: string) => threadId.includes(":D")), - isExternalChannel: vi.fn().mockReturnValue(false), getChannelVisibility: vi.fn().mockReturnValue("unknown"), openModal: vi.fn().mockResolvedValue({ viewId: "V123" }), channelIdFromThreadId: vi diff --git a/packages/chat/src/serialization.test.ts b/packages/chat/src/serialization.test.ts index 80f9b8f5..0cba853c 100644 --- a/packages/chat/src/serialization.test.ts +++ b/packages/chat/src/serialization.test.ts @@ -71,7 +71,6 @@ describe("Serialization", () => { expect(json._type).toBe("chat:Thread"); expect(json.channelVisibility).toBe("external"); - expect(json.isExternalChannel).toBe(true); }); it("should serialize private channel thread correctly", () => { @@ -90,10 +89,9 @@ describe("Serialization", () => { expect(json._type).toBe("chat:Thread"); expect(json.channelVisibility).toBe("private"); - expect(json.isExternalChannel).toBeUndefined(); }); - it("should omit isExternalChannel when not external", () => { + it("should serialize workspace channel thread correctly", () => { const mockAdapter = createMockAdapter("slack"); const mockState = createMockState(); @@ -107,7 +105,6 @@ describe("Serialization", () => { const json = thread.toJSON(); - expect(json.isExternalChannel).toBeUndefined(); expect(json.channelVisibility).toBe("workspace"); }); @@ -235,7 +232,6 @@ describe("Serialization", () => { const restored = ThreadImpl.fromJSON(json); expect(restored.channelVisibility).toBe("external"); - expect(restored.isExternalChannel).toBe(true); }); it("should default channelVisibility to unknown when missing from JSON", () => { @@ -250,23 +246,6 @@ describe("Serialization", () => { const thread = ThreadImpl.fromJSON(json); expect(thread.channelVisibility).toBe("unknown"); - expect(thread.isExternalChannel).toBe(false); - }); - - it("should migrate legacy isExternalChannel to channelVisibility", () => { - const json: SerializedThread = { - _type: "chat:Thread", - id: "slack:C123:1234.5678", - channelId: "C123", - isDM: false, - adapterName: "slack", - isExternalChannel: true, - }; - - const thread = ThreadImpl.fromJSON(json); - - expect(thread.channelVisibility).toBe("external"); - expect(thread.isExternalChannel).toBe(true); }); it("should serialize currentMessage", () => { diff --git a/packages/chat/src/thread.ts b/packages/chat/src/thread.ts index d0bb91fa..7d2b83be 100644 --- a/packages/chat/src/thread.ts +++ b/packages/chat/src/thread.ts @@ -40,10 +40,6 @@ export interface SerializedThread { currentMessage?: SerializedMessage; id: string; isDM: boolean; - /** - * @deprecated Use `channelVisibility` instead - */ - isExternalChannel?: boolean; } /** @@ -57,10 +53,6 @@ interface ThreadImplConfigWithAdapter { id: string; initialMessage?: Message; isDM?: boolean; - /** - * @deprecated Use `channelVisibility` instead - */ - isExternalChannel?: boolean; isSubscribedContext?: boolean; stateAdapter: StateAdapter; streamingUpdateIntervalMs?: number; @@ -78,10 +70,6 @@ interface ThreadImplConfigLazy { id: string; initialMessage?: Message; isDM?: boolean; - /** - * @deprecated Use `channelVisibility` instead - */ - isExternalChannel?: boolean; isSubscribedContext?: boolean; streamingUpdateIntervalMs?: number; } @@ -114,13 +102,6 @@ export class ThreadImpl> readonly isDM: boolean; readonly channelVisibility: ChannelVisibility; - /** - * @deprecated Use `channelVisibility === 'external'` instead - */ - get isExternalChannel(): boolean { - return this.channelVisibility === "external"; - } - /** Direct adapter instance (if provided) */ private _adapter?: Adapter; /** Adapter name for lazy resolution */ @@ -140,10 +121,7 @@ export class ThreadImpl> this.id = config.id; this.channelId = config.channelId; this.isDM = config.isDM ?? false; - // Support both new channelVisibility and deprecated isExternalChannel - this.channelVisibility = - config.channelVisibility ?? - (config.isExternalChannel ? "external" : "unknown"); + this.channelVisibility = config.channelVisibility ?? "unknown"; this._isSubscribedContext = config.isSubscribedContext ?? false; this._currentMessage = config.currentMessage; this._streamingUpdateIntervalMs = config.streamingUpdateIntervalMs ?? 500; @@ -582,8 +560,6 @@ export class ThreadImpl> channelVisibility: this.channelVisibility, currentMessage: this._currentMessage?.toJSON(), isDM: this.isDM, - // Keep isExternalChannel for backwards compatibility - ...(this.isExternalChannel ? { isExternalChannel: true } : {}), adapterName: this.adapter.name, }; } @@ -610,13 +586,11 @@ export class ThreadImpl> id: json.id, adapterName: json.adapterName, channelId: json.channelId, - // Prefer channelVisibility, fall back to isExternalChannel for backwards compat channelVisibility: json.channelVisibility, currentMessage: json.currentMessage ? Message.fromJSON(json.currentMessage) : undefined, isDM: json.isDM, - isExternalChannel: json.isExternalChannel, }); if (adapter) { thread._adapter = adapter; diff --git a/packages/chat/src/types.ts b/packages/chat/src/types.ts index be6c01c7..cae6492e 100644 --- a/packages/chat/src/types.ts +++ b/packages/chat/src/types.ts @@ -209,23 +209,11 @@ export interface Adapter { */ isDM?(threadId: string): boolean; - /** - * Check if a thread is in an external/shared channel (e.g., Slack Connect). - * - * External channels are shared between different organizations. Bots should - * be careful about what information they expose in external channels. - * - * @param threadId - The thread ID to check - * @returns True if the thread is in an external channel, false otherwise - * @deprecated Use `getChannelVisibility` instead for more granular visibility control - */ - isExternalChannel?(threadId: string): boolean; - /** * Get the visibility scope of a channel containing the thread. * - * This provides more granular control than `isExternalChannel` by distinguishing - * between private channels, workspace-visible channels, and externally shared channels. + * This distinguishes between private channels, workspace-visible channels, + * and externally shared channels (e.g., Slack Connect). * * @param threadId - The thread ID to check * @returns The channel visibility scope @@ -543,11 +531,6 @@ export interface Postable< readonly id: string; /** Whether this is a direct message conversation */ readonly isDM: boolean; - /** - * Whether this is an external/shared channel (e.g., Slack Connect). - * @deprecated Use `channelVisibility` instead for more granular visibility control - */ - readonly isExternalChannel: boolean; /** The visibility scope of this channel */ readonly channelVisibility: ChannelVisibility; @@ -644,11 +627,6 @@ export interface ThreadSummary { export interface ChannelInfo { id: string; isDM?: boolean; - /** - * Whether this is an external/shared channel (e.g., Slack Connect). - * @deprecated Use `channelVisibility` instead for more granular visibility control - */ - isExternalChannel?: boolean; /** The visibility scope of this channel */ channelVisibility?: ChannelVisibility; memberCount?: number; @@ -848,11 +826,6 @@ export interface ThreadInfo { id: string; /** Whether this is a direct message conversation */ isDM?: boolean; - /** - * Whether this is an external/shared channel (e.g., Slack Connect). - * @deprecated Use `channelVisibility` instead for more granular visibility control - */ - isExternalChannel?: boolean; /** The visibility scope of this channel */ channelVisibility?: ChannelVisibility; /** Platform-specific metadata */