Skip to content

Commit ecefc6c

Browse files
committed
fix(slack): align App Home with active commands
1 parent 12f7d9c commit ecefc6c

8 files changed

Lines changed: 95 additions & 25 deletions

File tree

CHANGELOG.md

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,6 @@ Docs: https://docs.openclaw.ai
4747
- **Windows Node resolution:** preserve the current executable when resolving bare case-insensitive `node.exe` entries under hostile `PATH` values. (#103907) Thanks @soldforaloss.
4848
- **Codex runtime switching:** accept the bundled Codex runtime for both `codex/*` and `openai/*` model routes while keeping unsupported provider/runtime pairs rejected. (#103762)
4949
- **Agent abort cleanup:** serialize prompt lock reacquisition with terminal cleanup so canceled embedded runs do not self-contend on session locks for up to 60 seconds.
50-
- **Slack App Home command hints:** show the configured native slash command instead of always `/openclaw` in the Home tab. (#102340) Thanks @jontsai.
5150
- **Chutes OAuth deadlines:** bound token exchange, profile lookup, and refresh requests, and keep issued tokens when optional userinfo enrichment stalls. (#102026) Thanks @Alix-007.
5251
- **Control UI workspace avatars:** inline validated agent avatar files in bootstrap and identity responses so Personal card images render without unauthenticated avatar-route requests, while preserving configured emoji precedence. (#102892, #97602) Thanks @LZY3538.
5352
- **Exec safe-bin flags:** auto-approve curated read-only boolean flags for default stdin-only filters while keeping unknown flags, tail follow/retry modes, file operands, and custom profiles fail-closed. (#88953) Thanks @yetval.

docs/channels/slack.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -912,7 +912,7 @@ For **HTTP Request URLs mode**, replace `settings` with the HTTP variant and add
912912

913913
Surface different features that extend the above defaults.
914914

915-
The default manifest enables the Slack App Home **Home** tab and subscribes to `app_home_opened`. When a workspace member opens the Home tab, OpenClaw publishes a safe default Home view with `views.publish`; no conversation payload or private configuration is included, and the command hint uses the configured `channels.slack.slashCommand.name`. The **Messages** tab remains enabled for Slack DMs. The manifest also enables Slack assistant threads with `features.assistant_view`, `assistant:write`, `assistant_thread_started`, and `assistant_thread_context_changed`; assistant threads route to their own OpenClaw thread sessions and keep Slack-provided thread context available to the agent.
915+
The default manifest enables the Slack App Home **Home** tab and subscribes to `app_home_opened`. When a workspace member opens the Home tab, OpenClaw publishes a safe default Home view with `views.publish`; no conversation payload or private configuration is included. When single slash command mode is enabled, the command hint uses `channels.slack.slashCommand.name`; installations using native commands or no slash commands omit that hint. The **Messages** tab remains enabled for Slack DMs. The manifest also enables Slack assistant threads with `features.assistant_view`, `assistant:write`, `assistant_thread_started`, and `assistant_thread_context_changed`; assistant threads route to their own OpenClaw thread sessions and keep Slack-provided thread context available to the agent.
916916

917917
<AccordionGroup>
918918
<Accordion title="Optional native slash commands">

extensions/slack/src/monitor/events.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ export function registerSlackMonitorEvents(params: {
1515
ctx: SlackMonitorContext;
1616
account: ResolvedSlackAccount;
1717
handleSlackMessage: SlackMessageHandler;
18+
appHomeSlashCommandName?: string;
1819
/** Called on each inbound event to update liveness tracking. */
1920
trackEvent?: () => void;
2021
}) {
@@ -29,7 +30,11 @@ export function registerSlackMonitorEvents(params: {
2930
registerSlackMemberEvents({ ctx: params.ctx, trackEvent: params.trackEvent });
3031
registerSlackChannelEvents({ ctx: params.ctx, trackEvent: params.trackEvent });
3132
registerSlackPinEvents({ ctx: params.ctx, trackEvent: params.trackEvent });
32-
registerSlackHomeEvents({ ctx: params.ctx, trackEvent: params.trackEvent });
33+
registerSlackHomeEvents({
34+
ctx: params.ctx,
35+
slashCommandName: params.appHomeSlashCommandName,
36+
trackEvent: params.trackEvent,
37+
});
3338
registerSlackInteractionEvents({ ctx: params.ctx, trackEvent: params.trackEvent });
3439
registerSlackAssistantEvents({ ctx: params.ctx, trackEvent: params.trackEvent });
3540
}

extensions/slack/src/monitor/events/home.test.ts

Lines changed: 12 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -17,17 +17,15 @@ function createHomeContext(params?: {
1717
if (params?.shouldDropMismatchedSlackEvent) {
1818
harness.ctx.shouldDropMismatchedSlackEvent = params.shouldDropMismatchedSlackEvent;
1919
}
20-
harness.ctx.slashCommand = {
21-
enabled: true,
22-
name: params?.slashCommandName ?? "openclaw",
23-
sessionPrefix: "slack:slash",
24-
ephemeral: true,
25-
};
2620
harness.ctx.botToken = "xoxb-test";
2721
(harness.ctx.app as unknown as { client: { views: { publish: typeof publish } } }).client = {
2822
views: { publish },
2923
};
30-
registerSlackHomeEvents({ ctx: harness.ctx, trackEvent: params?.trackEvent });
24+
registerSlackHomeEvents({
25+
ctx: harness.ctx,
26+
slashCommandName: params?.slashCommandName,
27+
trackEvent: params?.trackEvent,
28+
});
3129
return {
3230
publish,
3331
getHomeHandler: () => harness.getHandler("app_home_opened") as HomeHandler | null,
@@ -44,7 +42,7 @@ describe("registerSlackHomeEvents", () => {
4442
vi.clearAllMocks();
4543
});
4644

47-
it("publishes the default Home tab view for app_home_opened", async () => {
45+
it("publishes the Home tab without an inactive slash command hint", async () => {
4846
const trackEvent = vi.fn();
4947
const { publish, getHomeHandler } = createHomeContext({ trackEvent });
5048
const handler = getHomeHandler();
@@ -70,6 +68,12 @@ describe("registerSlackHomeEvents", () => {
7068
user_id: "U123",
7169
view: buildSlackHomeView(),
7270
});
71+
expect(buildSlackHomeView().blocks[1]).toMatchObject({
72+
type: "section",
73+
text: {
74+
text: "Send a DM or mention OpenClaw in a channel to start a session.",
75+
},
76+
});
7377
});
7478

7579
it("publishes the configured slash command name", async () => {

extensions/slack/src/monitor/events/home.ts

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,10 @@ import { danger } from "openclaw/plugin-sdk/runtime-env";
66
import type { SlackMonitorContext } from "../context.js";
77
import type { SlackAppHomeOpenedEvent } from "../types.js";
88

9-
export function buildSlackHomeView(slashCommandName = "openclaw"): HomeView {
9+
export function buildSlackHomeView(slashCommandName?: string): HomeView {
10+
const startSessionText = slashCommandName
11+
? `Send a DM, mention OpenClaw in a channel, or use \`/${slashCommandName}\` to start a session.`
12+
: "Send a DM or mention OpenClaw in a channel to start a session.";
1013
return {
1114
type: "home",
1215
callback_id: "openclaw:home",
@@ -22,7 +25,7 @@ export function buildSlackHomeView(slashCommandName = "openclaw"): HomeView {
2225
type: "section",
2326
text: {
2427
type: "mrkdwn",
25-
text: `Send a DM, mention OpenClaw in a channel, or use \`/${slashCommandName}\` to start a session.`,
28+
text: startSessionText,
2629
},
2730
},
2831
{
@@ -40,9 +43,10 @@ export function buildSlackHomeView(slashCommandName = "openclaw"): HomeView {
4043

4144
export function registerSlackHomeEvents(params: {
4245
ctx: SlackMonitorContext;
46+
slashCommandName?: string;
4347
trackEvent?: () => void;
4448
}) {
45-
const { ctx, trackEvent } = params;
49+
const { ctx, slashCommandName, trackEvent } = params;
4650

4751
ctx.app.event(
4852
"app_home_opened",
@@ -61,7 +65,7 @@ export function registerSlackHomeEvents(params: {
6165
await ctx.app.client.views.publish({
6266
token: ctx.botToken,
6367
user_id: payload.user,
64-
view: buildSlackHomeView(ctx.slashCommand.name),
68+
view: buildSlackHomeView(slashCommandName),
6569
});
6670
} catch (err) {
6771
ctx.runtime.error?.(danger(`slack app home handler failed: ${formatErrorMessage(err)}`));

extensions/slack/src/monitor/provider.ts

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -501,10 +501,20 @@ export async function monitorSlackProvider(opts: MonitorSlackOpts = {}) {
501501
});
502502
}
503503

504-
registerSlackMonitorEvents({ ctx, account, handleSlackMessage, trackEvent });
505-
if (installationIdentity.kind !== "enterprise") {
506-
await registerSlackMonitorSlashCommands({ ctx, account, trackEvent });
507-
}
504+
// Resolve command registration first so App Home never advertises an inactive single command.
505+
const commandRegistration =
506+
installationIdentity.kind === "enterprise"
507+
? ({ mode: "disabled" } as const)
508+
: await registerSlackMonitorSlashCommands({ ctx, account, trackEvent });
509+
const appHomeSlashCommandName =
510+
commandRegistration.mode === "single" ? commandRegistration.name : undefined;
511+
registerSlackMonitorEvents({
512+
ctx,
513+
account,
514+
handleSlackMessage,
515+
appHomeSlashCommandName,
516+
trackEvent,
517+
});
508518
if (slackMode === "http" && slackHttpHandler) {
509519
unregisterHttpHandler = registerSlackHttpHandler({
510520
path: slackWebhookPath,

extensions/slack/src/monitor/slash.test.ts

Lines changed: 39 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -306,7 +306,10 @@ vi.mock("./slash-commands.runtime.js", () => {
306306
};
307307
});
308308

309-
type RegisterFn = (params: { ctx: unknown; account: unknown }) => Promise<void>;
309+
type RegisterFn = (params: {
310+
ctx: unknown;
311+
account: unknown;
312+
}) => Promise<{ mode: "single"; name: string } | { mode: "native" } | { mode: "disabled" }>;
310313
const { registerSlackMonitorSlashCommands } = (await import("./slash.js")) as {
311314
registerSlackMonitorSlashCommands: RegisterFn;
312315
};
@@ -324,7 +327,7 @@ afterEach(() => {
324327
});
325328

326329
async function registerCommands(ctx: unknown, account: unknown, trackEvent?: () => void) {
327-
await registerSlackMonitorSlashCommands({
330+
return await registerSlackMonitorSlashCommands({
328331
ctx: ctx as never,
329332
account: account as never,
330333
trackEvent,
@@ -1207,6 +1210,8 @@ function createPolicyHarness(overrides?: {
12071210
allowFrom?: string[];
12081211
useAccessGroups?: boolean;
12091212
slashEphemeral?: boolean;
1213+
slashCommandEnabled?: boolean;
1214+
slashCommandName?: string;
12101215
shouldDropMismatchedSlackEvent?: (body: unknown) => boolean;
12111216
resolveChannelName?: () => Promise<{ name?: string; type?: string }>;
12121217
}) {
@@ -1238,8 +1243,8 @@ function createPolicyHarness(overrides?: {
12381243
useAccessGroups: overrides?.useAccessGroups ?? true,
12391244
channelsConfig: overrides?.channelsConfig,
12401245
slashCommand: {
1241-
enabled: true,
1242-
name: "openclaw",
1246+
enabled: overrides?.slashCommandEnabled ?? true,
1247+
name: overrides?.slashCommandName ?? "openclaw",
12431248
ephemeral: overrides?.slashEphemeral ?? true,
12441249
sessionPrefix: "slack:slash",
12451250
},
@@ -1335,6 +1340,36 @@ function expectUnauthorizedResponse(respond: ReturnType<typeof vi.fn>) {
13351340
});
13361341
}
13371342

1343+
describe("Slack App Home command presentation", () => {
1344+
it("returns the configured single command when it is registered", async () => {
1345+
const harness = createPolicyHarness({ slashCommandName: "acme" });
1346+
1347+
await expect(registerCommands(harness.ctx, harness.account)).resolves.toEqual({
1348+
mode: "single",
1349+
name: "acme",
1350+
});
1351+
expect(harness.commands.size).toBe(1);
1352+
});
1353+
1354+
it("omits the single command when slash commands are disabled", async () => {
1355+
const harness = createPolicyHarness({ slashCommandEnabled: false });
1356+
1357+
await expect(registerCommands(harness.ctx, harness.account)).resolves.toEqual({
1358+
mode: "disabled",
1359+
});
1360+
expect(harness.commands.size).toBe(0);
1361+
});
1362+
1363+
it("omits the single command when native commands take precedence", async () => {
1364+
const harness = createArgMenusHarness();
1365+
1366+
await expect(registerCommands(harness.ctx, harness.account)).resolves.toEqual({
1367+
mode: "native",
1368+
});
1369+
expect(harness.commands.size).toBeGreaterThan(0);
1370+
});
1371+
});
1372+
13381373
describe("slack slash commands channel policy", () => {
13391374
it("drops mismatched slash payloads before dispatch", async () => {
13401375
const harness = createPolicyHarness({

extensions/slack/src/monitor/slash.ts

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -367,11 +367,16 @@ function buildSlackCommandArgMenuBlocks(params: {
367367
];
368368
}
369369

370+
type SlackCommandRegistration =
371+
| { mode: "single"; name: string }
372+
| { mode: "native" }
373+
| { mode: "disabled" };
374+
370375
export async function registerSlackMonitorSlashCommands(params: {
371376
ctx: SlackMonitorContext;
372377
account: ResolvedSlackAccount;
373378
trackEvent?: () => void;
374-
}): Promise<void> {
379+
}): Promise<SlackCommandRegistration> {
375380
const { ctx, account, trackEvent } = params;
376381
const startupCfg = ctx.cfg;
377382
const runtime = ctx.runtime;
@@ -906,8 +911,15 @@ export async function registerSlackMonitorSlashCommands(params: {
906911
logVerbose("slack: slash commands disabled");
907912
}
908913

914+
const registration: SlackCommandRegistration =
915+
nativeCommands.length > 0
916+
? { mode: "native" }
917+
: slashCommand.enabled
918+
? { mode: "single", name: slashCommand.name }
919+
: { mode: "disabled" };
920+
909921
if (nativeCommands.length === 0 || !supportsInteractiveArgMenus) {
910-
return;
922+
return registration;
911923
}
912924

913925
const registerArgOptions = () => {
@@ -1082,4 +1094,5 @@ export async function registerSlackMonitorSlashCommands(params: {
10821094
});
10831095
};
10841096
registerArgAction(SLACK_COMMAND_ARG_ACTION_LISTENER);
1097+
return registration;
10851098
}

0 commit comments

Comments
 (0)