@inkeep/agents-work-apps 0.47.5 → 0.48.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/env.d.ts +24 -2
- package/dist/env.js +13 -2
- package/dist/github/index.d.ts +3 -3
- package/dist/github/mcp/auth.d.ts +2 -2
- package/dist/github/mcp/index.d.ts +2 -2
- package/dist/github/mcp/index.js +23 -34
- package/dist/github/mcp/schemas.d.ts +1 -1
- package/dist/github/routes/setup.d.ts +2 -2
- package/dist/github/routes/tokenExchange.d.ts +2 -2
- package/dist/github/routes/webhooks.d.ts +2 -2
- package/dist/slack/i18n/index.d.ts +2 -0
- package/dist/slack/i18n/index.js +3 -0
- package/dist/slack/i18n/strings.d.ts +73 -0
- package/dist/slack/i18n/strings.js +67 -0
- package/dist/slack/index.d.ts +18 -0
- package/dist/slack/index.js +28 -0
- package/dist/slack/middleware/permissions.d.ts +31 -0
- package/dist/slack/middleware/permissions.js +167 -0
- package/dist/slack/routes/events.d.ts +10 -0
- package/dist/slack/routes/events.js +551 -0
- package/dist/slack/routes/index.d.ts +10 -0
- package/dist/slack/routes/index.js +47 -0
- package/dist/slack/routes/oauth.d.ts +20 -0
- package/dist/slack/routes/oauth.js +344 -0
- package/dist/slack/routes/users.d.ts +10 -0
- package/dist/slack/routes/users.js +365 -0
- package/dist/slack/routes/workspaces.d.ts +10 -0
- package/dist/slack/routes/workspaces.js +909 -0
- package/dist/slack/services/agent-resolution.d.ts +41 -0
- package/dist/slack/services/agent-resolution.js +99 -0
- package/dist/slack/services/blocks/index.d.ts +73 -0
- package/dist/slack/services/blocks/index.js +103 -0
- package/dist/slack/services/client.d.ts +108 -0
- package/dist/slack/services/client.js +232 -0
- package/dist/slack/services/commands/index.d.ts +19 -0
- package/dist/slack/services/commands/index.js +553 -0
- package/dist/slack/services/events/app-mention.d.ts +40 -0
- package/dist/slack/services/events/app-mention.js +297 -0
- package/dist/slack/services/events/block-actions.d.ts +40 -0
- package/dist/slack/services/events/block-actions.js +265 -0
- package/dist/slack/services/events/index.d.ts +6 -0
- package/dist/slack/services/events/index.js +7 -0
- package/dist/slack/services/events/modal-submission.d.ts +30 -0
- package/dist/slack/services/events/modal-submission.js +400 -0
- package/dist/slack/services/events/streaming.d.ts +26 -0
- package/dist/slack/services/events/streaming.js +255 -0
- package/dist/slack/services/events/utils.d.ts +146 -0
- package/dist/slack/services/events/utils.js +370 -0
- package/dist/slack/services/index.d.ts +16 -0
- package/dist/slack/services/index.js +16 -0
- package/dist/slack/services/modals.d.ts +86 -0
- package/dist/slack/services/modals.js +355 -0
- package/dist/slack/services/nango.d.ts +85 -0
- package/dist/slack/services/nango.js +476 -0
- package/dist/slack/services/security.d.ts +35 -0
- package/dist/slack/services/security.js +65 -0
- package/dist/slack/services/types.d.ts +26 -0
- package/dist/slack/services/types.js +1 -0
- package/dist/slack/services/workspace-tokens.d.ts +25 -0
- package/dist/slack/services/workspace-tokens.js +27 -0
- package/dist/slack/tracer.d.ts +40 -0
- package/dist/slack/tracer.js +39 -0
- package/dist/slack/types.d.ts +10 -0
- package/dist/slack/types.js +1 -0
- package/package.json +11 -2
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
import { DefaultAgentConfig, SlackWorkspaceConnection } from "../nango.js";
|
|
2
|
+
import { AgentOption } from "../modals.js";
|
|
3
|
+
|
|
4
|
+
//#region src/slack/services/events/utils.d.ts
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Find a user mapping with bounded in-memory caching (5 min TTL, max 5000 entries).
|
|
8
|
+
* Called on every @mention and /inkeep command — caching avoids redundant DB queries.
|
|
9
|
+
*/
|
|
10
|
+
declare function findCachedUserMapping(tenantId: string, slackUserId: string, teamId: string, clientId?: string): Promise<{
|
|
11
|
+
createdAt: string;
|
|
12
|
+
updatedAt: string;
|
|
13
|
+
id: string;
|
|
14
|
+
tenantId: string;
|
|
15
|
+
clientId: string;
|
|
16
|
+
slackUserId: string;
|
|
17
|
+
slackTeamId: string;
|
|
18
|
+
slackEnterpriseId: string | null;
|
|
19
|
+
inkeepUserId: string;
|
|
20
|
+
slackUsername: string | null;
|
|
21
|
+
slackEmail: string | null;
|
|
22
|
+
linkedAt: string;
|
|
23
|
+
lastUsedAt: string | null;
|
|
24
|
+
} | null>;
|
|
25
|
+
/**
|
|
26
|
+
* Convert standard Markdown to Slack's mrkdwn format
|
|
27
|
+
*
|
|
28
|
+
* Key differences:
|
|
29
|
+
* - **bold** or __bold__ → *bold*
|
|
30
|
+
* - *italic* (when not bold) → _italic_
|
|
31
|
+
* - # Header → *Header* (Slack has no headers)
|
|
32
|
+
* - [text](url) → <url|text>
|
|
33
|
+
* - Keeps code blocks, inline code, and lists as-is
|
|
34
|
+
*/
|
|
35
|
+
declare function markdownToMrkdwn(markdown: string): string;
|
|
36
|
+
/**
|
|
37
|
+
* Error types for user-friendly error messages
|
|
38
|
+
*/
|
|
39
|
+
declare enum SlackErrorType {
|
|
40
|
+
TIMEOUT = "timeout",
|
|
41
|
+
RATE_LIMIT = "rate_limit",
|
|
42
|
+
API_ERROR = "api_error",
|
|
43
|
+
AUTH_ERROR = "auth_error",
|
|
44
|
+
UNKNOWN = "unknown",
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Classify an error into a SlackErrorType for appropriate user messaging
|
|
48
|
+
*/
|
|
49
|
+
declare function classifyError(error: unknown, httpStatus?: number): SlackErrorType;
|
|
50
|
+
/**
|
|
51
|
+
* Get a user-friendly error message based on error type
|
|
52
|
+
*/
|
|
53
|
+
declare function getUserFriendlyErrorMessage(errorType: SlackErrorType, agentName?: string): string;
|
|
54
|
+
type ProjectOption = {
|
|
55
|
+
id: string;
|
|
56
|
+
name: string;
|
|
57
|
+
};
|
|
58
|
+
declare function fetchProjectsForTenant(tenantId: string): Promise<ProjectOption[]>;
|
|
59
|
+
declare function fetchAgentsForProject(tenantId: string, projectId: string): Promise<AgentOption[]>;
|
|
60
|
+
declare function getWorkspaceDefaultAgent(teamId: string): Promise<DefaultAgentConfig | null>;
|
|
61
|
+
declare function getChannelAgentConfig(teamId: string, channelId: string): Promise<DefaultAgentConfig | null>;
|
|
62
|
+
/**
|
|
63
|
+
* Resolve channel agent config using a pre-resolved workspace connection.
|
|
64
|
+
* Avoids redundant workspace lookups when the connection is already available.
|
|
65
|
+
*/
|
|
66
|
+
declare function resolveChannelAgentConfig(teamId: string, channelId: string, workspace: SlackWorkspaceConnection | null): Promise<DefaultAgentConfig | null>;
|
|
67
|
+
declare function sendResponseUrlMessage(responseUrl: string, message: {
|
|
68
|
+
text: string;
|
|
69
|
+
response_type?: 'ephemeral' | 'in_channel';
|
|
70
|
+
replace_original?: boolean;
|
|
71
|
+
delete_original?: boolean;
|
|
72
|
+
blocks?: unknown[];
|
|
73
|
+
}): Promise<void>;
|
|
74
|
+
/**
|
|
75
|
+
* Generate a deterministic conversation ID for Slack threads/DMs.
|
|
76
|
+
* This ensures the same thread + agent combination gets the same conversation ID,
|
|
77
|
+
* allowing the agent to maintain conversation history.
|
|
78
|
+
*
|
|
79
|
+
* Including agentId ensures switching agents in the same thread starts a fresh
|
|
80
|
+
* conversation, avoiding sub-agent conflicts when the Run API tries to resume
|
|
81
|
+
* a conversation that was started by a different agent.
|
|
82
|
+
*
|
|
83
|
+
* Format: slack-thread-{teamId}-{identifier}[-{agentId}]
|
|
84
|
+
*/
|
|
85
|
+
declare function generateSlackConversationId(params: {
|
|
86
|
+
teamId: string;
|
|
87
|
+
threadTs?: string;
|
|
88
|
+
channel: string;
|
|
89
|
+
isDM?: boolean;
|
|
90
|
+
agentId?: string;
|
|
91
|
+
}): string;
|
|
92
|
+
/**
|
|
93
|
+
* Check if a thread was initiated by the bot (i.e., the parent message is from the bot).
|
|
94
|
+
* This helps distinguish "bot threads" (where users are conversing with the bot)
|
|
95
|
+
* from "user threads" (where users are having their own conversation).
|
|
96
|
+
*
|
|
97
|
+
* Uses conversations.replies which returns thread messages with the parent as the first message.
|
|
98
|
+
*/
|
|
99
|
+
declare function checkIfBotThread(slackClient: {
|
|
100
|
+
conversations: {
|
|
101
|
+
replies: (params: {
|
|
102
|
+
channel: string;
|
|
103
|
+
ts: string;
|
|
104
|
+
limit?: number;
|
|
105
|
+
}) => Promise<{
|
|
106
|
+
messages?: Array<{
|
|
107
|
+
bot_id?: string;
|
|
108
|
+
user?: string;
|
|
109
|
+
text?: string;
|
|
110
|
+
}>;
|
|
111
|
+
}>;
|
|
112
|
+
};
|
|
113
|
+
}, channel: string, threadTs: string): Promise<boolean>;
|
|
114
|
+
interface ThreadContextOptions {
|
|
115
|
+
includeLastMessage?: boolean;
|
|
116
|
+
resolveUserNames?: boolean;
|
|
117
|
+
}
|
|
118
|
+
declare function getThreadContext(slackClient: {
|
|
119
|
+
conversations: {
|
|
120
|
+
replies: (params: {
|
|
121
|
+
channel: string;
|
|
122
|
+
ts: string;
|
|
123
|
+
limit?: number;
|
|
124
|
+
}) => Promise<{
|
|
125
|
+
messages?: Array<{
|
|
126
|
+
bot_id?: string;
|
|
127
|
+
user?: string;
|
|
128
|
+
text?: string;
|
|
129
|
+
ts?: string;
|
|
130
|
+
}>;
|
|
131
|
+
}>;
|
|
132
|
+
};
|
|
133
|
+
users?: {
|
|
134
|
+
info: (params: {
|
|
135
|
+
user: string;
|
|
136
|
+
}) => Promise<{
|
|
137
|
+
user?: {
|
|
138
|
+
real_name?: string;
|
|
139
|
+
display_name?: string;
|
|
140
|
+
name?: string;
|
|
141
|
+
};
|
|
142
|
+
}>;
|
|
143
|
+
};
|
|
144
|
+
}, channel: string, threadTs: string, options?: ThreadContextOptions): Promise<string>;
|
|
145
|
+
//#endregion
|
|
146
|
+
export { ProjectOption, SlackErrorType, checkIfBotThread, classifyError, fetchAgentsForProject, fetchProjectsForTenant, findCachedUserMapping, generateSlackConversationId, getChannelAgentConfig, getThreadContext, getUserFriendlyErrorMessage, getWorkspaceDefaultAgent, markdownToMrkdwn, resolveChannelAgentConfig, sendResponseUrlMessage };
|
|
@@ -0,0 +1,370 @@
|
|
|
1
|
+
import { env } from "../../../env.js";
|
|
2
|
+
import { getLogger } from "../../../logger.js";
|
|
3
|
+
import runDbClient_default from "../../../db/runDbClient.js";
|
|
4
|
+
import { findWorkspaceConnectionByTeamId } from "../nango.js";
|
|
5
|
+
import { InternalServices, findWorkAppSlackChannelAgentConfig, findWorkAppSlackUserMapping, generateInternalServiceToken } from "@inkeep/agents-core";
|
|
6
|
+
|
|
7
|
+
//#region src/slack/services/events/utils.ts
|
|
8
|
+
/**
|
|
9
|
+
* Shared utilities for Slack event handlers
|
|
10
|
+
*/
|
|
11
|
+
const logger = getLogger("slack-event-utils");
|
|
12
|
+
const USER_MAPPING_CACHE_TTL_MS = 300 * 1e3;
|
|
13
|
+
const USER_MAPPING_CACHE_MAX_SIZE = 5e3;
|
|
14
|
+
const userMappingCache = /* @__PURE__ */ new Map();
|
|
15
|
+
function evictExpiredEntries() {
|
|
16
|
+
if (userMappingCache.size <= USER_MAPPING_CACHE_MAX_SIZE) return;
|
|
17
|
+
const now = Date.now();
|
|
18
|
+
for (const [key, entry] of userMappingCache) if (entry.expiresAt <= now) userMappingCache.delete(key);
|
|
19
|
+
if (userMappingCache.size > USER_MAPPING_CACHE_MAX_SIZE) {
|
|
20
|
+
const excess = userMappingCache.size - USER_MAPPING_CACHE_MAX_SIZE;
|
|
21
|
+
const keys = userMappingCache.keys();
|
|
22
|
+
for (let i = 0; i < excess; i++) {
|
|
23
|
+
const { value } = keys.next();
|
|
24
|
+
if (value) userMappingCache.delete(value);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Find a user mapping with bounded in-memory caching (5 min TTL, max 5000 entries).
|
|
30
|
+
* Called on every @mention and /inkeep command — caching avoids redundant DB queries.
|
|
31
|
+
*/
|
|
32
|
+
async function findCachedUserMapping(tenantId, slackUserId, teamId, clientId = "work-apps-slack") {
|
|
33
|
+
const cacheKey = `${tenantId}:${slackUserId}:${teamId}:${clientId}`;
|
|
34
|
+
const cached = userMappingCache.get(cacheKey);
|
|
35
|
+
if (cached && cached.expiresAt > Date.now()) return cached.mapping;
|
|
36
|
+
const mapping = await findWorkAppSlackUserMapping(runDbClient_default)(tenantId, slackUserId, teamId, clientId);
|
|
37
|
+
if (userMappingCache.size >= USER_MAPPING_CACHE_MAX_SIZE) {
|
|
38
|
+
evictExpiredEntries();
|
|
39
|
+
if (userMappingCache.size >= USER_MAPPING_CACHE_MAX_SIZE) {
|
|
40
|
+
const oldestKey = userMappingCache.keys().next().value;
|
|
41
|
+
if (oldestKey) userMappingCache.delete(oldestKey);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
userMappingCache.set(cacheKey, {
|
|
45
|
+
mapping,
|
|
46
|
+
expiresAt: Date.now() + USER_MAPPING_CACHE_TTL_MS
|
|
47
|
+
});
|
|
48
|
+
return mapping;
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Convert standard Markdown to Slack's mrkdwn format
|
|
52
|
+
*
|
|
53
|
+
* Key differences:
|
|
54
|
+
* - **bold** or __bold__ → *bold*
|
|
55
|
+
* - *italic* (when not bold) → _italic_
|
|
56
|
+
* - # Header → *Header* (Slack has no headers)
|
|
57
|
+
* - [text](url) → <url|text>
|
|
58
|
+
* - Keeps code blocks, inline code, and lists as-is
|
|
59
|
+
*/
|
|
60
|
+
function markdownToMrkdwn(markdown) {
|
|
61
|
+
if (!markdown) return markdown;
|
|
62
|
+
let result = markdown;
|
|
63
|
+
result = result.replace(/^#{1,6}\s+(.+)$/gm, "*$1*");
|
|
64
|
+
result = result.replace(/\[([^\]]+)\]\(([^)]+)\)/g, "<$2|$1>");
|
|
65
|
+
result = result.replace(/\*\*([^*]+)\*\*/g, "*$1*");
|
|
66
|
+
result = result.replace(/__([^_]+)__/g, "*$1*");
|
|
67
|
+
result = result.replace(/~~([^~]+)~~/g, "~$1~");
|
|
68
|
+
return result;
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Error types for user-friendly error messages
|
|
72
|
+
*/
|
|
73
|
+
let SlackErrorType = /* @__PURE__ */ function(SlackErrorType$1) {
|
|
74
|
+
SlackErrorType$1["TIMEOUT"] = "timeout";
|
|
75
|
+
SlackErrorType$1["RATE_LIMIT"] = "rate_limit";
|
|
76
|
+
SlackErrorType$1["API_ERROR"] = "api_error";
|
|
77
|
+
SlackErrorType$1["AUTH_ERROR"] = "auth_error";
|
|
78
|
+
SlackErrorType$1["UNKNOWN"] = "unknown";
|
|
79
|
+
return SlackErrorType$1;
|
|
80
|
+
}({});
|
|
81
|
+
/**
|
|
82
|
+
* Classify an error into a SlackErrorType for appropriate user messaging
|
|
83
|
+
*/
|
|
84
|
+
function classifyError(error, httpStatus) {
|
|
85
|
+
if (httpStatus === 429) return SlackErrorType.RATE_LIMIT;
|
|
86
|
+
if (httpStatus === 401 || httpStatus === 403) return SlackErrorType.AUTH_ERROR;
|
|
87
|
+
if (httpStatus && httpStatus >= 400) return SlackErrorType.API_ERROR;
|
|
88
|
+
const errorMessage = error instanceof Error ? error.message.toLowerCase() : String(error).toLowerCase();
|
|
89
|
+
if (errorMessage.includes("timeout") || errorMessage.includes("timed out") || errorMessage.includes("aborted") || errorMessage.includes("econnreset")) return SlackErrorType.TIMEOUT;
|
|
90
|
+
if (errorMessage.includes("rate limit") || errorMessage.includes("too many requests")) return SlackErrorType.RATE_LIMIT;
|
|
91
|
+
if (errorMessage.includes("unauthorized") || errorMessage.includes("forbidden")) return SlackErrorType.AUTH_ERROR;
|
|
92
|
+
return SlackErrorType.UNKNOWN;
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* Get a user-friendly error message based on error type
|
|
96
|
+
*/
|
|
97
|
+
function getUserFriendlyErrorMessage(errorType, agentName) {
|
|
98
|
+
const agent = agentName || "The agent";
|
|
99
|
+
switch (errorType) {
|
|
100
|
+
case SlackErrorType.TIMEOUT: return `⏱️ *Request timed out*\n\n${agent} took too long to respond. This can happen with complex queries.\n\n*Try:*\n• Simplifying your question\n• Breaking it into smaller parts\n• Trying again in a moment`;
|
|
101
|
+
case SlackErrorType.RATE_LIMIT: return `⚠️ *Too many requests*\n\nYou've hit the rate limit. Please wait a moment before trying again.\n\n*Tip:* Space out your requests to avoid this.`;
|
|
102
|
+
case SlackErrorType.AUTH_ERROR: return `🔐 *Authentication issue*\n\nThere was a problem with your account connection.\n\n*Try:*\n• Running \`/inkeep link\` to re-link your account\n• Contacting your workspace admin if the issue persists`;
|
|
103
|
+
case SlackErrorType.API_ERROR: return `❌ *Something went wrong*\n\n${agent} encountered an error processing your request.\n\n*Try:*\n• Rephrasing your question\n• Trying again in a moment\n• Using \`/inkeep help\` for more options`;
|
|
104
|
+
default: return `❌ *Unexpected error*\n\nSomething went wrong while processing your request.\n\n*Try:*\n• Trying again in a moment\n• Using \`/inkeep help\` for more options`;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
const INTERNAL_FETCH_TIMEOUT_MS = 1e4;
|
|
108
|
+
async function fetchProjectsForTenant(tenantId) {
|
|
109
|
+
const apiUrl = env.INKEEP_AGENTS_API_URL || "http://localhost:3002";
|
|
110
|
+
const token = await generateInternalServiceToken({
|
|
111
|
+
serviceId: InternalServices.INKEEP_AGENTS_MANAGE_API,
|
|
112
|
+
tenantId
|
|
113
|
+
});
|
|
114
|
+
const controller = new AbortController();
|
|
115
|
+
const timeout = setTimeout(() => controller.abort(), INTERNAL_FETCH_TIMEOUT_MS);
|
|
116
|
+
try {
|
|
117
|
+
const response = await fetch(`${apiUrl}/manage/tenants/${tenantId}/projects?limit=50`, {
|
|
118
|
+
method: "GET",
|
|
119
|
+
headers: {
|
|
120
|
+
Authorization: `Bearer ${token}`,
|
|
121
|
+
"Content-Type": "application/json"
|
|
122
|
+
},
|
|
123
|
+
signal: controller.signal
|
|
124
|
+
});
|
|
125
|
+
if (!response.ok) {
|
|
126
|
+
const errorBody = await response.text().catch(() => "");
|
|
127
|
+
logger.warn({
|
|
128
|
+
status: response.status,
|
|
129
|
+
tenantId,
|
|
130
|
+
errorBody
|
|
131
|
+
}, "Failed to fetch projects from API");
|
|
132
|
+
return [];
|
|
133
|
+
}
|
|
134
|
+
const result = await response.json();
|
|
135
|
+
logger.debug({
|
|
136
|
+
tenantId,
|
|
137
|
+
projectCount: result.data.length
|
|
138
|
+
}, "Fetched projects from API");
|
|
139
|
+
return result.data.map((p) => ({
|
|
140
|
+
id: p.id,
|
|
141
|
+
name: p.name || p.id
|
|
142
|
+
}));
|
|
143
|
+
} catch (error) {
|
|
144
|
+
logger.error({
|
|
145
|
+
error,
|
|
146
|
+
tenantId
|
|
147
|
+
}, "Error fetching projects from API");
|
|
148
|
+
return [];
|
|
149
|
+
} finally {
|
|
150
|
+
clearTimeout(timeout);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
async function fetchAgentsForProject(tenantId, projectId) {
|
|
154
|
+
const apiUrl = env.INKEEP_AGENTS_API_URL || "http://localhost:3002";
|
|
155
|
+
const token = await generateInternalServiceToken({
|
|
156
|
+
serviceId: InternalServices.INKEEP_AGENTS_MANAGE_API,
|
|
157
|
+
tenantId,
|
|
158
|
+
projectId
|
|
159
|
+
});
|
|
160
|
+
const controller = new AbortController();
|
|
161
|
+
const timeout = setTimeout(() => controller.abort(), INTERNAL_FETCH_TIMEOUT_MS);
|
|
162
|
+
try {
|
|
163
|
+
const response = await fetch(`${apiUrl}/manage/tenants/${tenantId}/projects/${projectId}/agents?limit=50`, {
|
|
164
|
+
method: "GET",
|
|
165
|
+
headers: {
|
|
166
|
+
Authorization: `Bearer ${token}`,
|
|
167
|
+
"Content-Type": "application/json",
|
|
168
|
+
"x-inkeep-project-id": projectId
|
|
169
|
+
},
|
|
170
|
+
signal: controller.signal
|
|
171
|
+
});
|
|
172
|
+
if (!response.ok) {
|
|
173
|
+
const errorBody = await response.text().catch(() => "");
|
|
174
|
+
logger.warn({
|
|
175
|
+
status: response.status,
|
|
176
|
+
tenantId,
|
|
177
|
+
projectId,
|
|
178
|
+
errorBody
|
|
179
|
+
}, "Failed to fetch agents from API");
|
|
180
|
+
return [];
|
|
181
|
+
}
|
|
182
|
+
const result = await response.json();
|
|
183
|
+
logger.debug({
|
|
184
|
+
tenantId,
|
|
185
|
+
projectId,
|
|
186
|
+
agentCount: result.data.length
|
|
187
|
+
}, "Fetched agents from API");
|
|
188
|
+
return result.data.map((a) => ({
|
|
189
|
+
id: a.id,
|
|
190
|
+
name: a.name || a.id,
|
|
191
|
+
projectId,
|
|
192
|
+
projectName: projectId
|
|
193
|
+
}));
|
|
194
|
+
} catch (error) {
|
|
195
|
+
logger.error({
|
|
196
|
+
error,
|
|
197
|
+
tenantId,
|
|
198
|
+
projectId
|
|
199
|
+
}, "Error fetching agents from API");
|
|
200
|
+
return [];
|
|
201
|
+
} finally {
|
|
202
|
+
clearTimeout(timeout);
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
async function getWorkspaceDefaultAgent(teamId) {
|
|
206
|
+
const workspace = await findWorkspaceConnectionByTeamId(teamId);
|
|
207
|
+
if (workspace?.defaultAgent) {
|
|
208
|
+
logger.debug({ teamId }, "Found workspace default agent");
|
|
209
|
+
return workspace.defaultAgent;
|
|
210
|
+
}
|
|
211
|
+
return null;
|
|
212
|
+
}
|
|
213
|
+
async function getChannelAgentConfig(teamId, channelId) {
|
|
214
|
+
return resolveChannelAgentConfig(teamId, channelId, await findWorkspaceConnectionByTeamId(teamId));
|
|
215
|
+
}
|
|
216
|
+
/**
|
|
217
|
+
* Resolve channel agent config using a pre-resolved workspace connection.
|
|
218
|
+
* Avoids redundant workspace lookups when the connection is already available.
|
|
219
|
+
*/
|
|
220
|
+
async function resolveChannelAgentConfig(teamId, channelId, workspace) {
|
|
221
|
+
const tenantId = workspace?.tenantId;
|
|
222
|
+
if (!tenantId) return null;
|
|
223
|
+
const channelConfig = await findWorkAppSlackChannelAgentConfig(runDbClient_default)(tenantId, teamId, channelId);
|
|
224
|
+
if (channelConfig?.enabled) return {
|
|
225
|
+
projectId: channelConfig.projectId,
|
|
226
|
+
agentId: channelConfig.agentId,
|
|
227
|
+
agentName: channelConfig.agentName || channelConfig.agentId,
|
|
228
|
+
projectName: channelConfig.projectId
|
|
229
|
+
};
|
|
230
|
+
return workspace?.defaultAgent || null;
|
|
231
|
+
}
|
|
232
|
+
async function sendResponseUrlMessage(responseUrl, message) {
|
|
233
|
+
try {
|
|
234
|
+
const payload = { text: message.text };
|
|
235
|
+
if (message.replace_original) payload.replace_original = true;
|
|
236
|
+
else if (message.delete_original) payload.delete_original = true;
|
|
237
|
+
else if (message.response_type) payload.response_type = message.response_type;
|
|
238
|
+
if (message.blocks) payload.blocks = message.blocks;
|
|
239
|
+
logger.info({
|
|
240
|
+
hasBlocks: !!message.blocks,
|
|
241
|
+
blockCount: Array.isArray(message.blocks) ? message.blocks.length : 0,
|
|
242
|
+
replace: !!message.replace_original
|
|
243
|
+
}, "Sending response_url message");
|
|
244
|
+
const controller = new AbortController();
|
|
245
|
+
const responseTimeout = setTimeout(() => controller.abort(), INTERNAL_FETCH_TIMEOUT_MS);
|
|
246
|
+
const response = await fetch(responseUrl, {
|
|
247
|
+
method: "POST",
|
|
248
|
+
headers: { "Content-Type": "application/json" },
|
|
249
|
+
body: JSON.stringify(payload),
|
|
250
|
+
signal: controller.signal
|
|
251
|
+
});
|
|
252
|
+
clearTimeout(responseTimeout);
|
|
253
|
+
const responseBody = await response.text().catch(() => "");
|
|
254
|
+
logger.info({
|
|
255
|
+
status: response.status,
|
|
256
|
+
responseBody: responseBody.slice(0, 300)
|
|
257
|
+
}, "response_url response received");
|
|
258
|
+
if (!response.ok) logger.error({
|
|
259
|
+
status: response.status,
|
|
260
|
+
errorBody: responseBody
|
|
261
|
+
}, "response_url request failed");
|
|
262
|
+
} catch (error) {
|
|
263
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
264
|
+
logger.error({ errorMessage }, "Failed to send response_url message");
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
/**
|
|
268
|
+
* Generate a deterministic conversation ID for Slack threads/DMs.
|
|
269
|
+
* This ensures the same thread + agent combination gets the same conversation ID,
|
|
270
|
+
* allowing the agent to maintain conversation history.
|
|
271
|
+
*
|
|
272
|
+
* Including agentId ensures switching agents in the same thread starts a fresh
|
|
273
|
+
* conversation, avoiding sub-agent conflicts when the Run API tries to resume
|
|
274
|
+
* a conversation that was started by a different agent.
|
|
275
|
+
*
|
|
276
|
+
* Format: slack-thread-{teamId}-{identifier}[-{agentId}]
|
|
277
|
+
*/
|
|
278
|
+
function generateSlackConversationId(params) {
|
|
279
|
+
const { teamId, threadTs, channel, isDM, agentId } = params;
|
|
280
|
+
const base = isDM ? `slack-dm-${teamId}-${channel}` : `slack-thread-${teamId}-${threadTs || channel}`;
|
|
281
|
+
return agentId ? `${base}-${agentId}` : base;
|
|
282
|
+
}
|
|
283
|
+
/**
|
|
284
|
+
* Check if a thread was initiated by the bot (i.e., the parent message is from the bot).
|
|
285
|
+
* This helps distinguish "bot threads" (where users are conversing with the bot)
|
|
286
|
+
* from "user threads" (where users are having their own conversation).
|
|
287
|
+
*
|
|
288
|
+
* Uses conversations.replies which returns thread messages with the parent as the first message.
|
|
289
|
+
*/
|
|
290
|
+
async function checkIfBotThread(slackClient, channel, threadTs) {
|
|
291
|
+
try {
|
|
292
|
+
const parentMessage = (await slackClient.conversations.replies({
|
|
293
|
+
channel,
|
|
294
|
+
ts: threadTs,
|
|
295
|
+
limit: 1
|
|
296
|
+
})).messages?.[0];
|
|
297
|
+
if (!parentMessage) {
|
|
298
|
+
logger.debug({
|
|
299
|
+
channel,
|
|
300
|
+
threadTs
|
|
301
|
+
}, "No parent message found for thread");
|
|
302
|
+
return false;
|
|
303
|
+
}
|
|
304
|
+
const isBotThread = Boolean(parentMessage.bot_id);
|
|
305
|
+
logger.debug({
|
|
306
|
+
channel,
|
|
307
|
+
threadTs,
|
|
308
|
+
isBotThread,
|
|
309
|
+
botId: parentMessage.bot_id
|
|
310
|
+
}, "Checked if thread is bot-owned");
|
|
311
|
+
return isBotThread;
|
|
312
|
+
} catch (error) {
|
|
313
|
+
logger.warn({
|
|
314
|
+
error,
|
|
315
|
+
channel,
|
|
316
|
+
threadTs
|
|
317
|
+
}, "Failed to check if thread is bot-owned");
|
|
318
|
+
return false;
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
async function getThreadContext(slackClient, channel, threadTs, options = {}) {
|
|
322
|
+
const { includeLastMessage = false, resolveUserNames = true } = options;
|
|
323
|
+
try {
|
|
324
|
+
const threadMessages = await slackClient.conversations.replies({
|
|
325
|
+
channel,
|
|
326
|
+
ts: threadTs,
|
|
327
|
+
limit: 50
|
|
328
|
+
});
|
|
329
|
+
if (!threadMessages.messages || threadMessages.messages.length === 0) return "";
|
|
330
|
+
const messagesToProcess = includeLastMessage ? threadMessages.messages : threadMessages.messages.slice(0, -1);
|
|
331
|
+
if (messagesToProcess.length === 0) return "";
|
|
332
|
+
const userNameCache = /* @__PURE__ */ new Map();
|
|
333
|
+
if (resolveUserNames && slackClient.users) {
|
|
334
|
+
const uniqueUserIds = [...new Set(messagesToProcess.filter((m) => !!m.user).map((m) => m.user))];
|
|
335
|
+
await Promise.all(uniqueUserIds.map(async (userId) => {
|
|
336
|
+
try {
|
|
337
|
+
const userInfo = await slackClient.users?.info({ user: userId });
|
|
338
|
+
const name = userInfo?.user?.display_name || userInfo?.user?.real_name || userInfo?.user?.name || userId;
|
|
339
|
+
userNameCache.set(userId, name);
|
|
340
|
+
} catch {
|
|
341
|
+
userNameCache.set(userId, userId);
|
|
342
|
+
}
|
|
343
|
+
}));
|
|
344
|
+
}
|
|
345
|
+
return messagesToProcess.map((msg, index) => {
|
|
346
|
+
const isBot = !!msg.bot_id;
|
|
347
|
+
const isParent = index === 0;
|
|
348
|
+
let role;
|
|
349
|
+
if (isBot) role = "Inkeep Agent";
|
|
350
|
+
else if (msg.user) role = resolveUserNames ? userNameCache.get(msg.user) || msg.user : `<@${msg.user}>`;
|
|
351
|
+
else role = "Unknown";
|
|
352
|
+
const prefix = isParent ? "[Thread Start] " : "";
|
|
353
|
+
const messageText = msg.text?.replace(/<@U[A-Z0-9]+>/g, (match) => {
|
|
354
|
+
const userId = match.slice(2, -1);
|
|
355
|
+
return `@${userNameCache.get(userId) || userId}`;
|
|
356
|
+
}) || "";
|
|
357
|
+
return `${prefix}${role}: ${messageText}`;
|
|
358
|
+
}).join("\n\n");
|
|
359
|
+
} catch (threadError) {
|
|
360
|
+
logger.warn({
|
|
361
|
+
threadError,
|
|
362
|
+
channel,
|
|
363
|
+
threadTs
|
|
364
|
+
}, "Failed to fetch thread context");
|
|
365
|
+
}
|
|
366
|
+
return "";
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
//#endregion
|
|
370
|
+
export { SlackErrorType, checkIfBotThread, classifyError, fetchAgentsForProject, fetchProjectsForTenant, findCachedUserMapping, generateSlackConversationId, getChannelAgentConfig, getThreadContext, getUserFriendlyErrorMessage, getWorkspaceDefaultAgent, markdownToMrkdwn, resolveChannelAgentConfig, sendResponseUrlMessage };
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { AgentResolutionParams, ResolvedAgentConfig, getAgentConfigSources, resolveEffectiveAgent } from "./agent-resolution.js";
|
|
2
|
+
import { AgentConfigSources, ContextBlockParams, FollowUpButtonParams, buildConversationResponseBlocks, buildFollowUpButton, createAgentListMessage, createAlreadyLinkedMessage, createContextBlock, createErrorMessage, createJwtLinkMessage, createNotLinkedMessage, createStatusMessage, createUnlinkSuccessMessage, createUpdatedHelpMessage } from "./blocks/index.js";
|
|
3
|
+
import { checkUserIsChannelMember, getSlackChannels, getSlackClient, getSlackTeamInfo, getSlackUserInfo, postMessage, postMessageInThread, revokeSlackToken } from "./client.js";
|
|
4
|
+
import { DefaultAgentConfig, SlackWorkspaceConnection, WorkspaceInstallData, clearWorkspaceConnectionCache, computeWorkspaceConnectionId, createConnectSession, deleteWorkspaceInstallation, findWorkspaceConnectionByTeamId, getConnectionAccessToken, getSlackIntegrationId, getSlackNango, getWorkspaceDefaultAgentFromNango, listWorkspaceInstallations, setWorkspaceDefaultAgent, storeWorkspaceInstallation, updateConnectionMetadata } from "./nango.js";
|
|
5
|
+
import { SlackCommandPayload, SlackCommandResponse } from "./types.js";
|
|
6
|
+
import { handleAgentListCommand, handleAgentPickerCommand, handleCommand, handleHelpCommand, handleLinkCommand, handleQuestionCommand, handleRunCommand, handleStatusCommand, handleUnlinkCommand } from "./commands/index.js";
|
|
7
|
+
import { InlineSelectorMetadata, handleAppMention } from "./events/app-mention.js";
|
|
8
|
+
import { handleMessageShortcut, handleOpenAgentSelectorModal, handleOpenFollowUpModal } from "./events/block-actions.js";
|
|
9
|
+
import { handleFollowUpSubmission, handleModalSubmission } from "./events/modal-submission.js";
|
|
10
|
+
import { AgentOption, BuildAgentSelectorModalParams, BuildMessageShortcutModalParams, FollowUpModalMetadata, ModalMetadata, buildAgentSelectorModal, buildFollowUpModal, buildMessageShortcutModal } from "./modals.js";
|
|
11
|
+
import { SlackErrorType, checkIfBotThread, classifyError, fetchAgentsForProject, fetchProjectsForTenant, findCachedUserMapping, generateSlackConversationId, getChannelAgentConfig, getThreadContext, getUserFriendlyErrorMessage, getWorkspaceDefaultAgent, markdownToMrkdwn, sendResponseUrlMessage } from "./events/utils.js";
|
|
12
|
+
import { StreamResult, streamAgentResponse } from "./events/streaming.js";
|
|
13
|
+
import "./events/index.js";
|
|
14
|
+
import { parseSlackCommandBody, parseSlackEventBody, verifySlackRequest } from "./security.js";
|
|
15
|
+
import { getBotTokenForTeam, setBotTokenForTeam } from "./workspace-tokens.js";
|
|
16
|
+
export { AgentConfigSources, AgentOption, AgentResolutionParams, BuildAgentSelectorModalParams, BuildMessageShortcutModalParams, ContextBlockParams, DefaultAgentConfig, FollowUpButtonParams, FollowUpModalMetadata, InlineSelectorMetadata, ModalMetadata, ResolvedAgentConfig, SlackCommandPayload, SlackCommandResponse, SlackErrorType, SlackWorkspaceConnection, StreamResult, WorkspaceInstallData, buildAgentSelectorModal, buildConversationResponseBlocks, buildFollowUpButton, buildFollowUpModal, buildMessageShortcutModal, checkIfBotThread, checkUserIsChannelMember, classifyError, clearWorkspaceConnectionCache, computeWorkspaceConnectionId, createAgentListMessage, createAlreadyLinkedMessage, createConnectSession, createContextBlock, createErrorMessage, createJwtLinkMessage, createNotLinkedMessage, createStatusMessage, createUnlinkSuccessMessage, createUpdatedHelpMessage, deleteWorkspaceInstallation, fetchAgentsForProject, fetchProjectsForTenant, findCachedUserMapping, findWorkspaceConnectionByTeamId, generateSlackConversationId, getAgentConfigSources, getBotTokenForTeam, getChannelAgentConfig, getConnectionAccessToken, getSlackChannels, getSlackClient, getSlackIntegrationId, getSlackNango, getSlackTeamInfo, getSlackUserInfo, getThreadContext, getUserFriendlyErrorMessage, getWorkspaceDefaultAgent, getWorkspaceDefaultAgentFromNango, handleAgentListCommand, handleAgentPickerCommand, handleAppMention, handleCommand, handleFollowUpSubmission, handleHelpCommand, handleLinkCommand, handleMessageShortcut, handleModalSubmission, handleOpenAgentSelectorModal, handleOpenFollowUpModal, handleQuestionCommand, handleRunCommand, handleStatusCommand, handleUnlinkCommand, listWorkspaceInstallations, markdownToMrkdwn, parseSlackCommandBody, parseSlackEventBody, postMessage, postMessageInThread, resolveEffectiveAgent, revokeSlackToken, sendResponseUrlMessage, setBotTokenForTeam, setWorkspaceDefaultAgent, storeWorkspaceInstallation, streamAgentResponse, updateConnectionMetadata, verifySlackRequest };
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { clearWorkspaceConnectionCache, computeWorkspaceConnectionId, createConnectSession, deleteWorkspaceInstallation, findWorkspaceConnectionByTeamId, getConnectionAccessToken, getSlackIntegrationId, getSlackNango, getWorkspaceDefaultAgentFromNango, listWorkspaceInstallations, setWorkspaceDefaultAgent, storeWorkspaceInstallation, updateConnectionMetadata } from "./nango.js";
|
|
2
|
+
import { getAgentConfigSources, resolveEffectiveAgent } from "./agent-resolution.js";
|
|
3
|
+
import { buildConversationResponseBlocks, buildFollowUpButton, createAgentListMessage, createAlreadyLinkedMessage, createContextBlock, createErrorMessage, createJwtLinkMessage, createNotLinkedMessage, createStatusMessage, createUnlinkSuccessMessage, createUpdatedHelpMessage } from "./blocks/index.js";
|
|
4
|
+
import { checkUserIsChannelMember, getSlackChannels, getSlackClient, getSlackTeamInfo, getSlackUserInfo, postMessage, postMessageInThread, revokeSlackToken } from "./client.js";
|
|
5
|
+
import { SlackErrorType, checkIfBotThread, classifyError, fetchAgentsForProject, fetchProjectsForTenant, findCachedUserMapping, generateSlackConversationId, getChannelAgentConfig, getThreadContext, getUserFriendlyErrorMessage, getWorkspaceDefaultAgent, markdownToMrkdwn, sendResponseUrlMessage } from "./events/utils.js";
|
|
6
|
+
import { buildAgentSelectorModal, buildFollowUpModal, buildMessageShortcutModal } from "./modals.js";
|
|
7
|
+
import { handleAgentListCommand, handleAgentPickerCommand, handleCommand, handleHelpCommand, handleLinkCommand, handleQuestionCommand, handleRunCommand, handleStatusCommand, handleUnlinkCommand } from "./commands/index.js";
|
|
8
|
+
import { getBotTokenForTeam, setBotTokenForTeam } from "./workspace-tokens.js";
|
|
9
|
+
import { streamAgentResponse } from "./events/streaming.js";
|
|
10
|
+
import { handleAppMention } from "./events/app-mention.js";
|
|
11
|
+
import { handleMessageShortcut, handleOpenAgentSelectorModal, handleOpenFollowUpModal } from "./events/block-actions.js";
|
|
12
|
+
import { handleFollowUpSubmission, handleModalSubmission } from "./events/modal-submission.js";
|
|
13
|
+
import "./events/index.js";
|
|
14
|
+
import { parseSlackCommandBody, parseSlackEventBody, verifySlackRequest } from "./security.js";
|
|
15
|
+
|
|
16
|
+
export { SlackErrorType, buildAgentSelectorModal, buildConversationResponseBlocks, buildFollowUpButton, buildFollowUpModal, buildMessageShortcutModal, checkIfBotThread, checkUserIsChannelMember, classifyError, clearWorkspaceConnectionCache, computeWorkspaceConnectionId, createAgentListMessage, createAlreadyLinkedMessage, createConnectSession, createContextBlock, createErrorMessage, createJwtLinkMessage, createNotLinkedMessage, createStatusMessage, createUnlinkSuccessMessage, createUpdatedHelpMessage, deleteWorkspaceInstallation, fetchAgentsForProject, fetchProjectsForTenant, findCachedUserMapping, findWorkspaceConnectionByTeamId, generateSlackConversationId, getAgentConfigSources, getBotTokenForTeam, getChannelAgentConfig, getConnectionAccessToken, getSlackChannels, getSlackClient, getSlackIntegrationId, getSlackNango, getSlackTeamInfo, getSlackUserInfo, getThreadContext, getUserFriendlyErrorMessage, getWorkspaceDefaultAgent, getWorkspaceDefaultAgentFromNango, handleAgentListCommand, handleAgentPickerCommand, handleAppMention, handleCommand, handleFollowUpSubmission, handleHelpCommand, handleLinkCommand, handleMessageShortcut, handleModalSubmission, handleOpenAgentSelectorModal, handleOpenFollowUpModal, handleQuestionCommand, handleRunCommand, handleStatusCommand, handleUnlinkCommand, listWorkspaceInstallations, markdownToMrkdwn, parseSlackCommandBody, parseSlackEventBody, postMessage, postMessageInThread, resolveEffectiveAgent, revokeSlackToken, sendResponseUrlMessage, setBotTokenForTeam, setWorkspaceDefaultAgent, storeWorkspaceInstallation, streamAgentResponse, updateConnectionMetadata, verifySlackRequest };
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { ModalView } from "@slack/web-api";
|
|
2
|
+
|
|
3
|
+
//#region src/slack/services/modals.d.ts
|
|
4
|
+
|
|
5
|
+
/** Agent option for dropdown selection */
|
|
6
|
+
interface AgentOption {
|
|
7
|
+
id: string;
|
|
8
|
+
name: string | null;
|
|
9
|
+
projectId: string;
|
|
10
|
+
projectName: string | null;
|
|
11
|
+
}
|
|
12
|
+
interface ModalMetadata {
|
|
13
|
+
channel: string;
|
|
14
|
+
threadTs?: string;
|
|
15
|
+
messageTs: string;
|
|
16
|
+
teamId: string;
|
|
17
|
+
slackUserId: string;
|
|
18
|
+
tenantId: string;
|
|
19
|
+
isInThread: boolean;
|
|
20
|
+
threadMessageCount?: number;
|
|
21
|
+
buttonResponseUrl?: string;
|
|
22
|
+
messageContext?: string;
|
|
23
|
+
}
|
|
24
|
+
interface FollowUpModalMetadata {
|
|
25
|
+
conversationId: string;
|
|
26
|
+
agentId: string;
|
|
27
|
+
projectId: string;
|
|
28
|
+
tenantId: string;
|
|
29
|
+
teamId: string;
|
|
30
|
+
slackUserId: string;
|
|
31
|
+
channel: string;
|
|
32
|
+
}
|
|
33
|
+
interface BuildAgentSelectorModalParams {
|
|
34
|
+
projects: Array<{
|
|
35
|
+
id: string;
|
|
36
|
+
name: string;
|
|
37
|
+
}>;
|
|
38
|
+
agents: AgentOption[];
|
|
39
|
+
metadata: ModalMetadata;
|
|
40
|
+
selectedProjectId?: string;
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Build the agent selector modal.
|
|
44
|
+
*
|
|
45
|
+
* Shows:
|
|
46
|
+
* - Project dropdown
|
|
47
|
+
* - Agent dropdown (updates based on project selection)
|
|
48
|
+
* - Include thread context checkbox (if in thread)
|
|
49
|
+
* - Question/instructions input
|
|
50
|
+
* - Dashboard link
|
|
51
|
+
*
|
|
52
|
+
* All responses from this modal are private (ephemeral).
|
|
53
|
+
*/
|
|
54
|
+
declare function buildAgentSelectorModal(params: BuildAgentSelectorModalParams): ModalView;
|
|
55
|
+
/**
|
|
56
|
+
* Build a follow-up modal for continuing a conversation.
|
|
57
|
+
*
|
|
58
|
+
* Shows only a prompt input. Agent and project are carried from the previous turn
|
|
59
|
+
* via metadata. The conversationId ensures the agent has full history.
|
|
60
|
+
*/
|
|
61
|
+
declare function buildFollowUpModal(metadata: FollowUpModalMetadata): ModalView;
|
|
62
|
+
interface BuildMessageShortcutModalParams {
|
|
63
|
+
projects: Array<{
|
|
64
|
+
id: string;
|
|
65
|
+
name: string;
|
|
66
|
+
}>;
|
|
67
|
+
agents: AgentOption[];
|
|
68
|
+
metadata: ModalMetadata;
|
|
69
|
+
selectedProjectId?: string;
|
|
70
|
+
messageContext: string;
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Build the modal for message shortcut (context menu on a message).
|
|
74
|
+
*
|
|
75
|
+
* Shows:
|
|
76
|
+
* - Message context (read-only display)
|
|
77
|
+
* - Project dropdown
|
|
78
|
+
* - Agent dropdown
|
|
79
|
+
* - Additional instructions input
|
|
80
|
+
* - Dashboard link
|
|
81
|
+
*
|
|
82
|
+
* All responses from this modal are private (ephemeral).
|
|
83
|
+
*/
|
|
84
|
+
declare function buildMessageShortcutModal(params: BuildMessageShortcutModalParams): ModalView;
|
|
85
|
+
//#endregion
|
|
86
|
+
export { AgentOption, BuildAgentSelectorModalParams, BuildMessageShortcutModalParams, FollowUpModalMetadata, ModalMetadata, buildAgentSelectorModal, buildFollowUpModal, buildMessageShortcutModal };
|