claude-threads 0.45.0 → 0.47.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,33 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.47.0] - 2026-01-07
11
+
12
+ ### Added
13
+ - **Session context in system prompt** - Claude now receives metadata about the session including version, current working directory, git status, and platform info (#119)
14
+
15
+ ### Fixed
16
+ - **Task list duplication fixed** - Resolved race condition causing duplicate task lists by extending promise lock scope (#122)
17
+ - **Code blocks now render correctly** - Added trailing newline to code blocks for proper markdown rendering (#123)
18
+ - **Worktree paths shortened in UI** - Paths now show as `[branch]/path` instead of full worktree paths for better readability (#121)
19
+ - **Worktree metadata centralized** - Moved `.claude-threads-meta.json` to central config directory to avoid polluting project directories (#120)
20
+
21
+ ### Changed
22
+ - **Bump @modelcontextprotocol/sdk** - Updated from 1.25.1 to 1.25.2 (#118)
23
+
24
+ ## [0.46.0] - 2026-01-07
25
+
26
+ ### Added
27
+ - **Emoji reactions for `!update` command** - React with 👍 to update immediately or 👎 to defer for 1 hour, easier than typing commands on mobile
28
+
29
+ ### Fixed
30
+ - **Auto-update uses bun instead of npm** - Fixed updates to use `bun install -g` matching the actual install location
31
+ - **ESLint warnings resolved** - Fixed 8 non-null assertion warnings with proper null checks
32
+ - **Dead code removed** - Removed unused Discord formatter/types and other dead code via Knip
33
+
34
+ ### Changed
35
+ - **Knip added to CI and pre-commit** - Dead code detection now runs automatically
36
+
10
37
  ## [0.45.0] - 2026-01-07
11
38
 
12
39
  ### Added
package/dist/index.js CHANGED
@@ -42492,7 +42492,8 @@ class MattermostFormatter {
42492
42492
  const lang = language || "";
42493
42493
  return `\`\`\`${lang}
42494
42494
  ${code}
42495
- \`\`\``;
42495
+ \`\`\`
42496
+ `;
42496
42497
  }
42497
42498
  formatUserMention(username) {
42498
42499
  return `@${username}`;
@@ -43147,7 +43148,8 @@ class SlackFormatter {
43147
43148
  formatCodeBlock(code, _language) {
43148
43149
  return `\`\`\`
43149
43150
  ${code}
43150
- \`\`\``;
43151
+ \`\`\`
43152
+ `;
43151
43153
  }
43152
43154
  formatUserMention(username, userId) {
43153
43155
  if (userId) {
@@ -44840,27 +44842,32 @@ function isValidBranchName(name) {
44840
44842
  return false;
44841
44843
  return true;
44842
44844
  }
44843
- var METADATA_FILENAME = ".claude-threads-meta.json";
44844
- function getMetadataPath(worktreePath) {
44845
- return path.join(worktreePath, METADATA_FILENAME);
44845
+ var METADATA_STORE_PATH = path.join(homedir3(), ".claude-threads", "worktree-metadata.json");
44846
+ async function readMetadataStore() {
44847
+ try {
44848
+ const content = await fs.readFile(METADATA_STORE_PATH, "utf-8");
44849
+ return JSON.parse(content);
44850
+ } catch {
44851
+ return {};
44852
+ }
44846
44853
  }
44847
- async function writeWorktreeMetadata(worktreePath, metadata) {
44848
- const metaPath = getMetadataPath(worktreePath);
44854
+ async function writeMetadataStore(store) {
44849
44855
  try {
44850
- await fs.writeFile(metaPath, JSON.stringify(metadata, null, 2), "utf-8");
44851
- log5.debug(`Wrote worktree metadata: ${metaPath}`);
44856
+ await fs.mkdir(path.dirname(METADATA_STORE_PATH), { recursive: true });
44857
+ await fs.writeFile(METADATA_STORE_PATH, JSON.stringify(store, null, 2), "utf-8");
44852
44858
  } catch (err) {
44853
- log5.warn(`Failed to write worktree metadata: ${err}`);
44859
+ log5.warn(`Failed to write worktree metadata store: ${err}`);
44854
44860
  }
44855
44861
  }
44862
+ async function writeWorktreeMetadata(worktreePath, metadata) {
44863
+ const store = await readMetadataStore();
44864
+ store[worktreePath] = metadata;
44865
+ await writeMetadataStore(store);
44866
+ log5.debug(`Wrote worktree metadata for: ${worktreePath}`);
44867
+ }
44856
44868
  async function readWorktreeMetadata(worktreePath) {
44857
- const metaPath = getMetadataPath(worktreePath);
44858
- try {
44859
- const content = await fs.readFile(metaPath, "utf-8");
44860
- return JSON.parse(content);
44861
- } catch {
44862
- return null;
44863
- }
44869
+ const store = await readMetadataStore();
44870
+ return store[worktreePath] || null;
44864
44871
  }
44865
44872
 
44866
44873
  // src/utils/format.ts
@@ -45183,29 +45190,43 @@ function stopTyping(session) {
45183
45190
  }
45184
45191
  }
45185
45192
  async function bumpTasksToBottomWithContent(session, newContent, registerPost) {
45186
- const oldTasksPostId = session.tasksPostId;
45187
- const oldTasksContent = session.lastTasksContent;
45188
- sessionLog(session).debug(`Bumping tasks to bottom, repurposing post ${oldTasksPostId.substring(0, 8)}`);
45189
- try {
45190
- await session.platform.removeReaction(oldTasksPostId, TASK_TOGGLE_EMOJIS[0]);
45191
- } catch (err) {
45192
- sessionLog(session).debug(`Could not remove toggle emoji: ${err}`);
45193
+ if (session.taskListCreationPromise) {
45194
+ await session.taskListCreationPromise;
45193
45195
  }
45194
- await session.platform.unpinPost(oldTasksPostId).catch(() => {});
45195
- await withErrorHandling(() => session.platform.updatePost(oldTasksPostId, newContent), { action: "Repurpose task post", session });
45196
- registerPost(oldTasksPostId, session.threadId);
45197
- if (oldTasksContent) {
45198
- const displayContent = getTaskDisplayContent(session);
45199
- const newTasksPost = await session.platform.createInteractivePost(displayContent, [TASK_TOGGLE_EMOJIS[0]], session.threadId);
45200
- session.tasksPostId = newTasksPost.id;
45201
- sessionLog(session).debug(`Created new task post ${newTasksPost.id.substring(0, 8)}`);
45202
- registerPost(newTasksPost.id, session.threadId);
45203
- updateLastMessage(session, newTasksPost);
45204
- await session.platform.pinPost(newTasksPost.id).catch(() => {});
45205
- } else {
45206
- session.tasksPostId = null;
45196
+ let resolveCreation;
45197
+ session.taskListCreationPromise = new Promise((resolve2) => {
45198
+ resolveCreation = resolve2;
45199
+ });
45200
+ try {
45201
+ const oldTasksPostId = session.tasksPostId;
45202
+ const oldTasksContent = session.lastTasksContent;
45203
+ sessionLog(session).debug(`Bumping tasks to bottom, repurposing post ${oldTasksPostId.substring(0, 8)}`);
45204
+ try {
45205
+ await session.platform.removeReaction(oldTasksPostId, TASK_TOGGLE_EMOJIS[0]);
45206
+ } catch (err) {
45207
+ sessionLog(session).debug(`Could not remove toggle emoji: ${err}`);
45208
+ }
45209
+ await session.platform.unpinPost(oldTasksPostId).catch(() => {});
45210
+ await withErrorHandling(() => session.platform.updatePost(oldTasksPostId, newContent), { action: "Repurpose task post", session });
45211
+ registerPost(oldTasksPostId, session.threadId);
45212
+ if (oldTasksContent) {
45213
+ const displayContent = getTaskDisplayContent(session);
45214
+ const newTasksPost = await session.platform.createInteractivePost(displayContent, [TASK_TOGGLE_EMOJIS[0]], session.threadId);
45215
+ session.tasksPostId = newTasksPost.id;
45216
+ sessionLog(session).debug(`Created new task post ${newTasksPost.id.substring(0, 8)}`);
45217
+ registerPost(newTasksPost.id, session.threadId);
45218
+ updateLastMessage(session, newTasksPost);
45219
+ await session.platform.pinPost(newTasksPost.id).catch(() => {});
45220
+ } else {
45221
+ session.tasksPostId = null;
45222
+ }
45223
+ return oldTasksPostId;
45224
+ } finally {
45225
+ if (resolveCreation) {
45226
+ resolveCreation();
45227
+ }
45228
+ session.taskListCreationPromise = undefined;
45207
45229
  }
45208
- return oldTasksPostId;
45209
45230
  }
45210
45231
  async function bumpTasksToBottom(session, registerPost) {
45211
45232
  if (!session.tasksPostId || !session.lastTasksContent) {
@@ -45216,9 +45237,20 @@ async function bumpTasksToBottom(session, registerPost) {
45216
45237
  sessionLog(session).debug("Tasks completed, not bumping");
45217
45238
  return;
45218
45239
  }
45219
- const oldPostId = session.tasksPostId;
45220
- sessionLog(session).debug(`Bumping tasks: deleting old post ${oldPostId.substring(0, 8)}`);
45240
+ if (session.taskListCreationPromise) {
45241
+ await session.taskListCreationPromise;
45242
+ }
45243
+ if (!session.tasksPostId || !session.lastTasksContent || session.tasksCompleted) {
45244
+ sessionLog(session).debug("Task list state changed while waiting for lock");
45245
+ return;
45246
+ }
45247
+ let resolveCreation;
45248
+ session.taskListCreationPromise = new Promise((resolve2) => {
45249
+ resolveCreation = resolve2;
45250
+ });
45221
45251
  try {
45252
+ const oldPostId = session.tasksPostId;
45253
+ sessionLog(session).debug(`Bumping tasks: deleting old post ${oldPostId.substring(0, 8)}`);
45222
45254
  await session.platform.unpinPost(session.tasksPostId).catch(() => {});
45223
45255
  await session.platform.deletePost(session.tasksPostId);
45224
45256
  const displayContent = getTaskDisplayContent(session);
@@ -45232,6 +45264,11 @@ async function bumpTasksToBottom(session, registerPost) {
45232
45264
  await session.platform.pinPost(newPost.id).catch(() => {});
45233
45265
  } catch (err) {
45234
45266
  sessionLog(session).error(`Failed to bump tasks to bottom: ${err}`);
45267
+ } finally {
45268
+ if (resolveCreation) {
45269
+ resolveCreation();
45270
+ }
45271
+ session.taskListCreationPromise = undefined;
45235
45272
  }
45236
45273
  }
45237
45274
  async function flush(session, registerPost) {
@@ -45243,6 +45280,7 @@ async function flush(session, registerPost) {
45243
45280
  const { maxLength: MAX_POST_LENGTH, hardThreshold: HARD_CONTINUATION_THRESHOLD } = session.platform.getMessageLimits();
45244
45281
  const shouldBreakEarly = session.currentPostId && content.length > MIN_BREAK_THRESHOLD && shouldFlushEarly(content);
45245
45282
  if (session.currentPostId && (content.length > HARD_CONTINUATION_THRESHOLD || shouldBreakEarly)) {
45283
+ const currentPostId = session.currentPostId;
45246
45284
  let breakPoint;
45247
45285
  let codeBlockLanguage;
45248
45286
  if (content.length > HARD_CONTINUATION_THRESHOLD) {
@@ -45275,7 +45313,7 @@ async function flush(session, registerPost) {
45275
45313
  breakPoint = breakInfo.position;
45276
45314
  } else {
45277
45315
  try {
45278
- await session.platform.updatePost(session.currentPostId, content);
45316
+ await session.platform.updatePost(currentPostId, content);
45279
45317
  } catch {
45280
45318
  sessionLog(session).debug("Update failed (no breakpoint), will create new post on next flush");
45281
45319
  session.currentPostId = null;
@@ -45295,7 +45333,7 @@ async function flush(session, registerPost) {
45295
45333
 
45296
45334
  ` + formatter2.formatItalic("... (continued below)") : firstPart;
45297
45335
  try {
45298
- await session.platform.updatePost(session.currentPostId, firstPartWithMarker);
45336
+ await session.platform.updatePost(currentPostId, firstPartWithMarker);
45299
45337
  } catch {
45300
45338
  sessionLog(session).debug("Update failed during split, continuing with new post");
45301
45339
  }
@@ -45329,8 +45367,9 @@ async function flush(session, registerPost) {
45329
45367
  ` + formatter2.formatItalic("... (truncated)");
45330
45368
  }
45331
45369
  if (session.currentPostId) {
45370
+ const postId = session.currentPostId;
45332
45371
  try {
45333
- await session.platform.updatePost(session.currentPostId, content);
45372
+ await session.platform.updatePost(postId, content);
45334
45373
  } catch {
45335
45374
  sessionLog(session).debug("Update failed, will create new post on next flush");
45336
45375
  session.currentPostId = null;
@@ -46581,7 +46620,9 @@ async function handleTodoWrite(session, input, ctx) {
46581
46620
  await session.platform.pinPost(post.id).catch(() => {});
46582
46621
  }
46583
46622
  } finally {
46584
- resolveCreation();
46623
+ if (resolveCreation) {
46624
+ resolveCreation();
46625
+ }
46585
46626
  session.taskListCreationPromise = undefined;
46586
46627
  }
46587
46628
  }
@@ -46633,7 +46674,8 @@ async function handleCompactionComplete(session, compactMetadata, _ctx) {
46633
46674
  const formatter = session.platform.getFormatter();
46634
46675
  const completionMessage = `\u2705 ${formatter.formatBold("Context compacted")} ${formatter.formatItalic(`(${info})`)}`;
46635
46676
  if (session.compactionPostId) {
46636
- await withErrorHandling(() => session.platform.updatePost(session.compactionPostId, completionMessage), { action: "Update compaction complete", session });
46677
+ const postId = session.compactionPostId;
46678
+ await withErrorHandling(() => session.platform.updatePost(postId, completionMessage), { action: "Update compaction complete", session });
46637
46679
  session.compactionPostId = undefined;
46638
46680
  } else {
46639
46681
  const post = await withErrorHandling(() => session.platform.createPost(completionMessage, session.threadId), { action: "Post compaction complete", session });
@@ -46933,7 +46975,7 @@ async function handleExistingWorktreeReaction(session, postId, emojiName, userna
46933
46975
  if (!isApprove && !isDeny) {
46934
46976
  return false;
46935
46977
  }
46936
- const shortPath = pending.worktreePath.replace(process.env.HOME || "", "~");
46978
+ const shortPath = shortenPath(pending.worktreePath, undefined, { path: pending.worktreePath, branch: pending.branch });
46937
46979
  const formatter = session.platform.getFormatter();
46938
46980
  if (isApprove) {
46939
46981
  await session.platform.updatePost(pending.postId, `\u2705 Joining worktree for branch ${formatter.formatCode(pending.branch)} at ${formatter.formatCode(shortPath)}`);
@@ -46949,6 +46991,37 @@ async function handleExistingWorktreeReaction(session, postId, emojiName, userna
46949
46991
  }
46950
46992
  return true;
46951
46993
  }
46994
+ async function handleUpdateReaction(session, postId, emojiName, username, ctx, updateHandler) {
46995
+ const pending = session.pendingUpdatePrompt;
46996
+ if (!pending || pending.postId !== postId) {
46997
+ return false;
46998
+ }
46999
+ if (session.startedBy !== username && !session.platform.isUserAllowed(username)) {
47000
+ return false;
47001
+ }
47002
+ const isUpdateNow = isApprovalEmoji(emojiName);
47003
+ const isDefer = isDenialEmoji(emojiName);
47004
+ if (!isUpdateNow && !isDefer) {
47005
+ return false;
47006
+ }
47007
+ const formatter = session.platform.getFormatter();
47008
+ if (isUpdateNow) {
47009
+ await withErrorHandling(() => session.platform.updatePost(pending.postId, `\uD83D\uDD04 ${formatter.formatBold("Forcing update")} - restarting shortly...
47010
+ ` + formatter.formatItalic("Sessions will resume automatically")), { action: "Update post for force update", session });
47011
+ session.pendingUpdatePrompt = undefined;
47012
+ ctx.ops.persistSession(session);
47013
+ sessionLog3(session).info(`\uD83D\uDD04 @${username} triggered immediate update`);
47014
+ await updateHandler.forceUpdate();
47015
+ } else {
47016
+ updateHandler.deferUpdate(60);
47017
+ await withErrorHandling(() => session.platform.updatePost(pending.postId, `\u23F8\uFE0F ${formatter.formatBold("Update deferred")} for 1 hour
47018
+ ` + formatter.formatItalic("Use !update now to apply earlier")), { action: "Update post for defer update", session });
47019
+ session.pendingUpdatePrompt = undefined;
47020
+ ctx.ops.persistSession(session);
47021
+ sessionLog3(session).info(`\u23F8\uFE0F @${username} deferred update for 1 hour`);
47022
+ }
47023
+ return true;
47024
+ }
46952
47025
 
46953
47026
  // src/claude/cli.ts
46954
47027
  import { spawn as spawn2 } from "child_process";
@@ -50700,7 +50773,7 @@ function checkForUpdates() {
50700
50773
  cachedUpdateInfo = notifier.update;
50701
50774
  notifier.notify({
50702
50775
  message: `Update available: {currentVersion} \u2192 {latestVersion}
50703
- Run: npm install -g claude-threads`
50776
+ Run: bun install -g claude-threads`
50704
50777
  });
50705
50778
  } catch {}
50706
50779
  }
@@ -51144,7 +51217,7 @@ function validateClaudeCli() {
51144
51217
  installed: false,
51145
51218
  version: null,
51146
51219
  compatible: false,
51147
- message: "Claude CLI not found. Install it with: npm install -g @anthropic-ai/claude-code"
51220
+ message: "Claude CLI not found. Install it with: bun install -g @anthropic-ai/claude-code"
51148
51221
  };
51149
51222
  }
51150
51223
  const compatible = isVersionCompatible(version);
@@ -51154,7 +51227,7 @@ function validateClaudeCli() {
51154
51227
  version,
51155
51228
  compatible: false,
51156
51229
  message: `Claude CLI version ${version} is not compatible. Required: ${CLAUDE_CLI_VERSION_RANGE}
51157
- ` + `Install a compatible version: npm install -g @anthropic-ai/claude-code@2.0.76`
51230
+ ` + `Install a compatible version: bun install -g @anthropic-ai/claude-code@2.0.76`
51158
51231
  };
51159
51232
  }
51160
51233
  return {
@@ -51271,7 +51344,8 @@ async function changeDirectory(session, newDir, username, ctx) {
51271
51344
  sessionLog4(session).warn(`\uD83D\uDCC2 Not a directory: ${newDir}`);
51272
51345
  return;
51273
51346
  }
51274
- const shortDir = absoluteDir.replace(process.env.HOME || "", "~");
51347
+ const worktreeContext = session.worktreeInfo ? { path: session.worktreeInfo.worktreePath, branch: session.worktreeInfo.branch } : undefined;
51348
+ const shortDir = shortenPath(absoluteDir, undefined, worktreeContext);
51275
51349
  sessionLog4(session).info(`\uD83D\uDCC2 Changing directory to ${shortDir}`);
51276
51350
  session.workingDir = absoluteDir;
51277
51351
  const newSessionId = randomUUID2();
@@ -51405,7 +51479,8 @@ async function updateSessionHeader(session, ctx) {
51405
51479
  if (!session.sessionStartPostId)
51406
51480
  return;
51407
51481
  const formatter = session.platform.getFormatter();
51408
- const shortDir = session.workingDir.replace(process.env.HOME || "", "~");
51482
+ const worktreeContext = session.worktreeInfo ? { path: session.worktreeInfo.worktreePath, branch: session.worktreeInfo.branch } : undefined;
51483
+ const shortDir = shortenPath(session.workingDir, undefined, worktreeContext);
51409
51484
  const isInteractive = !ctx.config.skipPermissions || session.forceInteractivePermissions;
51410
51485
  const permMode = isInteractive ? "\uD83D\uDD10 Interactive" : "\u26A1 Auto";
51411
51486
  const otherParticipants = [...session.sessionAllowedUsers].filter((u) => u !== session.startedBy).map((u) => formatter.formatUserMention(u)).join(", ");
@@ -51477,7 +51552,7 @@ async function updateSessionHeader(session, ctx) {
51477
51552
  items.push(["\uD83C\uDD94", "Session ID", formatter.formatCode(session.claudeSessionId.substring(0, 8))]);
51478
51553
  const updateInfo = getUpdateInfo();
51479
51554
  const updateNotice = updateInfo ? `
51480
- > \u26A0\uFE0F ${formatter.formatBold("Update available:")} v${updateInfo.current} \u2192 v${updateInfo.latest} - Run ${formatter.formatCode("npm install -g claude-threads")}
51555
+ > \u26A0\uFE0F ${formatter.formatBold("Update available:")} v${updateInfo.current} \u2192 v${updateInfo.latest} - Run ${formatter.formatCode("bun install -g claude-threads")}
51481
51556
  ` : "";
51482
51557
  const releaseNotes = getReleaseNotes(VERSION);
51483
51558
  const whatsNew = releaseNotes ? getWhatsNewSummary(releaseNotes) : "";
@@ -51520,15 +51595,15 @@ async function showUpdateStatus(session, updateManager) {
51520
51595
  } else {
51521
51596
  statusLine = `Mode: ${config.autoRestartMode}`;
51522
51597
  }
51523
- await postInfo(session, `\uD83D\uDD04 ${formatter.formatBold("Update available")}
51598
+ const message = `\uD83D\uDD04 ${formatter.formatBold("Update available")}
51524
51599
 
51525
51600
  ` + `Current: v${updateInfo.currentVersion}
51526
51601
  ` + `Latest: v${updateInfo.latestVersion}
51527
51602
  ` + `${statusLine}
51528
51603
 
51529
- ` + `Commands:
51530
- ` + `\u2022 ${formatter.formatCode("!update now")} - Update immediately
51531
- ` + `\u2022 ${formatter.formatCode("!update defer")} - Defer for 1 hour`);
51604
+ ` + `React: \uD83D\uDC4D Update now | \uD83D\uDC4E Defer for 1 hour`;
51605
+ const post = await session.platform.createInteractivePost(message, [APPROVAL_EMOJIS[0], DENIAL_EMOJIS[0]], session.threadId);
51606
+ session.pendingUpdatePrompt = { postId: post.id };
51532
51607
  }
51533
51608
  async function forceUpdateNow(session, username, updateManager) {
51534
51609
  if (!await requireSessionOwner(session, username, "force updates")) {
@@ -51607,6 +51682,10 @@ function findPersistedByThreadId(persisted, threadId) {
51607
51682
  }
51608
51683
  return;
51609
51684
  }
51685
+ function buildSessionContext(platform, workingDir) {
51686
+ const platformName = platform.platformType.charAt(0).toUpperCase() + platform.platformType.slice(1);
51687
+ return `**Platform:** ${platformName} (${platform.displayName}) | **Working Directory:** ${workingDir}`;
51688
+ }
51610
51689
  var CHAT_PLATFORM_PROMPT = `
51611
51690
  You are running inside a chat platform (like Mattermost or Slack). Users interact with you through chat messages in a thread.
51612
51691
 
@@ -51704,6 +51783,10 @@ ${startFormatter.formatItalic("Starting session...")}`, replyToPostId), { action
51704
51783
  const actualThreadId = replyToPostId || post.id;
51705
51784
  const sessionId = ctx.ops.getSessionId(platformId, actualThreadId);
51706
51785
  const claudeSessionId = randomUUID3();
51786
+ const sessionContext = buildSessionContext(platform, ctx.config.workingDir);
51787
+ const systemPrompt = `${sessionContext}
51788
+
51789
+ ${CHAT_PLATFORM_PROMPT}`;
51707
51790
  const platformMcpConfig = platform.getMcpConfig();
51708
51791
  const cliOptions = {
51709
51792
  workingDir: ctx.config.workingDir,
@@ -51713,7 +51796,7 @@ ${startFormatter.formatItalic("Starting session...")}`, replyToPostId), { action
51713
51796
  resume: false,
51714
51797
  chrome: ctx.config.chromeEnabled,
51715
51798
  platformConfig: platformMcpConfig,
51716
- appendSystemPrompt: CHAT_PLATFORM_PROMPT,
51799
+ appendSystemPrompt: systemPrompt,
51717
51800
  logSessionId: sessionId
51718
51801
  };
51719
51802
  const claude = new ClaudeCli(cliOptions);
@@ -51844,6 +51927,13 @@ Please start a new session.`, state.threadId), { action: "Post resume failure no
51844
51927
  const skipPerms = ctx.config.skipPermissions && !state.forceInteractivePermissions;
51845
51928
  const platformMcpConfig = platform.getMcpConfig();
51846
51929
  const needsTitlePrompt = !state.sessionTitle;
51930
+ let appendSystemPrompt;
51931
+ if (needsTitlePrompt) {
51932
+ const sessionContext = buildSessionContext(platform, state.workingDir);
51933
+ appendSystemPrompt = `${sessionContext}
51934
+
51935
+ ${CHAT_PLATFORM_PROMPT}`;
51936
+ }
51847
51937
  const cliOptions = {
51848
51938
  workingDir: state.workingDir,
51849
51939
  threadId: state.threadId,
@@ -51852,7 +51942,7 @@ Please start a new session.`, state.threadId), { action: "Post resume failure no
51852
51942
  resume: true,
51853
51943
  chrome: ctx.config.chromeEnabled,
51854
51944
  platformConfig: platformMcpConfig,
51855
- appendSystemPrompt: needsTitlePrompt ? CHAT_PLATFORM_PROMPT : undefined,
51945
+ appendSystemPrompt,
51856
51946
  logSessionId: sessionId
51857
51947
  };
51858
51948
  const claude = new ClaudeCli(cliOptions);
@@ -51929,9 +52019,10 @@ Please start a new session.`, state.threadId), { action: "Post resume failure no
51929
52019
  sessionLog5(session).info(`\uD83D\uDD04 Session resumed (@${state.startedBy})`);
51930
52020
  const sessionFormatter = session.platform.getFormatter();
51931
52021
  if (session.lifecyclePostId) {
52022
+ const postId = session.lifecyclePostId;
51932
52023
  const resumeMsg = `\uD83D\uDD04 ${sessionFormatter.formatBold("Session resumed")} by ${sessionFormatter.formatUserMention(session.startedBy)}
51933
52024
  ${sessionFormatter.formatItalic("Reconnected to Claude session. You can continue where you left off.")}`;
51934
- await withErrorHandling(() => session.platform.updatePost(session.lifecyclePostId, resumeMsg), { action: "Update timeout/shutdown post for resume", session });
52025
+ await withErrorHandling(() => session.platform.updatePost(postId, resumeMsg), { action: "Update timeout/shutdown post for resume", session });
51935
52026
  session.lifecyclePostId = undefined;
51936
52027
  } else {
51937
52028
  const restartMsg = `${sessionFormatter.formatBold("Session resumed")} after bot restart (v${VERSION})
@@ -52163,7 +52254,8 @@ async function cleanupIdleSessions(timeoutMs, warningMs, ctx) {
52163
52254
 
52164
52255
  \uD83D\uDCA1 React with \uD83D\uDD04 to resume, or send a new message to continue.`;
52165
52256
  if (session.lifecyclePostId) {
52166
- await withErrorHandling(() => session.platform.updatePost(session.lifecyclePostId, `\u23F1\uFE0F ${timeoutMessage}`), { action: "Update timeout post", session });
52257
+ const postId = session.lifecyclePostId;
52258
+ await withErrorHandling(() => session.platform.updatePost(postId, `\u23F1\uFE0F ${timeoutMessage}`), { action: "Update timeout post", session });
52167
52259
  } else {
52168
52260
  const timeoutPost = await withErrorHandling(() => postTimeout(session, timeoutMessage), { action: "Post session timeout", session });
52169
52261
  if (timeoutPost) {
@@ -52297,7 +52389,7 @@ async function createAndSwitchToWorktree(session, branch, username, options) {
52297
52389
  const repoRoot = await getRepositoryRoot(session.workingDir);
52298
52390
  const existing = await findWorktreeByBranch(repoRoot, branch);
52299
52391
  if (existing && !existing.isMain) {
52300
- const shortPath = existing.path.replace(process.env.HOME || "", "~");
52392
+ const shortPath = shortenPath(existing.path, undefined, { path: existing.path, branch });
52301
52393
  const fmt = session.platform.getFormatter();
52302
52394
  if (session.pendingWorktreePrompt) {
52303
52395
  sessionLog6(session).info(`\uD83C\uDF3F Auto-joining existing worktree ${branch} (user specified inline)`);
@@ -52430,7 +52522,7 @@ ${fmt.formatItalic("Claude Code restarted in the worktree")}`);
52430
52522
  session.claude.start();
52431
52523
  }
52432
52524
  await options.updateSessionHeader(session);
52433
- const shortWorktreePath = worktreePath.replace(process.env.HOME || "", "~");
52525
+ const shortWorktreePath = shortenPath(worktreePath, undefined, { path: worktreePath, branch });
52434
52526
  const fmt = session.platform.getFormatter();
52435
52527
  await postSuccess(session, `${fmt.formatBold("Created worktree")} for branch ${fmt.formatCode(branch)}
52436
52528
  \uD83D\uDCC1 Working directory: ${fmt.formatCode(shortWorktreePath)}
@@ -52512,7 +52604,7 @@ async function listWorktreesCommand(session) {
52512
52604
 
52513
52605
  `;
52514
52606
  for (const wt of worktrees) {
52515
- const shortPath = wt.path.replace(process.env.HOME || "", "~");
52607
+ const shortPath = wt.isMain ? wt.path.replace(process.env.HOME || "", "~") : shortenPath(wt.path, undefined, { path: wt.path, branch: wt.branch });
52516
52608
  const isCurrent = session.workingDir === wt.path;
52517
52609
  const marker = isCurrent ? " \u2190 current" : "";
52518
52610
  const label = wt.isMain ? "(main repository)" : "";
@@ -52547,7 +52639,7 @@ async function removeWorktreeCommand(session, branchOrPath, username) {
52547
52639
  }
52548
52640
  try {
52549
52641
  await removeWorktree(repoRoot, target.path);
52550
- const shortPath = target.path.replace(process.env.HOME || "", "~");
52642
+ const shortPath = shortenPath(target.path, undefined, { path: target.path, branch: target.branch });
52551
52643
  await postSuccess(session, `Removed worktree \`${target.branch}\` at \`${shortPath}\``);
52552
52644
  sessionLog6(session).info(`\uD83D\uDDD1\uFE0F Removed worktree ${target.branch} at ${shortPath}`);
52553
52645
  } catch (err) {
@@ -52593,7 +52685,7 @@ async function cleanupWorktreeCommand(session, username, hasOtherSessionsUsingWo
52593
52685
  try {
52594
52686
  sessionLog6(session).info(`\uD83D\uDDD1\uFE0F Cleaning up worktree: ${worktreePath}`);
52595
52687
  await removeWorktree(repoRoot, worktreePath);
52596
- const shortPath = worktreePath.replace(process.env.HOME || "", "~");
52688
+ const shortPath = shortenPath(worktreePath, undefined, { path: worktreePath, branch });
52597
52689
  await postSuccess(session, `Cleaned up worktree \`${branch}\` at \`${shortPath}\``);
52598
52690
  sessionLog6(session).info(`\u2705 Worktree cleaned up successfully`);
52599
52691
  } catch (err) {
@@ -53310,7 +53402,10 @@ class SessionManager extends EventEmitter4 {
53310
53402
  if (!this.worktreeUsers.has(worktreePath)) {
53311
53403
  this.worktreeUsers.set(worktreePath, new Set);
53312
53404
  }
53313
- this.worktreeUsers.get(worktreePath).add(sessionId);
53405
+ const users = this.worktreeUsers.get(worktreePath);
53406
+ if (users) {
53407
+ users.add(sessionId);
53408
+ }
53314
53409
  log18.debug(`Registered session ${sessionId.substring(0, 20)} as worktree user for ${worktreePath}`);
53315
53410
  }
53316
53411
  unregisterWorktreeUser(worktreePath, sessionId) {
@@ -53466,6 +53561,17 @@ class SessionManager extends EventEmitter4 {
53466
53561
  if (handled)
53467
53562
  return;
53468
53563
  }
53564
+ if (action === "added" && session.pendingUpdatePrompt?.postId === postId) {
53565
+ if (this.autoUpdateManager) {
53566
+ const updateHandler = {
53567
+ forceUpdate: () => this.autoUpdateManager.forceUpdate(),
53568
+ deferUpdate: (minutes) => this.autoUpdateManager.deferUpdate(minutes)
53569
+ };
53570
+ const handled = await handleUpdateReaction(session, postId, emojiName, username, this.getContext(), updateHandler);
53571
+ if (handled)
53572
+ return;
53573
+ }
53574
+ }
53469
53575
  if (action === "added" && session.pendingContextPrompt?.postId === postId) {
53470
53576
  await this.handleContextPromptReaction(session, emojiName, username);
53471
53577
  return;
@@ -64480,8 +64586,11 @@ async function installVersion(version) {
64480
64586
  justUpdated: false
64481
64587
  });
64482
64588
  return new Promise((resolve7) => {
64483
- const npmCmd = process.platform === "win32" ? "npm.cmd" : "npm";
64484
- const child = spawn5(npmCmd, ["install", "-g", `${PACKAGE_NAME3}@${version}`], {
64589
+ const useBun = process.platform !== "win32";
64590
+ const cmd = useBun ? "bun" : "npm.cmd";
64591
+ const args = useBun ? ["install", "-g", `${PACKAGE_NAME3}@${version}`] : ["install", "-g", `${PACKAGE_NAME3}@${version}`];
64592
+ log21.debug(`Using ${useBun ? "bun" : "npm"} for installation`);
64593
+ const child = spawn5(cmd, args, {
64485
64594
  stdio: ["ignore", "pipe", "pipe"],
64486
64595
  env: {
64487
64596
  ...process.env,
@@ -64529,8 +64638,9 @@ async function installVersion(version) {
64529
64638
  });
64530
64639
  }
64531
64640
  function getRollbackInstructions(previousVersion) {
64641
+ const cmd = process.platform === "win32" ? "npm" : "bun";
64532
64642
  return `To rollback to the previous version, run:
64533
- npm install -g ${PACKAGE_NAME3}@${previousVersion}`;
64643
+ ${cmd} install -g ${PACKAGE_NAME3}@${previousVersion}`;
64534
64644
  }
64535
64645
 
64536
64646
  class UpdateInstaller {
@@ -33787,7 +33787,8 @@ class MattermostFormatter {
33787
33787
  const lang = language || "";
33788
33788
  return `\`\`\`${lang}
33789
33789
  ${code}
33790
- \`\`\``;
33790
+ \`\`\`
33791
+ `;
33791
33792
  }
33792
33793
  formatUserMention(username) {
33793
33794
  return `@${username}`;
@@ -34092,7 +34093,8 @@ class SlackFormatter {
34092
34093
  formatCodeBlock(code, _language) {
34093
34094
  return `\`\`\`
34094
34095
  ${code}
34095
- \`\`\``;
34096
+ \`\`\`
34097
+ `;
34096
34098
  }
34097
34099
  formatUserMention(username, userId) {
34098
34100
  if (userId) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-threads",
3
- "version": "0.45.0",
3
+ "version": "0.47.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",
@@ -63,6 +63,7 @@
63
63
  "dependencies": {
64
64
  "@inkjs/ui": "^2.0.0",
65
65
  "@modelcontextprotocol/sdk": "^1.25.1",
66
+ "cli-spinners": "^3.3.0",
66
67
  "commander": "^14.0.2",
67
68
  "diff": "^8.0.2",
68
69
  "ink": "^6.6.0",
@@ -76,7 +77,6 @@
76
77
  "devDependencies": {
77
78
  "@eslint/js": "^9.39.2",
78
79
  "@types/bun": "latest",
79
- "@types/diff": "^8.0.0",
80
80
  "@types/node": "^25.0.3",
81
81
  "@types/prompts": "^2.4.9",
82
82
  "@types/react": "^19.2.7",