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,270 @@
1
+ // Home page — AI Activity contribution grid + summary cards
2
+ import { $ } from '../core/dom.js';
3
+ import { on as onState, setState, getState } from '../core/store.js';
4
+ import { fetchHomeData } from '../core/api.js';
5
+ import { loadHomeAnalytics } from './analytics.js';
6
+
7
+ const MONTHS = ['Jan','Feb','Mar','Apr','May','Jun','Jul','Aug','Sep','Oct','Nov','Dec'];
8
+
9
+ // ── View switching ─────────────────────────────────────
10
+ const chatAreaMain = document.querySelector('.chat-area-main');
11
+
12
+ // ── Data loading ───────────────────────────────────────
13
+ let loaded = false;
14
+
15
+ async function loadHome() {
16
+ loaded = false;
17
+ try {
18
+ const data = await fetchHomeData();
19
+ renderGrid(data.yearlyActivity);
20
+ renderCards(data.yearlyActivity, data.overview);
21
+ loadHomeAnalytics();
22
+ loaded = true;
23
+ } catch (err) {
24
+ console.error('Failed to load home data:', err);
25
+ }
26
+ }
27
+
28
+ onState('view', (view) => {
29
+ $.homePage.classList.toggle('hidden', view !== 'home');
30
+ chatAreaMain.classList.toggle('hidden', view !== 'chat');
31
+ $.homeBtn.classList.toggle('active', view === 'home');
32
+ if (view === 'home') loadHome();
33
+ });
34
+
35
+ onState('sessionId', (id) => {
36
+ if (id) setState('view', 'chat');
37
+ });
38
+
39
+ $.homeBtn.addEventListener('click', () => {
40
+ setState('view', 'home');
41
+ setState('sessionId', null);
42
+ $.projectSelect.value = '';
43
+ localStorage.removeItem('claudeck-cwd');
44
+ $.sessionList.innerHTML = '';
45
+ });
46
+
47
+ // ── Grid rendering ─────────────────────────────────────
48
+ function renderGrid(activity) {
49
+ const grid = document.getElementById('home-activity-grid');
50
+ const monthsRow = document.getElementById('home-grid-months');
51
+ const yearLabel = document.getElementById('home-year-label');
52
+
53
+ // Build date map
54
+ const dateMap = {};
55
+ for (const row of activity) {
56
+ dateMap[row.date] = row;
57
+ }
58
+
59
+ // Today and start date (364 days ago, aligned to Sunday)
60
+ const today = new Date();
61
+ today.setHours(0, 0, 0, 0);
62
+ const start = new Date(today);
63
+ start.setDate(start.getDate() - 364);
64
+ // Align to Sunday (day 0)
65
+ const dayOfWeek = start.getDay();
66
+ start.setDate(start.getDate() - dayOfWeek);
67
+
68
+ yearLabel.textContent = `${start.getFullYear()} – ${today.getFullYear()}`;
69
+
70
+ // Collect all cells with their scores
71
+ const cells = [];
72
+ const scores = [];
73
+ const d = new Date(start);
74
+
75
+ while (d <= today) {
76
+ const key = localDateStr(d);
77
+ const row = dateMap[key];
78
+ let score = 0;
79
+ if (row) {
80
+ score = row.sessions + row.queries + (row.cost * 50) + ((row.input_tokens + row.output_tokens) / 5000);
81
+ }
82
+ cells.push({ date: key, row, score });
83
+ if (score > 0) scores.push(score);
84
+ d.setDate(d.getDate() + 1);
85
+ }
86
+
87
+ // Quartile thresholds
88
+ scores.sort((a, b) => a - b);
89
+ const p25 = scores[Math.floor(scores.length * 0.25)] || 0;
90
+ const p50 = scores[Math.floor(scores.length * 0.50)] || 0;
91
+ const p75 = scores[Math.floor(scores.length * 0.75)] || 0;
92
+
93
+ function getLevel(score) {
94
+ if (score === 0) return 0;
95
+ if (score <= p25) return 1;
96
+ if (score <= p50) return 2;
97
+ if (score <= p75) return 3;
98
+ return 4;
99
+ }
100
+
101
+ // Render cells
102
+ grid.innerHTML = '';
103
+ for (const cell of cells) {
104
+ const div = document.createElement('div');
105
+ div.className = 'home-grid-cell';
106
+ div.dataset.level = getLevel(cell.score);
107
+ div.dataset.date = cell.date;
108
+ if (cell.row) {
109
+ div.dataset.sessions = cell.row.sessions;
110
+ div.dataset.queries = cell.row.queries;
111
+ div.dataset.cost = cell.row.cost.toFixed(2);
112
+ div.dataset.tokens = (cell.row.input_tokens + cell.row.output_tokens).toString();
113
+ }
114
+ grid.appendChild(div);
115
+ }
116
+
117
+ // Month labels
118
+ monthsRow.innerHTML = '';
119
+ let currentMonth = -1;
120
+ let weekIndex = 0;
121
+ const cellSize = 15; // 12px + 3px gap
122
+ const dc = new Date(start);
123
+
124
+ while (dc <= today) {
125
+ if (dc.getDay() === 0) { // start of week
126
+ const m = dc.getMonth();
127
+ if (m !== currentMonth) {
128
+ const span = document.createElement('span');
129
+ span.textContent = MONTHS[m];
130
+ span.style.marginLeft = (weekIndex * cellSize) + 'px';
131
+ span.style.position = 'absolute';
132
+ monthsRow.appendChild(span);
133
+ currentMonth = m;
134
+ }
135
+ weekIndex++;
136
+ }
137
+ dc.setDate(dc.getDate() + 1);
138
+ }
139
+ monthsRow.style.position = 'relative';
140
+ monthsRow.style.height = '16px';
141
+
142
+ // Tooltip
143
+ setupTooltip(grid);
144
+ }
145
+
146
+ // ── Tooltip ────────────────────────────────────────────
147
+ let tooltip = null;
148
+
149
+ function setupTooltip(grid) {
150
+ grid.addEventListener('mouseover', (e) => {
151
+ const cell = e.target.closest('.home-grid-cell');
152
+ if (!cell) return;
153
+ if (!tooltip) {
154
+ tooltip = document.createElement('div');
155
+ tooltip.className = 'home-grid-tooltip';
156
+ document.body.appendChild(tooltip);
157
+ }
158
+ const date = cell.dataset.date;
159
+ const sessions = cell.dataset.sessions || '0';
160
+ const queries = cell.dataset.queries || '0';
161
+ const cost = cell.dataset.cost || '0.00';
162
+ const tokens = cell.dataset.tokens || '0';
163
+
164
+ tooltip.innerHTML = `<strong>${date}</strong><br>` +
165
+ `${sessions} sessions &middot; ${queries} queries<br>` +
166
+ `${formatTokens(+tokens)} tokens &middot; $${cost}`;
167
+ tooltip.style.display = 'block';
168
+ });
169
+
170
+ grid.addEventListener('mousemove', (e) => {
171
+ if (tooltip) {
172
+ tooltip.style.left = (e.clientX + 12) + 'px';
173
+ tooltip.style.top = (e.clientY - 40) + 'px';
174
+ }
175
+ });
176
+
177
+ grid.addEventListener('mouseout', (e) => {
178
+ if (!e.target.closest('.home-grid-cell')) return;
179
+ if (tooltip) tooltip.style.display = 'none';
180
+ });
181
+ }
182
+
183
+ // ── Summary cards ──────────────────────────────────────
184
+ function renderCards(activity, overview) {
185
+ const cards = document.getElementById('home-cards');
186
+
187
+ // Streak calculations
188
+ const activeDates = new Set(activity.map(r => r.date));
189
+ const today = new Date();
190
+ today.setHours(0, 0, 0, 0);
191
+ const todayKey = localDateStr(today);
192
+
193
+ // Current streak
194
+ let currentStreak = 0;
195
+ const d = new Date(today);
196
+ while (true) {
197
+ const key = localDateStr(d);
198
+ if (activeDates.has(key)) {
199
+ currentStreak++;
200
+ d.setDate(d.getDate() - 1);
201
+ } else {
202
+ break;
203
+ }
204
+ }
205
+
206
+ // Longest streak
207
+ let longestStreak = 0;
208
+ let streak = 0;
209
+ const sortedDates = [...activeDates].sort();
210
+ for (let i = 0; i < sortedDates.length; i++) {
211
+ if (i === 0) {
212
+ streak = 1;
213
+ } else {
214
+ const prev = new Date(sortedDates[i - 1]);
215
+ const curr = new Date(sortedDates[i]);
216
+ const diff = (curr - prev) / 86400000;
217
+ streak = diff === 1 ? streak + 1 : 1;
218
+ }
219
+ longestStreak = Math.max(longestStreak, streak);
220
+ }
221
+
222
+ // Today's stats
223
+ const todayData = activity.find(r => r.date === todayKey);
224
+ const todayCost = todayData ? todayData.cost : 0;
225
+ const todayQueries = todayData ? todayData.queries : 0;
226
+
227
+ const totalTokens = overview.totalOutputTokens || 0;
228
+ const totalSessions = overview.sessions || 0;
229
+ const totalCost = overview.totalCost || 0;
230
+
231
+ const cardData = [
232
+ { label: 'Sessions', value: totalSessions.toLocaleString(), sub: 'all time' },
233
+ { label: 'Total Cost', value: '$' + totalCost.toFixed(2), sub: 'all time' },
234
+ { label: 'Output Tokens', value: formatTokens(totalTokens), sub: 'all time' },
235
+ { label: 'Current Streak', value: currentStreak + 'd', sub: currentStreak > 0 ? 'active' : 'no activity today' },
236
+ { label: 'Longest Streak', value: longestStreak + 'd', sub: 'consecutive days' },
237
+ { label: 'Today', value: todayQueries + ' queries', sub: '$' + todayCost.toFixed(2) },
238
+ ];
239
+
240
+ cards.innerHTML = cardData.map(c => `
241
+ <div class="home-card">
242
+ <div class="home-card-label">${c.label}</div>
243
+ <div class="home-card-value">${c.value}</div>
244
+ <div class="home-card-sub">${c.sub}</div>
245
+ </div>
246
+ `).join('');
247
+ }
248
+
249
+ // ── Helpers ────────────────────────────────────────────
250
+ function localDateStr(d) {
251
+ const y = d.getFullYear();
252
+ const m = String(d.getMonth() + 1).padStart(2, '0');
253
+ const day = String(d.getDate()).padStart(2, '0');
254
+ return `${y}-${m}-${day}`;
255
+ }
256
+
257
+ function formatTokens(n) {
258
+ if (n >= 1_000_000) return (n / 1_000_000).toFixed(1) + 'M';
259
+ if (n >= 1_000) return (n / 1_000).toFixed(1) + 'k';
260
+ return n.toString();
261
+ }
262
+
263
+ // If a session was already restored from localStorage before this module loaded,
264
+ // or a project was previously selected, switch to chat view; otherwise load home.
265
+ const savedProject = localStorage.getItem('claudeck-cwd');
266
+ if (getState('sessionId') || savedProject) {
267
+ setState('view', 'chat');
268
+ } else if (getState('view') === 'home') {
269
+ loadHome();
270
+ }
@@ -0,0 +1,372 @@
1
+ // Project selection & system prompts
2
+ import { $ } from '../core/dom.js';
3
+ import { getState, setState } from '../core/store.js';
4
+ import { CHAT_IDS } from '../core/constants.js';
5
+ import * as api from '../core/api.js';
6
+ import { commandRegistry, registerCommand } from '../ui/commands.js';
7
+ import { panes } from '../ui/parallel.js';
8
+ import { loadSessions } from './sessions.js';
9
+ import { loadStats } from './cost-dashboard.js';
10
+ import { showWhalyPlaceholder } from '../ui/messages.js';
11
+
12
+ export async function loadProjects() {
13
+ try {
14
+ const projects = await api.fetchProjects();
15
+ setState("projectsData", projects);
16
+ const saved = localStorage.getItem("claudeck-cwd") || "";
17
+
18
+ for (const p of projects) {
19
+ const opt = document.createElement("option");
20
+ opt.value = p.path;
21
+ opt.textContent = p.name;
22
+ $.projectSelect.appendChild(opt);
23
+ }
24
+
25
+ if (saved && [...$.projectSelect.options].some((o) => o.value === saved)) {
26
+ $.projectSelect.value = saved;
27
+ }
28
+ updateSystemPromptIndicator();
29
+ updateHeaderProjectName();
30
+ updateSessionControls();
31
+ loadProjectCommands();
32
+ // Load sessions after project dropdown is populated so they filter correctly
33
+ loadSessions();
34
+
35
+ // If a session was restored from localStorage, load its messages
36
+ const { getState } = await import('../core/store.js');
37
+ const { loadMessages } = await import('./sessions.js');
38
+ const restoredSid = getState("sessionId");
39
+ if (restoredSid) {
40
+ loadMessages(restoredSid);
41
+ }
42
+ } catch (err) {
43
+ console.error("Failed to load projects:", err);
44
+ }
45
+ }
46
+
47
+ const sessionControls = document.getElementById("session-controls");
48
+
49
+ function updateSessionControls() {
50
+ if ($.projectSelect.value) {
51
+ sessionControls.classList.remove("hidden");
52
+ } else {
53
+ sessionControls.classList.add("hidden");
54
+ }
55
+ }
56
+
57
+ export function updateSystemPromptIndicator() {
58
+ const cwd = $.projectSelect.value;
59
+ const project = getState("projectsData").find((p) => p.path === cwd);
60
+ if (project && project.systemPrompt) {
61
+ $.spBadge.classList.remove("hidden");
62
+ } else {
63
+ $.spBadge.classList.add("hidden");
64
+ }
65
+ }
66
+
67
+ export function openSystemPromptModal() {
68
+ const cwd = $.projectSelect.value;
69
+ if (!cwd) return;
70
+ const project = getState("projectsData").find((p) => p.path === cwd);
71
+ $.spTextarea.value = project?.systemPrompt || "";
72
+ $.spModal.classList.remove("hidden");
73
+ $.spTextarea.focus();
74
+ }
75
+
76
+ export async function saveSystemPrompt(prompt) {
77
+ const cwd = $.projectSelect.value;
78
+ if (!cwd) return;
79
+ try {
80
+ await api.saveSystemPromptApi(cwd, prompt);
81
+ const project = getState("projectsData").find((p) => p.path === cwd);
82
+ if (project) project.systemPrompt = prompt;
83
+ updateSystemPromptIndicator();
84
+ } catch (err) {
85
+ console.error("Failed to save system prompt:", err);
86
+ }
87
+ }
88
+
89
+ export function updateHeaderProjectName() {
90
+ const opt = $.projectSelect.options[$.projectSelect.selectedIndex];
91
+ $.headerProjectName.textContent = opt && opt.value ? opt.textContent : "";
92
+ }
93
+
94
+ export async function loadProjectCommands() {
95
+ // Remove old project commands and skills
96
+ for (const [name, cmd] of Object.entries(commandRegistry)) {
97
+ if (cmd.category === "project" || cmd.category === "skill") delete commandRegistry[name];
98
+ }
99
+
100
+ const cwd = $.projectSelect.value;
101
+ if (!cwd) return;
102
+
103
+ try {
104
+ const commands = await api.fetchProjectCommands(cwd);
105
+ if (!Array.isArray(commands) || commands.length === 0) return;
106
+
107
+ for (const c of commands) {
108
+ const slug = c.command;
109
+ if (!slug || commandRegistry[slug]) continue;
110
+ const hasArgs = c.prompt.includes("$ARGUMENTS");
111
+ const label = c.source === "skill" ? `${c.description}` : (c.description || c.command);
112
+ registerCommand(slug, {
113
+ category: c.source === "skill" ? "skill" : "project",
114
+ description: label,
115
+ needsArgs: hasArgs,
116
+ argumentHint: c.argumentHint || "",
117
+ execute(args, pane) {
118
+ let prompt = c.prompt;
119
+ if (hasArgs) {
120
+ prompt = prompt.replace(/\$ARGUMENTS/g, args || "");
121
+ }
122
+ pane.messageInput.value = prompt;
123
+ pane.messageInput.style.height = "auto";
124
+ pane.messageInput.style.height = Math.min(pane.messageInput.scrollHeight, 200) + "px";
125
+ // Lazy import to avoid circular dep
126
+ import('./chat.js').then(({ sendMessage }) => sendMessage(pane));
127
+ },
128
+ });
129
+ }
130
+ } catch (err) {
131
+ console.error("Failed to load project commands:", err);
132
+ }
133
+ }
134
+
135
+ // ── Add Project (folder browser) ────────────────────────
136
+ let currentBrowsePath = "";
137
+
138
+ function openAddProjectModal() {
139
+ $.addProjectModal.classList.remove("hidden");
140
+ $.addProjectName.value = "";
141
+ navigateToDir(""); // defaults to $HOME on server
142
+ }
143
+
144
+ function closeAddProjectModal() {
145
+ $.addProjectModal.classList.add("hidden");
146
+ }
147
+
148
+ async function navigateToDir(dir) {
149
+ $.folderList.innerHTML = '<div class="folder-list-loading">Loading...</div>';
150
+ try {
151
+ const data = await api.browseFolders(dir || undefined);
152
+ currentBrowsePath = data.current;
153
+ renderBreadcrumb(data.current);
154
+ renderFolderList(data);
155
+ // Auto-fill name from last segment
156
+ const base = data.current.split(/[/\\]/).filter(Boolean).pop() || "";
157
+ $.addProjectName.value = base;
158
+ } catch (err) {
159
+ $.folderList.innerHTML = `<div class="folder-list-empty">Error: ${err.message}</div>`;
160
+ }
161
+ }
162
+
163
+ function renderBreadcrumb(pathStr) {
164
+ $.folderBreadcrumb.innerHTML = "";
165
+ const parts = pathStr.split(/[/\\]/).filter(Boolean);
166
+ // Root
167
+ const rootSeg = document.createElement("span");
168
+ rootSeg.className = "folder-breadcrumb-seg";
169
+ rootSeg.textContent = "/";
170
+ rootSeg.addEventListener("click", () => navigateToDir("/"));
171
+ $.folderBreadcrumb.appendChild(rootSeg);
172
+
173
+ let accumulated = "";
174
+ for (const part of parts) {
175
+ accumulated += "/" + part;
176
+ const sep = document.createElement("span");
177
+ sep.className = "folder-breadcrumb-sep";
178
+ sep.textContent = "/";
179
+ $.folderBreadcrumb.appendChild(sep);
180
+
181
+ const seg = document.createElement("span");
182
+ seg.className = "folder-breadcrumb-seg";
183
+ seg.textContent = part;
184
+ const target = accumulated;
185
+ seg.addEventListener("click", () => navigateToDir(target));
186
+ $.folderBreadcrumb.appendChild(seg);
187
+ }
188
+ }
189
+
190
+ function renderFolderList(data) {
191
+ $.folderList.innerHTML = "";
192
+
193
+ // Parent directory entry
194
+ if (data.parent) {
195
+ const parentItem = document.createElement("div");
196
+ parentItem.className = "folder-list-item";
197
+ parentItem.innerHTML = '<span class="folder-icon">..</span><span>Parent directory</span>';
198
+ parentItem.addEventListener("click", () => navigateToDir(data.parent));
199
+ $.folderList.appendChild(parentItem);
200
+ }
201
+
202
+ if (data.dirs.length === 0 && !data.parent) {
203
+ $.folderList.innerHTML = '<div class="folder-list-empty">No subdirectories</div>';
204
+ return;
205
+ }
206
+
207
+ if (data.dirs.length === 0) {
208
+ const empty = document.createElement("div");
209
+ empty.className = "folder-list-empty";
210
+ empty.textContent = "No subdirectories";
211
+ $.folderList.appendChild(empty);
212
+ return;
213
+ }
214
+
215
+ for (const dir of data.dirs) {
216
+ const item = document.createElement("div");
217
+ item.className = "folder-list-item";
218
+ item.innerHTML = `<span class="folder-icon">\u{1F4C1}</span><span>${dir.name}</span>`;
219
+ item.addEventListener("click", () => navigateToDir(dir.path));
220
+ $.folderList.appendChild(item);
221
+ }
222
+ }
223
+
224
+ async function confirmAddProject() {
225
+ const name = $.addProjectName.value.trim();
226
+ if (!name) {
227
+ $.addProjectName.focus();
228
+ return;
229
+ }
230
+ if (!currentBrowsePath) return;
231
+
232
+ // Check for duplicate in dropdown
233
+ const existing = [...$.projectSelect.options].find((o) => o.value === currentBrowsePath);
234
+ if (existing) {
235
+ alert("This project path is already added.");
236
+ return;
237
+ }
238
+
239
+ try {
240
+ const result = await api.addProject(name, currentBrowsePath);
241
+ const project = result.project;
242
+
243
+ // Add to dropdown and select it
244
+ const opt = document.createElement("option");
245
+ opt.value = project.path;
246
+ opt.textContent = project.name;
247
+ $.projectSelect.appendChild(opt);
248
+ $.projectSelect.value = project.path;
249
+
250
+ // Update state
251
+ const projects = getState("projectsData");
252
+ projects.push({ name: project.name, path: project.path });
253
+
254
+ localStorage.setItem("claudeck-cwd", project.path);
255
+ updateSystemPromptIndicator();
256
+ updateHeaderProjectName();
257
+ updateSessionControls();
258
+ loadProjectCommands();
259
+ loadSessions();
260
+ loadStats();
261
+
262
+ closeAddProjectModal();
263
+ } catch (err) {
264
+ alert("Failed to add project: " + err.message);
265
+ }
266
+ }
267
+
268
+ // Open in VS Code
269
+ $.openVscodeBtn.addEventListener("click", async () => {
270
+ const path = $.projectSelect.value;
271
+ if (!path) return;
272
+ try {
273
+ await fetch("/api/exec", {
274
+ method: "POST",
275
+ headers: { "Content-Type": "application/json" },
276
+ body: JSON.stringify({ command: "code .", cwd: path }),
277
+ });
278
+ } catch { /* ignore */ }
279
+ });
280
+
281
+ // Add project button & modal event listeners
282
+ $.addProjectBtn.addEventListener("click", openAddProjectModal);
283
+ $.addProjectClose.addEventListener("click", closeAddProjectModal);
284
+ $.addProjectConfirm.addEventListener("click", confirmAddProject);
285
+ $.addProjectModal.addEventListener("click", (e) => {
286
+ if (e.target === $.addProjectModal) closeAddProjectModal();
287
+ });
288
+
289
+ // System prompt modal event listeners
290
+ $.spEditBtn.addEventListener("click", openSystemPromptModal);
291
+ $.spForm.addEventListener("submit", async (e) => {
292
+ e.preventDefault();
293
+ await saveSystemPrompt($.spTextarea.value.trim());
294
+ $.spModal.classList.add("hidden");
295
+ });
296
+ document.getElementById("sp-cancel-btn").addEventListener("click", () => {
297
+ $.spModal.classList.add("hidden");
298
+ });
299
+ document.getElementById("sp-modal-close").addEventListener("click", () => {
300
+ $.spModal.classList.add("hidden");
301
+ });
302
+ document.getElementById("sp-clear-btn").addEventListener("click", async () => {
303
+ $.spTextarea.value = "";
304
+ await saveSystemPrompt("");
305
+ $.spModal.classList.add("hidden");
306
+ });
307
+ $.spModal.addEventListener("click", (e) => {
308
+ if (e.target === $.spModal) $.spModal.classList.add("hidden");
309
+ });
310
+
311
+ // Project change handler
312
+ $.projectSelect.addEventListener("change", async () => {
313
+ const { guardSwitch } = await import('./background-sessions.js');
314
+ guardSwitch(() => {
315
+ localStorage.setItem("claudeck-cwd", $.projectSelect.value);
316
+ setState("sessionId", null);
317
+ if ($.projectSelect.value) {
318
+ setState("view", "chat");
319
+ }
320
+ updateSystemPromptIndicator();
321
+ updateHeaderProjectName();
322
+ updateSessionControls();
323
+ loadProjectCommands();
324
+ if (getState("parallelMode")) {
325
+ for (const chatId of CHAT_IDS) {
326
+ const pane = panes.get(chatId);
327
+ if (pane) {
328
+ pane.messagesDiv.innerHTML = "";
329
+ showWhalyPlaceholder(pane);
330
+ }
331
+ }
332
+ } else {
333
+ $.messagesDiv.innerHTML = "";
334
+ showWhalyPlaceholder();
335
+ }
336
+ loadSessions();
337
+ loadStats();
338
+ });
339
+ });
340
+
341
+ // New session button
342
+ $.newSessionBtn.addEventListener("click", async () => {
343
+ const { guardSwitch } = await import('./background-sessions.js');
344
+ guardSwitch(() => {
345
+ setState("view", "chat");
346
+ setState("sessionId", null);
347
+ if (getState("parallelMode")) {
348
+ for (const chatId of CHAT_IDS) {
349
+ const pane = panes.get(chatId);
350
+ if (pane) {
351
+ pane.messagesDiv.innerHTML = "";
352
+ pane.currentAssistantMsg = null;
353
+ showWhalyPlaceholder(pane);
354
+ }
355
+ }
356
+ } else {
357
+ $.messagesDiv.innerHTML = "";
358
+ showWhalyPlaceholder();
359
+ }
360
+ loadSessions();
361
+ if (!getState("parallelMode")) $.messageInput.focus();
362
+ });
363
+ });
364
+
365
+ // Parallel mode toggle
366
+ $.toggleParallelBtn.addEventListener("change", () => {
367
+ if ($.toggleParallelBtn.checked) {
368
+ import('../ui/parallel.js').then(({ enterParallelMode }) => enterParallelMode());
369
+ } else {
370
+ import('../ui/parallel.js').then(({ exitParallelMode }) => exitParallelMode());
371
+ }
372
+ });