Skip to content
Merged
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
5 changes: 5 additions & 0 deletions packages/junior/src/chat/slack/channel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,11 @@ export interface SlackThreadReply {
type?: string;
files?: SlackFileRef[];
attachments?: unknown[];
reactions?: Array<{
name?: string;
count?: number;
users?: string[];
}>;
}

/** List channel history using Slack-native, pre-validated timestamp bounds. */
Expand Down
33 changes: 22 additions & 11 deletions packages/junior/src/chat/slack/tools/channel-list-messages.ts
Original file line number Diff line number Diff line change
Expand Up @@ -79,34 +79,45 @@ export function createSlackChannelListMessagesTool(context: SlackToolContext) {
readOnlyHint: true,
},
inputSchema: z.object({
channel_id: slackChannelRefParam.optional(),
channel_id: slackChannelRefParam.nullable().optional(),
limit: z.coerce
.number()
.int()
.min(1)
.max(1000)
.describe("Maximum number of messages to return across pages.")
.nullable()
.optional(),
cursor: z
.string()
.min(1)
.describe("Optional cursor to continue from a prior call.")
.describe(
"Cursor from `next_cursor` in a prior result. Use null or omit it for the first page; never invent a cursor.",
)
.nullable()
.optional(),
oldest: slackTimestampParam(
"Oldest message timestamp (Slack ts) for range filtering.",
).optional(),
)
.nullable()
.optional(),
latest: slackTimestampParam(
"Latest message timestamp (Slack ts) for range filtering.",
).optional(),
)
.nullable()
.optional(),
inclusive: booleanInput(
"Whether oldest/latest bounds should be inclusive.",
).optional(),
)
.nullable()
.optional(),
max_pages: z.coerce
.number()
.int()
.min(1)
.max(10)
.describe("Maximum number of API pages to traverse in a single call.")
.nullable()
.optional(),
}),
outputSchema: juniorToolOutputSchema,
Expand All @@ -121,7 +132,7 @@ export function createSlackChannelListMessagesTool(context: SlackToolContext) {
}) => {
const target = await resolveOptionalSlackChannelRef({
field: "channel_id",
value: channel_id,
value: channel_id ?? undefined,
defaultChannelId: context.destinationChannelId,
teamId: context.teamId,
});
Expand All @@ -141,15 +152,15 @@ export function createSlackChannelListMessagesTool(context: SlackToolContext) {

const normalizedOldest = normalizeRangeTimestamp(
"oldest",
oldest,
oldest ?? undefined,
targetChannelId,
);
if (!normalizedOldest.ok) {
throw new ToolInputError(normalizedOldest.error);
}
const normalizedLatest = normalizeRangeTimestamp(
"latest",
latest,
latest ?? undefined,
targetChannelId,
);
if (!normalizedLatest.ok) {
Expand All @@ -160,11 +171,11 @@ export function createSlackChannelListMessagesTool(context: SlackToolContext) {
listChannelMessages({
channelId: targetChannelId,
limit: limit ?? 100,
cursor,
cursor: cursor ?? undefined,
oldest: normalizedOldest.value,
latest: normalizedLatest.value,
inclusive,
maxPages: max_pages,
inclusive: inclusive ?? undefined,
maxPages: max_pages ?? undefined,
});

let result: Awaited<ReturnType<typeof listChannelMessages>> | undefined;
Expand Down
28 changes: 23 additions & 5 deletions packages/junior/src/chat/slack/tools/thread-read.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,15 @@ function sanitizeMessage(
subtype: msg.subtype,
bot_id: msg.bot_id,
type: msg.type,
...(msg.reactions?.length
? {
reactions: msg.reactions.map((reaction) => ({
name: reaction.name,
count: reaction.count,
users: reaction.users,
})),
}
: undefined),
...(attachmentText ? { attachment_text: attachmentText } : undefined),
...(files.length
? {
Expand Down Expand Up @@ -119,32 +128,41 @@ export function createSlackThreadReadTool(
) {
return zodTool({
description:
"Read a Slack thread from a shared archive URL or explicit channel + timestamp. Works for the current conversation and public channels.",
"Read a Slack thread, including message reaction users, from a shared archive URL or explicit channel + timestamp. Works for the current conversation and public channels.",
annotations: {
destructiveHint: false,
idempotentHint: true,
openWorldHint: true,
readOnlyHint: true,
},
inputSchema: z.object({
url: z.string().min(1).describe("Slack message archive URL.").optional(),
channel_id: slackChannelRefParam.optional(),
url: z
.string()
.min(1)
.describe("Slack message archive URL.")
.nullable()
.optional(),
channel_id: slackChannelRefParam.nullable().optional(),
ts: slackTimestampParam(
"Slack message timestamp. May be the thread root or any message in the thread.",
).optional(),
)
.nullable()
.optional(),
limit: z.coerce
.number()
.int()
.min(1)
.max(1000)
.describe("Maximum number of thread messages to fetch.")
.nullable()
.optional(),
max_pages: z.coerce
.number()
.int()
.min(1)
.max(10)
.describe("Maximum number of Slack API pages to traverse.")
.nullable()
.optional(),
}),
outputSchema: juniorToolOutputSchema,
Expand Down Expand Up @@ -197,7 +215,7 @@ export function createSlackThreadReadTool(
channelId,
threadTs: lookupTs,
limit: limit ?? 1000,
maxPages: max_pages,
maxPages: max_pages ?? undefined,
});

let replies: SlackThreadReply[] | undefined;
Expand Down
3 changes: 3 additions & 0 deletions packages/junior/tests/integration/slack-channel-tools.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -193,9 +193,12 @@ describe("slack channel tools", () => {
);

const result = await executeTool(tool, {
channel_id: null,
limit: 150,
cursor: null,
oldest: "1690000000.000",
latest: "1710000000",
inclusive: null,
max_pages: 3,
});

Expand Down
23 changes: 22 additions & 1 deletion packages/junior/tests/integration/slack-thread-read.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -171,6 +171,13 @@ describe("slackThreadRead", () => {
thread_ts: "1700000000.500000",
user: "U1",
text: "standalone message",
reactions: [
{
name: "raised_hands",
count: 2,
users: ["U2", "U3"],
},
],
},
],
}),
Expand All @@ -180,13 +187,27 @@ describe("slackThreadRead", () => {
const result = await executeTool(tool, {
channel_id: "C0MANUAL",
ts: "1700000000.500000",
url: null,
limit: null,
max_pages: null,
});

expect(result).toMatchObject({
channel_id: "C0MANUAL",
count: 1,
messages: [
{
text: "standalone message",
reactions: [
{
name: "raised_hands",
count: 2,
users: ["U2", "U3"],
},
],
},
],
});
expect(result.messages[0].text).toBe("standalone message");
});

it("allows reading a private channel when it matches the current channel", async () => {
Expand Down
Loading