ec-web 0.0.5 → 0.0.7
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/fs-utils.js +89 -65
- package/dist/paths.js +1 -1
- package/dist/server.js +16 -1
- package/dist/sources/msg.js +218 -34
- package/dist/sources/session-codex.js +72 -6
- package/dist/sources/session.js +84 -11
- package/dist/sources/stats.js +1 -1
- package/dist/sources/transcript-window.js +35 -0
- package/dist/static/app.js +196 -21
- package/dist/static/index.html +1 -1
- package/dist/static/style.css +98 -62
- package/package.json +1 -1
|
@@ -14,6 +14,7 @@ import { createRequire } from 'module';
|
|
|
14
14
|
import { resolvePaths } from '../paths.js';
|
|
15
15
|
import { encodePath } from '../fs-utils.js';
|
|
16
16
|
import { getAgentSelfNames } from './msg.js';
|
|
17
|
+
import { parseTranscriptResultKey, previewLargeToolResults, windowTranscriptTurns } from './transcript-window.js';
|
|
17
18
|
import { buildSessionBindMap, classifyTranscriptSource, isEvolcoreRoutedMessage, isSyntheticCliPrompt, resolveSessionBindSelfName, } from './session-origin.js';
|
|
18
19
|
const requireFromHere = createRequire(import.meta.url);
|
|
19
20
|
let _dlog = null;
|
|
@@ -111,6 +112,8 @@ function listProjects() {
|
|
|
111
112
|
// ── 会话列表 ──
|
|
112
113
|
const CACHE_VERSION = 2;
|
|
113
114
|
const _metaCache = new Map();
|
|
115
|
+
const _detailCache = new Map();
|
|
116
|
+
const MAX_DETAIL_CACHE_ENTRIES = 6;
|
|
114
117
|
function cacheDir(encoded) {
|
|
115
118
|
const dataDir = resolvePaths().dataDir;
|
|
116
119
|
return path.join(dataDir, 'ecweb-cache', 'codex', encoded);
|
|
@@ -134,6 +137,16 @@ function writeDiskCache(encoded, id, mtime, size, meta) {
|
|
|
134
137
|
}
|
|
135
138
|
catch { }
|
|
136
139
|
}
|
|
140
|
+
function cacheTranscriptDetail(file, mtime, size, detail) {
|
|
141
|
+
_detailCache.delete(file);
|
|
142
|
+
_detailCache.set(file, { mtime, size, detail });
|
|
143
|
+
while (_detailCache.size > MAX_DETAIL_CACHE_ENTRIES) {
|
|
144
|
+
const oldest = _detailCache.keys().next().value;
|
|
145
|
+
if (!oldest)
|
|
146
|
+
break;
|
|
147
|
+
_detailCache.delete(oldest);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
137
150
|
function pruneCacheFiles(encoded, validIds) {
|
|
138
151
|
const dir = cacheDir(encoded);
|
|
139
152
|
try {
|
|
@@ -462,7 +475,7 @@ export function parseCodexToolEvent(event) {
|
|
|
462
475
|
}
|
|
463
476
|
return null;
|
|
464
477
|
}
|
|
465
|
-
function readTranscriptFile(threadId, cwd) {
|
|
478
|
+
function readTranscriptFile(threadId, cwd, readStats) {
|
|
466
479
|
const empty = {
|
|
467
480
|
turns: [],
|
|
468
481
|
totalTurns: 0,
|
|
@@ -495,6 +508,21 @@ function readTranscriptFile(threadId, cwd) {
|
|
|
495
508
|
}
|
|
496
509
|
if (!fs.existsSync(rolloutPath))
|
|
497
510
|
return empty;
|
|
511
|
+
let stat;
|
|
512
|
+
try {
|
|
513
|
+
stat = fs.statSync(rolloutPath);
|
|
514
|
+
}
|
|
515
|
+
catch {
|
|
516
|
+
return empty;
|
|
517
|
+
}
|
|
518
|
+
const cached = _detailCache.get(rolloutPath);
|
|
519
|
+
if (cached && cached.mtime === stat.mtimeMs && cached.size === stat.size) {
|
|
520
|
+
_detailCache.delete(rolloutPath);
|
|
521
|
+
_detailCache.set(rolloutPath, cached);
|
|
522
|
+
if (readStats)
|
|
523
|
+
readStats.detailCacheHit = true;
|
|
524
|
+
return { ...cached.detail, title: empty.title || cached.detail.title, cwd };
|
|
525
|
+
}
|
|
498
526
|
let raw;
|
|
499
527
|
try {
|
|
500
528
|
raw = fs.readFileSync(rolloutPath, 'utf-8');
|
|
@@ -602,9 +630,8 @@ function readTranscriptFile(threadId, cwd) {
|
|
|
602
630
|
}
|
|
603
631
|
}
|
|
604
632
|
}
|
|
605
|
-
const
|
|
606
|
-
|
|
607
|
-
turns: shown,
|
|
633
|
+
const detail = {
|
|
634
|
+
turns,
|
|
608
635
|
totalTurns: turns.length,
|
|
609
636
|
userMsgs,
|
|
610
637
|
totalMsgs,
|
|
@@ -619,13 +646,41 @@ function readTranscriptFile(threadId, cwd) {
|
|
|
619
646
|
title: empty.title,
|
|
620
647
|
cwd,
|
|
621
648
|
};
|
|
649
|
+
cacheTranscriptDetail(rolloutPath, stat.mtimeMs, stat.size, detail);
|
|
650
|
+
return detail;
|
|
651
|
+
}
|
|
652
|
+
export function readCodexSessionResult(params) {
|
|
653
|
+
const projects = listProjects();
|
|
654
|
+
const project = resolveProject(params, projects);
|
|
655
|
+
const sessionId = typeof params.sessionId === 'string' ? params.sessionId : '';
|
|
656
|
+
const key = parseTranscriptResultKey(params.resultKey);
|
|
657
|
+
if (!project || !sessionId || !key || !/^[A-Za-z0-9-]+$/.test(sessionId))
|
|
658
|
+
return null;
|
|
659
|
+
const detail = readTranscriptFile(sessionId, project.cwd);
|
|
660
|
+
const block = detail.turns[key[0]]?.blocks?.[key[1]];
|
|
661
|
+
return block?.kind === 'tool_result' && typeof block.text === 'string' ? block.text : null;
|
|
622
662
|
}
|
|
623
663
|
// ── Snapshot ──
|
|
624
664
|
async function buildSnapshot(params) {
|
|
665
|
+
const startedAt = Date.now();
|
|
666
|
+
const metrics = {
|
|
667
|
+
baseagent: 'codex',
|
|
668
|
+
projectMs: 0,
|
|
669
|
+
bindingsMs: 0,
|
|
670
|
+
transcriptListMs: 0,
|
|
671
|
+
detailMs: 0,
|
|
672
|
+
totalMs: 0,
|
|
673
|
+
detailCacheHit: false,
|
|
674
|
+
};
|
|
675
|
+
const projectStartedAt = Date.now();
|
|
625
676
|
const projects = listProjects();
|
|
677
|
+
metrics.projectMs = Date.now() - projectStartedAt;
|
|
678
|
+
const bindingsStartedAt = Date.now();
|
|
626
679
|
const bindMap = buildBindMap();
|
|
680
|
+
metrics.bindingsMs = Date.now() - bindingsStartedAt;
|
|
627
681
|
const project = resolveProject(params, projects);
|
|
628
682
|
if (!project) {
|
|
683
|
+
metrics.totalMs = Date.now() - startedAt;
|
|
629
684
|
return {
|
|
630
685
|
baseagent: 'codex',
|
|
631
686
|
projects: [],
|
|
@@ -633,12 +688,16 @@ async function buildSnapshot(params) {
|
|
|
633
688
|
transcripts: [],
|
|
634
689
|
turns: [],
|
|
635
690
|
sessionId: null,
|
|
691
|
+
metrics,
|
|
636
692
|
};
|
|
637
693
|
}
|
|
694
|
+
const transcriptListStartedAt = Date.now();
|
|
638
695
|
const metas = listTranscripts(project.encoded, project.cwd);
|
|
696
|
+
metrics.transcriptListMs = Date.now() - transcriptListStartedAt;
|
|
639
697
|
const selfNames = await getAgentSelfNames();
|
|
640
698
|
const transcripts = metas.map(m => {
|
|
641
699
|
const bind = bindMap.get(m.id);
|
|
700
|
+
metrics.totalMs = Date.now() - startedAt;
|
|
642
701
|
return {
|
|
643
702
|
...m,
|
|
644
703
|
sourceType: classifyTranscriptSource(bind, isCodexCliCandidate(m)),
|
|
@@ -663,9 +722,13 @@ async function buildSnapshot(params) {
|
|
|
663
722
|
transcripts,
|
|
664
723
|
turns: [],
|
|
665
724
|
sessionId: null,
|
|
725
|
+
metrics,
|
|
666
726
|
};
|
|
667
727
|
}
|
|
668
|
-
const
|
|
728
|
+
const detailStartedAt = Date.now();
|
|
729
|
+
const detail = readTranscriptFile(sessionId, project.cwd, metrics);
|
|
730
|
+
metrics.detailMs = Date.now() - detailStartedAt;
|
|
731
|
+
const page = windowTranscriptTurns(detail.turns, params.turnLimit, params.turnBefore);
|
|
669
732
|
const bind = bindMap.get(sessionId);
|
|
670
733
|
const transcript = metas.find(meta => meta.id === sessionId);
|
|
671
734
|
const header = {
|
|
@@ -689,14 +752,17 @@ async function buildSnapshot(params) {
|
|
|
689
752
|
boundSelf: resolveSessionBindSelfName(bind, selfNames),
|
|
690
753
|
online: bind ? (Date.now() - bind.updatedAt < 5 * 60 * 1000) : false,
|
|
691
754
|
};
|
|
755
|
+
metrics.totalMs = Date.now() - startedAt;
|
|
692
756
|
return {
|
|
693
757
|
baseagent: 'codex',
|
|
694
758
|
projects: projList,
|
|
695
759
|
project: project.encoded,
|
|
696
760
|
transcripts,
|
|
697
|
-
turns:
|
|
761
|
+
turns: previewLargeToolResults(page.turns, page.turnPage.start),
|
|
762
|
+
turnPage: page.turnPage,
|
|
698
763
|
sessionId,
|
|
699
764
|
header,
|
|
765
|
+
metrics,
|
|
700
766
|
};
|
|
701
767
|
}
|
|
702
768
|
// ── Export ──
|
package/dist/sources/session.js
CHANGED
|
@@ -16,6 +16,7 @@ import { resolvePaths, ccProjectsDir } from '../paths.js';
|
|
|
16
16
|
import { encodePath } from '../fs-utils.js';
|
|
17
17
|
import { snapshotCodex, subscribeCodex, setDebugLog as setCodexDebugLog } from './session-codex.js';
|
|
18
18
|
import { getAgentSelfNames } from './msg.js';
|
|
19
|
+
import { parseTranscriptResultKey, previewLargeToolResults, windowTranscriptTurns } from './transcript-window.js';
|
|
19
20
|
import { buildSessionBindMap, classifyTranscriptSource, isSyntheticCliPrompt, resolveSessionBindSelfName, } from './session-origin.js';
|
|
20
21
|
let _dlog = null;
|
|
21
22
|
export function setDebugLog(log) {
|
|
@@ -29,6 +30,8 @@ function dlog(line) { if (_dlog)
|
|
|
29
30
|
catch { } }
|
|
30
31
|
const CACHE_VERSION = 5;
|
|
31
32
|
const _metaCache = new Map();
|
|
33
|
+
const _detailCache = new Map();
|
|
34
|
+
const MAX_DETAIL_CACHE_ENTRIES = 6;
|
|
32
35
|
function cacheDir(encoded) {
|
|
33
36
|
const dataDir = resolvePaths().dataDir;
|
|
34
37
|
const currentRoot = path.join(dataDir, 'ecweb-cache');
|
|
@@ -60,6 +63,16 @@ function writeDiskCache(encoded, id, mtime, size, meta) {
|
|
|
60
63
|
}
|
|
61
64
|
catch { }
|
|
62
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
|
+
}
|
|
63
76
|
function pruneCacheFiles(encoded, validIds) {
|
|
64
77
|
const dir = cacheDir(encoded);
|
|
65
78
|
try {
|
|
@@ -486,8 +499,23 @@ function pricingFor(model) {
|
|
|
486
499
|
return PRICING['claude-haiku-4-5-20251001'];
|
|
487
500
|
return PRICING['claude-opus-4-8'];
|
|
488
501
|
}
|
|
489
|
-
function readTranscriptFile(file) {
|
|
502
|
+
function readTranscriptFile(file, readStats) {
|
|
490
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
|
+
}
|
|
491
519
|
let raw;
|
|
492
520
|
try {
|
|
493
521
|
raw = fs.readFileSync(file, 'utf-8');
|
|
@@ -575,20 +603,60 @@ function readTranscriptFile(file) {
|
|
|
575
603
|
else {
|
|
576
604
|
category = hasToolUse ? 'tool_call' : 'model_output';
|
|
577
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;
|
|
578
610
|
counts[category === 'user_input' ? 'userInput' : category === 'model_output' ? 'modelOutput' : category === 'tool_call' ? 'toolCall' : 'toolResult']++;
|
|
579
611
|
turns.push({ role: o.type, ts: o.timestamp ? Date.parse(o.timestamp) : 0, uuid: o.uuid, category, blocks });
|
|
580
612
|
}
|
|
581
613
|
}
|
|
582
|
-
const
|
|
583
|
-
|
|
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;
|
|
617
|
+
}
|
|
618
|
+
export function readClaudeSessionResult(params) {
|
|
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;
|
|
584
634
|
}
|
|
585
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();
|
|
586
647
|
const projects = listProjects();
|
|
648
|
+
metrics.projectMs = Date.now() - projectStartedAt;
|
|
649
|
+
const bindingsStartedAt = Date.now();
|
|
587
650
|
const bindMap = buildBindMap();
|
|
651
|
+
metrics.bindingsMs = Date.now() - bindingsStartedAt;
|
|
588
652
|
const project = resolveProject(params, projects);
|
|
589
|
-
if (!project)
|
|
590
|
-
|
|
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();
|
|
591
658
|
const metas = listTranscripts(project.encoded);
|
|
659
|
+
metrics.transcriptListMs = Date.now() - transcriptListStartedAt;
|
|
592
660
|
const selfNames = await getAgentSelfNames();
|
|
593
661
|
const transcripts = metas.map(m => {
|
|
594
662
|
const bind = bindMap.get(m.id);
|
|
@@ -603,17 +671,22 @@ async function buildSnapshot(params) {
|
|
|
603
671
|
});
|
|
604
672
|
const projList = projects.map(p => ({ encoded: p.encoded, label: p.label, cwd: p.cwd, count: p.count }));
|
|
605
673
|
const sessionId = params.sessionId || null;
|
|
606
|
-
if (!sessionId)
|
|
607
|
-
|
|
608
|
-
|
|
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;
|
|
609
681
|
const bind = bindMap.get(sessionId);
|
|
610
682
|
const taskTurns = bind?.evolSessionId ? readRunnerTaskTurns(resolvePaths().logs, bind.evolSessionId) : [];
|
|
611
683
|
const turns = detail.turns.concat(taskTurns).sort((a, b) => (a.ts || 0) - (b.ts || 0));
|
|
612
|
-
const
|
|
684
|
+
const page = windowTranscriptTurns(turns, params.turnLimit, params.turnBefore);
|
|
613
685
|
const counts = { ...detail.counts, subtask: taskTurns.length };
|
|
614
686
|
const transcript = metas.find(meta => meta.id === sessionId);
|
|
615
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 };
|
|
616
|
-
|
|
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 };
|
|
617
690
|
}
|
|
618
691
|
export const sessionSource = {
|
|
619
692
|
kind: 'session',
|
|
@@ -634,7 +707,7 @@ export const sessionSource = {
|
|
|
634
707
|
const fire = () => {
|
|
635
708
|
if (debounce)
|
|
636
709
|
clearTimeout(debounce);
|
|
637
|
-
debounce = setTimeout(() => { void buildSnapshot(params).then(push).catch(() => { }); },
|
|
710
|
+
debounce = setTimeout(() => { void buildSnapshot(params).then(push).catch(() => { }); }, 300);
|
|
638
711
|
};
|
|
639
712
|
const projects = listProjects();
|
|
640
713
|
const project = resolveProject(params, projects);
|
package/dist/sources/stats.js
CHANGED
|
@@ -447,7 +447,7 @@ function getPeerInfo(peerKey) {
|
|
|
447
447
|
if (target.startsWith('group.')) {
|
|
448
448
|
// 群聊:读取群信息
|
|
449
449
|
const { sessionsDir } = resolvePaths();
|
|
450
|
-
const groupDir = path.join(sessionsDir, '
|
|
450
|
+
const groupDir = path.join(sessionsDir, encodeSegment(agentAid), 'sessions', encodeSegment(`aun#${agentAid}#main`), encodeSegment(target));
|
|
451
451
|
const activeJsonPath = path.join(groupDir, 'active.json');
|
|
452
452
|
if (fs.existsSync(activeJsonPath)) {
|
|
453
453
|
const activeData = JSON.parse(fs.readFileSync(activeJsonPath, 'utf-8'));
|
|
@@ -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
|
+
}
|