@desplega.ai/agent-swarm 1.72.1 → 1.73.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@desplega.ai/agent-swarm",
3
- "version": "1.72.1",
3
+ "version": "1.73.0",
4
4
  "description": "Multi-agent orchestration for Claude Code, Codex, Gemini CLI, and other AI coding assistants",
5
5
  "license": "MIT",
6
6
  "author": "desplega.sh <contact@desplega.sh>",
@@ -136,6 +136,11 @@ function getWebhookUrl(req: IncomingMessage): string {
136
136
  }
137
137
 
138
138
  function getRedirectUri(req: IncomingMessage): string {
139
+ // Mirror src/jira/app.ts: prefer the explicit JIRA_REDIRECT_URI override,
140
+ // otherwise derive from the API base URL. Keeps the UI display consistent
141
+ // with the URI persisted into oauth_apps and used in the actual OAuth flow.
142
+ const override = process.env.JIRA_REDIRECT_URI?.trim();
143
+ if (override) return override;
139
144
  return `${deriveApiBaseUrl(req)}/api/trackers/jira/callback`;
140
145
  }
141
146
 
@@ -5,7 +5,7 @@
5
5
  * across responses.ts, handlers.ts, thread-buffer.ts).
6
6
  */
7
7
 
8
- const appUrl = process.env.APP_URL || "";
8
+ import { getAppUrl } from "../utils/constants";
9
9
 
10
10
  // Slack limits section text to 3000 chars; we use 2900 for safety
11
11
  const MAX_SECTION_LENGTH = 2900;
@@ -16,24 +16,21 @@ type SlackBlock = any;
16
16
  // --- Shared utilities ---
17
17
 
18
18
  /**
19
- * Get a Slack-formatted link to the task in the dashboard, or just the short ID.
19
+ * Get a Slack-formatted clickable link to the task in the dashboard.
20
+ * Always returns Slack mrkdwn link syntax (`<url|label>`) so partial task
21
+ * IDs are clickable in every message — falls back to the public dashboard
22
+ * when APP_URL is not configured.
20
23
  */
21
24
  export function getTaskLink(taskId: string): string {
22
25
  const shortId = taskId.slice(0, 8);
23
- if (appUrl) {
24
- return `<${appUrl}?tab=tasks&task=${taskId}&expand=true|\`${shortId}\`>`;
25
- }
26
- return `\`${shortId}\``;
26
+ return `<${getTaskUrl(taskId)}|\`${shortId}\`>`;
27
27
  }
28
28
 
29
29
  /**
30
30
  * Get a raw dashboard URL for a task (for link buttons).
31
31
  */
32
32
  export function getTaskUrl(taskId: string): string {
33
- if (appUrl) {
34
- return `${appUrl}?tab=tasks&task=${taskId}&expand=true`;
35
- }
36
- return "";
33
+ return `${getAppUrl()}/tasks/${taskId}`;
37
34
  }
38
35
 
39
36
  /**
@@ -214,9 +211,9 @@ export function buildProgressBlocks(opts: {
214
211
  taskId: string;
215
212
  progress: string;
216
213
  }): SlackBlock[] {
217
- const shortId = opts.taskId.slice(0, 8);
214
+ const taskLink = getTaskLink(opts.taskId);
218
215
  return [
219
- sectionBlock(`*${opts.agentName}* (\`${shortId}\`): ${opts.progress}`),
216
+ sectionBlock(`*${opts.agentName}* (${taskLink}): ${opts.progress}`),
220
217
  cancelActionBlock(opts.taskId),
221
218
  ];
222
219
  }
@@ -44,22 +44,65 @@ describe("markdownToSlack", () => {
44
44
  });
45
45
 
46
46
  describe("getTaskLink", () => {
47
- test("returns short ID when no APP_URL", () => {
48
- // APP_URL is not set in test env
49
- const link = getTaskLink("abcdef12-3456-7890-abcd-ef1234567890");
50
- expect(link).toContain("abcdef12");
47
+ test("always returns a Slack hyperlink with clickable short ID", () => {
48
+ const taskId = "abcdef12-3456-7890-abcd-ef1234567890";
49
+ const link = getTaskLink(taskId);
50
+ // Slack mrkdwn link syntax: <url|label>
51
+ expect(link).toMatch(
52
+ /^<https?:\/\/.+\/tasks\/abcdef12-3456-7890-abcd-ef1234567890\|`abcdef12`>$/,
53
+ );
54
+ expect(link).toContain("|`abcdef12`>");
55
+ expect(link).toContain(taskId);
56
+ });
57
+
58
+ test("uses APP_URL when set", () => {
59
+ const original = process.env.APP_URL;
60
+ process.env.APP_URL = "https://my-custom-dashboard.example.com";
61
+ try {
62
+ const link = getTaskLink("abcdef12-3456-7890-abcd-ef1234567890");
63
+ expect(link).toContain(
64
+ "https://my-custom-dashboard.example.com/tasks/abcdef12-3456-7890-abcd-ef1234567890",
65
+ );
66
+ } finally {
67
+ if (original === undefined) delete process.env.APP_URL;
68
+ else process.env.APP_URL = original;
69
+ }
70
+ });
71
+
72
+ test("strips trailing slash from APP_URL", () => {
73
+ const original = process.env.APP_URL;
74
+ process.env.APP_URL = "https://dashboard.example.com/";
75
+ try {
76
+ const link = getTaskLink("abcdef12-3456-7890-abcd-ef1234567890");
77
+ expect(link).toContain("https://dashboard.example.com/tasks/");
78
+ expect(link).not.toContain("//tasks/");
79
+ } finally {
80
+ if (original === undefined) delete process.env.APP_URL;
81
+ else process.env.APP_URL = original;
82
+ }
83
+ });
84
+
85
+ test("falls back to public dashboard when APP_URL is unset", () => {
86
+ const original = process.env.APP_URL;
87
+ delete process.env.APP_URL;
88
+ try {
89
+ const link = getTaskLink("abcdef12-3456-7890-abcd-ef1234567890");
90
+ expect(link).toContain(
91
+ "https://app.agent-swarm.dev/tasks/abcdef12-3456-7890-abcd-ef1234567890",
92
+ );
93
+ expect(link.startsWith("<")).toBe(true);
94
+ expect(link.endsWith(">")).toBe(true);
95
+ } finally {
96
+ if (original !== undefined) process.env.APP_URL = original;
97
+ }
51
98
  });
52
99
  });
53
100
 
54
101
  describe("getTaskUrl", () => {
55
- test("returns URL with task ID or empty string", () => {
102
+ test("always returns a non-empty URL containing the task ID", () => {
56
103
  const url = getTaskUrl("some-id");
57
- // When APP_URL is set, URL contains the task ID; when not set, returns ""
58
- if (url) {
59
- expect(url).toContain("some-id");
60
- } else {
61
- expect(url).toBe("");
62
- }
104
+ expect(url).toContain("/tasks/some-id");
105
+ expect(url).toMatch(/^https?:\/\//);
63
106
  });
64
107
  });
65
108
 
@@ -93,6 +136,17 @@ describe("buildCompletedBlocks", () => {
93
136
  expect(blocks[0].text.text).toContain("45s");
94
137
  });
95
138
 
139
+ test("partial task ID is rendered as a clickable Slack hyperlink", () => {
140
+ const blocks = buildCompletedBlocks({
141
+ agentName: "Alpha",
142
+ taskId: "abcdef12-3456-7890-abcd-ef1234567890",
143
+ body: "Done",
144
+ });
145
+ expect(blocks[0].text.text).toMatch(
146
+ /<https?:\/\/[^|>]+\/tasks\/abcdef12-3456-7890-abcd-ef1234567890\|`abcdef12`>/,
147
+ );
148
+ });
149
+
96
150
  test("splits long body into multiple sections", () => {
97
151
  const longBody = "x".repeat(6000);
98
152
  const blocks = buildCompletedBlocks({
@@ -148,7 +202,7 @@ describe("buildProgressBlocks", () => {
148
202
  });
149
203
 
150
204
  expect(blocks.length).toBe(2);
151
- // Single line: *Gamma* (`aabbccdd`): Analyzing codebase...
205
+ // Single line: *Gamma* (<URL|`aabbccdd`>): Analyzing codebase...
152
206
  // (no ⏳ prefix — progress strings now carry their own emoji)
153
207
  expect(blocks[0].type).toBe("section");
154
208
  expect(blocks[0].text.text).not.toContain("⏳");
@@ -161,6 +215,19 @@ describe("buildProgressBlocks", () => {
161
215
  expect(blocks[1].elements[0].style).toBe("danger");
162
216
  expect(blocks[1].elements[0].confirm).toBeDefined();
163
217
  });
218
+
219
+ test("partial task ID is rendered as a clickable Slack hyperlink", () => {
220
+ const taskId = "aabbccdd-1234-5678-9012-abcdefabcdef";
221
+ const blocks = buildProgressBlocks({
222
+ agentName: "Gamma",
223
+ taskId,
224
+ progress: "Working...",
225
+ });
226
+ // Slack mrkdwn link syntax: <url|`shortId`>
227
+ expect(blocks[0].text.text).toMatch(
228
+ /<https?:\/\/[^|>]+\/tasks\/aabbccdd-1234-5678-9012-abcdefabcdef\|`aabbccdd`>/,
229
+ );
230
+ });
164
231
  });
165
232
 
166
233
  describe("buildAssignmentSummaryBlocks", () => {
@@ -0,0 +1,19 @@
1
+ /**
2
+ * Shared constants used across worker- and server-side code.
3
+ */
4
+
5
+ /**
6
+ * Default dashboard URL used when `APP_URL` is unset. Points at the public
7
+ * production dashboard so links (Slack messages, approval URLs, etc.) are
8
+ * always renderable. Self-hosted operators should set `APP_URL` to override.
9
+ */
10
+ export const DEFAULT_APP_URL = "https://app.agent-swarm.dev";
11
+
12
+ /**
13
+ * Resolve the effective app/dashboard URL from `APP_URL` (with trailing
14
+ * slashes stripped), falling back to {@link DEFAULT_APP_URL}.
15
+ */
16
+ export function getAppUrl(): string {
17
+ const raw = process.env.APP_URL?.trim();
18
+ return (raw || DEFAULT_APP_URL).replace(/\/+$/, "");
19
+ }
@@ -1,5 +1,6 @@
1
1
  import { z } from "zod";
2
2
  import type { ExecutorMeta } from "../../types";
3
+ import { getAppUrl } from "../../utils/constants";
3
4
  import type { ExecutorResult } from "./base";
4
5
  import { BaseExecutor } from "./base";
5
6
 
@@ -173,7 +174,7 @@ export class HumanInTheLoopExecutor extends BaseExecutor<
173
174
  ): Promise<void> {
174
175
  if (!config.notifications?.length) return;
175
176
 
176
- const approvalUrl = `https://app.agent-swarm.dev/approval-requests/${requestId}`;
177
+ const approvalUrl = `${getAppUrl()}/approval-requests/${requestId}`;
177
178
  const updatedChannels = [...config.notifications] as Array<
178
179
  z.infer<typeof NotificationConfigSchema> & { messageTs?: string }
179
180
  >;