claude-threads 0.16.7 → 0.17.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.17.0] - 2025-12-31
11
+
12
+ ### Added
13
+ - **Sticky task list** - Task list now stays at the bottom of the thread
14
+ - When Claude posts new content, the task list moves below it
15
+ - When you send a follow-up message, the task list moves below your message
16
+ - Task list updates in place without visual noise
17
+ - Mirrors Claude Code CLI behavior where tasks are always at the bottom
18
+
19
+ ### Fixed
20
+ - **Context prompt after restart**: Context prompt now appears after session restarts (worktree creation, `!cd`)
21
+ - Previously, after worktree creation or directory change, the context prompt was skipped
22
+ - Now users can include thread history when Claude restarts in a new directory
23
+ - Added `needsContextPromptOnNextMessage` flag for deferred context prompt (after `!cd`)
24
+
25
+ ## [0.16.8] - 2025-12-31
26
+
27
+ ### Fixed
28
+ - **Context prompt**: Fixed context prompt appearing when starting a session with the first message in a thread
29
+ - The triggering message was incorrectly included in the count, making it show "1 message before this point" when there were none
30
+
31
+ ## [0.16.7] - 2025-12-31
32
+
33
+ ### Fixed
34
+ - **Session resume**: Validate working directory exists before resuming sessions after restart
35
+ - Prevents crashes when a worktree or directory has been deleted
36
+
10
37
  ## [0.16.6] - 2025-12-31
11
38
 
12
39
  ### Added
package/dist/index.js CHANGED
@@ -13395,6 +13395,9 @@ class MattermostClient extends EventEmitter {
13395
13395
  return null;
13396
13396
  }
13397
13397
  }
13398
+ async deletePost(postId) {
13399
+ await this.api("DELETE", `/posts/${postId}`);
13400
+ }
13398
13401
  async getThreadHistory(threadId, options) {
13399
13402
  try {
13400
13403
  const response = await this.api("GET", `/posts/${threadId}/thread`);
@@ -13803,6 +13806,31 @@ function stopTyping(session) {
13803
13806
  session.typingTimer = null;
13804
13807
  }
13805
13808
  }
13809
+ async function bumpTasksToBottomWithContent(session, newContent, registerPost) {
13810
+ const oldTasksPostId = session.tasksPostId;
13811
+ const oldTasksContent = session.lastTasksContent;
13812
+ await session.platform.updatePost(oldTasksPostId, newContent);
13813
+ registerPost(oldTasksPostId, session.threadId);
13814
+ if (oldTasksContent) {
13815
+ const newTasksPost = await session.platform.createPost(oldTasksContent, session.threadId);
13816
+ session.tasksPostId = newTasksPost.id;
13817
+ } else {
13818
+ session.tasksPostId = null;
13819
+ }
13820
+ return oldTasksPostId;
13821
+ }
13822
+ async function bumpTasksToBottom(session) {
13823
+ if (!session.tasksPostId || !session.lastTasksContent) {
13824
+ return;
13825
+ }
13826
+ try {
13827
+ await session.platform.deletePost(session.tasksPostId);
13828
+ const newPost = await session.platform.createPost(session.lastTasksContent, session.threadId);
13829
+ session.tasksPostId = newPost.id;
13830
+ } catch (err) {
13831
+ console.error(" \u26A0\uFE0F Failed to bump tasks to bottom:", err);
13832
+ }
13833
+ }
13806
13834
  async function flush(session, registerPost) {
13807
13835
  if (!session.pendingContent.trim())
13808
13836
  return;
@@ -13825,11 +13853,18 @@ async function flush(session, registerPost) {
13825
13853
  session.currentPostId = null;
13826
13854
  session.pendingContent = remainder;
13827
13855
  if (remainder) {
13828
- const post = await session.platform.createPost(`*(continued)*
13856
+ if (session.tasksPostId && session.lastTasksContent) {
13857
+ const postId = await bumpTasksToBottomWithContent(session, `*(continued)*
13858
+
13859
+ ` + remainder, registerPost);
13860
+ session.currentPostId = postId;
13861
+ } else {
13862
+ const post = await session.platform.createPost(`*(continued)*
13829
13863
 
13830
13864
  ` + remainder, session.threadId);
13831
- session.currentPostId = post.id;
13832
- registerPost(post.id, session.threadId);
13865
+ session.currentPostId = post.id;
13866
+ registerPost(post.id, session.threadId);
13867
+ }
13833
13868
  }
13834
13869
  return;
13835
13870
  }
@@ -13841,9 +13876,14 @@ async function flush(session, registerPost) {
13841
13876
  if (session.currentPostId) {
13842
13877
  await session.platform.updatePost(session.currentPostId, content);
13843
13878
  } else {
13844
- const post = await session.platform.createPost(content, session.threadId);
13845
- session.currentPostId = post.id;
13846
- registerPost(post.id, session.threadId);
13879
+ if (session.tasksPostId && session.lastTasksContent) {
13880
+ const postId = await bumpTasksToBottomWithContent(session, content, registerPost);
13881
+ session.currentPostId = postId;
13882
+ } else {
13883
+ const post = await session.platform.createPost(content, session.threadId);
13884
+ session.currentPostId = post.id;
13885
+ registerPost(post.id, session.threadId);
13886
+ }
13847
13887
  }
13848
13888
  }
13849
13889
 
@@ -14805,7 +14845,9 @@ async function handleTodoWrite(session, input) {
14805
14845
  if (!todos || todos.length === 0) {
14806
14846
  if (session.tasksPostId) {
14807
14847
  try {
14808
- await session.platform.updatePost(session.tasksPostId, "\uD83D\uDCCB ~~Tasks~~ *(completed)*");
14848
+ const completedMsg = "\uD83D\uDCCB ~~Tasks~~ *(completed)*";
14849
+ await session.platform.updatePost(session.tasksPostId, completedMsg);
14850
+ session.lastTasksContent = completedMsg;
14809
14851
  } catch (err) {
14810
14852
  console.error(" \u26A0\uFE0F Failed to update tasks:", err);
14811
14853
  }
@@ -14851,6 +14893,7 @@ async function handleTodoWrite(session, input) {
14851
14893
  message += `${icon} ${text}
14852
14894
  `;
14853
14895
  }
14896
+ session.lastTasksContent = message;
14854
14897
  try {
14855
14898
  if (session.tasksPostId) {
14856
14899
  await session.platform.updatePost(session.tasksPostId, message);
@@ -18773,6 +18816,7 @@ async function changeDirectory(session, newDir, username, ctx) {
18773
18816
  *Claude Code restarted in new directory*`, session.threadId);
18774
18817
  session.lastActivityAt = new Date;
18775
18818
  session.timeoutWarningPosted = false;
18819
+ session.needsContextPromptOnNextMessage = true;
18776
18820
  ctx.persistSession(session);
18777
18821
  }
18778
18822
  async function inviteUser(session, invitedUser, invitedBy, ctx) {
@@ -18996,6 +19040,7 @@ async function startSession(options, username, replyToPostId, platformId, ctx) {
18996
19040
  forceInteractivePermissions: false,
18997
19041
  sessionStartPostId: post.id,
18998
19042
  tasksPostId: null,
19043
+ lastTasksContent: null,
18999
19044
  activeSubagents: new Map,
19000
19045
  updateTimer: null,
19001
19046
  typingTimer: null,
@@ -19035,7 +19080,7 @@ async function startSession(options, username, replyToPostId, platformId, ctx) {
19035
19080
  const content = await ctx.buildMessageContent(options.prompt, session.platform, options.files);
19036
19081
  const messageText = typeof content === "string" ? content : options.prompt;
19037
19082
  if (replyToPostId) {
19038
- const contextOffered = await ctx.offerContextPrompt(session, messageText);
19083
+ const contextOffered = await ctx.offerContextPrompt(session, messageText, replyToPostId);
19039
19084
  if (contextOffered) {
19040
19085
  return;
19041
19086
  }
@@ -19107,6 +19152,7 @@ Please start a new session.`, state.threadId);
19107
19152
  forceInteractivePermissions: state.forceInteractivePermissions,
19108
19153
  sessionStartPostId: state.sessionStartPostId,
19109
19154
  tasksPostId: state.tasksPostId,
19155
+ lastTasksContent: state.lastTasksContent ?? null,
19110
19156
  activeSubagents: new Map,
19111
19157
  updateTimer: null,
19112
19158
  typingTimer: null,
@@ -19120,7 +19166,8 @@ Please start a new session.`, state.threadId);
19120
19166
  pendingWorktreePrompt: state.pendingWorktreePrompt,
19121
19167
  worktreePromptDisabled: state.worktreePromptDisabled,
19122
19168
  queuedPrompt: state.queuedPrompt,
19123
- firstPrompt: state.firstPrompt
19169
+ firstPrompt: state.firstPrompt,
19170
+ needsContextPromptOnNextMessage: state.needsContextPromptOnNextMessage
19124
19171
  };
19125
19172
  ctx.sessions.set(sessionId, session);
19126
19173
  if (state.sessionStartPostId) {
@@ -19148,7 +19195,17 @@ Please start a new session.`, state.threadId);
19148
19195
  async function sendFollowUp(session, message, files, ctx) {
19149
19196
  if (!session.claude.isRunning())
19150
19197
  return;
19198
+ await ctx.bumpTasksToBottom(session);
19151
19199
  const content = await ctx.buildMessageContent(message, session.platform, files);
19200
+ const messageText = typeof content === "string" ? content : message;
19201
+ if (session.needsContextPromptOnNextMessage) {
19202
+ session.needsContextPromptOnNextMessage = false;
19203
+ const contextOffered = await ctx.offerContextPrompt(session, messageText);
19204
+ if (contextOffered) {
19205
+ session.lastActivityAt = new Date;
19206
+ return;
19207
+ }
19208
+ }
19152
19209
  session.claude.sendMessage(content);
19153
19210
  session.lastActivityAt = new Date;
19154
19211
  ctx.startTyping(session);
@@ -19516,7 +19573,7 @@ async function handleWorktreeBranchResponse(session, branchName, username, creat
19516
19573
  await createAndSwitch(session.threadId, branchName, username);
19517
19574
  return true;
19518
19575
  }
19519
- async function handleWorktreeSkip(session, username, persistSession, startTyping2) {
19576
+ async function handleWorktreeSkip(session, username, persistSession, offerContextPrompt) {
19520
19577
  if (!session.pendingWorktreePrompt)
19521
19578
  return;
19522
19579
  if (session.startedBy !== username && !session.platform.isUserAllowed(username)) {
@@ -19535,8 +19592,7 @@ async function handleWorktreeSkip(session, username, persistSession, startTyping
19535
19592
  session.queuedPrompt = undefined;
19536
19593
  persistSession(session);
19537
19594
  if (queuedPrompt && session.claude.isRunning()) {
19538
- session.claude.sendMessage(queuedPrompt);
19539
- startTyping2(session);
19595
+ await offerContextPrompt(session, queuedPrompt);
19540
19596
  }
19541
19597
  }
19542
19598
  async function createAndSwitchToWorktree(session, branch, username, options) {
@@ -19611,11 +19667,9 @@ async function createAndSwitchToWorktree(session, branch, username, options) {
19611
19667
  options.persistSession(session);
19612
19668
  if (session.claude.isRunning()) {
19613
19669
  if (wasPending && queuedPrompt) {
19614
- session.claude.sendMessage(queuedPrompt);
19615
- options.startTyping(session);
19670
+ await options.offerContextPrompt(session, queuedPrompt);
19616
19671
  } else if (!wasPending && session.firstPrompt) {
19617
- session.claude.sendMessage(session.firstPrompt);
19618
- options.startTyping(session);
19672
+ await options.offerContextPrompt(session, session.firstPrompt);
19619
19673
  }
19620
19674
  }
19621
19675
  console.log(` \uD83C\uDF3F Session (${shortId}\u2026) switched to worktree ${branch} at ${shortWorktreePath}`);
@@ -19886,7 +19940,8 @@ class SessionManager {
19886
19940
  shouldPromptForWorktree: (s) => this.shouldPromptForWorktree(s),
19887
19941
  postWorktreePrompt: (s, r) => this.postWorktreePrompt(s, r),
19888
19942
  buildMessageContent: (t, p, f) => this.buildMessageContent(t, p, f),
19889
- offerContextPrompt: (s, q) => this.offerContextPrompt(s, q)
19943
+ offerContextPrompt: (s, q, e) => this.offerContextPrompt(s, q, e),
19944
+ bumpTasksToBottom: (s) => this.bumpTasksToBottom(s)
19890
19945
  };
19891
19946
  }
19892
19947
  getEventContext() {
@@ -19920,7 +19975,8 @@ class SessionManager {
19920
19975
  stopTyping: (s) => this.stopTyping(s),
19921
19976
  persistSession: (s) => this.persistSession(s),
19922
19977
  killSession: (tid) => this.killSession(tid),
19923
- registerPost: (pid, tid) => this.registerPost(pid, tid)
19978
+ registerPost: (pid, tid) => this.registerPost(pid, tid),
19979
+ offerContextPrompt: (s, q) => this.offerContextPrompt(s, q)
19924
19980
  };
19925
19981
  }
19926
19982
  getSessionId(platformId, threadId) {
@@ -19949,7 +20005,7 @@ class SessionManager {
19949
20005
  }
19950
20006
  async handleSessionReaction(session, postId, emojiName, username) {
19951
20007
  if (session.worktreePromptPostId === postId && emojiName === "x") {
19952
- await handleWorktreeSkip(session, username, (s) => this.persistSession(s), (s) => this.startTyping(s));
20008
+ await handleWorktreeSkip(session, username, (s) => this.persistSession(s), (s, q) => this.offerContextPrompt(s, q));
19953
20009
  return;
19954
20010
  }
19955
20011
  if (session.pendingContextPrompt?.postId === postId) {
@@ -20025,8 +20081,8 @@ class SessionManager {
20025
20081
  console.log(` \uD83E\uDDF5 Session (${shortId}\u2026) context prompt timed out, continuing without context`);
20026
20082
  }
20027
20083
  }
20028
- async offerContextPrompt(session, queuedPrompt) {
20029
- const messageCount = await getThreadContextCount(session);
20084
+ async offerContextPrompt(session, queuedPrompt, excludePostId) {
20085
+ const messageCount = await getThreadContextCount(session, excludePostId);
20030
20086
  if (messageCount === 0) {
20031
20087
  if (session.claude.isRunning()) {
20032
20088
  session.claude.sendMessage(queuedPrompt);
@@ -20075,6 +20131,9 @@ class SessionManager {
20075
20131
  async buildMessageContent(text, platform, files) {
20076
20132
  return buildMessageContent(text, platform, files, this.debug);
20077
20133
  }
20134
+ async bumpTasksToBottom(session) {
20135
+ return bumpTasksToBottom(session);
20136
+ }
20078
20137
  async shouldPromptForWorktree(session) {
20079
20138
  return shouldPromptForWorktree(session, this.worktreeMode, (repoRoot, excludeId) => this.hasOtherSessionInRepo(repoRoot, excludeId));
20080
20139
  }
@@ -20118,12 +20177,14 @@ class SessionManager {
20118
20177
  forceInteractivePermissions: session.forceInteractivePermissions,
20119
20178
  sessionStartPostId: session.sessionStartPostId,
20120
20179
  tasksPostId: session.tasksPostId,
20180
+ lastTasksContent: session.lastTasksContent,
20121
20181
  worktreeInfo: session.worktreeInfo,
20122
20182
  pendingWorktreePrompt: session.pendingWorktreePrompt,
20123
20183
  worktreePromptDisabled: session.worktreePromptDisabled,
20124
20184
  queuedPrompt: session.queuedPrompt,
20125
20185
  firstPrompt: session.firstPrompt,
20126
- pendingContextPrompt: persistedContextPrompt
20186
+ pendingContextPrompt: persistedContextPrompt,
20187
+ needsContextPromptOnNextMessage: session.needsContextPromptOnNextMessage
20127
20188
  };
20128
20189
  this.sessionStore.save(session.sessionId, state);
20129
20190
  }
@@ -20259,7 +20320,7 @@ class SessionManager {
20259
20320
  const session = this.findSessionByThreadId(threadId);
20260
20321
  if (!session)
20261
20322
  return;
20262
- await handleWorktreeSkip(session, username, (s) => this.persistSession(s), (s) => this.startTyping(s));
20323
+ await handleWorktreeSkip(session, username, (s) => this.persistSession(s), (s, q) => this.offerContextPrompt(s, q));
20263
20324
  }
20264
20325
  async createAndSwitchToWorktree(threadId, branch, username) {
20265
20326
  const session = this.findSessionByThreadId(threadId);
@@ -20274,7 +20335,8 @@ class SessionManager {
20274
20335
  flush: (s) => this.flush(s),
20275
20336
  persistSession: (s) => this.persistSession(s),
20276
20337
  startTyping: (s) => this.startTyping(s),
20277
- stopTyping: (s) => this.stopTyping(s)
20338
+ stopTyping: (s) => this.stopTyping(s),
20339
+ offerContextPrompt: (s, q) => this.offerContextPrompt(s, q)
20278
20340
  });
20279
20341
  }
20280
20342
  async switchToWorktree(threadId, branchOrPath, username) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-threads",
3
- "version": "0.16.7",
3
+ "version": "0.17.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",