mattermost-claude-code 0.10.2 β†’ 0.10.3

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,23 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.10.3] - 2025-12-28
11
+
12
+ ### Changed
13
+ - **Improved task list UX**
14
+ - Progress indicator: `πŸ“‹ **Tasks** (2/5 Β· 40%)`
15
+ - Elapsed time for in-progress tasks: `πŸ”„ **Running tests...** (45s)`
16
+ - Better pending icon: `β—‹` instead of `⬜` (no longer overlaps)
17
+ - **Tool output now shows elapsed time**
18
+ - Long-running tools (β‰₯3s) show completion time: `↳ βœ“ (12s)`
19
+ - Errors also show timing: `↳ ❌ Error (5s)`
20
+
21
+ ### Fixed
22
+ - **Paused sessions now resume on new message** - messages to paused sessions were being ignored
23
+ - After ⏸️ interrupt, sending a new message in the thread now resumes the session
24
+ - Previously messages without @mention were ignored because the session was removed from memory
25
+ - Added `hasPausedSession()`, `resumePausedSession()`, and `getPersistedSession()` methods
26
+
10
27
  ## [0.10.2] - 2025-12-28
11
28
 
12
29
  ### Changed
@@ -1,6 +1,7 @@
1
1
  import { ClaudeCli } from './cli.js';
2
2
  import { MattermostClient } from '../mattermost/client.js';
3
3
  import { MattermostFile } from '../mattermost/types.js';
4
+ import { PersistedSession } from '../persistence/session-store.js';
4
5
  interface QuestionOption {
5
6
  label: string;
6
7
  description: string;
@@ -59,6 +60,8 @@ interface Session {
59
60
  isRestarting: boolean;
60
61
  isResumed: boolean;
61
62
  wasInterrupted: boolean;
63
+ inProgressTaskStart: number | null;
64
+ activeToolStarts: Map<string, number>;
62
65
  }
63
66
  export declare class SessionManager {
64
67
  private mattermost;
@@ -141,6 +144,20 @@ export declare class SessionManager {
141
144
  isInSessionThread(threadRoot: string): boolean;
142
145
  /** Send a follow-up message to an existing session */
143
146
  sendFollowUp(threadId: string, message: string, files?: MattermostFile[]): Promise<void>;
147
+ /**
148
+ * Check if there's a paused (persisted but not active) session for this thread.
149
+ * This is used to detect when we should resume a session instead of ignoring the message.
150
+ */
151
+ hasPausedSession(threadId: string): boolean;
152
+ /**
153
+ * Resume a paused session and send a message to it.
154
+ * Called when a user sends a message to a thread with a paused session.
155
+ */
156
+ resumePausedSession(threadId: string, message: string, files?: MattermostFile[]): Promise<void>;
157
+ /**
158
+ * Get persisted session info for access control checks
159
+ */
160
+ getPersistedSession(threadId: string): PersistedSession | undefined;
144
161
  /** Kill a specific session */
145
162
  killSession(threadId: string, unpersist?: boolean): void;
146
163
  /** Cancel a session with user feedback */
@@ -146,6 +146,8 @@ export class SessionManager {
146
146
  isRestarting: false,
147
147
  isResumed: true,
148
148
  wasInterrupted: false,
149
+ inProgressTaskStart: null,
150
+ activeToolStarts: new Map(),
149
151
  };
150
152
  // Register session
151
153
  this.sessions.set(state.threadId, session);
@@ -322,6 +324,8 @@ export class SessionManager {
322
324
  isRestarting: false,
323
325
  isResumed: false,
324
326
  wasInterrupted: false,
327
+ inProgressTaskStart: null,
328
+ activeToolStarts: new Map(),
325
329
  };
326
330
  // Register session
327
331
  this.sessions.set(actualThreadId, session);
@@ -469,8 +473,20 @@ export class SessionManager {
469
473
  }
470
474
  return;
471
475
  }
472
- // Format tasks nicely
473
- let message = 'πŸ“‹ **Tasks**\n\n';
476
+ // Count progress
477
+ const completed = todos.filter(t => t.status === 'completed').length;
478
+ const total = todos.length;
479
+ const pct = Math.round((completed / total) * 100);
480
+ // Check if there's an in_progress task and track timing
481
+ const hasInProgress = todos.some(t => t.status === 'in_progress');
482
+ if (hasInProgress && !session.inProgressTaskStart) {
483
+ session.inProgressTaskStart = Date.now();
484
+ }
485
+ else if (!hasInProgress) {
486
+ session.inProgressTaskStart = null;
487
+ }
488
+ // Format tasks nicely with progress header
489
+ let message = `πŸ“‹ **Tasks** (${completed}/${total} Β· ${pct}%)\n\n`;
474
490
  for (const todo of todos) {
475
491
  let icon;
476
492
  let text;
@@ -479,12 +495,21 @@ export class SessionManager {
479
495
  icon = 'βœ…';
480
496
  text = `~~${todo.content}~~`;
481
497
  break;
482
- case 'in_progress':
498
+ case 'in_progress': {
483
499
  icon = 'πŸ”„';
484
- text = `**${todo.activeForm}**`;
500
+ // Add elapsed time if we have a start time
501
+ let elapsed = '';
502
+ if (session.inProgressTaskStart) {
503
+ const secs = Math.round((Date.now() - session.inProgressTaskStart) / 1000);
504
+ if (secs >= 5) { // Only show if >= 5 seconds
505
+ elapsed = ` (${secs}s)`;
506
+ }
507
+ }
508
+ text = `**${todo.activeForm}**${elapsed}`;
485
509
  break;
510
+ }
486
511
  default: // pending
487
- icon = '⬜';
512
+ icon = 'β—‹';
488
513
  text = todo.content;
489
514
  }
490
515
  message += `${icon} ${text}\n`;
@@ -770,12 +795,30 @@ export class SessionManager {
770
795
  }
771
796
  case 'tool_use': {
772
797
  const tool = e.tool_use;
798
+ // Track tool start time for elapsed display
799
+ if (tool.id) {
800
+ session.activeToolStarts.set(tool.id, Date.now());
801
+ }
773
802
  return this.formatToolUse(tool.name, tool.input || {}) || null;
774
803
  }
775
804
  case 'tool_result': {
776
805
  const result = e.tool_result;
806
+ // Calculate elapsed time
807
+ let elapsed = '';
808
+ if (result.tool_use_id) {
809
+ const startTime = session.activeToolStarts.get(result.tool_use_id);
810
+ if (startTime) {
811
+ const secs = Math.round((Date.now() - startTime) / 1000);
812
+ if (secs >= 3) { // Only show if >= 3 seconds
813
+ elapsed = ` (${secs}s)`;
814
+ }
815
+ session.activeToolStarts.delete(result.tool_use_id);
816
+ }
817
+ }
777
818
  if (result.is_error)
778
- return ` ↳ ❌ Error`;
819
+ return ` ↳ ❌ Error${elapsed}`;
820
+ if (elapsed)
821
+ return ` ↳ βœ“${elapsed}`;
779
822
  return null;
780
823
  }
781
824
  case 'result': {
@@ -1089,6 +1132,52 @@ export class SessionManager {
1089
1132
  session.lastActivityAt = new Date();
1090
1133
  this.startTyping(session);
1091
1134
  }
1135
+ /**
1136
+ * Check if there's a paused (persisted but not active) session for this thread.
1137
+ * This is used to detect when we should resume a session instead of ignoring the message.
1138
+ */
1139
+ hasPausedSession(threadId) {
1140
+ // If there's an active session, it's not paused
1141
+ if (this.sessions.has(threadId))
1142
+ return false;
1143
+ // Check persistence
1144
+ const persisted = this.sessionStore.load();
1145
+ return persisted.has(threadId);
1146
+ }
1147
+ /**
1148
+ * Resume a paused session and send a message to it.
1149
+ * Called when a user sends a message to a thread with a paused session.
1150
+ */
1151
+ async resumePausedSession(threadId, message, files) {
1152
+ const persisted = this.sessionStore.load();
1153
+ const state = persisted.get(threadId);
1154
+ if (!state) {
1155
+ console.log(` [resume] No persisted session found for ${threadId.substring(0, 8)}...`);
1156
+ return;
1157
+ }
1158
+ const shortId = threadId.substring(0, 8);
1159
+ console.log(` πŸ”„ Resuming paused session ${shortId}... for new message`);
1160
+ // Resume the session (similar to initialize() but for a single session)
1161
+ await this.resumeSession(state);
1162
+ // Wait a moment for the session to be ready, then send the message
1163
+ const session = this.sessions.get(threadId);
1164
+ if (session && session.claude.isRunning()) {
1165
+ const content = await this.buildMessageContent(message, files);
1166
+ session.claude.sendMessage(content);
1167
+ session.lastActivityAt = new Date();
1168
+ this.startTyping(session);
1169
+ }
1170
+ else {
1171
+ console.log(` ⚠️ Failed to resume session ${shortId}..., could not send message`);
1172
+ }
1173
+ }
1174
+ /**
1175
+ * Get persisted session info for access control checks
1176
+ */
1177
+ getPersistedSession(threadId) {
1178
+ const persisted = this.sessionStore.load();
1179
+ return persisted.get(threadId);
1180
+ }
1092
1181
  /** Kill a specific session */
1093
1182
  killSession(threadId, unpersist = true) {
1094
1183
  const session = this.sessions.get(threadId);
package/dist/index.js CHANGED
@@ -201,6 +201,33 @@ async function main() {
201
201
  await session.sendFollowUp(threadRoot, content, files);
202
202
  return;
203
203
  }
204
+ // Check for paused session that can be resumed
205
+ if (session.hasPausedSession(threadRoot)) {
206
+ // If message starts with @mention to someone else, ignore it (side conversation)
207
+ const mentionMatch = message.trim().match(/^@([\w.-]+)/);
208
+ if (mentionMatch && mentionMatch[1].toLowerCase() !== mattermost.getBotName().toLowerCase()) {
209
+ return; // Side conversation, don't interrupt
210
+ }
211
+ const content = mattermost.isBotMentioned(message)
212
+ ? mattermost.extractPrompt(message)
213
+ : message.trim();
214
+ // Check if user is allowed in the paused session
215
+ const persistedSession = session.getPersistedSession(threadRoot);
216
+ if (persistedSession) {
217
+ const allowedUsers = new Set(persistedSession.sessionAllowedUsers);
218
+ if (!allowedUsers.has(username) && !mattermost.isUserAllowed(username)) {
219
+ // Not allowed - could request approval but that would require the session to be active
220
+ await mattermost.createPost(`⚠️ @${username} is not authorized to resume this session`, threadRoot);
221
+ return;
222
+ }
223
+ }
224
+ // Get any attached files (images)
225
+ const files = post.metadata?.files;
226
+ if (content || files?.length) {
227
+ await session.resumePausedSession(threadRoot, content, files);
228
+ }
229
+ return;
230
+ }
204
231
  // New session requires @mention
205
232
  if (!mattermost.isBotMentioned(message))
206
233
  return;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mattermost-claude-code",
3
- "version": "0.10.2",
3
+ "version": "0.10.3",
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",