claudeck 1.0.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 (157) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +233 -0
  3. package/cli.js +2 -0
  4. package/config/agent-chains.json +16 -0
  5. package/config/agent-dags.json +16 -0
  6. package/config/agents.json +46 -0
  7. package/config/bot-prompt.json +3 -0
  8. package/config/folders.json +66 -0
  9. package/config/prompts.json +92 -0
  10. package/config/repos.json +86 -0
  11. package/config/telegram-config.json +17 -0
  12. package/config/workflows.json +90 -0
  13. package/db.js +1198 -0
  14. package/package.json +55 -0
  15. package/plugins/claude-editor/client.css +171 -0
  16. package/plugins/claude-editor/client.js +183 -0
  17. package/plugins/event-stream/client.css +207 -0
  18. package/plugins/event-stream/client.js +271 -0
  19. package/plugins/linear/client.css +345 -0
  20. package/plugins/linear/client.js +380 -0
  21. package/plugins/linear/config.json +5 -0
  22. package/plugins/linear/server.js +312 -0
  23. package/plugins/repos/client.css +549 -0
  24. package/plugins/repos/client.js +663 -0
  25. package/plugins/repos/server.js +232 -0
  26. package/plugins/sudoku/client.css +196 -0
  27. package/plugins/sudoku/client.js +329 -0
  28. package/plugins/tasks/client.css +414 -0
  29. package/plugins/tasks/client.js +394 -0
  30. package/plugins/tasks/server.js +116 -0
  31. package/plugins/tic-tac-toe/client.css +167 -0
  32. package/plugins/tic-tac-toe/client.js +241 -0
  33. package/public/css/core/components.css +232 -0
  34. package/public/css/core/layout.css +330 -0
  35. package/public/css/core/print.css +18 -0
  36. package/public/css/core/reset.css +36 -0
  37. package/public/css/core/responsive.css +378 -0
  38. package/public/css/core/theme.css +116 -0
  39. package/public/css/core/variables.css +93 -0
  40. package/public/css/features/agent-monitor.css +297 -0
  41. package/public/css/features/agent-sidebar.css +525 -0
  42. package/public/css/features/agents.css +996 -0
  43. package/public/css/features/analytics.css +181 -0
  44. package/public/css/features/background-sessions.css +321 -0
  45. package/public/css/features/cost-dashboard.css +168 -0
  46. package/public/css/features/home.css +313 -0
  47. package/public/css/features/retro-terminal.css +88 -0
  48. package/public/css/features/telegram.css +127 -0
  49. package/public/css/features/tour.css +148 -0
  50. package/public/css/features/voice-input.css +60 -0
  51. package/public/css/features/welcome.css +241 -0
  52. package/public/css/panels/assistant-bot.css +442 -0
  53. package/public/css/panels/dev-docs.css +292 -0
  54. package/public/css/panels/file-explorer.css +322 -0
  55. package/public/css/panels/git-panel.css +221 -0
  56. package/public/css/panels/mcp-manager.css +199 -0
  57. package/public/css/panels/tips-feed.css +353 -0
  58. package/public/css/ui/commands.css +273 -0
  59. package/public/css/ui/context-gauge.css +76 -0
  60. package/public/css/ui/file-picker.css +69 -0
  61. package/public/css/ui/image-attachments.css +106 -0
  62. package/public/css/ui/messages.css +884 -0
  63. package/public/css/ui/modals.css +122 -0
  64. package/public/css/ui/parallel.css +217 -0
  65. package/public/css/ui/permissions.css +110 -0
  66. package/public/css/ui/right-panel.css +481 -0
  67. package/public/css/ui/sessions.css +689 -0
  68. package/public/css/ui/status-bar.css +425 -0
  69. package/public/css/ui/toolbox.css +206 -0
  70. package/public/data/tips.json +218 -0
  71. package/public/icons/favicon.png +0 -0
  72. package/public/icons/icon-192.png +0 -0
  73. package/public/icons/icon-512.png +0 -0
  74. package/public/icons/whaly.png +0 -0
  75. package/public/index.html +1140 -0
  76. package/public/js/core/api.js +591 -0
  77. package/public/js/core/constants.js +3 -0
  78. package/public/js/core/dom.js +270 -0
  79. package/public/js/core/events.js +10 -0
  80. package/public/js/core/plugin-loader.js +153 -0
  81. package/public/js/core/store.js +39 -0
  82. package/public/js/core/utils.js +25 -0
  83. package/public/js/core/ws.js +64 -0
  84. package/public/js/features/agent-monitor.js +222 -0
  85. package/public/js/features/agents.js +1209 -0
  86. package/public/js/features/analytics.js +397 -0
  87. package/public/js/features/attachments.js +251 -0
  88. package/public/js/features/background-sessions.js +475 -0
  89. package/public/js/features/chat.js +589 -0
  90. package/public/js/features/cost-dashboard.js +152 -0
  91. package/public/js/features/dag-editor.js +399 -0
  92. package/public/js/features/easter-egg.js +46 -0
  93. package/public/js/features/home.js +270 -0
  94. package/public/js/features/projects.js +372 -0
  95. package/public/js/features/prompts.js +228 -0
  96. package/public/js/features/sessions.js +332 -0
  97. package/public/js/features/telegram.js +131 -0
  98. package/public/js/features/tour.js +210 -0
  99. package/public/js/features/voice-input.js +185 -0
  100. package/public/js/features/welcome.js +43 -0
  101. package/public/js/features/workflows.js +277 -0
  102. package/public/js/main.js +51 -0
  103. package/public/js/panels/assistant-bot.js +445 -0
  104. package/public/js/panels/dev-docs.js +380 -0
  105. package/public/js/panels/file-explorer.js +486 -0
  106. package/public/js/panels/git-panel.js +285 -0
  107. package/public/js/panels/mcp-manager.js +311 -0
  108. package/public/js/panels/tips-feed.js +303 -0
  109. package/public/js/ui/commands.js +114 -0
  110. package/public/js/ui/context-gauge.js +100 -0
  111. package/public/js/ui/diff.js +124 -0
  112. package/public/js/ui/disabled-tools.js +36 -0
  113. package/public/js/ui/export.js +74 -0
  114. package/public/js/ui/formatting.js +206 -0
  115. package/public/js/ui/header-dropdowns.js +72 -0
  116. package/public/js/ui/input-meta.js +71 -0
  117. package/public/js/ui/max-turns.js +21 -0
  118. package/public/js/ui/messages.js +387 -0
  119. package/public/js/ui/model-selector.js +20 -0
  120. package/public/js/ui/notifications.js +232 -0
  121. package/public/js/ui/parallel.js +176 -0
  122. package/public/js/ui/permissions.js +168 -0
  123. package/public/js/ui/right-panel.js +173 -0
  124. package/public/js/ui/shortcuts.js +143 -0
  125. package/public/js/ui/sidebar-toggle.js +29 -0
  126. package/public/js/ui/status-bar.js +172 -0
  127. package/public/js/ui/tab-sdk.js +623 -0
  128. package/public/js/ui/theme.js +38 -0
  129. package/public/manifest.json +13 -0
  130. package/public/offline.html +190 -0
  131. package/public/style.css +42 -0
  132. package/public/sw.js +91 -0
  133. package/server/agent-loop.js +385 -0
  134. package/server/dag-executor.js +265 -0
  135. package/server/orchestrator.js +514 -0
  136. package/server/paths.js +61 -0
  137. package/server/plugin-mount.js +56 -0
  138. package/server/push-sender.js +31 -0
  139. package/server/routes/agents.js +294 -0
  140. package/server/routes/bot.js +45 -0
  141. package/server/routes/exec.js +35 -0
  142. package/server/routes/files.js +218 -0
  143. package/server/routes/mcp.js +82 -0
  144. package/server/routes/messages.js +36 -0
  145. package/server/routes/notifications.js +37 -0
  146. package/server/routes/projects.js +207 -0
  147. package/server/routes/prompts.js +53 -0
  148. package/server/routes/sessions.js +103 -0
  149. package/server/routes/stats.js +143 -0
  150. package/server/routes/telegram.js +71 -0
  151. package/server/routes/tips.js +135 -0
  152. package/server/routes/workflows.js +81 -0
  153. package/server/summarizer.js +55 -0
  154. package/server/telegram-poller.js +205 -0
  155. package/server/telegram-sender.js +304 -0
  156. package/server/ws-handler.js +926 -0
  157. package/server.js +179 -0
@@ -0,0 +1,397 @@
1
+ // Analytics dashboard — renders inline on the home page
2
+ import { $ } from '../core/dom.js';
3
+ import { escapeHtml } from '../core/utils.js';
4
+ import { getState } from '../core/store.js';
5
+ import * as api from '../core/api.js';
6
+
7
+ function formatCost(n) {
8
+ if (n >= 100) return '$' + n.toFixed(0);
9
+ if (n >= 1) return '$' + n.toFixed(2);
10
+ return '$' + n.toFixed(4);
11
+ }
12
+
13
+ function formatNumber(n) {
14
+ if (n >= 1_000_000) return (n / 1_000_000).toFixed(1) + 'M';
15
+ if (n >= 1_000) return (n / 1_000).toFixed(1) + 'k';
16
+ return String(n);
17
+ }
18
+
19
+ function pct(value, max) {
20
+ return max > 0 ? Math.round((value / max) * 100) : 0;
21
+ }
22
+
23
+ function renderBarChart(container, items, labelKey, valueKey, formatFn = String) {
24
+ container.innerHTML = '';
25
+ if (!items || items.length === 0) {
26
+ container.innerHTML = '<div class="analytics-empty">No data</div>';
27
+ return;
28
+ }
29
+ const maxVal = Math.max(...items.map(d => d[valueKey]), 0.001);
30
+ for (const item of items) {
31
+ const p = pct(item[valueKey], maxVal);
32
+ const row = document.createElement('div');
33
+ row.className = 'cost-chart-row';
34
+ row.innerHTML = `
35
+ <span class="cost-chart-label">${escapeHtml(String(item[labelKey]))}</span>
36
+ <div class="cost-chart-bar-bg"><div class="cost-chart-bar" style="width:${p}%"></div></div>
37
+ <span class="cost-chart-value">${formatFn(item[valueKey])}</span>
38
+ `;
39
+ container.appendChild(row);
40
+ }
41
+ }
42
+
43
+ function sortTable(tbody, colIdx, numeric) {
44
+ const current = tbody.dataset.sortCol === String(colIdx) && tbody.dataset.sortDir === 'desc' ? 'asc' : 'desc';
45
+ tbody.dataset.sortCol = colIdx;
46
+ tbody.dataset.sortDir = current;
47
+ const rows = [...tbody.querySelectorAll('tr')];
48
+ rows.sort((a, b) => {
49
+ const aVal = a.children[colIdx].textContent;
50
+ const bVal = b.children[colIdx].textContent;
51
+ if (!numeric) return current === 'asc' ? aVal.localeCompare(bVal) : bVal.localeCompare(aVal);
52
+ const aNum = parseFloat(aVal.replace(/[$,%km]/g, '')) || 0;
53
+ const bNum = parseFloat(bVal.replace(/[$,%km]/g, '')) || 0;
54
+ return current === 'asc' ? aNum - bNum : bNum - aNum;
55
+ });
56
+ tbody.innerHTML = '';
57
+ rows.forEach(r => tbody.appendChild(r));
58
+ }
59
+
60
+ function renderTable(container, columns, rows) {
61
+ if (!rows || rows.length === 0) {
62
+ container.innerHTML += '<div class="analytics-empty">No data</div>';
63
+ return;
64
+ }
65
+ const table = document.createElement('table');
66
+ table.className = 'cost-table';
67
+
68
+ const thead = document.createElement('thead');
69
+ const headRow = document.createElement('tr');
70
+ columns.forEach((col, idx) => {
71
+ const th = document.createElement('th');
72
+ th.textContent = col.label;
73
+ th.onclick = () => sortTable(tbody, idx, col.numeric);
74
+ headRow.appendChild(th);
75
+ });
76
+ thead.appendChild(headRow);
77
+ table.appendChild(thead);
78
+
79
+ const tbody = document.createElement('tbody');
80
+ for (const row of rows) {
81
+ const tr = document.createElement('tr');
82
+ columns.forEach(col => {
83
+ const td = document.createElement('td');
84
+ const val = row[col.key];
85
+ td.textContent = col.format ? col.format(val) : (val ?? '');
86
+ if (col.title && row[col.title]) td.title = row[col.title];
87
+ tr.appendChild(td);
88
+ });
89
+ tbody.appendChild(tr);
90
+ }
91
+ table.appendChild(tbody);
92
+
93
+ const wrap = document.createElement('div');
94
+ wrap.className = 'cost-table-container';
95
+ wrap.appendChild(table);
96
+ container.appendChild(wrap);
97
+ }
98
+
99
+ function section(title) {
100
+ const div = document.createElement('div');
101
+ div.className = 'analytics-section';
102
+ const h4 = document.createElement('h4');
103
+ h4.textContent = title;
104
+ div.appendChild(h4);
105
+ return div;
106
+ }
107
+
108
+ function renderAnalytics(data, el) {
109
+ el.innerHTML = '';
110
+
111
+ // 1. Overview cards
112
+ const o = data.overview;
113
+ const cardsDiv = document.createElement('div');
114
+ cardsDiv.className = 'analytics-cards';
115
+ cardsDiv.innerHTML = `
116
+ <div class="cost-card">
117
+ <div class="cost-card-label">Total Cost</div>
118
+ <div class="cost-card-value">${formatCost(o.totalCost)}</div>
119
+ </div>
120
+ <div class="cost-card">
121
+ <div class="cost-card-label">Sessions</div>
122
+ <div class="cost-card-value">${formatNumber(o.sessions)}</div>
123
+ </div>
124
+ <div class="cost-card">
125
+ <div class="cost-card-label">Queries</div>
126
+ <div class="cost-card-value">${formatNumber(o.queries)}</div>
127
+ </div>
128
+ <div class="cost-card">
129
+ <div class="cost-card-label">Turns</div>
130
+ <div class="cost-card-value">${formatNumber(o.totalTurns)}</div>
131
+ </div>
132
+ <div class="cost-card">
133
+ <div class="cost-card-label">Output Tokens</div>
134
+ <div class="cost-card-value">${formatNumber(o.totalOutputTokens)}</div>
135
+ </div>
136
+ <div class="cost-card">
137
+ <div class="cost-card-label">Errors</div>
138
+ <div class="cost-card-value${o.errorRate > 5 ? ' analytics-error-value' : ''}">${data.errorCategories ? formatNumber(data.errorCategories.reduce((s, c) => s + c.count, 0)) : '0'} <span style="font-size:10px;color:var(--text-dim)">(${o.errorRate.toFixed(1)}%)</span></div>
139
+ ${data.errorCategories && data.errorCategories.length > 0 ? `<div style="font-size:9px;color:var(--text-dim);margin-top:2px">Top: ${escapeHtml(data.errorCategories[0].category)}</div>` : ''}
140
+ </div>
141
+ `;
142
+ el.appendChild(cardsDiv);
143
+
144
+ // 2. Daily Activity
145
+ const dailySec = section('Daily Activity (Last 30 Days)');
146
+ const dailyChart = document.createElement('div');
147
+ dailyChart.className = 'cost-chart';
148
+ renderBarChart(dailyChart, data.dailyBreakdown, 'date', 'cost', v => formatCost(v));
149
+ dailyChart.querySelectorAll('.cost-chart-label').forEach(lbl => {
150
+ lbl.textContent = lbl.textContent.slice(5);
151
+ });
152
+ dailySec.appendChild(dailyChart);
153
+ el.appendChild(dailySec);
154
+
155
+ // 3. Hourly Activity
156
+ const hourlySec = section('Activity by Hour');
157
+ const hourlyChart = document.createElement('div');
158
+ hourlyChart.className = 'cost-chart';
159
+ const hourMap = new Map(data.hourlyActivity.map(h => [h.hour, h]));
160
+ const allHours = [];
161
+ for (let h = 0; h < 24; h++) {
162
+ allHours.push(hourMap.get(h) || { hour: h, queries: 0, cost: 0 });
163
+ }
164
+ renderBarChart(hourlyChart, allHours, 'hour', 'queries', v => String(v));
165
+ hourlyChart.querySelectorAll('.cost-chart-label').forEach(lbl => {
166
+ const h = parseInt(lbl.textContent);
167
+ lbl.textContent = `${h.toString().padStart(2, '0')}:00`;
168
+ });
169
+ hourlySec.appendChild(hourlyChart);
170
+ el.appendChild(hourlySec);
171
+
172
+ // 4. Project Breakdown
173
+ if (data.projectBreakdown.length > 1) {
174
+ const projSec = section('Project Breakdown');
175
+ renderTable(projSec, [
176
+ { label: 'Project', key: 'name' },
177
+ { label: 'Sessions', key: 'sessions', numeric: true },
178
+ { label: 'Queries', key: 'queries', numeric: true },
179
+ { label: 'Cost', key: 'totalCost', numeric: true, format: formatCost },
180
+ { label: 'Avg Cost', key: 'avgCost', numeric: true, format: formatCost },
181
+ { label: 'Avg Turns', key: 'avgTurns', numeric: true, format: v => String(Math.round(v)) },
182
+ ], data.projectBreakdown);
183
+ el.appendChild(projSec);
184
+ }
185
+
186
+ // 5. Tool Usage
187
+ if (data.toolUsage.length > 0) {
188
+ const toolSec = section('Tool Usage');
189
+ const toolChart = document.createElement('div');
190
+ toolChart.className = 'cost-chart';
191
+ const errorMap = new Map((data.toolErrors || []).map(e => [e.name, e]));
192
+ const maxCount = Math.max(...data.toolUsage.map(t => t.count), 1);
193
+ for (const tool of data.toolUsage) {
194
+ const p = pct(tool.count, maxCount);
195
+ const row = document.createElement('div');
196
+ row.className = 'cost-chart-row';
197
+ const err = errorMap.get(tool.name);
198
+ const errHtml = err
199
+ ? ` <span class="analytics-error-badge">${err.errors} err (${err.errorRate.toFixed(0)}%)</span>`
200
+ : '';
201
+ row.innerHTML = `
202
+ <span class="cost-chart-label">${escapeHtml(tool.name)}</span>
203
+ <div class="cost-chart-bar-bg"><div class="cost-chart-bar" style="width:${p}%"></div></div>
204
+ <span class="cost-chart-value">${tool.count}${errHtml}</span>
205
+ `;
206
+ toolChart.appendChild(row);
207
+ }
208
+ toolSec.appendChild(toolChart);
209
+ el.appendChild(toolSec);
210
+ }
211
+
212
+ // 5b. Error Categories
213
+ if (data.errorCategories && data.errorCategories.length > 0) {
214
+ const errCatSec = section('Error Categories');
215
+ const errCatChart = document.createElement('div');
216
+ errCatChart.className = 'cost-chart analytics-error-bar';
217
+ const totalErrors = data.errorCategories.reduce((s, c) => s + c.count, 0);
218
+ const maxCat = Math.max(...data.errorCategories.map(c => c.count), 1);
219
+ for (const cat of data.errorCategories) {
220
+ const p = pct(cat.count, maxCat);
221
+ const catPct = totalErrors > 0 ? (cat.count / totalErrors * 100).toFixed(1) : '0';
222
+ const row = document.createElement('div');
223
+ row.className = 'cost-chart-row';
224
+ row.innerHTML = `
225
+ <span class="cost-chart-label analytics-error-category-label">${escapeHtml(cat.category)}</span>
226
+ <div class="cost-chart-bar-bg"><div class="cost-chart-bar" style="width:${p}%"></div></div>
227
+ <span class="cost-chart-value">${cat.count} (${catPct}%)</span>
228
+ `;
229
+ errCatChart.appendChild(row);
230
+ }
231
+ errCatSec.appendChild(errCatChart);
232
+ el.appendChild(errCatSec);
233
+ }
234
+
235
+ // 5c. Error Timeline
236
+ if (data.errorTimeline && data.errorTimeline.length > 0) {
237
+ const errTimeSec = section('Error Timeline (Last 30 Days)');
238
+ const errTimeChart = document.createElement('div');
239
+ errTimeChart.className = 'cost-chart analytics-error-bar';
240
+ renderBarChart(errTimeChart, data.errorTimeline, 'date', 'errors', String);
241
+ errTimeChart.querySelectorAll('.cost-chart-label').forEach(lbl => {
242
+ lbl.textContent = lbl.textContent.slice(5); // MM-DD
243
+ });
244
+ errTimeSec.appendChild(errTimeChart);
245
+ el.appendChild(errTimeSec);
246
+ }
247
+
248
+ // 5d. Top Failing Tools
249
+ if (data.errorsByTool && data.errorsByTool.length > 0) {
250
+ const errToolSec = section('Top Failing Tools');
251
+ const errToolChart = document.createElement('div');
252
+ errToolChart.className = 'cost-chart analytics-error-bar';
253
+ const toolMap = new Map();
254
+ for (const row of data.errorsByTool) {
255
+ if (!toolMap.has(row.tool)) toolMap.set(row.tool, { tool: row.tool, errors: 0, categories: [] });
256
+ const entry = toolMap.get(row.tool);
257
+ entry.errors += row.errors;
258
+ if (entry.categories.length < 2) entry.categories.push(row.category);
259
+ }
260
+ const toolList = [...toolMap.values()].sort((a, b) => b.errors - a.errors);
261
+ const maxToolErr = Math.max(...toolList.map(t => t.errors), 1);
262
+ for (const tool of toolList) {
263
+ const p = pct(tool.errors, maxToolErr);
264
+ const badges = tool.categories.map(c => `<span class="analytics-error-category-badge">${escapeHtml(c)}</span>`).join('');
265
+ const row = document.createElement('div');
266
+ row.className = 'cost-chart-row';
267
+ row.innerHTML = `
268
+ <span class="cost-chart-label analytics-error-category-label">${escapeHtml(tool.tool)}</span>
269
+ <div class="cost-chart-bar-bg"><div class="cost-chart-bar" style="width:${p}%"></div></div>
270
+ <span class="cost-chart-value">${tool.errors}${badges}</span>
271
+ `;
272
+ errToolChart.appendChild(row);
273
+ }
274
+ errToolSec.appendChild(errToolChart);
275
+ el.appendChild(errToolSec);
276
+ }
277
+
278
+ // 5e. Recent Errors
279
+ if (data.recentErrors && data.recentErrors.length > 0) {
280
+ const errRecSec = section('Recent Errors');
281
+ const errList = document.createElement('div');
282
+ errList.className = 'analytics-error-list';
283
+ for (const err of data.recentErrors) {
284
+ const item = document.createElement('div');
285
+ item.className = 'analytics-error-item';
286
+ const ts = new Date(err.timestamp * 1000);
287
+ const timeStr = ts.toLocaleDateString(undefined, { month: 'short', day: 'numeric' }) + ' ' + ts.toLocaleTimeString(undefined, { hour: '2-digit', minute: '2-digit' });
288
+ item.innerHTML = `
289
+ <div class="analytics-error-item-header">
290
+ <span class="analytics-error-tool">${escapeHtml(err.tool)}</span>
291
+ ${err.session_title ? `<span style="font-size:10px;color:var(--text-dim)">${escapeHtml(err.session_title)}</span>` : ''}
292
+ <span class="analytics-error-time">${timeStr}</span>
293
+ </div>
294
+ <div class="analytics-error-preview">${escapeHtml(err.preview || '')}</div>
295
+ `;
296
+ item.addEventListener('click', () => {
297
+ const existing = item.querySelector('.analytics-error-full');
298
+ if (existing) { existing.remove(); return; }
299
+ const full = document.createElement('div');
300
+ full.className = 'analytics-error-full';
301
+ full.textContent = err.full_content || err.preview || '';
302
+ item.appendChild(full);
303
+ });
304
+ errList.appendChild(item);
305
+ }
306
+ errRecSec.appendChild(errList);
307
+ el.appendChild(errRecSec);
308
+ }
309
+
310
+ // 6. Top Sessions
311
+ if (data.topSessions.length > 0) {
312
+ const sesSec = section('Top Sessions by Cost');
313
+ renderTable(sesSec, [
314
+ { label: 'Session', key: 'title', format: v => v || 'Untitled' },
315
+ { label: 'Project', key: 'project' },
316
+ { label: 'Cost', key: 'cost', numeric: true, format: formatCost },
317
+ { label: 'Turns', key: 'turns', numeric: true },
318
+ { label: 'Duration', key: 'duration_min', numeric: true, format: v => v > 0 ? Math.round(v) + 'm' : '-' },
319
+ ], data.topSessions);
320
+ el.appendChild(sesSec);
321
+ }
322
+
323
+ // 7. Session Depth
324
+ if (data.sessionDepth.length > 0) {
325
+ const depthSec = section('Session Depth');
326
+ const depthChart = document.createElement('div');
327
+ depthChart.className = 'cost-chart';
328
+ renderBarChart(depthChart, data.sessionDepth, 'bucket', 'count', String);
329
+ depthSec.appendChild(depthChart);
330
+ el.appendChild(depthSec);
331
+ }
332
+
333
+ // 8. Message Length Distribution
334
+ if (data.msgLength.length > 0) {
335
+ const msgSec = section('Message Length Distribution');
336
+ const msgChart = document.createElement('div');
337
+ msgChart.className = 'cost-chart';
338
+ renderBarChart(msgChart, data.msgLength, 'bucket', 'count', String);
339
+ msgSec.appendChild(msgChart);
340
+ el.appendChild(msgSec);
341
+ }
342
+
343
+ // 9. Top Bash Commands
344
+ if (data.topBashCommands.length > 0) {
345
+ const bashSec = section('Top Bash Commands');
346
+ renderTable(bashSec, [
347
+ { label: 'Command', key: 'command' },
348
+ { label: 'Count', key: 'count', numeric: true },
349
+ ], data.topBashCommands);
350
+ el.appendChild(bashSec);
351
+ }
352
+
353
+ // 10. Top Files
354
+ if (data.topFiles.length > 0) {
355
+ const filesSec = section('Top Files');
356
+ renderTable(filesSec, [
357
+ { label: 'Path', key: 'path', format: v => v ? v.split('/').slice(-2).join('/') : '', title: 'path' },
358
+ { label: 'Tool', key: 'tool' },
359
+ { label: 'Count', key: 'count', numeric: true },
360
+ ], data.topFiles);
361
+ el.appendChild(filesSec);
362
+ }
363
+ }
364
+
365
+ // ── Home page analytics ────────────────────────────────
366
+ const homeAnalyticsFilter = document.getElementById('home-analytics-filter');
367
+ const homeAnalyticsContent = document.getElementById('home-analytics-content');
368
+
369
+ async function loadAnalyticsData(projectPath) {
370
+ homeAnalyticsContent.innerHTML = '<div class="analytics-loading">Loading analytics...</div>';
371
+ try {
372
+ const data = await api.fetchAnalytics(projectPath || undefined);
373
+ renderAnalytics(data, homeAnalyticsContent);
374
+ } catch (err) {
375
+ homeAnalyticsContent.innerHTML = `<div class="analytics-empty">Failed to load analytics: ${escapeHtml(err.message)}</div>`;
376
+ }
377
+ }
378
+
379
+ export async function loadHomeAnalytics() {
380
+ // Populate project filter — use store data or fetch directly
381
+ let projects = getState('projectsData');
382
+ if (!projects || projects.length === 0) {
383
+ try { projects = await api.fetchProjects(); } catch { projects = []; }
384
+ }
385
+ homeAnalyticsFilter.innerHTML = '<option value="">All Projects</option>';
386
+ for (const p of projects) {
387
+ const opt = document.createElement('option');
388
+ opt.value = p.path;
389
+ opt.textContent = p.name;
390
+ homeAnalyticsFilter.appendChild(opt);
391
+ }
392
+ await loadAnalyticsData(homeAnalyticsFilter.value);
393
+ }
394
+
395
+ homeAnalyticsFilter.addEventListener('change', () => {
396
+ loadAnalyticsData(homeAnalyticsFilter.value);
397
+ });
@@ -0,0 +1,251 @@
1
+ // File & image attachments
2
+ import { $ } from '../core/dom.js';
3
+ import { getState, setState } from '../core/store.js';
4
+ import * as api from '../core/api.js';
5
+ import { registerCommand } from '../ui/commands.js';
6
+
7
+ const SUPPORTED_IMAGE_TYPES = ["image/png", "image/jpeg", "image/gif", "image/webp"];
8
+ const MAX_IMAGE_SIZE = 5 * 1024 * 1024; // 5MB
9
+
10
+ // ── Badge ────────────────────────────────────────────────
11
+ export function updateAttachmentBadge() {
12
+ const attachedFiles = getState("attachedFiles");
13
+ const imageAttachments = getState("imageAttachments");
14
+ const total = attachedFiles.length + imageAttachments.length;
15
+ if (total > 0) {
16
+ $.attachBadge.textContent = total;
17
+ $.attachBadge.classList.remove("hidden");
18
+ } else {
19
+ $.attachBadge.classList.add("hidden");
20
+ }
21
+ }
22
+
23
+ // ── File picker (existing) ───────────────────────────────
24
+ export async function openFilePicker() {
25
+ const cwd = $.projectSelect.value;
26
+ if (!cwd) return;
27
+ $.fpModal.classList.remove("hidden");
28
+ $.fpSearch.value = "";
29
+ $.fpSearch.focus();
30
+
31
+ try {
32
+ const files = await api.fetchFiles(cwd);
33
+ setState("allProjectFiles", files);
34
+ renderFilePicker("");
35
+ } catch (err) {
36
+ console.error("Failed to load files:", err);
37
+ setState("allProjectFiles", []);
38
+ renderFilePicker("");
39
+ }
40
+ }
41
+
42
+ export function renderFilePicker(filter) {
43
+ $.fpList.innerHTML = "";
44
+ const lower = filter.toLowerCase();
45
+ const allProjectFiles = getState("allProjectFiles");
46
+ const attachedFiles = getState("attachedFiles");
47
+ const filtered = lower
48
+ ? allProjectFiles.filter((f) => f.toLowerCase().includes(lower))
49
+ : allProjectFiles;
50
+
51
+ for (const filePath of filtered.slice(0, 200)) {
52
+ const item = document.createElement("div");
53
+ item.className = "file-picker-item";
54
+ const isSelected = attachedFiles.some((f) => f.path === filePath);
55
+ if (isSelected) item.classList.add("selected");
56
+ item.textContent = filePath;
57
+ item.addEventListener("click", () => toggleFileAttachment(filePath, item));
58
+ $.fpList.appendChild(item);
59
+ }
60
+ }
61
+
62
+ async function toggleFileAttachment(filePath, itemEl) {
63
+ const attachedFiles = [...getState("attachedFiles")];
64
+ const idx = attachedFiles.findIndex((f) => f.path === filePath);
65
+ if (idx >= 0) {
66
+ attachedFiles.splice(idx, 1);
67
+ setState("attachedFiles", attachedFiles);
68
+ itemEl.classList.remove("selected");
69
+ } else {
70
+ try {
71
+ const cwd = $.projectSelect.value;
72
+ const data = await api.fetchFileContent(cwd, filePath);
73
+ attachedFiles.push({ path: filePath, content: data.content });
74
+ setState("attachedFiles", attachedFiles);
75
+ itemEl.classList.add("selected");
76
+ } catch (err) {
77
+ console.error("Failed to read file:", err);
78
+ return;
79
+ }
80
+ }
81
+ $.fpCount.textContent = `${attachedFiles.length} file${attachedFiles.length !== 1 ? "s" : ""} selected`;
82
+ updateAttachmentBadge();
83
+ }
84
+
85
+ function closeFilePicker() {
86
+ $.fpModal.classList.add("hidden");
87
+ }
88
+
89
+ // ── Image attachments ────────────────────────────────────
90
+ export function addImageAttachment(file) {
91
+ if (!SUPPORTED_IMAGE_TYPES.includes(file.type)) {
92
+ showImageError(`Unsupported image type: ${file.type}. Use PNG, JPEG, GIF, or WebP.`);
93
+ return;
94
+ }
95
+ if (file.size > MAX_IMAGE_SIZE) {
96
+ showImageError(`Image too large (${(file.size / 1024 / 1024).toFixed(1)}MB). Max 5MB.`);
97
+ return;
98
+ }
99
+
100
+ const reader = new FileReader();
101
+ reader.onload = () => {
102
+ const base64 = reader.result.split(",")[1];
103
+ const images = [...getState("imageAttachments")];
104
+ images.push({ name: file.name, data: base64, mimeType: file.type });
105
+ setState("imageAttachments", images);
106
+ renderImagePreview();
107
+ updateAttachmentBadge();
108
+ };
109
+ reader.readAsDataURL(file);
110
+ }
111
+
112
+ export function removeImageAttachment(index) {
113
+ const images = [...getState("imageAttachments")];
114
+ images.splice(index, 1);
115
+ setState("imageAttachments", images);
116
+ renderImagePreview();
117
+ updateAttachmentBadge();
118
+ }
119
+
120
+ export function getImageAttachments() {
121
+ return getState("imageAttachments");
122
+ }
123
+
124
+ export function clearImageAttachments() {
125
+ setState("imageAttachments", []);
126
+ renderImagePreview();
127
+ updateAttachmentBadge();
128
+ }
129
+
130
+ function renderImagePreview() {
131
+ const strip = $.imagePreviewStrip;
132
+ const images = getState("imageAttachments");
133
+ strip.innerHTML = "";
134
+
135
+ if (images.length === 0) {
136
+ strip.classList.add("hidden");
137
+ return;
138
+ }
139
+
140
+ strip.classList.remove("hidden");
141
+ images.forEach((img, i) => {
142
+ const item = document.createElement("div");
143
+ item.className = "image-preview-item";
144
+
145
+ const imgEl = document.createElement("img");
146
+ imgEl.src = `data:${img.mimeType};base64,${img.data}`;
147
+ imgEl.alt = img.name;
148
+ imgEl.title = img.name;
149
+
150
+ const removeBtn = document.createElement("button");
151
+ removeBtn.className = "image-preview-remove";
152
+ removeBtn.textContent = "\u00d7";
153
+ removeBtn.addEventListener("click", (e) => {
154
+ e.stopPropagation();
155
+ removeImageAttachment(i);
156
+ });
157
+
158
+ item.appendChild(imgEl);
159
+ item.appendChild(removeBtn);
160
+ strip.appendChild(item);
161
+ });
162
+ }
163
+
164
+ function showImageError(message) {
165
+ // Use toast container if available
166
+ const container = document.getElementById("toast-container");
167
+ if (container) {
168
+ const toast = document.createElement("div");
169
+ toast.className = "toast error";
170
+ toast.textContent = message;
171
+ container.appendChild(toast);
172
+ setTimeout(() => toast.remove(), 4000);
173
+ } else {
174
+ alert(message);
175
+ }
176
+ }
177
+
178
+ // ── Event listeners ──────────────────────────────────────
179
+
180
+ // File picker buttons
181
+ $.attachBtn.addEventListener("click", openFilePicker);
182
+ document.getElementById("fp-modal-close").addEventListener("click", closeFilePicker);
183
+ document.getElementById("fp-done-btn").addEventListener("click", closeFilePicker);
184
+ $.fpModal.addEventListener("click", (e) => {
185
+ if (e.target === $.fpModal) closeFilePicker();
186
+ });
187
+ $.fpSearch.addEventListener("input", () => {
188
+ renderFilePicker($.fpSearch.value.trim());
189
+ });
190
+
191
+ // Image button → open file picker
192
+ $.imageBtn.addEventListener("click", () => {
193
+ $.imageFileInput.click();
194
+ });
195
+
196
+ // Hidden file input change
197
+ $.imageFileInput.addEventListener("change", () => {
198
+ for (const file of $.imageFileInput.files) {
199
+ addImageAttachment(file);
200
+ }
201
+ $.imageFileInput.value = "";
202
+ });
203
+
204
+ // Paste handler — detect images in clipboard
205
+ document.addEventListener("paste", (e) => {
206
+ // Only handle when focus is in the chat input area
207
+ const active = document.activeElement;
208
+ if (active !== $.messageInput && !$.messageInput.contains(active)) return;
209
+
210
+ const items = e.clipboardData?.items;
211
+ if (!items) return;
212
+
213
+ for (const item of items) {
214
+ if (item.kind === "file" && SUPPORTED_IMAGE_TYPES.includes(item.type)) {
215
+ e.preventDefault();
216
+ addImageAttachment(item.getAsFile());
217
+ }
218
+ }
219
+ });
220
+
221
+ // Drag-and-drop on message input
222
+ $.messageInput.addEventListener("dragover", (e) => {
223
+ if ([...e.dataTransfer.types].includes("Files")) {
224
+ e.preventDefault();
225
+ $.messageInput.classList.add("drag-highlight");
226
+ }
227
+ });
228
+
229
+ $.messageInput.addEventListener("dragleave", () => {
230
+ $.messageInput.classList.remove("drag-highlight");
231
+ });
232
+
233
+ $.messageInput.addEventListener("drop", (e) => {
234
+ $.messageInput.classList.remove("drag-highlight");
235
+ if (!e.dataTransfer.files.length) return;
236
+ e.preventDefault();
237
+ for (const file of e.dataTransfer.files) {
238
+ if (SUPPORTED_IMAGE_TYPES.includes(file.type)) {
239
+ addImageAttachment(file);
240
+ }
241
+ }
242
+ });
243
+
244
+ // ── Commands ─────────────────────────────────────────────
245
+ registerCommand("attach", {
246
+ category: "app",
247
+ description: "Attach files to next message",
248
+ execute() {
249
+ openFilePicker();
250
+ },
251
+ });