rufloui 0.3.1
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/'1' +0 -0
- package/.env.example +46 -0
- package/CHANGELOG.md +87 -0
- package/CLAUDE.md +287 -0
- package/LICENSE +21 -0
- package/README.md +316 -0
- package/Webhooks) +0 -0
- package/docs/plans/2026-03-11-github-webhooks.md +957 -0
- package/docs/screenshot-swarm-monitor.png +0 -0
- package/frontend +0 -0
- package/index.html +13 -0
- package/package.json +56 -0
- package/public/vite.svg +4 -0
- package/src/backend/__tests__/webhook-github.test.ts +934 -0
- package/src/backend/jsonl-monitor.ts +430 -0
- package/src/backend/server.ts +2972 -0
- package/src/backend/telegram-bot.ts +511 -0
- package/src/backend/webhook-github.ts +350 -0
- package/src/frontend/App.tsx +461 -0
- package/src/frontend/api.ts +281 -0
- package/src/frontend/components/ErrorBoundary.tsx +98 -0
- package/src/frontend/components/Layout.tsx +431 -0
- package/src/frontend/components/ui/Button.tsx +111 -0
- package/src/frontend/components/ui/Card.tsx +51 -0
- package/src/frontend/components/ui/StatusBadge.tsx +60 -0
- package/src/frontend/main.tsx +63 -0
- package/src/frontend/pages/AgentVizPanel.tsx +428 -0
- package/src/frontend/pages/AgentsPanel.tsx +445 -0
- package/src/frontend/pages/ConfigPanel.tsx +661 -0
- package/src/frontend/pages/Dashboard.tsx +482 -0
- package/src/frontend/pages/HiveMindPanel.tsx +355 -0
- package/src/frontend/pages/HooksPanel.tsx +240 -0
- package/src/frontend/pages/LogsPanel.tsx +261 -0
- package/src/frontend/pages/MemoryPanel.tsx +444 -0
- package/src/frontend/pages/NeuralPanel.tsx +301 -0
- package/src/frontend/pages/PerformancePanel.tsx +198 -0
- package/src/frontend/pages/SessionsPanel.tsx +428 -0
- package/src/frontend/pages/SetupWizard.tsx +181 -0
- package/src/frontend/pages/SwarmMonitorPanel.tsx +634 -0
- package/src/frontend/pages/SwarmPanel.tsx +322 -0
- package/src/frontend/pages/TasksPanel.tsx +535 -0
- package/src/frontend/pages/WebhooksPanel.tsx +335 -0
- package/src/frontend/pages/WorkflowsPanel.tsx +448 -0
- package/src/frontend/store.ts +185 -0
- package/src/frontend/styles/global.css +113 -0
- package/src/frontend/test-setup.ts +1 -0
- package/src/frontend/tour/TourContext.tsx +161 -0
- package/src/frontend/tour/tourSteps.ts +181 -0
- package/src/frontend/tour/tourStyles.css +116 -0
- package/src/frontend/types.ts +239 -0
- package/src/frontend/utils/formatTime.test.ts +83 -0
- package/src/frontend/utils/formatTime.ts +23 -0
- package/tsconfig.json +23 -0
- package/vite.config.ts +26 -0
- package/vitest.config.ts +17 -0
- package/{,+ +0 -0
|
@@ -0,0 +1,430 @@
|
|
|
1
|
+
import fs from 'fs'
|
|
2
|
+
import path from 'path'
|
|
3
|
+
import os from 'os'
|
|
4
|
+
|
|
5
|
+
export interface SessionNode {
|
|
6
|
+
id: string
|
|
7
|
+
sessionId: string
|
|
8
|
+
agentId?: string
|
|
9
|
+
slug?: string
|
|
10
|
+
agentType?: string
|
|
11
|
+
status: 'active' | 'idle' | 'done' | 'error'
|
|
12
|
+
currentTool?: string
|
|
13
|
+
currentFile?: string
|
|
14
|
+
lastActivity?: string
|
|
15
|
+
taskId?: string
|
|
16
|
+
children: SessionNode[]
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface MonitoredSession {
|
|
20
|
+
sessionId: string
|
|
21
|
+
taskId: string
|
|
22
|
+
tree: SessionNode
|
|
23
|
+
startedAt: string
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
interface FileState {
|
|
27
|
+
path: string
|
|
28
|
+
bytesRead: number
|
|
29
|
+
watcher: fs.StatWatcher | null
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
interface Monitor {
|
|
33
|
+
sessionId: string
|
|
34
|
+
taskId: string
|
|
35
|
+
tree: SessionNode
|
|
36
|
+
files: Map<string, FileState>
|
|
37
|
+
subagentDir: string
|
|
38
|
+
subagentDirWatcher: fs.StatWatcher | null
|
|
39
|
+
broadcastFn: (type: string, payload: unknown) => void
|
|
40
|
+
startedAt: string
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const monitors = new Map<string, Monitor>()
|
|
44
|
+
|
|
45
|
+
function getProjectSlug(): string {
|
|
46
|
+
const cwd = process.cwd()
|
|
47
|
+
// On Windows: C:\GitHub\rufloui → C--GitHub-rufloui
|
|
48
|
+
// On Unix: /home/user/project → -home-user-project
|
|
49
|
+
const normalized = cwd.replace(/\\/g, '/')
|
|
50
|
+
const match = normalized.match(/^([A-Za-z]):\/(.*)$/)
|
|
51
|
+
if (match) {
|
|
52
|
+
const drive = match[1]
|
|
53
|
+
const rest = match[2].replace(/\//g, '-')
|
|
54
|
+
return `${drive}--${rest}`
|
|
55
|
+
}
|
|
56
|
+
return normalized.replace(/\//g, '-').replace(/^-/, '')
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function getSessionDir(): string {
|
|
60
|
+
const homeDir = os.homedir()
|
|
61
|
+
const slug = getProjectSlug()
|
|
62
|
+
return path.join(homeDir, '.claude', 'projects', slug)
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function parseNewLines(buffer: string): object[] {
|
|
66
|
+
const lines = buffer.split('\n').filter(Boolean)
|
|
67
|
+
const parsed: object[] = []
|
|
68
|
+
for (const line of lines) {
|
|
69
|
+
try {
|
|
70
|
+
parsed.push(JSON.parse(line))
|
|
71
|
+
} catch { /* skip malformed lines */ }
|
|
72
|
+
}
|
|
73
|
+
return parsed
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function extractFileFromInput(input: Record<string, unknown>): string | undefined {
|
|
77
|
+
if (typeof input.file_path === 'string') return input.file_path
|
|
78
|
+
if (typeof input.path === 'string') return input.path
|
|
79
|
+
if (typeof input.command === 'string') {
|
|
80
|
+
// Try to extract file path from command
|
|
81
|
+
const m = (input.command as string).match(/["']?([A-Za-z]:\\[^\s"']+|\/[^\s"']+\.\w+)["']?/)
|
|
82
|
+
if (m) return m[1]
|
|
83
|
+
}
|
|
84
|
+
if (typeof input.pattern === 'string') return input.pattern
|
|
85
|
+
return undefined
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function processEvents(events: object[], node: SessionNode): string[] {
|
|
89
|
+
const newAgentIds: string[] = []
|
|
90
|
+
|
|
91
|
+
for (const evt of events) {
|
|
92
|
+
const e = evt as Record<string, unknown>
|
|
93
|
+
const timestamp = (e.timestamp as string) || new Date().toISOString()
|
|
94
|
+
|
|
95
|
+
// Extract slug if present
|
|
96
|
+
if (e.slug && typeof e.slug === 'string') {
|
|
97
|
+
node.slug = e.slug
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
if (e.type === 'assistant') {
|
|
101
|
+
const msg = e.message as Record<string, unknown> | undefined
|
|
102
|
+
if (!msg?.content) continue
|
|
103
|
+
const content = msg.content as Array<Record<string, unknown>>
|
|
104
|
+
for (const block of content) {
|
|
105
|
+
if (block.type === 'tool_use') {
|
|
106
|
+
node.status = 'active'
|
|
107
|
+
node.currentTool = block.name as string
|
|
108
|
+
node.lastActivity = timestamp
|
|
109
|
+
if (block.input && typeof block.input === 'object') {
|
|
110
|
+
const file = extractFileFromInput(block.input as Record<string, unknown>)
|
|
111
|
+
if (file) node.currentFile = file
|
|
112
|
+
}
|
|
113
|
+
// Detect Agent subagent spawn
|
|
114
|
+
if (block.name === 'Agent') {
|
|
115
|
+
const input = block.input as Record<string, unknown>
|
|
116
|
+
if (input.subagent_type) {
|
|
117
|
+
node.currentTool = `Agent(${input.subagent_type})`
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
} else if (block.type === 'text') {
|
|
121
|
+
node.status = 'active'
|
|
122
|
+
node.lastActivity = timestamp
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
// Check stop_reason
|
|
126
|
+
const stopReason = (msg.stop_reason as string) || ''
|
|
127
|
+
if (stopReason === 'end_turn') {
|
|
128
|
+
node.status = 'idle'
|
|
129
|
+
}
|
|
130
|
+
} else if (e.type === 'user') {
|
|
131
|
+
const msg = e.message as Record<string, unknown> | undefined
|
|
132
|
+
if (!msg?.content) continue
|
|
133
|
+
const content = msg.content as Array<Record<string, unknown>>
|
|
134
|
+
for (const block of content) {
|
|
135
|
+
if (block.type === 'tool_result') {
|
|
136
|
+
node.lastActivity = timestamp
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
} else if (e.type === 'result') {
|
|
140
|
+
node.status = 'done'
|
|
141
|
+
node.currentTool = undefined
|
|
142
|
+
node.currentFile = undefined
|
|
143
|
+
node.lastActivity = timestamp
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// Check for toolUseResult with agentId (subagent completed)
|
|
147
|
+
if (e.toolUseResult && typeof e.toolUseResult === 'object') {
|
|
148
|
+
const tur = e.toolUseResult as Record<string, unknown>
|
|
149
|
+
if (tur.agentId && typeof tur.agentId === 'string') {
|
|
150
|
+
newAgentIds.push(tur.agentId)
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
return newAgentIds
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function tailRead(fileState: FileState): string {
|
|
159
|
+
try {
|
|
160
|
+
const stat = fs.statSync(fileState.path)
|
|
161
|
+
if (stat.size <= fileState.bytesRead) return ''
|
|
162
|
+
const fd = fs.openSync(fileState.path, 'r')
|
|
163
|
+
const len = stat.size - fileState.bytesRead
|
|
164
|
+
const buf = Buffer.alloc(len)
|
|
165
|
+
fs.readSync(fd, buf, 0, len, fileState.bytesRead)
|
|
166
|
+
fs.closeSync(fd)
|
|
167
|
+
fileState.bytesRead = stat.size
|
|
168
|
+
return buf.toString('utf-8')
|
|
169
|
+
} catch {
|
|
170
|
+
return ''
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function startFileWatch(monitor: Monitor, filePath: string, node: SessionNode) {
|
|
175
|
+
if (monitor.files.has(filePath)) return
|
|
176
|
+
if (!fs.existsSync(filePath)) return
|
|
177
|
+
|
|
178
|
+
const fileState: FileState = { path: filePath, bytesRead: 0, watcher: null }
|
|
179
|
+
monitor.files.set(filePath, fileState)
|
|
180
|
+
|
|
181
|
+
// Initial read
|
|
182
|
+
const initial = tailRead(fileState)
|
|
183
|
+
if (initial) {
|
|
184
|
+
const events = parseNewLines(initial)
|
|
185
|
+
const newAgentIds = processEvents(events, node)
|
|
186
|
+
for (const agentId of newAgentIds) {
|
|
187
|
+
addSubagent(monitor, node, agentId)
|
|
188
|
+
}
|
|
189
|
+
monitor.broadcastFn('viz:update', { sessionId: monitor.sessionId, tree: monitor.tree })
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// Poll with fs.watchFile (reliable on Windows)
|
|
193
|
+
fileState.watcher = fs.watchFile(filePath, { interval: 1000 }, () => {
|
|
194
|
+
const newData = tailRead(fileState)
|
|
195
|
+
if (!newData) return
|
|
196
|
+
const events = parseNewLines(newData)
|
|
197
|
+
const newAgentIds = processEvents(events, node)
|
|
198
|
+
for (const agentId of newAgentIds) {
|
|
199
|
+
addSubagent(monitor, node, agentId)
|
|
200
|
+
}
|
|
201
|
+
monitor.broadcastFn('viz:update', { sessionId: monitor.sessionId, tree: monitor.tree })
|
|
202
|
+
})
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
function addSubagent(monitor: Monitor, parentNode: SessionNode, agentId: string) {
|
|
206
|
+
// Check if already tracked
|
|
207
|
+
if (parentNode.children.some(c => c.agentId === agentId)) return
|
|
208
|
+
|
|
209
|
+
// Find subagent JSONL — try various naming patterns
|
|
210
|
+
const patterns = [
|
|
211
|
+
`agent-a${agentId}.jsonl`,
|
|
212
|
+
`agent-${agentId}.jsonl`,
|
|
213
|
+
]
|
|
214
|
+
let subagentPath: string | undefined
|
|
215
|
+
for (const p of patterns) {
|
|
216
|
+
const candidate = path.join(monitor.subagentDir, p)
|
|
217
|
+
if (fs.existsSync(candidate)) {
|
|
218
|
+
subagentPath = candidate
|
|
219
|
+
break
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
const childNode: SessionNode = {
|
|
224
|
+
id: agentId,
|
|
225
|
+
sessionId: monitor.sessionId,
|
|
226
|
+
agentId,
|
|
227
|
+
status: subagentPath ? 'active' : 'idle',
|
|
228
|
+
taskId: monitor.taskId,
|
|
229
|
+
children: [],
|
|
230
|
+
}
|
|
231
|
+
parentNode.children.push(childNode)
|
|
232
|
+
|
|
233
|
+
if (subagentPath) {
|
|
234
|
+
startFileWatch(monitor, subagentPath, childNode)
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
function scanSubagentDir(monitor: Monitor) {
|
|
239
|
+
if (!fs.existsSync(monitor.subagentDir)) return
|
|
240
|
+
try {
|
|
241
|
+
const files = fs.readdirSync(monitor.subagentDir)
|
|
242
|
+
for (const file of files) {
|
|
243
|
+
if (!file.endsWith('.jsonl')) continue
|
|
244
|
+
// Extract agentId from filename like agent-a<id>.jsonl or agent-<id>.jsonl
|
|
245
|
+
const m = file.match(/^agent-a?(.+)\.jsonl$/)
|
|
246
|
+
if (!m) continue
|
|
247
|
+
const agentId = m[1]
|
|
248
|
+
// Check if already tracked in tree
|
|
249
|
+
const alreadyTracked = monitor.tree.children.some(c => {
|
|
250
|
+
const cId = c.agentId || ''
|
|
251
|
+
return cId === agentId || cId === `a${agentId}` || `a${cId}` === agentId
|
|
252
|
+
})
|
|
253
|
+
if (!alreadyTracked) {
|
|
254
|
+
const fullAgentId = file.replace('agent-', '').replace('.jsonl', '')
|
|
255
|
+
const childNode: SessionNode = {
|
|
256
|
+
id: fullAgentId,
|
|
257
|
+
sessionId: monitor.sessionId,
|
|
258
|
+
agentId: fullAgentId,
|
|
259
|
+
status: 'active',
|
|
260
|
+
taskId: monitor.taskId,
|
|
261
|
+
children: [],
|
|
262
|
+
}
|
|
263
|
+
monitor.tree.children.push(childNode)
|
|
264
|
+
startFileWatch(monitor, path.join(monitor.subagentDir, file), childNode)
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
} catch { /* dir may not exist yet */ }
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
export function startMonitoring(
|
|
271
|
+
sessionId: string,
|
|
272
|
+
taskId: string,
|
|
273
|
+
broadcastFn: (type: string, payload: unknown) => void
|
|
274
|
+
): void {
|
|
275
|
+
if (monitors.has(sessionId)) return
|
|
276
|
+
|
|
277
|
+
const sessionDir = getSessionDir()
|
|
278
|
+
const jsonlPath = path.join(sessionDir, `${sessionId}.jsonl`)
|
|
279
|
+
const subagentDir = path.join(sessionDir, sessionId, 'subagents')
|
|
280
|
+
|
|
281
|
+
const rootNode: SessionNode = {
|
|
282
|
+
id: sessionId,
|
|
283
|
+
sessionId,
|
|
284
|
+
status: 'active',
|
|
285
|
+
taskId,
|
|
286
|
+
children: [],
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
const monitor: Monitor = {
|
|
290
|
+
sessionId,
|
|
291
|
+
taskId,
|
|
292
|
+
tree: rootNode,
|
|
293
|
+
files: new Map(),
|
|
294
|
+
subagentDir,
|
|
295
|
+
subagentDirWatcher: null,
|
|
296
|
+
broadcastFn,
|
|
297
|
+
startedAt: new Date().toISOString(),
|
|
298
|
+
}
|
|
299
|
+
monitors.set(sessionId, monitor)
|
|
300
|
+
|
|
301
|
+
// Start watching main JSONL (may not exist yet, poll until it does)
|
|
302
|
+
const waitForFile = setInterval(() => {
|
|
303
|
+
if (fs.existsSync(jsonlPath)) {
|
|
304
|
+
clearInterval(waitForFile)
|
|
305
|
+
startFileWatch(monitor, jsonlPath, rootNode)
|
|
306
|
+
}
|
|
307
|
+
}, 1000)
|
|
308
|
+
|
|
309
|
+
// Also poll for subagent directory appearance
|
|
310
|
+
const waitForSubagents = setInterval(() => {
|
|
311
|
+
if (!monitors.has(sessionId)) {
|
|
312
|
+
clearInterval(waitForSubagents)
|
|
313
|
+
return
|
|
314
|
+
}
|
|
315
|
+
scanSubagentDir(monitor)
|
|
316
|
+
}, 2000)
|
|
317
|
+
|
|
318
|
+
// Store interval refs for cleanup
|
|
319
|
+
;(monitor as unknown as Record<string, NodeJS.Timeout>)._waitFile = waitForFile
|
|
320
|
+
;(monitor as unknown as Record<string, NodeJS.Timeout>)._waitSub = waitForSubagents
|
|
321
|
+
|
|
322
|
+
broadcastFn('viz:update', { sessionId, tree: rootNode })
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
export function stopMonitoring(sessionId: string): void {
|
|
326
|
+
const monitor = monitors.get(sessionId)
|
|
327
|
+
if (!monitor) return
|
|
328
|
+
|
|
329
|
+
// Mark tree as done
|
|
330
|
+
function markDone(node: SessionNode) {
|
|
331
|
+
if (node.status === 'active' || node.status === 'idle') {
|
|
332
|
+
node.status = 'done'
|
|
333
|
+
}
|
|
334
|
+
node.children.forEach(markDone)
|
|
335
|
+
}
|
|
336
|
+
markDone(monitor.tree)
|
|
337
|
+
monitor.broadcastFn('viz:update', { sessionId, tree: monitor.tree })
|
|
338
|
+
|
|
339
|
+
// Clean up watchers
|
|
340
|
+
for (const fs_ of monitor.files.values()) {
|
|
341
|
+
if (fs_.watcher) fs.unwatchFile(fs_.path)
|
|
342
|
+
}
|
|
343
|
+
const m = monitor as unknown as Record<string, NodeJS.Timeout>
|
|
344
|
+
if (m._waitFile) clearInterval(m._waitFile)
|
|
345
|
+
if (m._waitSub) clearInterval(m._waitSub)
|
|
346
|
+
|
|
347
|
+
monitors.delete(sessionId)
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
export function getSessionTree(sessionId: string): SessionNode | null {
|
|
351
|
+
return monitors.get(sessionId)?.tree ?? null
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
export interface LogEntry {
|
|
355
|
+
timestamp: string
|
|
356
|
+
type: string
|
|
357
|
+
tool?: string
|
|
358
|
+
content: string
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
export function getNodeLogs(sessionId: string, nodeId: string, tail = 100): LogEntry[] {
|
|
362
|
+
const sessionDir = getSessionDir()
|
|
363
|
+
// Determine file path based on nodeId
|
|
364
|
+
let filePath: string
|
|
365
|
+
if (nodeId === sessionId) {
|
|
366
|
+
// Root session
|
|
367
|
+
filePath = path.join(sessionDir, `${sessionId}.jsonl`)
|
|
368
|
+
} else {
|
|
369
|
+
// Subagent — try various naming patterns
|
|
370
|
+
const subDir = path.join(sessionDir, sessionId, 'subagents')
|
|
371
|
+
const candidates = [
|
|
372
|
+
path.join(subDir, `agent-${nodeId}.jsonl`),
|
|
373
|
+
path.join(subDir, `agent-a${nodeId}.jsonl`),
|
|
374
|
+
]
|
|
375
|
+
filePath = candidates.find(p => fs.existsSync(p)) || candidates[0]
|
|
376
|
+
}
|
|
377
|
+
// Also check active monitors for file paths
|
|
378
|
+
const monitor = monitors.get(sessionId)
|
|
379
|
+
if (monitor) {
|
|
380
|
+
for (const [fPath] of monitor.files) {
|
|
381
|
+
if (fPath.includes(nodeId)) { filePath = fPath; break }
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
if (!fs.existsSync(filePath)) return []
|
|
385
|
+
|
|
386
|
+
try {
|
|
387
|
+
const raw = fs.readFileSync(filePath, 'utf-8')
|
|
388
|
+
const lines = raw.split('\n').filter(Boolean)
|
|
389
|
+
const entries: LogEntry[] = []
|
|
390
|
+
for (const line of lines) {
|
|
391
|
+
try {
|
|
392
|
+
const obj = JSON.parse(line) as Record<string, unknown>
|
|
393
|
+
const ts = (obj.timestamp as string) || ''
|
|
394
|
+
const type = (obj.type as string) || ''
|
|
395
|
+
if (type === 'assistant') {
|
|
396
|
+
const msg = obj.message as Record<string, unknown> | undefined
|
|
397
|
+
if (!msg?.content) continue
|
|
398
|
+
for (const block of msg.content as Array<Record<string, unknown>>) {
|
|
399
|
+
if (block.type === 'tool_use') {
|
|
400
|
+
const input = block.input as Record<string, unknown> | undefined
|
|
401
|
+
const summary = input?.file_path || input?.command || input?.pattern || input?.query || ''
|
|
402
|
+
entries.push({
|
|
403
|
+
timestamp: ts, type: 'tool_use',
|
|
404
|
+
tool: block.name as string,
|
|
405
|
+
content: String(summary).slice(0, 200),
|
|
406
|
+
})
|
|
407
|
+
} else if (block.type === 'text') {
|
|
408
|
+
const text = (block.text as string || '').slice(0, 300)
|
|
409
|
+
if (text.trim()) {
|
|
410
|
+
entries.push({ timestamp: ts, type: 'text', content: text })
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
} else if (type === 'result') {
|
|
415
|
+
entries.push({ timestamp: ts, type: 'result', content: 'Session completed' })
|
|
416
|
+
}
|
|
417
|
+
} catch { /* skip malformed */ }
|
|
418
|
+
}
|
|
419
|
+
return entries.slice(-tail)
|
|
420
|
+
} catch { return [] }
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
export function getAllMonitoredSessions(): MonitoredSession[] {
|
|
424
|
+
return Array.from(monitors.values()).map(m => ({
|
|
425
|
+
sessionId: m.sessionId,
|
|
426
|
+
taskId: m.taskId,
|
|
427
|
+
tree: m.tree,
|
|
428
|
+
startedAt: m.startedAt,
|
|
429
|
+
}))
|
|
430
|
+
}
|