mirror of
https://github.com/openclaw/openclaw.git
synced 2026-07-18 22:41:43 +00:00
fix(slack): align App Home with active commands
This commit is contained in:
committed by
Peter Steinberger
parent
12f7d9c784
commit
ecefc6cab6
@@ -47,7 +47,6 @@ Docs: https://docs.openclaw.ai
|
||||
- **Windows Node resolution:** preserve the current executable when resolving bare case-insensitive `node.exe` entries under hostile `PATH` values. (#103907) Thanks @soldforaloss.
|
||||
- **Codex runtime switching:** accept the bundled Codex runtime for both `codex/*` and `openai/*` model routes while keeping unsupported provider/runtime pairs rejected. (#103762)
|
||||
- **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.
|
||||
- **Slack App Home command hints:** show the configured native slash command instead of always `/openclaw` in the Home tab. (#102340) Thanks @jontsai.
|
||||
- **Chutes OAuth deadlines:** bound token exchange, profile lookup, and refresh requests, and keep issued tokens when optional userinfo enrichment stalls. (#102026) Thanks @Alix-007.
|
||||
- **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.
|
||||
- **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.
|
||||
|
||||
@@ -912,7 +912,7 @@ For **HTTP Request URLs mode**, replace `settings` with the HTTP variant and add
|
||||
|
||||
Surface different features that extend the above defaults.
|
||||
|
||||
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.
|
||||
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.
|
||||
|
||||
<AccordionGroup>
|
||||
<Accordion title="Optional native slash commands">
|
||||
|
||||
@@ -15,6 +15,7 @@ export function registerSlackMonitorEvents(params: {
|
||||
ctx: SlackMonitorContext;
|
||||
account: ResolvedSlackAccount;
|
||||
handleSlackMessage: SlackMessageHandler;
|
||||
appHomeSlashCommandName?: string;
|
||||
/** Called on each inbound event to update liveness tracking. */
|
||||
trackEvent?: () => void;
|
||||
}) {
|
||||
@@ -29,7 +30,11 @@ export function registerSlackMonitorEvents(params: {
|
||||
registerSlackMemberEvents({ ctx: params.ctx, trackEvent: params.trackEvent });
|
||||
registerSlackChannelEvents({ ctx: params.ctx, trackEvent: params.trackEvent });
|
||||
registerSlackPinEvents({ ctx: params.ctx, trackEvent: params.trackEvent });
|
||||
registerSlackHomeEvents({ ctx: params.ctx, trackEvent: params.trackEvent });
|
||||
registerSlackHomeEvents({
|
||||
ctx: params.ctx,
|
||||
slashCommandName: params.appHomeSlashCommandName,
|
||||
trackEvent: params.trackEvent,
|
||||
});
|
||||
registerSlackInteractionEvents({ ctx: params.ctx, trackEvent: params.trackEvent });
|
||||
registerSlackAssistantEvents({ ctx: params.ctx, trackEvent: params.trackEvent });
|
||||
}
|
||||
|
||||
@@ -17,17 +17,15 @@ function createHomeContext(params?: {
|
||||
if (params?.shouldDropMismatchedSlackEvent) {
|
||||
harness.ctx.shouldDropMismatchedSlackEvent = params.shouldDropMismatchedSlackEvent;
|
||||
}
|
||||
harness.ctx.slashCommand = {
|
||||
enabled: true,
|
||||
name: params?.slashCommandName ?? "openclaw",
|
||||
sessionPrefix: "slack:slash",
|
||||
ephemeral: true,
|
||||
};
|
||||
harness.ctx.botToken = "xoxb-test";
|
||||
(harness.ctx.app as unknown as { client: { views: { publish: typeof publish } } }).client = {
|
||||
views: { publish },
|
||||
};
|
||||
registerSlackHomeEvents({ ctx: harness.ctx, trackEvent: params?.trackEvent });
|
||||
registerSlackHomeEvents({
|
||||
ctx: harness.ctx,
|
||||
slashCommandName: params?.slashCommandName,
|
||||
trackEvent: params?.trackEvent,
|
||||
});
|
||||
return {
|
||||
publish,
|
||||
getHomeHandler: () => harness.getHandler("app_home_opened") as HomeHandler | null,
|
||||
@@ -44,7 +42,7 @@ describe("registerSlackHomeEvents", () => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
it("publishes the default Home tab view for app_home_opened", async () => {
|
||||
it("publishes the Home tab without an inactive slash command hint", async () => {
|
||||
const trackEvent = vi.fn();
|
||||
const { publish, getHomeHandler } = createHomeContext({ trackEvent });
|
||||
const handler = getHomeHandler();
|
||||
@@ -70,6 +68,12 @@ describe("registerSlackHomeEvents", () => {
|
||||
user_id: "U123",
|
||||
view: buildSlackHomeView(),
|
||||
});
|
||||
expect(buildSlackHomeView().blocks[1]).toMatchObject({
|
||||
type: "section",
|
||||
text: {
|
||||
text: "Send a DM or mention OpenClaw in a channel to start a session.",
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it("publishes the configured slash command name", async () => {
|
||||
|
||||
@@ -6,7 +6,10 @@ import { danger } from "openclaw/plugin-sdk/runtime-env";
|
||||
import type { SlackMonitorContext } from "../context.js";
|
||||
import type { SlackAppHomeOpenedEvent } from "../types.js";
|
||||
|
||||
export function buildSlackHomeView(slashCommandName = "openclaw"): HomeView {
|
||||
export function buildSlackHomeView(slashCommandName?: string): HomeView {
|
||||
const startSessionText = slashCommandName
|
||||
? `Send a DM, mention OpenClaw in a channel, or use \`/${slashCommandName}\` to start a session.`
|
||||
: "Send a DM or mention OpenClaw in a channel to start a session.";
|
||||
return {
|
||||
type: "home",
|
||||
callback_id: "openclaw:home",
|
||||
@@ -22,7 +25,7 @@ export function buildSlackHomeView(slashCommandName = "openclaw"): HomeView {
|
||||
type: "section",
|
||||
text: {
|
||||
type: "mrkdwn",
|
||||
text: `Send a DM, mention OpenClaw in a channel, or use \`/${slashCommandName}\` to start a session.`,
|
||||
text: startSessionText,
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -40,9 +43,10 @@ export function buildSlackHomeView(slashCommandName = "openclaw"): HomeView {
|
||||
|
||||
export function registerSlackHomeEvents(params: {
|
||||
ctx: SlackMonitorContext;
|
||||
slashCommandName?: string;
|
||||
trackEvent?: () => void;
|
||||
}) {
|
||||
const { ctx, trackEvent } = params;
|
||||
const { ctx, slashCommandName, trackEvent } = params;
|
||||
|
||||
ctx.app.event(
|
||||
"app_home_opened",
|
||||
@@ -61,7 +65,7 @@ export function registerSlackHomeEvents(params: {
|
||||
await ctx.app.client.views.publish({
|
||||
token: ctx.botToken,
|
||||
user_id: payload.user,
|
||||
view: buildSlackHomeView(ctx.slashCommand.name),
|
||||
view: buildSlackHomeView(slashCommandName),
|
||||
});
|
||||
} catch (err) {
|
||||
ctx.runtime.error?.(danger(`slack app home handler failed: ${formatErrorMessage(err)}`));
|
||||
|
||||
@@ -501,10 +501,20 @@ export async function monitorSlackProvider(opts: MonitorSlackOpts = {}) {
|
||||
});
|
||||
}
|
||||
|
||||
registerSlackMonitorEvents({ ctx, account, handleSlackMessage, trackEvent });
|
||||
if (installationIdentity.kind !== "enterprise") {
|
||||
await registerSlackMonitorSlashCommands({ ctx, account, trackEvent });
|
||||
}
|
||||
// Resolve command registration first so App Home never advertises an inactive single command.
|
||||
const commandRegistration =
|
||||
installationIdentity.kind === "enterprise"
|
||||
? ({ mode: "disabled" } as const)
|
||||
: await registerSlackMonitorSlashCommands({ ctx, account, trackEvent });
|
||||
const appHomeSlashCommandName =
|
||||
commandRegistration.mode === "single" ? commandRegistration.name : undefined;
|
||||
registerSlackMonitorEvents({
|
||||
ctx,
|
||||
account,
|
||||
handleSlackMessage,
|
||||
appHomeSlashCommandName,
|
||||
trackEvent,
|
||||
});
|
||||
if (slackMode === "http" && slackHttpHandler) {
|
||||
unregisterHttpHandler = registerSlackHttpHandler({
|
||||
path: slackWebhookPath,
|
||||
|
||||
@@ -306,7 +306,10 @@ vi.mock("./slash-commands.runtime.js", () => {
|
||||
};
|
||||
});
|
||||
|
||||
type RegisterFn = (params: { ctx: unknown; account: unknown }) => Promise<void>;
|
||||
type RegisterFn = (params: {
|
||||
ctx: unknown;
|
||||
account: unknown;
|
||||
}) => Promise<{ mode: "single"; name: string } | { mode: "native" } | { mode: "disabled" }>;
|
||||
const { registerSlackMonitorSlashCommands } = (await import("./slash.js")) as {
|
||||
registerSlackMonitorSlashCommands: RegisterFn;
|
||||
};
|
||||
@@ -324,7 +327,7 @@ afterEach(() => {
|
||||
});
|
||||
|
||||
async function registerCommands(ctx: unknown, account: unknown, trackEvent?: () => void) {
|
||||
await registerSlackMonitorSlashCommands({
|
||||
return await registerSlackMonitorSlashCommands({
|
||||
ctx: ctx as never,
|
||||
account: account as never,
|
||||
trackEvent,
|
||||
@@ -1207,6 +1210,8 @@ function createPolicyHarness(overrides?: {
|
||||
allowFrom?: string[];
|
||||
useAccessGroups?: boolean;
|
||||
slashEphemeral?: boolean;
|
||||
slashCommandEnabled?: boolean;
|
||||
slashCommandName?: string;
|
||||
shouldDropMismatchedSlackEvent?: (body: unknown) => boolean;
|
||||
resolveChannelName?: () => Promise<{ name?: string; type?: string }>;
|
||||
}) {
|
||||
@@ -1238,8 +1243,8 @@ function createPolicyHarness(overrides?: {
|
||||
useAccessGroups: overrides?.useAccessGroups ?? true,
|
||||
channelsConfig: overrides?.channelsConfig,
|
||||
slashCommand: {
|
||||
enabled: true,
|
||||
name: "openclaw",
|
||||
enabled: overrides?.slashCommandEnabled ?? true,
|
||||
name: overrides?.slashCommandName ?? "openclaw",
|
||||
ephemeral: overrides?.slashEphemeral ?? true,
|
||||
sessionPrefix: "slack:slash",
|
||||
},
|
||||
@@ -1335,6 +1340,36 @@ function expectUnauthorizedResponse(respond: ReturnType<typeof vi.fn>) {
|
||||
});
|
||||
}
|
||||
|
||||
describe("Slack App Home command presentation", () => {
|
||||
it("returns the configured single command when it is registered", async () => {
|
||||
const harness = createPolicyHarness({ slashCommandName: "acme" });
|
||||
|
||||
await expect(registerCommands(harness.ctx, harness.account)).resolves.toEqual({
|
||||
mode: "single",
|
||||
name: "acme",
|
||||
});
|
||||
expect(harness.commands.size).toBe(1);
|
||||
});
|
||||
|
||||
it("omits the single command when slash commands are disabled", async () => {
|
||||
const harness = createPolicyHarness({ slashCommandEnabled: false });
|
||||
|
||||
await expect(registerCommands(harness.ctx, harness.account)).resolves.toEqual({
|
||||
mode: "disabled",
|
||||
});
|
||||
expect(harness.commands.size).toBe(0);
|
||||
});
|
||||
|
||||
it("omits the single command when native commands take precedence", async () => {
|
||||
const harness = createArgMenusHarness();
|
||||
|
||||
await expect(registerCommands(harness.ctx, harness.account)).resolves.toEqual({
|
||||
mode: "native",
|
||||
});
|
||||
expect(harness.commands.size).toBeGreaterThan(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe("slack slash commands channel policy", () => {
|
||||
it("drops mismatched slash payloads before dispatch", async () => {
|
||||
const harness = createPolicyHarness({
|
||||
|
||||
@@ -367,11 +367,16 @@ function buildSlackCommandArgMenuBlocks(params: {
|
||||
];
|
||||
}
|
||||
|
||||
type SlackCommandRegistration =
|
||||
| { mode: "single"; name: string }
|
||||
| { mode: "native" }
|
||||
| { mode: "disabled" };
|
||||
|
||||
export async function registerSlackMonitorSlashCommands(params: {
|
||||
ctx: SlackMonitorContext;
|
||||
account: ResolvedSlackAccount;
|
||||
trackEvent?: () => void;
|
||||
}): Promise<void> {
|
||||
}): Promise<SlackCommandRegistration> {
|
||||
const { ctx, account, trackEvent } = params;
|
||||
const startupCfg = ctx.cfg;
|
||||
const runtime = ctx.runtime;
|
||||
@@ -906,8 +911,15 @@ export async function registerSlackMonitorSlashCommands(params: {
|
||||
logVerbose("slack: slash commands disabled");
|
||||
}
|
||||
|
||||
const registration: SlackCommandRegistration =
|
||||
nativeCommands.length > 0
|
||||
? { mode: "native" }
|
||||
: slashCommand.enabled
|
||||
? { mode: "single", name: slashCommand.name }
|
||||
: { mode: "disabled" };
|
||||
|
||||
if (nativeCommands.length === 0 || !supportsInteractiveArgMenus) {
|
||||
return;
|
||||
return registration;
|
||||
}
|
||||
|
||||
const registerArgOptions = () => {
|
||||
@@ -1082,4 +1094,5 @@ export async function registerSlackMonitorSlashCommands(params: {
|
||||
});
|
||||
};
|
||||
registerArgAction(SLACK_COMMAND_ARG_ACTION_LISTENER);
|
||||
return registration;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user