codex-session-insights 0.1.0 → 0.2.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/README.md +4 -2
- package/lib/cli.js +22 -22
- package/lib/codex-data.js +42 -7
- package/lib/llm-insights.js +83 -7
- package/lib/report.js +85 -21
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -108,8 +108,8 @@ npx codex-session-insights --provider openai --api-key $OPENAI_API_KEY
|
|
|
108
108
|
Current default analysis plan:
|
|
109
109
|
|
|
110
110
|
- `days`: `30`
|
|
111
|
-
- `limit`: `
|
|
112
|
-
- `facet-limit`: `
|
|
111
|
+
- `limit`: `200`
|
|
112
|
+
- `facet-limit`: `50`
|
|
113
113
|
- `provider`: `codex-cli`
|
|
114
114
|
- `facet-model`: `gpt-5.4-mini`
|
|
115
115
|
- `fast-section-model`: `gpt-5.4-mini`
|
|
@@ -120,6 +120,8 @@ Current default analysis plan:
|
|
|
120
120
|
|
|
121
121
|
Important behavior defaults:
|
|
122
122
|
|
|
123
|
+
- `limit` means the target number of substantive threads to include in the report, not just the first 50 indexed threads
|
|
124
|
+
- `facet-limit` means the max number of uncached per-thread facet analyses to run in a single report
|
|
123
125
|
- Report language follows a best-effort system locale check
|
|
124
126
|
- Main-thread analysis is the default; sub-agent threads are excluded unless you pass `--include-subagents`
|
|
125
127
|
- The CLI shows an estimate before running in interactive terminals
|
package/lib/cli.js
CHANGED
|
@@ -141,7 +141,7 @@ function parseArgs(argv) {
|
|
|
141
141
|
jsonPath: null,
|
|
142
142
|
htmlPath: null,
|
|
143
143
|
days: 30,
|
|
144
|
-
limit:
|
|
144
|
+
limit: 200,
|
|
145
145
|
preview: 50,
|
|
146
146
|
provider: 'codex-cli',
|
|
147
147
|
codexBin: null,
|
|
@@ -154,7 +154,7 @@ function parseArgs(argv) {
|
|
|
154
154
|
insightModel: null,
|
|
155
155
|
insightEffort: null,
|
|
156
156
|
cacheDir: null,
|
|
157
|
-
facetLimit:
|
|
157
|
+
facetLimit: 50,
|
|
158
158
|
lang: detectSystemLanguage(),
|
|
159
159
|
includeArchived: false,
|
|
160
160
|
includeSubagents: false,
|
|
@@ -330,7 +330,7 @@ Options:
|
|
|
330
330
|
--json-path <path> Exact path for report.json
|
|
331
331
|
--html-path <path> Exact path for report.html
|
|
332
332
|
--days <n> Only include threads updated in the last N days (default: 30)
|
|
333
|
-
--limit <n>
|
|
333
|
+
--limit <n> Target number of substantive threads to include (default: 200)
|
|
334
334
|
--preview <n> Number of threads to embed in the HTML report (default: 50)
|
|
335
335
|
--provider <name> Model provider: codex-cli or openai (default: codex-cli)
|
|
336
336
|
--codex-bin <path> Override the Codex CLI binary path for provider=codex-cli
|
|
@@ -344,7 +344,7 @@ Options:
|
|
|
344
344
|
Reasoning effort for lower-risk sections
|
|
345
345
|
--insight-model <name> Model for final report generation
|
|
346
346
|
--insight-effort <level> Reasoning effort for higher-risk sections
|
|
347
|
-
--facet-limit <n> Max uncached
|
|
347
|
+
--facet-limit <n> Max uncached thread facets to analyze (default: 50)
|
|
348
348
|
--cache-dir <path> Cache directory for session-meta and facet caches
|
|
349
349
|
--lang <code> Report language: en or zh-CN (default: system language)
|
|
350
350
|
--include-archived Include archived threads
|
|
@@ -657,8 +657,8 @@ function inferScopePreset(days) {
|
|
|
657
657
|
|
|
658
658
|
function inferDepthPreset(options) {
|
|
659
659
|
if (options.limit === 20 && options.facetLimit === 8) return 'conservative'
|
|
660
|
-
if (options.limit ===
|
|
661
|
-
if (options.limit ===
|
|
660
|
+
if (options.limit === 200 && options.facetLimit === 50) return 'standard'
|
|
661
|
+
if (options.limit === 400 && options.facetLimit === 50) return 'deep'
|
|
662
662
|
return 'custom'
|
|
663
663
|
}
|
|
664
664
|
|
|
@@ -682,8 +682,8 @@ function inferQualityPreset(options) {
|
|
|
682
682
|
|
|
683
683
|
function applyScopePreset(options, preset) {
|
|
684
684
|
if (preset === 'conservative') return { ...options, limit: 20, facetLimit: 8 }
|
|
685
|
-
if (preset === 'deep') return { ...options, limit:
|
|
686
|
-
return { ...options, limit:
|
|
685
|
+
if (preset === 'deep') return { ...options, limit: 400, facetLimit: 50 }
|
|
686
|
+
return { ...options, limit: 200, facetLimit: 50 }
|
|
687
687
|
}
|
|
688
688
|
|
|
689
689
|
function applyQualityPreset(options, preset) {
|
|
@@ -833,17 +833,17 @@ function getUiText(lang) {
|
|
|
833
833
|
scopeCustom: '自定义天数',
|
|
834
834
|
customDaysQuestion: '输入要分析的天数',
|
|
835
835
|
depthQuestion: '选择分析深度:',
|
|
836
|
-
depthConservative: '保守(20
|
|
837
|
-
depthStandard: '标准(
|
|
838
|
-
depthDeep: '深度(
|
|
836
|
+
depthConservative: '保守(20 个有效线程 / 8 个 facets)',
|
|
837
|
+
depthStandard: '标准(200 个有效线程 / 50 个 facets)',
|
|
838
|
+
depthDeep: '深度(400 个有效线程 / 50 个 facets)',
|
|
839
839
|
depthCustom: '自定义',
|
|
840
|
-
limitQuestion: '
|
|
841
|
-
facetLimitQuestion: '
|
|
840
|
+
limitQuestion: '输入目标有效线程数',
|
|
841
|
+
facetLimitQuestion: '输入最大新增 facet 数',
|
|
842
842
|
languageQuestion: '选择报告语言:',
|
|
843
843
|
outputDirQuestion: '输出目录',
|
|
844
844
|
openBrowserQuestion: '生成后自动打开浏览器?',
|
|
845
845
|
qualityQuestion: '选择分析质量预设:',
|
|
846
|
-
qualityCheaper: '更省(尽量用
|
|
846
|
+
qualityCheaper: '更省(尽量用 gpt-5.4-mini)',
|
|
847
847
|
qualityBalanced: '平衡',
|
|
848
848
|
qualityHigher: '更高质量(更多使用 gpt-5.4)',
|
|
849
849
|
yesDefault: '[Y/n]',
|
|
@@ -860,7 +860,7 @@ function getUiText(lang) {
|
|
|
860
860
|
toWord: '到',
|
|
861
861
|
likelyWord: '左右',
|
|
862
862
|
plannedCallsLabel: '预计调用数',
|
|
863
|
-
substantiveThreadsLabel: '
|
|
863
|
+
substantiveThreadsLabel: '纳入报告线程',
|
|
864
864
|
uncachedFacetsLabel: '未缓存 facets',
|
|
865
865
|
longTranscriptsLabel: '长 transcript',
|
|
866
866
|
inputEstimateLabel: '输入',
|
|
@@ -891,17 +891,17 @@ function getUiText(lang) {
|
|
|
891
891
|
scopeCustom: 'Custom days',
|
|
892
892
|
customDaysQuestion: 'Enter number of days to analyze',
|
|
893
893
|
depthQuestion: 'Choose analysis depth:',
|
|
894
|
-
depthConservative: 'Conservative (20 threads / 8 facets)',
|
|
895
|
-
depthStandard: 'Standard (
|
|
896
|
-
depthDeep: 'Deep (
|
|
894
|
+
depthConservative: 'Conservative (20 substantive threads / 8 facets)',
|
|
895
|
+
depthStandard: 'Standard (200 substantive threads / 50 facets)',
|
|
896
|
+
depthDeep: 'Deep (400 substantive threads / 50 facets)',
|
|
897
897
|
depthCustom: 'Custom',
|
|
898
|
-
limitQuestion: 'Enter
|
|
899
|
-
facetLimitQuestion: 'Enter max facet extraction count',
|
|
898
|
+
limitQuestion: 'Enter target substantive thread count',
|
|
899
|
+
facetLimitQuestion: 'Enter max new facet extraction count',
|
|
900
900
|
languageQuestion: 'Choose report language:',
|
|
901
901
|
outputDirQuestion: 'Output directory',
|
|
902
902
|
openBrowserQuestion: 'Open the report in your browser after generation?',
|
|
903
903
|
qualityQuestion: 'Choose quality preset:',
|
|
904
|
-
qualityCheaper: 'Cheaper (more
|
|
904
|
+
qualityCheaper: 'Cheaper (more gpt-5.4-mini)',
|
|
905
905
|
qualityBalanced: 'Balanced',
|
|
906
906
|
qualityHigher: 'Higher quality (more gpt-5.4)',
|
|
907
907
|
yesDefault: '[Y/n]',
|
|
@@ -918,7 +918,7 @@ function getUiText(lang) {
|
|
|
918
918
|
toWord: 'to',
|
|
919
919
|
likelyWord: 'likely',
|
|
920
920
|
plannedCallsLabel: 'planned calls',
|
|
921
|
-
substantiveThreadsLabel: '
|
|
921
|
+
substantiveThreadsLabel: 'threads in report',
|
|
922
922
|
uncachedFacetsLabel: 'uncached facets',
|
|
923
923
|
longTranscriptsLabel: 'long transcripts',
|
|
924
924
|
inputEstimateLabel: 'input',
|
package/lib/codex-data.js
CHANGED
|
@@ -44,6 +44,7 @@ export async function loadThreads({
|
|
|
44
44
|
codexHome,
|
|
45
45
|
sinceEpochSeconds,
|
|
46
46
|
limit,
|
|
47
|
+
offset,
|
|
47
48
|
includeArchived,
|
|
48
49
|
includeSubagents,
|
|
49
50
|
}) {
|
|
@@ -66,6 +67,7 @@ export async function loadThreads({
|
|
|
66
67
|
where.length ? `where ${where.join(' and ')}` : '',
|
|
67
68
|
'order by updated_at desc',
|
|
68
69
|
limit ? `limit ${Number(limit)}` : '',
|
|
70
|
+
offset ? `offset ${Number(offset)}` : '',
|
|
69
71
|
';',
|
|
70
72
|
]
|
|
71
73
|
.filter(Boolean)
|
|
@@ -115,11 +117,15 @@ export async function loadRolloutEvents(rolloutPath) {
|
|
|
115
117
|
return events
|
|
116
118
|
}
|
|
117
119
|
|
|
120
|
+
export function isSubstantiveThread(thread) {
|
|
121
|
+
if (thread.userMessages < 2) return false
|
|
122
|
+
if (thread.durationMinutes < 1) return false
|
|
123
|
+
return Boolean(String(thread.transcriptForAnalysis || '').trim())
|
|
124
|
+
}
|
|
125
|
+
|
|
118
126
|
export function filterSubstantiveThreads(threadSummaries) {
|
|
119
127
|
return [...threadSummaries]
|
|
120
|
-
.filter(
|
|
121
|
-
.filter(thread => thread.durationMinutes >= 1)
|
|
122
|
-
.filter(thread => Boolean(String(thread.transcriptForAnalysis || '').trim()))
|
|
128
|
+
.filter(isSubstantiveThread)
|
|
123
129
|
.sort((a, b) => Date.parse(b.updatedAt) - Date.parse(a.updatedAt))
|
|
124
130
|
}
|
|
125
131
|
|
|
@@ -440,14 +446,43 @@ export function summarizeThread(thread, events) {
|
|
|
440
446
|
}
|
|
441
447
|
|
|
442
448
|
export async function collectThreadSummaries(options) {
|
|
443
|
-
const threads = await loadThreads(options)
|
|
444
449
|
const cacheDir = resolveSessionMetaCacheDir(options.cacheDir)
|
|
445
450
|
await fs.mkdir(cacheDir, { recursive: true })
|
|
446
451
|
|
|
452
|
+
const substantiveTarget = Number(options.limit ?? 0)
|
|
453
|
+
const pageSize = substantiveTarget > 0 ? substantiveTarget : 200
|
|
447
454
|
const summaries = []
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
455
|
+
let offset = 0
|
|
456
|
+
let substantiveCount = 0
|
|
457
|
+
|
|
458
|
+
while (true) {
|
|
459
|
+
const threads = await loadThreads({
|
|
460
|
+
...options,
|
|
461
|
+
limit: pageSize,
|
|
462
|
+
offset,
|
|
463
|
+
})
|
|
464
|
+
if (!threads.length) break
|
|
465
|
+
|
|
466
|
+
for (const thread of threads) {
|
|
467
|
+
const summary = await loadCachedOrFreshSummary(thread, cacheDir)
|
|
468
|
+
summaries.push(summary)
|
|
469
|
+
if (isSubstantiveThread(summary)) {
|
|
470
|
+
substantiveCount += 1
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
if (substantiveTarget > 0 && substantiveCount >= substantiveTarget) {
|
|
475
|
+
break
|
|
476
|
+
}
|
|
477
|
+
if (threads.length < pageSize) {
|
|
478
|
+
break
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
offset += threads.length
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
if (substantiveTarget > 0) {
|
|
485
|
+
return filterSubstantiveThreads(summaries).slice(0, substantiveTarget)
|
|
451
486
|
}
|
|
452
487
|
return summaries
|
|
453
488
|
}
|
package/lib/llm-insights.js
CHANGED
|
@@ -276,24 +276,27 @@ const SECTION_DEFS = [
|
|
|
276
276
|
contextKind: 'project_areas',
|
|
277
277
|
schemaName: 'codex_project_areas',
|
|
278
278
|
schema: PROJECT_AREAS_SCHEMA,
|
|
279
|
-
prompt: `Analyze this Codex usage data and identify
|
|
279
|
+
prompt: `Analyze this Codex usage data and identify the user's main workstreams.
|
|
280
280
|
|
|
281
281
|
RESPOND WITH ONLY A VALID JSON OBJECT:
|
|
282
282
|
{
|
|
283
283
|
"areas": [
|
|
284
|
-
{"name": "Area name", "session_count": 0, "description": "2-3 sentences
|
|
284
|
+
{"name": "Area name", "session_count": 0, "description": "2-3 sentences describing the workstream, its recurring tasks, and why it matters."}
|
|
285
285
|
]
|
|
286
286
|
}
|
|
287
287
|
|
|
288
|
-
Include 4
|
|
288
|
+
Include 3-4 areas. Skip Codex self-hosting/meta work unless it is a dominant project area.
|
|
289
289
|
|
|
290
290
|
Guardrails:
|
|
291
291
|
- Use concrete project or workstream names, not generic labels like "coding" or "development"
|
|
292
292
|
- Base areas on repeated evidence across summaries, not one-off threads
|
|
293
293
|
- Prefer project + task framing over tool-centric framing
|
|
294
|
-
- Group related tasks into a coherent workstream instead of listing each task separately
|
|
295
|
-
-
|
|
296
|
-
-
|
|
294
|
+
- Group related tasks into a coherent long-running workstream instead of listing each task separately
|
|
295
|
+
- Prefer fewer, broader areas that still feel accurate over a more complete but fragmented list
|
|
296
|
+
- Do not turn recent sub-tasks, bugfixes, or cleanup passes into separate areas unless they clearly form their own repeated stream
|
|
297
|
+
- Each description should read like a workstream summary, not a changelog
|
|
298
|
+
- Mention representative tasks, artifacts, or decisions so the area feels concrete without enumerating every thread
|
|
299
|
+
- Keep the focus on what the user was trying to accomplish; mention Codex only lightly when it clarifies the shape of the work`,
|
|
297
300
|
},
|
|
298
301
|
{
|
|
299
302
|
name: 'interaction_style',
|
|
@@ -311,9 +314,12 @@ RESPOND WITH ONLY A VALID JSON OBJECT:
|
|
|
311
314
|
|
|
312
315
|
Guardrails:
|
|
313
316
|
- Focus on stable interaction patterns, not isolated moments
|
|
314
|
-
- Talk about how the user scopes work,
|
|
317
|
+
- Talk about how the user scopes work, redirects goals, sets acceptance bars, or trusts execution
|
|
318
|
+
- Prefer evidence from user requests, follow-up corrections, repeated constraints, and outcome patterns over implementation telemetry
|
|
315
319
|
- Do not infer user preference from Codex's default tool mix or harness behavior; high exec/tool usage can reflect the agent's operating style rather than the user's instructions
|
|
316
320
|
- Treat shell usage, file reads, and verification commands as weak evidence unless the user explicitly asked for that working style
|
|
321
|
+
- Do not infer style from repository type, documentation volume, or language mix alone
|
|
322
|
+
- Avoid turning a single repo's workflow shape into a personality claim about the user
|
|
317
323
|
- If evidence is mixed, describe the tension instead of forcing one clean story`,
|
|
318
324
|
},
|
|
319
325
|
{
|
|
@@ -360,6 +366,7 @@ Include 3 friction categories with 2 examples each.
|
|
|
360
366
|
Guardrails:
|
|
361
367
|
- Separate model-side friction from user/workflow-side friction when useful
|
|
362
368
|
- Examples must be concrete and tied to the supplied evidence
|
|
369
|
+
- Treat overlap or concurrency metrics as weak supporting evidence unless the summaries or friction details also show real switching pain
|
|
363
370
|
- Do not invent root causes that are not visible in the data`,
|
|
364
371
|
},
|
|
365
372
|
{
|
|
@@ -401,6 +408,9 @@ Guardrails:
|
|
|
401
408
|
- Suggest only actions that clearly connect to repeated evidence
|
|
402
409
|
- Avoid generic advice like "give more context" unless it is overwhelmingly justified
|
|
403
410
|
- Prefer changes with strong leverage: repo memory, repeatable workflows, automation, or parallelism
|
|
411
|
+
- Do not recommend first-time adoption of AGENTS.md, Skills, codex exec, Sub-agents, or MCP Servers when the capability_adoption evidence shows the user already uses them in a moderate or strong way
|
|
412
|
+
- When a capability is already adopted, suggest a deeper refinement or a tighter operating pattern instead of basic adoption
|
|
413
|
+
- Distinguish "you should start using this" from "you should formalize or deepen how you already use this"
|
|
404
414
|
- Write AGENTS.md additions as directly pasteable instruction lines, not commentary about instructions
|
|
405
415
|
- Make feature examples immediately usable; avoid placeholders like "insert your repo path here" unless unavoidable
|
|
406
416
|
- Make usage pattern suggestions sound like concrete next actions the user can try today, not abstract best practices`,
|
|
@@ -989,6 +999,8 @@ function buildInsightContext(report, threadSummaries, facets) {
|
|
|
989
999
|
),
|
|
990
1000
|
).slice(0, MAX_USER_INSTRUCTIONS)
|
|
991
1001
|
|
|
1002
|
+
const capabilityAdoption = summarizeCapabilityAdoption(report, threadSummaries, facets)
|
|
1003
|
+
|
|
992
1004
|
return {
|
|
993
1005
|
metadata: {
|
|
994
1006
|
generated_at: report.metadata.generatedAt,
|
|
@@ -1025,6 +1037,7 @@ function buildInsightContext(report, threadSummaries, facets) {
|
|
|
1025
1037
|
friction,
|
|
1026
1038
|
success,
|
|
1027
1039
|
},
|
|
1040
|
+
capability_adoption: capabilityAdoption,
|
|
1028
1041
|
session_summaries: sortedFacets.slice(0, MAX_CONTEXT_FACETS).map(facet => ({
|
|
1029
1042
|
thread_id: facet.threadId,
|
|
1030
1043
|
title: truncateForContext(facet.title, 80),
|
|
@@ -1051,6 +1064,69 @@ function buildInsightContext(report, threadSummaries, facets) {
|
|
|
1051
1064
|
}
|
|
1052
1065
|
}
|
|
1053
1066
|
|
|
1067
|
+
function summarizeCapabilityAdoption(report, threadSummaries, facets) {
|
|
1068
|
+
const textByThread = new Map()
|
|
1069
|
+
for (const thread of threadSummaries) {
|
|
1070
|
+
textByThread.set(
|
|
1071
|
+
thread.id,
|
|
1072
|
+
[thread.title, thread.firstUserMessage]
|
|
1073
|
+
.map(value => String(value || ''))
|
|
1074
|
+
.join('\n')
|
|
1075
|
+
.toLowerCase(),
|
|
1076
|
+
)
|
|
1077
|
+
}
|
|
1078
|
+
|
|
1079
|
+
for (const facet of facets) {
|
|
1080
|
+
const existing = textByThread.get(facet.threadId) || ''
|
|
1081
|
+
const facetText = [
|
|
1082
|
+
facet.underlying_goal,
|
|
1083
|
+
facet.brief_summary,
|
|
1084
|
+
...(facet.user_instructions || []),
|
|
1085
|
+
]
|
|
1086
|
+
.map(value => String(value || ''))
|
|
1087
|
+
.join('\n')
|
|
1088
|
+
.toLowerCase()
|
|
1089
|
+
textByThread.set(facet.threadId, `${existing}\n${facetText}`.trim())
|
|
1090
|
+
}
|
|
1091
|
+
|
|
1092
|
+
const detectMentionedThreads = regex => {
|
|
1093
|
+
let count = 0
|
|
1094
|
+
for (const text of textByThread.values()) {
|
|
1095
|
+
if (regex.test(text)) count += 1
|
|
1096
|
+
}
|
|
1097
|
+
return count
|
|
1098
|
+
}
|
|
1099
|
+
|
|
1100
|
+
const totalThreads = Math.max(1, Number(report.metadata.threadCount || threadSummaries.length || 0))
|
|
1101
|
+
const signals = {
|
|
1102
|
+
agents_md: detectMentionedThreads(/\bagents\.md\b/i),
|
|
1103
|
+
skills: detectMentionedThreads(/\bskills?\b/i),
|
|
1104
|
+
codex_exec: detectMentionedThreads(/\bcodex exec\b/i),
|
|
1105
|
+
subagents: Number(report.summary.sessionsUsingTaskAgent || 0),
|
|
1106
|
+
mcp_servers: Number(report.summary.sessionsUsingMcp || 0),
|
|
1107
|
+
web_search: Number(report.summary.sessionsUsingWebSearch || 0),
|
|
1108
|
+
web_fetch: Number(report.summary.sessionsUsingWebFetch || 0),
|
|
1109
|
+
}
|
|
1110
|
+
|
|
1111
|
+
return Object.fromEntries(
|
|
1112
|
+
Object.entries(signals).map(([key, count]) => [
|
|
1113
|
+
key,
|
|
1114
|
+
{
|
|
1115
|
+
count,
|
|
1116
|
+
status: classifyCapabilityAdoption(count, totalThreads),
|
|
1117
|
+
},
|
|
1118
|
+
]),
|
|
1119
|
+
)
|
|
1120
|
+
}
|
|
1121
|
+
|
|
1122
|
+
function classifyCapabilityAdoption(count, totalThreads) {
|
|
1123
|
+
const share = Number(count || 0) / Math.max(1, Number(totalThreads || 0))
|
|
1124
|
+
if (count >= 10 || share >= 0.25) return 'strong'
|
|
1125
|
+
if (count >= 4 || share >= 0.1) return 'moderate'
|
|
1126
|
+
if (count > 0) return 'light'
|
|
1127
|
+
return 'none'
|
|
1128
|
+
}
|
|
1129
|
+
|
|
1054
1130
|
function buildAtAGlancePrompt(context, insights) {
|
|
1055
1131
|
return `You are writing an "At a Glance" summary for a Codex usage insights report.
|
|
1056
1132
|
|
package/lib/report.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import os from 'node:os'
|
|
1
2
|
import path from 'node:path'
|
|
2
3
|
import { promises as fs } from 'node:fs'
|
|
3
4
|
|
|
@@ -6,6 +7,9 @@ export function buildReport(threadSummaries, options = {}) {
|
|
|
6
7
|
const modelCounts = {}
|
|
7
8
|
const toolCounts = {}
|
|
8
9
|
const commandKindCounts = {}
|
|
10
|
+
const capabilityCounts = {}
|
|
11
|
+
const outcomeCounts = {}
|
|
12
|
+
const sessionTypeCounts = {}
|
|
9
13
|
const toolFailureCounts = {}
|
|
10
14
|
const activeHourCounts = {}
|
|
11
15
|
const responseTimes = []
|
|
@@ -81,8 +85,20 @@ export function buildReport(threadSummaries, options = {}) {
|
|
|
81
85
|
if (thread.usesMcp) sessionsUsingMcp += 1
|
|
82
86
|
if (thread.usesWebSearch) sessionsUsingWebSearch += 1
|
|
83
87
|
if (thread.usesWebFetch) sessionsUsingWebFetch += 1
|
|
88
|
+
if (thread.filesModified > 0) increment(capabilityCounts, 'Repo edits')
|
|
89
|
+
if (thread.gitCommits > 0 || thread.gitPushes > 0) increment(capabilityCounts, 'Git activity')
|
|
84
90
|
}
|
|
85
91
|
|
|
92
|
+
for (const facet of options.facets || []) {
|
|
93
|
+
if (facet.outcome) increment(outcomeCounts, facet.outcome)
|
|
94
|
+
if (facet.session_type) increment(sessionTypeCounts, facet.session_type)
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
if (sessionsUsingTaskAgent > 0) increment(capabilityCounts, 'Sub-agents', sessionsUsingTaskAgent)
|
|
98
|
+
if (sessionsUsingMcp > 0) increment(capabilityCounts, 'MCP servers', sessionsUsingMcp)
|
|
99
|
+
if (sessionsUsingWebSearch > 0) increment(capabilityCounts, 'Web search', sessionsUsingWebSearch)
|
|
100
|
+
if (sessionsUsingWebFetch > 0) increment(capabilityCounts, 'Web fetch', sessionsUsingWebFetch)
|
|
101
|
+
|
|
86
102
|
const sortedThreads = [...threadSummaries].sort(
|
|
87
103
|
(a, b) => Date.parse(b.updatedAt) - Date.parse(a.updatedAt),
|
|
88
104
|
)
|
|
@@ -130,6 +146,9 @@ export function buildReport(threadSummaries, options = {}) {
|
|
|
130
146
|
models: topEntries(modelCounts, 10),
|
|
131
147
|
tools: topEntries(toolCounts, 12),
|
|
132
148
|
commandKinds: topEntries(commandKindCounts, 12),
|
|
149
|
+
capabilities: topEntries(capabilityCounts, 8),
|
|
150
|
+
outcomes: topEntries(outcomeCounts, 8),
|
|
151
|
+
sessionTypes: topEntries(sessionTypeCounts, 8),
|
|
133
152
|
toolFailures: topEntries(toolFailureCounts, 8),
|
|
134
153
|
toolErrorCategories: topEntries(toolErrorCategoryCounts, 8),
|
|
135
154
|
activeHours: buildHourSeries(activeHourCounts),
|
|
@@ -176,13 +195,13 @@ export function renderTerminalSummary(report) {
|
|
|
176
195
|
lines.push(`${report.metadata.dateRange.start} -> ${report.metadata.dateRange.end}`)
|
|
177
196
|
}
|
|
178
197
|
lines.push('')
|
|
179
|
-
lines.push(`${text.topTools}:`)
|
|
180
|
-
for (const item of report.charts.tools.slice(0, 5)) {
|
|
181
|
-
lines.push(` ${item.label}: ${item.value}`)
|
|
182
|
-
}
|
|
183
|
-
lines.push('')
|
|
184
198
|
lines.push(`${text.topProjects}:`)
|
|
185
199
|
for (const item of report.charts.projects.slice(0, 5)) {
|
|
200
|
+
lines.push(` ${formatProjectLabel(item.label)}: ${item.value}`)
|
|
201
|
+
}
|
|
202
|
+
lines.push('')
|
|
203
|
+
lines.push(`${text.modelMix}:`)
|
|
204
|
+
for (const item of report.charts.models.slice(0, 5)) {
|
|
186
205
|
lines.push(` ${item.label}: ${item.value}`)
|
|
187
206
|
}
|
|
188
207
|
return lines.join('\n')
|
|
@@ -193,9 +212,11 @@ function renderHtmlReport(report) {
|
|
|
193
212
|
const insights = report.insights
|
|
194
213
|
if (insights && !insights.__lang) insights.__lang = report.metadata.language
|
|
195
214
|
const analysisUsage = report.analysisUsage || null
|
|
196
|
-
const
|
|
197
|
-
const
|
|
198
|
-
const
|
|
215
|
+
const topProjects = renderBarList(report.charts.projects, { formatLabel: formatProjectLabel })
|
|
216
|
+
const modelMix = renderBarList(report.charts.models)
|
|
217
|
+
const sessionTypes = renderBarList(report.charts.sessionTypes)
|
|
218
|
+
const outcomes = renderBarList(report.charts.outcomes)
|
|
219
|
+
const capabilitySignals = renderBarList(report.charts.capabilities)
|
|
199
220
|
const toolFailures = renderBarList(report.charts.toolFailures)
|
|
200
221
|
const toolErrorCategories = renderBarList(report.charts.toolErrorCategories)
|
|
201
222
|
const activeHours = renderHourHistogram(report.charts.activeHours)
|
|
@@ -639,7 +660,7 @@ function renderHtmlReport(report) {
|
|
|
639
660
|
<section class="hero">
|
|
640
661
|
<span class="eyebrow">${escapeHtml(text.eyebrow)}</span>
|
|
641
662
|
<h1>${escapeHtml(text.reportTitle)}</h1>
|
|
642
|
-
<p class="meta">${escapeHtml(text.generatedLabel)} ${escapeHtml(report.metadata.generatedAt)} ${escapeHtml(text.generatedFrom)} ${report.metadata.threadCount} ${escapeHtml(text.substantiveThreads)} ${escapeHtml(text.inCodexHome)} ${escapeHtml(report.metadata.codexHome)}.</p>
|
|
663
|
+
<p class="meta">${escapeHtml(text.generatedLabel)} ${escapeHtml(report.metadata.generatedAt)} ${escapeHtml(text.generatedFrom)} ${report.metadata.threadCount} ${escapeHtml(text.substantiveThreads)} ${escapeHtml(text.inCodexHome)} ${escapeHtml(formatCodexHome(report.metadata.codexHome))}.</p>
|
|
643
664
|
<div class="summary-grid">
|
|
644
665
|
${renderStat(text.userMessages, formatNumber(report.summary.totalUserMessages))}
|
|
645
666
|
${renderStat(text.toolCalls, formatNumber(report.summary.totalToolCalls))}
|
|
@@ -672,17 +693,25 @@ function renderHtmlReport(report) {
|
|
|
672
693
|
${renderOnTheHorizon(insights)}
|
|
673
694
|
</div>
|
|
674
695
|
<aside class="side-column">
|
|
675
|
-
<section class="chart-panel">
|
|
676
|
-
<h2>${escapeHtml(text.topTools)}</h2>
|
|
677
|
-
${topTools}
|
|
678
|
-
</section>
|
|
679
696
|
<section class="chart-panel">
|
|
680
697
|
<h2>${escapeHtml(text.topProjects)}</h2>
|
|
681
698
|
${topProjects}
|
|
682
699
|
</section>
|
|
683
700
|
<section class="chart-panel">
|
|
684
|
-
<h2>${escapeHtml(text.
|
|
685
|
-
${
|
|
701
|
+
<h2>${escapeHtml(text.modelMix)}</h2>
|
|
702
|
+
${modelMix}
|
|
703
|
+
</section>
|
|
704
|
+
<section class="chart-panel">
|
|
705
|
+
<h2>${escapeHtml(text.sessionTypes)}</h2>
|
|
706
|
+
${sessionTypes}
|
|
707
|
+
</section>
|
|
708
|
+
<section class="chart-panel">
|
|
709
|
+
<h2>${escapeHtml(text.outcomes)}</h2>
|
|
710
|
+
${outcomes}
|
|
711
|
+
</section>
|
|
712
|
+
<section class="chart-panel">
|
|
713
|
+
<h2>${escapeHtml(text.capabilitySignals)}</h2>
|
|
714
|
+
${capabilitySignals}
|
|
686
715
|
</section>
|
|
687
716
|
<section class="chart-panel">
|
|
688
717
|
<h2>${escapeHtml(text.failureHotspots)}</h2>
|
|
@@ -1054,15 +1083,16 @@ function renderStat(label, value) {
|
|
|
1054
1083
|
return `<div class="stat"><div class="value">${escapeHtml(String(value))}</div><div>${escapeHtml(label)}</div></div>`
|
|
1055
1084
|
}
|
|
1056
1085
|
|
|
1057
|
-
function renderBarList(items) {
|
|
1086
|
+
function renderBarList(items, options = {}) {
|
|
1058
1087
|
if (!items.length) return '<p class="meta">No data available.</p>'
|
|
1088
|
+
const formatLabel = options.formatLabel || (value => value)
|
|
1059
1089
|
const maxValue = Math.max(...items.map(item => item.value), 1)
|
|
1060
1090
|
return `<div class="bar-list">${items
|
|
1061
1091
|
.map(
|
|
1062
1092
|
item => `
|
|
1063
1093
|
<div class="bar-row">
|
|
1064
1094
|
<div class="bar-label">
|
|
1065
|
-
<span>${escapeHtml(item.label)}</span>
|
|
1095
|
+
<span>${escapeHtml(formatLabel(item.label))}</span>
|
|
1066
1096
|
<strong>${formatNumber(item.value)}</strong>
|
|
1067
1097
|
</div>
|
|
1068
1098
|
<div class="bar-track"><div class="bar-fill" style="width:${Math.max(6, (item.value / maxValue) * 100)}%"></div></div>
|
|
@@ -1133,6 +1163,36 @@ function topEntries(map, limit) {
|
|
|
1133
1163
|
.map(([label, value]) => ({ label, value }))
|
|
1134
1164
|
}
|
|
1135
1165
|
|
|
1166
|
+
function formatCodexHome(value) {
|
|
1167
|
+
return formatDisplayPath(value, { tailSegments: 2, preferHomeAlias: true, ellipsis: false })
|
|
1168
|
+
}
|
|
1169
|
+
|
|
1170
|
+
function formatProjectLabel(value) {
|
|
1171
|
+
return formatDisplayPath(value, { tailSegments: 2, preferHomeAlias: false, ellipsis: true })
|
|
1172
|
+
}
|
|
1173
|
+
|
|
1174
|
+
function formatDisplayPath(value, options = {}) {
|
|
1175
|
+
const text = String(value || '').trim()
|
|
1176
|
+
if (!text) return '(unknown)'
|
|
1177
|
+
|
|
1178
|
+
const normalized = text.replace(/\\/g, '/')
|
|
1179
|
+
const home = os.homedir().replace(/\\/g, '/')
|
|
1180
|
+
if (options.preferHomeAlias !== false && normalized === home) return '~'
|
|
1181
|
+
if (options.preferHomeAlias !== false && normalized.startsWith(`${home}/`)) {
|
|
1182
|
+
return `~/${normalized.slice(home.length + 1)}`
|
|
1183
|
+
}
|
|
1184
|
+
|
|
1185
|
+
const parts = normalized.split('/').filter(Boolean)
|
|
1186
|
+
const tailSegments = Math.max(1, Number(options.tailSegments || 2))
|
|
1187
|
+
if (parts.length <= tailSegments) {
|
|
1188
|
+
return normalized.startsWith('/') ? `/${parts.join('/')}` : parts.join('/')
|
|
1189
|
+
}
|
|
1190
|
+
|
|
1191
|
+
const tail = parts.slice(-tailSegments).join('/')
|
|
1192
|
+
if (options.ellipsis === false) return tail
|
|
1193
|
+
return `…/${tail}`
|
|
1194
|
+
}
|
|
1195
|
+
|
|
1136
1196
|
function buildHourSeries(hourMap) {
|
|
1137
1197
|
return Array.from({ length: 24 }, (_, hour) => ({
|
|
1138
1198
|
hour,
|
|
@@ -1244,9 +1304,11 @@ function getReportText(lang) {
|
|
|
1244
1304
|
filesModified: '修改文件',
|
|
1245
1305
|
toolErrors: '工具错误',
|
|
1246
1306
|
avgResponse: '平均响应',
|
|
1247
|
-
topTools: 'Top Tools',
|
|
1248
1307
|
topProjects: 'Top Projects',
|
|
1249
|
-
|
|
1308
|
+
modelMix: '模型分布',
|
|
1309
|
+
sessionTypes: '会话类型',
|
|
1310
|
+
outcomes: '结果分布',
|
|
1311
|
+
capabilitySignals: '能力信号',
|
|
1250
1312
|
failureHotspots: '失败热点',
|
|
1251
1313
|
errorCategories: '错误分类',
|
|
1252
1314
|
timeOfDay: '活跃时段',
|
|
@@ -1321,9 +1383,11 @@ function getReportText(lang) {
|
|
|
1321
1383
|
filesModified: 'Files Modified',
|
|
1322
1384
|
toolErrors: 'Tool Errors',
|
|
1323
1385
|
avgResponse: 'Avg Response',
|
|
1324
|
-
topTools: 'Top Tools',
|
|
1325
1386
|
topProjects: 'Top Projects',
|
|
1326
|
-
|
|
1387
|
+
modelMix: 'Model Mix',
|
|
1388
|
+
sessionTypes: 'Session Types',
|
|
1389
|
+
outcomes: 'Outcomes',
|
|
1390
|
+
capabilitySignals: 'Capability Signals',
|
|
1327
1391
|
failureHotspots: 'Failure Hotspots',
|
|
1328
1392
|
errorCategories: 'Error Categories',
|
|
1329
1393
|
timeOfDay: 'Time of Day',
|