sidekick-shared 0.27.6 → 0.28.0

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.
Files changed (269) hide show
  1. package/CHANGELOG.md +28 -0
  2. package/README.md +58 -18
  3. package/dist/browser.d.ts +13 -2
  4. package/dist/browser.js +30 -2
  5. package/dist/claudePaths.d.ts +11 -0
  6. package/dist/claudePaths.js +61 -0
  7. package/dist/claudeProfiles.d.ts +2 -9
  8. package/dist/claudeProfiles.js +10 -25
  9. package/dist/codexPaths.d.ts +22 -0
  10. package/dist/codexPaths.js +67 -0
  11. package/dist/codexProfiles.d.ts +1 -4
  12. package/dist/codexProfiles.js +5 -43
  13. package/dist/doctor.js +2 -2
  14. package/dist/esm/accountChangeSignal.js +15 -0
  15. package/dist/esm/accountChanges.js +152 -0
  16. package/dist/esm/accountHealth.js +288 -0
  17. package/dist/esm/accountLaunch.js +145 -0
  18. package/dist/esm/accountLease.js +60 -0
  19. package/dist/esm/accountManager.js +606 -0
  20. package/dist/esm/accountRegistry.js +214 -0
  21. package/dist/esm/accountStatus.js +51 -0
  22. package/dist/esm/accountSwitch.js +74 -0
  23. package/dist/esm/accountSync.js +156 -0
  24. package/dist/esm/accountSyncTypes.js +4 -0
  25. package/dist/esm/accounts.js +1291 -0
  26. package/dist/esm/aggregation/EventAggregator.js +2034 -0
  27. package/dist/esm/aggregation/FrequencyTracker.js +69 -0
  28. package/dist/esm/aggregation/HeatmapTracker.js +89 -0
  29. package/dist/esm/aggregation/PatternExtractor.js +168 -0
  30. package/dist/esm/aggregation/costProvenance.js +31 -0
  31. package/dist/esm/aggregation/snapshot.js +161 -0
  32. package/dist/esm/aggregation/types.js +5 -0
  33. package/dist/esm/analytics/codeImpact.js +18 -0
  34. package/dist/esm/analytics/compactionLedger.js +37 -0
  35. package/dist/esm/analytics/qualityScore.js +112 -0
  36. package/dist/esm/autoSwitch.js +185 -0
  37. package/dist/esm/browser.js +34 -0
  38. package/dist/esm/claudeCredentialDoctor.js +239 -0
  39. package/dist/esm/claudeCredentialLedger.js +91 -0
  40. package/dist/esm/claudeCredentials.js +111 -0
  41. package/dist/esm/claudeLiveGuard.js +43 -0
  42. package/dist/esm/claudePaths.js +22 -0
  43. package/dist/esm/claudeProfiles.js +142 -0
  44. package/dist/esm/claudeRefreshLock.js +106 -0
  45. package/dist/esm/codexAuth.js +101 -0
  46. package/dist/esm/codexCredentialDoctor.js +265 -0
  47. package/dist/esm/codexCredentialStore.js +164 -0
  48. package/dist/esm/codexKeepAlive.js +112 -0
  49. package/dist/esm/codexPaths.js +172 -0
  50. package/dist/esm/codexProfiles.js +1251 -0
  51. package/dist/esm/codexQuota.js +564 -0
  52. package/dist/esm/codexQuotaWatcher.js +363 -0
  53. package/dist/esm/context/composer.js +71 -0
  54. package/dist/esm/context/sessionContext.js +546 -0
  55. package/dist/esm/credentialIO.js +283 -0
  56. package/dist/esm/credentials.js +49 -0
  57. package/dist/esm/doctor.js +305 -0
  58. package/dist/esm/ensureDefaultAccounts.js +62 -0
  59. package/dist/esm/errorHistory.js +56 -0
  60. package/dist/esm/extractors/errorTaxonomy.js +43 -0
  61. package/dist/esm/extractors/gatherAssets.js +32 -0
  62. package/dist/esm/extractors/sessionAssets.js +176 -0
  63. package/dist/esm/extractors/sources/claudeAssets.js +158 -0
  64. package/dist/esm/extractors/sources/codexAssets.js +234 -0
  65. package/dist/esm/extractors/tokenUsage.js +41 -0
  66. package/dist/esm/extractors/toolCall.js +142 -0
  67. package/dist/esm/failingTools.js +65 -0
  68. package/dist/esm/fileHead.js +27 -0
  69. package/dist/esm/formatters/eventHighlighter.js +332 -0
  70. package/dist/esm/formatters/noiseClassifier.js +209 -0
  71. package/dist/esm/formatters/sessionDump.js +328 -0
  72. package/dist/esm/formatters/toolSummary.js +226 -0
  73. package/dist/esm/formatting.js +82 -0
  74. package/dist/esm/handoffUrl.js +23 -0
  75. package/dist/esm/historicalStore.js +305 -0
  76. package/dist/esm/humanPrompt.js +88 -0
  77. package/dist/esm/index.js +213 -0
  78. package/dist/esm/keepAlive.js +189 -0
  79. package/dist/esm/liveCredentialGuard.js +33 -0
  80. package/dist/esm/modelAliases.js +51 -0
  81. package/dist/esm/modelCatalog.js +139 -0
  82. package/dist/esm/modelContext.js +280 -0
  83. package/dist/esm/modelInfo.js +832 -0
  84. package/dist/esm/multiProviderQuotaService.js +450 -0
  85. package/dist/esm/node.js +46 -0
  86. package/dist/esm/observedContextWindows.js +165 -0
  87. package/dist/esm/observedSessionCollector.js +939 -0
  88. package/dist/esm/parsers/changelogParser.js +71 -0
  89. package/dist/esm/parsers/codexHistory.js +89 -0
  90. package/dist/esm/parsers/codexParser.js +906 -0
  91. package/dist/esm/parsers/debugLogParser.js +126 -0
  92. package/dist/esm/parsers/jsonl.js +66 -0
  93. package/dist/esm/parsers/mcpToolName.js +28 -0
  94. package/dist/esm/parsers/openCodeParser.js +625 -0
  95. package/dist/esm/parsers/planExtractor.js +335 -0
  96. package/dist/esm/parsers/sessionActivityDetector.js +246 -0
  97. package/dist/esm/parsers/sessionPathResolver.js +713 -0
  98. package/dist/esm/parsers/subagentScanner.js +360 -0
  99. package/dist/esm/parsers/subagentStatsCache.js +45 -0
  100. package/dist/esm/parsers/subagentTraceParser.js +325 -0
  101. package/dist/esm/paths.js +134 -0
  102. package/dist/esm/peakHours.js +194 -0
  103. package/dist/esm/phrases.js +1330 -0
  104. package/dist/esm/pricingCatalog.js +247 -0
  105. package/dist/esm/processDetection.js +200 -0
  106. package/dist/esm/projectMigration.js +59 -0
  107. package/dist/esm/promptHistory.js +1221 -0
  108. package/dist/esm/providerFailure.js +362 -0
  109. package/dist/esm/providerIds.js +6 -0
  110. package/dist/esm/providerQuota.js +1 -0
  111. package/dist/esm/providerServiceStatus.js +145 -0
  112. package/dist/esm/providerServiceStatusTypes.js +1 -0
  113. package/dist/esm/providerStatus.js +117 -0
  114. package/dist/esm/providers/claudeCode.js +575 -0
  115. package/dist/esm/providers/claudeListing.js +121 -0
  116. package/dist/esm/providers/codex.js +1034 -0
  117. package/dist/esm/providers/codexDatabase.js +297 -0
  118. package/dist/esm/providers/codexRollouts.js +215 -0
  119. package/dist/esm/providers/detect.js +185 -0
  120. package/dist/esm/providers/diagnostics.js +83 -0
  121. package/dist/esm/providers/directoryListingCache.js +206 -0
  122. package/dist/esm/providers/factory.js +54 -0
  123. package/dist/esm/providers/openCode.js +2008 -0
  124. package/dist/esm/providers/openCodeDatabase.js +419 -0
  125. package/dist/esm/providers/rolloutWalker.js +252 -0
  126. package/dist/esm/providers/types.js +12 -0
  127. package/dist/esm/quota.js +182 -0
  128. package/dist/esm/quotaHistory.js +364 -0
  129. package/dist/esm/quotaPoller.js +230 -0
  130. package/dist/esm/quotaPresentation.js +97 -0
  131. package/dist/esm/quotaResolver.js +231 -0
  132. package/dist/esm/quotaSnapshots.js +130 -0
  133. package/dist/esm/quotaThresholds.js +85 -0
  134. package/dist/esm/readers/decisions.js +24 -0
  135. package/dist/esm/readers/handoff.js +20 -0
  136. package/dist/esm/readers/helpers.js +27 -0
  137. package/dist/esm/readers/history.js +9 -0
  138. package/dist/esm/readers/notes.js +48 -0
  139. package/dist/esm/readers/plans.js +225 -0
  140. package/dist/esm/readers/tasks.js +19 -0
  141. package/dist/esm/report/htmlHelpers.js +280 -0
  142. package/dist/esm/report/htmlReportGenerator.js +704 -0
  143. package/dist/esm/report/index.js +7 -0
  144. package/dist/esm/report/logo.js +2 -0
  145. package/dist/esm/report/openBrowser.js +19 -0
  146. package/dist/esm/report/sessionReportInputs.js +33 -0
  147. package/dist/esm/report/transcriptParser.js +83 -0
  148. package/dist/esm/report/types.js +4 -0
  149. package/dist/esm/schemas/accountManager.js +204 -0
  150. package/dist/esm/schemas/accountStatus.js +25 -0
  151. package/dist/esm/schemas/assistantTurn.js +74 -0
  152. package/dist/esm/schemas/index.js +25 -0
  153. package/dist/esm/schemas/observedSessionV1.js +77 -0
  154. package/dist/esm/schemas/providerFailure.js +37 -0
  155. package/dist/esm/schemas/quota.js +132 -0
  156. package/dist/esm/schemas/quotaHistory.js +40 -0
  157. package/dist/esm/schemas/sessionEvent.js +216 -0
  158. package/dist/esm/schemas/stateFile.js +75 -0
  159. package/dist/esm/schemas/tokenEstimation.js +7 -0
  160. package/dist/esm/schemas/transcript.js +86 -0
  161. package/dist/esm/schemas/usageNormalization.js +38 -0
  162. package/dist/esm/search/advancedFilter.js +197 -0
  163. package/dist/esm/search/sessionSearch.js +66 -0
  164. package/dist/esm/sessionFingerprint.js +40 -0
  165. package/dist/esm/sessionMonitor.js +198 -0
  166. package/dist/esm/sessionPreviews.js +522 -0
  167. package/dist/esm/sessionPromptHistory.js +254 -0
  168. package/dist/esm/sessionPromptSignals.js +364 -0
  169. package/dist/esm/sessionStats.js +160 -0
  170. package/dist/esm/sessionTokenTotals.js +98 -0
  171. package/dist/esm/sessionTranscripts.js +49 -0
  172. package/dist/esm/stateFile.js +119 -0
  173. package/dist/esm/statusline/BurnRateCalculator.js +40 -0
  174. package/dist/esm/statusline/claudeStatuslinePayload.js +148 -0
  175. package/dist/esm/statusline/formatter.js +122 -0
  176. package/dist/esm/statusline/index.js +8 -0
  177. package/dist/esm/terminalSync.js +95 -0
  178. package/dist/esm/testing/credentialGuard.js +197 -0
  179. package/dist/esm/testing/index.js +12 -0
  180. package/dist/esm/testing/sessionLogFixtures.js +128 -0
  181. package/dist/esm/tokenEstimation.js +67 -0
  182. package/dist/esm/tokenSummary.js +113 -0
  183. package/dist/esm/toolCategory.js +82 -0
  184. package/dist/esm/transcript.js +257 -0
  185. package/dist/esm/turns/assistantTurn.js +373 -0
  186. package/dist/esm/types/codex.js +23 -0
  187. package/dist/esm/types/decisionLog.js +5 -0
  188. package/dist/esm/types/historicalData.js +47 -0
  189. package/dist/esm/types/knowledgeNote.js +15 -0
  190. package/dist/esm/types/observedSessionV1.js +257 -0
  191. package/dist/esm/types/opencode.js +12 -0
  192. package/dist/esm/types/plan.js +7 -0
  193. package/dist/esm/types/sessionEvent.js +10 -0
  194. package/dist/esm/types/taskPersistence.js +12 -0
  195. package/dist/esm/usage/billingBlocks.js +148 -0
  196. package/dist/esm/usage/claudeUsageDedupe.js +189 -0
  197. package/dist/esm/usage/importSessionHistory.js +84 -0
  198. package/dist/esm/usage/subagentUsage.js +18 -0
  199. package/dist/esm/usage/usageEvents.js +402 -0
  200. package/dist/esm/usage/usageReports.js +202 -0
  201. package/dist/esm/usageNormalization.js +95 -0
  202. package/dist/esm/watchers/eventBridge.js +219 -0
  203. package/dist/esm/watchers/factory.js +63 -0
  204. package/dist/esm/watchers/jsonlTail.js +289 -0
  205. package/dist/esm/watchers/jsonlWatcher.js +435 -0
  206. package/dist/esm/watchers/providerReaderWatcher.js +118 -0
  207. package/dist/esm/watchers/sqliteWatcher.js +285 -0
  208. package/dist/esm/watchers/types.js +4 -0
  209. package/dist/esm/workspaceRoots.js +56 -0
  210. package/dist/esm/writers/atomic.js +375 -0
  211. package/dist/esm/writers/decisions.js +51 -0
  212. package/dist/esm/writers/notes.js +46 -0
  213. package/dist/esm/writers/tasks.js +63 -0
  214. package/dist/esm/zaiQuota.js +323 -0
  215. package/dist/esm/zaiQuotaApi.js +271 -0
  216. package/dist/esm/zaiQuotaWatcher.js +265 -0
  217. package/dist/fileHead.d.ts +22 -0
  218. package/dist/fileHead.js +33 -0
  219. package/dist/index.d.ts +16 -7
  220. package/dist/index.js +49 -15
  221. package/dist/node.d.ts +13 -1
  222. package/dist/node.js +43 -2
  223. package/dist/parsers/codexHistory.js +2 -2
  224. package/dist/parsers/codexParser.d.ts +12 -0
  225. package/dist/parsers/codexParser.js +34 -12
  226. package/dist/parsers/debugLogParser.js +2 -2
  227. package/dist/parsers/sessionPathResolver.js +5 -4
  228. package/dist/promptHistory.d.ts +32 -0
  229. package/dist/promptHistory.js +115 -59
  230. package/dist/providerFailure.d.ts +12 -6
  231. package/dist/providerFailure.js +45 -0
  232. package/dist/providers/claudeCode.js +7 -99
  233. package/dist/providers/claudeListing.d.ts +22 -0
  234. package/dist/providers/claudeListing.js +158 -0
  235. package/dist/providers/codex.d.ts +3 -1
  236. package/dist/providers/codex.js +26 -227
  237. package/dist/providers/codexRollouts.d.ts +44 -0
  238. package/dist/providers/codexRollouts.js +261 -0
  239. package/dist/providers/detect.js +4 -4
  240. package/dist/readers/plans.js +3 -3
  241. package/dist/schemas/index.d.ts +2 -0
  242. package/dist/schemas/index.js +7 -2
  243. package/dist/schemas/providerFailure.d.ts +154 -0
  244. package/dist/schemas/providerFailure.js +40 -0
  245. package/dist/sessionPromptHistory.d.ts +12 -7
  246. package/dist/sessionPromptHistory.js +15 -3
  247. package/dist/sessionPromptSignals.d.ts +9 -0
  248. package/dist/sessionPromptSignals.js +13 -0
  249. package/dist/testing/credentialGuard.d.ts +23 -0
  250. package/dist/testing/credentialGuard.js +233 -0
  251. package/dist/testing/index.d.ts +14 -0
  252. package/dist/testing/index.js +22 -0
  253. package/dist/testing/sessionLogFixtures.d.ts +28 -0
  254. package/dist/testing/sessionLogFixtures.js +169 -0
  255. package/dist/tokenEstimation.d.ts +2 -0
  256. package/dist/tokenEstimation.js +21 -11
  257. package/dist/toolCategory.d.ts +18 -0
  258. package/dist/toolCategory.js +86 -0
  259. package/dist/types/codex.d.ts +4 -0
  260. package/dist/usage/usageEvents.d.ts +16 -0
  261. package/dist/usage/usageEvents.js +53 -9
  262. package/dist/watchers/jsonlTail.d.ts +26 -1
  263. package/dist/watchers/jsonlTail.js +136 -28
  264. package/dist/watchers/jsonlWatcher.js +7 -0
  265. package/dist/watchers/providerReaderWatcher.js +1 -0
  266. package/dist/watchers/types.d.ts +6 -0
  267. package/dist/workspaceRoots.d.ts +23 -0
  268. package/dist/workspaceRoots.js +93 -0
  269. package/package.json +22 -2
@@ -0,0 +1,704 @@
1
+ /**
2
+ * Generate a self-contained HTML session report from AggregatedMetrics + TranscriptEntry[].
3
+ *
4
+ * All CSS and JS are inlined — the output HTML file has zero external dependencies.
5
+ */
6
+ import { describeCostProvenance } from '../aggregation/costProvenance';
7
+ import { summarizeTokens, TOKEN_MAIN_THREAD_LABEL, TOKEN_SESSION_TOTAL_LABEL, TOKEN_SUBAGENTS_LABEL, TOKEN_TOTAL_LABEL, } from '../tokenSummary';
8
+ import { combineSessionTokenTotals } from '../sessionTokenTotals';
9
+ import { escapeHtml, simpleMarkdownToHtml, highlightCodeBlock, fmtTokens, fmtCost, formatTimestamp, formatDuration, } from './htmlHelpers';
10
+ import { SIDEKICK_LOGO_BASE64 } from './logo';
11
+ /**
12
+ * Generate a complete, self-contained HTML report.
13
+ */
14
+ export function generateHtmlReport(metrics, transcript, options = {}) {
15
+ const { sessionFileName, includeThinking = true, includeToolDetail = true, theme = 'dark', } = options;
16
+ const duration = formatDuration(metrics.sessionStartTime, metrics.lastEventTime);
17
+ const tokens = combineSessionTokenTotals(metrics.tokens, options.subagents ?? []);
18
+ return `<!DOCTYPE html>
19
+ <html lang="en" data-theme="${theme}">
20
+ <head>
21
+ <meta charset="UTF-8">
22
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
23
+ <title>Sidekick Session Report${sessionFileName ? ` — ${escapeHtml(sessionFileName)}` : ''}</title>
24
+ ${generateStyles()}
25
+ </head>
26
+ <body>
27
+ ${generateHeader(metrics, sessionFileName, duration)}
28
+ ${generateStatsCards(metrics, tokens, duration)}
29
+ ${generateModelBreakdown(metrics)}
30
+ ${generateToolBreakdown(metrics)}
31
+ ${generateTranscriptControls()}
32
+ ${generateTranscript(transcript, includeThinking, includeToolDetail)}
33
+ ${generateBackToTop()}
34
+ ${generateScript()}
35
+ </body>
36
+ </html>`;
37
+ }
38
+ function generateStyles() {
39
+ return `<style>
40
+ :root {
41
+ --bg-primary: #0f0a1a;
42
+ --bg-secondary: #1a1230;
43
+ --bg-card: #221a3a;
44
+ --bg-code: #151020;
45
+ --text-primary: #e8e0f0;
46
+ --text-secondary: #a89bc0;
47
+ --text-muted: #6b5f80;
48
+ --accent-purple: #9b6dff;
49
+ --accent-purple-dim: #6b4db0;
50
+ --accent-blue: #5b9cf5;
51
+ --accent-orange: #f5a623;
52
+ --accent-green: #4ade80;
53
+ --accent-red: #f87171;
54
+ --border-color: #2a2045;
55
+ --border-subtle: #1e1535;
56
+ --user-border: #9b6dff;
57
+ --assistant-border: #5b9cf5;
58
+ --system-border: #6b5f80;
59
+ --tool-border: #f5a623;
60
+ --thinking-border: #4b3d65;
61
+ --radius: 8px;
62
+ --shadow: 0 2px 8px rgba(0,0,0,0.3);
63
+ }
64
+
65
+ * { margin: 0; padding: 0; box-sizing: border-box; }
66
+
67
+ body {
68
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
69
+ background: var(--bg-primary);
70
+ color: var(--text-primary);
71
+ line-height: 1.6;
72
+ min-height: 100vh;
73
+ }
74
+
75
+ .container { max-width: 1000px; margin: 0 auto; padding: 20px; }
76
+
77
+ /* Header */
78
+ .header {
79
+ background: linear-gradient(135deg, var(--bg-secondary), var(--bg-card));
80
+ border-bottom: 2px solid var(--accent-purple-dim);
81
+ padding: 24px 0;
82
+ margin-bottom: 24px;
83
+ }
84
+ .header-inner {
85
+ max-width: 1000px;
86
+ margin: 0 auto;
87
+ padding: 0 20px;
88
+ display: flex;
89
+ align-items: center;
90
+ gap: 16px;
91
+ flex-wrap: wrap;
92
+ }
93
+ .logo { display: flex; align-items: center; gap: 10px; }
94
+ .logo svg { width: 32px; height: 32px; }
95
+ .logo-text {
96
+ font-size: 22px;
97
+ font-weight: 700;
98
+ background: linear-gradient(135deg, var(--accent-purple), var(--accent-blue));
99
+ -webkit-background-clip: text;
100
+ -webkit-text-fill-color: transparent;
101
+ background-clip: text;
102
+ }
103
+ .header-meta {
104
+ margin-left: auto;
105
+ text-align: right;
106
+ color: var(--text-secondary);
107
+ font-size: 13px;
108
+ }
109
+ .header-meta .session-file { font-family: monospace; color: var(--text-muted); }
110
+
111
+ /* Stats Cards */
112
+ .stats-grid {
113
+ display: grid;
114
+ grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
115
+ gap: 12px;
116
+ margin-bottom: 24px;
117
+ }
118
+ .stat-card {
119
+ background: var(--bg-card);
120
+ border: 1px solid var(--border-color);
121
+ border-radius: var(--radius);
122
+ padding: 16px;
123
+ box-shadow: var(--shadow);
124
+ }
125
+ .stat-card .label { color: var(--text-muted); font-size: 12px; text-transform: uppercase; letter-spacing: 0.5px; }
126
+ .stat-card .value { font-size: 24px; font-weight: 700; color: var(--accent-purple); margin-top: 4px; }
127
+ .stat-card .sub { color: var(--text-secondary); font-size: 12px; margin-top: 2px; }
128
+
129
+ /* Tables */
130
+ .section { margin-bottom: 24px; }
131
+ .section-title {
132
+ font-size: 16px;
133
+ font-weight: 600;
134
+ color: var(--text-primary);
135
+ margin-bottom: 12px;
136
+ padding-bottom: 8px;
137
+ border-bottom: 1px solid var(--border-color);
138
+ }
139
+ table {
140
+ width: 100%;
141
+ border-collapse: collapse;
142
+ background: var(--bg-card);
143
+ border-radius: var(--radius);
144
+ overflow: hidden;
145
+ box-shadow: var(--shadow);
146
+ }
147
+ th {
148
+ background: var(--bg-secondary);
149
+ color: var(--text-secondary);
150
+ font-size: 12px;
151
+ text-transform: uppercase;
152
+ letter-spacing: 0.5px;
153
+ padding: 10px 14px;
154
+ text-align: left;
155
+ font-weight: 600;
156
+ }
157
+ td { padding: 10px 14px; border-top: 1px solid var(--border-subtle); color: var(--text-primary); font-size: 14px; }
158
+ tr:hover td { background: rgba(155,109,255,0.05); }
159
+
160
+ /* Transcript */
161
+ .transcript-controls {
162
+ display: flex;
163
+ gap: 12px;
164
+ align-items: center;
165
+ flex-wrap: wrap;
166
+ margin-bottom: 16px;
167
+ padding: 12px;
168
+ background: var(--bg-card);
169
+ border-radius: var(--radius);
170
+ border: 1px solid var(--border-color);
171
+ }
172
+ .transcript-controls label { color: var(--text-secondary); font-size: 13px; cursor: pointer; display: flex; align-items: center; gap: 4px; }
173
+ .transcript-controls input[type="checkbox"] { accent-color: var(--accent-purple); }
174
+ .transcript-controls button {
175
+ background: var(--accent-purple-dim);
176
+ color: white;
177
+ border: none;
178
+ padding: 6px 12px;
179
+ border-radius: 4px;
180
+ cursor: pointer;
181
+ font-size: 12px;
182
+ }
183
+ .transcript-controls button:hover { background: var(--accent-purple); }
184
+
185
+ .message {
186
+ margin-bottom: 12px;
187
+ border-left: 3px solid var(--border-color);
188
+ border-radius: 0 var(--radius) var(--radius) 0;
189
+ background: var(--bg-card);
190
+ box-shadow: var(--shadow);
191
+ overflow: hidden;
192
+ }
193
+ .message.user { border-left-color: var(--user-border); }
194
+ .message.assistant { border-left-color: var(--assistant-border); }
195
+ .message.system { border-left-color: var(--system-border); }
196
+ .message.summary { border-left-color: var(--system-border); }
197
+
198
+ .message-header {
199
+ display: flex;
200
+ align-items: center;
201
+ gap: 8px;
202
+ padding: 8px 14px;
203
+ background: rgba(0,0,0,0.15);
204
+ font-size: 12px;
205
+ }
206
+ .role-badge {
207
+ font-weight: 700;
208
+ text-transform: uppercase;
209
+ letter-spacing: 0.5px;
210
+ font-size: 11px;
211
+ padding: 2px 8px;
212
+ border-radius: 3px;
213
+ }
214
+ .role-badge.user { background: rgba(155,109,255,0.2); color: var(--accent-purple); }
215
+ .role-badge.assistant { background: rgba(91,156,245,0.2); color: var(--accent-blue); }
216
+ .role-badge.system { background: rgba(107,95,128,0.2); color: var(--text-muted); }
217
+ .role-badge.summary { background: rgba(107,95,128,0.2); color: var(--text-muted); }
218
+ .message-ts { color: var(--text-muted); margin-left: auto; }
219
+ .message-model { color: var(--text-secondary); font-family: monospace; font-size: 11px; }
220
+ .message-source { color: var(--text-muted); font-family: monospace; font-size: 11px; font-style: italic; }
221
+ .message-tokens { color: var(--text-muted); font-size: 11px; }
222
+
223
+ .message-body { padding: 12px 14px; }
224
+ .message-body p { margin-bottom: 8px; }
225
+ .message-body p:last-child { margin-bottom: 0; }
226
+ .message-body h1, .message-body h2, .message-body h3, .message-body h4 {
227
+ margin: 12px 0 6px 0;
228
+ color: var(--text-primary);
229
+ }
230
+ .message-body ul { margin: 6px 0; padding-left: 20px; }
231
+ .message-body li { margin: 2px 0; }
232
+ .message-body blockquote {
233
+ border-left: 3px solid var(--accent-purple-dim);
234
+ padding-left: 12px;
235
+ color: var(--text-secondary);
236
+ margin: 8px 0;
237
+ }
238
+ .message-body a { color: var(--accent-blue); text-decoration: none; }
239
+ .message-body a:hover { text-decoration: underline; }
240
+ .message-body hr { border: none; border-top: 1px solid var(--border-color); margin: 12px 0; }
241
+
242
+ /* Tool blocks */
243
+ .tool-block {
244
+ margin: 8px 0;
245
+ border: 1px solid var(--tool-border);
246
+ border-radius: var(--radius);
247
+ overflow: hidden;
248
+ }
249
+ .tool-block summary {
250
+ padding: 8px 12px;
251
+ background: rgba(245,166,35,0.08);
252
+ color: var(--accent-orange);
253
+ cursor: pointer;
254
+ font-size: 13px;
255
+ font-weight: 600;
256
+ font-family: monospace;
257
+ display: flex;
258
+ align-items: center;
259
+ gap: 6px;
260
+ }
261
+ .tool-block summary::marker { color: var(--accent-orange); }
262
+ .tool-block .tool-content {
263
+ padding: 10px 12px;
264
+ background: var(--bg-code);
265
+ font-size: 13px;
266
+ max-height: 500px;
267
+ overflow: auto;
268
+ }
269
+ .tool-label { font-size: 10px; text-transform: uppercase; color: var(--text-muted); margin-bottom: 4px; letter-spacing: 0.5px; }
270
+ .tool-io { margin-bottom: 8px; }
271
+ .tool-io:last-child { margin-bottom: 0; }
272
+ .tool-io pre {
273
+ background: var(--bg-primary);
274
+ padding: 8px;
275
+ border-radius: 4px;
276
+ overflow-x: auto;
277
+ font-size: 12px;
278
+ line-height: 1.5;
279
+ white-space: pre-wrap;
280
+ word-break: break-word;
281
+ }
282
+ .tool-error { color: var(--accent-red); }
283
+ .tool-result-block {
284
+ margin: 8px 0;
285
+ border: 1px solid var(--border-color);
286
+ border-radius: var(--radius);
287
+ overflow: hidden;
288
+ }
289
+ .tool-result-block summary {
290
+ padding: 8px 12px;
291
+ background: rgba(91,156,245,0.05);
292
+ color: var(--text-secondary);
293
+ cursor: pointer;
294
+ font-size: 13px;
295
+ font-family: monospace;
296
+ }
297
+ .tool-result-block .tool-content {
298
+ padding: 10px 12px;
299
+ background: var(--bg-code);
300
+ font-size: 13px;
301
+ max-height: 500px;
302
+ overflow: auto;
303
+ }
304
+
305
+ /* Thinking blocks */
306
+ .thinking-block {
307
+ margin: 8px 0;
308
+ border: 1px solid var(--thinking-border);
309
+ border-radius: var(--radius);
310
+ opacity: 0.75;
311
+ }
312
+ .thinking-block summary {
313
+ padding: 8px 12px;
314
+ color: var(--text-muted);
315
+ cursor: pointer;
316
+ font-size: 12px;
317
+ font-style: italic;
318
+ }
319
+ .thinking-block .thinking-content {
320
+ padding: 10px 12px;
321
+ background: rgba(0,0,0,0.1);
322
+ color: var(--text-secondary);
323
+ font-size: 13px;
324
+ max-height: 400px;
325
+ overflow: auto;
326
+ white-space: pre-wrap;
327
+ word-break: break-word;
328
+ }
329
+
330
+ /* Code */
331
+ .code-block-wrapper { position: relative; margin: 8px 0; }
332
+ .code-block {
333
+ background: var(--bg-primary);
334
+ border: 1px solid var(--border-color);
335
+ border-radius: 4px;
336
+ padding: 12px;
337
+ overflow-x: auto;
338
+ font-size: 13px;
339
+ line-height: 1.5;
340
+ }
341
+ .code-block code { font-family: 'SF Mono', 'Fira Code', 'JetBrains Mono', monospace; }
342
+ .copy-btn {
343
+ position: absolute;
344
+ top: 6px;
345
+ right: 6px;
346
+ background: var(--bg-card);
347
+ color: var(--text-muted);
348
+ border: 1px solid var(--border-color);
349
+ padding: 3px 8px;
350
+ border-radius: 3px;
351
+ cursor: pointer;
352
+ font-size: 11px;
353
+ opacity: 0;
354
+ transition: opacity 0.15s;
355
+ }
356
+ .code-block-wrapper:hover .copy-btn { opacity: 1; }
357
+ .copy-btn:hover { color: var(--text-primary); background: var(--bg-secondary); }
358
+ .inline-code {
359
+ background: var(--bg-code);
360
+ padding: 1px 5px;
361
+ border-radius: 3px;
362
+ font-family: monospace;
363
+ font-size: 0.9em;
364
+ color: var(--accent-purple);
365
+ }
366
+
367
+ /* Syntax highlighting */
368
+ .hl-keyword { color: #c678dd; }
369
+ .hl-string { color: #98c379; }
370
+ .hl-number { color: #d19a66; }
371
+ .hl-comment { color: #5c6370; font-style: italic; }
372
+ .hl-key { color: #61afef; }
373
+
374
+ /* Back to top */
375
+ .back-to-top {
376
+ position: fixed;
377
+ bottom: 24px;
378
+ right: 24px;
379
+ background: var(--accent-purple);
380
+ color: white;
381
+ border: none;
382
+ width: 40px;
383
+ height: 40px;
384
+ border-radius: 50%;
385
+ cursor: pointer;
386
+ font-size: 18px;
387
+ display: none;
388
+ align-items: center;
389
+ justify-content: center;
390
+ box-shadow: 0 2px 12px rgba(155,109,255,0.4);
391
+ z-index: 100;
392
+ }
393
+ .back-to-top:hover { transform: scale(1.1); }
394
+
395
+ /* Responsive */
396
+ @media (max-width: 640px) {
397
+ .container { padding: 12px; }
398
+ .header-inner { flex-direction: column; align-items: flex-start; }
399
+ .header-meta { margin-left: 0; text-align: left; }
400
+ .stats-grid { grid-template-columns: repeat(2, 1fr); }
401
+ .stat-card .value { font-size: 18px; }
402
+ }
403
+ </style>`;
404
+ }
405
+ function generateHeader(metrics, sessionFileName, duration) {
406
+ const startDate = metrics.sessionStartTime
407
+ ? new Date(metrics.sessionStartTime).toLocaleString()
408
+ : 'N/A';
409
+ return `<div class="header">
410
+ <div class="header-inner">
411
+ <div class="logo">
412
+ <img src="${SIDEKICK_LOGO_BASE64}" alt="Sidekick" width="32" height="32" style="border-radius:6px">
413
+ <span class="logo-text">Sidekick</span>
414
+ </div>
415
+ <div class="header-meta">
416
+ <div>Session Report &middot; ${escapeHtml(startDate)} &middot; ${escapeHtml(duration)}</div>
417
+ <div>${metrics.currentModel ? `<span class="message-model">${escapeHtml(metrics.currentModel)}</span> &middot; ` : ''}${metrics.providerId ? escapeHtml(metrics.providerId) : ''}</div>
418
+ ${sessionFileName ? `<div class="session-file">${escapeHtml(sessionFileName)}</div>` : ''}
419
+ </div>
420
+ </div>
421
+ </div>`;
422
+ }
423
+ function generateStatsCards(metrics, tokens, duration) {
424
+ const main = tokens.mainThread;
425
+ const hasSubagents = tokens.subagents.length > 0;
426
+ const toolCount = metrics.toolStats.reduce((sum, t) => sum + t.successCount + t.failureCount, 0);
427
+ const cards = [
428
+ { label: 'Messages', value: String(metrics.messageCount), sub: `${metrics.eventCount} events` },
429
+ {
430
+ label: 'Duration',
431
+ value: duration,
432
+ sub: metrics.sessionStartTime ? formatTimestamp(metrics.sessionStartTime) + ' start' : '',
433
+ },
434
+ {
435
+ label: hasSubagents ? `${TOKEN_MAIN_THREAD_LABEL} — ${TOKEN_TOTAL_LABEL}` : TOKEN_TOTAL_LABEL,
436
+ value: fmtTokens(main.total),
437
+ sub: `${fmtTokens(main.input)} in · ${fmtTokens(main.cacheRead)} cache read · ${fmtTokens(main.cacheWrite)} cache write · ${fmtTokens(main.output)} out${main.billedOutsideBuckets > 0 ? ` · ${fmtTokens(main.billedOutsideBuckets)} reasoning` : ''}`,
438
+ },
439
+ ...(hasSubagents
440
+ ? [
441
+ {
442
+ label: `${TOKEN_SUBAGENTS_LABEL} (${tokens.subagents.length})`,
443
+ value: fmtTokens(tokens.subagentTotal.total),
444
+ sub: `${fmtTokens(tokens.subagentTotal.cacheRead)} cache read · ${fmtTokens(tokens.subagentTotal.output)} out`,
445
+ },
446
+ {
447
+ label: TOKEN_SESSION_TOTAL_LABEL,
448
+ value: fmtTokens(tokens.combined.total),
449
+ sub: 'main thread + subagents',
450
+ },
451
+ ]
452
+ : []),
453
+ {
454
+ label: 'Cache hit',
455
+ value: main.cacheHitRatio === null ? '—' : `${Math.round(main.cacheHitRatio * 100)}%`,
456
+ sub: `${fmtTokens(main.cacheRead)} of ${fmtTokens(main.context)} context tokens`,
457
+ },
458
+ {
459
+ label: 'Tool Calls',
460
+ value: String(toolCount),
461
+ sub: `${metrics.toolStats.length} unique tools`,
462
+ },
463
+ ];
464
+ if (metrics.tokens.costUsd > 0 || metrics.tokens.unpricedCalls > 0) {
465
+ cards.push({
466
+ label: 'Cost',
467
+ value: metrics.tokens.costUsd > 0 ? fmtCost(metrics.tokens.costUsd) : '—',
468
+ sub: escapeHtml(describeCostProvenance(metrics.tokens)),
469
+ });
470
+ }
471
+ return `<div class="container">
472
+ <div class="stats-grid">
473
+ ${cards
474
+ .map((c) => ` <div class="stat-card">
475
+ <div class="label">${c.label}</div>
476
+ <div class="value">${c.value}</div>
477
+ ${c.sub ? `<div class="sub">${c.sub}</div>` : ''}
478
+ </div>`)
479
+ .join('\n')}
480
+ </div>`;
481
+ }
482
+ function generateModelBreakdown(metrics) {
483
+ if (metrics.modelStats.length === 0)
484
+ return '';
485
+ const rows = metrics.modelStats
486
+ .map((m) => `<tr><td>${escapeHtml(m.model)}</td><td>${m.calls}</td><td>${fmtTokens(m.tokens)}</td><td>${fmtTokens(m.inputTokens)} / ${fmtTokens(m.outputTokens)}</td><td>${fmtCost(m.cost)}</td></tr>`)
487
+ .join('\n');
488
+ return `<div class="section">
489
+ <div class="section-title">Model Breakdown</div>
490
+ <table>
491
+ <thead><tr><th>Model</th><th>Calls</th><th>${TOKEN_TOTAL_LABEL}</th><th>In / Out</th><th>Cost</th></tr></thead>
492
+ <tbody>${rows}</tbody>
493
+ </table>
494
+ </div>`;
495
+ }
496
+ function generateToolBreakdown(metrics) {
497
+ if (metrics.toolStats.length === 0)
498
+ return '';
499
+ const sorted = [...metrics.toolStats].sort((a, b) => b.successCount + b.failureCount - (a.successCount + a.failureCount));
500
+ const rows = sorted
501
+ .map((t) => {
502
+ const total = t.successCount + t.failureCount;
503
+ const avgMs = t.completedCount > 0 ? Math.round(t.totalDuration / t.completedCount) : 0;
504
+ const failHtml = t.failureCount > 0 ? `<span style="color:var(--accent-red)">${t.failureCount}</span>` : '0';
505
+ return `<tr><td><code>${escapeHtml(t.name)}</code></td><td>${total}</td><td>${failHtml}</td><td>${avgMs > 0 ? `${avgMs}ms` : '-'}</td></tr>`;
506
+ })
507
+ .join('\n');
508
+ return `<div class="section">
509
+ <div class="section-title">Tool Breakdown</div>
510
+ <table>
511
+ <thead><tr><th>Tool</th><th>Total</th><th>Failed</th><th>Avg Duration</th></tr></thead>
512
+ <tbody>${rows}</tbody>
513
+ </table>
514
+ </div>`;
515
+ }
516
+ function generateTranscriptControls() {
517
+ return `<div class="section">
518
+ <div class="section-title">Conversation Transcript</div>
519
+ <div class="transcript-controls">
520
+ <label><input type="checkbox" id="filter-user" checked onchange="filterMessages()"> User</label>
521
+ <label><input type="checkbox" id="filter-assistant" checked onchange="filterMessages()"> Assistant</label>
522
+ <label><input type="checkbox" id="filter-tool" checked onchange="filterMessages()"> Tools</label>
523
+ <label><input type="checkbox" id="filter-thinking" checked onchange="filterMessages()"> Thinking</label>
524
+ <label><input type="checkbox" id="filter-system" checked onchange="filterMessages()"> System</label>
525
+ <button onclick="toggleAllDetails(true)">Expand All</button>
526
+ <button onclick="toggleAllDetails(false)">Collapse All</button>
527
+ </div>`;
528
+ }
529
+ function generateTranscript(transcript, includeThinking, includeToolDetail) {
530
+ if (transcript.length === 0) {
531
+ return `<div class="message"><div class="message-body"><em>No transcript data available</em></div></div>\n</div>`;
532
+ }
533
+ const parts = [];
534
+ for (const entry of transcript) {
535
+ parts.push(renderMessage(entry, includeThinking, includeToolDetail));
536
+ }
537
+ parts.push('</div>'); // close .section container opened in generateStatsCards
538
+ parts.push('</div>'); // close .container
539
+ return parts.join('\n');
540
+ }
541
+ function renderMessage(entry, includeThinking, includeToolDetail) {
542
+ const ts = entry.timestamp ? formatTimestamp(entry.timestamp) : '';
543
+ const roleBadge = `<span class="role-badge ${entry.type}">${entry.type}</span>`;
544
+ const sourceStr = entry.sourceLabel
545
+ ? `<span class="message-source">${escapeHtml(entry.sourceLabel)}</span>`
546
+ : '';
547
+ const modelStr = entry.model
548
+ ? `<span class="message-model">${escapeHtml(entry.model)}</span>`
549
+ : '';
550
+ const tokenStr = entry.usage
551
+ ? `<span class="message-tokens">${fmtTokens(summarizeTokens({
552
+ inputTokens: entry.usage.input_tokens,
553
+ outputTokens: entry.usage.output_tokens,
554
+ cacheWriteTokens: entry.usage.cache_creation_input_tokens,
555
+ cacheReadTokens: entry.usage.cache_read_input_tokens,
556
+ }).total)} tokens incl. cache</span>`
557
+ : '';
558
+ const bodyParts = [];
559
+ for (const block of entry.content) {
560
+ bodyParts.push(renderContentBlock(block, includeThinking, includeToolDetail));
561
+ }
562
+ const bodyContent = bodyParts.filter(Boolean).join('\n');
563
+ if (!bodyContent.trim())
564
+ return '';
565
+ return `<div class="message ${entry.type}" data-type="${entry.type}">
566
+ <div class="message-header">
567
+ ${roleBadge}
568
+ ${sourceStr}
569
+ ${modelStr}
570
+ ${tokenStr}
571
+ <span class="message-ts">${ts}</span>
572
+ </div>
573
+ <div class="message-body">
574
+ ${bodyContent}
575
+ </div>
576
+ </div>`;
577
+ }
578
+ function renderContentBlock(block, includeThinking, includeToolDetail) {
579
+ switch (block.type) {
580
+ case 'text':
581
+ return `<div class="text-content">${simpleMarkdownToHtml(block.text || '')}</div>`;
582
+ case 'thinking':
583
+ if (!includeThinking)
584
+ return '';
585
+ return `<details class="thinking-block" data-block-type="thinking">
586
+ <summary>Thinking...</summary>
587
+ <div class="thinking-content">${escapeHtml(block.text || '')}</div>
588
+ </details>`;
589
+ case 'tool_use':
590
+ return renderToolUse(block, includeToolDetail);
591
+ case 'tool_result':
592
+ return renderToolResult(block, includeToolDetail);
593
+ case 'image':
594
+ return '<div class="text-content"><em>[Image content]</em></div>';
595
+ default:
596
+ return '';
597
+ }
598
+ }
599
+ function renderToolUse(block, includeDetail) {
600
+ const name = block.toolName || 'unknown';
601
+ if (!includeDetail) {
602
+ return `<div class="tool-block" data-block-type="tool"><div style="padding:8px 12px;background:rgba(245,166,35,0.08);color:var(--accent-orange);font-family:monospace;font-size:13px;">${escapeHtml(name)}</div></div>`;
603
+ }
604
+ let inputHtml = '';
605
+ if (block.toolInput && Object.keys(block.toolInput).length > 0) {
606
+ const inputJson = JSON.stringify(block.toolInput, null, 2);
607
+ const highlighted = highlightCodeBlock(inputJson, 'json');
608
+ inputHtml = `<div class="tool-io">
609
+ <div class="tool-label">Input</div>
610
+ <pre>${highlighted}</pre>
611
+ </div>`;
612
+ }
613
+ return `<details class="tool-block" data-block-type="tool">
614
+ <summary>${escapeHtml(name)}${block.toolUseId ? ` <span style="color:var(--text-muted);font-size:11px">${escapeHtml(block.toolUseId)}</span>` : ''}</summary>
615
+ <div class="tool-content">
616
+ ${inputHtml}
617
+ </div>
618
+ </details>`;
619
+ }
620
+ function renderToolResult(block, includeDetail) {
621
+ if (!includeDetail)
622
+ return '';
623
+ const output = block.output || '';
624
+ const isError = block.isError;
625
+ const errorClass = isError ? ' tool-error' : '';
626
+ const label = isError ? 'Error Output' : 'Output';
627
+ if (!output.trim()) {
628
+ return `<details class="tool-result-block" data-block-type="tool">
629
+ <summary>${label}: <span style="color:var(--text-muted)">(empty)</span></summary>
630
+ <div class="tool-content"></div>
631
+ </details>`;
632
+ }
633
+ // Truncate extremely long output in the HTML to prevent huge files
634
+ const maxLen = 50000;
635
+ const displayOutput = output.length > maxLen
636
+ ? output.substring(0, maxLen) + `\n\n... (truncated ${output.length - maxLen} characters)`
637
+ : output;
638
+ return `<details class="tool-result-block${errorClass}" data-block-type="tool">
639
+ <summary class="${errorClass}">${label} (${formatOutputSize(output.length)})</summary>
640
+ <div class="tool-content">
641
+ <pre>${escapeHtml(displayOutput)}</pre>
642
+ </div>
643
+ </details>`;
644
+ }
645
+ function formatOutputSize(len) {
646
+ if (len >= 1000)
647
+ return `${(len / 1000).toFixed(1)}k chars`;
648
+ return `${len} chars`;
649
+ }
650
+ function generateBackToTop() {
651
+ return `<button class="back-to-top" id="backToTop" onclick="window.scrollTo({top:0,behavior:'smooth'})">&uarr;</button>`;
652
+ }
653
+ function generateScript() {
654
+ return `<script>
655
+ // Back to top visibility
656
+ window.addEventListener('scroll', function() {
657
+ var btn = document.getElementById('backToTop');
658
+ btn.style.display = window.scrollY > 300 ? 'flex' : 'none';
659
+ });
660
+
661
+ // Filter messages by type
662
+ function filterMessages() {
663
+ var showUser = document.getElementById('filter-user').checked;
664
+ var showAssistant = document.getElementById('filter-assistant').checked;
665
+ var showTool = document.getElementById('filter-tool').checked;
666
+ var showThinking = document.getElementById('filter-thinking').checked;
667
+ var showSystem = document.getElementById('filter-system').checked;
668
+
669
+ document.querySelectorAll('.message').forEach(function(msg) {
670
+ var type = msg.getAttribute('data-type');
671
+ var visible = true;
672
+ if (type === 'user') visible = showUser;
673
+ else if (type === 'assistant') visible = showAssistant;
674
+ else if (type === 'system' || type === 'summary') visible = showSystem;
675
+ msg.style.display = visible ? '' : 'none';
676
+ });
677
+
678
+ // Tool blocks within messages
679
+ document.querySelectorAll('[data-block-type="tool"]').forEach(function(el) {
680
+ el.style.display = showTool ? '' : 'none';
681
+ });
682
+
683
+ // Thinking blocks
684
+ document.querySelectorAll('[data-block-type="thinking"]').forEach(function(el) {
685
+ el.style.display = showThinking ? '' : 'none';
686
+ });
687
+ }
688
+
689
+ // Expand/collapse all details
690
+ function toggleAllDetails(open) {
691
+ document.querySelectorAll('details').forEach(function(d) { d.open = open; });
692
+ }
693
+
694
+ // Copy code block content
695
+ function copyCode(btn) {
696
+ var pre = btn.parentElement.querySelector('pre');
697
+ var text = pre.textContent || pre.innerText;
698
+ navigator.clipboard.writeText(text).then(function() {
699
+ btn.textContent = 'Copied!';
700
+ setTimeout(function() { btn.textContent = 'Copy'; }, 1500);
701
+ });
702
+ }
703
+ </script>`;
704
+ }