@yu_robotics/remote-cli-router 1.6.37 → 1.6.43
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/README.md +4 -1
- package/dist/feishu/FeishuLongConnHandler.d.ts +12 -4
- package/dist/feishu/FeishuLongConnHandler.d.ts.map +1 -1
- package/dist/feishu/FeishuLongConnHandler.js +116 -55
- package/dist/feishu/FeishuLongConnHandler.js.map +1 -1
- package/dist/server.d.ts +4 -0
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +139 -10
- package/dist/server.js.map +1 -1
- package/dist/types/index.d.ts +8 -0
- package/dist/types/index.d.ts.map +1 -1
- package/dist/websocket/ConnectionHub.d.ts +6 -1
- package/dist/websocket/ConnectionHub.d.ts.map +1 -1
- package/dist/websocket/ConnectionHub.js +12 -1
- package/dist/websocket/ConnectionHub.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -11,6 +11,7 @@ The router server acts as a bridge between Feishu messaging and developer machin
|
|
|
11
11
|
- **Code-change rendering** with collapsible, line-aware diff previews inside streaming cards
|
|
12
12
|
- **Image message forwarding** for standalone images and mixed text-image posts to supported local backends, plus generated Codex images back to Feishu Card 2.0
|
|
13
13
|
- **Client service management** is handled by the local CLI; the Router remains a separate long-running server process
|
|
14
|
+
- **Queued task cards** appear at execution time for capable clients, with the thread, workspace, task preview, and remaining queue count; older clients retain waiting cards
|
|
14
15
|
- **WebSocket connections** from local clients
|
|
15
16
|
- **Feishu long connection** for receiving and sending messages
|
|
16
17
|
|
|
@@ -127,7 +128,9 @@ curl https://your-domain.com/health
|
|
|
127
128
|
|
|
128
129
|
## Advanced Usage
|
|
129
130
|
|
|
130
|
-
Use one Router for multiple trusted clients. Each client connects from the developer's machine, while the Router owns Feishu configuration, device bindings, routing, and thread switch state. A thread can override its backend with `/backend <index> @`; `/backend <index>` changes the global backend and clears per-thread overrides.
|
|
131
|
+
Use one Router for multiple trusted clients. Each client connects from the developer's machine, while the Router owns Feishu configuration, device bindings, routing, and thread switch state. Feishu thread buttons show automatic names such as `thread-2` as their sequence number, such as `2`, while callbacks retain the full internal name. Continuation cards repeat the thread and working-directory header of the original card. A thread can override its backend with `/backend <index> @`; `/backend <index>` changes the global backend and clears per-thread overrides.
|
|
132
|
+
|
|
133
|
+
Thread switch panels show `Reply from:` with the reply's full thread name and workspace. Under `Switch thread`, a check mark (`✓`) and primary button styling identify the destination of new top-level messages when the card is finalized or clicked. Selected buttons remain clickable. Switching updates only the clicked card; other historical cards retain their last displayed selection.
|
|
131
134
|
|
|
132
135
|
When a client thread is busy, the Router sends a confirmation card before accepting another message into its queue. `/queue` shows queue state, `/queue clear` discards queued messages, and `/abort` stops the active task and clears that thread's queue. Messages received while abort cleanup is in progress wait for the client backend to become safe before execution starts.
|
|
133
136
|
|
|
@@ -32,6 +32,7 @@ export declare class FeishuLongConnHandler {
|
|
|
32
32
|
private threadSwitchStateCleanupTimer;
|
|
33
33
|
private lastProcessedLengths;
|
|
34
34
|
private messageLocks;
|
|
35
|
+
private queueCardDecisions;
|
|
35
36
|
private queueActionStates;
|
|
36
37
|
private readonly QUEUE_ACTION_STATE_TTL_MS;
|
|
37
38
|
private readonly TOOL_USE_PATTERN;
|
|
@@ -50,7 +51,7 @@ export declare class FeishuLongConnHandler {
|
|
|
50
51
|
/**
|
|
51
52
|
* Set streaming start callback
|
|
52
53
|
*/
|
|
53
|
-
setOnStartStreaming(callback: (messageId: string, openId: string, feishuMessageId: string | null, deviceId: string, threadId?: string, pendingNewThread?: boolean) => void): void;
|
|
54
|
+
setOnStartStreaming(callback: (messageId: string, openId: string, feishuMessageId: string | null, deviceId: string, threadId?: string, pendingNewThread?: boolean, queueCardId?: string) => void): void;
|
|
54
55
|
/**
|
|
55
56
|
* Callback to resolve threadId from a Feishu card message_id (for parent_id routing)
|
|
56
57
|
*/
|
|
@@ -216,6 +217,8 @@ export declare class FeishuLongConnHandler {
|
|
|
216
217
|
* are counted, including nested components, text elements, and container children.
|
|
217
218
|
*
|
|
218
219
|
* @param elements Array of Feishu Card 2.0 elements
|
|
220
|
+
* @param continuationHeaderElements Header elements repeated at the top of continuation cards
|
|
221
|
+
* @param trailingElements Final panel elements that must remain together
|
|
219
222
|
* @returns Array of element chunks, each satisfying Feishu's limits
|
|
220
223
|
*/
|
|
221
224
|
private splitElementsIntoChunks;
|
|
@@ -267,8 +270,9 @@ export declare class FeishuLongConnHandler {
|
|
|
267
270
|
* @param messageId The Feishu message ID
|
|
268
271
|
* @param elements Array of Feishu Card 2.0 elements
|
|
269
272
|
* @param openId User's open_id for creating continuation messages
|
|
273
|
+
* @param threadName Thread name shown in continuation card headers
|
|
270
274
|
*/
|
|
271
|
-
updateStreamingMessage(messageId: string, elements: any[], openId?: string): Promise<boolean>;
|
|
275
|
+
updateStreamingMessage(messageId: string, elements: any[], openId?: string, threadName?: string): Promise<boolean>;
|
|
272
276
|
private _updateStreamingMessage;
|
|
273
277
|
/**
|
|
274
278
|
* Finalize streaming message
|
|
@@ -278,8 +282,9 @@ export declare class FeishuLongConnHandler {
|
|
|
278
282
|
* @param elements Array of Feishu Card 2.0 elements
|
|
279
283
|
* @param sessionAbbr Optional session abbreviation
|
|
280
284
|
* @param openId User's open_id for creating continuation messages
|
|
285
|
+
* @param replyThreadId Thread that produced the reply, independent of the selected thread
|
|
281
286
|
*/
|
|
282
|
-
finalizeStreamingMessage(messageId: string, elements: any[], sessionAbbr?: string, openId?: string, cwd?: string, threadName?: string, threads?: ThreadSummary[],
|
|
287
|
+
finalizeStreamingMessage(messageId: string, elements: any[], sessionAbbr?: string, openId?: string, cwd?: string, threadName?: string, threads?: ThreadSummary[], replyThreadId?: string, queueConfirmation?: QueueConfirmationInfo): Promise<boolean>;
|
|
283
288
|
private _finalizeStreamingMessage;
|
|
284
289
|
/**
|
|
285
290
|
* Create Feishu Card 2.0 elements for thread switching buttons.
|
|
@@ -288,6 +293,7 @@ export declare class FeishuLongConnHandler {
|
|
|
288
293
|
*/
|
|
289
294
|
private createThreadSwitchElements;
|
|
290
295
|
private threadButtonLabel;
|
|
296
|
+
private createResponseHeaderElements;
|
|
291
297
|
/**
|
|
292
298
|
* Sort automatically named threads by their numeric suffix while preserving
|
|
293
299
|
* the positions and creation order of default and user-named threads.
|
|
@@ -312,8 +318,10 @@ export declare class FeishuLongConnHandler {
|
|
|
312
318
|
* Reuses handleRegularCommand flow: creates streaming card, sends command to device.
|
|
313
319
|
*/
|
|
314
320
|
sendCommandFromCardAction(openId: string, content: string, pendingNewThread?: boolean): Promise<void>;
|
|
315
|
-
/**
|
|
321
|
+
/** Register confirmed work; capable clients request a card when execution starts. */
|
|
316
322
|
sendQueueActionFromCardAction(openId: string, action: 'confirm' | 'cancel', queueId: string, threadId: string, cardMessageId?: string): Promise<'sent' | 'already_processed'>;
|
|
323
|
+
/** Leave a historical queue receipt after output moves to a new card. */
|
|
324
|
+
markQueueCardStarted(cardMessageId: string): Promise<void>;
|
|
317
325
|
private removeExpiredQueueActionStates;
|
|
318
326
|
private updateQueueConfirmationCard;
|
|
319
327
|
/**
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"FeishuLongConnHandler.d.ts","sourceRoot":"","sources":["../../src/feishu/FeishuLongConnHandler.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,cAAc,EAAE,MAAM,2BAA2B,CAAC;AAC3D,OAAO,EAAE,aAAa,EAAE,MAAM,4BAA4B,CAAC;AAC3D,OAAO,EAA4B,aAAa,EAAc,qBAAqB,EAAE,MAAM,UAAU,CAAC;AACtG,OAAO,EAAE,SAAS,EAAE,MAAM,sBAAsB,CAAC;AAKjD;;GAEG;AACH,MAAM,WAAW,2BAA2B;IAC1C,KAAK,EAAE,MAAM,CAAC;IACd,SAAS,EAAE,MAAM,CAAC;IAClB,KAAK,EAAE,SAAS,CAAC;CAClB;AAED;;;GAGG;AACH,qBAAa,qBAAqB;IAChC,OAAO,CAAC,MAAM,CAAc;IAC5B,OAAO,CAAC,QAAQ,CAA8B;IAC9C,OAAO,CAAC,cAAc,CAAiB;IACvC,OAAO,CAAC,aAAa,CAA8B;IACnD,OAAO,CAAC,KAAK,CAAS;IACtB,OAAO,CAAC,SAAS,CAAS;IAE1B,OAAO,CAAC,QAAQ,CAAC,oBAAoB,CAAQ;IAI7C,OAAO,CAAC,QAAQ,CAAC,kBAAkB,CAAO;IAC1C,OAAO,CAAC,QAAQ,CAAC,oBAAoB,CAAW;IAChD,OAAO,CAAC,QAAQ,CAAC,gBAAgB,CAAU;IAC3C,OAAO,CAAC,QAAQ,CAAC,sBAAsB,CAAK;IAE5C,OAAO,CAAC,aAAa,CAAoC;IAGzD,OAAO,CAAC,qBAAqB,
|
|
1
|
+
{"version":3,"file":"FeishuLongConnHandler.d.ts","sourceRoot":"","sources":["../../src/feishu/FeishuLongConnHandler.ts"],"names":[],"mappings":"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"}
|
|
@@ -73,6 +73,7 @@ class FeishuLongConnHandler {
|
|
|
73
73
|
lastProcessedLengths = new Map();
|
|
74
74
|
// Per-message serialization locks to prevent concurrent updates from creating duplicates
|
|
75
75
|
messageLocks = new Map();
|
|
76
|
+
queueCardDecisions = new Map();
|
|
76
77
|
// Prevent repeated clicks on the same queue confirmation card from sending duplicate commands.
|
|
77
78
|
queueActionStates = new Map();
|
|
78
79
|
QUEUE_ACTION_STATE_TTL_MS = 24 * 60 * 60 * 1000;
|
|
@@ -933,9 +934,11 @@ Examples:
|
|
|
933
934
|
* are counted, including nested components, text elements, and container children.
|
|
934
935
|
*
|
|
935
936
|
* @param elements Array of Feishu Card 2.0 elements
|
|
937
|
+
* @param continuationHeaderElements Header elements repeated at the top of continuation cards
|
|
938
|
+
* @param trailingElements Final panel elements that must remain together
|
|
936
939
|
* @returns Array of element chunks, each satisfying Feishu's limits
|
|
937
940
|
*/
|
|
938
|
-
splitElementsIntoChunks(elements) {
|
|
941
|
+
splitElementsIntoChunks(elements, continuationHeaderElements = [], trailingElements = []) {
|
|
939
942
|
// If empty or very small, return as-is
|
|
940
943
|
if (elements.length === 0) {
|
|
941
944
|
return [elements];
|
|
@@ -950,17 +953,24 @@ Examples:
|
|
|
950
953
|
let currentChunk = [];
|
|
951
954
|
let currentChunkSize = 0;
|
|
952
955
|
let currentChunkTaggedNodes = 0;
|
|
953
|
-
// Reserve
|
|
954
|
-
const
|
|
955
|
-
const
|
|
956
|
+
// Reserve space for continuation indicators and the repeated card header.
|
|
957
|
+
const continuationHeaderSize = JSON.stringify(continuationHeaderElements).length;
|
|
958
|
+
const continuationHeaderTaggedNodes = continuationHeaderElements.reduce((total, element) => total + this.countTaggedNodes(element), 0);
|
|
959
|
+
const continuationIndicatorSize = 200 + continuationHeaderSize;
|
|
960
|
+
const continuationIndicatorCount = 2 + continuationHeaderTaggedNodes;
|
|
956
961
|
for (let i = 0; i < elements.length; i++) {
|
|
957
962
|
const element = elements[i];
|
|
958
963
|
const elementSize = JSON.stringify(element).length;
|
|
959
964
|
const elementTaggedNodes = this.countTaggedNodes(element);
|
|
965
|
+
// Keep the switch panel caption and all button rows on the same card.
|
|
966
|
+
const groupSize = element === trailingElements[0]
|
|
967
|
+
? trailingElements.reduce((sum, item) => sum + JSON.stringify(item).length, 0) : elementSize;
|
|
968
|
+
const groupTaggedNodes = element === trailingElements[0]
|
|
969
|
+
? trailingElements.reduce((sum, item) => sum + this.countTaggedNodes(item), 0) : elementTaggedNodes;
|
|
960
970
|
// Check if adding this element would exceed limits
|
|
961
971
|
// Reserve space for continuation indicators
|
|
962
|
-
const wouldExceedElementLimit = currentChunkTaggedNodes +
|
|
963
|
-
const wouldExceedSizeLimit = currentChunkSize +
|
|
972
|
+
const wouldExceedElementLimit = currentChunkTaggedNodes + groupTaggedNodes > (this.CARD_ELEMENT_LIMIT - continuationIndicatorCount);
|
|
973
|
+
const wouldExceedSizeLimit = currentChunkSize + groupSize + continuationIndicatorSize >
|
|
964
974
|
(this.CARD_DATA_SIZE_LIMIT - this.CARD_SIZE_BUFFER);
|
|
965
975
|
if (currentChunk.length > 0 && (wouldExceedElementLimit || wouldExceedSizeLimit)) {
|
|
966
976
|
// Start a new chunk
|
|
@@ -998,8 +1008,9 @@ Examples:
|
|
|
998
1008
|
});
|
|
999
1009
|
}
|
|
1000
1010
|
if (!isFirst) {
|
|
1001
|
-
//
|
|
1002
|
-
|
|
1011
|
+
// Repeat the normal card header before the continuation indicator so
|
|
1012
|
+
// every card in a long response identifies its thread and workspace.
|
|
1013
|
+
chunks[i].unshift(...continuationHeaderElements, {
|
|
1003
1014
|
tag: 'markdown',
|
|
1004
1015
|
content: '_⬅️ Continued from previous message..._\n\n',
|
|
1005
1016
|
});
|
|
@@ -1122,8 +1133,8 @@ Examples:
|
|
|
1122
1133
|
async uploadImage(data, mimeType) {
|
|
1123
1134
|
try {
|
|
1124
1135
|
const image = Buffer.from(data, 'base64');
|
|
1125
|
-
if (image.length === 0 || image.length >
|
|
1126
|
-
console.error('[FeishuHandler] Generated image exceeds the
|
|
1136
|
+
if (image.length === 0 || image.length > 2 * 1024 * 1024) {
|
|
1137
|
+
console.error('[FeishuHandler] Generated image exceeds the 2 MiB upload limit');
|
|
1127
1138
|
return null;
|
|
1128
1139
|
}
|
|
1129
1140
|
const supportedTypes = new Set(['image/jpeg', 'image/png', 'image/webp', 'image/gif', 'image/tiff', 'image/bmp', 'image/x-icon']);
|
|
@@ -1153,11 +1164,13 @@ Examples:
|
|
|
1153
1164
|
* @param messageId The Feishu message ID
|
|
1154
1165
|
* @param elements Array of Feishu Card 2.0 elements
|
|
1155
1166
|
* @param openId User's open_id for creating continuation messages
|
|
1167
|
+
* @param threadName Thread name shown in continuation card headers
|
|
1156
1168
|
*/
|
|
1157
|
-
async updateStreamingMessage(messageId, elements, openId) {
|
|
1158
|
-
|
|
1169
|
+
async updateStreamingMessage(messageId, elements, openId, threadName) {
|
|
1170
|
+
const continuationHeaderElements = this.createResponseHeaderElements(threadName);
|
|
1171
|
+
return this.withMessageLock(messageId, () => this._updateStreamingMessage(messageId, elements, openId, continuationHeaderElements));
|
|
1159
1172
|
}
|
|
1160
|
-
async _updateStreamingMessage(messageId, elements, openId) {
|
|
1173
|
+
async _updateStreamingMessage(messageId, elements, openId, continuationHeaderElements = [], trailingElements = []) {
|
|
1161
1174
|
try {
|
|
1162
1175
|
// Get or initialize message chain
|
|
1163
1176
|
let chain = this.messageChains.get(messageId);
|
|
@@ -1166,7 +1179,7 @@ Examples:
|
|
|
1166
1179
|
this.messageChains.set(messageId, chain);
|
|
1167
1180
|
}
|
|
1168
1181
|
// Split elements into chunks based on Feishu Card 2.0 limits
|
|
1169
|
-
const chunks = this.splitElementsIntoChunks(elements);
|
|
1182
|
+
const chunks = this.splitElementsIntoChunks(elements, continuationHeaderElements, trailingElements);
|
|
1170
1183
|
console.log(`[FeishuHandler] Need ${chunks.length} card(s), currently have ${chain.length} card(s)`);
|
|
1171
1184
|
// Update the first message with the first chunk
|
|
1172
1185
|
await this.client.im.message.patch({
|
|
@@ -1230,51 +1243,49 @@ Examples:
|
|
|
1230
1243
|
* @param elements Array of Feishu Card 2.0 elements
|
|
1231
1244
|
* @param sessionAbbr Optional session abbreviation
|
|
1232
1245
|
* @param openId User's open_id for creating continuation messages
|
|
1246
|
+
* @param replyThreadId Thread that produced the reply, independent of the selected thread
|
|
1233
1247
|
*/
|
|
1234
|
-
async finalizeStreamingMessage(messageId, elements, sessionAbbr, openId, cwd, threadName, threads,
|
|
1235
|
-
return this.withMessageLock(messageId, () => this._finalizeStreamingMessage(messageId, elements, sessionAbbr, openId, cwd, threadName, threads,
|
|
1248
|
+
async finalizeStreamingMessage(messageId, elements, sessionAbbr, openId, cwd, threadName, threads, replyThreadId, queueConfirmation) {
|
|
1249
|
+
return this.withMessageLock(messageId, () => this._finalizeStreamingMessage(messageId, elements, sessionAbbr, openId, cwd, threadName, threads, replyThreadId, queueConfirmation));
|
|
1236
1250
|
}
|
|
1237
|
-
async _finalizeStreamingMessage(messageId, elements, sessionAbbr, openId, cwd, threadName, threads,
|
|
1251
|
+
async _finalizeStreamingMessage(messageId, elements, sessionAbbr, openId, cwd, threadName, threads, replyThreadId, queueConfirmation) {
|
|
1238
1252
|
try {
|
|
1239
|
-
// Build header element (thread name + working directory) at the top
|
|
1240
|
-
const headerParts = [];
|
|
1241
|
-
if (threadName) {
|
|
1242
|
-
headerParts.push(`🧵 **${threadName}**`);
|
|
1243
|
-
}
|
|
1244
|
-
if (cwd) {
|
|
1245
|
-
const formattedCwd = cwd.replace(process.env.HOME || '/Users', '~');
|
|
1246
|
-
headerParts.push(`📂 \`${formattedCwd}\``);
|
|
1247
|
-
}
|
|
1248
1253
|
// Build completion note
|
|
1249
1254
|
let noteContent = queueConfirmation ? '⏳ Awaiting queue confirmation' : '✅ Completed';
|
|
1250
1255
|
if (sessionAbbr) {
|
|
1251
1256
|
noteContent += ` · Session: ${sessionAbbr}`;
|
|
1252
1257
|
}
|
|
1253
|
-
const
|
|
1254
|
-
|
|
1255
|
-
finalElements.push({ tag: 'markdown', content: headerParts.join(' · ') });
|
|
1256
|
-
finalElements.push({ tag: 'hr' });
|
|
1257
|
-
}
|
|
1258
|
+
const headerElements = this.createResponseHeaderElements(threadName, cwd);
|
|
1259
|
+
const finalElements = [...headerElements];
|
|
1258
1260
|
finalElements.push(...elements, { tag: 'markdown', content: noteContent });
|
|
1259
1261
|
if (queueConfirmation) {
|
|
1260
1262
|
finalElements.push(...this.createQueueConfirmationElements(queueConfirmation));
|
|
1261
1263
|
}
|
|
1262
1264
|
// Append thread switch buttons when at least one thread exists
|
|
1263
1265
|
let threadSwitchElements = [];
|
|
1266
|
+
// The reply's owner and the destination of new messages are independent.
|
|
1267
|
+
const activeThreadId = (openId ? this.onResolveActiveThread?.(openId)?.threadId : undefined)
|
|
1268
|
+
?? threads?.find(thread => thread.name === 'default')?.id;
|
|
1269
|
+
const replyThread = threads?.find(thread => thread.id === replyThreadId);
|
|
1270
|
+
const replyThreadName = threadName ?? replyThread?.name;
|
|
1271
|
+
const replyWorkspace = cwd ? path.posix.basename(cwd.replace(/\\/g, '/')) || cwd : replyThread?.workspaceName;
|
|
1272
|
+
const replyLabel = replyThreadName
|
|
1273
|
+
? [replyThreadName, replyWorkspace].filter(Boolean).join(' · ')
|
|
1274
|
+
: undefined;
|
|
1264
1275
|
if (threads && threads.length >= 1) {
|
|
1265
|
-
threadSwitchElements = this.createThreadSwitchElements(threads, activeThreadId);
|
|
1276
|
+
threadSwitchElements = this.createThreadSwitchElements(threads, activeThreadId, replyLabel);
|
|
1266
1277
|
finalElements.push(...threadSwitchElements);
|
|
1267
1278
|
}
|
|
1268
1279
|
// Reuse streaming update logic: only create cards, never delete.
|
|
1269
1280
|
// Whatever layout was built during streaming stays as-is.
|
|
1270
|
-
const updated = await this._updateStreamingMessage(messageId, finalElements, openId);
|
|
1281
|
+
const updated = await this._updateStreamingMessage(messageId, finalElements, openId, headerElements, threadSwitchElements);
|
|
1271
1282
|
if (!updated) {
|
|
1272
1283
|
return false;
|
|
1273
1284
|
}
|
|
1274
1285
|
// Store thread switch card state for later refresh (before cleanup)
|
|
1275
1286
|
if (threads && threads.length >= 1) {
|
|
1276
1287
|
const chain = this.messageChains.get(messageId);
|
|
1277
|
-
const chunks = this.splitElementsIntoChunks(finalElements);
|
|
1288
|
+
const chunks = this.splitElementsIntoChunks(finalElements, headerElements, threadSwitchElements);
|
|
1278
1289
|
const lastChunkIndex = chunks.length - 1;
|
|
1279
1290
|
const lastCardId = chain?.[lastChunkIndex];
|
|
1280
1291
|
if (lastCardId) {
|
|
@@ -1282,6 +1293,7 @@ Examples:
|
|
|
1282
1293
|
baseElements: chunks[lastChunkIndex].filter((element) => !threadSwitchElements.includes(element)),
|
|
1283
1294
|
threads,
|
|
1284
1295
|
activeThreadId,
|
|
1296
|
+
replyLabel,
|
|
1285
1297
|
createdAt: Date.now(),
|
|
1286
1298
|
});
|
|
1287
1299
|
}
|
|
@@ -1301,7 +1313,7 @@ Examples:
|
|
|
1301
1313
|
* Renders one button per thread (active is highlighted) plus a "+ New" button,
|
|
1302
1314
|
* split across rows so desktop clients do not compress every button into one line.
|
|
1303
1315
|
*/
|
|
1304
|
-
createThreadSwitchElements(threads, activeThreadId) {
|
|
1316
|
+
createThreadSwitchElements(threads, activeThreadId, replyLabel) {
|
|
1305
1317
|
const orderedThreads = this.orderThreadsForDisplay(threads);
|
|
1306
1318
|
const threadColumns = orderedThreads.map((t) => ({
|
|
1307
1319
|
tag: 'column',
|
|
@@ -1311,7 +1323,7 @@ Examples:
|
|
|
1311
1323
|
tag: 'button',
|
|
1312
1324
|
text: { tag: 'plain_text', content: this.threadButtonLabel(t, t.id === activeThreadId) },
|
|
1313
1325
|
type: t.id === activeThreadId ? 'primary' : 'default',
|
|
1314
|
-
disabled:
|
|
1326
|
+
disabled: false,
|
|
1315
1327
|
behaviors: [{ type: 'callback', value: { action: 'switch_thread', threadId: t.id, threadName: t.name } }],
|
|
1316
1328
|
},
|
|
1317
1329
|
],
|
|
@@ -1340,14 +1352,34 @@ Examples:
|
|
|
1340
1352
|
columns: columns.slice(index, index + this.THREAD_BUTTONS_PER_ROW),
|
|
1341
1353
|
});
|
|
1342
1354
|
}
|
|
1343
|
-
|
|
1355
|
+
const caption = {
|
|
1356
|
+
tag: 'markdown',
|
|
1357
|
+
text_size: 'notation',
|
|
1358
|
+
content: replyLabel ? `Reply from: ${replyLabel}\nSwitch thread` : 'Switch thread',
|
|
1359
|
+
};
|
|
1360
|
+
return [{ tag: 'hr' }, caption, ...rows];
|
|
1344
1361
|
}
|
|
1345
1362
|
threadButtonLabel(thread, active) {
|
|
1346
1363
|
const details = [
|
|
1347
1364
|
thread.workspaceName,
|
|
1348
1365
|
thread.backend ? this.backendLabel(thread.backend) : undefined,
|
|
1349
1366
|
].filter((detail) => Boolean(detail));
|
|
1350
|
-
|
|
1367
|
+
const automaticName = /^thread-(\d+)$/.exec(thread.name);
|
|
1368
|
+
const displayName = automaticName ? automaticName[1] : thread.name;
|
|
1369
|
+
return `${active ? '✓ ' : ''}${displayName}${details.length > 0 ? ` · ${details.join(' · ')}` : ''}`;
|
|
1370
|
+
}
|
|
1371
|
+
createResponseHeaderElements(threadName, cwd) {
|
|
1372
|
+
const headerParts = [];
|
|
1373
|
+
if (threadName) {
|
|
1374
|
+
headerParts.push(`🧵 **${threadName}**`);
|
|
1375
|
+
}
|
|
1376
|
+
if (cwd) {
|
|
1377
|
+
const formattedCwd = cwd.replace(process.env.HOME || '/Users', '~');
|
|
1378
|
+
headerParts.push(`📂 \`${formattedCwd}\``);
|
|
1379
|
+
}
|
|
1380
|
+
return headerParts.length > 0
|
|
1381
|
+
? [{ tag: 'markdown', content: headerParts.join(' · ') }, { tag: 'hr' }]
|
|
1382
|
+
: [];
|
|
1351
1383
|
}
|
|
1352
1384
|
/**
|
|
1353
1385
|
* Sort automatically named threads by their numeric suffix while preserving
|
|
@@ -1426,7 +1458,7 @@ Examples:
|
|
|
1426
1458
|
}
|
|
1427
1459
|
const updatedElements = [
|
|
1428
1460
|
...state.baseElements,
|
|
1429
|
-
...this.createThreadSwitchElements(state.threads, newActiveThreadId),
|
|
1461
|
+
...this.createThreadSwitchElements(state.threads, newActiveThreadId, state.replyLabel),
|
|
1430
1462
|
];
|
|
1431
1463
|
await this.client.im.message.patch({
|
|
1432
1464
|
path: { message_id: cardMessageId },
|
|
@@ -1498,7 +1530,7 @@ Examples:
|
|
|
1498
1530
|
const syntheticMessageId = (0, uuid_1.v4)();
|
|
1499
1531
|
await this.handleRegularCommand(openId, syntheticMessageId, content, undefined, pendingNewThread);
|
|
1500
1532
|
}
|
|
1501
|
-
/**
|
|
1533
|
+
/** Register confirmed work; capable clients request a card when execution starts. */
|
|
1502
1534
|
async sendQueueActionFromCardAction(openId, action, queueId, threadId, cardMessageId) {
|
|
1503
1535
|
this.removeExpiredQueueActionStates();
|
|
1504
1536
|
const previousAction = this.queueActionStates.get(queueId);
|
|
@@ -1515,27 +1547,56 @@ Examples:
|
|
|
1515
1547
|
let executionMessageId;
|
|
1516
1548
|
if (action === 'confirm') {
|
|
1517
1549
|
executionMessageId = (0, uuid_1.v4)();
|
|
1518
|
-
const feishuMessageId =
|
|
1519
|
-
|
|
1550
|
+
const feishuMessageId = this.connectionHub.supportsQueueStarted(activeDevice.deviceId)
|
|
1551
|
+
? null
|
|
1552
|
+
: await this.sendStreamingStart(openId, '🤔 Queued message confirmed. Waiting for the current task to finish...');
|
|
1553
|
+
this.onStartStreaming?.(executionMessageId, openId, feishuMessageId, activeDevice.deviceId, threadId, false, cardMessageId);
|
|
1520
1554
|
}
|
|
1521
1555
|
const controlMessageId = (0, uuid_1.v4)();
|
|
1522
1556
|
const suffix = executionMessageId ? ` ${executionMessageId}` : '';
|
|
1523
|
-
|
|
1524
|
-
|
|
1525
|
-
|
|
1526
|
-
|
|
1527
|
-
|
|
1528
|
-
|
|
1529
|
-
|
|
1530
|
-
|
|
1531
|
-
|
|
1557
|
+
// An idle thread can start immediately. Its receipt must not be patched
|
|
1558
|
+
// back to "waiting" after the start event has already arrived.
|
|
1559
|
+
let finishDecision;
|
|
1560
|
+
const decision = new Promise((resolve) => { finishDecision = resolve; });
|
|
1561
|
+
if (cardMessageId)
|
|
1562
|
+
this.queueCardDecisions.set(cardMessageId, decision);
|
|
1563
|
+
try {
|
|
1564
|
+
const success = await this.connectionHub.sendToDevice(activeDevice.deviceId, {
|
|
1565
|
+
type: types_1.MessageType.COMMAND,
|
|
1566
|
+
messageId: controlMessageId,
|
|
1567
|
+
timestamp: Date.now(),
|
|
1568
|
+
content: `/queue ${action} ${queueId}${suffix}`,
|
|
1569
|
+
openId,
|
|
1570
|
+
threadId,
|
|
1571
|
+
});
|
|
1572
|
+
if (!success)
|
|
1573
|
+
throw new Error('Failed to send queue action to device');
|
|
1574
|
+
if (cardMessageId)
|
|
1575
|
+
await this.updateQueueConfirmationCard(cardMessageId, action);
|
|
1576
|
+
return 'sent';
|
|
1577
|
+
}
|
|
1578
|
+
catch (error) {
|
|
1532
1579
|
this.queueActionStates.delete(queueId);
|
|
1533
|
-
throw
|
|
1580
|
+
throw error;
|
|
1534
1581
|
}
|
|
1535
|
-
|
|
1536
|
-
|
|
1582
|
+
finally {
|
|
1583
|
+
finishDecision();
|
|
1584
|
+
if (cardMessageId)
|
|
1585
|
+
this.queueCardDecisions.delete(cardMessageId);
|
|
1537
1586
|
}
|
|
1538
|
-
|
|
1587
|
+
}
|
|
1588
|
+
/** Leave a historical queue receipt after output moves to a new card. */
|
|
1589
|
+
async markQueueCardStarted(cardMessageId) {
|
|
1590
|
+
await this.queueCardDecisions.get(cardMessageId);
|
|
1591
|
+
await this.withMessageLock(cardMessageId, () => this.client.im.message.patch({
|
|
1592
|
+
path: { message_id: cardMessageId },
|
|
1593
|
+
data: {
|
|
1594
|
+
content: JSON.stringify({
|
|
1595
|
+
schema: '2.0',
|
|
1596
|
+
body: { elements: [{ tag: 'markdown', content: '▶️ **Queued task started**\n\nFollow progress in the new execution card at the bottom of the chat.' }] },
|
|
1597
|
+
}),
|
|
1598
|
+
},
|
|
1599
|
+
}));
|
|
1539
1600
|
}
|
|
1540
1601
|
removeExpiredQueueActionStates() {
|
|
1541
1602
|
const expiry = Date.now() - this.QUEUE_ACTION_STATE_TTL_MS;
|