claude-threads 0.22.1 → 0.24.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.
Files changed (3) hide show
  1. package/CHANGELOG.md +40 -0
  2. package/dist/index.js +224 -14
  3. package/package.json +1 -1
package/CHANGELOG.md CHANGED
@@ -7,6 +7,46 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.24.0] - 2026-01-02
11
+
12
+ ### Added
13
+ - **Enhanced session status bar with model and context info** - The session header now displays real-time usage information similar to Claude Code's status line:
14
+ - Model name (`🤖 Opus 4.5`, `🤖 Sonnet 4`, etc.)
15
+ - Context usage with visual progress bar (`🟢▓▓░░░░░░░░ 23%`)
16
+ - Session cost (`💰 $0.07`)
17
+ - Color-coded context indicator:
18
+ - 🟢 Green: < 50% (plenty of context)
19
+ - 🟡 Yellow: 50-75% (moderate usage)
20
+ - 🟠 Orange: 75-90% (getting full)
21
+ - 🔴 Red: 90%+ (almost full)
22
+ - **Periodic status bar updates** - Status bar now refreshes every 30 seconds automatically to keep uptime and usage stats current
23
+ - **Usage stats tracking** - Session now tracks token usage, cost, and model information extracted from Claude CLI result events
24
+
25
+ ### Improved
26
+ - **Existing worktree handling** - When a worktree already exists for a branch, the bot now offers to join it with a reaction prompt (👍 to join, ❌ to skip) instead of just showing a warning message that required manually typing `!worktree switch`
27
+
28
+ ### Fixed
29
+ - **Task list 🔽 emoji not preserved when bumped** - Fixed issues where the collapse/expand toggle emoji would disappear or get stuck on the wrong post:
30
+ - When a task list is bumped to the bottom, the new post now gets the 🔽 emoji via `createInteractivePost`
31
+ - When a task post is repurposed for other content, the emoji is removed from the old post before reuse
32
+ - Added `removeReaction` method to platform client interface for proper emoji cleanup
33
+ - **WorktreeMode type inconsistency** - Aligned the WorktreeMode type definition across the codebase to include 'off' mode
34
+
35
+ ## [0.23.0] - 2026-01-02
36
+
37
+ ### Added
38
+ - **Sticky message status bar** - Added a compact status line to the channel sticky message showing system-level info:
39
+ - Bot version (`v0.22.0`)
40
+ - Active sessions count (`3/5 sessions`)
41
+ - Permission mode (`🔐 Interactive` or `⚡ Auto`)
42
+ - Worktree mode (`🌿 Worktree: always/never`) - only shown if not default 'prompt'
43
+ - Chrome status (`🌐 Chrome`) - only when enabled
44
+ - Debug mode (`🐛 Debug`) - only when enabled
45
+ - Battery level (`🔋 85%` or `🔌 AC`) - macOS and Linux
46
+ - Bot uptime (`⏱️ 2h15m`) - how long the bot has been running
47
+ - Working directory (`📂 ~/projects`)
48
+ - Hostname (`💻 hostname`) - machine name for identification
49
+
10
50
  ## [0.22.1] - 2026-01-01
11
51
 
12
52
  ### Fixed
package/dist/index.js CHANGED
@@ -13388,6 +13388,9 @@ class MattermostClient extends EventEmitter {
13388
13388
  emoji_name: emojiName
13389
13389
  });
13390
13390
  }
13391
+ async removeReaction(postId, emojiName) {
13392
+ await this.api("DELETE", `/users/${this.botUserId}/posts/${postId}/reactions/${emojiName}`);
13393
+ }
13391
13394
  async createInteractivePost(message, reactions, threadId) {
13392
13395
  const post = await this.createPost(message, threadId);
13393
13396
  for (const emoji of reactions) {
@@ -13988,10 +13991,13 @@ function stopTyping(session) {
13988
13991
  async function bumpTasksToBottomWithContent(session, newContent, registerPost) {
13989
13992
  const oldTasksPostId = session.tasksPostId;
13990
13993
  const oldTasksContent = session.lastTasksContent;
13994
+ try {
13995
+ await session.platform.removeReaction(oldTasksPostId, TASK_TOGGLE_EMOJIS[0]);
13996
+ } catch {}
13991
13997
  await session.platform.updatePost(oldTasksPostId, newContent);
13992
13998
  registerPost(oldTasksPostId, session.threadId);
13993
13999
  if (oldTasksContent) {
13994
- const newTasksPost = await session.platform.createPost(oldTasksContent, session.threadId);
14000
+ const newTasksPost = await session.platform.createInteractivePost(oldTasksContent, [TASK_TOGGLE_EMOJIS[0]], session.threadId);
13995
14001
  session.tasksPostId = newTasksPost.id;
13996
14002
  registerPost(newTasksPost.id, session.threadId);
13997
14003
  } else {
@@ -14008,7 +14014,7 @@ async function bumpTasksToBottom(session, registerPost) {
14008
14014
  }
14009
14015
  try {
14010
14016
  await session.platform.deletePost(session.tasksPostId);
14011
- const newPost = await session.platform.createPost(session.lastTasksContent, session.threadId);
14017
+ const newPost = await session.platform.createInteractivePost(session.lastTasksContent, [TASK_TOGGLE_EMOJIS[0]], session.threadId);
14012
14018
  session.tasksPostId = newPost.id;
14013
14019
  if (registerPost) {
14014
14020
  registerPost(newPost.id, session.threadId);
@@ -15056,6 +15062,7 @@ function formatEvent(session, e, ctx) {
15056
15062
  ctx.flush(session);
15057
15063
  session.currentPostId = null;
15058
15064
  session.pendingContent = "";
15065
+ updateUsageStats(session, e, ctx);
15059
15066
  return null;
15060
15067
  }
15061
15068
  case "system":
@@ -15266,6 +15273,74 @@ async function postCurrentQuestion(session, ctx) {
15266
15273
  session.pendingQuestionSet.currentPostId = post.id;
15267
15274
  ctx.registerPost(post.id, session.threadId);
15268
15275
  }
15276
+ function getModelDisplayName(modelId) {
15277
+ if (modelId.includes("opus-4-5") || modelId.includes("opus-4.5"))
15278
+ return "Opus 4.5";
15279
+ if (modelId.includes("opus-4"))
15280
+ return "Opus 4";
15281
+ if (modelId.includes("opus"))
15282
+ return "Opus";
15283
+ if (modelId.includes("sonnet-4"))
15284
+ return "Sonnet 4";
15285
+ if (modelId.includes("sonnet-3-5") || modelId.includes("sonnet-3.5"))
15286
+ return "Sonnet 3.5";
15287
+ if (modelId.includes("sonnet"))
15288
+ return "Sonnet";
15289
+ if (modelId.includes("haiku-4-5") || modelId.includes("haiku-4.5"))
15290
+ return "Haiku 4.5";
15291
+ if (modelId.includes("haiku"))
15292
+ return "Haiku";
15293
+ const match = modelId.match(/claude-(\w+)/);
15294
+ return match ? match[1].charAt(0).toUpperCase() + match[1].slice(1) : modelId;
15295
+ }
15296
+ function updateUsageStats(session, event, ctx) {
15297
+ const result = event;
15298
+ if (!result.modelUsage)
15299
+ return;
15300
+ let primaryModel = "";
15301
+ let highestCost = 0;
15302
+ let contextWindowSize = 200000;
15303
+ const modelUsage = {};
15304
+ let totalTokensUsed = 0;
15305
+ for (const [modelId, usage] of Object.entries(result.modelUsage)) {
15306
+ modelUsage[modelId] = {
15307
+ inputTokens: usage.inputTokens,
15308
+ outputTokens: usage.outputTokens,
15309
+ cacheReadInputTokens: usage.cacheReadInputTokens,
15310
+ cacheCreationInputTokens: usage.cacheCreationInputTokens,
15311
+ contextWindow: usage.contextWindow,
15312
+ costUSD: usage.costUSD
15313
+ };
15314
+ totalTokensUsed += usage.inputTokens + usage.outputTokens + usage.cacheReadInputTokens + usage.cacheCreationInputTokens;
15315
+ if (usage.costUSD > highestCost) {
15316
+ highestCost = usage.costUSD;
15317
+ primaryModel = modelId;
15318
+ contextWindowSize = usage.contextWindow;
15319
+ }
15320
+ }
15321
+ const usageStats = {
15322
+ primaryModel,
15323
+ modelDisplayName: getModelDisplayName(primaryModel),
15324
+ contextWindowSize,
15325
+ totalTokensUsed,
15326
+ totalCostUSD: result.total_cost_usd || 0,
15327
+ modelUsage,
15328
+ lastUpdated: new Date
15329
+ };
15330
+ session.usageStats = usageStats;
15331
+ if (ctx.debug) {
15332
+ console.log(`[DEBUG] Updated usage stats: ${usageStats.modelDisplayName}, ` + `${usageStats.totalTokensUsed}/${usageStats.contextWindowSize} tokens, ` + `$${usageStats.totalCostUSD.toFixed(4)}`);
15333
+ }
15334
+ if (!session.statusBarTimer) {
15335
+ const STATUS_BAR_UPDATE_INTERVAL = 30000;
15336
+ session.statusBarTimer = setInterval(() => {
15337
+ if (session.claude.isRunning()) {
15338
+ ctx.updateSessionHeader(session).catch(() => {});
15339
+ }
15340
+ }, STATUS_BAR_UPDATE_INTERVAL);
15341
+ }
15342
+ ctx.updateSessionHeader(session).catch(() => {});
15343
+ }
15269
15344
 
15270
15345
  // src/session/reactions.ts
15271
15346
  async function handleQuestionReaction(session, postId, emojiName, username, ctx) {
@@ -15404,6 +15479,34 @@ async function handleTaskToggleReaction(session, ctx) {
15404
15479
  }
15405
15480
  return true;
15406
15481
  }
15482
+ async function handleExistingWorktreeReaction(session, postId, emojiName, username, ctx) {
15483
+ const pending = session.pendingExistingWorktreePrompt;
15484
+ if (!pending || pending.postId !== postId) {
15485
+ return false;
15486
+ }
15487
+ if (session.startedBy !== username && !session.platform.isUserAllowed(username)) {
15488
+ return false;
15489
+ }
15490
+ const isApprove = isApprovalEmoji(emojiName);
15491
+ const isDeny = isDenialEmoji(emojiName);
15492
+ if (!isApprove && !isDeny) {
15493
+ return false;
15494
+ }
15495
+ const shortPath = pending.worktreePath.replace(process.env.HOME || "", "~");
15496
+ if (isApprove) {
15497
+ await session.platform.updatePost(pending.postId, `\u2705 Joining worktree for branch \`${pending.branch}\` at \`${shortPath}\``);
15498
+ session.pendingExistingWorktreePrompt = undefined;
15499
+ ctx.persistSession(session);
15500
+ await ctx.switchToWorktree(session.threadId, pending.worktreePath, pending.username);
15501
+ console.log(` \uD83C\uDF3F @${username} joined existing worktree ${pending.branch} at ${shortPath}`);
15502
+ } else {
15503
+ await session.platform.updatePost(pending.postId, `\u2705 Continuing in current directory (skipped by @${username})`);
15504
+ session.pendingExistingWorktreePrompt = undefined;
15505
+ ctx.persistSession(session);
15506
+ console.log(` \u274C @${username} skipped joining existing worktree ${pending.branch}`);
15507
+ }
15508
+ return true;
15509
+ }
15407
15510
 
15408
15511
  // src/claude/cli.ts
15409
15512
  import { spawn } from "child_process";
@@ -19333,6 +19436,24 @@ class KeepAliveManager {
19333
19436
  var keepAlive = new KeepAliveManager;
19334
19437
 
19335
19438
  // src/session/commands.ts
19439
+ function formatContextBar(percent) {
19440
+ const totalBlocks = 10;
19441
+ const filledBlocks = Math.round(percent / 100 * totalBlocks);
19442
+ const emptyBlocks = totalBlocks - filledBlocks;
19443
+ let indicator;
19444
+ if (percent < 50) {
19445
+ indicator = "\uD83D\uDFE2";
19446
+ } else if (percent < 75) {
19447
+ indicator = "\uD83D\uDFE1";
19448
+ } else if (percent < 90) {
19449
+ indicator = "\uD83D\uDFE0";
19450
+ } else {
19451
+ indicator = "\uD83D\uDD34";
19452
+ }
19453
+ const filled = "\u2593".repeat(filledBlocks);
19454
+ const empty = "\u2591".repeat(emptyBlocks);
19455
+ return `${indicator}${filled}${empty}`;
19456
+ }
19336
19457
  async function cancelSession(session, username, ctx) {
19337
19458
  const shortId = session.threadId.substring(0, 8);
19338
19459
  console.log(` \uD83D\uDED1 Session (${shortId}\u2026) cancelled by @${username}`);
@@ -19516,6 +19637,14 @@ async function updateSessionHeader(session, ctx) {
19516
19637
  const permMode = isInteractive ? "\uD83D\uDD10 Interactive" : "\u26A1 Auto";
19517
19638
  const otherParticipants = [...session.sessionAllowedUsers].filter((u) => u !== session.startedBy).map((u) => `@${u}`).join(", ");
19518
19639
  const statusItems = [];
19640
+ if (session.usageStats) {
19641
+ const stats = session.usageStats;
19642
+ statusItems.push(`\`\uD83E\uDD16 ${stats.modelDisplayName}\``);
19643
+ const contextPercent = Math.round(stats.totalTokensUsed / stats.contextWindowSize * 100);
19644
+ const contextBar = formatContextBar(contextPercent);
19645
+ statusItems.push(`\`${contextBar} ${contextPercent}%\``);
19646
+ statusItems.push(`\`\uD83D\uDCB0 $${stats.totalCostUSD.toFixed(2)}\``);
19647
+ }
19519
19648
  statusItems.push(`\`${session.sessionNumber}/${ctx.maxSessions}\``);
19520
19649
  statusItems.push(`\`${permMode}\``);
19521
19650
  if (ctx.chromeEnabled) {
@@ -19705,7 +19834,8 @@ async function startSession(options, username, displayName, replyToPostId, platf
19705
19834
  inProgressTaskStart: null,
19706
19835
  activeToolStarts: new Map,
19707
19836
  firstPrompt: options.prompt,
19708
- messageCount: 0
19837
+ messageCount: 0,
19838
+ statusBarTimer: null
19709
19839
  };
19710
19840
  ctx.sessions.set(sessionId, session);
19711
19841
  ctx.registerPost(post.id, actualThreadId);
@@ -19834,7 +19964,8 @@ Please start a new session.`, state.threadId);
19834
19964
  needsContextPromptOnNextMessage: state.needsContextPromptOnNextMessage,
19835
19965
  sessionTitle: state.sessionTitle,
19836
19966
  sessionDescription: state.sessionDescription,
19837
- messageCount: state.messageCount ?? 0
19967
+ messageCount: state.messageCount ?? 0,
19968
+ statusBarTimer: null
19838
19969
  };
19839
19970
  ctx.sessions.set(sessionId, session);
19840
19971
  if (state.sessionStartPostId) {
@@ -19927,6 +20058,10 @@ async function handleExit(sessionId, code, ctx) {
19927
20058
  clearTimeout(session.updateTimer);
19928
20059
  session.updateTimer = null;
19929
20060
  }
20061
+ if (session.statusBarTimer) {
20062
+ clearInterval(session.statusBarTimer);
20063
+ session.statusBarTimer = null;
20064
+ }
19930
20065
  ctx.sessions.delete(session.sessionId);
19931
20066
  keepAlive.sessionEnded();
19932
20067
  return;
@@ -19938,6 +20073,10 @@ async function handleExit(sessionId, code, ctx) {
19938
20073
  clearTimeout(session.updateTimer);
19939
20074
  session.updateTimer = null;
19940
20075
  }
20076
+ if (session.statusBarTimer) {
20077
+ clearInterval(session.statusBarTimer);
20078
+ session.statusBarTimer = null;
20079
+ }
19941
20080
  ctx.persistSession(session);
19942
20081
  ctx.sessions.delete(session.sessionId);
19943
20082
  for (const [postId, tid] of ctx.postIndex.entries()) {
@@ -19960,6 +20099,10 @@ async function handleExit(sessionId, code, ctx) {
19960
20099
  clearTimeout(session.updateTimer);
19961
20100
  session.updateTimer = null;
19962
20101
  }
20102
+ if (session.statusBarTimer) {
20103
+ clearInterval(session.statusBarTimer);
20104
+ session.statusBarTimer = null;
20105
+ }
19963
20106
  ctx.sessions.delete(session.sessionId);
19964
20107
  keepAlive.sessionEnded();
19965
20108
  try {
@@ -19974,6 +20117,10 @@ async function handleExit(sessionId, code, ctx) {
19974
20117
  clearTimeout(session.updateTimer);
19975
20118
  session.updateTimer = null;
19976
20119
  }
20120
+ if (session.statusBarTimer) {
20121
+ clearInterval(session.statusBarTimer);
20122
+ session.statusBarTimer = null;
20123
+ }
19977
20124
  await ctx.flush(session);
19978
20125
  if (code !== 0 && code !== null) {
19979
20126
  await session.platform.createPost(`**[Exited: ${code}]**`, session.threadId);
@@ -20302,7 +20449,17 @@ async function createAndSwitchToWorktree(session, branch, username, options) {
20302
20449
  const repoRoot = await getRepositoryRoot(session.workingDir);
20303
20450
  const existing = await findWorktreeByBranch(repoRoot, branch);
20304
20451
  if (existing && !existing.isMain) {
20305
- await session.platform.createPost(`\u26A0\uFE0F Worktree for branch \`${branch}\` already exists at \`${existing.path}\`. Use \`!worktree switch ${branch}\` to switch to it.`, session.threadId);
20452
+ const shortPath = existing.path.replace(process.env.HOME || "", "~");
20453
+ const post = await session.platform.createInteractivePost(`\uD83C\uDF3F **Worktree for branch \`${branch}\` already exists** at \`${shortPath}\`.
20454
+ ` + `React with \uD83D\uDC4D to join this worktree, or \u274C to continue in the current directory.`, ["+1", "x"], session.threadId);
20455
+ session.pendingExistingWorktreePrompt = {
20456
+ postId: post.id,
20457
+ branch,
20458
+ worktreePath: existing.path,
20459
+ username
20460
+ };
20461
+ options.registerPost(post.id, session.threadId);
20462
+ options.persistSession(session);
20306
20463
  return;
20307
20464
  }
20308
20465
  const shortId = session.threadId.substring(0, 8);
@@ -20571,6 +20728,8 @@ function clearContextPromptTimeout(pending) {
20571
20728
  }
20572
20729
 
20573
20730
  // src/session/sticky-message.ts
20731
+ import { hostname } from "os";
20732
+ var botStartedAt = new Date;
20574
20733
  var stickyPostIds = new Map;
20575
20734
  var needsBump = new Map;
20576
20735
  var updateLocks = new Map;
@@ -20613,6 +20772,35 @@ function getSessionTopic(session) {
20613
20772
  }
20614
20773
  return formatTopicFromPrompt(session.firstPrompt);
20615
20774
  }
20775
+ async function buildStatusBar(sessionCount, config) {
20776
+ const items = [];
20777
+ items.push(`\`v${VERSION}\``);
20778
+ items.push(`\`${sessionCount}/${config.maxSessions} sessions\``);
20779
+ const permMode = config.skipPermissions ? "\u26A1 Auto" : "\uD83D\uDD10 Interactive";
20780
+ items.push(`\`${permMode}\``);
20781
+ if (config.worktreeMode === "require") {
20782
+ items.push("`\uD83C\uDF3F Worktree: require`");
20783
+ } else if (config.worktreeMode === "off") {
20784
+ items.push("`\uD83C\uDF3F Worktree: off`");
20785
+ }
20786
+ if (config.chromeEnabled) {
20787
+ items.push("`\uD83C\uDF10 Chrome`");
20788
+ }
20789
+ if (config.debug) {
20790
+ items.push("`\uD83D\uDC1B Debug`");
20791
+ }
20792
+ const battery = await formatBatteryStatus();
20793
+ if (battery) {
20794
+ items.push(`\`${battery}\``);
20795
+ }
20796
+ const uptime = formatUptime(botStartedAt);
20797
+ items.push(`\`\u23F1\uFE0F ${uptime}\``);
20798
+ const shortDir = config.workingDir.replace(process.env.HOME || "", "~");
20799
+ items.push(`\`\uD83D\uDCC2 ${shortDir}\``);
20800
+ const host = hostname();
20801
+ items.push(`\`\uD83D\uDCBB ${host}\``);
20802
+ return items.join(" \xB7 ");
20803
+ }
20616
20804
  function formatTopicFromPrompt(prompt) {
20617
20805
  if (!prompt)
20618
20806
  return "_No topic_";
@@ -20626,13 +20814,16 @@ function formatTopicFromPrompt(prompt) {
20626
20814
  }
20627
20815
  return cleaned || "_No topic_";
20628
20816
  }
20629
- function buildStickyMessage(sessions, platformId) {
20817
+ async function buildStickyMessage(sessions, platformId, config) {
20630
20818
  const platformSessions = [...sessions.values()].filter((s) => s.platformId === platformId);
20819
+ const statusBar = await buildStatusBar(platformSessions.length, config);
20631
20820
  if (platformSessions.length === 0) {
20632
20821
  return [
20633
20822
  "---",
20634
20823
  "**Active Claude Threads**",
20635
20824
  "",
20825
+ statusBar,
20826
+ "",
20636
20827
  "_No active sessions_",
20637
20828
  "",
20638
20829
  "_Mention me to start a session_ \xB7 `npm i -g claude-threads`"
@@ -20644,6 +20835,8 @@ function buildStickyMessage(sessions, platformId) {
20644
20835
  const lines = [
20645
20836
  "---",
20646
20837
  `**Active Claude Threads** (${count})`,
20838
+ "",
20839
+ statusBar,
20647
20840
  ""
20648
20841
  ];
20649
20842
  for (const session of platformSessions) {
@@ -20664,7 +20857,7 @@ function buildStickyMessage(sessions, platformId) {
20664
20857
  `);
20665
20858
  }
20666
20859
  var DEBUG = process.env.DEBUG === "1";
20667
- async function updateStickyMessage(platform, sessions) {
20860
+ async function updateStickyMessage(platform, sessions, config) {
20668
20861
  const platformId = platform.platformId;
20669
20862
  const pendingUpdate = updateLocks.get(platformId);
20670
20863
  if (pendingUpdate) {
@@ -20676,14 +20869,14 @@ async function updateStickyMessage(platform, sessions) {
20676
20869
  });
20677
20870
  updateLocks.set(platformId, lock);
20678
20871
  try {
20679
- await updateStickyMessageImpl(platform, sessions);
20872
+ await updateStickyMessageImpl(platform, sessions, config);
20680
20873
  } finally {
20681
20874
  if (releaseLock)
20682
20875
  releaseLock();
20683
20876
  updateLocks.delete(platformId);
20684
20877
  }
20685
20878
  }
20686
- async function updateStickyMessageImpl(platform, sessions) {
20879
+ async function updateStickyMessageImpl(platform, sessions, config) {
20687
20880
  if (DEBUG) {
20688
20881
  const platformSessions = [...sessions.values()].filter((s) => s.platformId === platform.platformId);
20689
20882
  console.log(`[sticky] updateStickyMessage called for ${platform.platformId}, ${platformSessions.length} sessions`);
@@ -20691,7 +20884,7 @@ async function updateStickyMessageImpl(platform, sessions) {
20691
20884
  console.log(`[sticky] - ${s.sessionId}: title="${s.sessionTitle}" firstPrompt="${s.firstPrompt?.substring(0, 30)}..."`);
20692
20885
  }
20693
20886
  }
20694
- const content = buildStickyMessage(sessions, platform.platformId);
20887
+ const content = await buildStickyMessage(sessions, platform.platformId, config);
20695
20888
  const existingPostId = stickyPostIds.get(platform.platformId);
20696
20889
  const shouldBump = needsBump.get(platform.platformId) ?? false;
20697
20890
  if (DEBUG) {
@@ -20762,8 +20955,8 @@ async function updateStickyMessageImpl(platform, sessions) {
20762
20955
  console.error(` \u26A0\uFE0F Failed to update sticky message for ${platform.platformId}:`, err);
20763
20956
  }
20764
20957
  }
20765
- async function updateAllStickyMessages(platforms, sessions) {
20766
- const updates = [...platforms.values()].map((platform) => updateStickyMessage(platform, sessions));
20958
+ async function updateAllStickyMessages(platforms, sessions, config) {
20959
+ const updates = [...platforms.values()].map((platform) => updateStickyMessage(platform, sessions, config));
20767
20960
  await Promise.all(updates);
20768
20961
  }
20769
20962
  function markNeedsBump(platformId) {
@@ -20985,6 +21178,15 @@ class SessionManager {
20985
21178
  await handleWorktreeSkip(session, username, (s) => this.persistSession(s), (s, q) => this.offerContextPrompt(s, q));
20986
21179
  return;
20987
21180
  }
21181
+ if (session.pendingExistingWorktreePrompt?.postId === postId) {
21182
+ const handled = await handleExistingWorktreeReaction(session, postId, emojiName, username, {
21183
+ ...this.getReactionContext(),
21184
+ switchToWorktree: (tid, branchOrPath, user) => this.switchToWorktree(tid, branchOrPath, user),
21185
+ persistSession: (s) => this.persistSession(s)
21186
+ });
21187
+ if (handled)
21188
+ return;
21189
+ }
20988
21190
  if (session.pendingContextPrompt?.postId === postId) {
20989
21191
  await this.handleContextPromptReaction(session, emojiName, username);
20990
21192
  return;
@@ -21189,7 +21391,14 @@ class SessionManager {
21189
21391
  await updateSessionHeader(session, this.getCommandContext());
21190
21392
  }
21191
21393
  async updateStickyMessage() {
21192
- await updateAllStickyMessages(this.platforms, this.sessions);
21394
+ await updateAllStickyMessages(this.platforms, this.sessions, {
21395
+ maxSessions: MAX_SESSIONS,
21396
+ chromeEnabled: this.chromeEnabled,
21397
+ skipPermissions: this.skipPermissions,
21398
+ worktreeMode: this.worktreeMode,
21399
+ workingDir: this.workingDir,
21400
+ debug: this.debug
21401
+ });
21193
21402
  }
21194
21403
  async initialize() {
21195
21404
  initialize(this.sessionStore);
@@ -21345,7 +21554,8 @@ class SessionManager {
21345
21554
  startTyping: (s) => this.startTyping(s),
21346
21555
  stopTyping: (s) => this.stopTyping(s),
21347
21556
  offerContextPrompt: (s, q) => this.offerContextPrompt(s, q),
21348
- appendSystemPrompt: CHAT_PLATFORM_PROMPT
21557
+ appendSystemPrompt: CHAT_PLATFORM_PROMPT,
21558
+ registerPost: (postId, tid) => this.registerPost(postId, tid)
21349
21559
  });
21350
21560
  }
21351
21561
  async switchToWorktree(threadId, branchOrPath, username) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-threads",
3
- "version": "0.22.1",
3
+ "version": "0.24.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",