tide-commander 0.67.3 โ 0.69.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/dist/assets/main-C8GYJbe5.css +1 -0
- package/dist/assets/{main-C0I0fw2M.js โ main-MxBRZvMh.js} +87 -87
- package/dist/index.html +2 -2
- package/dist/src/packages/landing/index.html +10 -0
- package/dist/src/packages/server/claude/backend.js +70 -10
- package/dist/src/packages/server/claude/runner/stdout-pipeline.js +13 -3
- package/dist/src/packages/server/services/agent-service.js +3 -1
- package/dist/src/packages/server/services/runtime-events.js +72 -30
- package/dist/src/packages/server/services/runtime-subagents.js +4 -0
- package/dist/src/packages/server/services/supervisor-service.js +4 -2
- package/dist/src/packages/server/websocket/handlers/agent-handler.js +285 -21
- package/dist/src/packages/server/websocket/handlers/command-handler.js +13 -18
- package/dist/src/packages/server/websocket/listeners/runtime-listeners.js +46 -6
- package/package.json +3 -2
- package/dist/assets/main-flegxlsj.css +0 -1
|
@@ -3,9 +3,12 @@
|
|
|
3
3
|
* Handles spawn, kill, stop, remove, rename, and update operations for agents
|
|
4
4
|
*/
|
|
5
5
|
import * as fs from 'fs';
|
|
6
|
+
import { spawn } from 'child_process';
|
|
6
7
|
import { agentService, runtimeService, skillService, customClassService, bossService, permissionService } from '../../services/index.js';
|
|
7
8
|
import { createLogger } from '../../utils/index.js';
|
|
9
|
+
import { ClaudeBackend, parseContextOutput } from '../../claude/backend.js';
|
|
8
10
|
const log = createLogger('AgentHandler');
|
|
11
|
+
const claudeBackend = new ClaudeBackend();
|
|
9
12
|
// Test change: Server restart validation - if you see this log, the server restarted successfully
|
|
10
13
|
log.log('๐ AgentHandler loaded - server restart test');
|
|
11
14
|
/**
|
|
@@ -183,37 +186,298 @@ export async function handleCollapseContext(ctx, payload) {
|
|
|
183
186
|
}
|
|
184
187
|
}
|
|
185
188
|
/**
|
|
186
|
-
*
|
|
189
|
+
* Parse the visual terminal format from /context command output.
|
|
190
|
+
* Example: "claude-opus-4-6 ยท 46k/200k tokens (23%)"
|
|
191
|
+
* "โ System prompt: 6.7k tokens (3.4%)"
|
|
187
192
|
*/
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
const
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
193
|
+
function parseVisualContextOutput(content) {
|
|
194
|
+
try {
|
|
195
|
+
// Match: model-name ยท 46k/200k tokens (23%)
|
|
196
|
+
const headerMatch = content.match(/([\w.-]+)\s*[ยทโข]\s*([\d.]+k?)\s*\/\s*([\d.]+k?)\s*tokens\s*\((\d+)%\)/);
|
|
197
|
+
if (!headerMatch) {
|
|
198
|
+
return null;
|
|
199
|
+
}
|
|
200
|
+
const parseTokenVal = (str) => {
|
|
201
|
+
if (str.endsWith('k') || str.endsWith('K')) {
|
|
202
|
+
return parseFloat(str) * 1000;
|
|
203
|
+
}
|
|
204
|
+
return parseFloat(str);
|
|
205
|
+
};
|
|
206
|
+
const model = headerMatch[1];
|
|
207
|
+
const totalTokens = parseTokenVal(headerMatch[2]);
|
|
208
|
+
const contextWindow = parseTokenVal(headerMatch[3]);
|
|
209
|
+
const usedPercent = parseInt(headerMatch[4], 10);
|
|
210
|
+
// Parse categories from visual format: "โ Category Name: 6.7k tokens (3.4%)" or "โ Category Name: 479 tokens (0.2%)"
|
|
211
|
+
const parseVisualCategory = (name) => {
|
|
212
|
+
const regex = new RegExp(`${name}:\\s*([\\.\\d]+k?)\\s*(?:tokens)?\\s*\\(([\\.\\d]+)%\\)`, 'i');
|
|
213
|
+
const match = content.match(regex);
|
|
214
|
+
if (match) {
|
|
215
|
+
return { tokens: parseTokenVal(match[1]), percent: parseFloat(match[2]) };
|
|
216
|
+
}
|
|
217
|
+
return { tokens: 0, percent: 0 };
|
|
218
|
+
};
|
|
219
|
+
return {
|
|
220
|
+
model,
|
|
221
|
+
contextWindow,
|
|
222
|
+
totalTokens,
|
|
223
|
+
usedPercent,
|
|
224
|
+
categories: {
|
|
225
|
+
systemPrompt: parseVisualCategory('System prompt'),
|
|
226
|
+
systemTools: parseVisualCategory('System tools'),
|
|
227
|
+
messages: parseVisualCategory('Messages'),
|
|
228
|
+
freeSpace: parseVisualCategory('Free space'),
|
|
229
|
+
autocompactBuffer: parseVisualCategory('Autocompact buffer'),
|
|
202
230
|
},
|
|
231
|
+
lastUpdated: Date.now(),
|
|
232
|
+
};
|
|
233
|
+
}
|
|
234
|
+
catch (err) {
|
|
235
|
+
log.error('parseVisualContextOutput error:', err);
|
|
236
|
+
return null;
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
/**
|
|
240
|
+
* Spawn a short-lived Claude CLI process to fetch real context stats for a session.
|
|
241
|
+
*
|
|
242
|
+
* Strategy: Two attempts.
|
|
243
|
+
* 1. stream-json mode (--print --input/output-format stream-json)
|
|
244
|
+
* The /context slash command IS recognised (0 tokens, no API call) but
|
|
245
|
+
* its output may come as a `user` event with <local-command-stdout> tags
|
|
246
|
+
* or may be completely absent from the JSON stream.
|
|
247
|
+
* 2. Plain pipe mode (no --print, no format flags)
|
|
248
|
+
* Pipe `/context\n` as plain text. The CLI should run the local command
|
|
249
|
+
* and write the visual bar-chart output to stdout/stderr.
|
|
250
|
+
*/
|
|
251
|
+
function fetchContextFromCLI(sessionId, cwd) {
|
|
252
|
+
return new Promise((resolve) => {
|
|
253
|
+
// Attempt 1: stream-json mode (fast, preferred if output is available)
|
|
254
|
+
tryStreamJson(sessionId, cwd).then((stats) => {
|
|
255
|
+
if (stats) {
|
|
256
|
+
resolve(stats);
|
|
257
|
+
return;
|
|
258
|
+
}
|
|
259
|
+
// Attempt 2: plain pipe mode (interactive-like, captures visual output)
|
|
260
|
+
tryPlainPipe(sessionId, cwd).then((stats2) => {
|
|
261
|
+
resolve(stats2);
|
|
262
|
+
});
|
|
263
|
+
});
|
|
264
|
+
});
|
|
265
|
+
}
|
|
266
|
+
function tryStreamJson(sessionId, cwd) {
|
|
267
|
+
return new Promise((resolve) => {
|
|
268
|
+
const executable = claudeBackend.getExecutablePath();
|
|
269
|
+
const args = [
|
|
270
|
+
'--resume', sessionId,
|
|
271
|
+
'--print',
|
|
272
|
+
'--output-format', 'stream-json',
|
|
273
|
+
'--input-format', 'stream-json',
|
|
274
|
+
];
|
|
275
|
+
log.log(`[fetchContext:stream-json] Spawning: ${executable} ${args.join(' ')}`);
|
|
276
|
+
const child = spawn(executable, args, {
|
|
277
|
+
cwd,
|
|
278
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
279
|
+
});
|
|
280
|
+
let stdout = '';
|
|
281
|
+
let stderr = '';
|
|
282
|
+
child.stdout.on('data', (data) => { stdout += data.toString(); });
|
|
283
|
+
child.stderr.on('data', (data) => { stderr += data.toString(); });
|
|
284
|
+
const input = JSON.stringify({
|
|
285
|
+
type: 'user',
|
|
286
|
+
message: { role: 'user', content: '/context' },
|
|
287
|
+
});
|
|
288
|
+
child.stdin.write(input + '\n');
|
|
289
|
+
child.stdin.end();
|
|
290
|
+
const timer = setTimeout(() => {
|
|
291
|
+
log.warn('[fetchContext:stream-json] Timed out');
|
|
292
|
+
child.kill();
|
|
293
|
+
resolve(null);
|
|
294
|
+
}, 10000);
|
|
295
|
+
child.on('close', () => {
|
|
296
|
+
clearTimeout(timer);
|
|
297
|
+
log.log(`[fetchContext:stream-json] exited, stdout=${stdout.length}, stderr=${stderr.length}`);
|
|
298
|
+
const combined = stdout + '\n' + stderr;
|
|
299
|
+
// Look for <local-command-stdout> in raw text or inside JSON user events
|
|
300
|
+
const localCmdMatch = combined.match(/<local-command-stdout>([\s\S]*?)<\/local-command-stdout>/);
|
|
301
|
+
if (localCmdMatch) {
|
|
302
|
+
const stats = parseContextOutput(localCmdMatch[1]);
|
|
303
|
+
if (stats) {
|
|
304
|
+
log.log(`[fetchContext:stream-json] Parsed from tags: ${stats.totalTokens}/${stats.contextWindow}`);
|
|
305
|
+
resolve(stats);
|
|
306
|
+
return;
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
for (const line of combined.split('\n')) {
|
|
310
|
+
if (!line.trim())
|
|
311
|
+
continue;
|
|
312
|
+
try {
|
|
313
|
+
const event = JSON.parse(line);
|
|
314
|
+
if (event.type === 'user' && typeof event.message?.content === 'string') {
|
|
315
|
+
const tagMatch = event.message.content.match(/<local-command-stdout>([\s\S]*?)<\/local-command-stdout>/);
|
|
316
|
+
if (tagMatch) {
|
|
317
|
+
const stats = parseContextOutput(tagMatch[1]);
|
|
318
|
+
if (stats) {
|
|
319
|
+
log.log(`[fetchContext:stream-json] Parsed from user event: ${stats.totalTokens}/${stats.contextWindow}`);
|
|
320
|
+
resolve(stats);
|
|
321
|
+
return;
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
catch { /* not JSON */ }
|
|
327
|
+
}
|
|
328
|
+
log.log('[fetchContext:stream-json] No context data found, will try plain pipe');
|
|
329
|
+
resolve(null);
|
|
330
|
+
});
|
|
331
|
+
child.on('error', () => { clearTimeout(timer); resolve(null); });
|
|
332
|
+
});
|
|
333
|
+
}
|
|
334
|
+
function tryPlainPipe(sessionId, cwd) {
|
|
335
|
+
return new Promise((resolve) => {
|
|
336
|
+
const executable = claudeBackend.getExecutablePath();
|
|
337
|
+
// No --print, no format flags. Pipe /context as plain text.
|
|
338
|
+
// The CLI should recognise it as a slash command in interactive-like mode.
|
|
339
|
+
const args = ['--resume', sessionId];
|
|
340
|
+
log.log(`[fetchContext:plain] Spawning: ${executable} ${args.join(' ')}`);
|
|
341
|
+
const child = spawn(executable, args, {
|
|
342
|
+
cwd,
|
|
343
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
344
|
+
env: { ...process.env, NO_COLOR: '1', TERM: 'dumb' }, // suppress ANSI codes
|
|
203
345
|
});
|
|
346
|
+
let stdout = '';
|
|
347
|
+
let stderr = '';
|
|
348
|
+
child.stdout.on('data', (data) => { stdout += data.toString(); });
|
|
349
|
+
child.stderr.on('data', (data) => { stderr += data.toString(); });
|
|
350
|
+
// Send the slash command and close stdin so CLI exits
|
|
351
|
+
child.stdin.write('/context\n');
|
|
352
|
+
child.stdin.end();
|
|
353
|
+
const timer = setTimeout(() => {
|
|
354
|
+
log.warn('[fetchContext:plain] Timed out');
|
|
355
|
+
child.kill('SIGTERM');
|
|
356
|
+
// Even on timeout, try to parse what we have
|
|
357
|
+
const stats = parseAllFormats(stdout + '\n' + stderr);
|
|
358
|
+
resolve(stats);
|
|
359
|
+
}, 10000);
|
|
360
|
+
child.on('close', () => {
|
|
361
|
+
clearTimeout(timer);
|
|
362
|
+
log.log(`[fetchContext:plain] exited, stdout=${stdout.length}, stderr=${stderr.length}`);
|
|
363
|
+
const stats = parseAllFormats(stdout + '\n' + stderr);
|
|
364
|
+
if (stats) {
|
|
365
|
+
log.log(`[fetchContext:plain] Parsed: ${stats.totalTokens}/${stats.contextWindow} (${stats.model})`);
|
|
366
|
+
}
|
|
367
|
+
else {
|
|
368
|
+
log.warn('[fetchContext:plain] Could not parse context');
|
|
369
|
+
if (stdout.length < 2000)
|
|
370
|
+
log.log(`[fetchContext:plain] stdout: ${stdout}`);
|
|
371
|
+
if (stderr.length < 2000)
|
|
372
|
+
log.log(`[fetchContext:plain] stderr: ${stderr}`);
|
|
373
|
+
}
|
|
374
|
+
resolve(stats);
|
|
375
|
+
});
|
|
376
|
+
child.on('error', (err) => {
|
|
377
|
+
clearTimeout(timer);
|
|
378
|
+
log.error(`[fetchContext:plain] error: ${err}`);
|
|
379
|
+
resolve(null);
|
|
380
|
+
});
|
|
381
|
+
});
|
|
382
|
+
}
|
|
383
|
+
/** Try every known format parser on the combined output. */
|
|
384
|
+
function parseAllFormats(raw) {
|
|
385
|
+
// Strip ANSI escape codes
|
|
386
|
+
const stripped = raw.replace(/\x1B\[[0-9;]*[A-Za-z]/g, '');
|
|
387
|
+
// 1. Markdown table format (from <local-command-stdout> or raw)
|
|
388
|
+
const localCmd = stripped.match(/<local-command-stdout>([\s\S]*?)<\/local-command-stdout>/);
|
|
389
|
+
if (localCmd) {
|
|
390
|
+
const stats = parseContextOutput(localCmd[1]);
|
|
391
|
+
if (stats)
|
|
392
|
+
return stats;
|
|
393
|
+
}
|
|
394
|
+
const mdStats = parseContextOutput(stripped);
|
|
395
|
+
if (mdStats)
|
|
396
|
+
return mdStats;
|
|
397
|
+
// 2. Visual terminal format (โ bar chart)
|
|
398
|
+
const vizStats = parseVisualContextOutput(stripped);
|
|
399
|
+
if (vizStats)
|
|
400
|
+
return vizStats;
|
|
401
|
+
return null;
|
|
402
|
+
}
|
|
403
|
+
/**
|
|
404
|
+
* Build context stats from tracked agent data (fallback when CLI fetch isn't possible).
|
|
405
|
+
*/
|
|
406
|
+
function buildStatsFromTrackedData(agent) {
|
|
407
|
+
const contextUsed = Math.max(0, Math.round(agent.contextUsed || 0));
|
|
408
|
+
const contextLimit = Math.max(1, Math.round(agent.contextLimit || 200000));
|
|
409
|
+
const usedPercent = Math.min(100, Math.round((contextUsed / contextLimit) * 100));
|
|
410
|
+
const freeTokens = Math.max(0, contextLimit - contextUsed);
|
|
411
|
+
const model = agent.model || agent.codexModel || 'unknown';
|
|
412
|
+
return {
|
|
413
|
+
model,
|
|
414
|
+
contextWindow: contextLimit,
|
|
415
|
+
totalTokens: contextUsed,
|
|
416
|
+
usedPercent,
|
|
417
|
+
categories: {
|
|
418
|
+
systemPrompt: { tokens: 0, percent: 0 },
|
|
419
|
+
systemTools: { tokens: 0, percent: 0 },
|
|
420
|
+
messages: { tokens: contextUsed, percent: Number(((contextUsed / contextLimit) * 100).toFixed(1)) },
|
|
421
|
+
freeSpace: { tokens: freeTokens, percent: Number(((freeTokens / contextLimit) * 100).toFixed(1)) },
|
|
422
|
+
autocompactBuffer: { tokens: 0, percent: 0 },
|
|
423
|
+
},
|
|
424
|
+
lastUpdated: Date.now(),
|
|
425
|
+
};
|
|
426
|
+
}
|
|
427
|
+
/**
|
|
428
|
+
* Broadcast context stats to UI (both the modal and the context bar).
|
|
429
|
+
*/
|
|
430
|
+
function broadcastContextStats(ctx, agentId, stats, label) {
|
|
431
|
+
const freePercent = stats.categories?.freeSpace?.percent ?? (100 - stats.usedPercent);
|
|
432
|
+
agentService.updateAgent(agentId, {
|
|
433
|
+
contextStats: stats,
|
|
434
|
+
contextUsed: stats.totalTokens,
|
|
435
|
+
contextLimit: stats.contextWindow,
|
|
436
|
+
}, false);
|
|
437
|
+
ctx.broadcast({ type: 'context_stats', payload: { agentId, stats } });
|
|
438
|
+
ctx.broadcast({ type: 'context_update', payload: { agentId, contextUsed: stats.totalTokens, contextLimit: stats.contextWindow } });
|
|
439
|
+
ctx.broadcast({
|
|
440
|
+
type: 'output',
|
|
441
|
+
payload: {
|
|
442
|
+
agentId,
|
|
443
|
+
text: `Context (${label}): ${(stats.totalTokens / 1000).toFixed(1)}k/${(stats.contextWindow / 1000).toFixed(1)}k (${freePercent}% free)`,
|
|
444
|
+
isStreaming: false,
|
|
445
|
+
timestamp: Date.now(),
|
|
446
|
+
},
|
|
447
|
+
});
|
|
448
|
+
}
|
|
449
|
+
/**
|
|
450
|
+
* Handle request_context_stats message.
|
|
451
|
+
* For Claude agents with a session, fetches REAL context stats from the CLI.
|
|
452
|
+
* Falls back to tracked data if CLI fetch fails or agent has no session.
|
|
453
|
+
*/
|
|
454
|
+
export async function handleRequestContextStats(ctx, payload) {
|
|
455
|
+
const agent = agentService.getAgent(payload.agentId);
|
|
456
|
+
if (!agent) {
|
|
457
|
+
log.error(` Agent not found for context stats: ${payload.agentId}`);
|
|
204
458
|
return;
|
|
205
459
|
}
|
|
206
|
-
|
|
460
|
+
const isClaudeProvider = (agent.provider ?? 'claude') === 'claude';
|
|
461
|
+
// For Claude agents with an active session, fetch real context stats from the CLI
|
|
462
|
+
if (isClaudeProvider && agent.sessionId) {
|
|
463
|
+
log.log(`[contextStats] Fetching real context from CLI for ${agent.name} (session=${agent.sessionId})`);
|
|
207
464
|
try {
|
|
208
|
-
await
|
|
465
|
+
const stats = await fetchContextFromCLI(agent.sessionId, agent.cwd || process.cwd());
|
|
466
|
+
if (stats) {
|
|
467
|
+
log.log(`[contextStats] Got real stats: ${stats.totalTokens}/${stats.contextWindow} (${stats.model})`);
|
|
468
|
+
broadcastContextStats(ctx, payload.agentId, stats, 'from CLI');
|
|
469
|
+
return;
|
|
470
|
+
}
|
|
471
|
+
log.warn(`[contextStats] CLI fetch returned null, falling back to tracked data`);
|
|
209
472
|
}
|
|
210
473
|
catch (err) {
|
|
211
|
-
log.error(`
|
|
474
|
+
log.error(`[contextStats] CLI fetch failed: ${err}`);
|
|
212
475
|
}
|
|
213
476
|
}
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
477
|
+
// Fallback: generate from tracked data
|
|
478
|
+
const stats = buildStatsFromTrackedData(agent);
|
|
479
|
+
const label = agent.provider === 'codex' ? 'estimated from turn usage' : 'tracked from token usage';
|
|
480
|
+
broadcastContextStats(ctx, payload.agentId, stats, label);
|
|
217
481
|
}
|
|
218
482
|
/**
|
|
219
483
|
* Handle move_agent message
|
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
import { agentService, runtimeService, skillService, customClassService } from '../../services/index.js';
|
|
6
6
|
import { createLogger } from '../../utils/index.js';
|
|
7
7
|
import { getAuthToken } from '../../auth/index.js';
|
|
8
|
+
import { handleRequestContextStats } from './agent-handler.js';
|
|
8
9
|
const log = createLogger('CommandHandler');
|
|
9
10
|
/**
|
|
10
11
|
* Track last boss commands for delegation parsing
|
|
@@ -95,8 +96,17 @@ export async function handleSendCommand(ctx, payload, buildBossMessage) {
|
|
|
95
96
|
log.error(` Agent not found: ${agentId}`);
|
|
96
97
|
return;
|
|
97
98
|
}
|
|
99
|
+
const trimmedCmd = command.trim();
|
|
100
|
+
// Intercept /context and /cost for ALL agents (boss or regular) BEFORE routing.
|
|
101
|
+
// The CLI /context slash command does NOT work via stdin in --print mode
|
|
102
|
+
// (it gets treated as a user message). We generate stats from tracked data instead.
|
|
103
|
+
if (trimmedCmd === '/context' || trimmedCmd === '/cost') {
|
|
104
|
+
log.log(`Agent ${agent.name}: Intercepting ${trimmedCmd} - generating stats from tracked data`);
|
|
105
|
+
await handleRequestContextStats(ctx, { agentId });
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
98
108
|
// Handle /clear command - clear session and start fresh
|
|
99
|
-
if (
|
|
109
|
+
if (trimmedCmd === '/clear') {
|
|
100
110
|
log.log(`Agent ${agent.name}: /clear command - clearing session`);
|
|
101
111
|
await runtimeService.stopAgent(agentId);
|
|
102
112
|
agentService.updateAgent(agentId, {
|
|
@@ -153,23 +163,8 @@ async function handleBossCommand(ctx, agentId, command, agentName, buildBossMess
|
|
|
153
163
|
* Regular agents get custom class instructions and skills combined into a prompt
|
|
154
164
|
*/
|
|
155
165
|
async function handleRegularAgentCommand(ctx, agentId, command, agent) {
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
const contextUsed = Math.max(0, Math.round(agent.contextUsed || 0));
|
|
159
|
-
const contextLimit = Math.max(1, Math.round(agent.contextLimit || 200000));
|
|
160
|
-
const usedPercent = Math.min(100, Math.round((contextUsed / contextLimit) * 100));
|
|
161
|
-
const freePercent = 100 - usedPercent;
|
|
162
|
-
ctx.broadcast({
|
|
163
|
-
type: 'output',
|
|
164
|
-
payload: {
|
|
165
|
-
agentId,
|
|
166
|
-
text: `Context (estimated from Codex turn usage): ${(contextUsed / 1000).toFixed(1)}k/${(contextLimit / 1000).toFixed(1)}k (${freePercent}% free)`,
|
|
167
|
-
isStreaming: false,
|
|
168
|
-
timestamp: Date.now(),
|
|
169
|
-
},
|
|
170
|
-
});
|
|
171
|
-
return;
|
|
172
|
-
}
|
|
166
|
+
// Note: /context, /cost, /compact are intercepted at the handleSendCommand level
|
|
167
|
+
// so they never reach here. This function only handles actual commands to send to the agent.
|
|
173
168
|
const customAgentConfig = buildCustomAgentConfig(agentId, agent.class);
|
|
174
169
|
if (customAgentConfig) {
|
|
175
170
|
log.log(` Agent ${agent.name} customAgentConfig: name=${customAgentConfig.name}, promptLen=${customAgentConfig.definition.prompt.length}`);
|
|
@@ -58,15 +58,14 @@ export function setupRuntimeListeners(ctx) {
|
|
|
58
58
|
cleanPreview = parsed
|
|
59
59
|
.filter((b) => b.type === 'text' && b.text)
|
|
60
60
|
.map((b) => b.text)
|
|
61
|
-
.join(' ')
|
|
62
|
-
.slice(0, 200);
|
|
61
|
+
.join(' ');
|
|
63
62
|
}
|
|
64
63
|
else {
|
|
65
|
-
cleanPreview = event.toolOutput
|
|
64
|
+
cleanPreview = event.toolOutput;
|
|
66
65
|
}
|
|
67
66
|
}
|
|
68
67
|
catch {
|
|
69
|
-
cleanPreview = event.toolOutput
|
|
68
|
+
cleanPreview = event.toolOutput;
|
|
70
69
|
}
|
|
71
70
|
}
|
|
72
71
|
ctx.broadcast({
|
|
@@ -77,11 +76,29 @@ export function setupRuntimeListeners(ctx) {
|
|
|
77
76
|
success: true,
|
|
78
77
|
resultPreview: cleanPreview,
|
|
79
78
|
subagentName: event.subagentName,
|
|
79
|
+
// Completion stats from Task tool metadata
|
|
80
|
+
durationMs: event.subagentStats?.durationMs,
|
|
81
|
+
tokensUsed: event.subagentStats?.tokensUsed,
|
|
82
|
+
toolUseCount: event.subagentStats?.toolUseCount,
|
|
80
83
|
},
|
|
81
84
|
});
|
|
82
|
-
log.log(`[Subagent] Broadcast subagent_completed for toolUseId=${event.toolUseId}, name=${event.subagentName || 'unknown'}`);
|
|
85
|
+
log.log(`[Subagent] Broadcast subagent_completed for toolUseId=${event.toolUseId}, name=${event.subagentName || 'unknown'}, stats=${event.subagentStats ? `${event.subagentStats.durationMs}ms/${event.subagentStats.tokensUsed}tok/${event.subagentStats.toolUseCount}tools` : 'none'}`);
|
|
83
86
|
}
|
|
84
|
-
|
|
87
|
+
// Forward subagent internal tool activity to client (events with parentToolUseId)
|
|
88
|
+
if (event.parentToolUseId && event.type === 'tool_start' && event.toolName !== 'Task') {
|
|
89
|
+
const toolDesc = formatToolActivity(event.toolName, event.toolInput);
|
|
90
|
+
ctx.broadcast({
|
|
91
|
+
type: 'subagent_output',
|
|
92
|
+
payload: {
|
|
93
|
+
subagentId: event.parentToolUseId,
|
|
94
|
+
parentAgentId: agentId,
|
|
95
|
+
text: toolDesc,
|
|
96
|
+
isStreaming: false,
|
|
97
|
+
timestamp: Date.now(),
|
|
98
|
+
},
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
if (event.type === 'error') {
|
|
85
102
|
ctx.sendActivity(agentId, `Error: ${event.errorMessage}`);
|
|
86
103
|
}
|
|
87
104
|
else if (event.type === 'tool_result' && event.toolName === 'Bash') {
|
|
@@ -125,6 +142,20 @@ export function setupRuntimeListeners(ctx) {
|
|
|
125
142
|
parseBossSpawn(agentId, agent.name, event.resultText, ctx.broadcast, ctx.sendActivity);
|
|
126
143
|
}
|
|
127
144
|
}
|
|
145
|
+
// Real-time context tracking: broadcast lightweight context_update on usage_snapshot and step_complete
|
|
146
|
+
if ((event.type === 'usage_snapshot' && event.tokens) || event.type === 'step_complete') {
|
|
147
|
+
const agent = agentService.getAgent(agentId);
|
|
148
|
+
if (agent) {
|
|
149
|
+
ctx.broadcast({
|
|
150
|
+
type: 'context_update',
|
|
151
|
+
payload: {
|
|
152
|
+
agentId,
|
|
153
|
+
contextUsed: agent.contextUsed,
|
|
154
|
+
contextLimit: agent.contextLimit,
|
|
155
|
+
},
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
}
|
|
128
159
|
if (event.type === 'context_stats' && event.contextStatsRaw) {
|
|
129
160
|
log.log(`[context_stats] Received for agent ${agentId}, raw length: ${event.contextStatsRaw.length}`);
|
|
130
161
|
const stats = parseContextOutput(event.contextStatsRaw);
|
|
@@ -139,6 +170,15 @@ export function setupRuntimeListeners(ctx) {
|
|
|
139
170
|
type: 'context_stats',
|
|
140
171
|
payload: { agentId, stats },
|
|
141
172
|
});
|
|
173
|
+
// Also send lightweight context_update so the context bar updates immediately
|
|
174
|
+
ctx.broadcast({
|
|
175
|
+
type: 'context_update',
|
|
176
|
+
payload: {
|
|
177
|
+
agentId,
|
|
178
|
+
contextUsed: stats.totalTokens,
|
|
179
|
+
contextLimit: stats.contextWindow,
|
|
180
|
+
},
|
|
181
|
+
});
|
|
142
182
|
}
|
|
143
183
|
else {
|
|
144
184
|
log.log(`[context_stats] Failed to parse context output for agent ${agentId}`);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "tide-commander",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.69.0",
|
|
4
4
|
"description": "Visual multi-agent orchestrator and manager for Claude Code with 3D/2D interface",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -32,7 +32,8 @@
|
|
|
32
32
|
"test:ci": "vitest run --exclude src/packages/client/hooks/__tests__/useSnapshots.test.ts --exclude src/packages/client/hooks/__tests__/useKeyboardShortcuts.test.ts",
|
|
33
33
|
"test:watch": "vitest",
|
|
34
34
|
"test:coverage": "vitest run --coverage",
|
|
35
|
-
"prepack": "npm run build"
|
|
35
|
+
"prepack": "npm run build",
|
|
36
|
+
"tc": "tsx tools/tc.ts"
|
|
36
37
|
},
|
|
37
38
|
"dependencies": {
|
|
38
39
|
"@anthropic-ai/sdk": "^0.71.2",
|