Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ Add to your `~/.openacp/config.json`:
| `autoCreateSession` | Create a startup session on boot. Default: `true` |
| `outputMode` | Default verbosity: `"low"`, `"medium"`, or `"high"`. Default: `"medium"` |
| `subscribedChannels` | Optional. Channels to watch: `[{ "channelId": "C...", "trigger": "mention" \| "all" }]`. Invite the bot to each. Default: `[]` |
| `broadcastReplies` | Optional. Also mirror the bot's threaded replies into the channel's main timeline (Slack `reply_broadcast`), so important answers are visible without opening the thread. Default: `false` |

### Output Mode

Expand Down
10 changes: 10 additions & 0 deletions src/__tests__/config-schema.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,4 +11,14 @@ describe("SlackChannelConfigSchema", () => {
const cfg = SlackChannelConfigSchema.parse({ respondToDms: false });
expect(cfg.respondToDms).toBe(false);
});

it("defaults broadcastReplies to false", () => {
const cfg = SlackChannelConfigSchema.parse({});
expect(cfg.broadcastReplies).toBe(false);
});

it("allows broadcastReplies to be enabled", () => {
const cfg = SlackChannelConfigSchema.parse({ broadcastReplies: true });
expect(cfg.broadcastReplies).toBe(true);
});
});
30 changes: 30 additions & 0 deletions src/__tests__/text-buffer.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -137,4 +137,34 @@ describe("SlackTextBuffer", () => {
expect.objectContaining({ channel: "C_SUB", thread_ts: "169.1" }),
);
});

it("does not set reply_broadcast by default", async () => {
const enqueue = vi.fn().mockResolvedValue({ ts: "1.1" });
const queue = { enqueue } as any;
const buf = new SlackTextBuffer("C_SUB", "169.1", "sess-1", queue);
buf.append("hello world");
await buf.flush();
expect(enqueue.mock.calls[0][1]).not.toHaveProperty("reply_broadcast");
});

it("sets reply_broadcast on threaded replies when broadcast is enabled", async () => {
const enqueue = vi.fn().mockResolvedValue({ ts: "1.1" });
const queue = { enqueue } as any;
const buf = new SlackTextBuffer("C_SUB", "169.1", "sess-1", queue, undefined, true);
buf.append("important answer");
await buf.flush();
expect(enqueue).toHaveBeenCalledWith(
"chat.postMessage",
expect.objectContaining({ thread_ts: "169.1", reply_broadcast: true }),
);
});

it("omits reply_broadcast when broadcast is enabled but there is no thread", async () => {
const enqueue = vi.fn().mockResolvedValue({ ts: "1.1" });
const queue = { enqueue } as any;
const buf = new SlackTextBuffer("C_SUB", undefined, "sess-1", queue, undefined, true);
buf.append("dm answer");
await buf.flush();
expect(enqueue.mock.calls[0][1]).not.toHaveProperty("reply_broadcast");
});
});
2 changes: 1 addition & 1 deletion src/adapter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1053,7 +1053,7 @@ export class SlackAdapter extends MessagingAdapter {
private getTextBuffer(sessionId: string, channelId: string, threadTs?: string): SlackTextBuffer {
let buf = this.textBuffers.get(sessionId);
if (!buf) {
buf = new SlackTextBuffer(channelId, threadTs, sessionId, this.queue, this.log);
buf = new SlackTextBuffer(channelId, threadTs, sessionId, this.queue, this.log, this.slackConfig.broadcastReplies);
this.textBuffers.set(sessionId, buf);
}
return buf;
Expand Down
4 changes: 4 additions & 0 deletions src/text-buffer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ export class SlackTextBuffer {
private sessionId: string,
private queue: ISlackSendQueue,
logger?: Logger,
private broadcast: boolean = false,
) {
this.log = logger ?? { info() {}, warn() {}, error() {}, debug() {} };
}
Expand Down Expand Up @@ -57,6 +58,9 @@ export class SlackTextBuffer {
const result = await this.queue.enqueue("chat.postMessage", {
channel: this.channelId,
...(this.threadTs ? { thread_ts: this.threadTs } : {}),
// reply_broadcast only has meaning on a threaded reply; Slack
// ignores it on top-level posts.
...(this.threadTs && this.broadcast ? { reply_broadcast: true } : {}),
text: chunk,
blocks: [{ type: "section", text: { type: "mrkdwn", text: chunk } }],
});
Expand Down
7 changes: 7 additions & 0 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,13 @@ export const SlackChannelConfigSchema = z.object({
* gates who may start a DM session. Set false to ignore DMs entirely.
*/
respondToDms: z.boolean().default(true),
/**
* When true, the bot's reply text in subscribed-channel threads is also
* mirrored into the channel's main timeline via Slack's `reply_broadcast`,
* so important answers are visible without opening the thread. Only affects
* threaded replies; DM and non-threaded output are unchanged. Default false.
*/
broadcastReplies: z.boolean().default(false),
});

export type SlackChannelConfig = z.infer<typeof SlackChannelConfigSchema>;
Expand Down