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,625 @@
1
+ /**
2
+ * @fileoverview Converts OpenCode message + parts into SessionEvent format.
3
+ *
4
+ * OpenCode stores messages and their parts as separate JSON/DB records.
5
+ * This module converts them to the SessionEvent format used by
6
+ * SessionMonitor and CLI consumers, enabling all existing event processing
7
+ * logic to work unchanged with OpenCode sessions.
8
+ *
9
+ * Moved from sidekick-vscode/src/services/providers/OpenCodeMessageParser.ts
10
+ *
11
+ * @module parsers/openCodeParser
12
+ */
13
+ import { normalizeProviderUsage } from '../usageNormalization';
14
+ /**
15
+ * OpenCode → Claude Code tool name mapping.
16
+ * OpenCode uses lowercase names; downstream code expects PascalCase.
17
+ */
18
+ const TOOL_NAME_MAP = {
19
+ read: 'Read',
20
+ write: 'Write',
21
+ edit: 'Edit',
22
+ multiedit: 'MultiEdit',
23
+ bash: 'Bash',
24
+ glob: 'Glob',
25
+ grep: 'Grep',
26
+ webfetch: 'WebFetch',
27
+ websearch: 'WebSearch',
28
+ task: 'Task',
29
+ todowrite: 'TodoWrite',
30
+ todoread: 'TodoRead',
31
+ apply_patch: 'ApplyPatch',
32
+ list: 'List',
33
+ lsp: 'Lsp',
34
+ question: 'AskUserQuestion',
35
+ skill: 'Skill',
36
+ plan_enter: 'EnterPlanMode',
37
+ plan_exit: 'ExitPlanMode',
38
+ };
39
+ /**
40
+ * Normalizes an OpenCode tool name to PascalCase.
41
+ *
42
+ * Uses the known mapping table for common tools, and falls back to
43
+ * capitalizing the first letter for unknown names.
44
+ */
45
+ export function normalizeToolName(name) {
46
+ if (!name)
47
+ return name;
48
+ // Check mapping (case-insensitive lookup via lowercase key)
49
+ const mapped = TOOL_NAME_MAP[name.toLowerCase()];
50
+ if (mapped)
51
+ return mapped;
52
+ // Already PascalCase (starts with uppercase)? Pass through.
53
+ if (name[0] >= 'A' && name[0] <= 'Z')
54
+ return name;
55
+ // Fallback: capitalize first letter
56
+ return name.charAt(0).toUpperCase() + name.slice(1);
57
+ }
58
+ /**
59
+ * Detects plan mode enter/exit from assistant text content.
60
+ *
61
+ * OpenCode models mention plan mode in text/thinking blocks but never emit
62
+ * plan_enter/plan_exit tool invocations. This heuristic detects those
63
+ * mentions and returns the appropriate direction.
64
+ *
65
+ * @param text - Text content to scan for plan mode mentions
66
+ * @returns 'enter' if entering plan mode, 'exit' if leaving, null if no match
67
+ */
68
+ export function detectPlanModeFromText(text) {
69
+ // Check exit first (more specific patterns)
70
+ if (/(exiting|leaving|deactivat(?:e|ing)|left) plan mode/i.test(text) ||
71
+ /plan mode (?:is )?(?:off|disabled|ended|complete)/i.test(text)) {
72
+ return 'exit';
73
+ }
74
+ // Check enter
75
+ if (/(entering|switch(?:ed|ing)? to|activat(?:e|ing)|now in) plan mode/i.test(text) ||
76
+ /plan mode (?:is )?(?:active|enabled|on)/i.test(text)) {
77
+ return 'enter';
78
+ }
79
+ return null;
80
+ }
81
+ /**
82
+ * Normalizes tool input fields for downstream compatibility.
83
+ *
84
+ * OpenCode uses camelCase field names (e.g. `filePath`); downstream code
85
+ * (MindMapDataService, SessionMonitor) expects snake_case (e.g. `file_path`).
86
+ * This adds snake_case aliases without removing the originals.
87
+ */
88
+ export function normalizeToolInput(input) {
89
+ const result = { ...input };
90
+ // filePath → file_path (used by MindMapDataService.extractFiles, SessionMonitor.extractToolContext)
91
+ if ('filePath' in result && !('file_path' in result)) {
92
+ result.file_path = result.filePath;
93
+ }
94
+ // oldString → old_string (used by lineChangeCalculator, TempFilesTreeProvider, ToolInspectorProvider)
95
+ if ('oldString' in result && !('old_string' in result)) {
96
+ result.old_string = result.oldString;
97
+ }
98
+ // newString → new_string (used by lineChangeCalculator, TempFilesTreeProvider, ToolInspectorProvider)
99
+ if ('newString' in result && !('new_string' in result)) {
100
+ result.new_string = result.newString;
101
+ }
102
+ // replaceAll → replace_all (used by ToolInspectorProvider)
103
+ if ('replaceAll' in result && !('replace_all' in result)) {
104
+ result.replace_all = result.replaceAll;
105
+ }
106
+ return result;
107
+ }
108
+ /**
109
+ * Converts a timestamp value (ISO string or Unix ms) to ISO string.
110
+ */
111
+ function toISOString(time) {
112
+ if (!time)
113
+ return new Date().toISOString();
114
+ if (typeof time === 'string')
115
+ return time;
116
+ return new Date(time).toISOString();
117
+ }
118
+ /**
119
+ * Converts an OpenCode message and its parts into SessionEvent array.
120
+ *
121
+ * Maps OpenCode's message/part model to the flat event stream that
122
+ * SessionMonitor expects. A single OpenCode message may produce multiple
123
+ * events (assistant message, tool results, compaction markers).
124
+ *
125
+ * @param message - OpenCode MessageV2 object
126
+ * @param parts - Array of OpenCode parts belonging to this message
127
+ * @returns Array of SessionEvent objects
128
+ */
129
+ export function convertOpenCodeMessage(message, parts) {
130
+ const events = [];
131
+ // Sort parts by index if available
132
+ const sortedParts = [...parts].sort((a, b) => (a.index ?? 0) - (b.index ?? 0));
133
+ if (message.role === 'user') {
134
+ events.push(...convertUserMessage(message, sortedParts));
135
+ }
136
+ else if (message.role === 'assistant') {
137
+ events.push(...convertAssistantMessage(message, sortedParts));
138
+ }
139
+ return events;
140
+ }
141
+ /**
142
+ * Converts a user-role message to events.
143
+ */
144
+ function convertUserMessage(message, parts) {
145
+ const events = [];
146
+ const content = [];
147
+ for (const part of parts) {
148
+ if (part.type === 'text') {
149
+ content.push({ type: 'text', text: part.text });
150
+ }
151
+ else if (part.type === 'file') {
152
+ content.push({
153
+ type: 'text',
154
+ text: `[File: ${part.filename || 'unknown'} (${part.mime || 'unknown'})]`,
155
+ });
156
+ }
157
+ else if (part.type === 'subtask') {
158
+ content.push({ type: 'text', text: `[Subtask: ${part.description || 'unknown'}]` });
159
+ }
160
+ }
161
+ if (content.length > 0) {
162
+ events.push({
163
+ type: 'user',
164
+ message: { role: 'user', id: message.id, content },
165
+ timestamp: toISOString(message.time.created),
166
+ });
167
+ }
168
+ return events;
169
+ }
170
+ /**
171
+ * Converts an assistant-role message to events.
172
+ *
173
+ * Produces:
174
+ * 1. An assistant event with text, thinking, and tool_use content blocks
175
+ * 2. Synthetic user events for completed tool results
176
+ * 3. A summary event if compaction was detected
177
+ */
178
+ function convertAssistantMessage(message, parts) {
179
+ const events = [];
180
+ const content = [];
181
+ for (const part of parts) {
182
+ switch (part.type) {
183
+ case 'text':
184
+ content.push({ type: 'text', text: part.text });
185
+ break;
186
+ case 'reasoning':
187
+ content.push({ type: 'thinking', thinking: part.text });
188
+ break;
189
+ case 'tool-invocation':
190
+ content.push({
191
+ type: 'tool_use',
192
+ id: part.callID,
193
+ name: normalizeToolName(part.tool),
194
+ input: normalizeToolInput(part.state.input || {}),
195
+ });
196
+ break;
197
+ case 'tool':
198
+ content.push({
199
+ type: 'tool_use',
200
+ id: part.callID,
201
+ name: normalizeToolName(part.tool),
202
+ input: normalizeToolInput(part.state.input || {}),
203
+ });
204
+ break;
205
+ case 'patch':
206
+ // Emit per-file synthetic Write tool_use blocks so downstream code
207
+ // (TempFilesTreeProvider, MindMapDataService) picks up file touches
208
+ // via the standard file_path field extraction.
209
+ if (part.files && part.files.length > 0) {
210
+ for (let i = 0; i < part.files.length; i++) {
211
+ content.push({
212
+ type: 'tool_use',
213
+ id: `patch-${part.id}-${i}`,
214
+ name: 'Write',
215
+ input: { file_path: part.files[i] },
216
+ });
217
+ }
218
+ }
219
+ else {
220
+ content.push({
221
+ type: 'tool_use',
222
+ id: `patch-${part.id}`,
223
+ name: 'Patch',
224
+ input: { hash: part.hash, files: part.files },
225
+ });
226
+ }
227
+ break;
228
+ case 'step-start':
229
+ case 'step-finish':
230
+ // Timeline markers — no content to emit
231
+ break;
232
+ case 'subtask':
233
+ content.push({
234
+ type: 'tool_use',
235
+ id: `subtask-${part.id}`,
236
+ name: 'Subtask',
237
+ input: {
238
+ description: part.description,
239
+ agent: part.agent,
240
+ model: part.model,
241
+ prompt: part.prompt,
242
+ command: part.command,
243
+ },
244
+ });
245
+ break;
246
+ case 'file':
247
+ content.push({
248
+ type: 'text',
249
+ text: `[File: ${part.filename || 'unknown'} (${part.mime || 'unknown'})]`,
250
+ });
251
+ break;
252
+ case 'retry':
253
+ content.push({
254
+ type: 'text',
255
+ text: `[Retry attempt ${part.attempt ?? '?'}: ${part.error?.message || 'unknown error'}]`,
256
+ });
257
+ break;
258
+ case 'agent':
259
+ case 'snapshot':
260
+ // Metadata only — no user-visible content
261
+ break;
262
+ case 'compaction':
263
+ // Will add summary event below
264
+ break;
265
+ }
266
+ }
267
+ // Surface message-level errors as text blocks so they appear in the timeline.
268
+ // OpenCode messages can carry structured errors (AuthError, APIError, etc.)
269
+ if (message.error) {
270
+ const errType = message.error.type || 'Error';
271
+ const errMsg = message.error.message || 'Unknown error';
272
+ const errCode = message.error.code ? ` (code: ${message.error.code})` : '';
273
+ content.push({
274
+ type: 'text',
275
+ text: `[${errType}]${errCode} ${errMsg}`,
276
+ });
277
+ }
278
+ // Detect plan mode mentions in text/thinking blocks and inject synthetic
279
+ // tool_use blocks (OpenCode doesn't emit plan_enter/plan_exit tools).
280
+ const hasExistingPlanMode = content.some((b) => {
281
+ const block = b;
282
+ return (block.type === 'tool_use' && (block.name === 'EnterPlanMode' || block.name === 'ExitPlanMode'));
283
+ });
284
+ if (!hasExistingPlanMode) {
285
+ for (const block of content) {
286
+ const b = block;
287
+ let text = null;
288
+ if (b.type === 'text')
289
+ text = b.text;
290
+ else if (b.type === 'thinking')
291
+ text = b.thinking;
292
+ if (!text)
293
+ continue;
294
+ const direction = detectPlanModeFromText(text);
295
+ if (direction) {
296
+ const toolName = direction === 'enter' ? 'EnterPlanMode' : 'ExitPlanMode';
297
+ const rawName = direction === 'enter' ? 'plan_enter' : 'plan_exit';
298
+ content.push({
299
+ type: 'tool_use',
300
+ id: `plan-${direction}-${message.id}`,
301
+ name: toolName,
302
+ input: { source: 'opencode_text_heuristic', _sidekickRawToolName: rawName },
303
+ });
304
+ break; // Only inject one plan mode block per message
305
+ }
306
+ }
307
+ }
308
+ // Sum cost from step-finish parts as fallback for reported_cost
309
+ // when the message-level cost is unavailable. Step-finish parts carry
310
+ // per-step cost from the provider which can improve accuracy.
311
+ let stepFinishCost = 0;
312
+ for (const part of parts) {
313
+ if (part.type === 'step-finish' && part.cost && part.cost > 0) {
314
+ stepFinishCost += part.cost;
315
+ }
316
+ }
317
+ // Build usage data from message tokens
318
+ const reportedCost = typeof message.cost === 'number' && Number.isFinite(message.cost) && message.cost >= 0
319
+ ? message.cost
320
+ : stepFinishCost > 0
321
+ ? stepFinishCost
322
+ : undefined;
323
+ const usage = {
324
+ input_tokens: message.tokens.input || 0,
325
+ output_tokens: message.tokens.output || 0,
326
+ cache_creation_input_tokens: message.tokens.cacheWrite || 0,
327
+ cache_read_input_tokens: message.tokens.cacheRead || 0,
328
+ reported_cost: reportedCost,
329
+ reasoning_tokens: message.tokens.reasoning || 0,
330
+ };
331
+ const normalizedUsage = normalizeProviderUsage({
332
+ semantics: message.providerID === 'openai'
333
+ ? 'openai'
334
+ : message.providerID === 'anthropic'
335
+ ? 'anthropic'
336
+ : 'sidekick',
337
+ provider: message.providerID ?? 'opencode',
338
+ source: 'opencode-message',
339
+ model: message.modelID,
340
+ inputTokens: message.tokens.input,
341
+ outputTokens: message.tokens.output,
342
+ cacheReadTokens: message.tokens.cacheRead,
343
+ cacheWriteTokens: message.tokens.cacheWrite,
344
+ reasoningTokens: message.tokens.reasoning,
345
+ reasoningIncludedInOutput: message.providerID === 'openai',
346
+ reportedCostUsd: reportedCost,
347
+ });
348
+ const timestamp = toISOString(message.time.completed || message.time.created);
349
+ const retryAttempts = parts
350
+ .filter((part) => part.type === 'retry')
351
+ .map((part) => part.attempt)
352
+ .filter((attempt) => typeof attempt === 'number');
353
+ const finishReasons = parts
354
+ .filter((part) => part.type === 'step-finish')
355
+ .map((part) => part.reason)
356
+ .filter((reason) => typeof reason === 'string' && reason.length > 0);
357
+ // Emit assistant event (even if content is empty, usage data is valuable)
358
+ if (content.length > 0 || message.tokens.input > 0 || message.tokens.output > 0) {
359
+ events.push({
360
+ type: 'assistant',
361
+ message: {
362
+ role: 'assistant',
363
+ id: message.id,
364
+ model: message.modelID,
365
+ usage,
366
+ normalizedUsage,
367
+ content,
368
+ },
369
+ timestamp,
370
+ providerMetadata: {
371
+ providerId: message.providerID ?? 'opencode',
372
+ source: 'opencode-message',
373
+ ...(retryAttempts.length > 0 ? { retryAttempts } : {}),
374
+ ...(finishReasons.length > 0 ? { finishReasons } : {}),
375
+ ...(message.error ? { providerError: message.error } : {}),
376
+ },
377
+ });
378
+ }
379
+ // Emit synthetic tool_result events for completed tool invocations
380
+ // and subtask parts. In Claude Code's format, tool results come as
381
+ // user messages with tool_result content blocks.
382
+ for (const part of parts) {
383
+ if ((part.type === 'tool-invocation' || part.type === 'tool') &&
384
+ (part.state.status === 'completed' || part.state.status === 'error')) {
385
+ // Calculate duration from provider-supplied timestamps when available
386
+ let duration;
387
+ if (part.state.time?.start && part.state.time?.end) {
388
+ const startMs = typeof part.state.time.start === 'number'
389
+ ? part.state.time.start
390
+ : new Date(part.state.time.start).getTime();
391
+ const endMs = typeof part.state.time.end === 'number'
392
+ ? part.state.time.end
393
+ : new Date(part.state.time.end).getTime();
394
+ if (endMs > startMs) {
395
+ duration = endMs - startMs;
396
+ }
397
+ }
398
+ const resultContent = [
399
+ {
400
+ type: 'tool_result',
401
+ tool_use_id: part.callID,
402
+ content: part.state.status === 'error' ? part.state.error : part.state.output,
403
+ is_error: part.state.status === 'error',
404
+ ...(duration !== undefined && { duration }),
405
+ },
406
+ ];
407
+ const resultTimestamp = part.state.time?.end ? toISOString(part.state.time.end) : timestamp;
408
+ events.push({
409
+ type: 'user',
410
+ message: {
411
+ role: 'user',
412
+ id: `${message.id}:${part.callID}:result`,
413
+ content: resultContent,
414
+ },
415
+ timestamp: resultTimestamp,
416
+ });
417
+ }
418
+ // Subtask parts get a synthetic tool_result so they appear in the timeline
419
+ if (part.type === 'subtask') {
420
+ events.push({
421
+ type: 'user',
422
+ message: {
423
+ role: 'user',
424
+ id: `${message.id}:subtask-${part.id}:result`,
425
+ content: [
426
+ {
427
+ type: 'tool_result',
428
+ tool_use_id: `subtask-${part.id}`,
429
+ content: part.description || 'Subtask completed',
430
+ is_error: false,
431
+ },
432
+ ],
433
+ },
434
+ timestamp,
435
+ });
436
+ }
437
+ }
438
+ // Detect compaction via summary flag or compaction part
439
+ const hasCompaction = message.summary || parts.some((p) => p.type === 'compaction');
440
+ if (hasCompaction) {
441
+ events.push({
442
+ type: 'summary',
443
+ message: { role: 'assistant', id: `${message.id}:summary`, content: 'Context compacted' },
444
+ timestamp,
445
+ });
446
+ }
447
+ return events;
448
+ }
449
+ // --- DB-to-OpenCode adapter functions ---
450
+ /**
451
+ * Parses a database message row's JSON `data` field into an OpenCodeMessage.
452
+ *
453
+ * DB messages store data as a JSON blob with slightly different field layout
454
+ * than the file-based format (e.g., `modelID` is at top level, tokens are
455
+ * nested under `tokens`).
456
+ */
457
+ export function parseDbMessageData(row) {
458
+ const data = JSON.parse(row.data);
459
+ const time = data.time;
460
+ const tokens = data.tokens;
461
+ const cache = tokens?.cache;
462
+ const summary = data.summary;
463
+ // Parse error field if present (AuthError, APIError, OutputLengthError, etc.)
464
+ const errorData = data.error;
465
+ const error = errorData
466
+ ? {
467
+ type: errorData.type || undefined,
468
+ message: errorData.message || undefined,
469
+ code: errorData.code,
470
+ }
471
+ : undefined;
472
+ return {
473
+ id: row.id,
474
+ sessionID: row.session_id,
475
+ role: data.role,
476
+ modelID: data.modelID,
477
+ summary: summary != null ? true : undefined,
478
+ cost: data.cost || undefined,
479
+ providerID: data.providerID || undefined,
480
+ tokens: {
481
+ input: tokens?.input || 0,
482
+ output: tokens?.output || 0,
483
+ cacheRead: cache?.read || 0,
484
+ cacheWrite: cache?.write || 0,
485
+ reasoning: tokens?.reasoning || 0,
486
+ },
487
+ time: {
488
+ created: time?.created || row.time_created,
489
+ completed: time?.completed || undefined,
490
+ },
491
+ error,
492
+ };
493
+ }
494
+ /**
495
+ * Parses a database part row's JSON `data` field into an OpenCodePart.
496
+ */
497
+ export function parseDbPartData(row) {
498
+ const data = JSON.parse(row.data);
499
+ const type = data.type;
500
+ const base = { id: row.id, messageID: row.message_id };
501
+ switch (type) {
502
+ case 'text':
503
+ return { ...base, type: 'text', text: data.text || '' };
504
+ case 'reasoning':
505
+ return { ...base, type: 'reasoning', text: data.text || '' };
506
+ case 'tool': {
507
+ const state = data.state;
508
+ return {
509
+ ...base,
510
+ type: 'tool',
511
+ callID: data.callID || '',
512
+ tool: data.tool || '',
513
+ state: {
514
+ status: (state?.status || 'completed'),
515
+ input: state?.input,
516
+ output: state?.output,
517
+ error: state?.error,
518
+ title: state?.title,
519
+ metadata: state?.metadata,
520
+ time: state?.time,
521
+ },
522
+ };
523
+ }
524
+ case 'tool-invocation': {
525
+ const state = data.state;
526
+ return {
527
+ ...base,
528
+ type: 'tool-invocation',
529
+ callID: data.callID || '',
530
+ tool: data.tool || '',
531
+ state: {
532
+ status: (state?.status || 'completed'),
533
+ input: state?.input,
534
+ output: state?.output,
535
+ error: state?.error,
536
+ time: state?.time,
537
+ },
538
+ };
539
+ }
540
+ case 'compaction':
541
+ return { ...base, type: 'compaction', text: data.text };
542
+ case 'step-start':
543
+ return { ...base, type: 'step-start', snapshot: data.snapshot };
544
+ case 'step-finish': {
545
+ const tokensData = data.tokens;
546
+ const cacheData = tokensData?.cache;
547
+ return {
548
+ ...base,
549
+ type: 'step-finish',
550
+ reason: data.reason,
551
+ snapshot: data.snapshot,
552
+ cost: data.cost,
553
+ tokens: tokensData
554
+ ? {
555
+ input: tokensData.input,
556
+ output: tokensData.output,
557
+ reasoning: tokensData.reasoning,
558
+ cache: cacheData
559
+ ? {
560
+ read: cacheData.read,
561
+ write: cacheData.write,
562
+ }
563
+ : undefined,
564
+ }
565
+ : undefined,
566
+ };
567
+ }
568
+ case 'patch':
569
+ return {
570
+ ...base,
571
+ type: 'patch',
572
+ hash: data.hash,
573
+ files: data.files,
574
+ };
575
+ case 'subtask':
576
+ return {
577
+ ...base,
578
+ type: 'subtask',
579
+ prompt: data.prompt,
580
+ description: data.description,
581
+ agent: data.agent,
582
+ model: data.model,
583
+ command: data.command,
584
+ };
585
+ case 'agent':
586
+ return {
587
+ ...base,
588
+ type: 'agent',
589
+ name: data.name,
590
+ source: data.source,
591
+ };
592
+ case 'file':
593
+ return {
594
+ ...base,
595
+ type: 'file',
596
+ mime: data.mime,
597
+ filename: data.filename,
598
+ url: data.url,
599
+ };
600
+ case 'retry': {
601
+ const errorData = data.error;
602
+ return {
603
+ ...base,
604
+ type: 'retry',
605
+ attempt: data.attempt,
606
+ error: errorData
607
+ ? {
608
+ message: errorData.message,
609
+ code: errorData.code,
610
+ }
611
+ : undefined,
612
+ time: data.time,
613
+ };
614
+ }
615
+ case 'snapshot':
616
+ return {
617
+ ...base,
618
+ type: 'snapshot',
619
+ snapshot: data.snapshot,
620
+ };
621
+ default:
622
+ // Unknown part type — treat as text with raw JSON
623
+ return { ...base, type: 'text', text: JSON.stringify(data) };
624
+ }
625
+ }