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 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`: `50`
112
- - `facet-limit`: `20`
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: 50,
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: 20,
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> Max threads to scan from the thread registry (default: 50)
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 substantive threads to analyze (default: 20)
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 === 50 && options.facetLimit === 20) return 'standard'
661
- if (options.limit === 200 && options.facetLimit === 50) return 'deep'
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: 200, facetLimit: 50 }
686
- return { ...options, limit: 50, facetLimit: 20 }
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 threads / 8 facets)',
837
- depthStandard: '标准(50 threads / 20 facets)',
838
- depthDeep: '深度(200 threads / 50 facets)',
836
+ depthConservative: '保守(20 个有效线程 / 8 facets)',
837
+ depthStandard: '标准(200 个有效线程 / 50 facets)',
838
+ depthDeep: '深度(400 个有效线程 / 50 facets)',
839
839
  depthCustom: '自定义',
840
- limitQuestion: '输入最大 thread 数',
841
- facetLimitQuestion: '输入最大 facet 提取数',
840
+ limitQuestion: '输入目标有效线程数',
841
+ facetLimitQuestion: '输入最大新增 facet ',
842
842
  languageQuestion: '选择报告语言:',
843
843
  outputDirQuestion: '输出目录',
844
844
  openBrowserQuestion: '生成后自动打开浏览器?',
845
845
  qualityQuestion: '选择分析质量预设:',
846
- qualityCheaper: '更省(尽量用 spark)',
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 (50 threads / 20 facets)',
896
- depthDeep: 'Deep (200 threads / 50 facets)',
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 max thread count',
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 spark)',
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: 'substantive threads',
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(thread => thread.userMessages >= 2)
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
- for (const thread of threads) {
449
- const summary = await loadCachedOrFreshSummary(thread, cacheDir)
450
- summaries.push(summary)
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
  }
@@ -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 project areas.
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 about what was worked on and how Codex was used."}
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-5 areas. Skip Codex self-hosting/meta work unless it is a dominant project area.
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
- - Each description should sound like a mini report paragraph: what kinds of work clustered together, then how Codex contributed
296
- - Prefer descriptions that mention representative tasks or artifacts instead of vague labels`,
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, interrupts, redirects, or trusts execution
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 topTools = renderBarList(report.charts.tools)
197
- const topProjects = renderBarList(report.charts.projects)
198
- const commandKinds = renderBarList(report.charts.commandKinds)
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.commandKinds)}</h2>
685
- ${commandKinds}
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
- commandKinds: '命令类型',
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
- commandKinds: 'Command Kinds',
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',
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "codex-session-insights",
3
- "version": "0.1.0",
3
+ "version": "0.2.1",
4
4
  "description": "Generate a report analyzing your Codex sessions.",
5
5
  "type": "module",
6
6
  "bin": {