42aa566e32
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. -->
36 lines
1.1 KiB
TypeScript
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;
|
|
}
|
|
};
|