Files
twenty/packages/twenty-apps/public/slack/src/logic-functions/utils/fetch-workspace-base-url.ts
T
Abdul Rahman 42aa566e32 feat(twenty-slack): link records and format the assistant reply footer (#23539)
Follow-up to the Slack bot branch, improving how assistant replies read
in Slack.

## Problem

The assistant never had the workspace URL. `buildSlackAssistantPrompt`
injected the request, requester and thread context, and the agent prompt
said nothing about links, so a record it created or found came back as
plain text with no way to open it. The reply was also a single
`markdown_text` blob with `_Answered in 3s_` appended to the answer.

## Changes

**Record deep links.** `fetchWorkspaceBaseUrl` resolves the workspace
URL from `currentWorkspace { workspaceUrls }`, preferring a custom
domain over the subdomain. It runs in parallel with the existing Slack
context fetch, so no extra latency. The prompt carries the base URL plus
the `[Record Name](base/object/<objectNameSingular>/<recordId>)` rule.
When the URL cannot be resolved the prompt explicitly forbids writing
any Twenty URL, so a failed lookup degrades to plain record names rather
than invented links.

**Reply structure.** The answer now goes out as Block Kit: a `markdown`
block for the body and a `context` block for the duration, so it reads
as a footer rather than italic text tacked onto the answer.
`getSlackChatMessageBodyFields` grew a blocks variant that keeps the
message text as Slack's notification and screen-reader fallback, and
`slackUpdateMessageHandler` now falls back to plain text on
`invalid_blocks` for blocks as well as markdown.

## Screenshots

### Before
<img width="344" height="161" alt="Screenshot 2026-07-30 at 8 13 55 AM"
src="https://github.com/user-attachments/assets/c4a76654-b4bc-4f3d-a8ad-a00073ec5674"
/>

### After
<img width="408" height="126" alt="Screenshot 2026-07-30 at 8 26 15 AM"
src="https://github.com/user-attachments/assets/ba2ec249-0520-42ca-87d1-c272515bddef"
/>


---
_Generated by [Claude
Code](https://claude.ai/code/session_0148FpKn9T41aVHsZZ2d1Lrw)_

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/23539?utm_source=github"
target="_blank" rel="noopener noreferrer"
data-no-image-dialog="true"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source
media="(prefers-color-scheme: light)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img
alt="Review in cubic"
src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a>
<!-- End of auto-generated description by cubic. -->
2026-08-05 08:45:46 +00:00

36 lines
1.1 KiB
TypeScript

import { isNonEmptyString } from '@sniptt/guards';
import { MetadataApiClient } from 'twenty-client-sdk/metadata';
const stripTrailingSlashes = (url: string): string => url.replace(/\/+$/, '');
export const fetchWorkspaceBaseUrl = async (): Promise<string | undefined> => {
try {
const { currentWorkspace } = await new MetadataApiClient().query({
currentWorkspace: {
workspaceUrls: { customUrl: true, subdomainUrl: true },
},
});
const customUrl = currentWorkspace?.workspaceUrls?.customUrl;
const subdomainUrl = currentWorkspace?.workspaceUrls?.subdomainUrl;
if (isNonEmptyString(customUrl)) {
return stripTrailingSlashes(customUrl);
}
if (isNonEmptyString(subdomainUrl)) {
return stripTrailingSlashes(subdomainUrl);
}
console.warn('[slack] workspace URL is missing, record links are disabled');
return undefined;
} catch (error) {
console.warn(
`[slack] failed to read the workspace URL, record links are disabled: ${error instanceof Error ? error.message : String(error)}`,
);
return undefined;
}
};