claude-threads 0.41.0 → 0.42.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 +13 -0
- package/README.md +1 -1
- package/dist/index.js +105 -14
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,19 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.42.0] - 2026-01-07
|
|
11
|
+
|
|
12
|
+
### Fixed
|
|
13
|
+
- **Slack message visibility for long sessions** - Add platform-specific message size limits (Slack: 12K, Mattermost: 16K) with error recovery when `updatePost` fails - automatically creates new message instead of silently losing content
|
|
14
|
+
- **ExitPlanMode approval on Slack** - Fix emoji reaction handling by normalizing `thumbsup` → `+1` across platforms
|
|
15
|
+
|
|
16
|
+
### Added
|
|
17
|
+
- **`!approve` / `!yes` commands** - Text-based alternative to 👍 reaction for plan approval
|
|
18
|
+
- **Plan mode status in session header** - Shows 📋 Plan pending or 🔨 Implementing status
|
|
19
|
+
|
|
20
|
+
### Changed
|
|
21
|
+
- **User follow-up message handling** - Reset `currentPostId` on user follow-up messages so Claude's responses start in fresh messages with proper code block closure
|
|
22
|
+
|
|
10
23
|
## [0.41.0] - 2026-01-07
|
|
11
24
|
|
|
12
25
|
### Changed
|
package/README.md
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
[](https://www.npmjs.com/package/claude-threads)
|
|
10
10
|
[](https://www.npmjs.com/package/claude-threads)
|
|
11
11
|
[](https://github.com/anneschuth/claude-threads/actions/workflows/ci.yml)
|
|
12
|
-
[](https://github.com/anneschuth/claude-threads/actions/workflows/ci.yml)
|
|
13
13
|
[](https://opensource.org/licenses/Apache-2.0)
|
|
14
14
|
[](https://bun.sh/)
|
|
15
15
|
[](https://www.typescriptlang.org/)
|
package/dist/index.js
CHANGED
|
@@ -42768,6 +42768,9 @@ class MattermostClient extends EventEmitter {
|
|
|
42768
42768
|
const response = await this.api("GET", `/channels/${this.channelId}/pinned`);
|
|
42769
42769
|
return response.order || [];
|
|
42770
42770
|
}
|
|
42771
|
+
getMessageLimits() {
|
|
42772
|
+
return { maxLength: 16000, hardThreshold: 14000 };
|
|
42773
|
+
}
|
|
42771
42774
|
async getThreadHistory(threadId, options) {
|
|
42772
42775
|
try {
|
|
42773
42776
|
const response = await this.api("GET", `/posts/${threadId}/thread`);
|
|
@@ -43065,6 +43068,30 @@ function getPlatformIcon(platformType) {
|
|
|
43065
43068
|
return "\uD83D\uDCAC";
|
|
43066
43069
|
}
|
|
43067
43070
|
}
|
|
43071
|
+
function normalizeEmojiName(emojiName) {
|
|
43072
|
+
const name = emojiName.replace(/^:|:$/g, "");
|
|
43073
|
+
const aliases = {
|
|
43074
|
+
thumbsup: "+1",
|
|
43075
|
+
thumbs_up: "+1",
|
|
43076
|
+
thumbsdown: "-1",
|
|
43077
|
+
thumbs_down: "-1",
|
|
43078
|
+
heavy_check_mark: "white_check_mark",
|
|
43079
|
+
x: "x",
|
|
43080
|
+
cross_mark: "x",
|
|
43081
|
+
heavy_multiplication_x: "x",
|
|
43082
|
+
pause_button: "pause",
|
|
43083
|
+
double_vertical_bar: "pause",
|
|
43084
|
+
play_button: "arrow_forward",
|
|
43085
|
+
stop_button: "stop",
|
|
43086
|
+
octagonal_sign: "stop",
|
|
43087
|
+
"1": "one",
|
|
43088
|
+
"2": "two",
|
|
43089
|
+
"3": "three",
|
|
43090
|
+
"4": "four",
|
|
43091
|
+
"5": "five"
|
|
43092
|
+
};
|
|
43093
|
+
return aliases[name.toLowerCase()] ?? name;
|
|
43094
|
+
}
|
|
43068
43095
|
function convertMarkdownToSlack(content) {
|
|
43069
43096
|
const codeBlocks = [];
|
|
43070
43097
|
const CODE_BLOCK_PLACEHOLDER = "\x00CODE_BLOCK_";
|
|
@@ -43809,6 +43836,9 @@ class SlackClient extends EventEmitter2 {
|
|
|
43809
43836
|
const response = await this.api("GET", `pins.list?channel=${this.channelId}`);
|
|
43810
43837
|
return (response.items || []).filter((item) => !!item.message).map((item) => item.message.ts);
|
|
43811
43838
|
}
|
|
43839
|
+
getMessageLimits() {
|
|
43840
|
+
return { maxLength: 12000, hardThreshold: 1e4 };
|
|
43841
|
+
}
|
|
43812
43842
|
async getThreadHistory(threadId, options) {
|
|
43813
43843
|
try {
|
|
43814
43844
|
const limit = options?.limit || 100;
|
|
@@ -44538,8 +44568,8 @@ var APPROVAL_EMOJIS = ["+1", "thumbsup"];
|
|
|
44538
44568
|
var DENIAL_EMOJIS = ["-1", "thumbsdown"];
|
|
44539
44569
|
var ALLOW_ALL_EMOJIS = ["white_check_mark", "heavy_check_mark"];
|
|
44540
44570
|
var NUMBER_EMOJIS = ["one", "two", "three", "four"];
|
|
44541
|
-
var CANCEL_EMOJIS = ["x", "octagonal_sign", "stop_sign"];
|
|
44542
|
-
var ESCAPE_EMOJIS = ["double_vertical_bar", "pause_button"];
|
|
44571
|
+
var CANCEL_EMOJIS = ["x", "octagonal_sign", "stop_sign", "stop"];
|
|
44572
|
+
var ESCAPE_EMOJIS = ["double_vertical_bar", "pause_button", "pause"];
|
|
44543
44573
|
var RESUME_EMOJIS = ["arrows_counterclockwise", "arrow_forward", "repeat"];
|
|
44544
44574
|
var TASK_TOGGLE_EMOJIS = ["arrow_down_small", "small_red_triangle_down"];
|
|
44545
44575
|
function isApprovalEmoji(emoji) {
|
|
@@ -45193,8 +45223,7 @@ async function flush(session, registerPost) {
|
|
|
45193
45223
|
}
|
|
45194
45224
|
const formatter = session.platform.getFormatter();
|
|
45195
45225
|
let content = formatter.formatMarkdown(session.pendingContent).trim();
|
|
45196
|
-
const MAX_POST_LENGTH =
|
|
45197
|
-
const HARD_CONTINUATION_THRESHOLD = 14000;
|
|
45226
|
+
const { maxLength: MAX_POST_LENGTH, hardThreshold: HARD_CONTINUATION_THRESHOLD } = session.platform.getMessageLimits();
|
|
45198
45227
|
const shouldBreakEarly = session.currentPostId && content.length > MIN_BREAK_THRESHOLD && shouldFlushEarly(content);
|
|
45199
45228
|
if (session.currentPostId && (content.length > HARD_CONTINUATION_THRESHOLD || shouldBreakEarly)) {
|
|
45200
45229
|
let breakPoint;
|
|
@@ -45228,7 +45257,13 @@ async function flush(session, registerPost) {
|
|
|
45228
45257
|
if (breakInfo && breakInfo.position < content.length) {
|
|
45229
45258
|
breakPoint = breakInfo.position;
|
|
45230
45259
|
} else {
|
|
45231
|
-
await withErrorHandling(() => session.platform.updatePost(session.currentPostId, content), { action: "Update post (no breakpoint)", session });
|
|
45260
|
+
const result = await withErrorHandling(() => session.platform.updatePost(session.currentPostId, content), { action: "Update post (no breakpoint)", session });
|
|
45261
|
+
if (result === undefined) {
|
|
45262
|
+
sessionLog(session).warn("Update failed (no breakpoint), starting new message");
|
|
45263
|
+
session.currentPostId = null;
|
|
45264
|
+
session.currentPostContent = "";
|
|
45265
|
+
return flush(session, registerPost);
|
|
45266
|
+
}
|
|
45232
45267
|
return;
|
|
45233
45268
|
}
|
|
45234
45269
|
}
|
|
@@ -45243,14 +45278,20 @@ async function flush(session, registerPost) {
|
|
|
45243
45278
|
const firstPartWithMarker = remainder ? firstPart + `
|
|
45244
45279
|
|
|
45245
45280
|
` + formatter2.formatItalic("... (continued below)") : firstPart;
|
|
45246
|
-
await withErrorHandling(() => session.platform.updatePost(session.currentPostId, firstPartWithMarker), { action: "Update post with first part", session });
|
|
45281
|
+
const updateResult = await withErrorHandling(() => session.platform.updatePost(session.currentPostId, firstPartWithMarker), { action: "Update post with first part", session });
|
|
45282
|
+
if (updateResult === undefined) {
|
|
45283
|
+
sessionLog(session).warn("Update failed during split, including content in new message");
|
|
45284
|
+
session.pendingContent = content;
|
|
45285
|
+
} else {
|
|
45286
|
+
session.pendingContent = remainder;
|
|
45287
|
+
}
|
|
45247
45288
|
session.currentPostId = null;
|
|
45248
|
-
session.pendingContent
|
|
45249
|
-
if (
|
|
45250
|
-
const continuationMarker = formatter2.formatItalic("(continued)")
|
|
45251
|
-
const continuationContent = continuationMarker + `
|
|
45289
|
+
const contentToPost = session.pendingContent;
|
|
45290
|
+
if (contentToPost) {
|
|
45291
|
+
const continuationMarker = updateResult !== undefined ? formatter2.formatItalic("(continued)") + `
|
|
45252
45292
|
|
|
45253
|
-
`
|
|
45293
|
+
` : "";
|
|
45294
|
+
const continuationContent = continuationMarker + contentToPost;
|
|
45254
45295
|
const hasActiveTasks = session.tasksPostId && session.lastTasksContent && !session.tasksCompleted;
|
|
45255
45296
|
if (hasActiveTasks) {
|
|
45256
45297
|
const postId = await bumpTasksToBottomWithContent(session, continuationContent, registerPost);
|
|
@@ -45274,7 +45315,13 @@ async function flush(session, registerPost) {
|
|
|
45274
45315
|
` + formatter2.formatItalic("... (truncated)");
|
|
45275
45316
|
}
|
|
45276
45317
|
if (session.currentPostId) {
|
|
45277
|
-
await withErrorHandling(() => session.platform.updatePost(session.currentPostId, content), { action: "Update current post", session });
|
|
45318
|
+
const result = await withErrorHandling(() => session.platform.updatePost(session.currentPostId, content), { action: "Update current post", session });
|
|
45319
|
+
if (result === undefined) {
|
|
45320
|
+
sessionLog(session).warn("Update failed, starting new message");
|
|
45321
|
+
session.currentPostId = null;
|
|
45322
|
+
session.currentPostContent = "";
|
|
45323
|
+
return flush(session, registerPost);
|
|
45324
|
+
}
|
|
45278
45325
|
} else {
|
|
45279
45326
|
const hasActiveTasks = session.tasksPostId && session.lastTasksContent && !session.tasksCompleted;
|
|
45280
45327
|
if (hasActiveTasks) {
|
|
@@ -51153,6 +51200,24 @@ async function interruptSession(session, username) {
|
|
|
51153
51200
|
await postInterrupt(session, `${formatter.formatBold("Interrupted")} by ${formatter.formatUserMention(username)}`);
|
|
51154
51201
|
}
|
|
51155
51202
|
}
|
|
51203
|
+
async function approvePendingPlan(session, username, ctx) {
|
|
51204
|
+
if (!session.pendingApproval || session.pendingApproval.type !== "plan") {
|
|
51205
|
+
await postInfo(session, `No pending plan to approve`);
|
|
51206
|
+
sessionLog4(session).debug(`Approve requested but no pending plan`);
|
|
51207
|
+
return;
|
|
51208
|
+
}
|
|
51209
|
+
const { postId } = session.pendingApproval;
|
|
51210
|
+
sessionLog4(session).info(`\u2705 Plan approved by @${username} via command`);
|
|
51211
|
+
const formatter = session.platform.getFormatter();
|
|
51212
|
+
const statusMessage = `\u2705 ${formatter.formatBold("Plan approved")} by ${formatter.formatUserMention(username)} - starting implementation...`;
|
|
51213
|
+
await withErrorHandling(() => session.platform.updatePost(postId, statusMessage), { action: "Update approval post", session });
|
|
51214
|
+
session.pendingApproval = null;
|
|
51215
|
+
session.planApproved = true;
|
|
51216
|
+
if (session.claude.isRunning()) {
|
|
51217
|
+
session.claude.sendMessage("Plan approved! Please proceed with the implementation.");
|
|
51218
|
+
ctx.ops.startTyping(session);
|
|
51219
|
+
}
|
|
51220
|
+
}
|
|
51156
51221
|
async function changeDirectory(session, newDir, username, ctx) {
|
|
51157
51222
|
if (!await requireSessionOwner(session, username, "change the working directory")) {
|
|
51158
51223
|
return;
|
|
@@ -51318,6 +51383,11 @@ async function updateSessionHeader(session, ctx) {
|
|
|
51318
51383
|
statusItems.push(formatter.formatCode(`\uD83D\uDCB0 $${stats.totalCostUSD.toFixed(2)}`));
|
|
51319
51384
|
}
|
|
51320
51385
|
statusItems.push(formatter.formatCode(permMode));
|
|
51386
|
+
if (session.pendingApproval?.type === "plan") {
|
|
51387
|
+
statusItems.push(formatter.formatCode("\uD83D\uDCCB Plan pending"));
|
|
51388
|
+
} else if (session.planApproved) {
|
|
51389
|
+
statusItems.push(formatter.formatCode("\uD83D\uDD28 Implementing"));
|
|
51390
|
+
}
|
|
51321
51391
|
if (ctx.config.chromeEnabled) {
|
|
51322
51392
|
statusItems.push(formatter.formatCode("\uD83C\uDF10 Chrome"));
|
|
51323
51393
|
}
|
|
@@ -51448,11 +51518,13 @@ You are running inside a chat platform (like Mattermost or Slack). Users interac
|
|
|
51448
51518
|
- Permission requests (file writes, commands, etc.) appear as messages with emoji options
|
|
51449
51519
|
- Users approve with \uD83D\uDC4D or deny with \uD83D\uDC4E by reacting to the message
|
|
51450
51520
|
- Plan approvals and questions also use emoji reactions (\uD83D\uDC4D/\uD83D\uDC4E for plans, number emoji for choices)
|
|
51521
|
+
- Users can also type \`!approve\` or \`!yes\` to approve pending plans
|
|
51451
51522
|
|
|
51452
51523
|
## User Commands
|
|
51453
51524
|
Users can control sessions with these commands:
|
|
51454
51525
|
- \`!stop\` or \u274C reaction: End the current operation
|
|
51455
51526
|
- \`!escape\` or \u23F8\uFE0F reaction: Interrupt without ending the session
|
|
51527
|
+
- \`!approve\` or \uD83D\uDC4D reaction: Approve pending plan
|
|
51456
51528
|
- \`!invite @user\`: Allow another user to send messages in this session
|
|
51457
51529
|
- \`!kick @user\`: Remove a user from the session
|
|
51458
51530
|
- \`!cd /path\`: Change working directory (restarts the session)
|
|
@@ -51552,6 +51624,7 @@ ${startFormatter.formatItalic("Starting session...")}`, replyToPostId), { action
|
|
|
51552
51624
|
workingDir: ctx.config.workingDir,
|
|
51553
51625
|
claude,
|
|
51554
51626
|
currentPostId: null,
|
|
51627
|
+
currentPostContent: "",
|
|
51555
51628
|
pendingContent: "",
|
|
51556
51629
|
pendingApproval: null,
|
|
51557
51630
|
pendingQuestionSet: null,
|
|
@@ -51690,6 +51763,7 @@ Please start a new session.`, state.threadId), { action: "Post resume failure no
|
|
|
51690
51763
|
workingDir: state.workingDir,
|
|
51691
51764
|
claude,
|
|
51692
51765
|
currentPostId: null,
|
|
51766
|
+
currentPostContent: "",
|
|
51693
51767
|
pendingContent: "",
|
|
51694
51768
|
pendingApproval: null,
|
|
51695
51769
|
pendingQuestionSet: null,
|
|
@@ -51774,6 +51848,9 @@ ${failFormatter.formatItalic("Your previous conversation context is preserved, b
|
|
|
51774
51848
|
async function sendFollowUp(session, message, files, ctx) {
|
|
51775
51849
|
if (!session.claude.isRunning())
|
|
51776
51850
|
return;
|
|
51851
|
+
await ctx.ops.flush(session);
|
|
51852
|
+
session.currentPostId = null;
|
|
51853
|
+
session.currentPostContent = "";
|
|
51777
51854
|
await ctx.ops.bumpTasksToBottom(session);
|
|
51778
51855
|
const content = await ctx.ops.buildMessageContent(message, session.platform, files);
|
|
51779
51856
|
const messageText = typeof content === "string" ? content : message;
|
|
@@ -53209,7 +53286,8 @@ class SessionManager extends EventEmitter4 {
|
|
|
53209
53286
|
}
|
|
53210
53287
|
async handleMessage(_platformId, _post, _user) {}
|
|
53211
53288
|
async handleReaction(platformId, postId, emojiName, username, action) {
|
|
53212
|
-
|
|
53289
|
+
const normalizedEmoji = normalizeEmojiName(emojiName);
|
|
53290
|
+
if (action === "added" && isResumeEmoji(normalizedEmoji)) {
|
|
53213
53291
|
const resumed = await this.tryResumeFromReaction(platformId, postId, username);
|
|
53214
53292
|
if (resumed)
|
|
53215
53293
|
return;
|
|
@@ -53222,7 +53300,7 @@ class SessionManager extends EventEmitter4 {
|
|
|
53222
53300
|
if (!session.sessionAllowedUsers.has(username) && !session.platform.isUserAllowed(username)) {
|
|
53223
53301
|
return;
|
|
53224
53302
|
}
|
|
53225
|
-
await this.handleSessionReaction(session, postId,
|
|
53303
|
+
await this.handleSessionReaction(session, postId, normalizedEmoji, username, action);
|
|
53226
53304
|
}
|
|
53227
53305
|
async tryResumeFromReaction(platformId, postId, username) {
|
|
53228
53306
|
const persistedSession = this.sessionStore.findByPostId(platformId, postId);
|
|
@@ -53681,6 +53759,12 @@ class SessionManager extends EventEmitter4 {
|
|
|
53681
53759
|
return;
|
|
53682
53760
|
await interruptSession(session, username);
|
|
53683
53761
|
}
|
|
53762
|
+
async approvePendingPlan(threadId, username) {
|
|
53763
|
+
const session = this.findSessionByThreadId(threadId);
|
|
53764
|
+
if (!session)
|
|
53765
|
+
return;
|
|
53766
|
+
await approvePendingPlan(session, username, this.getContext());
|
|
53767
|
+
}
|
|
53684
53768
|
async changeDirectory(threadId, newDir, username) {
|
|
53685
53769
|
const session = this.findSessionByThreadId(threadId);
|
|
53686
53770
|
if (!session)
|
|
@@ -63276,6 +63360,12 @@ async function handleMessage(client, session, post, user, options) {
|
|
|
63276
63360
|
}
|
|
63277
63361
|
return;
|
|
63278
63362
|
}
|
|
63363
|
+
if (lowerContent === "!approve" || lowerContent === "!yes") {
|
|
63364
|
+
if (session.isUserAllowedInSession(threadRoot, username)) {
|
|
63365
|
+
await session.approvePendingPlan(threadRoot, username);
|
|
63366
|
+
}
|
|
63367
|
+
return;
|
|
63368
|
+
}
|
|
63279
63369
|
if (lowerContent === "!help") {
|
|
63280
63370
|
const code = formatter.formatCode.bind(formatter);
|
|
63281
63371
|
const commandTable = formatter.formatTable(["Command", "Description"], [
|
|
@@ -63289,6 +63379,7 @@ async function handleMessage(client, session, post, user, options) {
|
|
|
63289
63379
|
[code("!invite @user"), "Invite a user to this session"],
|
|
63290
63380
|
[code("!kick @user"), "Remove an invited user"],
|
|
63291
63381
|
[code("!permissions interactive"), "Enable interactive permissions"],
|
|
63382
|
+
[code("!approve"), "Approve pending plan (alternative to \uD83D\uDC4D reaction)"],
|
|
63292
63383
|
[code("!escape"), "Interrupt current task (session stays active)"],
|
|
63293
63384
|
[code("!stop"), "Stop this session"],
|
|
63294
63385
|
[code("!kill"), "Emergency shutdown (kills ALL sessions, exits bot)"]
|