@sentry/junior 0.26.0 → 0.27.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/app.js CHANGED
@@ -25,7 +25,7 @@ import {
25
25
  sandboxSkillDir,
26
26
  sandboxSkillFile,
27
27
  toOptionalTrimmed
28
- } from "./chunk-A75TWGF2.js";
28
+ } from "./chunk-4PVJHUEV.js";
29
29
  import {
30
30
  CredentialUnavailableError,
31
31
  buildOAuthTokenRequest,
@@ -884,6 +884,9 @@ function mapSlackError(error) {
884
884
  if (apiError === "invalid_arguments") {
885
885
  return new SlackActionError(message, "invalid_arguments", baseOptions);
886
886
  }
887
+ if (apiError === "invalid_cursor") {
888
+ return new SlackActionError(message, "invalid_arguments", baseOptions);
889
+ }
887
890
  if (apiError === "invalid_name") {
888
891
  return new SlackActionError(message, "invalid_arguments", baseOptions);
889
892
  }
@@ -2396,20 +2399,23 @@ function buildConversationContext(conversation, options = {}) {
2396
2399
  lines.push("<thread-compactions>");
2397
2400
  for (const [index, compaction] of conversation.compactions.entries()) {
2398
2401
  lines.push(
2399
- [
2400
- `summary_${index + 1}:`,
2401
- compaction.summary,
2402
- `covered_messages: ${compaction.coveredMessageIds.length}`,
2403
- `created_at: ${new Date(compaction.createdAtMs).toISOString()}`
2404
- ].join(" ")
2402
+ ` <compaction index="${index + 1}" covered_messages="${compaction.coveredMessageIds.length}" created_at="${new Date(compaction.createdAtMs).toISOString()}">`,
2403
+ compaction.summary,
2404
+ " </compaction>"
2405
2405
  );
2406
2406
  }
2407
- lines.push("</thread-compactions>");
2408
- lines.push("");
2407
+ lines.push("</thread-compactions>", "");
2409
2408
  }
2410
2409
  lines.push("<thread-transcript>");
2411
- for (const message of messages) {
2412
- lines.push(renderConversationMessageLine(message, conversation));
2410
+ for (const [index, message] of messages.entries()) {
2411
+ const author = escapeXml(message.author?.userName ?? message.role);
2412
+ const ts = new Date(message.createdAtMs).toISOString();
2413
+ const slackTsAttr = message.meta?.slackTs ? ` slack_ts="${escapeXml(message.meta.slackTs)}"` : "";
2414
+ lines.push(
2415
+ ` <message index="${index + 1}" ts="${ts}" role="${message.role}" author="${author}"${slackTsAttr}>`,
2416
+ renderConversationMessageLine(message, conversation),
2417
+ " </message>"
2418
+ );
2413
2419
  }
2414
2420
  lines.push("</thread-transcript>");
2415
2421
  return lines.join("\n");
@@ -2441,9 +2447,14 @@ async function summarizeConversationChunk(messages, conversation, context, deps)
2441
2447
  role: "user",
2442
2448
  content: [
2443
2449
  "Summarize the following older Slack thread transcript segment for future assistant turns.",
2444
- "Keep the summary factual and concise.",
2445
- "Preserve decisions, commitments, constraints, locations, hiring criteria, and unresolved asks.",
2446
- "Do not invent details.",
2450
+ "Keep the summary factual and concise. Do not invent details.",
2451
+ "",
2452
+ "Output exactly three XML sections in this order:",
2453
+ "<active-asks> one bullet per outstanding user ask that has not been narrowed, answered, or superseded by a later turn. Omit the section body if none. </active-asks>",
2454
+ "<superseded-or-completed-asks> one bullet per ask that has been rescoped, narrowed, answered, or already acted on in this segment. Include the replacement/outcome inline. Omit the section body if none. </superseded-or-completed-asks>",
2455
+ "<facts> one bullet per durable fact useful regardless of scope: names, ids, URLs, decisions, locations, preferences, constraints that remain true. Omit the section body if none. </facts>",
2456
+ "",
2457
+ "Do not output any text outside the three sections.",
2447
2458
  "",
2448
2459
  transcript
2449
2460
  ].join("\n"),
@@ -3472,7 +3483,7 @@ function buildSystemPrompt(params) {
3472
3483
  [
3473
3484
  "- For factual or external questions, run tools/skills first, then answer from evidence.",
3474
3485
  "- Use tool descriptions as the source of truth for when each tool should or should not be called.",
3475
- "- Use `bash` to inspect skill files from `skill_dir` and run shell commands inside the sandbox workspace.",
3486
+ "- Use `reportProgress` only when you start a major new phase of work, such as researching, reading, executing, reviewing, or drafting. Do not call it for every tool or small substep.",
3476
3487
  "- When using CLI tools through `bash`, prefer deterministic non-interactive flags and avoid commands that wait for prompts or editors.",
3477
3488
  "- Keep routine setup and research steps silent in user-facing replies. Do not narrate duplicate checks, credential issuance, file writes, or similar internal progress unless the result is user-relevant.",
3478
3489
  "- If a routine prerequisite check finds nothing notable, omit it entirely from the final reply and report only the user-relevant outcome.",
@@ -3482,22 +3493,11 @@ function buildSystemPrompt(params) {
3482
3493
  "- When the user provides multiple sources, synthesize them explicitly as one combined answer instead of anchoring the reply on a single page or API call.",
3483
3494
  "- When the user provides explicit URLs or named sources, briefly anchor the answer to those provided sources (for example, 'Across the provided Slack docs...') so the summary reads as researched rather than generic memory.",
3484
3495
  "- Do not include internal process chatter such as 'let me find', 'fetching now', 'good, I have sources', 'trying smaller limits', or 'I now have sufficient context' in the final user-facing reply.",
3485
- "- Use `attachFile` for files that actually exist in the sandbox (for example screenshots, PDFs, logs), or for `attachment_path` values returned by `imageGenerate`.",
3486
- "- If the user asks to see/share/show a screenshot or file, attach the file with `attachFile` instead of only reporting its path.",
3487
3496
  "- Never claim a screenshot/file is attached unless `attachFile` succeeded in this turn.",
3488
3497
  "- If `attachFile` fails, explain the failure and do not say the file was shared.",
3489
- "- Use `imageGenerate` when the user asks for image creation.",
3490
- "- `imageGenerate` returns generated image metadata, including `attachment_path` values you can pass to `attachFile` when the user should receive the image.",
3491
- "- Use `slackCanvasCreate` for long-form docs/specs and `slackCanvasUpdate` for doc follow-ups.",
3492
- "- `slackCanvasUpdate` targets the active artifact-context canvas automatically; do not ask the user for `canvas_id`.",
3493
3498
  "- When you create or update a Slack artifact in this turn (for example a canvas, list, posted message, or attached file), mention it explicitly in the final reply and include its link when the tool returned one.",
3494
- "- Use `slackListCreate`, `slackListAddItems`, and `slackListUpdateItem` for actionable task tracking.",
3495
- "- `slackListAddItems`, `slackListGetItems`, and `slackListUpdateItem` target the active artifact-context list automatically; do not ask the user for `list_id`.",
3496
- "- If the user explicitly asks to post/send/share/say/show/announce/broadcast in the channel (outside this thread), call `slackChannelPostMessage` with the requested text instead of only replying in-thread.",
3497
3499
  "- For explicit in-channel post requests, prefer no thread text reply after a successful channel post. A reaction-only acknowledgment is acceptable when useful.",
3498
- "- Use `slackMessageAddReaction` for rare lightweight acknowledgements. It reacts to the current inbound message via runtime context; never pick a target message yourself.",
3499
- "- If the user explicitly asks for an emoji reaction instead of text, use `slackMessageAddReaction` with a Slack emoji alias name (for example `thumbsup`, `white_check_mark`, or `eyes`, not unicode emoji), and avoid redundant acknowledgment text.",
3500
- "- Suggested acknowledgement reactions include `wave`, `white_check_mark`, `thumbsup`, and `eyes`, but choose what best fits the request.",
3500
+ "- When the user explicitly asks for an emoji reaction instead of text, react and skip the text reply.",
3501
3501
  "- After the matching plugin-owned skill is loaded, authenticated bash commands for that skill get provider credentials injected automatically for the current turn.",
3502
3502
  "- Resolve repo/project/org defaults before authenticated provider commands so the runtime can narrow injected credentials correctly.",
3503
3503
  "- If no loaded skill clearly owns the authenticated command, load the matching skill first instead of guessing from the provider catalog.",
@@ -3508,10 +3508,7 @@ function buildSystemPrompt(params) {
3508
3508
  "- `jr-rpc` config commands are built into the bash runtime for conversation-scoped config work; they do not require a separate helper binary in the sandbox.",
3509
3509
  "- When your work is complete, provide the exact user-facing markdown response.",
3510
3510
  "- Do not use reaction-based progress signals; Assistants API status already covers in-progress UX.",
3511
- "- Prefer `webSearch` before `webFetch` when the user gave no URL.",
3512
3511
  "- Never call side-effecting tools when the user only asked for analysis or options.",
3513
- "- `loadSkill` activates MCP tools when the loaded skill exposes them. After loading, call them directly by name (for example `mcp__provider__tool_name`).",
3514
- "- `searchTools` searches active MCP tools exposed by currently loaded skills when you need to rediscover or filter them.",
3515
3512
  "- When the user asks for their conversation ID, trace ID, or a reference for Sentry lookup, use the IDs from `<session-context>` and `<turn-context>` in the user turn."
3516
3513
  ].join("\n")
3517
3514
  ),
@@ -3529,8 +3526,7 @@ function buildSystemPrompt(params) {
3529
3526
  "- Never apply skill-specific behavior unless the skill is present in <loaded_skills> or `loadSkill` succeeded in this turn.",
3530
3527
  "- Load only the best matching skill first; do not load multiple skills upfront.",
3531
3528
  "- After `loadSkill`, use `skill_dir` as the root for any referenced files you read via `bash`.",
3532
- "- If a loaded skill exposes MCP tools, they are registered as callable tools after `loadSkill` returns. Call them directly by name.",
3533
- "- Use `searchTools` only when you need to rediscover or filter the currently exposed MCP tools.",
3529
+ "- If a loaded skill exposes MCP tools, they are registered as callable tools after `loadSkill` returns. Call them directly by name (for example `mcp__provider__tool_name`).",
3534
3530
  "- If no skill is a clear fit, continue with normal tool usage."
3535
3531
  ].join("\n")
3536
3532
  ),
@@ -5193,8 +5189,33 @@ function createReadFileTool() {
5193
5189
  });
5194
5190
  }
5195
5191
 
5196
- // src/chat/tools/skill/search-tools.ts
5192
+ // src/chat/tools/runtime/report-progress.ts
5197
5193
  import { Type as Type6 } from "@sinclair/typebox";
5194
+ function createReportProgressTool() {
5195
+ return tool({
5196
+ description: "Update assistant status when you start a major new phase of work. Use for sparse phase changes such as researching, reading, executing, reviewing, or drafting. Do not call this for every tool or minor substep.",
5197
+ inputSchema: Type6.Object({
5198
+ phase: Type6.Union([
5199
+ Type6.Literal("thinking"),
5200
+ Type6.Literal("researching"),
5201
+ Type6.Literal("reading"),
5202
+ Type6.Literal("executing"),
5203
+ Type6.Literal("reviewing"),
5204
+ Type6.Literal("drafting")
5205
+ ]),
5206
+ detail: Type6.Optional(
5207
+ Type6.String({
5208
+ minLength: 1,
5209
+ maxLength: 40,
5210
+ description: "Optional short user-facing detail, such as docs, tests, source files, or reply."
5211
+ })
5212
+ )
5213
+ })
5214
+ });
5215
+ }
5216
+
5217
+ // src/chat/tools/skill/search-tools.ts
5218
+ import { Type as Type7 } from "@sinclair/typebox";
5198
5219
 
5199
5220
  // src/chat/tools/skill/mcp-tool-summary.ts
5200
5221
  function summarizeInputSchema(schema) {
@@ -5226,20 +5247,20 @@ var MAX_LIMIT = 20;
5226
5247
  function createSearchToolsTool(mcpToolManager, getActiveSkills) {
5227
5248
  return tool({
5228
5249
  description: "Search active MCP tools exposed by the currently loaded skills. Use when you need to rediscover or filter active tools.",
5229
- inputSchema: Type6.Object(
5250
+ inputSchema: Type7.Object(
5230
5251
  {
5231
- query: Type6.String({
5252
+ query: Type7.String({
5232
5253
  minLength: 1,
5233
5254
  description: "Search query for matching MCP tool names or descriptions."
5234
5255
  }),
5235
- provider: Type6.Optional(
5236
- Type6.String({
5256
+ provider: Type7.Optional(
5257
+ Type7.String({
5237
5258
  minLength: 1,
5238
5259
  description: "Optional MCP provider filter, for example notion or sentry."
5239
5260
  })
5240
5261
  ),
5241
- limit: Type6.Optional(
5242
- Type6.Integer({
5262
+ limit: Type7.Optional(
5263
+ Type7.Integer({
5243
5264
  minimum: 1,
5244
5265
  maximum: MAX_LIMIT,
5245
5266
  description: "Maximum number of matching tools to return."
@@ -5265,7 +5286,7 @@ function createSearchToolsTool(mcpToolManager, getActiveSkills) {
5265
5286
  }
5266
5287
 
5267
5288
  // src/chat/tools/slack/channel-list-messages.ts
5268
- import { Type as Type7 } from "@sinclair/typebox";
5289
+ import { Type as Type8 } from "@sinclair/typebox";
5269
5290
 
5270
5291
  // src/chat/slack/channel.ts
5271
5292
  async function listChannelMessages(input) {
@@ -5354,39 +5375,39 @@ async function listThreadReplies(input) {
5354
5375
  function createSlackChannelListMessagesTool(context) {
5355
5376
  return tool({
5356
5377
  description: "List channel messages from Slack history in the active channel context. Use when the user asks for recent or historical channel context outside this thread. Do not use for live monitoring or when current thread context already answers the question.",
5357
- inputSchema: Type7.Object({
5358
- limit: Type7.Optional(
5359
- Type7.Integer({
5378
+ inputSchema: Type8.Object({
5379
+ limit: Type8.Optional(
5380
+ Type8.Integer({
5360
5381
  minimum: 1,
5361
5382
  maximum: 1e3,
5362
5383
  description: "Maximum number of messages to return across pages."
5363
5384
  })
5364
5385
  ),
5365
- cursor: Type7.Optional(
5366
- Type7.String({
5386
+ cursor: Type8.Optional(
5387
+ Type8.String({
5367
5388
  minLength: 1,
5368
5389
  description: "Optional cursor to continue from a prior call."
5369
5390
  })
5370
5391
  ),
5371
- oldest: Type7.Optional(
5372
- Type7.String({
5392
+ oldest: Type8.Optional(
5393
+ Type8.String({
5373
5394
  minLength: 1,
5374
5395
  description: "Optional oldest message timestamp (Slack ts) for range filtering."
5375
5396
  })
5376
5397
  ),
5377
- latest: Type7.Optional(
5378
- Type7.String({
5398
+ latest: Type8.Optional(
5399
+ Type8.String({
5379
5400
  minLength: 1,
5380
5401
  description: "Optional latest message timestamp (Slack ts) for range filtering."
5381
5402
  })
5382
5403
  ),
5383
- inclusive: Type7.Optional(
5384
- Type7.Boolean({
5404
+ inclusive: Type8.Optional(
5405
+ Type8.Boolean({
5385
5406
  description: "Whether oldest/latest bounds should be inclusive."
5386
5407
  })
5387
5408
  ),
5388
- max_pages: Type7.Optional(
5389
- Type7.Integer({
5409
+ max_pages: Type8.Optional(
5410
+ Type8.Integer({
5390
5411
  minimum: 1,
5391
5412
  maximum: 10,
5392
5413
  description: "Maximum number of API pages to traverse in a single call."
@@ -5408,15 +5429,26 @@ function createSlackChannelListMessagesTool(context) {
5408
5429
  error: "No active channel context is available for history lookup"
5409
5430
  };
5410
5431
  }
5411
- const result = await listChannelMessages({
5412
- channelId: targetChannelId,
5413
- limit: limit ?? 100,
5414
- cursor,
5415
- oldest,
5416
- latest,
5417
- inclusive,
5418
- maxPages: max_pages
5419
- });
5432
+ let result;
5433
+ try {
5434
+ result = await listChannelMessages({
5435
+ channelId: targetChannelId,
5436
+ limit: limit ?? 100,
5437
+ cursor,
5438
+ oldest,
5439
+ latest,
5440
+ inclusive,
5441
+ maxPages: max_pages
5442
+ });
5443
+ } catch (error) {
5444
+ if (error instanceof SlackActionError && error.apiError === "invalid_cursor") {
5445
+ return {
5446
+ ok: false,
5447
+ error: "The supplied Slack history cursor is no longer valid. Retry the lookup without `cursor` to start from the newest page again."
5448
+ };
5449
+ }
5450
+ throw error;
5451
+ }
5420
5452
  return {
5421
5453
  ok: true,
5422
5454
  channel_id: targetChannelId,
@@ -5429,7 +5461,7 @@ function createSlackChannelListMessagesTool(context) {
5429
5461
  }
5430
5462
 
5431
5463
  // src/chat/tools/slack/channel-post-message.ts
5432
- import { Type as Type8 } from "@sinclair/typebox";
5464
+ import { Type as Type9 } from "@sinclair/typebox";
5433
5465
 
5434
5466
  // src/chat/tools/idempotency.ts
5435
5467
  function stableSerialize(value) {
@@ -5451,8 +5483,8 @@ function createOperationKey(toolName, input) {
5451
5483
  function createSlackChannelPostMessageTool(context, state) {
5452
5484
  return tool({
5453
5485
  description: "Post a message in the active Slack channel context (outside the thread). Use this when the user explicitly asks to post/send/share/say something in the channel. Do not use for normal thread replies or speculative broadcasts. Do not claim a channel message was posted unless this tool succeeds in this turn.",
5454
- inputSchema: Type8.Object({
5455
- text: Type8.String({
5486
+ inputSchema: Type9.Object({
5487
+ text: Type9.String({
5456
5488
  minLength: 1,
5457
5489
  maxLength: 4e4,
5458
5490
  description: "Slack mrkdwn text to post."
@@ -5495,12 +5527,12 @@ function createSlackChannelPostMessageTool(context, state) {
5495
5527
  }
5496
5528
 
5497
5529
  // src/chat/tools/slack/message-add-reaction.ts
5498
- import { Type as Type9 } from "@sinclair/typebox";
5530
+ import { Type as Type10 } from "@sinclair/typebox";
5499
5531
  function createSlackMessageAddReactionTool(context, state) {
5500
5532
  return tool({
5501
5533
  description: "Add an emoji reaction to the current inbound Slack message. Use sparingly for lightweight acknowledgements. Provide a Slack emoji alias name (for example `thumbsup`, `white_check_mark`, or `thumbsup::skin-tone-6`), not a unicode emoji glyph. The target message is injected by runtime context; do not use this for arbitrary historical messages.",
5502
- inputSchema: Type9.Object({
5503
- emoji: Type9.String({
5534
+ inputSchema: Type10.Object({
5535
+ emoji: Type10.String({
5504
5536
  minLength: 1,
5505
5537
  maxLength: 64,
5506
5538
  description: "Slack emoji alias name to react with (for example `thumbsup`, `white_check_mark`, or `thumbsup::skin-tone-6`). Optional surrounding colons are allowed."
@@ -5558,7 +5590,7 @@ function createSlackMessageAddReactionTool(context, state) {
5558
5590
  }
5559
5591
 
5560
5592
  // src/chat/tools/slack/canvas-tools.ts
5561
- import { Type as Type10 } from "@sinclair/typebox";
5593
+ import { Type as Type11 } from "@sinclair/typebox";
5562
5594
 
5563
5595
  // src/chat/tools/slack/canvases.ts
5564
5596
  function normalizeCanvasMarkdown(markdown) {
@@ -5577,19 +5609,11 @@ function normalizeCanvasMarkdown(markdown) {
5577
5609
  async function createCanvas(input) {
5578
5610
  const client2 = getSlackClient();
5579
5611
  const normalizedChannelId = normalizeSlackConversationId(input.channelId);
5580
- const isConversationScoped = isConversationScopedChannel(normalizedChannelId);
5581
- if (!isConversationScoped) {
5582
- throw new Error(
5583
- "Canvas creation requires an active Slack conversation context (C/G/D)."
5584
- );
5585
- }
5586
5612
  const channelPrefix = normalizedChannelId?.slice(0, 1) ?? "none";
5587
- const action = "conversations.canvases.create";
5588
5613
  const normalizedContent = normalizeCanvasMarkdown(input.markdown);
5589
5614
  const result = await withSlackRetries(
5590
5615
  async () => {
5591
- return client2.conversations.canvases.create({
5592
- channel_id: normalizedChannelId,
5616
+ return client2.canvases.create({
5593
5617
  title: input.title,
5594
5618
  document_content: {
5595
5619
  type: "markdown",
@@ -5599,7 +5623,7 @@ async function createCanvas(input) {
5599
5623
  },
5600
5624
  3,
5601
5625
  {
5602
- action,
5626
+ action: "canvases.create",
5603
5627
  attributes: {
5604
5628
  "app.slack.canvas.channel_id_prefix": channelPrefix,
5605
5629
  "app.slack.canvas.has_channel_id": Boolean(input.channelId),
@@ -5613,6 +5637,9 @@ async function createCanvas(input) {
5613
5637
  if (!result.canvas_id) {
5614
5638
  throw new Error("Slack canvas was created without canvas_id");
5615
5639
  }
5640
+ if (normalizedChannelId && isConversationScopedChannel(normalizedChannelId)) {
5641
+ await grantChannelCanvasAccess(result.canvas_id, normalizedChannelId);
5642
+ }
5616
5643
  let permalink;
5617
5644
  try {
5618
5645
  permalink = await getFilePermalink(result.canvas_id);
@@ -5623,6 +5650,39 @@ async function createCanvas(input) {
5623
5650
  permalink
5624
5651
  };
5625
5652
  }
5653
+ async function grantChannelCanvasAccess(canvasId, channelId) {
5654
+ const client2 = getSlackClient();
5655
+ try {
5656
+ await withSlackRetries(
5657
+ () => client2.canvases.access.set({
5658
+ canvas_id: canvasId,
5659
+ access_level: "write",
5660
+ channel_ids: [channelId]
5661
+ }),
5662
+ 3,
5663
+ {
5664
+ action: "canvases.access.set",
5665
+ attributes: {
5666
+ "app.slack.canvas.canvas_id_prefix": canvasId.slice(0, 1),
5667
+ "app.slack.canvas.channel_id_prefix": channelId.slice(0, 1),
5668
+ "app.slack.canvas.access_level": "write"
5669
+ }
5670
+ }
5671
+ );
5672
+ } catch (error) {
5673
+ logWarn(
5674
+ "slack_canvas_access_set_failed",
5675
+ {},
5676
+ {
5677
+ "app.slack.action": "canvases.access.set",
5678
+ "app.slack.canvas.canvas_id_prefix": canvasId.slice(0, 1),
5679
+ "app.slack.canvas.channel_id_prefix": channelId.slice(0, 1),
5680
+ "app.slack.canvas.access_level": "write"
5681
+ },
5682
+ error instanceof Error ? error.message : "Failed to grant channel access to canvas"
5683
+ );
5684
+ }
5685
+ }
5626
5686
  async function lookupCanvasSection(canvasId, containsText) {
5627
5687
  const client2 = getSlackClient();
5628
5688
  const response = await withSlackRetries(
@@ -5673,8 +5733,64 @@ async function updateCanvas(input) {
5673
5733
  }
5674
5734
  );
5675
5735
  }
5736
+ var CANVAS_ID_PATTERN = /^F[A-Z0-9]+$/i;
5737
+ var CANVAS_URL_FILE_ID_PATTERN = /\/(?:docs|canvas|files)\/(?:T[A-Z0-9]+\/)?(?:U[A-Z0-9]+\/)?(F[A-Z0-9]+)/i;
5738
+ function extractCanvasId(input) {
5739
+ const trimmed = input.trim();
5740
+ if (!trimmed) return void 0;
5741
+ if (CANVAS_ID_PATTERN.test(trimmed)) {
5742
+ return trimmed.toUpperCase();
5743
+ }
5744
+ const urlMatch = trimmed.match(CANVAS_URL_FILE_ID_PATTERN);
5745
+ if (urlMatch?.[1]) {
5746
+ return urlMatch[1].toUpperCase();
5747
+ }
5748
+ return void 0;
5749
+ }
5750
+ async function readCanvas(canvasIdOrUrl) {
5751
+ const canvasId = extractCanvasId(canvasIdOrUrl);
5752
+ if (!canvasId) {
5753
+ throw new Error(
5754
+ "Could not parse a Slack canvas/file ID from the provided input."
5755
+ );
5756
+ }
5757
+ const client2 = getSlackClient();
5758
+ const info = await withSlackRetries(
5759
+ () => client2.files.info({
5760
+ file: canvasId
5761
+ }),
5762
+ 3,
5763
+ {
5764
+ action: "files.info",
5765
+ attributes: {
5766
+ "app.slack.canvas.canvas_id_prefix": canvasId.slice(0, 1)
5767
+ }
5768
+ }
5769
+ );
5770
+ const file = info.file;
5771
+ if (!file) {
5772
+ throw new Error("Slack returned no file metadata for canvas.");
5773
+ }
5774
+ const downloadUrl = file.url_private_download ?? file.url_private;
5775
+ if (!downloadUrl) {
5776
+ throw new Error(
5777
+ "Canvas has no downloadable URL; bot token may lack file access."
5778
+ );
5779
+ }
5780
+ const buffer = await downloadPrivateSlackFile(downloadUrl);
5781
+ return {
5782
+ canvasId,
5783
+ title: file.title ?? file.name,
5784
+ permalink: file.permalink,
5785
+ mimetype: file.mimetype,
5786
+ filetype: file.filetype,
5787
+ content: buffer.toString("utf-8"),
5788
+ byteLength: buffer.byteLength
5789
+ };
5790
+ }
5676
5791
 
5677
5792
  // src/chat/tools/slack/canvas-tools.ts
5793
+ var MAX_CANVAS_READ_CHARS = 4e4;
5678
5794
  var MAX_RECENT_CANVASES = 5;
5679
5795
  function mergeRecentCanvases(existing, created) {
5680
5796
  const nextEntry = {
@@ -5690,13 +5806,13 @@ function mergeRecentCanvases(existing, created) {
5690
5806
  function createSlackCanvasCreateTool(context, state) {
5691
5807
  return tool({
5692
5808
  description: "Create a Slack canvas for long-form output in the active assistant context channel. Use when content is too long for a thread reply or needs a persistent document. Do not use for short answers that fit in-thread.",
5693
- inputSchema: Type10.Object({
5694
- title: Type10.String({
5809
+ inputSchema: Type11.Object({
5810
+ title: Type11.String({
5695
5811
  minLength: 1,
5696
5812
  maxLength: 160,
5697
5813
  description: "Canvas title."
5698
5814
  }),
5699
- markdown: Type10.String({
5815
+ markdown: Type11.String({
5700
5816
  minLength: 1,
5701
5817
  description: "Canvas markdown body content."
5702
5818
  })
@@ -5762,29 +5878,29 @@ function createSlackCanvasCreateTool(context, state) {
5762
5878
  function createSlackCanvasUpdateTool(state, _context) {
5763
5879
  return tool({
5764
5880
  description: "Update the active Slack canvas tracked in artifact context. Use when continuing or correcting a document already tracked in this thread. Do not use to create a brand-new long-form artifact.",
5765
- inputSchema: Type10.Object({
5766
- markdown: Type10.String({
5881
+ inputSchema: Type11.Object({
5882
+ markdown: Type11.String({
5767
5883
  minLength: 1,
5768
5884
  description: "Markdown content to insert or use as replacement text."
5769
5885
  }),
5770
- operation: Type10.Optional(
5771
- Type10.Union(
5886
+ operation: Type11.Optional(
5887
+ Type11.Union(
5772
5888
  [
5773
- Type10.Literal("insert_at_end"),
5774
- Type10.Literal("insert_at_start"),
5775
- Type10.Literal("replace")
5889
+ Type11.Literal("insert_at_end"),
5890
+ Type11.Literal("insert_at_start"),
5891
+ Type11.Literal("replace")
5776
5892
  ],
5777
5893
  { description: "Canvas update mode." }
5778
5894
  )
5779
5895
  ),
5780
- section_id: Type10.Optional(
5781
- Type10.String({
5896
+ section_id: Type11.Optional(
5897
+ Type11.String({
5782
5898
  minLength: 1,
5783
5899
  description: "Optional section ID required for targeted replace operations."
5784
5900
  })
5785
5901
  ),
5786
- section_contains_text: Type10.Optional(
5787
- Type10.String({
5902
+ section_contains_text: Type11.Optional(
5903
+ Type11.String({
5788
5904
  minLength: 1,
5789
5905
  description: "Optional helper text used to find the target section when section_id is not provided."
5790
5906
  })
@@ -5847,9 +5963,61 @@ function createSlackCanvasUpdateTool(state, _context) {
5847
5963
  }
5848
5964
  });
5849
5965
  }
5966
+ function createSlackCanvasReadTool() {
5967
+ return tool({
5968
+ description: "Read a Slack canvas the bot has access to (including canvases the bot created) by canvas ID or Slack canvas/docs URL. Use when the user shares a Slack canvas link (https://*.slack.com/docs/... or /canvas/...) or references a canvas ID and you need its contents. Do not use for generic web pages \u2014 use webFetch for those.",
5969
+ inputSchema: Type11.Object({
5970
+ canvas: Type11.String({
5971
+ minLength: 1,
5972
+ description: "Canvas/file ID (e.g. `F0ABCDEF`) or Slack canvas/docs URL (e.g. `https://team.slack.com/docs/T.../F...`)."
5973
+ })
5974
+ }),
5975
+ execute: async ({ canvas }) => {
5976
+ const canvasId = extractCanvasId(canvas);
5977
+ if (!canvasId) {
5978
+ return {
5979
+ ok: false,
5980
+ error: "Could not parse a Slack canvas/file ID from input. Provide an F-prefixed ID or a Slack canvas/docs URL."
5981
+ };
5982
+ }
5983
+ try {
5984
+ const result = await readCanvas(canvas);
5985
+ const truncated = result.content.length > MAX_CANVAS_READ_CHARS;
5986
+ const content = truncated ? result.content.slice(0, MAX_CANVAS_READ_CHARS) : result.content;
5987
+ return {
5988
+ ok: true,
5989
+ canvas_id: result.canvasId,
5990
+ title: result.title,
5991
+ permalink: result.permalink,
5992
+ mimetype: result.mimetype,
5993
+ filetype: result.filetype,
5994
+ original_byte_length: result.byteLength,
5995
+ truncated,
5996
+ content
5997
+ };
5998
+ } catch (error) {
5999
+ const message = error instanceof Error ? error.message : "canvas read failed";
6000
+ logWarn(
6001
+ "slack_canvas_read_failed",
6002
+ {},
6003
+ {
6004
+ "gen_ai.tool.name": "slackCanvasRead",
6005
+ "app.slack.canvas.canvas_id_prefix": canvasId.slice(0, 1)
6006
+ },
6007
+ message
6008
+ );
6009
+ return {
6010
+ ok: false,
6011
+ canvas_id: canvasId,
6012
+ error: message
6013
+ };
6014
+ }
6015
+ }
6016
+ });
6017
+ }
5850
6018
 
5851
6019
  // src/chat/tools/slack/list-tools.ts
5852
- import { Type as Type11 } from "@sinclair/typebox";
6020
+ import { Type as Type12 } from "@sinclair/typebox";
5853
6021
 
5854
6022
  // src/chat/tools/slack/lists.ts
5855
6023
  function normalizeKey(value) {
@@ -6023,8 +6191,8 @@ async function updateListItem(input) {
6023
6191
  function createSlackListCreateTool(state) {
6024
6192
  return tool({
6025
6193
  description: "Create a Slack todo list for action tracking. Use when the user needs structured tasks with ownership/completion tracking. Do not use for one-off notes without task management needs.",
6026
- inputSchema: Type11.Object({
6027
- name: Type11.String({
6194
+ inputSchema: Type12.Object({
6195
+ name: Type12.String({
6028
6196
  minLength: 1,
6029
6197
  maxLength: 160,
6030
6198
  description: "Name for the new Slack list."
@@ -6059,20 +6227,20 @@ function createSlackListCreateTool(state) {
6059
6227
  function createSlackListAddItemsTool(state) {
6060
6228
  return tool({
6061
6229
  description: "Add tasks to the active Slack list tracked in artifact context. Use when the user wants actionable items recorded in the current thread list. Do not use when no list exists and list creation was not requested.",
6062
- inputSchema: Type11.Object({
6063
- items: Type11.Array(Type11.String({ minLength: 1 }), {
6230
+ inputSchema: Type12.Object({
6231
+ items: Type12.Array(Type12.String({ minLength: 1 }), {
6064
6232
  minItems: 1,
6065
6233
  maxItems: 25,
6066
6234
  description: "List item titles to create."
6067
6235
  }),
6068
- assignee_user_id: Type11.Optional(
6069
- Type11.String({
6236
+ assignee_user_id: Type12.Optional(
6237
+ Type12.String({
6070
6238
  minLength: 1,
6071
6239
  description: "Optional Slack user ID assigned to all created items."
6072
6240
  })
6073
6241
  ),
6074
- due_date: Type11.Optional(
6075
- Type11.String({
6242
+ due_date: Type12.Optional(
6243
+ Type12.String({
6076
6244
  pattern: "^\\d{4}-\\d{2}-\\d{2}$",
6077
6245
  description: "Optional due date in YYYY-MM-DD format."
6078
6246
  })
@@ -6121,9 +6289,9 @@ function createSlackListAddItemsTool(state) {
6121
6289
  function createSlackListGetItemsTool(state) {
6122
6290
  return tool({
6123
6291
  description: "Read items from the active Slack list tracked in artifact context. Use when the user asks for task status, open items, or list contents. Do not use when list state is already known from the immediately prior result.",
6124
- inputSchema: Type11.Object({
6125
- limit: Type11.Optional(
6126
- Type11.Integer({
6292
+ inputSchema: Type12.Object({
6293
+ limit: Type12.Optional(
6294
+ Type12.Integer({
6127
6295
  minimum: 1,
6128
6296
  maximum: 200,
6129
6297
  description: "Maximum number of list items to return."
@@ -6148,19 +6316,19 @@ function createSlackListGetItemsTool(state) {
6148
6316
  function createSlackListUpdateItemTool(state) {
6149
6317
  return tool({
6150
6318
  description: "Update an item in the active Slack list tracked in artifact context (title/completion). Use when the user asks to mark progress or rename a tracked task. Do not use to add new tasks.",
6151
- inputSchema: Type11.Object(
6319
+ inputSchema: Type12.Object(
6152
6320
  {
6153
- item_id: Type11.String({
6321
+ item_id: Type12.String({
6154
6322
  minLength: 1,
6155
6323
  description: "ID of the Slack list item to update."
6156
6324
  }),
6157
- completed: Type11.Optional(
6158
- Type11.Boolean({
6325
+ completed: Type12.Optional(
6326
+ Type12.Boolean({
6159
6327
  description: "Optional completion status update."
6160
6328
  })
6161
6329
  ),
6162
- title: Type11.Optional(
6163
- Type11.String({
6330
+ title: Type12.Optional(
6331
+ Type12.String({
6164
6332
  minLength: 1,
6165
6333
  description: "Optional new item title."
6166
6334
  })
@@ -6210,11 +6378,11 @@ function createSlackListUpdateItemTool(state) {
6210
6378
  }
6211
6379
 
6212
6380
  // src/chat/tools/system-time.ts
6213
- import { Type as Type12 } from "@sinclair/typebox";
6381
+ import { Type as Type13 } from "@sinclair/typebox";
6214
6382
  function createSystemTimeTool() {
6215
6383
  return tool({
6216
6384
  description: "Return current system time in UTC and local ISO formats. Use when the user asks for current time/date context. Do not use as a substitute for historical or timezone-conversion research.",
6217
- inputSchema: Type12.Object({}),
6385
+ inputSchema: Type13.Object({}),
6218
6386
  execute: async () => {
6219
6387
  const now = /* @__PURE__ */ new Date();
6220
6388
  return {
@@ -6229,7 +6397,7 @@ function createSystemTimeTool() {
6229
6397
  }
6230
6398
 
6231
6399
  // src/chat/tools/web/fetch-tool.ts
6232
- import { Type as Type13 } from "@sinclair/typebox";
6400
+ import { Type as Type14 } from "@sinclair/typebox";
6233
6401
 
6234
6402
  // src/chat/tools/web/constants.ts
6235
6403
  var USER_AGENT = "junior-bot/0.1";
@@ -6577,13 +6745,13 @@ function extractHttpStatusFromMessage(message) {
6577
6745
  function createWebFetchTool(hooks) {
6578
6746
  return tool({
6579
6747
  description: "Fetch and extract readable content from a specific URL. Use when you need details from a known page or document. Do not use for discovery when search is the first step.",
6580
- inputSchema: Type13.Object({
6581
- url: Type13.String({
6748
+ inputSchema: Type14.Object({
6749
+ url: Type14.String({
6582
6750
  minLength: 1,
6583
6751
  description: "HTTP(S) URL to fetch."
6584
6752
  }),
6585
- max_chars: Type13.Optional(
6586
- Type13.Integer({
6753
+ max_chars: Type14.Optional(
6754
+ Type14.Integer({
6587
6755
  minimum: 500,
6588
6756
  maximum: MAX_FETCH_CHARS,
6589
6757
  description: "Optional maximum number of extracted characters to return."
@@ -6643,7 +6811,7 @@ function createWebFetchTool(hooks) {
6643
6811
  // src/chat/tools/web/search.ts
6644
6812
  import { generateText } from "ai";
6645
6813
  import { createGatewayProvider } from "@ai-sdk/gateway";
6646
- import { Type as Type14 } from "@sinclair/typebox";
6814
+ import { Type as Type15 } from "@sinclair/typebox";
6647
6815
  var SEARCH_TIMEOUT_MS = 6e4;
6648
6816
  var MAX_RESULTS = 5;
6649
6817
  var DEFAULT_SEARCH_MODEL = "xai/grok-4-fast-reasoning";
@@ -6686,14 +6854,14 @@ function isAuthFailure(message) {
6686
6854
  function createWebSearchTool() {
6687
6855
  return tool({
6688
6856
  description: "Search public web sources and return top snippets/URLs. Use when you need discovery or source candidates. Do not use when the user already provided a specific URL to inspect.",
6689
- inputSchema: Type14.Object({
6690
- query: Type14.String({
6857
+ inputSchema: Type15.Object({
6858
+ query: Type15.String({
6691
6859
  minLength: 1,
6692
6860
  maxLength: 500,
6693
6861
  description: "Search query."
6694
6862
  }),
6695
- max_results: Type14.Optional(
6696
- Type14.Integer({
6863
+ max_results: Type15.Optional(
6864
+ Type15.Integer({
6697
6865
  minimum: 1,
6698
6866
  maximum: MAX_RESULTS,
6699
6867
  description: "Max results to return."
@@ -6759,17 +6927,17 @@ function createWebSearchTool() {
6759
6927
  }
6760
6928
 
6761
6929
  // src/chat/tools/sandbox/write-file.ts
6762
- import { Type as Type15 } from "@sinclair/typebox";
6930
+ import { Type as Type16 } from "@sinclair/typebox";
6763
6931
  function createWriteFileTool() {
6764
6932
  return tool({
6765
6933
  description: "Write UTF-8 content to a file in the sandbox workspace. Use for intentional file creation or replacement after validation. Do not use for exploratory analysis-only turns.",
6766
- inputSchema: Type15.Object(
6934
+ inputSchema: Type16.Object(
6767
6935
  {
6768
- path: Type15.String({
6936
+ path: Type16.String({
6769
6937
  minLength: 1,
6770
6938
  description: "Path to write in the sandbox workspace."
6771
6939
  }),
6772
- content: Type15.String({
6940
+ content: Type16.String({
6773
6941
  description: "UTF-8 file content to write."
6774
6942
  })
6775
6943
  },
@@ -6824,6 +6992,7 @@ function createTools(availableSkills, hooks = {}, context) {
6824
6992
  loadSkill: createLoadSkillTool(availableSkills, {
6825
6993
  onSkillLoaded: hooks.onSkillLoaded
6826
6994
  }),
6995
+ reportProgress: createReportProgressTool(),
6827
6996
  systemTime: createSystemTimeTool(),
6828
6997
  bash: createBashTool(),
6829
6998
  attachFile: createAttachFileTool(context.sandbox, hooks),
@@ -6835,6 +7004,7 @@ function createTools(availableSkills, hooks = {}, context) {
6835
7004
  hooks,
6836
7005
  hooks.toolOverrides?.imageGenerate
6837
7006
  ),
7007
+ slackCanvasRead: createSlackCanvasReadTool(),
6838
7008
  slackCanvasUpdate: createSlackCanvasUpdateTool(state, context),
6839
7009
  slackListCreate: createSlackListCreateTool(state),
6840
7010
  slackListAddItems: createSlackListAddItemsTool(state),
@@ -7102,6 +7272,10 @@ var STATUS_PATTERNS = {
7102
7272
  defaultContext: "results",
7103
7273
  variants: ["Reviewing", "Checking", "Inspecting", "Auditing"]
7104
7274
  },
7275
+ drafting: {
7276
+ defaultContext: "reply",
7277
+ variants: ["Drafting", "Writing", "Composing", "Shaping"]
7278
+ },
7105
7279
  loading: {
7106
7280
  defaultContext: "task",
7107
7281
  variants: ["Loading", "Priming", "Booting", "Spinning up"]
@@ -7135,24 +7309,46 @@ var STATUS_PATTERNS = {
7135
7309
  variants: ["Running", "Executing", "Launching", "Processing"]
7136
7310
  }
7137
7311
  };
7138
- function makeAssistantStatus(kind, context) {
7139
- return { kind, ...context ? { context } : {} };
7312
+ function makeAssistantStatus(kind, context, options) {
7313
+ return {
7314
+ kind,
7315
+ ...context ? { context } : {},
7316
+ ...options?.source ? { source: options.source } : {}
7317
+ };
7140
7318
  }
7141
7319
  function renderAssistantStatus(args) {
7142
7320
  const random = args.random ?? Math.random;
7143
7321
  const pattern = STATUS_PATTERNS[args.status.kind];
7322
+ const source = args.status.source ?? "fallback";
7144
7323
  const context = normalizeSlackStatusText(args.status.context ?? "") || pattern.defaultContext;
7145
7324
  const index = Math.floor(random() * pattern.variants.length);
7146
7325
  const verb = pattern.variants[index] ?? pattern.variants[0];
7147
7326
  const visible = truncateStatusText(`${verb} ${context}`);
7148
- const hint = truncateStatusText(`${pattern.variants[0]} ${context}`);
7149
7327
  return {
7150
- key: `${args.status.kind}:${context}`,
7151
- hint,
7152
- visible,
7153
- suggestions: Array.from(/* @__PURE__ */ new Set([visible, hint]))
7328
+ key: `${source}:${args.status.kind}:${context}`,
7329
+ visible
7154
7330
  };
7155
7331
  }
7332
+ function selectAssistantLoadingMessages(args) {
7333
+ const random = args.random ?? Math.random;
7334
+ const normalized = Array.from(
7335
+ new Set(
7336
+ args.messages.map((message) => truncateStatusText(normalizeSlackStatusText(message))).filter((message) => message.length > 0)
7337
+ )
7338
+ );
7339
+ if (normalized.length === 0) {
7340
+ return void 0;
7341
+ }
7342
+ const shuffled = [...normalized];
7343
+ for (let index = shuffled.length - 1; index > 0; index -= 1) {
7344
+ const otherIndex = Math.floor(random() * (index + 1));
7345
+ [shuffled[index], shuffled[otherIndex]] = [
7346
+ shuffled[otherIndex],
7347
+ shuffled[index]
7348
+ ];
7349
+ }
7350
+ return shuffled.slice(0, 10);
7351
+ }
7156
7352
 
7157
7353
  // src/chat/slack/assistant-thread/status-scheduler.ts
7158
7354
  var STATUS_UPDATE_DEBOUNCE_MS = 1e3;
@@ -7163,10 +7359,16 @@ function createAssistantStatusScheduler(args) {
7163
7359
  const setTimer = args.setTimer ?? ((callback, delayMs) => setTimeout(callback, delayMs));
7164
7360
  const clearTimer = args.clearTimer ?? ((timer) => clearTimeout(timer));
7165
7361
  const random = args.random ?? Math.random;
7362
+ const loadingMessages = selectAssistantLoadingMessages({
7363
+ messages: args.loadingMessages ?? [],
7364
+ random
7365
+ });
7366
+ const defaultStatus = makeAssistantStatus("thinking");
7166
7367
  let active = false;
7167
7368
  let currentKey = "";
7168
- let currentStatus = makeAssistantStatus("thinking");
7369
+ let currentStatus = defaultStatus;
7169
7370
  let currentVisibleStatus = "";
7371
+ let currentLoadingMessages;
7170
7372
  let lastStatusAt = 0;
7171
7373
  let pendingStatus = null;
7172
7374
  let pendingKey = "";
@@ -7193,18 +7395,43 @@ function createAssistantStatusScheduler(args) {
7193
7395
  if (!active || !currentVisibleStatus) {
7194
7396
  return;
7195
7397
  }
7196
- void postRenderedStatus(currentStatus);
7398
+ void postStatus(currentVisibleStatus, currentLoadingMessages);
7197
7399
  }, STATUS_ROTATION_INTERVAL_MS);
7198
7400
  };
7199
- const postStatus = async (text, suggestions) => {
7401
+ const getLoadingMessagesForStatus = (_status, visible) => {
7402
+ if (!visible) {
7403
+ return void 0;
7404
+ }
7405
+ return [visible];
7406
+ };
7407
+ const getInitialStatusText = () => {
7408
+ if (loadingMessages?.length) {
7409
+ return loadingMessages[0];
7410
+ }
7411
+ return renderAssistantStatus({
7412
+ status: defaultStatus,
7413
+ random
7414
+ }).visible;
7415
+ };
7416
+ const haveSameLoadingMessages = (left, right) => {
7417
+ if (left === right) {
7418
+ return true;
7419
+ }
7420
+ if (!left || !right || left.length !== right.length) {
7421
+ return false;
7422
+ }
7423
+ return left.every((message, index) => message === right[index]);
7424
+ };
7425
+ const postStatus = async (text, nextLoadingMessages) => {
7200
7426
  if (!text && !currentVisibleStatus) {
7201
7427
  return;
7202
7428
  }
7203
7429
  currentVisibleStatus = text;
7430
+ currentLoadingMessages = nextLoadingMessages;
7204
7431
  lastStatusAt = now();
7205
7432
  scheduleRotation();
7206
7433
  await enqueueStatusUpdate(async () => {
7207
- await args.sendStatus(text, suggestions);
7434
+ await args.sendStatus(text, nextLoadingMessages);
7208
7435
  });
7209
7436
  };
7210
7437
  const postRenderedStatus = async (status) => {
@@ -7212,9 +7439,13 @@ function createAssistantStatusScheduler(args) {
7212
7439
  status,
7213
7440
  random
7214
7441
  });
7442
+ const nextLoadingMessages = getLoadingMessagesForStatus(
7443
+ status,
7444
+ presentation.visible
7445
+ );
7215
7446
  currentStatus = status;
7216
7447
  currentKey = presentation.key;
7217
- await postStatus(presentation.visible, presentation.suggestions);
7448
+ await postStatus(presentation.visible, nextLoadingMessages);
7218
7449
  };
7219
7450
  const clearPending = () => {
7220
7451
  if (pendingTimer) {
@@ -7243,9 +7474,9 @@ function createAssistantStatusScheduler(args) {
7243
7474
  start() {
7244
7475
  active = true;
7245
7476
  clearPending();
7246
- currentStatus = makeAssistantStatus("thinking");
7247
- currentKey = "";
7248
- void postRenderedStatus(currentStatus);
7477
+ currentStatus = defaultStatus;
7478
+ currentKey = "initial";
7479
+ void postStatus(getInitialStatusText(), loadingMessages);
7249
7480
  },
7250
7481
  async stop() {
7251
7482
  active = false;
@@ -7268,9 +7499,25 @@ function createAssistantStatusScheduler(args) {
7268
7499
  if (!presentation.visible) {
7269
7500
  return;
7270
7501
  }
7502
+ if (status.source !== "major" && (currentStatus.source === "major" || pendingStatus?.source === "major")) {
7503
+ return;
7504
+ }
7271
7505
  if (presentation.key === currentKey || presentation.key === pendingKey) {
7272
7506
  return;
7273
7507
  }
7508
+ if (presentation.visible === currentVisibleStatus) {
7509
+ clearPending();
7510
+ currentStatus = status;
7511
+ currentKey = presentation.key;
7512
+ const nextLoadingMessages = getLoadingMessagesForStatus(
7513
+ status,
7514
+ presentation.visible
7515
+ );
7516
+ if (!haveSameLoadingMessages(currentLoadingMessages, nextLoadingMessages)) {
7517
+ void postStatus(presentation.visible, nextLoadingMessages);
7518
+ }
7519
+ return;
7520
+ }
7274
7521
  const elapsed = now() - lastStatusAt;
7275
7522
  const waitMs = Math.max(
7276
7523
  STATUS_UPDATE_DEBOUNCE_MS - elapsed,
@@ -7299,10 +7546,11 @@ function createAssistantStatusScheduler(args) {
7299
7546
  }
7300
7547
 
7301
7548
  // src/chat/slack/assistant-thread/status-send.ts
7549
+ var SLACK_ASSISTANT_ACTIVE_STATUS = "is working on your request...";
7302
7550
  function createSlackAdapterStatusSender(args) {
7303
7551
  const adapter = args.getSlackAdapter();
7304
7552
  const boundToken = getSlackAdapterRequestToken(adapter);
7305
- return async (text, suggestions) => {
7553
+ return async (text, loadingMessages) => {
7306
7554
  const channelId = args.channelId;
7307
7555
  const threadTs = args.threadTs;
7308
7556
  if (!channelId || !threadTs) {
@@ -7312,6 +7560,7 @@ function createSlackAdapterStatusSender(args) {
7312
7560
  if (!normalizedChannelId) {
7313
7561
  return;
7314
7562
  }
7563
+ const nextLoadingMessages = text ? loadingMessages ?? [text] : void 0;
7315
7564
  try {
7316
7565
  await runWithBoundSlackToken(
7317
7566
  adapter,
@@ -7319,8 +7568,8 @@ function createSlackAdapterStatusSender(args) {
7319
7568
  () => adapter.setAssistantStatus(
7320
7569
  normalizedChannelId,
7321
7570
  threadTs,
7322
- text,
7323
- suggestions
7571
+ text ? SLACK_ASSISTANT_ACTIVE_STATUS : "",
7572
+ nextLoadingMessages
7324
7573
  )
7325
7574
  );
7326
7575
  } catch (error) {
@@ -7336,7 +7585,7 @@ function createSlackAdapterStatusSender(args) {
7336
7585
  }
7337
7586
  function createSlackWebApiStatusSender(args) {
7338
7587
  const getClient2 = args.getSlackClient ?? getSlackClient;
7339
- return async (text, suggestions) => {
7588
+ return async (text, loadingMessages) => {
7340
7589
  const channelId = args.channelId;
7341
7590
  const threadTs = args.threadTs;
7342
7591
  if (!channelId || !threadTs) {
@@ -7346,12 +7595,13 @@ function createSlackWebApiStatusSender(args) {
7346
7595
  if (!normalizedChannelId) {
7347
7596
  return;
7348
7597
  }
7598
+ const nextLoadingMessages = text ? loadingMessages ?? [text] : void 0;
7349
7599
  try {
7350
7600
  await getClient2().assistant.threads.setStatus({
7351
7601
  channel_id: normalizedChannelId,
7352
7602
  thread_ts: threadTs,
7353
- status: text,
7354
- ...suggestions ? { loading_messages: suggestions } : {}
7603
+ status: text ? SLACK_ASSISTANT_ACTIVE_STATUS : "",
7604
+ ...nextLoadingMessages ? { loading_messages: nextLoadingMessages } : {}
7355
7605
  });
7356
7606
  } catch (error) {
7357
7607
  logAssistantStatusFailure({
@@ -7401,6 +7651,7 @@ function createSlackAdapterAssistantStatusSession(args) {
7401
7651
  threadTs: args.threadTs,
7402
7652
  getSlackAdapter: args.getSlackAdapter
7403
7653
  }),
7654
+ loadingMessages: args.loadingMessages ?? botConfig.loadingMessages,
7404
7655
  now: args.now,
7405
7656
  setTimer: args.setTimer,
7406
7657
  clearTimer: args.clearTimer,
@@ -7414,6 +7665,7 @@ function createSlackWebApiAssistantStatusSession(args) {
7414
7665
  threadTs: args.threadTs,
7415
7666
  getSlackClient: args.getSlackClient
7416
7667
  }),
7668
+ loadingMessages: args.loadingMessages ?? botConfig.loadingMessages,
7417
7669
  now: args.now,
7418
7670
  setTimer: args.setTimer,
7419
7671
  clearTimer: args.clearTimer,
@@ -8938,6 +9190,34 @@ function createPluginAuthOrchestration(deps, abortAgent) {
8938
9190
  };
8939
9191
  }
8940
9192
 
9193
+ // src/chat/runtime/report-progress.ts
9194
+ function buildReportedProgressStatus(input) {
9195
+ if (!input || typeof input !== "object") {
9196
+ return void 0;
9197
+ }
9198
+ const phase = input.phase;
9199
+ if (typeof phase !== "string") {
9200
+ return void 0;
9201
+ }
9202
+ const detail = compactStatusText(input.detail, 40);
9203
+ switch (phase) {
9204
+ case "thinking":
9205
+ return makeAssistantStatus("thinking", detail, { source: "major" });
9206
+ case "researching":
9207
+ return makeAssistantStatus("searching", detail, { source: "major" });
9208
+ case "reading":
9209
+ return makeAssistantStatus("reading", detail, { source: "major" });
9210
+ case "executing":
9211
+ return makeAssistantStatus("running", detail, { source: "major" });
9212
+ case "reviewing":
9213
+ return makeAssistantStatus("reviewing", detail, { source: "major" });
9214
+ case "drafting":
9215
+ return makeAssistantStatus("drafting", detail, { source: "major" });
9216
+ default:
9217
+ return void 0;
9218
+ }
9219
+ }
9220
+
8941
9221
  // src/chat/runtime/tool-status.ts
8942
9222
  function buildToolStatus(toolName, input) {
8943
9223
  const obj = input && typeof input === "object" ? input : void 0;
@@ -8948,6 +9228,10 @@ function buildToolStatus(toolName, input) {
8948
9228
  const domain = obj ? extractStatusUrlDomain(obj.url) : void 0;
8949
9229
  const skillName = obj ? compactStatusText(obj.skill_name ?? obj.skillName, 40) : void 0;
8950
9230
  const provider = obj ? compactStatusText(obj.provider, 20) : void 0;
9231
+ const reportedProgress = buildReportedProgressStatus(obj);
9232
+ if (toolName === "reportProgress" && reportedProgress) {
9233
+ return reportedProgress;
9234
+ }
8951
9235
  if (command && toolName === "bash") {
8952
9236
  return makeAssistantStatus("running", command);
8953
9237
  }
@@ -8988,6 +9272,7 @@ function buildToolStatus(toolName, input) {
8988
9272
  slackChannelListMessages: makeAssistantStatus("listing", "messages"),
8989
9273
  slackCanvasCreate: makeAssistantStatus("creating", "brief"),
8990
9274
  slackCanvasUpdate: makeAssistantStatus("updating", "brief"),
9275
+ slackCanvasRead: makeAssistantStatus("reading", "brief"),
8991
9276
  slackListCreate: makeAssistantStatus("creating", "tracking list"),
8992
9277
  slackListAddItems: makeAssistantStatus("updating", "tracking list"),
8993
9278
  slackListUpdateItem: makeAssistantStatus("updating", "tracking list"),
@@ -9322,41 +9607,41 @@ function summarizeMessageText(text) {
9322
9607
  }
9323
9608
  function buildUserTurnText(userInput, conversationContext, metadata) {
9324
9609
  const trimmedContext = conversationContext?.trim();
9325
- const hasSessionContext = Boolean(metadata?.sessionContext?.conversationId);
9326
- const hasTurnContext = Boolean(metadata?.turnContext?.traceId);
9327
- if (!trimmedContext && !hasSessionContext && !hasTurnContext) {
9610
+ const conversationId = metadata?.sessionContext?.conversationId;
9611
+ const traceId = metadata?.turnContext?.traceId;
9612
+ if (!trimmedContext && !conversationId && !traceId) {
9328
9613
  return userInput;
9329
9614
  }
9330
- const sections = [
9331
- "<current-message>",
9332
- userInput,
9333
- "</current-message>"
9334
- ];
9615
+ const sections = [];
9335
9616
  if (trimmedContext) {
9336
9617
  sections.push(
9337
- "",
9338
- "<thread-conversation-context>",
9339
- "Use this context for continuity across prior thread turns.",
9618
+ "<thread-background>",
9340
9619
  trimmedContext,
9341
- "</thread-conversation-context>"
9620
+ "</thread-background>",
9621
+ ""
9342
9622
  );
9343
9623
  }
9344
- if (metadata?.sessionContext?.conversationId) {
9624
+ if (conversationId) {
9345
9625
  sections.push(
9346
- "",
9347
9626
  "<session-context>",
9348
- `- gen_ai.conversation.id: ${metadata.sessionContext.conversationId}`,
9349
- "</session-context>"
9627
+ `- gen_ai.conversation.id: ${conversationId}`,
9628
+ "</session-context>",
9629
+ ""
9350
9630
  );
9351
9631
  }
9352
- if (metadata?.turnContext?.traceId) {
9632
+ if (traceId) {
9353
9633
  sections.push(
9354
- "",
9355
9634
  "<turn-context>",
9356
- `- trace_id: ${metadata.turnContext.traceId}`,
9357
- "</turn-context>"
9635
+ `- trace_id: ${traceId}`,
9636
+ "</turn-context>",
9637
+ ""
9358
9638
  );
9359
9639
  }
9640
+ sections.push(
9641
+ '<current-instruction priority="highest">',
9642
+ userInput,
9643
+ "</current-instruction>"
9644
+ );
9360
9645
  return sections.join("\n");
9361
9646
  }
9362
9647
  function encodeNonImageAttachmentForPrompt(attachment) {
@@ -22,6 +22,20 @@ var MIN_AGENT_TURN_TIMEOUT_MS = 10 * 1e3;
22
22
  var DEFAULT_AGENT_TURN_TIMEOUT_MS = 12 * 60 * 1e3;
23
23
  var DEFAULT_FUNCTION_MAX_DURATION_SECONDS = 800;
24
24
  var FUNCTION_TIMEOUT_BUFFER_SECONDS = 20;
25
+ var DEFAULT_ASSISTANT_LOADING_MESSAGES = [
26
+ "Consulting the orb",
27
+ "Bribing the gremlins",
28
+ "Shuffling the papers dramatically",
29
+ "Summoning the right stack trace",
30
+ "Negotiating with the mutex",
31
+ "Poking the internet with a stick",
32
+ "Asking the docs nicely",
33
+ "Searching for the least cursed path",
34
+ "Pretending this was obvious",
35
+ "Waking up the test suite",
36
+ "Untangling the spaghetti carefully",
37
+ "Rattling the command line"
38
+ ];
25
39
  function parseAgentTurnTimeoutMs(rawValue, maxTimeoutMs) {
26
40
  const value = Number.parseInt(rawValue ?? "", 10);
27
41
  if (Number.isNaN(value)) {
@@ -44,6 +58,27 @@ function resolveMaxTurnTimeoutMs(functionMaxDurationSeconds) {
44
58
  const budgetSeconds = functionMaxDurationSeconds - FUNCTION_TIMEOUT_BUFFER_SECONDS;
45
59
  return Math.max(MIN_AGENT_TURN_TIMEOUT_MS, budgetSeconds * 1e3);
46
60
  }
61
+ function parseLoadingMessages(rawValue) {
62
+ const trimmed = rawValue?.trim();
63
+ if (!trimmed) {
64
+ return [...DEFAULT_ASSISTANT_LOADING_MESSAGES];
65
+ }
66
+ let parsed;
67
+ try {
68
+ parsed = JSON.parse(trimmed);
69
+ } catch {
70
+ throw new Error("JUNIOR_LOADING_MESSAGES must be a JSON array of strings");
71
+ }
72
+ if (!Array.isArray(parsed)) {
73
+ throw new Error("JUNIOR_LOADING_MESSAGES must be a JSON array of strings");
74
+ }
75
+ return parsed.map((value, index) => {
76
+ if (typeof value !== "string") {
77
+ throw new Error(`JUNIOR_LOADING_MESSAGES[${index}] must be a string`);
78
+ }
79
+ return value.trim();
80
+ });
81
+ }
47
82
  function readBotConfig(env) {
48
83
  const functionMaxDurationSeconds = resolveFunctionMaxDurationSeconds(env);
49
84
  const maxTurnTimeoutMs = resolveMaxTurnTimeoutMs(functionMaxDurationSeconds);
@@ -51,6 +86,7 @@ function readBotConfig(env) {
51
86
  userName: env.JUNIOR_BOT_NAME ?? "junior",
52
87
  modelId: env.AI_MODEL ?? "anthropic/claude-sonnet-4.6",
53
88
  fastModelId: env.AI_FAST_MODEL ?? env.AI_MODEL ?? "anthropic/claude-haiku-4.5",
89
+ loadingMessages: parseLoadingMessages(env.JUNIOR_LOADING_MESSAGES),
54
90
  visionModelId: toOptionalTrimmed(env.AI_VISION_MODEL),
55
91
  turnTimeoutMs: parseAgentTurnTimeoutMs(
56
92
  env.AGENT_TURN_TIMEOUT_MS,
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  disconnectStateAdapter,
3
3
  resolveRuntimeDependencySnapshot
4
- } from "../chunk-A75TWGF2.js";
4
+ } from "../chunk-4PVJHUEV.js";
5
5
  import {
6
6
  getPluginProviders,
7
7
  getPluginRuntimeDependencies,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sentry/junior",
3
- "version": "0.26.0",
3
+ "version": "0.27.0",
4
4
  "private": false,
5
5
  "publishConfig": {
6
6
  "access": "public"