codebase-onboarder 0.1.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 (216) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +275 -0
  3. package/bin/onboarder.js +14 -0
  4. package/bin/postinstall.js +6 -0
  5. package/cli/commands.js +436 -0
  6. package/cli/main.js +129 -0
  7. package/cli/prompt.js +101 -0
  8. package/cli/ui.js +55 -0
  9. package/cli/wizard.js +325 -0
  10. package/package.json +48 -0
  11. package/public/app.js +1339 -0
  12. package/public/index.html +455 -0
  13. package/public/js/about.js +284 -0
  14. package/public/js/aiDraft.js +163 -0
  15. package/public/js/analysisPanel.js +270 -0
  16. package/public/js/analysisReport.js +154 -0
  17. package/public/js/api.js +264 -0
  18. package/public/js/atlas.js +98 -0
  19. package/public/js/blameView.js +41 -0
  20. package/public/js/codeTab.js +256 -0
  21. package/public/js/codeViewer.js +184 -0
  22. package/public/js/components/FileChip.js +102 -0
  23. package/public/js/components/MetricSparkline.js +89 -0
  24. package/public/js/components/RiskBadge.js +105 -0
  25. package/public/js/deepAnalysisView.js +496 -0
  26. package/public/js/diagramPane.js +154 -0
  27. package/public/js/diffView.js +352 -0
  28. package/public/js/docsView.js +566 -0
  29. package/public/js/fileSourceBrowser.js +47 -0
  30. package/public/js/flameGraph.js +75 -0
  31. package/public/js/forceGraph.js +592 -0
  32. package/public/js/heatmap.js +185 -0
  33. package/public/js/highlight.js +122 -0
  34. package/public/js/html.js +50 -0
  35. package/public/js/insightsView.js +236 -0
  36. package/public/js/inspector.js +535 -0
  37. package/public/js/llm.js +345 -0
  38. package/public/js/mapView.js +159 -0
  39. package/public/js/markdown.js +119 -0
  40. package/public/js/mindmap.js +289 -0
  41. package/public/js/repoFiles.js +44 -0
  42. package/public/js/sbomView.js +142 -0
  43. package/public/js/scanCache.js +76 -0
  44. package/public/js/search.js +874 -0
  45. package/public/js/serverSettings.js +169 -0
  46. package/public/js/state.js +192 -0
  47. package/public/js/tour.js +17 -0
  48. package/public/js/transitions.js +15 -0
  49. package/public/js/tree.js +200 -0
  50. package/public/js/workflowsView.js +95 -0
  51. package/public/styles.css +2767 -0
  52. package/public/vendor/mermaid.min.js +3587 -0
  53. package/public/vendor/monaco/vs/base/browser/ui/codicons/codicon/codicon.ttf +0 -0
  54. package/public/vendor/monaco/vs/base/worker/workerMain.js +31 -0
  55. package/public/vendor/monaco/vs/basic-languages/abap/abap.js +10 -0
  56. package/public/vendor/monaco/vs/basic-languages/apex/apex.js +10 -0
  57. package/public/vendor/monaco/vs/basic-languages/azcli/azcli.js +10 -0
  58. package/public/vendor/monaco/vs/basic-languages/bat/bat.js +10 -0
  59. package/public/vendor/monaco/vs/basic-languages/bicep/bicep.js +11 -0
  60. package/public/vendor/monaco/vs/basic-languages/cameligo/cameligo.js +10 -0
  61. package/public/vendor/monaco/vs/basic-languages/clojure/clojure.js +10 -0
  62. package/public/vendor/monaco/vs/basic-languages/coffee/coffee.js +10 -0
  63. package/public/vendor/monaco/vs/basic-languages/cpp/cpp.js +10 -0
  64. package/public/vendor/monaco/vs/basic-languages/csharp/csharp.js +10 -0
  65. package/public/vendor/monaco/vs/basic-languages/csp/csp.js +10 -0
  66. package/public/vendor/monaco/vs/basic-languages/css/css.js +12 -0
  67. package/public/vendor/monaco/vs/basic-languages/cypher/cypher.js +10 -0
  68. package/public/vendor/monaco/vs/basic-languages/dart/dart.js +10 -0
  69. package/public/vendor/monaco/vs/basic-languages/dockerfile/dockerfile.js +10 -0
  70. package/public/vendor/monaco/vs/basic-languages/ecl/ecl.js +10 -0
  71. package/public/vendor/monaco/vs/basic-languages/elixir/elixir.js +10 -0
  72. package/public/vendor/monaco/vs/basic-languages/flow9/flow9.js +10 -0
  73. package/public/vendor/monaco/vs/basic-languages/freemarker2/freemarker2.js +12 -0
  74. package/public/vendor/monaco/vs/basic-languages/fsharp/fsharp.js +10 -0
  75. package/public/vendor/monaco/vs/basic-languages/go/go.js +10 -0
  76. package/public/vendor/monaco/vs/basic-languages/graphql/graphql.js +10 -0
  77. package/public/vendor/monaco/vs/basic-languages/handlebars/handlebars.js +10 -0
  78. package/public/vendor/monaco/vs/basic-languages/hcl/hcl.js +10 -0
  79. package/public/vendor/monaco/vs/basic-languages/html/html.js +10 -0
  80. package/public/vendor/monaco/vs/basic-languages/ini/ini.js +10 -0
  81. package/public/vendor/monaco/vs/basic-languages/java/java.js +10 -0
  82. package/public/vendor/monaco/vs/basic-languages/javascript/javascript.js +10 -0
  83. package/public/vendor/monaco/vs/basic-languages/julia/julia.js +10 -0
  84. package/public/vendor/monaco/vs/basic-languages/kotlin/kotlin.js +10 -0
  85. package/public/vendor/monaco/vs/basic-languages/less/less.js +11 -0
  86. package/public/vendor/monaco/vs/basic-languages/lexon/lexon.js +10 -0
  87. package/public/vendor/monaco/vs/basic-languages/liquid/liquid.js +10 -0
  88. package/public/vendor/monaco/vs/basic-languages/lua/lua.js +10 -0
  89. package/public/vendor/monaco/vs/basic-languages/m3/m3.js +10 -0
  90. package/public/vendor/monaco/vs/basic-languages/markdown/markdown.js +10 -0
  91. package/public/vendor/monaco/vs/basic-languages/mdx/mdx.js +10 -0
  92. package/public/vendor/monaco/vs/basic-languages/mips/mips.js +10 -0
  93. package/public/vendor/monaco/vs/basic-languages/msdax/msdax.js +10 -0
  94. package/public/vendor/monaco/vs/basic-languages/mysql/mysql.js +10 -0
  95. package/public/vendor/monaco/vs/basic-languages/objective-c/objective-c.js +10 -0
  96. package/public/vendor/monaco/vs/basic-languages/pascal/pascal.js +10 -0
  97. package/public/vendor/monaco/vs/basic-languages/pascaligo/pascaligo.js +10 -0
  98. package/public/vendor/monaco/vs/basic-languages/perl/perl.js +10 -0
  99. package/public/vendor/monaco/vs/basic-languages/pgsql/pgsql.js +10 -0
  100. package/public/vendor/monaco/vs/basic-languages/php/php.js +10 -0
  101. package/public/vendor/monaco/vs/basic-languages/pla/pla.js +10 -0
  102. package/public/vendor/monaco/vs/basic-languages/postiats/postiats.js +10 -0
  103. package/public/vendor/monaco/vs/basic-languages/powerquery/powerquery.js +10 -0
  104. package/public/vendor/monaco/vs/basic-languages/powershell/powershell.js +10 -0
  105. package/public/vendor/monaco/vs/basic-languages/protobuf/protobuf.js +11 -0
  106. package/public/vendor/monaco/vs/basic-languages/pug/pug.js +10 -0
  107. package/public/vendor/monaco/vs/basic-languages/python/python.js +10 -0
  108. package/public/vendor/monaco/vs/basic-languages/qsharp/qsharp.js +10 -0
  109. package/public/vendor/monaco/vs/basic-languages/r/r.js +10 -0
  110. package/public/vendor/monaco/vs/basic-languages/razor/razor.js +10 -0
  111. package/public/vendor/monaco/vs/basic-languages/redis/redis.js +10 -0
  112. package/public/vendor/monaco/vs/basic-languages/redshift/redshift.js +10 -0
  113. package/public/vendor/monaco/vs/basic-languages/restructuredtext/restructuredtext.js +10 -0
  114. package/public/vendor/monaco/vs/basic-languages/ruby/ruby.js +10 -0
  115. package/public/vendor/monaco/vs/basic-languages/rust/rust.js +10 -0
  116. package/public/vendor/monaco/vs/basic-languages/sb/sb.js +10 -0
  117. package/public/vendor/monaco/vs/basic-languages/scala/scala.js +10 -0
  118. package/public/vendor/monaco/vs/basic-languages/scheme/scheme.js +10 -0
  119. package/public/vendor/monaco/vs/basic-languages/scss/scss.js +12 -0
  120. package/public/vendor/monaco/vs/basic-languages/shell/shell.js +10 -0
  121. package/public/vendor/monaco/vs/basic-languages/solidity/solidity.js +10 -0
  122. package/public/vendor/monaco/vs/basic-languages/sophia/sophia.js +10 -0
  123. package/public/vendor/monaco/vs/basic-languages/sparql/sparql.js +10 -0
  124. package/public/vendor/monaco/vs/basic-languages/sql/sql.js +10 -0
  125. package/public/vendor/monaco/vs/basic-languages/st/st.js +10 -0
  126. package/public/vendor/monaco/vs/basic-languages/swift/swift.js +13 -0
  127. package/public/vendor/monaco/vs/basic-languages/systemverilog/systemverilog.js +10 -0
  128. package/public/vendor/monaco/vs/basic-languages/tcl/tcl.js +10 -0
  129. package/public/vendor/monaco/vs/basic-languages/twig/twig.js +10 -0
  130. package/public/vendor/monaco/vs/basic-languages/typescript/typescript.js +10 -0
  131. package/public/vendor/monaco/vs/basic-languages/typespec/typespec.js +10 -0
  132. package/public/vendor/monaco/vs/basic-languages/vb/vb.js +10 -0
  133. package/public/vendor/monaco/vs/basic-languages/wgsl/wgsl.js +307 -0
  134. package/public/vendor/monaco/vs/basic-languages/xml/xml.js +10 -0
  135. package/public/vendor/monaco/vs/basic-languages/yaml/yaml.js +10 -0
  136. package/public/vendor/monaco/vs/editor/editor.main.css +8 -0
  137. package/public/vendor/monaco/vs/editor/editor.main.js +798 -0
  138. package/public/vendor/monaco/vs/language/css/cssMode.js +13 -0
  139. package/public/vendor/monaco/vs/language/css/cssWorker.js +77 -0
  140. package/public/vendor/monaco/vs/language/html/htmlMode.js +13 -0
  141. package/public/vendor/monaco/vs/language/html/htmlWorker.js +454 -0
  142. package/public/vendor/monaco/vs/language/json/jsonMode.js +19 -0
  143. package/public/vendor/monaco/vs/language/json/jsonWorker.js +42 -0
  144. package/public/vendor/monaco/vs/language/typescript/tsMode.js +20 -0
  145. package/public/vendor/monaco/vs/language/typescript/tsWorker.js +51328 -0
  146. package/public/vendor/monaco/vs/loader.js +11 -0
  147. package/public/vendor/monaco/worker-boot.js +9 -0
  148. package/server/.fuse_hidden0000000800000001 +36 -0
  149. package/server/apiDiff.js +25 -0
  150. package/server/apiDocs.js +67 -0
  151. package/server/apiFile.js +36 -0
  152. package/server/apiGitBlame.js +63 -0
  153. package/server/apiMcp.js +110 -0
  154. package/server/apiScan.js +119 -0
  155. package/server/apiSearch.js +253 -0
  156. package/server/apiSettings.js +118 -0
  157. package/server/apiTools.js +90 -0
  158. package/server/config.js +227 -0
  159. package/server/fileSourceNode.js +44 -0
  160. package/server/gitClone.js +95 -0
  161. package/server/gitDiff.js +184 -0
  162. package/server/gitHistory.js +110 -0
  163. package/server/htmlText.js +44 -0
  164. package/server/http.js +55 -0
  165. package/server/httpGuards.js +87 -0
  166. package/server/index.js +156 -0
  167. package/server/llmProxy.js +162 -0
  168. package/server/logger.js +29 -0
  169. package/server/mcp/analysis.js +209 -0
  170. package/server/mcp/http.js +213 -0
  171. package/server/mcp/runner.js +278 -0
  172. package/server/mcp/server.js +241 -0
  173. package/server/mcp/standalone.js +42 -0
  174. package/server/mcp/tools.js +683 -0
  175. package/server/paths.js +39 -0
  176. package/server/router.js +218 -0
  177. package/server/searchIndex.js +118 -0
  178. package/server/sessions.js +163 -0
  179. package/server/static.js +59 -0
  180. package/server/tools/install.js +246 -0
  181. package/server/tools/parse.js +170 -0
  182. package/server/tools/platform.js +91 -0
  183. package/server/tools/registry.js +212 -0
  184. package/server/tools/scan.js +136 -0
  185. package/server/tools.js +212 -0
  186. package/server/tunnel.js +92 -0
  187. package/shared/analyzer/docs.js +135 -0
  188. package/shared/analyzer/explainLocal.js +163 -0
  189. package/shared/analyzer/graph.js +461 -0
  190. package/shared/analyzer/health.js +215 -0
  191. package/shared/analyzer/history.js +146 -0
  192. package/shared/analyzer/languages/csharp.js +39 -0
  193. package/shared/analyzer/languages/generic.js +131 -0
  194. package/shared/analyzer/languages/go.js +70 -0
  195. package/shared/analyzer/languages/index.js +43 -0
  196. package/shared/analyzer/languages/java.js +39 -0
  197. package/shared/analyzer/languages/javascript.js +240 -0
  198. package/shared/analyzer/languages/python.js +120 -0
  199. package/shared/analyzer/languages/rust.js +42 -0
  200. package/shared/analyzer/languages/typescript.js +138 -0
  201. package/shared/analyzer/licenses.js +151 -0
  202. package/shared/analyzer/metrics.js +120 -0
  203. package/shared/analyzer/pathUtil.js +69 -0
  204. package/shared/analyzer/patterns.js +272 -0
  205. package/shared/analyzer/scan.js +473 -0
  206. package/shared/analyzer/security.js +187 -0
  207. package/shared/analyzer/services.js +157 -0
  208. package/shared/analyzer/stack.js +173 -0
  209. package/shared/analyzer/tour.js +69 -0
  210. package/shared/analyzer/util.js +123 -0
  211. package/shared/analyzer/workflows.js +143 -0
  212. package/shared/diagram/aiFacts.js +145 -0
  213. package/shared/diagram/aiMermaid.js +59 -0
  214. package/shared/diagram/atlas.js +93 -0
  215. package/shared/diagram/mermaid.js +459 -0
  216. package/shared/search/query.js +518 -0
@@ -0,0 +1,352 @@
1
+ // PR / Branch Diff & Blast Radius View.
2
+ // Renders side-by-side / unified git diffs and calculates downstream architectural blast radius.
3
+
4
+ import { escapeHtml } from './html.js';
5
+
6
+ let activeScanId = null;
7
+ let currentDiff = null;
8
+ let currentRefs = null;
9
+ let selectedFile = null;
10
+ let diffMode = 'unified'; // 'unified' | 'split'
11
+
12
+ export async function renderDiffView(container, { scan, facts, scanId }) {
13
+ if (!container) return;
14
+ activeScanId = scanId;
15
+
16
+ container.innerHTML = `
17
+ <div class="diff-layout">
18
+ <header class="diff-header">
19
+ <div class="diff-ref-selectors">
20
+ <div class="diff-select-group">
21
+ <span class="diff-select-label">Base:</span>
22
+ <select class="diff-select" id="diffBaseSelect">
23
+ <option value="HEAD~1">HEAD~1 (previous commit)</option>
24
+ <option value="HEAD">HEAD (committed)</option>
25
+ <option value="main">main</option>
26
+ </select>
27
+ </div>
28
+ <span class="diff-arrow">←</span>
29
+ <div class="diff-select-group">
30
+ <span class="diff-select-label">Compare (Head):</span>
31
+ <select class="diff-select" id="diffHeadSelect">
32
+ <option value="">Working Tree (Uncommitted)</option>
33
+ <option value="HEAD">HEAD</option>
34
+ </select>
35
+ </div>
36
+ <button class="btn btn-ink btn-sm" id="diffRunBtn">Compare</button>
37
+ </div>
38
+ <div class="diff-view-modes">
39
+ <button class="btn btn-ghost btn-sm is-active" id="diffModeUnified">Unified</button>
40
+ <button class="btn btn-ghost btn-sm" id="diffModeSplit">Split</button>
41
+ </div>
42
+ </header>
43
+
44
+ <div class="diff-body">
45
+ <!-- Sidebar: Changed Files & Blast Radius -->
46
+ <aside class="diff-sidebar">
47
+ <div class="diff-sidebar-head">
48
+ <h4>Changed Files</h4>
49
+ <span class="diff-stat-chip" id="diffSummaryChip">Loading…</span>
50
+ </div>
51
+
52
+ <!-- Blast Radius Impact Overview -->
53
+ <div class="blast-radius-box" id="blastRadiusBox">
54
+ <div class="br-title">Downstream Blast Radius</div>
55
+ <div class="br-metric" id="blastMetric">—</div>
56
+ <div class="br-desc" id="blastDesc">Analyzing impacted dependent modules…</div>
57
+ </div>
58
+
59
+ <div class="diff-files-list" id="diffFilesList">
60
+ <p class="diff-empty">Fetching diff…</p>
61
+ </div>
62
+ </aside>
63
+
64
+ <!-- Main: Diff Viewer -->
65
+ <main class="diff-content" id="diffViewerMain">
66
+ <div class="diff-placeholder">Select a file to inspect diff hunks.</div>
67
+ </main>
68
+ </div>
69
+ </div>
70
+ `;
71
+
72
+ // Wire controls
73
+ const baseSelect = container.querySelector('#diffBaseSelect');
74
+ const headSelect = container.querySelector('#diffHeadSelect');
75
+ const runBtn = container.querySelector('#diffRunBtn');
76
+ const unifiedBtn = container.querySelector('#diffModeUnified');
77
+ const splitBtn = container.querySelector('#diffModeSplit');
78
+
79
+ unifiedBtn?.addEventListener('click', () => {
80
+ diffMode = 'unified';
81
+ unifiedBtn.classList.add('is-active');
82
+ splitBtn?.classList.remove('is-active');
83
+ renderCurrentFileDiff(container);
84
+ });
85
+
86
+ splitBtn?.addEventListener('click', () => {
87
+ diffMode = 'split';
88
+ splitBtn.classList.add('is-active');
89
+ unifiedBtn?.classList.remove('is-active');
90
+ renderCurrentFileDiff(container);
91
+ });
92
+
93
+ runBtn?.addEventListener('click', () => {
94
+ loadDiffData(container, scan, facts, baseSelect.value, headSelect.value);
95
+ });
96
+
97
+ // Initial load
98
+ await loadRefs(container, scanId);
99
+ await loadDiffData(container, scan, facts, 'HEAD~1', '');
100
+ }
101
+
102
+ async function loadRefs(container, scanId) {
103
+ if (!scanId) return;
104
+ try {
105
+ const res = await fetch('/api/diff/refs?scan=' + encodeURIComponent(scanId));
106
+ if (res.ok) {
107
+ currentRefs = await res.json();
108
+ const baseSelect = container.querySelector('#diffBaseSelect');
109
+ const headSelect = container.querySelector('#diffHeadSelect');
110
+ if (baseSelect && currentRefs.branches?.length) {
111
+ baseSelect.innerHTML = `
112
+ <option value="HEAD~1">HEAD~1 (previous commit)</option>
113
+ <option value="HEAD">HEAD</option>
114
+ ${currentRefs.branches.map((b) => `<option value="${escapeHtml(b)}">${escapeHtml(b)}</option>`).join('')}
115
+ ${(currentRefs.tags || []).map((t) => `<option value="${escapeHtml(t)}">tag: ${escapeHtml(t)}</option>`).join('')}
116
+ `;
117
+ }
118
+ }
119
+ } catch (e) {
120
+ /* fallback to defaults */
121
+ }
122
+ }
123
+
124
+ async function loadDiffData(container, scan, facts, base, head) {
125
+ const listEl = container.querySelector('#diffFilesList');
126
+ const summaryEl = container.querySelector('#diffSummaryChip');
127
+ if (listEl) listEl.innerHTML = '<p class="diff-empty">Loading diff…</p>';
128
+
129
+ try {
130
+ const params = new URLSearchParams();
131
+ if (activeScanId) params.set('scan', activeScanId);
132
+ if (base) params.set('base', base);
133
+ if (head) params.set('head', head);
134
+
135
+ const res = await fetch('/api/diff?' + params.toString());
136
+ if (!res.ok) {
137
+ if (listEl) listEl.innerHTML = '<p class="diff-empty">No git repository or diff available for this target.</p>';
138
+ return;
139
+ }
140
+
141
+ currentDiff = await res.json();
142
+ const files = currentDiff.files || [];
143
+
144
+ if (summaryEl) {
145
+ summaryEl.innerHTML = `<b>${files.length}</b> files <span class="diff-add">+${currentDiff.stats?.additions || 0}</span> <span class="diff-del">-${currentDiff.stats?.deletions || 0}</span>`;
146
+ }
147
+
148
+ // Compute blast radius from modified files
149
+ computeAndRenderBlastRadius(container, files, scan, facts);
150
+
151
+ if (!files.length) {
152
+ if (listEl) listEl.innerHTML = '<p class="diff-empty">No differences between selected references.</p>';
153
+ const mainEl = container.querySelector('#diffViewerMain');
154
+ if (mainEl) mainEl.innerHTML = '<div class="diff-placeholder">Working tree clean. No changes detected.</div>';
155
+ return;
156
+ }
157
+
158
+ selectedFile = files[0].newPath || files[0].oldPath;
159
+ renderFilesList(container, files);
160
+ renderCurrentFileDiff(container);
161
+ } catch (err) {
162
+ if (listEl) listEl.innerHTML = `<p class="diff-empty">Failed to load diff: ${escapeHtml(err.message)}</p>`;
163
+ }
164
+ }
165
+
166
+ function computeAndRenderBlastRadius(container, diffFiles, scan, facts) {
167
+ const metricEl = container.querySelector('#blastMetric');
168
+ const descEl = container.querySelector('#blastDesc');
169
+ if (!metricEl || !descEl) return;
170
+
171
+ if (!diffFiles.length) {
172
+ metricEl.textContent = '0 files';
173
+ descEl.textContent = 'No modified files to analyze.';
174
+ return;
175
+ }
176
+
177
+ const modifiedPaths = new Set(diffFiles.map((f) => f.newPath || f.oldPath));
178
+ const impacted = new Set();
179
+ const queue = [...modifiedPaths];
180
+
181
+ while (queue.length) {
182
+ const curr = queue.shift();
183
+ const dependents = facts?.importers?.[curr] || [];
184
+ for (const dep of dependents) {
185
+ if (!impacted.has(dep) && !modifiedPaths.has(dep)) {
186
+ impacted.add(dep);
187
+ queue.push(dep);
188
+ }
189
+ }
190
+ }
191
+
192
+ const totalFiles = scan?.files?.length || 1;
193
+ const impactCount = impacted.size;
194
+ const pct = Math.round((impactCount / totalFiles) * 100);
195
+
196
+ metricEl.innerHTML = `<b>${impactCount}</b> dependent files affected (${pct}%)`;
197
+ descEl.innerHTML = impactCount > 0
198
+ ? `Directly impacts <b>${impactCount}</b> downstream module${impactCount === 1 ? '' : 's'} across the import graph.`
199
+ : `Self-contained change: no downstream imports affected.`;
200
+ }
201
+
202
+ function renderFilesList(container, files) {
203
+ const listEl = container.querySelector('#diffFilesList');
204
+ if (!listEl) return;
205
+
206
+ listEl.innerHTML = files.map((f) => {
207
+ const path = f.newPath || f.oldPath;
208
+ const isSelected = path === selectedFile;
209
+ return `
210
+ <div class="diff-file-item ${isSelected ? 'is-selected' : ''}" data-path="${escapeHtml(path)}">
211
+ <span class="diff-file-badge status-${f.status}">${f.status.slice(0, 1).toUpperCase()}</span>
212
+ <span class="diff-file-path" title="${escapeHtml(path)}">${escapeHtml(path)}</span>
213
+ <span class="diff-file-counts">
214
+ <span class="diff-add">+${f.additions}</span>
215
+ <span class="diff-del">-${f.deletions}</span>
216
+ </span>
217
+ </div>
218
+ `;
219
+ }).join('');
220
+
221
+ listEl.querySelectorAll('.diff-file-item').forEach((item) => {
222
+ item.addEventListener('click', () => {
223
+ selectedFile = item.dataset.path;
224
+ listEl.querySelectorAll('.diff-file-item').forEach((el) => el.classList.remove('is-selected'));
225
+ item.classList.add('is-selected');
226
+ renderCurrentFileDiff(container);
227
+ });
228
+ });
229
+ }
230
+
231
+ function renderCurrentFileDiff(container) {
232
+ const mainEl = container.querySelector('#diffViewerMain');
233
+ if (!mainEl || !currentDiff) return;
234
+
235
+ const file = currentDiff.files?.find((f) => (f.newPath || f.oldPath) === selectedFile);
236
+ if (!file) {
237
+ mainEl.innerHTML = '<div class="diff-placeholder">Select a file to inspect diff.</div>';
238
+ return;
239
+ }
240
+
241
+ const hunksHtml = file.hunks.map((hunk) => {
242
+ if (diffMode === 'split') {
243
+ return renderSplitHunk(hunk);
244
+ }
245
+ return renderUnifiedHunk(hunk);
246
+ }).join('');
247
+
248
+ mainEl.innerHTML = `
249
+ <div class="diff-file-card">
250
+ <div class="diff-file-card-head">
251
+ <span class="diff-card-title">${escapeHtml(file.newPath || file.oldPath)}</span>
252
+ <span class="diff-card-meta">${file.hunks.length} hunk${file.hunks.length === 1 ? '' : 's'} · +${file.additions} -${file.deletions}</span>
253
+ </div>
254
+ <div class="diff-hunks-body">
255
+ ${hunksHtml || '<div class="diff-empty-hunk">Binary file or metadata change</div>'}
256
+ </div>
257
+ </div>
258
+ `;
259
+ }
260
+
261
+ function renderUnifiedHunk(hunk) {
262
+ let oldLine = hunk.oldStart;
263
+ let newLine = hunk.newStart;
264
+
265
+ const linesHtml = hunk.lines.map((line) => {
266
+ let oldNum = '';
267
+ let newNum = '';
268
+
269
+ if (line.type === 'del') {
270
+ oldNum = oldLine++;
271
+ } else if (line.type === 'add') {
272
+ newNum = newLine++;
273
+ } else {
274
+ oldNum = oldLine++;
275
+ newNum = newLine++;
276
+ }
277
+
278
+ const sign = line.type === 'add' ? '+' : (line.type === 'del' ? '-' : ' ');
279
+
280
+ return `
281
+ <div class="diff-line line-${line.type}">
282
+ <span class="diff-ln old-ln">${oldNum}</span>
283
+ <span class="diff-ln new-ln">${newNum}</span>
284
+ <span class="diff-sign">${sign}</span>
285
+ <span class="diff-code">${escapeHtml(line.text)}</span>
286
+ </div>
287
+ `;
288
+ }).join('');
289
+
290
+ return `
291
+ <div class="diff-hunk">
292
+ <div class="diff-hunk-header">${escapeHtml(hunk.header)}</div>
293
+ ${linesHtml}
294
+ </div>
295
+ `;
296
+ }
297
+
298
+ function renderSplitHunk(hunk) {
299
+ let oldLine = hunk.oldStart;
300
+ let newLine = hunk.newStart;
301
+
302
+ const rows = [];
303
+ for (let i = 0; i < hunk.lines.length; i++) {
304
+ const line = hunk.lines[i];
305
+ if (line.type === 'del') {
306
+ const delNum = oldLine++;
307
+ // Check if next is add
308
+ const next = hunk.lines[i + 1];
309
+ if (next && next.type === 'add') {
310
+ const addNum = newLine++;
311
+ rows.push(`
312
+ <div class="split-diff-row">
313
+ <div class="split-pane line-del"><span class="diff-ln">${delNum}</span><span class="diff-code">-${escapeHtml(line.text)}</span></div>
314
+ <div class="split-pane line-add"><span class="diff-ln">${addNum}</span><span class="diff-code">+${escapeHtml(next.text)}</span></div>
315
+ </div>
316
+ `);
317
+ i++;
318
+ } else {
319
+ rows.push(`
320
+ <div class="split-diff-row">
321
+ <div class="split-pane line-del"><span class="diff-ln">${delNum}</span><span class="diff-code">-${escapeHtml(line.text)}</span></div>
322
+ <div class="split-pane line-empty"></div>
323
+ </div>
324
+ `);
325
+ }
326
+ } else if (line.type === 'add') {
327
+ const addNum = newLine++;
328
+ rows.push(`
329
+ <div class="split-diff-row">
330
+ <div class="split-pane line-empty"></div>
331
+ <div class="split-pane line-add"><span class="diff-ln">${addNum}</span><span class="diff-code">+${escapeHtml(line.text)}</span></div>
332
+ </div>
333
+ `);
334
+ } else {
335
+ const oNum = oldLine++;
336
+ const nNum = newLine++;
337
+ rows.push(`
338
+ <div class="split-diff-row">
339
+ <div class="split-pane line-context"><span class="diff-ln">${oNum}</span><span class="diff-code"> ${escapeHtml(line.text)}</span></div>
340
+ <div class="split-pane line-context"><span class="diff-ln">${nNum}</span><span class="diff-code"> ${escapeHtml(line.text)}</span></div>
341
+ </div>
342
+ `);
343
+ }
344
+ }
345
+
346
+ return `
347
+ <div class="diff-hunk">
348
+ <div class="diff-hunk-header">${escapeHtml(hunk.header)}</div>
349
+ <div class="split-diff-table">${rows.join('')}</div>
350
+ </div>
351
+ `;
352
+ }