claude-threads 0.50.0 → 0.51.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/CHANGELOG.md CHANGED
@@ -7,6 +7,16 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.51.0] - 2026-01-09
11
+
12
+ ### Added
13
+ - **Image upload for bug reports** - Bug reports can now include screenshots uploaded to Catbox.moe. Use `!bug <description>` with an attached image or paste a screenshot (#153)
14
+
15
+ ### Fixed
16
+ - **Duplicate task lists** - Fixed issue where multiple task lists would appear in threads due to race conditions (#152, #151)
17
+ - **Code block rendering** - Fixed issues with code blocks not rendering correctly, including improved handling of language tags and empty blocks (#154)
18
+ - **Website logo rendering** - Improved SVG logo rendering on the project website (#155)
19
+
10
20
  ## [0.50.0] - 2026-01-09
11
21
 
12
22
  ### Added
package/README.md CHANGED
@@ -6,6 +6,10 @@
6
6
  ✴ ▀█▄ █ ✴
7
7
  ```
8
8
 
9
+ <p align="center">
10
+ <a href="https://claude-threads.run"><strong>claude-threads.run</strong></a>
11
+ </p>
12
+
9
13
  [![npm version](https://img.shields.io/npm/v/claude-threads.svg)](https://www.npmjs.com/package/claude-threads)
10
14
  [![npm downloads](https://img.shields.io/npm/dm/claude-threads.svg)](https://www.npmjs.com/package/claude-threads)
11
15
  [![CI](https://github.com/anneschuth/claude-threads/actions/workflows/ci.yml/badge.svg)](https://github.com/anneschuth/claude-threads/actions/workflows/ci.yml)
package/dist/index.js CHANGED
@@ -42763,9 +42763,11 @@ ${code}
42763
42763
  `);
42764
42764
  }
42765
42765
  formatMarkdown(content) {
42766
- return content.replace(/\n{3,}/g, `
42766
+ let processed = content.replace(/(?<=\n)```(?=\S)(?![a-zA-Z]*\n)/g, "```\n");
42767
+ processed = processed.replace(/\n{3,}/g, `
42767
42768
 
42768
42769
  `);
42770
+ return processed;
42769
42771
  }
42770
42772
  }
42771
42773
 
@@ -43407,6 +43409,7 @@ function convertMarkdownToSlack(content) {
43407
43409
  for (let i = 0;i < codeBlocks.length; i++) {
43408
43410
  preserved = preserved.replace(`${CODE_BLOCK_PLACEHOLDER}${i}\x00`, codeBlocks[i]);
43409
43411
  }
43412
+ preserved = preserved.replace(/(?<=\n)```(?=\S)(?![a-zA-Z]*\n)/g, "```\n");
43410
43413
  return preserved;
43411
43414
  }
43412
43415
  function convertMarkdownTablesToSlack(content) {
@@ -46403,14 +46406,15 @@ function formatToolUse(toolName, input, formatter, options = {}) {
46403
46406
  for (const line of lines) {
46404
46407
  if (lineCount >= maxLines)
46405
46408
  break;
46409
+ const escapedLine = line.replace(/```/g, "` ``");
46406
46410
  if (change.added) {
46407
- diffLines2.push(`+ ${line}`);
46411
+ diffLines2.push(`+ ${escapedLine}`);
46408
46412
  lineCount++;
46409
46413
  } else if (change.removed) {
46410
- diffLines2.push(`- ${line}`);
46414
+ diffLines2.push(`- ${escapedLine}`);
46411
46415
  lineCount++;
46412
46416
  } else {
46413
- diffLines2.push(` ${line}`);
46417
+ diffLines2.push(` ${escapedLine}`);
46414
46418
  lineCount++;
46415
46419
  }
46416
46420
  }
@@ -46441,7 +46445,7 @@ ${formatter.formatCodeBlock(diffLines2.join(`
46441
46445
  const lineCount = lines.length;
46442
46446
  if (detailed && content && lineCount > 0) {
46443
46447
  const maxLines = 6;
46444
- const previewLines = lines.slice(0, maxLines);
46448
+ const previewLines = lines.slice(0, maxLines).map((line) => line.replace(/```/g, "` ``"));
46445
46449
  let preview = `\uD83D\uDCDD ${formatter.formatBold("Write")} ${formatter.formatCode(filePath)} ${formatter.formatItalic(`(${lineCount} lines)`)}
46446
46450
  `;
46447
46451
  if (lineCount > maxLines) {
@@ -50562,6 +50566,54 @@ function validateClaudeCli() {
50562
50566
  }
50563
50567
 
50564
50568
  // src/session/bug-report.ts
50569
+ var CATBOX_API_URL = "https://catbox.moe/user/api.php";
50570
+ async function uploadImageToCatbox(imageBuffer, filename) {
50571
+ const arrayBuffer = imageBuffer.buffer.slice(imageBuffer.byteOffset, imageBuffer.byteOffset + imageBuffer.byteLength);
50572
+ const formData = new FormData;
50573
+ formData.append("reqtype", "fileupload");
50574
+ formData.append("fileToUpload", new Blob([arrayBuffer]), filename);
50575
+ const response = await fetch(CATBOX_API_URL, {
50576
+ method: "POST",
50577
+ body: formData
50578
+ });
50579
+ if (!response.ok) {
50580
+ throw new Error(`Catbox upload failed: ${response.status} ${response.statusText}`);
50581
+ }
50582
+ const responseText = await response.text();
50583
+ if (responseText.startsWith("https://")) {
50584
+ return responseText.trim();
50585
+ }
50586
+ throw new Error(`Catbox upload failed: ${responseText}`);
50587
+ }
50588
+ async function uploadImages(files, downloadFile) {
50589
+ const results = [];
50590
+ for (const file of files) {
50591
+ if (!file.mimeType.startsWith("image/")) {
50592
+ results.push({
50593
+ success: false,
50594
+ error: "Not an image file",
50595
+ originalFile: file
50596
+ });
50597
+ continue;
50598
+ }
50599
+ try {
50600
+ const buffer = await downloadFile(file.id);
50601
+ const url = await uploadImageToCatbox(buffer, file.name);
50602
+ results.push({
50603
+ success: true,
50604
+ url,
50605
+ originalFile: file
50606
+ });
50607
+ } catch (err) {
50608
+ results.push({
50609
+ success: false,
50610
+ error: err instanceof Error ? err.message : String(err),
50611
+ originalFile: file
50612
+ });
50613
+ }
50614
+ }
50615
+ return results;
50616
+ }
50565
50617
  var MAX_RECENT_EVENTS = 10;
50566
50618
  var GITHUB_REPO = "anneschuth/claude-threads";
50567
50619
  function trackEvent(session, type, summary) {
@@ -50647,11 +50699,19 @@ function formatRecentEvents(events) {
50647
50699
  }).join(`
50648
50700
  `);
50649
50701
  }
50650
- function formatIssueBody(context, userDescription) {
50702
+ function formatIssueBody(context, userDescription, imageUrls = []) {
50651
50703
  const sections = [];
50652
50704
  sections.push(`## Description
50653
50705
 
50654
50706
  ${sanitizeText(userDescription)}`);
50707
+ if (imageUrls.length > 0) {
50708
+ const imageSection = imageUrls.map((url, i) => `![Screenshot ${i + 1}](${url})`).join(`
50709
+
50710
+ `);
50711
+ sections.push(`## Screenshots
50712
+
50713
+ ${imageSection}`);
50714
+ }
50655
50715
  sections.push(`## Environment
50656
50716
 
50657
50717
  | Property | Value |
@@ -50729,7 +50789,7 @@ function checkGitHubCli() {
50729
50789
  }
50730
50790
  return { installed: true, authenticated: true };
50731
50791
  }
50732
- async function createGitHubIssue(title, body, _attachments, workingDir) {
50792
+ async function createGitHubIssue(title, body, workingDir) {
50733
50793
  const ghStatus = checkGitHubCli();
50734
50794
  if (!ghStatus.installed || !ghStatus.authenticated) {
50735
50795
  throw new Error(ghStatus.error);
@@ -50744,15 +50804,14 @@ async function createGitHubIssue(title, body, _attachments, workingDir) {
50744
50804
  timeout: 30000,
50745
50805
  stdio: ["pipe", "pipe", "pipe"]
50746
50806
  });
50747
- const issueUrl = result.trim();
50748
- return issueUrl;
50807
+ return result.trim();
50749
50808
  } finally {
50750
50809
  try {
50751
50810
  unlinkSync2(bodyFile);
50752
50811
  } catch {}
50753
50812
  }
50754
50813
  }
50755
- function formatBugPreview(title, description, context, attachments, formatter) {
50814
+ function formatBugPreview(title, description, context, imageUrls, imageErrors, formatter) {
50756
50815
  const lines = [];
50757
50816
  lines.push(`${formatter.formatBold("Bug Report Preview")}`);
50758
50817
  lines.push("");
@@ -50779,10 +50838,15 @@ function formatBugPreview(title, description, context, attachments, formatter) {
50779
50838
  lines.push(formatter.formatListItem(`${event.type}: ${event.summary.substring(0, 40)}...`));
50780
50839
  }
50781
50840
  }
50782
- if (attachments.length > 0) {
50841
+ if (imageUrls.length > 0 || imageErrors.length > 0) {
50783
50842
  lines.push("");
50784
- lines.push(formatter.formatBold("Attachments:"));
50785
- lines.push(formatter.formatListItem(`${attachments.length} file(s) - will be noted in issue (manual upload required)`));
50843
+ lines.push(formatter.formatBold("Screenshots:"));
50844
+ if (imageUrls.length > 0) {
50845
+ lines.push(formatter.formatListItem(`\u2705 ${imageUrls.length} image(s) uploaded successfully`));
50846
+ }
50847
+ if (imageErrors.length > 0) {
50848
+ lines.push(formatter.formatListItem(`\u26A0\uFE0F ${imageErrors.length} image(s) failed: ${imageErrors[0]}`));
50849
+ }
50786
50850
  }
50787
50851
  lines.push("");
50788
50852
  lines.push(`React ${formatter.formatCode("\uD83D\uDC4D")} to create GitHub issue or ${formatter.formatCode("\uD83D\uDC4E")} to cancel`);
@@ -51465,12 +51529,13 @@ async function deferUpdate(session, username, updateManager) {
51465
51529
  await postSuccess(session, `\u23F8\uFE0F ${formatter.formatBold("Update deferred")} for 1 hour
51466
51530
  ` + formatter.formatItalic("Use !update now to apply earlier"));
51467
51531
  }
51468
- async function reportBug(session, description, username, ctx, errorContext, _attachmentFileIds) {
51532
+ async function reportBug(session, description, username, ctx, errorContext, attachedFiles) {
51469
51533
  const formatter = session.platform.getFormatter();
51470
51534
  if (!description && !errorContext) {
51471
51535
  await postInfo(session, `Usage: ${formatter.formatCode("!bug <description>")}
51472
51536
  ` + `Example: ${formatter.formatCode("!bug Session crashed when uploading large image")}
51473
51537
 
51538
+ ` + `You can also attach screenshots to the !bug message.
51474
51539
  ` + `Or react with \uD83D\uDC1B on any error message to report it.`);
51475
51540
  return;
51476
51541
  }
@@ -51481,9 +51546,18 @@ async function reportBug(session, description, username, ctx, errorContext, _att
51481
51546
  }
51482
51547
  const bugDescription = description || (errorContext ? `Error: ${errorContext.message.substring(0, 200)}` : "Unknown error");
51483
51548
  const context = await collectBugReportContext(session, errorContext);
51549
+ let imageUrls = [];
51550
+ let imageErrors = [];
51551
+ const downloadFile = session.platform.downloadFile;
51552
+ if (attachedFiles && attachedFiles.length > 0 && downloadFile) {
51553
+ await postInfo(session, `\uD83D\uDCE4 Uploading ${attachedFiles.length} image(s)...`);
51554
+ const uploadResults = await uploadImages(attachedFiles, (fileId) => downloadFile(fileId));
51555
+ imageUrls = uploadResults.filter((r) => r.success && typeof r.url === "string").map((r) => r.url);
51556
+ imageErrors = uploadResults.filter((r) => !r.success).map((r) => `${r.originalFile.name}: ${r.error}`);
51557
+ }
51484
51558
  const title = generateIssueTitle(bugDescription);
51485
- const body = formatIssueBody(context, bugDescription);
51486
- const preview = formatBugPreview(title, bugDescription, context, [], formatter);
51559
+ const body = formatIssueBody(context, bugDescription, imageUrls);
51560
+ const preview = formatBugPreview(title, bugDescription, context, imageUrls, imageErrors, formatter);
51487
51561
  const previewMessage = `\uD83D\uDC1B ${preview}`;
51488
51562
  const post = await session.platform.createInteractivePost(previewMessage, [APPROVAL_EMOJIS[0], DENIAL_EMOJIS[0]], session.threadId);
51489
51563
  session.pendingBugReport = {
@@ -51491,7 +51565,8 @@ async function reportBug(session, description, username, ctx, errorContext, _att
51491
51565
  title,
51492
51566
  body,
51493
51567
  userDescription: bugDescription,
51494
- attachments: [],
51568
+ imageUrls,
51569
+ imageErrors,
51495
51570
  errorContext
51496
51571
  };
51497
51572
  ctx.ops.registerPost(post.id, session.threadId);
@@ -51505,7 +51580,7 @@ async function handleBugReportApproval(session, isApproved, username) {
51505
51580
  const formatter = session.platform.getFormatter();
51506
51581
  if (isApproved) {
51507
51582
  try {
51508
- const issueUrl = await createGitHubIssue(pending.title, pending.body, pending.attachments, session.workingDir);
51583
+ const issueUrl = await createGitHubIssue(pending.title, pending.body, session.workingDir);
51509
51584
  await withErrorHandling(() => session.platform.updatePost(pending.postId, `\u2705 ${formatter.formatBold("Bug report submitted")}: ${issueUrl}`), { action: "Update bug report post", session });
51510
51585
  sessionLog2(session).info(`\uD83D\uDC1B Bug report created by @${username}: ${issueUrl}`);
51511
51586
  } catch (err) {
@@ -52300,6 +52375,28 @@ async function handleExitPlanMode(session, toolUseId, ctx) {
52300
52375
  session.pendingApproval = { postId: post.id, type: "plan", toolUseId };
52301
52376
  ctx.ops.stopTyping(session);
52302
52377
  }
52378
+ async function cleanupOrphanedTaskPosts(session, currentTaskPostId) {
52379
+ try {
52380
+ const history = await session.platform.getThreadHistory(session.threadId, { limit: 50 });
52381
+ const taskPostPattern = /^(?:(?:---|___|\*\*\*|\u2014+)\s*\n)?\uD83D\uDCCB/;
52382
+ let cleanedCount = 0;
52383
+ for (const msg of history) {
52384
+ if (msg.id === currentTaskPostId)
52385
+ continue;
52386
+ if (!taskPostPattern.test(msg.message))
52387
+ continue;
52388
+ sessionLog4(session).info(`Cleaning up orphaned task post ${msg.id.substring(0, 8)}`);
52389
+ await session.platform.unpinPost(msg.id).catch(() => {});
52390
+ await session.platform.deletePost(msg.id).catch(() => {});
52391
+ cleanedCount++;
52392
+ }
52393
+ if (cleanedCount > 0) {
52394
+ sessionLog4(session).info(`Cleaned up ${cleanedCount} orphaned task post(s)`);
52395
+ }
52396
+ } catch (err) {
52397
+ sessionLog4(session).debug(`Task cleanup failed: ${err}`);
52398
+ }
52399
+ }
52303
52400
  async function handleTodoWrite(session, input, ctx) {
52304
52401
  const releaseLock = await acquireTaskListLock(session);
52305
52402
  try {
@@ -52388,14 +52485,20 @@ async function handleTodoWriteWithLock(session, input, ctx) {
52388
52485
  const displayMessage = session.tasksMinimized ? minimizedMessage : fullMessage;
52389
52486
  const existingTasksPostId = session.tasksPostId;
52390
52487
  if (existingTasksPostId) {
52391
- await withErrorHandling(() => session.platform.updatePost(existingTasksPostId, displayMessage), { action: "Update tasks", session });
52392
- } else {
52488
+ const updated = await withErrorHandling(() => session.platform.updatePost(existingTasksPostId, displayMessage), { action: "Update tasks", session });
52489
+ if (updated === undefined) {
52490
+ sessionLog4(session).warn(`Task post ${existingTasksPostId.substring(0, 8)} update failed, will create new one`);
52491
+ session.tasksPostId = null;
52492
+ }
52493
+ }
52494
+ if (!session.tasksPostId) {
52393
52495
  const post = await withErrorHandling(() => session.platform.createInteractivePost(displayMessage, [TASK_TOGGLE_EMOJIS[0]], session.threadId), { action: "Create tasks post", session });
52394
52496
  if (post) {
52395
52497
  session.tasksPostId = post.id;
52396
52498
  ctx.ops.registerPost(post.id, session.threadId);
52397
52499
  updateLastMessage(session, post);
52398
52500
  await session.platform.pinPost(post.id).catch(() => {});
52501
+ await cleanupOrphanedTaskPosts(session, post.id);
52399
52502
  }
52400
52503
  }
52401
52504
  ctx.ops.updateStickyMessage().catch(() => {});
@@ -54857,11 +54960,11 @@ class SessionManager extends EventEmitter4 {
54857
54960
  return;
54858
54961
  await enableInteractivePermissions(session, username, this.getContext());
54859
54962
  }
54860
- async reportBug(threadId, description, username) {
54963
+ async reportBug(threadId, description, username, files) {
54861
54964
  const session = this.findSessionByThreadId(threadId);
54862
54965
  if (!session)
54863
54966
  return;
54864
- await reportBug(session, description, username, this.getContext());
54967
+ await reportBug(session, description, username, this.getContext(), undefined, files);
54865
54968
  }
54866
54969
  async showUpdateStatus(threadId, _username) {
54867
54970
  const session = this.findSessionByThreadId(threadId);
@@ -65701,7 +65804,8 @@ Release notes not available. See ${formatter.formatLink("GitHub releases", "http
65701
65804
  return;
65702
65805
  case "bug":
65703
65806
  if (isAllowed) {
65704
- await session.reportBug(threadRoot, parsed.args, username);
65807
+ const files3 = post.metadata?.files;
65808
+ await session.reportBug(threadRoot, parsed.args, username, files3);
65705
65809
  }
65706
65810
  return;
65707
65811
  case "kill":
@@ -33831,9 +33831,11 @@ ${code}
33831
33831
  `);
33832
33832
  }
33833
33833
  formatMarkdown(content) {
33834
- return content.replace(/\n{3,}/g, `
33834
+ let processed = content.replace(/(?<=\n)```(?=\S)(?![a-zA-Z]*\n)/g, "```\n");
33835
+ processed = processed.replace(/\n{3,}/g, `
33835
33836
 
33836
33837
  `);
33838
+ return processed;
33837
33839
  }
33838
33840
  }
33839
33841
 
@@ -34104,6 +34106,7 @@ function convertMarkdownToSlack(content) {
34104
34106
  for (let i = 0;i < codeBlocks.length; i++) {
34105
34107
  preserved = preserved.replace(`${CODE_BLOCK_PLACEHOLDER}${i}\x00`, codeBlocks[i]);
34106
34108
  }
34109
+ preserved = preserved.replace(/(?<=\n)```(?=\S)(?![a-zA-Z]*\n)/g, "```\n");
34107
34110
  return preserved;
34108
34111
  }
34109
34112
  function convertMarkdownTablesToSlack(content) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-threads",
3
- "version": "0.50.0",
3
+ "version": "0.51.0",
4
4
  "description": "Share Claude Code sessions live in a Mattermost channel with interactive features",
5
5
  "main": "dist/index.js",
6
6
  "type": "module",