@fonz/tgcc 0.6.13 → 0.6.14
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/dist/bridge.js +39 -24
- package/dist/bridge.js.map +1 -1
- package/dist/cc-process.d.ts +6 -0
- package/dist/cc-process.js +53 -2
- package/dist/cc-process.js.map +1 -1
- package/dist/cc-protocol.d.ts +1 -0
- package/dist/cc-protocol.js.map +1 -1
- package/dist/streaming.d.ts +30 -4
- package/dist/streaming.js +331 -38
- package/dist/streaming.js.map +1 -1
- package/dist/telegram.d.ts +1 -0
- package/dist/telegram.js +3 -0
- package/dist/telegram.js.map +1 -1
- package/package.json +1 -1
package/dist/streaming.js
CHANGED
|
@@ -34,6 +34,8 @@ export class StreamAccumulator {
|
|
|
34
34
|
sender;
|
|
35
35
|
editIntervalMs;
|
|
36
36
|
splitThreshold;
|
|
37
|
+
logger;
|
|
38
|
+
onError;
|
|
37
39
|
// State
|
|
38
40
|
tgMessageId = null;
|
|
39
41
|
buffer = '';
|
|
@@ -43,16 +45,21 @@ export class StreamAccumulator {
|
|
|
43
45
|
lastEditTime = 0;
|
|
44
46
|
editTimer = null;
|
|
45
47
|
thinkingIndicatorShown = false;
|
|
46
|
-
toolIndicators = [];
|
|
47
48
|
messageIds = []; // all message IDs sent during this turn
|
|
48
49
|
finished = false;
|
|
49
50
|
sendQueue = Promise.resolve();
|
|
50
51
|
turnUsage = null;
|
|
52
|
+
// Per-tool-use independent indicator messages (persists across resets)
|
|
53
|
+
toolMessages = new Map();
|
|
54
|
+
toolInputBuffers = new Map(); // tool block ID → accumulated input JSON
|
|
55
|
+
currentToolBlockId = null;
|
|
51
56
|
constructor(options) {
|
|
52
57
|
this.chatId = options.chatId;
|
|
53
58
|
this.sender = options.sender;
|
|
54
59
|
this.editIntervalMs = options.editIntervalMs ?? 1000;
|
|
55
60
|
this.splitThreshold = options.splitThreshold ?? 4000;
|
|
61
|
+
this.logger = options.logger;
|
|
62
|
+
this.onError = options.onError;
|
|
56
63
|
}
|
|
57
64
|
get allMessageIds() { return [...this.messageIds]; }
|
|
58
65
|
/** Set usage stats for the current turn (called from bridge on result event) */
|
|
@@ -91,20 +98,23 @@ export class StreamAccumulator {
|
|
|
91
98
|
if (blockType === 'thinking') {
|
|
92
99
|
this.currentBlockType = 'thinking';
|
|
93
100
|
if (!this.thinkingIndicatorShown && !this.buffer) {
|
|
94
|
-
await this.sendOrEdit('<
|
|
101
|
+
await this.sendOrEdit('<blockquote>💭 Thinking...</blockquote>', true);
|
|
95
102
|
this.thinkingIndicatorShown = true;
|
|
96
103
|
}
|
|
97
104
|
}
|
|
98
105
|
else if (blockType === 'text') {
|
|
99
106
|
this.currentBlockType = 'text';
|
|
100
|
-
//
|
|
101
|
-
this.
|
|
107
|
+
// Text always gets its own message — don't touch tool indicator messages
|
|
108
|
+
if (!this.tgMessageId) {
|
|
109
|
+
// Will create a new message on next sendOrEdit
|
|
110
|
+
}
|
|
102
111
|
}
|
|
103
112
|
else if (blockType === 'tool_use') {
|
|
104
113
|
this.currentBlockType = 'tool_use';
|
|
105
|
-
const
|
|
106
|
-
this.
|
|
107
|
-
|
|
114
|
+
const block = event.content_block;
|
|
115
|
+
this.currentToolBlockId = block.id;
|
|
116
|
+
// Send an independent indicator message for this tool_use block
|
|
117
|
+
await this.sendToolIndicator(block.id, block.name);
|
|
108
118
|
}
|
|
109
119
|
else if (blockType === 'image') {
|
|
110
120
|
this.currentBlockType = 'image';
|
|
@@ -130,22 +140,37 @@ export class StreamAccumulator {
|
|
|
130
140
|
this.thinkingBuffer += delta.thinking;
|
|
131
141
|
}
|
|
132
142
|
}
|
|
143
|
+
else if (this.currentBlockType === 'tool_use' && 'delta' in event) {
|
|
144
|
+
const delta = event.delta;
|
|
145
|
+
if (delta?.type === 'input_json_delta' && delta.partial_json && this.currentToolBlockId) {
|
|
146
|
+
const blockId = this.currentToolBlockId;
|
|
147
|
+
const prev = this.toolInputBuffers.get(blockId) ?? '';
|
|
148
|
+
this.toolInputBuffers.set(blockId, prev + delta.partial_json);
|
|
149
|
+
// Update indicator with input preview once we have enough
|
|
150
|
+
await this.updateToolIndicatorWithInput(blockId);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
133
153
|
else if (this.currentBlockType === 'image' && 'delta' in event) {
|
|
134
154
|
const delta = event.delta;
|
|
135
155
|
if (delta?.type === 'image_delta' && delta.data) {
|
|
136
156
|
this.imageBase64Buffer += delta.data;
|
|
137
157
|
}
|
|
138
158
|
}
|
|
139
|
-
// Ignore input_json_delta content
|
|
140
159
|
}
|
|
141
160
|
// ── TG message management ──
|
|
142
161
|
/** Send or edit a message. If rawHtml is true, text is already HTML-safe. */
|
|
143
162
|
async sendOrEdit(text, rawHtml = false) {
|
|
144
|
-
this.sendQueue = this.sendQueue.then(() => this._doSendOrEdit(text, rawHtml))
|
|
163
|
+
this.sendQueue = this.sendQueue.then(() => this._doSendOrEdit(text, rawHtml)).catch(err => {
|
|
164
|
+
this.logger?.error?.({ err }, 'sendOrEdit failed');
|
|
165
|
+
this.onError?.(err, 'Failed to send/edit message');
|
|
166
|
+
});
|
|
145
167
|
return this.sendQueue;
|
|
146
168
|
}
|
|
147
169
|
async _doSendOrEdit(text, rawHtml = false) {
|
|
148
|
-
|
|
170
|
+
let safeText = (rawHtml ? text : makeHtmlSafe(text)) || '...';
|
|
171
|
+
// Guard against HTML that has tags but no visible text content (e.g. "<b></b>")
|
|
172
|
+
if (!safeText.replace(/<[^>]*>/g, '').trim())
|
|
173
|
+
safeText = '...';
|
|
149
174
|
// Update timing BEFORE API call to prevent races
|
|
150
175
|
this.lastEditTime = Date.now();
|
|
151
176
|
try {
|
|
@@ -158,17 +183,20 @@ export class StreamAccumulator {
|
|
|
158
183
|
}
|
|
159
184
|
}
|
|
160
185
|
catch (err) {
|
|
161
|
-
|
|
162
|
-
|
|
186
|
+
const errorCode = err && typeof err === 'object' && 'error_code' in err
|
|
187
|
+
? err.error_code : 0;
|
|
188
|
+
// Handle TG rate limit (429) — retry
|
|
189
|
+
if (errorCode === 429) {
|
|
163
190
|
const retryAfter = err.parameters?.retry_after ?? 5;
|
|
164
191
|
this.editIntervalMs = Math.min(this.editIntervalMs * 2, 5000);
|
|
165
192
|
await sleep(retryAfter * 1000);
|
|
166
193
|
return this._doSendOrEdit(text);
|
|
167
194
|
}
|
|
168
|
-
// Ignore "message is not modified" errors
|
|
195
|
+
// Ignore "message is not modified" errors (harmless)
|
|
169
196
|
if (err instanceof Error && err.message.includes('message is not modified'))
|
|
170
197
|
return;
|
|
171
|
-
throw
|
|
198
|
+
// 400 (bad request), 403 (forbidden), and all other errors — log and skip, never throw
|
|
199
|
+
this.logger?.error?.({ err, errorCode }, 'Telegram API error in _doSendOrEdit — skipping');
|
|
172
200
|
}
|
|
173
201
|
}
|
|
174
202
|
async sendImage() {
|
|
@@ -181,16 +209,149 @@ export class StreamAccumulator {
|
|
|
181
209
|
}
|
|
182
210
|
catch (err) {
|
|
183
211
|
// Fall back to text indicator on failure
|
|
212
|
+
this.logger?.error?.({ err }, 'Failed to send image');
|
|
184
213
|
this.buffer += '\n[Image could not be sent]';
|
|
185
214
|
}
|
|
186
215
|
this.imageBase64Buffer = '';
|
|
187
216
|
}
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
const
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
217
|
+
/** Send an independent tool indicator message (not through the accumulator's sendOrEdit). */
|
|
218
|
+
async sendToolIndicator(blockId, toolName) {
|
|
219
|
+
const startTime = Date.now();
|
|
220
|
+
this.sendQueue = this.sendQueue.then(async () => {
|
|
221
|
+
try {
|
|
222
|
+
const html = `<blockquote expandable>⚡ ${escapeHtml(toolName)}…</blockquote>`;
|
|
223
|
+
const msgId = await this.sender.sendMessage(this.chatId, html, 'HTML');
|
|
224
|
+
this.toolMessages.set(blockId, { msgId, toolName, startTime });
|
|
225
|
+
}
|
|
226
|
+
catch (err) {
|
|
227
|
+
// Tool indicator is non-critical — log and continue
|
|
228
|
+
this.logger?.debug?.({ err, toolName }, 'Failed to send tool indicator');
|
|
229
|
+
}
|
|
230
|
+
}).catch(err => {
|
|
231
|
+
this.logger?.error?.({ err }, 'sendToolIndicator queue error');
|
|
232
|
+
});
|
|
233
|
+
return this.sendQueue;
|
|
234
|
+
}
|
|
235
|
+
/** Update a tool indicator message with input preview once the JSON value is complete. */
|
|
236
|
+
toolIndicatorLastSummary = new Map(); // blockId → last rendered summary
|
|
237
|
+
async updateToolIndicatorWithInput(blockId) {
|
|
238
|
+
const entry = this.toolMessages.get(blockId);
|
|
239
|
+
if (!entry)
|
|
240
|
+
return;
|
|
241
|
+
const inputJson = this.toolInputBuffers.get(blockId) ?? '';
|
|
242
|
+
// Only extract from complete JSON (try parse succeeds) or complete regex match
|
|
243
|
+
// (the value must have a closing quote to avoid truncated paths)
|
|
244
|
+
const summary = extractToolInputSummary(entry.toolName, inputJson, 120, true);
|
|
245
|
+
if (!summary)
|
|
246
|
+
return; // not enough input yet or value still streaming
|
|
247
|
+
// Skip if summary hasn't changed since last edit
|
|
248
|
+
if (this.toolIndicatorLastSummary.get(blockId) === summary)
|
|
249
|
+
return;
|
|
250
|
+
this.toolIndicatorLastSummary.set(blockId, summary);
|
|
251
|
+
const codeLine = `\n<code>${escapeHtml(summary)}</code>`;
|
|
252
|
+
const html = `<blockquote expandable>⚡ ${escapeHtml(entry.toolName)}…${codeLine}</blockquote>`;
|
|
253
|
+
this.sendQueue = this.sendQueue.then(async () => {
|
|
254
|
+
try {
|
|
255
|
+
await this.sender.editMessage(this.chatId, entry.msgId, html, 'HTML');
|
|
256
|
+
}
|
|
257
|
+
catch (err) {
|
|
258
|
+
// "message is not modified" or other edit failure — non-critical
|
|
259
|
+
this.logger?.debug?.({ err }, 'Failed to update tool indicator with input');
|
|
260
|
+
}
|
|
261
|
+
}).catch(err => {
|
|
262
|
+
this.logger?.error?.({ err }, 'updateToolIndicatorWithInput queue error');
|
|
263
|
+
});
|
|
264
|
+
return this.sendQueue;
|
|
265
|
+
}
|
|
266
|
+
/** Resolve a tool indicator with success/failure status. Edits to a compact summary with input detail. */
|
|
267
|
+
async resolveToolMessage(blockId, isError, errorMessage, resultContent) {
|
|
268
|
+
const entry = this.toolMessages.get(blockId);
|
|
269
|
+
if (!entry)
|
|
270
|
+
return;
|
|
271
|
+
const { msgId, toolName, startTime } = entry;
|
|
272
|
+
const elapsed = ((Date.now() - startTime) / 1000).toFixed(1);
|
|
273
|
+
const inputJson = this.toolInputBuffers.get(blockId) ?? '';
|
|
274
|
+
const summary = extractToolInputSummary(toolName, inputJson);
|
|
275
|
+
const resultStat = resultContent ? extractToolResultStat(toolName, resultContent) : '';
|
|
276
|
+
const codeLine = summary ? `\n<code>${escapeHtml(summary)}</code>` : '';
|
|
277
|
+
const statLine = resultStat ? `\n${escapeHtml(resultStat)}` : '';
|
|
278
|
+
const icon = isError ? '❌' : '✅';
|
|
279
|
+
const html = `<blockquote expandable>${icon} ${escapeHtml(toolName)} (${elapsed}s)${codeLine}${statLine}</blockquote>`;
|
|
280
|
+
// Clean up input buffer
|
|
281
|
+
this.toolInputBuffers.delete(blockId);
|
|
282
|
+
this.toolIndicatorLastSummary.delete(blockId);
|
|
283
|
+
this.sendQueue = this.sendQueue.then(async () => {
|
|
284
|
+
try {
|
|
285
|
+
await this.sender.editMessage(this.chatId, msgId, html, 'HTML');
|
|
286
|
+
}
|
|
287
|
+
catch (err) {
|
|
288
|
+
// Edit failure on resolve — non-critical
|
|
289
|
+
this.logger?.debug?.({ err, toolName }, 'Failed to resolve tool indicator');
|
|
290
|
+
}
|
|
291
|
+
}).catch(err => {
|
|
292
|
+
this.logger?.error?.({ err }, 'resolveToolMessage queue error');
|
|
293
|
+
});
|
|
294
|
+
return this.sendQueue;
|
|
295
|
+
}
|
|
296
|
+
/** Edit a specific tool indicator message by block ID. */
|
|
297
|
+
async editToolMessage(blockId, html) {
|
|
298
|
+
const entry = this.toolMessages.get(blockId);
|
|
299
|
+
if (!entry)
|
|
300
|
+
return;
|
|
301
|
+
this.sendQueue = this.sendQueue.then(async () => {
|
|
302
|
+
try {
|
|
303
|
+
await this.sender.editMessage(this.chatId, entry.msgId, html, 'HTML');
|
|
304
|
+
}
|
|
305
|
+
catch (err) {
|
|
306
|
+
// Edit failure — non-critical
|
|
307
|
+
this.logger?.debug?.({ err }, 'Failed to edit tool message');
|
|
308
|
+
}
|
|
309
|
+
}).catch(err => {
|
|
310
|
+
this.logger?.error?.({ err }, 'editToolMessage queue error');
|
|
311
|
+
});
|
|
312
|
+
return this.sendQueue;
|
|
313
|
+
}
|
|
314
|
+
/** Delete a specific tool indicator message by block ID. */
|
|
315
|
+
async deleteToolMessage(blockId) {
|
|
316
|
+
const entry = this.toolMessages.get(blockId);
|
|
317
|
+
if (!entry)
|
|
318
|
+
return;
|
|
319
|
+
this.toolMessages.delete(blockId);
|
|
320
|
+
this.sendQueue = this.sendQueue.then(async () => {
|
|
321
|
+
try {
|
|
322
|
+
if (this.sender.deleteMessage) {
|
|
323
|
+
await this.sender.deleteMessage(this.chatId, entry.msgId);
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
catch (err) {
|
|
327
|
+
// Delete failure — non-critical
|
|
328
|
+
this.logger?.debug?.({ err }, 'Failed to delete tool message');
|
|
329
|
+
}
|
|
330
|
+
}).catch(err => {
|
|
331
|
+
this.logger?.error?.({ err }, 'deleteToolMessage queue error');
|
|
332
|
+
});
|
|
333
|
+
return this.sendQueue;
|
|
334
|
+
}
|
|
335
|
+
/** Delete all tool indicator messages. */
|
|
336
|
+
async deleteAllToolMessages() {
|
|
337
|
+
const ids = [...this.toolMessages.values()];
|
|
338
|
+
this.toolMessages.clear();
|
|
339
|
+
if (!this.sender.deleteMessage)
|
|
340
|
+
return;
|
|
341
|
+
this.sendQueue = this.sendQueue.then(async () => {
|
|
342
|
+
for (const { msgId } of ids) {
|
|
343
|
+
try {
|
|
344
|
+
await this.sender.deleteMessage(this.chatId, msgId);
|
|
345
|
+
}
|
|
346
|
+
catch (err) {
|
|
347
|
+
// Delete failure — non-critical
|
|
348
|
+
this.logger?.debug?.({ err }, 'Failed to delete tool message in batch');
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
}).catch(err => {
|
|
352
|
+
this.logger?.error?.({ err }, 'deleteAllToolMessages queue error');
|
|
353
|
+
});
|
|
354
|
+
return this.sendQueue;
|
|
194
355
|
}
|
|
195
356
|
async throttledEdit() {
|
|
196
357
|
const now = Date.now();
|
|
@@ -301,24 +462,24 @@ export class StreamAccumulator {
|
|
|
301
462
|
this.editTimer = null;
|
|
302
463
|
}
|
|
303
464
|
}
|
|
304
|
-
/** Soft reset: clear buffer/state but keep tgMessageId so next turn edits the same message
|
|
465
|
+
/** Soft reset: clear buffer/state but keep tgMessageId so next turn edits the same message.
|
|
466
|
+
* toolMessages persists across resets — they are independent of the text accumulator. */
|
|
305
467
|
softReset() {
|
|
306
468
|
this.buffer = '';
|
|
307
469
|
this.thinkingBuffer = '';
|
|
308
470
|
this.imageBase64Buffer = '';
|
|
309
471
|
this.currentBlockType = null;
|
|
472
|
+
this.currentToolBlockId = null;
|
|
310
473
|
this.lastEditTime = 0;
|
|
311
474
|
this.thinkingIndicatorShown = false;
|
|
312
|
-
this.toolIndicators = [];
|
|
313
475
|
this.finished = false;
|
|
314
476
|
this.turnUsage = null;
|
|
315
|
-
this.clearEditTimer();
|
|
477
|
+
this.clearEditTimer();
|
|
316
478
|
}
|
|
317
479
|
/** Full reset: also clears tgMessageId (next send creates a new message).
|
|
318
|
-
* Chains on the existing sendQueue so any pending finalize() edits complete first.
|
|
480
|
+
* Chains on the existing sendQueue so any pending finalize() edits complete first.
|
|
481
|
+
* toolMessages persists — they are independent fire-and-forget messages. */
|
|
319
482
|
reset() {
|
|
320
|
-
// Chain on the existing queue so pending sends (e.g. finalize) complete
|
|
321
|
-
// before the new turn starts sending.
|
|
322
483
|
const prevQueue = this.sendQueue;
|
|
323
484
|
this.softReset();
|
|
324
485
|
this.tgMessageId = null;
|
|
@@ -327,6 +488,130 @@ export class StreamAccumulator {
|
|
|
327
488
|
}
|
|
328
489
|
}
|
|
329
490
|
// ── Helpers ──
|
|
491
|
+
/** Extract a human-readable summary from a tool's input JSON (may be partial/incomplete). */
|
|
492
|
+
/** Shorten absolute paths to relative-ish display: /home/fonz/Botverse/KYO/src/foo.ts → KYO/src/foo.ts */
|
|
493
|
+
function shortenPath(p) {
|
|
494
|
+
// Strip common prefixes
|
|
495
|
+
return p
|
|
496
|
+
.replace(/^\/home\/[^/]+\/Botverse\//, '')
|
|
497
|
+
.replace(/^\/home\/[^/]+\/Projects\//, '')
|
|
498
|
+
.replace(/^\/home\/[^/]+\//, '~/');
|
|
499
|
+
}
|
|
500
|
+
function extractToolInputSummary(toolName, inputJson, maxLen = 120, requireComplete = false) {
|
|
501
|
+
if (!inputJson)
|
|
502
|
+
return null;
|
|
503
|
+
// Determine which field(s) to look for based on tool name
|
|
504
|
+
const fieldsByTool = {
|
|
505
|
+
Bash: ['command'],
|
|
506
|
+
Read: ['file_path', 'path'],
|
|
507
|
+
Write: ['file_path', 'path'],
|
|
508
|
+
Edit: ['file_path', 'path'],
|
|
509
|
+
MultiEdit: ['file_path', 'path'],
|
|
510
|
+
Search: ['pattern', 'query'],
|
|
511
|
+
Grep: ['pattern', 'query'],
|
|
512
|
+
Glob: ['pattern'],
|
|
513
|
+
TodoWrite: ['content', 'task', 'text'],
|
|
514
|
+
};
|
|
515
|
+
const skipTools = new Set(['TodoRead']);
|
|
516
|
+
if (skipTools.has(toolName))
|
|
517
|
+
return null;
|
|
518
|
+
const fields = fieldsByTool[toolName];
|
|
519
|
+
const isPathTool = ['Read', 'Write', 'Edit', 'MultiEdit'].includes(toolName);
|
|
520
|
+
// Try parsing complete JSON first
|
|
521
|
+
try {
|
|
522
|
+
const parsed = JSON.parse(inputJson);
|
|
523
|
+
if (fields) {
|
|
524
|
+
for (const f of fields) {
|
|
525
|
+
if (typeof parsed[f] === 'string' && parsed[f].trim()) {
|
|
526
|
+
let val = parsed[f].trim();
|
|
527
|
+
if (isPathTool)
|
|
528
|
+
val = shortenPath(val);
|
|
529
|
+
return val.length > maxLen ? val.slice(0, maxLen) + '…' : val;
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
// Default: first string value
|
|
534
|
+
for (const val of Object.values(parsed)) {
|
|
535
|
+
if (typeof val === 'string' && val.trim()) {
|
|
536
|
+
const v = val.trim();
|
|
537
|
+
return v.length > maxLen ? v.slice(0, maxLen) + '…' : v;
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
return null;
|
|
541
|
+
}
|
|
542
|
+
catch {
|
|
543
|
+
if (requireComplete)
|
|
544
|
+
return null; // Only use fully-parsed JSON when requireComplete
|
|
545
|
+
// Partial JSON — regex extraction (only used for final resolve, not live preview)
|
|
546
|
+
const targetFields = fields ?? ['command', 'file_path', 'path', 'pattern', 'query'];
|
|
547
|
+
for (const key of targetFields) {
|
|
548
|
+
const re = new RegExp(`"${key}"\\s*:\\s*"((?:[^"\\\\]|\\\\.)*)"`, 'i'); // require closing quote
|
|
549
|
+
const m = inputJson.match(re);
|
|
550
|
+
if (m?.[1]) {
|
|
551
|
+
let val = m[1].replace(/\\n/g, ' ').replace(/\\t/g, ' ').replace(/\\"/g, '"');
|
|
552
|
+
if (isPathTool)
|
|
553
|
+
val = shortenPath(val);
|
|
554
|
+
return val.length > maxLen ? val.slice(0, maxLen) + '…' : val;
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
return null;
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
/** Extract a compact stat from a tool result for display in the indicator. */
|
|
561
|
+
function extractToolResultStat(toolName, content) {
|
|
562
|
+
if (!content)
|
|
563
|
+
return '';
|
|
564
|
+
const first = content.split('\n')[0].trim();
|
|
565
|
+
switch (toolName) {
|
|
566
|
+
case 'Write': {
|
|
567
|
+
// "Successfully wrote 42 lines to src/foo.ts" or byte count
|
|
568
|
+
const lines = content.match(/(\d+)\s*lines?/i);
|
|
569
|
+
const bytes = content.match(/(\d[\d,.]*)\s*(bytes?|KB|MB)/i);
|
|
570
|
+
if (lines)
|
|
571
|
+
return `${lines[1]} lines`;
|
|
572
|
+
if (bytes)
|
|
573
|
+
return `${bytes[1]} ${bytes[2]}`;
|
|
574
|
+
return first.length > 60 ? first.slice(0, 60) + '…' : first;
|
|
575
|
+
}
|
|
576
|
+
case 'Edit':
|
|
577
|
+
case 'MultiEdit': {
|
|
578
|
+
// "Edited src/foo.ts: replaced 3 lines" or snippet
|
|
579
|
+
const replaced = content.match(/replaced\s+(\d+)\s*lines?/i);
|
|
580
|
+
const chars = content.match(/(\d+)\s*characters?/i);
|
|
581
|
+
if (replaced)
|
|
582
|
+
return `${replaced[1]} lines replaced`;
|
|
583
|
+
if (chars)
|
|
584
|
+
return `${chars[1]} chars changed`;
|
|
585
|
+
return first.length > 60 ? first.slice(0, 60) + '…' : first;
|
|
586
|
+
}
|
|
587
|
+
case 'Bash': {
|
|
588
|
+
// Show exit code or first line of output (truncated)
|
|
589
|
+
const exit = content.match(/exit\s*code\s*[:=]?\s*(\d+)/i);
|
|
590
|
+
if (exit && exit[1] !== '0')
|
|
591
|
+
return `exit ${exit[1]}`;
|
|
592
|
+
// Count output lines
|
|
593
|
+
const outputLines = content.split('\n').filter(l => l.trim()).length;
|
|
594
|
+
if (outputLines > 3)
|
|
595
|
+
return `${outputLines} lines output`;
|
|
596
|
+
return first.length > 60 ? first.slice(0, 60) + '…' : first;
|
|
597
|
+
}
|
|
598
|
+
case 'Read': {
|
|
599
|
+
const lines = content.split('\n').length;
|
|
600
|
+
return `${lines} lines`;
|
|
601
|
+
}
|
|
602
|
+
case 'Search':
|
|
603
|
+
case 'Grep':
|
|
604
|
+
case 'Glob': {
|
|
605
|
+
const matches = content.match(/(\d+)\s*(match|result|file)/i);
|
|
606
|
+
if (matches)
|
|
607
|
+
return `${matches[1]} ${matches[2]}s`;
|
|
608
|
+
const resultLines = content.split('\n').filter(l => l.trim()).length;
|
|
609
|
+
return `${resultLines} results`;
|
|
610
|
+
}
|
|
611
|
+
default:
|
|
612
|
+
return first.length > 60 ? first.slice(0, 60) + '…' : first;
|
|
613
|
+
}
|
|
614
|
+
}
|
|
330
615
|
function findSplitPoint(text, threshold) {
|
|
331
616
|
// Try to split at paragraph break
|
|
332
617
|
const paragraphBreak = text.lastIndexOf('\n\n', threshold);
|
|
@@ -510,8 +795,10 @@ export class SubAgentTracker {
|
|
|
510
795
|
try {
|
|
511
796
|
await this.sender.editMessage(this.chatId, info.tgMessageId, text, 'HTML');
|
|
512
797
|
}
|
|
513
|
-
catch {
|
|
514
|
-
|
|
798
|
+
catch (err) {
|
|
799
|
+
// Non-critical — edit failure on dispatched agent status
|
|
800
|
+
}
|
|
801
|
+
}).catch(() => { });
|
|
515
802
|
}
|
|
516
803
|
}
|
|
517
804
|
async handleEvent(event) {
|
|
@@ -591,9 +878,9 @@ export class SubAgentTracker {
|
|
|
591
878
|
await this.sender.editMessage(this.chatId, info.tgMessageId, text, 'HTML');
|
|
592
879
|
}
|
|
593
880
|
catch {
|
|
594
|
-
//
|
|
881
|
+
// Edit failure on tool result — non-critical
|
|
595
882
|
}
|
|
596
|
-
});
|
|
883
|
+
}).catch(() => { });
|
|
597
884
|
await this.sendQueue;
|
|
598
885
|
}
|
|
599
886
|
async onBlockStart(event) {
|
|
@@ -630,9 +917,9 @@ export class SubAgentTracker {
|
|
|
630
917
|
this.consolidatedAgentMsgId = msgId;
|
|
631
918
|
}
|
|
632
919
|
catch {
|
|
633
|
-
//
|
|
920
|
+
// Sub-agent indicator is non-critical
|
|
634
921
|
}
|
|
635
|
-
});
|
|
922
|
+
}).catch(() => { });
|
|
636
923
|
await this.sendQueue;
|
|
637
924
|
}
|
|
638
925
|
}
|
|
@@ -654,8 +941,10 @@ export class SubAgentTracker {
|
|
|
654
941
|
try {
|
|
655
942
|
await this.sender.editMessage(this.chatId, msgId, text, 'HTML');
|
|
656
943
|
}
|
|
657
|
-
catch {
|
|
658
|
-
|
|
944
|
+
catch {
|
|
945
|
+
// Consolidated message edit failure — non-critical
|
|
946
|
+
}
|
|
947
|
+
}).catch(() => { });
|
|
659
948
|
}
|
|
660
949
|
async onInputDelta(event) {
|
|
661
950
|
const toolUseId = this.blockToAgent.get(event.index);
|
|
@@ -797,8 +1086,10 @@ export class SubAgentTracker {
|
|
|
797
1086
|
try {
|
|
798
1087
|
await this.sender.setReaction?.(this.chatId, msgId, emoji);
|
|
799
1088
|
}
|
|
800
|
-
catch {
|
|
801
|
-
|
|
1089
|
+
catch {
|
|
1090
|
+
// Reaction failure — non-critical, might not be supported
|
|
1091
|
+
}
|
|
1092
|
+
}).catch(() => { });
|
|
802
1093
|
}
|
|
803
1094
|
// Check if ALL dispatched agents are now completed
|
|
804
1095
|
if (this.onAllReported && !this.hasDispatchedAgents && this.agents.size > 0) {
|
|
@@ -890,8 +1181,10 @@ export class SubAgentTracker {
|
|
|
890
1181
|
try {
|
|
891
1182
|
await this.sender.editMessage(this.chatId, msgId, text, 'HTML');
|
|
892
1183
|
}
|
|
893
|
-
catch {
|
|
894
|
-
|
|
1184
|
+
catch {
|
|
1185
|
+
// Progress message edit failure — non-critical
|
|
1186
|
+
}
|
|
1187
|
+
}).catch(() => { });
|
|
895
1188
|
}
|
|
896
1189
|
/** Find a tracked sub-agent by tool_use_id. */
|
|
897
1190
|
getAgentByToolUseId(toolUseId) {
|