tide-commander 0.69.2 → 0.69.4
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-D-4CYuC3.js +299 -0
- package/dist/assets/main-Zi2pcuPE.css +1 -0
- package/dist/index.html +2 -2
- package/dist/src/packages/server/routes/files.js +22 -6
- package/dist/src/packages/server/services/runtime-service.js +0 -19
- package/dist/src/packages/server/services/subagent-jsonl-watcher.js +413 -0
- package/dist/src/packages/server/websocket/listeners/runtime-listeners.js +13 -0
- package/package.json +1 -1
- package/dist/assets/main-BOFXGJQm.css +0 -1
- package/dist/assets/main-DkIjeEeR.js +0 -299
package/dist/index.html
CHANGED
|
@@ -22,11 +22,11 @@
|
|
|
22
22
|
<link rel="icon" type="image/png" sizes="16x16" href="/assets/icons/favicon-16x16.png" />
|
|
23
23
|
<link rel="apple-touch-icon" sizes="180x180" href="/assets/icons/apple-touch-icon.png" />
|
|
24
24
|
<title>Tide Commander</title>
|
|
25
|
-
<script type="module" crossorigin src="/assets/main-
|
|
25
|
+
<script type="module" crossorigin src="/assets/main-D-4CYuC3.js"></script>
|
|
26
26
|
<link rel="modulepreload" crossorigin href="/assets/modulepreload-polyfill-B5Qt9EMX.js">
|
|
27
27
|
<link rel="modulepreload" crossorigin href="/assets/vendor-react-uS-d4TUT.js">
|
|
28
28
|
<link rel="modulepreload" crossorigin href="/assets/vendor-three-DJ4p3FLF.js">
|
|
29
|
-
<link rel="stylesheet" crossorigin href="/assets/main-
|
|
29
|
+
<link rel="stylesheet" crossorigin href="/assets/main-Zi2pcuPE.css">
|
|
30
30
|
</head>
|
|
31
31
|
<body>
|
|
32
32
|
<div id="app"></div>
|
|
@@ -1243,7 +1243,7 @@ router.post('/git-pull', async (req, res) => {
|
|
|
1243
1243
|
return;
|
|
1244
1244
|
}
|
|
1245
1245
|
try {
|
|
1246
|
-
let cmd = 'git pull';
|
|
1246
|
+
let cmd = 'git pull --no-rebase';
|
|
1247
1247
|
if (remote)
|
|
1248
1248
|
cmd += ` "${remote}"`;
|
|
1249
1249
|
if (branch)
|
|
@@ -1252,15 +1252,31 @@ router.post('/git-pull', async (req, res) => {
|
|
|
1252
1252
|
res.json({ success: true, output: output.trim() });
|
|
1253
1253
|
}
|
|
1254
1254
|
catch (err) {
|
|
1255
|
-
const stderr = err.stderr?.toString() ||
|
|
1256
|
-
|
|
1257
|
-
|
|
1255
|
+
const stderr = err.stderr?.toString() || '';
|
|
1256
|
+
const stdout = err.stdout?.toString() || '';
|
|
1257
|
+
const combined = stdout + '\n' + stderr;
|
|
1258
|
+
if (combined.includes('CONFLICT') || combined.includes('Automatic merge failed')) {
|
|
1259
|
+
// Parse conflict file paths from output (same as merge endpoint)
|
|
1260
|
+
const conflicts = [];
|
|
1261
|
+
const conflictRegex = /CONFLICT \([^)]+\): Merge conflict in (.+)/g;
|
|
1262
|
+
let match;
|
|
1263
|
+
while ((match = conflictRegex.exec(combined)) !== null) {
|
|
1264
|
+
conflicts.push(path.join(gitRoot, match[1].trim()));
|
|
1265
|
+
}
|
|
1266
|
+
const bothRegex = /CONFLICT \([^)]+\):.+?(?:both modified|both added):\s*(.+)/g;
|
|
1267
|
+
while ((match = bothRegex.exec(combined)) !== null) {
|
|
1268
|
+
const conflictPath = path.join(gitRoot, match[1].trim());
|
|
1269
|
+
if (!conflicts.includes(conflictPath)) {
|
|
1270
|
+
conflicts.push(conflictPath);
|
|
1271
|
+
}
|
|
1272
|
+
}
|
|
1273
|
+
res.json({ success: false, output: combined.trim(), conflicts });
|
|
1258
1274
|
}
|
|
1259
|
-
else if (
|
|
1275
|
+
else if (combined.includes('ETIMEDOUT') || combined.includes('Could not resolve')) {
|
|
1260
1276
|
res.status(504).json({ success: false, error: 'Network error. Check your connection.' });
|
|
1261
1277
|
}
|
|
1262
1278
|
else {
|
|
1263
|
-
res.status(500).json({ success: false, error: stderr.trim() || err.message });
|
|
1279
|
+
res.status(500).json({ success: false, error: (stderr || stdout).trim() || err.message });
|
|
1264
1280
|
}
|
|
1265
1281
|
}
|
|
1266
1282
|
}
|
|
@@ -79,24 +79,6 @@ function emit(event, ...args) {
|
|
|
79
79
|
listeners.forEach((listener) => listener(...args));
|
|
80
80
|
}
|
|
81
81
|
}
|
|
82
|
-
function scheduleSilentContextRefresh(agentId, reason) {
|
|
83
|
-
setTimeout(() => {
|
|
84
|
-
if (reason === 'step_complete') {
|
|
85
|
-
log.log(`[step_complete] Sending silent /context for agent ${agentId}`);
|
|
86
|
-
}
|
|
87
|
-
if (reason === 'handle_complete') {
|
|
88
|
-
log.log(`[handleComplete] Triggering fallback /context refresh for agent ${agentId}`);
|
|
89
|
-
}
|
|
90
|
-
sendSilentCommand(agentId, '/context').catch((err) => {
|
|
91
|
-
if (reason === 'step_complete') {
|
|
92
|
-
log.log(`[step_complete] Silent /context failed for ${agentId}: ${err}`);
|
|
93
|
-
}
|
|
94
|
-
else {
|
|
95
|
-
log.log(`[handleComplete] Fallback /context failed for ${agentId}: ${err}`);
|
|
96
|
-
}
|
|
97
|
-
});
|
|
98
|
-
}, 300);
|
|
99
|
-
}
|
|
100
82
|
const commandExecution = createRuntimeCommandExecution({
|
|
101
83
|
log,
|
|
102
84
|
getRunner,
|
|
@@ -117,7 +99,6 @@ const runtimeEvents = createRuntimeEventHandlers({
|
|
|
117
99
|
emitError: (agentId, error) => emit('error', agentId, error),
|
|
118
100
|
parseUsageOutput: (raw) => parseUsageOutput(raw),
|
|
119
101
|
executeCommand: (agentId, command, systemPrompt, forceNewSession) => commandExecution.executeCommand(agentId, command, systemPrompt, forceNewSession),
|
|
120
|
-
scheduleSilentContextRefresh,
|
|
121
102
|
});
|
|
122
103
|
const statusSync = createRuntimeStatusSync({
|
|
123
104
|
log,
|
|
@@ -0,0 +1,413 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Subagent JSONL File Watcher
|
|
3
|
+
*
|
|
4
|
+
* Watches Claude Code's subagent JSONL files for real-time activity streaming.
|
|
5
|
+
* Files are located at: ~/.claude/projects/<encoded-project>/<sessionId>/subagents/agent-<id>.jsonl
|
|
6
|
+
*
|
|
7
|
+
* Each JSONL line contains a message entry (user prompt, assistant text, tool_use, tool_result).
|
|
8
|
+
* We parse these and broadcast them to the UI for real-time subagent visibility.
|
|
9
|
+
*
|
|
10
|
+
* Lifecycle: The watcher starts when a Task tool spawns a subagent. For team agents,
|
|
11
|
+
* the Task tool returns immediately but the subagent process keeps running and writing
|
|
12
|
+
* to the JSONL file. The watcher uses idle-based auto-stop: it keeps running as long as
|
|
13
|
+
* the file is growing, and stops after IDLE_TIMEOUT_MS of no new content.
|
|
14
|
+
*/
|
|
15
|
+
import * as fs from 'fs';
|
|
16
|
+
import * as path from 'path';
|
|
17
|
+
import * as os from 'os';
|
|
18
|
+
import { createLogger } from '../utils/logger.js';
|
|
19
|
+
import { encodeProjectPath } from '../claude/session-loader.js';
|
|
20
|
+
const log = createLogger('SubagentJSONL');
|
|
21
|
+
const MAX_ENTRIES_PER_BROADCAST = 20;
|
|
22
|
+
const IDLE_TIMEOUT_MS = 180_000; // Stop after 3 minutes of no file changes (subagents can run long builds)
|
|
23
|
+
const POLL_INTERVAL_MS = 2_000; // Poll file every 2s (fallback if fs.watch misses events)
|
|
24
|
+
const MAX_WATCH_DURATION_MS = 900_000; // Hard limit: 15 minutes max per watcher
|
|
25
|
+
// Key param extraction per tool name
|
|
26
|
+
const TOOL_KEY_PARAMS = {
|
|
27
|
+
Bash: 'command',
|
|
28
|
+
Read: 'file_path',
|
|
29
|
+
Edit: 'file_path',
|
|
30
|
+
Write: 'file_path',
|
|
31
|
+
Grep: 'pattern',
|
|
32
|
+
Glob: 'pattern',
|
|
33
|
+
WebSearch: 'query',
|
|
34
|
+
WebFetch: 'url',
|
|
35
|
+
Task: 'description',
|
|
36
|
+
NotebookEdit: 'notebook_path',
|
|
37
|
+
};
|
|
38
|
+
const activeWatchers = new Map();
|
|
39
|
+
/**
|
|
40
|
+
* Get the subagents directory for a given agent's session
|
|
41
|
+
*/
|
|
42
|
+
export function getSubagentsDir(cwd, sessionId) {
|
|
43
|
+
const encoded = encodeProjectPath(cwd);
|
|
44
|
+
return path.join(os.homedir(), '.claude', 'projects', encoded, sessionId, 'subagents');
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Start watching for a subagent's JSONL file
|
|
48
|
+
*/
|
|
49
|
+
export function startWatching(toolUseId, parentAgentId, subagentsDir, onBroadcast) {
|
|
50
|
+
if (activeWatchers.has(toolUseId)) {
|
|
51
|
+
log.warn(`[Watcher] Already watching for toolUseId=${toolUseId}`);
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
const watcher = {
|
|
55
|
+
toolUseId,
|
|
56
|
+
parentAgentId,
|
|
57
|
+
subagentsDir,
|
|
58
|
+
readPosition: 0,
|
|
59
|
+
lineBuffer: '',
|
|
60
|
+
pendingEntries: [],
|
|
61
|
+
onBroadcast,
|
|
62
|
+
stopped: false,
|
|
63
|
+
lastReadTime: Date.now(),
|
|
64
|
+
};
|
|
65
|
+
activeWatchers.set(toolUseId, watcher);
|
|
66
|
+
log.log(`[Watcher] Starting watch for toolUseId=${toolUseId}, dir=${subagentsDir}`);
|
|
67
|
+
// Set a hard max duration to prevent leaks
|
|
68
|
+
watcher.maxTimer = setTimeout(() => {
|
|
69
|
+
log.log(`[Watcher] Max duration reached for toolUseId=${toolUseId}, stopping`);
|
|
70
|
+
doStop(watcher);
|
|
71
|
+
}, MAX_WATCH_DURATION_MS);
|
|
72
|
+
// Try to find existing files first, then watch for new ones
|
|
73
|
+
tryFindAndWatchFile(watcher);
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Signal that the subagent's Task tool has completed.
|
|
77
|
+
* For team agents, the Task returns immediately but the subagent keeps running.
|
|
78
|
+
* We don't stop the watcher - it will auto-stop when the file goes idle.
|
|
79
|
+
*/
|
|
80
|
+
export function stopWatching(toolUseId) {
|
|
81
|
+
const watcher = activeWatchers.get(toolUseId);
|
|
82
|
+
if (!watcher)
|
|
83
|
+
return;
|
|
84
|
+
// Just do a read to catch any pending content, but DON'T stop.
|
|
85
|
+
// The idle timeout will handle actual cleanup.
|
|
86
|
+
if (watcher.jsonlPath) {
|
|
87
|
+
readNewLines(watcher);
|
|
88
|
+
}
|
|
89
|
+
log.log(`[Watcher] Task completed for toolUseId=${toolUseId}, watcher continues (idle-based stop)`);
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* Actually stop and cleanup a watcher
|
|
93
|
+
*/
|
|
94
|
+
function doStop(watcher) {
|
|
95
|
+
if (watcher.stopped)
|
|
96
|
+
return;
|
|
97
|
+
watcher.stopped = true;
|
|
98
|
+
// Final read
|
|
99
|
+
if (watcher.jsonlPath) {
|
|
100
|
+
readNewLines(watcher);
|
|
101
|
+
}
|
|
102
|
+
flushEntries(watcher);
|
|
103
|
+
// Cleanup all timers and watchers
|
|
104
|
+
watcher.dirWatcher?.close();
|
|
105
|
+
watcher.fileWatcher?.close();
|
|
106
|
+
if (watcher.broadcastTimer)
|
|
107
|
+
clearTimeout(watcher.broadcastTimer);
|
|
108
|
+
if (watcher.pollTimer)
|
|
109
|
+
clearInterval(watcher.pollTimer);
|
|
110
|
+
if (watcher.idleTimer)
|
|
111
|
+
clearTimeout(watcher.idleTimer);
|
|
112
|
+
if (watcher.maxTimer)
|
|
113
|
+
clearTimeout(watcher.maxTimer);
|
|
114
|
+
activeWatchers.delete(watcher.toolUseId);
|
|
115
|
+
log.log(`[Watcher] Stopped watching toolUseId=${watcher.toolUseId}`);
|
|
116
|
+
}
|
|
117
|
+
/**
|
|
118
|
+
* Stop all watchers (server shutdown)
|
|
119
|
+
*/
|
|
120
|
+
export function stopAll() {
|
|
121
|
+
for (const watcher of activeWatchers.values()) {
|
|
122
|
+
doStop(watcher);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
/**
|
|
126
|
+
* Reset the idle timer - called whenever new content is read
|
|
127
|
+
*/
|
|
128
|
+
function resetIdleTimer(watcher) {
|
|
129
|
+
if (watcher.idleTimer)
|
|
130
|
+
clearTimeout(watcher.idleTimer);
|
|
131
|
+
watcher.idleTimer = setTimeout(() => {
|
|
132
|
+
log.log(`[Watcher] Idle timeout for toolUseId=${watcher.toolUseId}, stopping`);
|
|
133
|
+
doStop(watcher);
|
|
134
|
+
}, IDLE_TIMEOUT_MS);
|
|
135
|
+
}
|
|
136
|
+
/**
|
|
137
|
+
* Try to find an existing JSONL file or watch the directory for new ones
|
|
138
|
+
*/
|
|
139
|
+
function tryFindAndWatchFile(watcher) {
|
|
140
|
+
const { subagentsDir } = watcher;
|
|
141
|
+
// Check if directory exists yet
|
|
142
|
+
if (!fs.existsSync(subagentsDir)) {
|
|
143
|
+
// Directory doesn't exist yet - watch parent for it to appear
|
|
144
|
+
const parentDir = path.dirname(subagentsDir);
|
|
145
|
+
if (!fs.existsSync(parentDir)) {
|
|
146
|
+
// Session directory doesn't exist either - retry periodically
|
|
147
|
+
const retryInterval = setInterval(() => {
|
|
148
|
+
if (watcher.stopped) {
|
|
149
|
+
clearInterval(retryInterval);
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
if (fs.existsSync(subagentsDir)) {
|
|
153
|
+
clearInterval(retryInterval);
|
|
154
|
+
watchDirectory(watcher);
|
|
155
|
+
}
|
|
156
|
+
}, 500);
|
|
157
|
+
// Give up after 30 seconds
|
|
158
|
+
setTimeout(() => clearInterval(retryInterval), 30000);
|
|
159
|
+
return;
|
|
160
|
+
}
|
|
161
|
+
// Watch parent directory for subagents/ to appear
|
|
162
|
+
try {
|
|
163
|
+
const parentWatcher = fs.watch(parentDir, (eventType, filename) => {
|
|
164
|
+
if (watcher.stopped)
|
|
165
|
+
return;
|
|
166
|
+
if (filename === 'subagents' && fs.existsSync(subagentsDir)) {
|
|
167
|
+
parentWatcher.close();
|
|
168
|
+
watchDirectory(watcher);
|
|
169
|
+
}
|
|
170
|
+
});
|
|
171
|
+
parentWatcher.on('error', () => parentWatcher.close());
|
|
172
|
+
}
|
|
173
|
+
catch {
|
|
174
|
+
log.warn(`[Watcher] Failed to watch parent dir: ${parentDir}`);
|
|
175
|
+
}
|
|
176
|
+
return;
|
|
177
|
+
}
|
|
178
|
+
// Directory exists - look for existing files or watch for new ones
|
|
179
|
+
watchDirectory(watcher);
|
|
180
|
+
}
|
|
181
|
+
/**
|
|
182
|
+
* Watch the subagents directory for JSONL files
|
|
183
|
+
*/
|
|
184
|
+
function watchDirectory(watcher) {
|
|
185
|
+
if (watcher.stopped)
|
|
186
|
+
return;
|
|
187
|
+
const { subagentsDir } = watcher;
|
|
188
|
+
// Check for existing .jsonl files
|
|
189
|
+
try {
|
|
190
|
+
const files = fs.readdirSync(subagentsDir).filter(f => f.endsWith('.jsonl'));
|
|
191
|
+
if (files.length > 0) {
|
|
192
|
+
// Pick the most recently modified file
|
|
193
|
+
let newest = files[0];
|
|
194
|
+
let newestMtime = 0;
|
|
195
|
+
for (const f of files) {
|
|
196
|
+
try {
|
|
197
|
+
const stat = fs.statSync(path.join(subagentsDir, f));
|
|
198
|
+
if (stat.mtimeMs > newestMtime) {
|
|
199
|
+
newestMtime = stat.mtimeMs;
|
|
200
|
+
newest = f;
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
catch { /* skip */ }
|
|
204
|
+
}
|
|
205
|
+
startFileWatch(watcher, path.join(subagentsDir, newest));
|
|
206
|
+
return;
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
catch { /* directory may have been removed */ }
|
|
210
|
+
// No files yet - watch for new ones
|
|
211
|
+
try {
|
|
212
|
+
watcher.dirWatcher = fs.watch(subagentsDir, (eventType, filename) => {
|
|
213
|
+
if (watcher.stopped)
|
|
214
|
+
return;
|
|
215
|
+
if (filename && filename.endsWith('.jsonl') && !watcher.jsonlPath) {
|
|
216
|
+
const filePath = path.join(subagentsDir, filename);
|
|
217
|
+
if (fs.existsSync(filePath)) {
|
|
218
|
+
watcher.dirWatcher?.close();
|
|
219
|
+
watcher.dirWatcher = undefined;
|
|
220
|
+
startFileWatch(watcher, filePath);
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
});
|
|
224
|
+
watcher.dirWatcher.on('error', () => {
|
|
225
|
+
watcher.dirWatcher?.close();
|
|
226
|
+
watcher.dirWatcher = undefined;
|
|
227
|
+
});
|
|
228
|
+
}
|
|
229
|
+
catch {
|
|
230
|
+
log.warn(`[Watcher] Failed to watch directory: ${subagentsDir}`);
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
/**
|
|
234
|
+
* Start watching a specific JSONL file for new content
|
|
235
|
+
*/
|
|
236
|
+
function startFileWatch(watcher, filePath) {
|
|
237
|
+
if (watcher.stopped)
|
|
238
|
+
return;
|
|
239
|
+
watcher.jsonlPath = filePath;
|
|
240
|
+
log.log(`[Watcher] Found JSONL file: ${filePath} for toolUseId=${watcher.toolUseId}`);
|
|
241
|
+
// Read existing content
|
|
242
|
+
readNewLines(watcher);
|
|
243
|
+
// Start idle timer
|
|
244
|
+
resetIdleTimer(watcher);
|
|
245
|
+
// Watch for changes via fs.watch
|
|
246
|
+
try {
|
|
247
|
+
watcher.fileWatcher = fs.watch(filePath, (eventType) => {
|
|
248
|
+
if (watcher.stopped)
|
|
249
|
+
return;
|
|
250
|
+
if (eventType === 'change') {
|
|
251
|
+
readNewLines(watcher);
|
|
252
|
+
}
|
|
253
|
+
});
|
|
254
|
+
watcher.fileWatcher.on('error', () => {
|
|
255
|
+
watcher.fileWatcher?.close();
|
|
256
|
+
watcher.fileWatcher = undefined;
|
|
257
|
+
});
|
|
258
|
+
}
|
|
259
|
+
catch {
|
|
260
|
+
log.warn(`[Watcher] Failed to watch file: ${filePath}`);
|
|
261
|
+
}
|
|
262
|
+
// Also poll periodically as fallback (fs.watch can miss events on some systems)
|
|
263
|
+
watcher.pollTimer = setInterval(() => {
|
|
264
|
+
if (watcher.stopped)
|
|
265
|
+
return;
|
|
266
|
+
readNewLines(watcher);
|
|
267
|
+
}, POLL_INTERVAL_MS);
|
|
268
|
+
}
|
|
269
|
+
/**
|
|
270
|
+
* Read new lines from the JSONL file since last read position
|
|
271
|
+
*/
|
|
272
|
+
function readNewLines(watcher) {
|
|
273
|
+
if (!watcher.jsonlPath)
|
|
274
|
+
return;
|
|
275
|
+
let fd;
|
|
276
|
+
try {
|
|
277
|
+
fd = fs.openSync(watcher.jsonlPath, 'r');
|
|
278
|
+
const stat = fs.fstatSync(fd);
|
|
279
|
+
if (stat.size <= watcher.readPosition) {
|
|
280
|
+
fs.closeSync(fd);
|
|
281
|
+
return;
|
|
282
|
+
}
|
|
283
|
+
const bytesToRead = stat.size - watcher.readPosition;
|
|
284
|
+
const buffer = Buffer.alloc(bytesToRead);
|
|
285
|
+
fs.readSync(fd, buffer, 0, bytesToRead, watcher.readPosition);
|
|
286
|
+
fs.closeSync(fd);
|
|
287
|
+
fd = undefined;
|
|
288
|
+
watcher.readPosition = stat.size;
|
|
289
|
+
watcher.lastReadTime = Date.now();
|
|
290
|
+
// Reset idle timer since we got new content
|
|
291
|
+
resetIdleTimer(watcher);
|
|
292
|
+
// Split into lines, handling partial lines
|
|
293
|
+
const text = watcher.lineBuffer + buffer.toString('utf8');
|
|
294
|
+
const lines = text.split('\n');
|
|
295
|
+
// Last element may be incomplete - save as buffer
|
|
296
|
+
watcher.lineBuffer = lines.pop() || '';
|
|
297
|
+
for (const line of lines) {
|
|
298
|
+
const trimmed = line.trim();
|
|
299
|
+
if (!trimmed)
|
|
300
|
+
continue;
|
|
301
|
+
const entries = parseLine(trimmed);
|
|
302
|
+
if (entries.length > 0) {
|
|
303
|
+
watcher.pendingEntries.push(...entries);
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
// Debounce broadcast
|
|
307
|
+
scheduleBroadcast(watcher);
|
|
308
|
+
}
|
|
309
|
+
catch {
|
|
310
|
+
if (fd !== undefined) {
|
|
311
|
+
try {
|
|
312
|
+
fs.closeSync(fd);
|
|
313
|
+
}
|
|
314
|
+
catch { /* ignore */ }
|
|
315
|
+
}
|
|
316
|
+
// File may have been removed or not ready
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
/**
|
|
320
|
+
* Schedule a debounced broadcast of pending entries
|
|
321
|
+
*/
|
|
322
|
+
function scheduleBroadcast(watcher) {
|
|
323
|
+
if (watcher.broadcastTimer)
|
|
324
|
+
return; // Already scheduled
|
|
325
|
+
watcher.broadcastTimer = setTimeout(() => {
|
|
326
|
+
watcher.broadcastTimer = undefined;
|
|
327
|
+
flushEntries(watcher);
|
|
328
|
+
}, 300);
|
|
329
|
+
}
|
|
330
|
+
/**
|
|
331
|
+
* Flush pending entries to the broadcast callback
|
|
332
|
+
*/
|
|
333
|
+
function flushEntries(watcher) {
|
|
334
|
+
if (watcher.pendingEntries.length === 0)
|
|
335
|
+
return;
|
|
336
|
+
const entries = watcher.pendingEntries.splice(0, MAX_ENTRIES_PER_BROADCAST);
|
|
337
|
+
// If there are still more, keep the rest for next flush
|
|
338
|
+
if (watcher.pendingEntries.length > 0) {
|
|
339
|
+
scheduleBroadcast(watcher);
|
|
340
|
+
}
|
|
341
|
+
watcher.onBroadcast(watcher.toolUseId, watcher.parentAgentId, entries);
|
|
342
|
+
}
|
|
343
|
+
/**
|
|
344
|
+
* Parse a single JSONL line into SubagentStreamEntry items
|
|
345
|
+
*/
|
|
346
|
+
function parseLine(line) {
|
|
347
|
+
const entries = [];
|
|
348
|
+
try {
|
|
349
|
+
const data = JSON.parse(line);
|
|
350
|
+
const message = data.message;
|
|
351
|
+
if (!message || !message.content)
|
|
352
|
+
return entries;
|
|
353
|
+
const timestamp = data.timestamp || new Date().toISOString();
|
|
354
|
+
const contentArray = Array.isArray(message.content) ? message.content : [];
|
|
355
|
+
// Skip initial user prompts (the task delegation message)
|
|
356
|
+
if (data.type === 'user' && message.role === 'user') {
|
|
357
|
+
// Check if this is a tool_result
|
|
358
|
+
for (const block of contentArray) {
|
|
359
|
+
if (block.type === 'tool_result') {
|
|
360
|
+
const resultText = typeof block.content === 'string'
|
|
361
|
+
? block.content
|
|
362
|
+
: Array.isArray(block.content)
|
|
363
|
+
? block.content.filter((b) => b.type === 'text').map((b) => b.text).join(' ')
|
|
364
|
+
: '';
|
|
365
|
+
if (resultText) {
|
|
366
|
+
entries.push({
|
|
367
|
+
type: 'tool_result',
|
|
368
|
+
timestamp,
|
|
369
|
+
resultPreview: resultText.slice(0, 200),
|
|
370
|
+
isError: block.is_error === true,
|
|
371
|
+
toolUseId: block.tool_use_id,
|
|
372
|
+
});
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
return entries;
|
|
377
|
+
}
|
|
378
|
+
// Parse assistant messages
|
|
379
|
+
if (data.type === 'assistant' && message.role === 'assistant') {
|
|
380
|
+
for (const block of contentArray) {
|
|
381
|
+
if (block.type === 'text' && block.text) {
|
|
382
|
+
const text = block.text.trim();
|
|
383
|
+
if (text) {
|
|
384
|
+
entries.push({
|
|
385
|
+
type: 'text',
|
|
386
|
+
timestamp,
|
|
387
|
+
text: text.slice(0, 200),
|
|
388
|
+
});
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
else if (block.type === 'tool_use') {
|
|
392
|
+
const toolName = block.name || 'Unknown';
|
|
393
|
+
const input = block.input || {};
|
|
394
|
+
const keyParamName = TOOL_KEY_PARAMS[toolName];
|
|
395
|
+
const keyParam = keyParamName && input[keyParamName]
|
|
396
|
+
? String(input[keyParamName]).slice(0, 120)
|
|
397
|
+
: undefined;
|
|
398
|
+
entries.push({
|
|
399
|
+
type: 'tool_use',
|
|
400
|
+
timestamp,
|
|
401
|
+
toolName,
|
|
402
|
+
toolKeyParam: keyParam,
|
|
403
|
+
toolUseId: block.id,
|
|
404
|
+
});
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
catch {
|
|
410
|
+
// Invalid JSON line - skip
|
|
411
|
+
}
|
|
412
|
+
return entries;
|
|
413
|
+
}
|
|
@@ -6,6 +6,7 @@ import { parseAllFormats } from '../handlers/agent-handler.js';
|
|
|
6
6
|
import { agentService, runtimeService } from '../../services/index.js';
|
|
7
7
|
import { logger, formatToolActivity } from '../../utils/index.js';
|
|
8
8
|
import { parseBossDelegation, parseBossSpawn, getBossForSubordinate, clearDelegation } from '../handlers/boss-response-handler.js';
|
|
9
|
+
import { startWatching as startJsonlWatching, stopWatching as stopJsonlWatching, getSubagentsDir } from '../../services/subagent-jsonl-watcher.js';
|
|
9
10
|
const log = logger.ws;
|
|
10
11
|
const MAX_SYNTHETIC_DIFF_FILE_BYTES = 256 * 1024;
|
|
11
12
|
export function setupRuntimeListeners(ctx) {
|
|
@@ -47,6 +48,16 @@ export function setupRuntimeListeners(ctx) {
|
|
|
47
48
|
});
|
|
48
49
|
ctx.sendActivity(agentId, `Spawned subagent: ${subagent.name} (${subagent.subagentType})`);
|
|
49
50
|
log.log(`[Subagent] Broadcast subagent_started: ${subagent.name} (${subagent.id})`);
|
|
51
|
+
// Start streaming JSONL file for this subagent
|
|
52
|
+
if (parentAgent?.sessionId) {
|
|
53
|
+
const subagentsDir = getSubagentsDir(parentAgent.cwd, parentAgent.sessionId);
|
|
54
|
+
startJsonlWatching(event.toolUseId, agentId, subagentsDir, (toolUseId, parentAgentId, entries) => {
|
|
55
|
+
ctx.broadcast({
|
|
56
|
+
type: 'subagent_stream',
|
|
57
|
+
payload: { toolUseId, parentAgentId, entries },
|
|
58
|
+
});
|
|
59
|
+
});
|
|
60
|
+
}
|
|
50
61
|
}
|
|
51
62
|
}
|
|
52
63
|
}
|
|
@@ -84,6 +95,8 @@ export function setupRuntimeListeners(ctx) {
|
|
|
84
95
|
},
|
|
85
96
|
});
|
|
86
97
|
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'}`);
|
|
98
|
+
// Stop streaming JSONL file for this subagent
|
|
99
|
+
stopJsonlWatching(event.toolUseId);
|
|
87
100
|
}
|
|
88
101
|
// Forward subagent internal tool activity to client (events with parentToolUseId)
|
|
89
102
|
if (event.parentToolUseId && event.type === 'tool_start' && event.toolName !== 'Task') {
|