ec-web 0.0.4 → 0.0.6
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/server.js +63 -6
- package/dist/sources/msg.js +371 -14
- package/dist/sources/session-codex.js +115 -35
- package/dist/sources/session-origin.js +195 -0
- package/dist/sources/session.js +292 -41
- package/dist/sources/transcript-window.js +35 -0
- package/dist/static/app.js +456 -62
- package/dist/static/index.html +4 -5
- package/dist/static/style.css +153 -63
- package/dist/static/vendor/echarts-5.6.0.min.js +45 -0
- package/package.json +2 -2
package/dist/sources/session.js
CHANGED
|
@@ -13,8 +13,11 @@
|
|
|
13
13
|
import fs from 'fs';
|
|
14
14
|
import path from 'path';
|
|
15
15
|
import { resolvePaths, ccProjectsDir } from '../paths.js';
|
|
16
|
-
import { encodePath
|
|
16
|
+
import { encodePath } from '../fs-utils.js';
|
|
17
17
|
import { snapshotCodex, subscribeCodex, setDebugLog as setCodexDebugLog } from './session-codex.js';
|
|
18
|
+
import { getAgentSelfNames } from './msg.js';
|
|
19
|
+
import { parseTranscriptResultKey, previewLargeToolResults, windowTranscriptTurns } from './transcript-window.js';
|
|
20
|
+
import { buildSessionBindMap, classifyTranscriptSource, isSyntheticCliPrompt, resolveSessionBindSelfName, } from './session-origin.js';
|
|
18
21
|
let _dlog = null;
|
|
19
22
|
export function setDebugLog(log) {
|
|
20
23
|
_dlog = log;
|
|
@@ -25,8 +28,10 @@ function dlog(line) { if (_dlog)
|
|
|
25
28
|
_dlog(line);
|
|
26
29
|
}
|
|
27
30
|
catch { } }
|
|
28
|
-
const CACHE_VERSION =
|
|
31
|
+
const CACHE_VERSION = 5;
|
|
29
32
|
const _metaCache = new Map();
|
|
33
|
+
const _detailCache = new Map();
|
|
34
|
+
const MAX_DETAIL_CACHE_ENTRIES = 6;
|
|
30
35
|
function cacheDir(encoded) {
|
|
31
36
|
const dataDir = resolvePaths().dataDir;
|
|
32
37
|
const currentRoot = path.join(dataDir, 'ecweb-cache');
|
|
@@ -58,6 +63,16 @@ function writeDiskCache(encoded, id, mtime, size, meta) {
|
|
|
58
63
|
}
|
|
59
64
|
catch { }
|
|
60
65
|
}
|
|
66
|
+
function cacheTranscriptDetail(file, mtime, size, detail) {
|
|
67
|
+
_detailCache.delete(file);
|
|
68
|
+
_detailCache.set(file, { mtime, size, detail });
|
|
69
|
+
while (_detailCache.size > MAX_DETAIL_CACHE_ENTRIES) {
|
|
70
|
+
const oldest = _detailCache.keys().next().value;
|
|
71
|
+
if (!oldest)
|
|
72
|
+
break;
|
|
73
|
+
_detailCache.delete(oldest);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
61
76
|
function pruneCacheFiles(encoded, validIds) {
|
|
62
77
|
const dir = cacheDir(encoded);
|
|
63
78
|
try {
|
|
@@ -120,7 +135,8 @@ function extractMeta(file, id, stat) {
|
|
|
120
135
|
raw = fs.readFileSync(file, 'utf-8');
|
|
121
136
|
}
|
|
122
137
|
catch { }
|
|
123
|
-
let title = '', firstUser = '', gitBranch = '', version = '', userMsgs = 0, totalMsgs = 0;
|
|
138
|
+
let title = '', firstUser = '', gitBranch = '', version = '', entrypoint = '', userMsgs = 0, totalMsgs = 0;
|
|
139
|
+
let hasRealCliMessage = false, internalSubagent = id.startsWith('agent-');
|
|
124
140
|
for (const line of raw.split('\n')) {
|
|
125
141
|
if (!line)
|
|
126
142
|
continue;
|
|
@@ -140,8 +156,20 @@ function extractMeta(file, id, stat) {
|
|
|
140
156
|
totalMsgs++;
|
|
141
157
|
else if (isUser) {
|
|
142
158
|
totalMsgs++;
|
|
143
|
-
if (o
|
|
144
|
-
|
|
159
|
+
if (o) {
|
|
160
|
+
if (isCountableUserMessage(o, id))
|
|
161
|
+
userMsgs++;
|
|
162
|
+
if (!entrypoint && typeof o.entrypoint === 'string')
|
|
163
|
+
entrypoint = o.entrypoint;
|
|
164
|
+
if (o.isSidechain || o.agentId)
|
|
165
|
+
internalSubagent = true;
|
|
166
|
+
const content = o.message?.content;
|
|
167
|
+
if (!isToolResultOnly(content)) {
|
|
168
|
+
const text = extractMessageText(content);
|
|
169
|
+
if (text.trim() && !isSyntheticCliPrompt(text, 'claude'))
|
|
170
|
+
hasRealCliMessage = true;
|
|
171
|
+
}
|
|
172
|
+
}
|
|
145
173
|
}
|
|
146
174
|
if ((!gitBranch || !version || !firstUser) && (line.indexOf('gitBranch') !== -1 || line.indexOf('"version"') !== -1 || line.indexOf('ai-title') !== -1 || (isUser && !firstUser))) {
|
|
147
175
|
if (!o) {
|
|
@@ -165,7 +193,12 @@ function extractMeta(file, id, stat) {
|
|
|
165
193
|
}
|
|
166
194
|
}
|
|
167
195
|
}
|
|
168
|
-
return {
|
|
196
|
+
return {
|
|
197
|
+
id, title, firstUser, gitBranch, version, userMsgs, totalMsgs,
|
|
198
|
+
sizeKB: Math.round(stat.size / 1024),
|
|
199
|
+
lastActivity: stat.mtimeMs,
|
|
200
|
+
cliCandidate: !internalSubagent && (entrypoint === 'cli' || entrypoint === 'sdk-cli') && hasRealCliMessage,
|
|
201
|
+
};
|
|
169
202
|
}
|
|
170
203
|
function listTranscripts(encoded) {
|
|
171
204
|
const dir = path.join(ccProjectsDir(), encoded);
|
|
@@ -293,25 +326,162 @@ function resolveProject(params, projects) {
|
|
|
293
326
|
return projects[0] || null;
|
|
294
327
|
}
|
|
295
328
|
export function buildBindMap() {
|
|
296
|
-
|
|
329
|
+
return buildSessionBindMap();
|
|
330
|
+
}
|
|
331
|
+
// ── EvolCore runner 子任务事件 ──
|
|
332
|
+
const RUNNER_TASK_EVENT_TYPES = new Set([
|
|
333
|
+
'runner:task-started',
|
|
334
|
+
'runner:task-progress',
|
|
335
|
+
'runner:task-notification',
|
|
336
|
+
]);
|
|
337
|
+
const EVENT_LOG_RE = /^events(?:-.+)?\.log$/;
|
|
338
|
+
const _runnerLogCache = new Map();
|
|
339
|
+
function parseRunnerTaskLines(raw) {
|
|
340
|
+
const events = [];
|
|
341
|
+
for (const line of raw.split(/\r?\n/)) {
|
|
342
|
+
if (!line.includes('"type":"runner:task-'))
|
|
343
|
+
continue;
|
|
344
|
+
try {
|
|
345
|
+
const event = JSON.parse(line);
|
|
346
|
+
if (RUNNER_TASK_EVENT_TYPES.has(event.type) && event.sessionId)
|
|
347
|
+
events.push(event);
|
|
348
|
+
}
|
|
349
|
+
catch { }
|
|
350
|
+
}
|
|
351
|
+
return events;
|
|
352
|
+
}
|
|
353
|
+
function readRunnerLogFile(file) {
|
|
354
|
+
let stat;
|
|
297
355
|
try {
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
356
|
+
stat = fs.statSync(file);
|
|
357
|
+
}
|
|
358
|
+
catch {
|
|
359
|
+
_runnerLogCache.delete(file);
|
|
360
|
+
return [];
|
|
361
|
+
}
|
|
362
|
+
let cached = _runnerLogCache.get(file);
|
|
363
|
+
if (!cached || stat.size < cached.offset)
|
|
364
|
+
cached = { offset: 0, events: [] };
|
|
365
|
+
if (stat.size === cached.offset) {
|
|
366
|
+
_runnerLogCache.set(file, cached);
|
|
367
|
+
return cached.events;
|
|
368
|
+
}
|
|
369
|
+
const bytes = stat.size - cached.offset;
|
|
370
|
+
const buffer = Buffer.allocUnsafe(bytes);
|
|
371
|
+
let fd = null;
|
|
372
|
+
try {
|
|
373
|
+
fd = fs.openSync(file, 'r');
|
|
374
|
+
fs.readSync(fd, buffer, 0, bytes, cached.offset);
|
|
375
|
+
}
|
|
376
|
+
catch {
|
|
377
|
+
return cached.events;
|
|
378
|
+
}
|
|
379
|
+
finally {
|
|
380
|
+
if (fd != null)
|
|
381
|
+
try {
|
|
382
|
+
fs.closeSync(fd);
|
|
383
|
+
}
|
|
384
|
+
catch { }
|
|
385
|
+
}
|
|
386
|
+
// 只推进到最后一个完整 JSONL 行,避免 watcher 在写入中途触发时丢事件。
|
|
387
|
+
const lastBreak = buffer.lastIndexOf(0x0a);
|
|
388
|
+
if (lastBreak < 0) {
|
|
389
|
+
_runnerLogCache.set(file, cached);
|
|
390
|
+
return cached.events;
|
|
391
|
+
}
|
|
392
|
+
const parsed = parseRunnerTaskLines(buffer.subarray(0, lastBreak + 1).toString('utf8'));
|
|
393
|
+
cached = {
|
|
394
|
+
offset: cached.offset + lastBreak + 1,
|
|
395
|
+
events: cached.events.concat(parsed),
|
|
396
|
+
};
|
|
397
|
+
_runnerLogCache.set(file, cached);
|
|
398
|
+
return cached.events;
|
|
399
|
+
}
|
|
400
|
+
function runnerEventTimestamp(event) {
|
|
401
|
+
const timestamp = Number(event.timestamp);
|
|
402
|
+
if (Number.isFinite(timestamp) && timestamp > 0)
|
|
403
|
+
return timestamp;
|
|
404
|
+
const parsed = event.ts ? Date.parse(event.ts) : 0;
|
|
405
|
+
return Number.isFinite(parsed) ? parsed : 0;
|
|
406
|
+
}
|
|
407
|
+
export function readRunnerTaskTurns(logsDir, evolSessionId) {
|
|
408
|
+
let files;
|
|
409
|
+
try {
|
|
410
|
+
files = fs.readdirSync(logsDir)
|
|
411
|
+
.filter(name => EVENT_LOG_RE.test(name))
|
|
412
|
+
.sort()
|
|
413
|
+
.map(name => path.join(logsDir, name));
|
|
414
|
+
}
|
|
415
|
+
catch {
|
|
416
|
+
return [];
|
|
417
|
+
}
|
|
418
|
+
const validFiles = new Set(files);
|
|
419
|
+
for (const cachedFile of _runnerLogCache.keys()) {
|
|
420
|
+
if (path.dirname(cachedFile) === logsDir && !validFiles.has(cachedFile))
|
|
421
|
+
_runnerLogCache.delete(cachedFile);
|
|
422
|
+
}
|
|
423
|
+
const events = files
|
|
424
|
+
.flatMap(readRunnerLogFile)
|
|
425
|
+
.filter(event => event.sessionId === evolSessionId)
|
|
426
|
+
.sort((a, b) => runnerEventTimestamp(a) - runnerEventTimestamp(b));
|
|
427
|
+
const tasks = new Map();
|
|
428
|
+
for (const event of events) {
|
|
429
|
+
const taskId = event.taskId || event.toolUseId;
|
|
430
|
+
if (!taskId)
|
|
431
|
+
continue;
|
|
432
|
+
const at = runnerEventTimestamp(event);
|
|
433
|
+
let task = tasks.get(taskId);
|
|
434
|
+
if (!task) {
|
|
435
|
+
task = {
|
|
436
|
+
taskId,
|
|
437
|
+
taskKind: event.taskKind || 'agent',
|
|
438
|
+
toolUseId: event.toolUseId || null,
|
|
439
|
+
description: event.description || taskId,
|
|
440
|
+
subagentType: event.subagentType || null,
|
|
441
|
+
status: 'running',
|
|
442
|
+
terminalReason: '',
|
|
443
|
+
progress: '',
|
|
444
|
+
lastToolName: '',
|
|
445
|
+
totalTokens: 0,
|
|
446
|
+
toolUses: 0,
|
|
447
|
+
durationMs: 0,
|
|
448
|
+
startedAt: at,
|
|
449
|
+
updatedAt: at,
|
|
450
|
+
};
|
|
451
|
+
tasks.set(taskId, task);
|
|
311
452
|
}
|
|
453
|
+
if (event.type === 'runner:task-started') {
|
|
454
|
+
task.startedAt = task.startedAt ? Math.min(task.startedAt, at) : at;
|
|
455
|
+
task.status = 'running';
|
|
456
|
+
}
|
|
457
|
+
else if (event.type === 'runner:task-progress') {
|
|
458
|
+
task.progress = event.summary || event.description || task.progress;
|
|
459
|
+
}
|
|
460
|
+
else if (event.type === 'runner:task-notification') {
|
|
461
|
+
task.status = event.status || 'completed';
|
|
462
|
+
task.terminalReason = event.terminalReason || task.terminalReason;
|
|
463
|
+
}
|
|
464
|
+
task.updatedAt = Math.max(task.updatedAt || 0, at);
|
|
465
|
+
task.taskKind = event.taskKind || task.taskKind;
|
|
466
|
+
task.toolUseId = event.toolUseId || task.toolUseId;
|
|
467
|
+
task.description = event.type === 'runner:task-progress'
|
|
468
|
+
? task.description
|
|
469
|
+
: (event.description || task.description);
|
|
470
|
+
task.subagentType = event.subagentType || task.subagentType;
|
|
471
|
+
task.lastToolName = event.lastToolName || task.lastToolName;
|
|
472
|
+
task.totalTokens = event.usage?.totalTokens ?? event.totalTokens ?? task.totalTokens;
|
|
473
|
+
task.toolUses = event.usage?.toolUses ?? event.toolUses ?? task.toolUses;
|
|
474
|
+
task.durationMs = event.usage?.durationMs ?? event.durationMs ?? task.durationMs;
|
|
312
475
|
}
|
|
313
|
-
|
|
314
|
-
|
|
476
|
+
return Array.from(tasks.values())
|
|
477
|
+
.sort((a, b) => a.startedAt - b.startedAt)
|
|
478
|
+
.map(task => ({
|
|
479
|
+
role: 'system',
|
|
480
|
+
ts: task.startedAt,
|
|
481
|
+
uuid: `runner-task:${evolSessionId}:${task.taskId}`,
|
|
482
|
+
category: 'subtask',
|
|
483
|
+
blocks: [{ kind: 'subtask', ...task }],
|
|
484
|
+
}));
|
|
315
485
|
}
|
|
316
486
|
// ── 完整 transcript 解析(省略 tool 参数/chat send 检测等细节,保留费用计算)──
|
|
317
487
|
const PRICING = {
|
|
@@ -329,8 +499,23 @@ function pricingFor(model) {
|
|
|
329
499
|
return PRICING['claude-haiku-4-5-20251001'];
|
|
330
500
|
return PRICING['claude-opus-4-8'];
|
|
331
501
|
}
|
|
332
|
-
function readTranscriptFile(file) {
|
|
502
|
+
function readTranscriptFile(file, readStats) {
|
|
333
503
|
const empty = { turns: [], totalTurns: 0, userMsgs: 0, totalMsgs: 0, counts: { userInput: 0, modelOutput: 0, toolCall: 0, toolResult: 0, msgSend: 0 }, inputTokens: 0, outputTokens: 0, contextTokens: 0, costUsd: 0, model: '', gitBranch: '', version: '', title: '', cwd: '' };
|
|
504
|
+
let stat;
|
|
505
|
+
try {
|
|
506
|
+
stat = fs.statSync(file);
|
|
507
|
+
}
|
|
508
|
+
catch {
|
|
509
|
+
return empty;
|
|
510
|
+
}
|
|
511
|
+
const cached = _detailCache.get(file);
|
|
512
|
+
if (cached && cached.mtime === stat.mtimeMs && cached.size === stat.size) {
|
|
513
|
+
_detailCache.delete(file);
|
|
514
|
+
_detailCache.set(file, cached);
|
|
515
|
+
if (readStats)
|
|
516
|
+
readStats.detailCacheHit = true;
|
|
517
|
+
return cached.detail;
|
|
518
|
+
}
|
|
334
519
|
let raw;
|
|
335
520
|
try {
|
|
336
521
|
raw = fs.readFileSync(file, 'utf-8');
|
|
@@ -418,32 +603,90 @@ function readTranscriptFile(file) {
|
|
|
418
603
|
else {
|
|
419
604
|
category = hasToolUse ? 'tool_call' : 'model_output';
|
|
420
605
|
}
|
|
606
|
+
// Claude can emit assistant events that contain usage metadata but no visible blocks.
|
|
607
|
+
// They are not useful transcript entries and previously rendered as empty model output.
|
|
608
|
+
if (o.type === 'assistant' && blocks.length === 0)
|
|
609
|
+
continue;
|
|
421
610
|
counts[category === 'user_input' ? 'userInput' : category === 'model_output' ? 'modelOutput' : category === 'tool_call' ? 'toolCall' : 'toolResult']++;
|
|
422
611
|
turns.push({ role: o.type, ts: o.timestamp ? Date.parse(o.timestamp) : 0, uuid: o.uuid, category, blocks });
|
|
423
612
|
}
|
|
424
613
|
}
|
|
425
|
-
const
|
|
426
|
-
|
|
614
|
+
const detail = { turns, totalTurns: turns.length, userMsgs, totalMsgs, counts, inputTokens: inTok, outputTokens: outTok, contextTokens, costUsd, model, gitBranch: branch, version, title, cwd };
|
|
615
|
+
cacheTranscriptDetail(file, stat.mtimeMs, stat.size, detail);
|
|
616
|
+
return detail;
|
|
427
617
|
}
|
|
428
|
-
function
|
|
618
|
+
export function readClaudeSessionResult(params) {
|
|
429
619
|
const projects = listProjects();
|
|
620
|
+
const project = resolveProject(params, projects);
|
|
621
|
+
const sessionId = typeof params.sessionId === 'string' ? params.sessionId : '';
|
|
622
|
+
const key = parseTranscriptResultKey(params.resultKey);
|
|
623
|
+
if (!project || !sessionId || !key || !/^[A-Za-z0-9-]+$/.test(sessionId))
|
|
624
|
+
return null;
|
|
625
|
+
const file = path.join(ccProjectsDir(), project.encoded, `${sessionId}.jsonl`);
|
|
626
|
+
if (!file.startsWith(`${path.join(ccProjectsDir(), project.encoded)}${path.sep}`))
|
|
627
|
+
return null;
|
|
628
|
+
const detail = readTranscriptFile(file);
|
|
629
|
+
const bind = buildBindMap().get(sessionId);
|
|
630
|
+
const taskTurns = bind?.evolSessionId ? readRunnerTaskTurns(resolvePaths().logs, bind.evolSessionId) : [];
|
|
631
|
+
const turns = detail.turns.concat(taskTurns).sort((a, b) => (a.ts || 0) - (b.ts || 0));
|
|
632
|
+
const block = turns[key[0]]?.blocks?.[key[1]];
|
|
633
|
+
return block?.kind === 'tool_result' && typeof block.text === 'string' ? block.text : null;
|
|
634
|
+
}
|
|
635
|
+
async function buildSnapshot(params) {
|
|
636
|
+
const startedAt = Date.now();
|
|
637
|
+
const metrics = {
|
|
638
|
+
baseagent: 'claude',
|
|
639
|
+
projectMs: 0,
|
|
640
|
+
bindingsMs: 0,
|
|
641
|
+
transcriptListMs: 0,
|
|
642
|
+
detailMs: 0,
|
|
643
|
+
totalMs: 0,
|
|
644
|
+
detailCacheHit: false,
|
|
645
|
+
};
|
|
646
|
+
const projectStartedAt = Date.now();
|
|
647
|
+
const projects = listProjects();
|
|
648
|
+
metrics.projectMs = Date.now() - projectStartedAt;
|
|
649
|
+
const bindingsStartedAt = Date.now();
|
|
430
650
|
const bindMap = buildBindMap();
|
|
651
|
+
metrics.bindingsMs = Date.now() - bindingsStartedAt;
|
|
431
652
|
const project = resolveProject(params, projects);
|
|
432
|
-
if (!project)
|
|
433
|
-
|
|
653
|
+
if (!project) {
|
|
654
|
+
metrics.totalMs = Date.now() - startedAt;
|
|
655
|
+
return { baseagent: 'claude', projects: [], project: null, transcripts: [], turns: [], sessionId: null, metrics };
|
|
656
|
+
}
|
|
657
|
+
const transcriptListStartedAt = Date.now();
|
|
434
658
|
const metas = listTranscripts(project.encoded);
|
|
659
|
+
metrics.transcriptListMs = Date.now() - transcriptListStartedAt;
|
|
660
|
+
const selfNames = await getAgentSelfNames();
|
|
435
661
|
const transcripts = metas.map(m => {
|
|
436
662
|
const bind = bindMap.get(m.id);
|
|
437
|
-
return {
|
|
663
|
+
return {
|
|
664
|
+
...m,
|
|
665
|
+
sourceType: classifyTranscriptSource(bind, m.cliCandidate),
|
|
666
|
+
bound: !!bind,
|
|
667
|
+
boundChannel: bind?.channelType ?? null,
|
|
668
|
+
boundSelf: resolveSessionBindSelfName(bind, selfNames),
|
|
669
|
+
online: bind ? (Date.now() - bind.updatedAt < 5 * 60 * 1000) : false,
|
|
670
|
+
};
|
|
438
671
|
});
|
|
439
672
|
const projList = projects.map(p => ({ encoded: p.encoded, label: p.label, cwd: p.cwd, count: p.count }));
|
|
440
673
|
const sessionId = params.sessionId || null;
|
|
441
|
-
if (!sessionId)
|
|
442
|
-
|
|
443
|
-
|
|
674
|
+
if (!sessionId) {
|
|
675
|
+
metrics.totalMs = Date.now() - startedAt;
|
|
676
|
+
return { baseagent: 'claude', projects: projList, project: project.encoded, transcripts, turns: [], sessionId: null, metrics };
|
|
677
|
+
}
|
|
678
|
+
const detailStartedAt = Date.now();
|
|
679
|
+
const detail = readTranscriptFile(path.join(ccProjectsDir(), project.encoded, `${sessionId}.jsonl`), metrics);
|
|
680
|
+
metrics.detailMs = Date.now() - detailStartedAt;
|
|
444
681
|
const bind = bindMap.get(sessionId);
|
|
445
|
-
const
|
|
446
|
-
|
|
682
|
+
const taskTurns = bind?.evolSessionId ? readRunnerTaskTurns(resolvePaths().logs, bind.evolSessionId) : [];
|
|
683
|
+
const turns = detail.turns.concat(taskTurns).sort((a, b) => (a.ts || 0) - (b.ts || 0));
|
|
684
|
+
const page = windowTranscriptTurns(turns, params.turnLimit, params.turnBefore);
|
|
685
|
+
const counts = { ...detail.counts, subtask: taskTurns.length };
|
|
686
|
+
const transcript = metas.find(meta => meta.id === sessionId);
|
|
687
|
+
const header = { sessionId, title: detail.title, model: detail.model, gitBranch: detail.gitBranch, version: detail.version, cwd: detail.cwd || project.cwd, totalTurns: detail.totalTurns, userMsgs: detail.userMsgs, totalMsgs: detail.totalMsgs, counts, inputTokens: detail.inputTokens, outputTokens: detail.outputTokens, contextTokens: detail.contextTokens, costUsd: detail.costUsd, sourceType: classifyTranscriptSource(bind, transcript?.cliCandidate === true), bound: !!bind, boundChannel: bind?.channelType ?? null, boundSelf: resolveSessionBindSelfName(bind, selfNames), online: bind ? (Date.now() - bind.updatedAt < 5 * 60 * 1000) : false };
|
|
688
|
+
metrics.totalMs = Date.now() - startedAt;
|
|
689
|
+
return { baseagent: 'claude', projects: projList, project: project.encoded, transcripts, turns: previewLargeToolResults(page.turns, page.turnPage.start), turnPage: page.turnPage, sessionId, header, metrics };
|
|
447
690
|
}
|
|
448
691
|
export const sessionSource = {
|
|
449
692
|
kind: 'session',
|
|
@@ -460,12 +703,12 @@ export const sessionSource = {
|
|
|
460
703
|
return subscribeCodex(params, push);
|
|
461
704
|
}
|
|
462
705
|
const p = resolvePaths();
|
|
463
|
-
let projectWatcher = null, sessionWatcher = null, debounce = null;
|
|
464
|
-
const fire = () => {
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
706
|
+
let projectWatcher = null, sessionWatcher = null, logWatcher = null, debounce = null;
|
|
707
|
+
const fire = () => {
|
|
708
|
+
if (debounce)
|
|
709
|
+
clearTimeout(debounce);
|
|
710
|
+
debounce = setTimeout(() => { void buildSnapshot(params).then(push).catch(() => { }); }, 300);
|
|
711
|
+
};
|
|
469
712
|
const projects = listProjects();
|
|
470
713
|
const project = resolveProject(params, projects);
|
|
471
714
|
if (project) {
|
|
@@ -481,9 +724,17 @@ export const sessionSource = {
|
|
|
481
724
|
fire(); });
|
|
482
725
|
}
|
|
483
726
|
catch { }
|
|
727
|
+
if (params.sessionId) {
|
|
728
|
+
try {
|
|
729
|
+
logWatcher = fs.watch(p.logs, (_evt, filename) => { if (!filename || EVENT_LOG_RE.test(String(filename)))
|
|
730
|
+
fire(); });
|
|
731
|
+
}
|
|
732
|
+
catch { }
|
|
733
|
+
}
|
|
484
734
|
return () => { if (projectWatcher)
|
|
485
735
|
projectWatcher.close(); if (sessionWatcher)
|
|
486
|
-
sessionWatcher.close(); if (
|
|
736
|
+
sessionWatcher.close(); if (logWatcher)
|
|
737
|
+
logWatcher.close(); if (debounce)
|
|
487
738
|
clearTimeout(debounce); };
|
|
488
739
|
},
|
|
489
740
|
};
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
export function windowTranscriptTurns(turns, requestedLimit, requestedBefore, defaultLimit = 100, maxLimit = 200) {
|
|
2
|
+
const requested = Number(requestedLimit);
|
|
3
|
+
const limit = Number.isFinite(requested)
|
|
4
|
+
? Math.min(Math.max(Math.trunc(requested), 20), maxLimit)
|
|
5
|
+
: defaultLimit;
|
|
6
|
+
const beforeValue = requestedBefore == null ? Number.NaN : Number(requestedBefore);
|
|
7
|
+
const before = Number.isFinite(beforeValue) && beforeValue >= 0 ? Math.trunc(beforeValue) : null;
|
|
8
|
+
const end = before == null ? turns.length : Math.min(Math.max(before, 0), turns.length);
|
|
9
|
+
const start = Math.max(0, end - limit);
|
|
10
|
+
return {
|
|
11
|
+
turns: turns.slice(start, end),
|
|
12
|
+
turnPage: { limit, before, nextBefore: start > 0 ? start : null, hasMore: start > 0, start },
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
export function previewLargeToolResults(turns, start, previewChars = 4_000) {
|
|
16
|
+
return turns.map((turn, turnOffset) => ({
|
|
17
|
+
...turn,
|
|
18
|
+
blocks: (turn.blocks || []).map((block, blockIndex) => {
|
|
19
|
+
if (block.kind !== 'tool_result' || typeof block.text !== 'string' || block.text.length <= previewChars)
|
|
20
|
+
return block;
|
|
21
|
+
return {
|
|
22
|
+
...block,
|
|
23
|
+
text: block.text.slice(0, previewChars),
|
|
24
|
+
resultKey: `${start + turnOffset}:${blockIndex}`,
|
|
25
|
+
truncated: true,
|
|
26
|
+
};
|
|
27
|
+
}),
|
|
28
|
+
}));
|
|
29
|
+
}
|
|
30
|
+
export function parseTranscriptResultKey(value) {
|
|
31
|
+
const match = /^(\d+):(\d+)$/.exec(String(value || ''));
|
|
32
|
+
if (!match)
|
|
33
|
+
return null;
|
|
34
|
+
return [Number(match[1]), Number(match[2])];
|
|
35
|
+
}
|