@fonz/tgcc 0.6.17 β 0.6.18
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 +74 -50
- package/dist/bridge.d.ts +19 -7
- package/dist/bridge.js +882 -646
- package/dist/bridge.js.map +1 -1
- package/dist/cc-process.js +7 -0
- package/dist/cc-process.js.map +1 -1
- package/dist/ctl-server.d.ts +4 -0
- package/dist/ctl-server.js +30 -4
- package/dist/ctl-server.js.map +1 -1
- package/dist/event-buffer.d.ts +27 -0
- package/dist/event-buffer.js +50 -0
- package/dist/event-buffer.js.map +1 -0
- package/dist/high-signal.d.ts +53 -0
- package/dist/high-signal.js +391 -0
- package/dist/high-signal.js.map +1 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.js.map +1 -1
- package/dist/mcp-bridge.d.ts +3 -5
- package/dist/mcp-server.js +80 -0
- package/dist/mcp-server.js.map +1 -1
- package/dist/session.d.ts +13 -8
- package/dist/session.js +61 -37
- package/dist/session.js.map +1 -1
- package/dist/streaming.d.ts +20 -7
- package/dist/streaming.js +232 -112
- package/dist/streaming.js.map +1 -1
- package/dist/telegram.d.ts +3 -1
- package/dist/telegram.js +18 -1
- package/dist/telegram.js.map +1 -1
- package/package.json +1 -1
package/dist/streaming.js
CHANGED
|
@@ -10,6 +10,12 @@ export function escapeHtml(text) {
|
|
|
10
10
|
.replace(/</g, '<')
|
|
11
11
|
.replace(/>/g, '>');
|
|
12
12
|
}
|
|
13
|
+
/** Create visually distinct system message with enhanced styling */
|
|
14
|
+
export function formatSystemMessage(type, content, expandable = false) {
|
|
15
|
+
const emoji = { thinking: 'π', tool: 'β‘', usage: 'π', error: 'β οΈ', status: 'βΉοΈ' }[type];
|
|
16
|
+
const wrapper = expandable ? 'blockquote expandable' : 'blockquote';
|
|
17
|
+
return `<${wrapper}>${emoji} ${content}</${wrapper.split(' ')[0]}>`;
|
|
18
|
+
}
|
|
13
19
|
/**
|
|
14
20
|
* Convert markdown text to Telegram-safe HTML using the marked library.
|
|
15
21
|
* Replaces the old hand-rolled implementation with a proper markdown parser.
|
|
@@ -45,21 +51,23 @@ export class StreamAccumulator {
|
|
|
45
51
|
lastEditTime = 0;
|
|
46
52
|
editTimer = null;
|
|
47
53
|
thinkingIndicatorShown = false;
|
|
54
|
+
thinkingMessageId = null; // preserved separately from tgMessageId
|
|
48
55
|
messageIds = []; // all message IDs sent during this turn
|
|
49
56
|
finished = false;
|
|
50
57
|
sendQueue = Promise.resolve();
|
|
51
58
|
turnUsage = null;
|
|
52
59
|
/** Usage from the most recent message_start event β represents a single API call's context (not cumulative). */
|
|
53
60
|
_lastMsgStartCtx = null;
|
|
54
|
-
// Per-tool-use
|
|
61
|
+
// Per-tool-use consolidated indicator message (persists across resets)
|
|
55
62
|
toolMessages = new Map();
|
|
56
63
|
toolInputBuffers = new Map(); // tool block ID β accumulated input JSON
|
|
57
64
|
currentToolBlockId = null;
|
|
65
|
+
consolidatedToolMsgId = null; // shared TG message for all tool indicators
|
|
58
66
|
constructor(options) {
|
|
59
67
|
this.chatId = options.chatId;
|
|
60
68
|
this.sender = options.sender;
|
|
61
69
|
this.editIntervalMs = options.editIntervalMs ?? 1000;
|
|
62
|
-
this.splitThreshold = options.splitThreshold ??
|
|
70
|
+
this.splitThreshold = options.splitThreshold ?? 3500;
|
|
63
71
|
this.logger = options.logger;
|
|
64
72
|
this.onError = options.onError;
|
|
65
73
|
}
|
|
@@ -122,23 +130,44 @@ export class StreamAccumulator {
|
|
|
122
130
|
if (blockType === 'thinking') {
|
|
123
131
|
this.currentBlockType = 'thinking';
|
|
124
132
|
if (!this.thinkingIndicatorShown && !this.buffer) {
|
|
125
|
-
await this.sendOrEdit('
|
|
133
|
+
await this.sendOrEdit(formatSystemMessage('thinking', 'Processing...'), true);
|
|
126
134
|
this.thinkingIndicatorShown = true;
|
|
127
135
|
}
|
|
128
136
|
}
|
|
129
137
|
else if (blockType === 'text') {
|
|
130
138
|
this.currentBlockType = 'text';
|
|
131
|
-
//
|
|
132
|
-
|
|
133
|
-
|
|
139
|
+
// If thinking indicator was shown, update it with actual thinking content NOW
|
|
140
|
+
// (before text starts streaming below) β don't wait for finalize()
|
|
141
|
+
if (this.thinkingIndicatorShown && this.tgMessageId) {
|
|
142
|
+
this.thinkingMessageId = this.tgMessageId;
|
|
143
|
+
this.tgMessageId = null; // force new message for response text
|
|
144
|
+
// Flush "Processing..." β actual thinking content immediately
|
|
145
|
+
if (this.thinkingBuffer && this.thinkingMessageId) {
|
|
146
|
+
const thinkingPreview = this.thinkingBuffer.length > 1024
|
|
147
|
+
? this.thinkingBuffer.slice(0, 1024) + 'β¦'
|
|
148
|
+
: this.thinkingBuffer;
|
|
149
|
+
const html = formatSystemMessage('thinking', markdownToTelegramHtml(thinkingPreview), true);
|
|
150
|
+
this.sendQueue = this.sendQueue.then(async () => {
|
|
151
|
+
try {
|
|
152
|
+
await this.sender.editMessage(this.chatId, this.thinkingMessageId, html, 'HTML');
|
|
153
|
+
}
|
|
154
|
+
catch {
|
|
155
|
+
// Non-critical β thinking message update
|
|
156
|
+
}
|
|
157
|
+
}).catch(err => {
|
|
158
|
+
this.logger?.error?.({ err }, 'Failed to flush thinking indicator');
|
|
159
|
+
});
|
|
160
|
+
}
|
|
134
161
|
}
|
|
135
162
|
}
|
|
136
163
|
else if (blockType === 'tool_use') {
|
|
137
164
|
this.currentBlockType = 'tool_use';
|
|
138
165
|
const block = event.content_block;
|
|
139
166
|
this.currentToolBlockId = block.id;
|
|
140
|
-
//
|
|
141
|
-
|
|
167
|
+
// Sub-agent tools are handled by SubAgentTracker β skip duplicate indicator
|
|
168
|
+
if (!isSubAgentTool(block.name)) {
|
|
169
|
+
await this.sendToolIndicator(block.id, block.name);
|
|
170
|
+
}
|
|
142
171
|
}
|
|
143
172
|
else if (blockType === 'image') {
|
|
144
173
|
this.currentBlockType = 'image';
|
|
@@ -238,23 +267,74 @@ export class StreamAccumulator {
|
|
|
238
267
|
}
|
|
239
268
|
this.imageBase64Buffer = '';
|
|
240
269
|
}
|
|
241
|
-
/** Send
|
|
270
|
+
/** Send or update the consolidated tool indicator message. */
|
|
242
271
|
async sendToolIndicator(blockId, toolName) {
|
|
243
272
|
const startTime = Date.now();
|
|
273
|
+
if (this.consolidatedToolMsgId) {
|
|
274
|
+
// Reuse existing consolidated message
|
|
275
|
+
this.toolMessages.set(blockId, { msgId: this.consolidatedToolMsgId, toolName, startTime, resolved: false, isError: false, elapsed: null });
|
|
276
|
+
this.updateConsolidatedToolMessage();
|
|
277
|
+
}
|
|
278
|
+
else {
|
|
279
|
+
// Create the first consolidated tool message
|
|
280
|
+
this.sendQueue = this.sendQueue.then(async () => {
|
|
281
|
+
try {
|
|
282
|
+
const html = this.buildConsolidatedToolHtml(blockId, toolName);
|
|
283
|
+
const msgId = await this.sender.sendMessage(this.chatId, html, 'HTML');
|
|
284
|
+
this.consolidatedToolMsgId = msgId;
|
|
285
|
+
this.toolMessages.set(blockId, { msgId, toolName, startTime, resolved: false, isError: false, elapsed: null });
|
|
286
|
+
}
|
|
287
|
+
catch (err) {
|
|
288
|
+
this.logger?.debug?.({ err, toolName }, 'Failed to send tool indicator');
|
|
289
|
+
}
|
|
290
|
+
}).catch(err => {
|
|
291
|
+
this.logger?.error?.({ err }, 'sendToolIndicator queue error');
|
|
292
|
+
});
|
|
293
|
+
return this.sendQueue;
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
/** Build HTML for the consolidated tool indicator message. */
|
|
297
|
+
buildConsolidatedToolHtml(pendingBlockId, pendingToolName) {
|
|
298
|
+
const lines = [];
|
|
299
|
+
for (const [blockId, entry] of this.toolMessages) {
|
|
300
|
+
const summary = this.toolIndicatorLastSummary.get(blockId);
|
|
301
|
+
const codePart = summary ? ` Β· <code>${escapeHtml(summary)}</code>` : '';
|
|
302
|
+
if (entry.resolved) {
|
|
303
|
+
const statLine = this.toolResolvedStats.get(blockId);
|
|
304
|
+
const statPart = statLine ? ` Β· ${escapeHtml(statLine)}` : '';
|
|
305
|
+
const icon = entry.isError ? 'β' : 'β
';
|
|
306
|
+
lines.push(`${icon} ${escapeHtml(entry.toolName)} (${entry.elapsed})${codePart}${statPart}`);
|
|
307
|
+
}
|
|
308
|
+
else {
|
|
309
|
+
lines.push(`β‘ ${escapeHtml(entry.toolName)}β¦${codePart}`);
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
// Include pending tool that hasn't been added to toolMessages yet
|
|
313
|
+
if (pendingBlockId && pendingToolName && !this.toolMessages.has(pendingBlockId)) {
|
|
314
|
+
lines.push(`β‘ ${escapeHtml(pendingToolName)}β¦`);
|
|
315
|
+
}
|
|
316
|
+
return `<blockquote expandable>${lines.join('\n')}</blockquote>`;
|
|
317
|
+
}
|
|
318
|
+
/** Resolved tool result stats, keyed by blockId */
|
|
319
|
+
toolResolvedStats = new Map();
|
|
320
|
+
/** Edit the consolidated tool message with current state of all tools. */
|
|
321
|
+
updateConsolidatedToolMessage() {
|
|
322
|
+
if (!this.consolidatedToolMsgId)
|
|
323
|
+
return;
|
|
324
|
+
const msgId = this.consolidatedToolMsgId;
|
|
325
|
+
const html = this.buildConsolidatedToolHtml();
|
|
244
326
|
this.sendQueue = this.sendQueue.then(async () => {
|
|
245
327
|
try {
|
|
246
|
-
|
|
247
|
-
const msgId = await this.sender.sendMessage(this.chatId, html, 'HTML');
|
|
248
|
-
this.toolMessages.set(blockId, { msgId, toolName, startTime });
|
|
328
|
+
await this.sender.editMessage(this.chatId, msgId, html, 'HTML');
|
|
249
329
|
}
|
|
250
330
|
catch (err) {
|
|
251
|
-
|
|
252
|
-
|
|
331
|
+
if (err instanceof Error && err.message.includes('message is not modified'))
|
|
332
|
+
return;
|
|
333
|
+
this.logger?.debug?.({ err }, 'Failed to update consolidated tool message');
|
|
253
334
|
}
|
|
254
335
|
}).catch(err => {
|
|
255
|
-
this.logger?.error?.({ err }, '
|
|
336
|
+
this.logger?.error?.({ err }, 'updateConsolidatedToolMessage queue error');
|
|
256
337
|
});
|
|
257
|
-
return this.sendQueue;
|
|
258
338
|
}
|
|
259
339
|
/** Update a tool indicator message with input preview once the JSON value is complete. */
|
|
260
340
|
toolIndicatorLastSummary = new Map(); // blockId β last rendered summary
|
|
@@ -272,105 +352,118 @@ export class StreamAccumulator {
|
|
|
272
352
|
if (this.toolIndicatorLastSummary.get(blockId) === summary)
|
|
273
353
|
return;
|
|
274
354
|
this.toolIndicatorLastSummary.set(blockId, summary);
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
this.sendQueue = this.sendQueue.then(async () => {
|
|
278
|
-
try {
|
|
279
|
-
await this.sender.editMessage(this.chatId, entry.msgId, html, 'HTML');
|
|
280
|
-
}
|
|
281
|
-
catch (err) {
|
|
282
|
-
// "message is not modified" or other edit failure β non-critical
|
|
283
|
-
this.logger?.debug?.({ err }, 'Failed to update tool indicator with input');
|
|
284
|
-
}
|
|
285
|
-
}).catch(err => {
|
|
286
|
-
this.logger?.error?.({ err }, 'updateToolIndicatorWithInput queue error');
|
|
287
|
-
});
|
|
288
|
-
return this.sendQueue;
|
|
355
|
+
// Update the consolidated tool message
|
|
356
|
+
this.updateConsolidatedToolMessage();
|
|
289
357
|
}
|
|
290
|
-
/**
|
|
358
|
+
/** MCP media tools β on success, delete indicator (the media was sent directly). On failure, keep + react β. */
|
|
359
|
+
static MCP_MEDIA_TOOLS = new Set(['mcp__tgcc__send_image', 'mcp__tgcc__send_file', 'mcp__tgcc__send_voice']);
|
|
360
|
+
/** Resolve a tool indicator with success/failure status. Updates the consolidated message. */
|
|
291
361
|
async resolveToolMessage(blockId, isError, errorMessage, resultContent, toolUseResult) {
|
|
292
362
|
const entry = this.toolMessages.get(blockId);
|
|
293
363
|
if (!entry)
|
|
294
364
|
return;
|
|
295
|
-
const {
|
|
365
|
+
const { toolName, startTime } = entry;
|
|
296
366
|
const elapsed = ((Date.now() - startTime) / 1000).toFixed(1);
|
|
367
|
+
// MCP media tools: remove from consolidated on success (the media itself is the result)
|
|
368
|
+
if (StreamAccumulator.MCP_MEDIA_TOOLS.has(toolName) && !isError) {
|
|
369
|
+
this.toolInputBuffers.delete(blockId);
|
|
370
|
+
this.toolIndicatorLastSummary.delete(blockId);
|
|
371
|
+
this.toolResolvedStats.delete(blockId);
|
|
372
|
+
this.toolMessages.delete(blockId);
|
|
373
|
+
// If that was the only tool, delete the consolidated message
|
|
374
|
+
if (this.toolMessages.size === 0 && this.consolidatedToolMsgId) {
|
|
375
|
+
const msgId = this.consolidatedToolMsgId;
|
|
376
|
+
this.consolidatedToolMsgId = null;
|
|
377
|
+
this.sendQueue = this.sendQueue.then(async () => {
|
|
378
|
+
try {
|
|
379
|
+
if (this.sender.deleteMessage)
|
|
380
|
+
await this.sender.deleteMessage(this.chatId, msgId);
|
|
381
|
+
}
|
|
382
|
+
catch (err) {
|
|
383
|
+
this.logger?.debug?.({ err, toolName }, 'Failed to delete consolidated tool message');
|
|
384
|
+
}
|
|
385
|
+
}).catch(err => {
|
|
386
|
+
this.logger?.error?.({ err }, 'resolveToolMessage queue error');
|
|
387
|
+
});
|
|
388
|
+
}
|
|
389
|
+
else {
|
|
390
|
+
this.updateConsolidatedToolMessage();
|
|
391
|
+
}
|
|
392
|
+
return;
|
|
393
|
+
}
|
|
394
|
+
// Compute final input summary if we don't have one yet
|
|
297
395
|
const inputJson = this.toolInputBuffers.get(blockId) ?? '';
|
|
298
|
-
|
|
396
|
+
if (!this.toolIndicatorLastSummary.has(blockId)) {
|
|
397
|
+
const summary = extractToolInputSummary(toolName, inputJson);
|
|
398
|
+
if (summary)
|
|
399
|
+
this.toolIndicatorLastSummary.set(blockId, summary);
|
|
400
|
+
}
|
|
401
|
+
// Store result stat for display
|
|
299
402
|
const resultStat = extractToolResultStat(toolName, resultContent, toolUseResult);
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
403
|
+
if (resultStat)
|
|
404
|
+
this.toolResolvedStats.set(blockId, resultStat);
|
|
405
|
+
// Mark as resolved
|
|
406
|
+
entry.resolved = true;
|
|
407
|
+
entry.isError = isError;
|
|
408
|
+
entry.elapsed = elapsed + 's';
|
|
304
409
|
// Clean up input buffer
|
|
305
410
|
this.toolInputBuffers.delete(blockId);
|
|
306
|
-
|
|
307
|
-
this.
|
|
308
|
-
try {
|
|
309
|
-
await this.sender.editMessage(this.chatId, msgId, html, 'HTML');
|
|
310
|
-
}
|
|
311
|
-
catch (err) {
|
|
312
|
-
// Edit failure on resolve β non-critical
|
|
313
|
-
this.logger?.debug?.({ err, toolName }, 'Failed to resolve tool indicator');
|
|
314
|
-
}
|
|
315
|
-
}).catch(err => {
|
|
316
|
-
this.logger?.error?.({ err }, 'resolveToolMessage queue error');
|
|
317
|
-
});
|
|
318
|
-
return this.sendQueue;
|
|
411
|
+
// Update the consolidated message
|
|
412
|
+
this.updateConsolidatedToolMessage();
|
|
319
413
|
}
|
|
320
|
-
/** Edit a specific tool indicator message by block ID. */
|
|
321
|
-
async editToolMessage(blockId,
|
|
322
|
-
|
|
323
|
-
if (!entry)
|
|
414
|
+
/** Edit a specific tool indicator message by block ID (updates the consolidated message). */
|
|
415
|
+
async editToolMessage(blockId, _html) {
|
|
416
|
+
if (!this.toolMessages.has(blockId))
|
|
324
417
|
return;
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
await this.sender.editMessage(this.chatId, entry.msgId, html, 'HTML');
|
|
328
|
-
}
|
|
329
|
-
catch (err) {
|
|
330
|
-
// Edit failure β non-critical
|
|
331
|
-
this.logger?.debug?.({ err }, 'Failed to edit tool message');
|
|
332
|
-
}
|
|
333
|
-
}).catch(err => {
|
|
334
|
-
this.logger?.error?.({ err }, 'editToolMessage queue error');
|
|
335
|
-
});
|
|
336
|
-
return this.sendQueue;
|
|
418
|
+
// With consolidation, just rebuild the consolidated message
|
|
419
|
+
this.updateConsolidatedToolMessage();
|
|
337
420
|
}
|
|
338
|
-
/** Delete a specific tool
|
|
421
|
+
/** Delete a specific tool from the consolidated indicator. */
|
|
339
422
|
async deleteToolMessage(blockId) {
|
|
340
|
-
|
|
341
|
-
if (!entry)
|
|
423
|
+
if (!this.toolMessages.has(blockId))
|
|
342
424
|
return;
|
|
343
425
|
this.toolMessages.delete(blockId);
|
|
344
|
-
this.
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
426
|
+
this.toolInputBuffers.delete(blockId);
|
|
427
|
+
this.toolIndicatorLastSummary.delete(blockId);
|
|
428
|
+
this.toolResolvedStats.delete(blockId);
|
|
429
|
+
if (this.toolMessages.size === 0 && this.consolidatedToolMsgId) {
|
|
430
|
+
// Last tool removed β delete the consolidated message
|
|
431
|
+
const msgId = this.consolidatedToolMsgId;
|
|
432
|
+
this.consolidatedToolMsgId = null;
|
|
433
|
+
this.sendQueue = this.sendQueue.then(async () => {
|
|
434
|
+
try {
|
|
435
|
+
if (this.sender.deleteMessage)
|
|
436
|
+
await this.sender.deleteMessage(this.chatId, msgId);
|
|
348
437
|
}
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
438
|
+
catch (err) {
|
|
439
|
+
this.logger?.debug?.({ err }, 'Failed to delete consolidated tool message');
|
|
440
|
+
}
|
|
441
|
+
}).catch(err => {
|
|
442
|
+
this.logger?.error?.({ err }, 'deleteToolMessage queue error');
|
|
443
|
+
});
|
|
444
|
+
}
|
|
445
|
+
else {
|
|
446
|
+
this.updateConsolidatedToolMessage();
|
|
447
|
+
}
|
|
358
448
|
}
|
|
359
|
-
/** Delete
|
|
449
|
+
/** Delete the consolidated tool indicator message. */
|
|
360
450
|
async deleteAllToolMessages() {
|
|
361
|
-
const ids = [...this.toolMessages.values()];
|
|
362
451
|
this.toolMessages.clear();
|
|
363
|
-
|
|
452
|
+
this.toolInputBuffers.clear();
|
|
453
|
+
this.toolIndicatorLastSummary.clear();
|
|
454
|
+
this.toolResolvedStats.clear();
|
|
455
|
+
if (!this.consolidatedToolMsgId || !this.sender.deleteMessage) {
|
|
456
|
+
this.consolidatedToolMsgId = null;
|
|
364
457
|
return;
|
|
458
|
+
}
|
|
459
|
+
const msgId = this.consolidatedToolMsgId;
|
|
460
|
+
this.consolidatedToolMsgId = null;
|
|
365
461
|
this.sendQueue = this.sendQueue.then(async () => {
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
// Delete failure β non-critical
|
|
372
|
-
this.logger?.debug?.({ err }, 'Failed to delete tool message in batch');
|
|
373
|
-
}
|
|
462
|
+
try {
|
|
463
|
+
await this.sender.deleteMessage(this.chatId, msgId);
|
|
464
|
+
}
|
|
465
|
+
catch (err) {
|
|
466
|
+
this.logger?.debug?.({ err }, 'Failed to delete consolidated tool message');
|
|
374
467
|
}
|
|
375
468
|
}).catch(err => {
|
|
376
469
|
this.logger?.error?.({ err }, 'deleteAllToolMessages queue error');
|
|
@@ -401,16 +494,19 @@ export class StreamAccumulator {
|
|
|
401
494
|
*/
|
|
402
495
|
buildFullText(includeSuffix = false) {
|
|
403
496
|
let text = '';
|
|
404
|
-
|
|
497
|
+
// Thinking goes in its own message (thinkingMessageId), not prepended to response
|
|
498
|
+
if (this.thinkingBuffer && !this.thinkingMessageId) {
|
|
499
|
+
// Only prepend if thinking didn't get its own message (edge case: no indicator was shown)
|
|
405
500
|
const thinkingPreview = this.thinkingBuffer.length > 1024
|
|
406
501
|
? this.thinkingBuffer.slice(0, 1024) + 'β¦'
|
|
407
502
|
: this.thinkingBuffer;
|
|
408
|
-
text +=
|
|
503
|
+
text += formatSystemMessage('thinking', markdownToTelegramHtml(thinkingPreview), true) + '\n';
|
|
409
504
|
}
|
|
410
505
|
// Convert markdown buffer to HTML-safe text
|
|
411
506
|
text += makeHtmlSafe(this.buffer);
|
|
412
507
|
if (includeSuffix && this.turnUsage) {
|
|
413
|
-
|
|
508
|
+
const usageContent = formatUsageFooter(this.turnUsage, this.turnUsage.model).replace(/<\/?i>/g, '');
|
|
509
|
+
text += '\n' + formatSystemMessage('usage', usageContent);
|
|
414
510
|
}
|
|
415
511
|
return { text, hasHtmlSuffix: includeSuffix && !!this.turnUsage };
|
|
416
512
|
}
|
|
@@ -467,17 +563,29 @@ export class StreamAccumulator {
|
|
|
467
563
|
clearTimeout(this.editTimer);
|
|
468
564
|
this.editTimer = null;
|
|
469
565
|
}
|
|
566
|
+
// Update thinking message with final content (if it has its own message)
|
|
567
|
+
if (this.thinkingBuffer && this.thinkingMessageId) {
|
|
568
|
+
const thinkingPreview = this.thinkingBuffer.length > 1024
|
|
569
|
+
? this.thinkingBuffer.slice(0, 1024) + 'β¦'
|
|
570
|
+
: this.thinkingBuffer;
|
|
571
|
+
try {
|
|
572
|
+
await this.sender.editMessage(this.chatId, this.thinkingMessageId, formatSystemMessage('thinking', markdownToTelegramHtml(thinkingPreview), true), 'HTML');
|
|
573
|
+
}
|
|
574
|
+
catch {
|
|
575
|
+
// Thinking message edit failed β not critical
|
|
576
|
+
}
|
|
577
|
+
}
|
|
470
578
|
if (this.buffer) {
|
|
471
|
-
// Final edit with complete text
|
|
579
|
+
// Final edit with complete text (thinking is in its own message)
|
|
472
580
|
const { text } = this.buildFullText(true);
|
|
473
581
|
await this.sendOrEdit(text, true); // buildFullText already does makeHtmlSafe
|
|
474
582
|
}
|
|
475
|
-
else if (this.thinkingBuffer && this.thinkingIndicatorShown) {
|
|
476
|
-
// Only thinking happened, no text β show
|
|
583
|
+
else if (this.thinkingBuffer && !this.thinkingMessageId && this.thinkingIndicatorShown) {
|
|
584
|
+
// Only thinking happened, no text, no separate message β show as expandable blockquote
|
|
477
585
|
const thinkingPreview = this.thinkingBuffer.length > 1024
|
|
478
586
|
? this.thinkingBuffer.slice(0, 1024) + 'β¦'
|
|
479
587
|
: this.thinkingBuffer;
|
|
480
|
-
await this.sendOrEdit(
|
|
588
|
+
await this.sendOrEdit(formatSystemMessage('thinking', markdownToTelegramHtml(thinkingPreview), true), true);
|
|
481
589
|
}
|
|
482
590
|
}
|
|
483
591
|
clearEditTimer() {
|
|
@@ -496,6 +604,7 @@ export class StreamAccumulator {
|
|
|
496
604
|
this.currentToolBlockId = null;
|
|
497
605
|
this.lastEditTime = 0;
|
|
498
606
|
this.thinkingIndicatorShown = false;
|
|
607
|
+
this.thinkingMessageId = null;
|
|
499
608
|
this.finished = false;
|
|
500
609
|
this.turnUsage = null;
|
|
501
610
|
this._lastMsgStartCtx = null;
|
|
@@ -503,12 +612,18 @@ export class StreamAccumulator {
|
|
|
503
612
|
}
|
|
504
613
|
/** Full reset: also clears tgMessageId (next send creates a new message).
|
|
505
614
|
* Chains on the existing sendQueue so any pending finalize() edits complete first.
|
|
506
|
-
*
|
|
615
|
+
* Consolidated tool message resets so next turn starts a fresh batch. */
|
|
507
616
|
reset() {
|
|
508
617
|
const prevQueue = this.sendQueue;
|
|
509
618
|
this.softReset();
|
|
510
619
|
this.tgMessageId = null;
|
|
511
620
|
this.messageIds = [];
|
|
621
|
+
// Reset tool consolidation for next turn
|
|
622
|
+
this.consolidatedToolMsgId = null;
|
|
623
|
+
this.toolMessages.clear();
|
|
624
|
+
this.toolInputBuffers.clear();
|
|
625
|
+
this.toolIndicatorLastSummary.clear();
|
|
626
|
+
this.toolResolvedStats.clear();
|
|
512
627
|
this.sendQueue = prevQueue.catch(() => { }); // swallow errors from prev turn
|
|
513
628
|
}
|
|
514
629
|
}
|
|
@@ -727,7 +842,7 @@ export function formatUsageFooter(usage, _model) {
|
|
|
727
842
|
const ctxPct = Math.round(totalCtx / CONTEXT_WINDOW * 100);
|
|
728
843
|
const overLimit = ctxPct > 90;
|
|
729
844
|
const parts = [
|
|
730
|
-
|
|
845
|
+
`${formatTokens(usage.inputTokens)} in`,
|
|
731
846
|
`${formatTokens(usage.outputTokens)} out`,
|
|
732
847
|
];
|
|
733
848
|
if (usage.costUsd != null) {
|
|
@@ -878,10 +993,12 @@ export class SubAgentTracker {
|
|
|
878
993
|
continue;
|
|
879
994
|
info.status = 'completed';
|
|
880
995
|
const label = info.label || info.toolName;
|
|
881
|
-
const text =
|
|
996
|
+
const text = `<blockquote>π€ ${escapeHtml(label)} β see main message</blockquote>`;
|
|
997
|
+
const agentMsgId = info.tgMessageId;
|
|
882
998
|
this.sendQueue = this.sendQueue.then(async () => {
|
|
883
999
|
try {
|
|
884
|
-
await this.sender.editMessage(this.chatId,
|
|
1000
|
+
await this.sender.editMessage(this.chatId, agentMsgId, text, 'HTML');
|
|
1001
|
+
await this.sender.setReaction?.(this.chatId, agentMsgId, 'π');
|
|
885
1002
|
}
|
|
886
1003
|
catch (err) {
|
|
887
1004
|
// Non-critical β edit failure on dispatched agent status
|
|
@@ -956,14 +1073,17 @@ export class SubAgentTracker {
|
|
|
956
1073
|
return;
|
|
957
1074
|
info.status = 'completed';
|
|
958
1075
|
const label = info.label || info.toolName;
|
|
959
|
-
// Truncate result for blockquote (TG message limit ~4096 chars)
|
|
960
1076
|
const maxResultLen = 3500;
|
|
961
1077
|
const resultText = result.length > maxResultLen ? result.slice(0, maxResultLen) + 'β¦' : result;
|
|
962
|
-
//
|
|
963
|
-
const text = `<blockquote
|
|
1078
|
+
// Blockquote header + expandable result (not nested β TG doesn't support nested blockquotes)
|
|
1079
|
+
const text = `<blockquote>π€ ${escapeHtml(label)}</blockquote>\n<blockquote expandable>${escapeHtml(resultText)}</blockquote>`;
|
|
1080
|
+
const msgId = info.tgMessageId;
|
|
964
1081
|
this.sendQueue = this.sendQueue.then(async () => {
|
|
965
1082
|
try {
|
|
966
|
-
await this.sender.editMessage(this.chatId,
|
|
1083
|
+
await this.sender.editMessage(this.chatId, msgId, text, 'HTML');
|
|
1084
|
+
if (this.sender.setReaction) {
|
|
1085
|
+
await this.sender.setReaction(this.chatId, msgId, 'π');
|
|
1086
|
+
}
|
|
967
1087
|
}
|
|
968
1088
|
catch {
|
|
969
1089
|
// Edit failure on tool result β non-critical
|
|
@@ -1000,7 +1120,7 @@ export class SubAgentTracker {
|
|
|
1000
1120
|
else {
|
|
1001
1121
|
this.sendQueue = this.sendQueue.then(async () => {
|
|
1002
1122
|
try {
|
|
1003
|
-
const msgId = await this.sender.sendMessage(this.chatId, '
|
|
1123
|
+
const msgId = await this.sender.sendMessage(this.chatId, '<blockquote>π€ Starting sub-agentβ¦</blockquote>', 'HTML');
|
|
1004
1124
|
info.tgMessageId = msgId;
|
|
1005
1125
|
this.consolidatedAgentMsgId = msgId;
|
|
1006
1126
|
}
|
|
@@ -1024,7 +1144,7 @@ export class SubAgentTracker {
|
|
|
1024
1144
|
: 'Workingβ¦';
|
|
1025
1145
|
lines.push(`π€ ${escapeHtml(label)} β ${status}`);
|
|
1026
1146
|
}
|
|
1027
|
-
const text = lines.join('\n')
|
|
1147
|
+
const text = `<blockquote>${lines.join('\n')}</blockquote>`;
|
|
1028
1148
|
this.sendQueue = this.sendQueue.then(async () => {
|
|
1029
1149
|
try {
|
|
1030
1150
|
await this.sender.editMessage(this.chatId, msgId, text, 'HTML');
|
|
@@ -1264,7 +1384,7 @@ export class SubAgentTracker {
|
|
|
1264
1384
|
lines.push(`π€ ${escapeHtml(label)} β ${status}`);
|
|
1265
1385
|
}
|
|
1266
1386
|
}
|
|
1267
|
-
const text = lines.join('\n')
|
|
1387
|
+
const text = `<blockquote>${lines.join('\n')}</blockquote>`;
|
|
1268
1388
|
this.sendQueue = this.sendQueue.then(async () => {
|
|
1269
1389
|
try {
|
|
1270
1390
|
await this.sender.editMessage(this.chatId, msgId, text, 'HTML');
|
|
@@ -1295,7 +1415,7 @@ export class SubAgentTracker {
|
|
|
1295
1415
|
}
|
|
1296
1416
|
}
|
|
1297
1417
|
// ββ Utility: split a completed text into TG-sized chunks ββ
|
|
1298
|
-
export function splitText(text, maxLength =
|
|
1418
|
+
export function splitText(text, maxLength = 3500) {
|
|
1299
1419
|
if (text.length <= maxLength)
|
|
1300
1420
|
return [text];
|
|
1301
1421
|
const chunks = [];
|