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,486 @@
1
+ // File Explorer — lazy-loaded tree view with file preview
2
+ import { $ } from "../core/dom.js";
3
+ import { on } from "../core/events.js";
4
+ import { getState, on as onState } from "../core/store.js";
5
+ import { fetchFileTree, fetchFileContent, searchFiles } from "../core/api.js";
6
+ import { highlightCodeBlocks } from "../ui/formatting.js";
7
+ import { escapeHtml } from "../core/utils.js";
8
+
9
+ const treeCache = new Map();
10
+ let currentProject = null;
11
+ let activeFilePath = null;
12
+ let searchMode = false;
13
+ let searchDebounce = null;
14
+
15
+ function isFilesTabActive() {
16
+ const pane = $.rightPanel?.querySelector('.right-panel-pane[data-tab="files"]');
17
+ return pane && pane.classList.contains("active") && !$.rightPanel.classList.contains("hidden");
18
+ }
19
+
20
+ // SVG icons
21
+ const IMAGE_EXTENSIONS = new Set([".png", ".jpg", ".jpeg", ".gif", ".svg", ".webp"]);
22
+
23
+ function isImageFile(name) {
24
+ const dot = name.lastIndexOf(".");
25
+ return dot !== -1 && IMAGE_EXTENSIONS.has(name.slice(dot).toLowerCase());
26
+ }
27
+
28
+ const CHEVRON_SVG = `<svg class="file-tree-chevron" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="9 18 15 12 9 6"/></svg>`;
29
+ const FOLDER_SVG = `<svg class="file-tree-icon folder" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z"/></svg>`;
30
+ const FILE_SVG = `<svg class="file-tree-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M13 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V9z"/><polyline points="13 2 13 9 20 9"/></svg>`;
31
+
32
+ function getProjectPath() {
33
+ const projects = getState("projectsData");
34
+ const selected = $.projectSelect.value;
35
+ if (!selected || !projects) return null;
36
+ const proj = projects.find((p) => p.path === selected);
37
+ return proj ? proj.path : selected;
38
+ }
39
+
40
+ async function loadTree(dir = "") {
41
+ const base = getProjectPath();
42
+ if (!base) return;
43
+
44
+ const cacheKey = `${base}::${dir}`;
45
+ if (treeCache.has(cacheKey)) return treeCache.get(cacheKey);
46
+
47
+ const entries = await fetchFileTree(base, dir);
48
+ treeCache.set(cacheKey, entries);
49
+ return entries;
50
+ }
51
+
52
+ function createTreeItem(entry, depth) {
53
+ const item = document.createElement("div");
54
+ item.className = "file-tree-item";
55
+ item.style.paddingLeft = `${8 + depth * 16}px`;
56
+ item.dataset.path = entry.path;
57
+ item.dataset.type = entry.type;
58
+
59
+ const isDir = entry.type === "dir";
60
+ const chevron = isDir ? CHEVRON_SVG : `<svg class="file-tree-chevron hidden" viewBox="0 0 24 24"></svg>`;
61
+ const icon = isDir ? FOLDER_SVG : FILE_SVG;
62
+
63
+ item.innerHTML = `${chevron}${icon}<span class="file-tree-name">${escapeHtml(entry.name)}</span>`;
64
+
65
+ // Make all items draggable
66
+ item.draggable = true;
67
+ item.addEventListener("dragstart", (e) => {
68
+ const base = getProjectPath() || "";
69
+ const fullPath = base ? `${base}/${entry.path}` : entry.path;
70
+ e.dataTransfer.setData("text/plain", fullPath);
71
+ e.dataTransfer.setData("application/x-file-path", fullPath);
72
+ e.dataTransfer.effectAllowed = "copy";
73
+ item.classList.add("dragging");
74
+ });
75
+ item.addEventListener("dragend", () => {
76
+ item.classList.remove("dragging");
77
+ });
78
+
79
+ if (isDir) {
80
+ const childrenContainer = document.createElement("div");
81
+ childrenContainer.className = "file-tree-children";
82
+ item.after(childrenContainer);
83
+
84
+ item.addEventListener("click", async () => {
85
+ const chevronEl = item.querySelector(".file-tree-chevron");
86
+ const isExpanded = childrenContainer.classList.contains("expanded");
87
+
88
+ if (isExpanded) {
89
+ childrenContainer.classList.remove("expanded");
90
+ chevronEl.classList.remove("expanded");
91
+ } else {
92
+ chevronEl.classList.add("expanded");
93
+ if (childrenContainer.children.length === 0) {
94
+ childrenContainer.innerHTML = `<div class="file-tree-loading" style="padding-left:${8 + (depth + 1) * 16}px">Loading...</div>`;
95
+ childrenContainer.classList.add("expanded");
96
+ try {
97
+ const children = await loadTree(entry.path);
98
+ childrenContainer.innerHTML = "";
99
+ renderEntries(children, childrenContainer, depth + 1);
100
+ } catch {
101
+ childrenContainer.innerHTML = `<div class="file-tree-loading" style="padding-left:${8 + (depth + 1) * 16}px">Failed to load</div>`;
102
+ }
103
+ } else {
104
+ childrenContainer.classList.add("expanded");
105
+ }
106
+ }
107
+ });
108
+
109
+ return [item, childrenContainer];
110
+ }
111
+
112
+ // File click — show preview
113
+ item.addEventListener("click", () => openFilePreview(entry));
114
+ return [item];
115
+ }
116
+
117
+ function renderEntries(entries, container, depth) {
118
+ if (!entries || entries.length === 0) {
119
+ container.innerHTML = `<div class="file-tree-loading" style="padding-left:${8 + depth * 16}px">Empty</div>`;
120
+ return;
121
+ }
122
+ for (const entry of entries) {
123
+ const elements = createTreeItem(entry, depth);
124
+ for (const el of elements) {
125
+ container.appendChild(el);
126
+ }
127
+ }
128
+ }
129
+
130
+ async function openFilePreview(entry) {
131
+ const base = getProjectPath();
132
+ if (!base) return;
133
+
134
+ // Highlight active item
135
+ $.fileTree.querySelectorAll(".file-tree-item.active").forEach((el) => el.classList.remove("active"));
136
+ const item = $.fileTree.querySelector(`[data-path="${CSS.escape(entry.path)}"][data-type="file"]`);
137
+ if (item) item.classList.add("active");
138
+
139
+ activeFilePath = entry.path;
140
+ $.filePreviewName.textContent = entry.path;
141
+ $.filePreview.classList.remove("hidden");
142
+
143
+ if (isImageFile(entry.name || entry.path)) {
144
+ // Image preview
145
+ $.filePreviewContent.classList.add("hidden");
146
+ $.filePreviewImage.classList.remove("hidden");
147
+ $.filePreviewImage.alt = entry.name || entry.path;
148
+ $.filePreviewImage.src = `/api/files/raw?base=${encodeURIComponent(base)}&path=${encodeURIComponent(entry.path)}`;
149
+ $.filePreviewImage.onerror = () => {
150
+ if (activeFilePath !== entry.path) return;
151
+ $.filePreviewImage.classList.add("hidden");
152
+ $.filePreviewContent.classList.remove("hidden");
153
+ $.filePreviewContent.querySelector("code").textContent = "Error: failed to load image";
154
+ };
155
+ } else {
156
+ // Text preview
157
+ $.filePreviewImage.classList.add("hidden");
158
+ $.filePreviewImage.src = "";
159
+ $.filePreviewContent.classList.remove("hidden");
160
+ $.filePreviewContent.querySelector("code").textContent = "Loading...";
161
+
162
+ try {
163
+ const data = await fetchFileContent(base, entry.path);
164
+ if (activeFilePath !== entry.path) return; // stale
165
+ const code = $.filePreviewContent.querySelector("code");
166
+ code.textContent = data.content;
167
+ code.className = ""; // reset hljs classes
168
+ delete code.dataset.highlighted;
169
+ highlightCodeBlocks($.filePreviewContent);
170
+ } catch (err) {
171
+ if (activeFilePath !== entry.path) return;
172
+ $.filePreviewContent.querySelector("code").textContent = `Error: ${err.message}`;
173
+ }
174
+ }
175
+ }
176
+
177
+ function closePreview() {
178
+ $.filePreview.classList.add("hidden");
179
+ $.filePreview.style.height = "";
180
+ activeFilePath = null;
181
+ $.fileTree.querySelectorAll(".file-tree-item.active").forEach((el) => el.classList.remove("active"));
182
+ }
183
+
184
+ async function loadRootTree() {
185
+ const base = getProjectPath();
186
+ if (!base) {
187
+ $.fileTree.innerHTML = `
188
+ <div class="file-tree-empty">
189
+ <svg width="28" height="28" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
190
+ <path d="M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z"/>
191
+ </svg>
192
+ <span>Select a project to browse files</span>
193
+ </div>`;
194
+ return;
195
+ }
196
+
197
+ if (currentProject === base && $.fileTree.children.length > 0) return;
198
+ currentProject = base;
199
+
200
+ $.fileTree.innerHTML = `<div class="file-tree-loading">Loading...</div>`;
201
+ closePreview();
202
+
203
+ try {
204
+ const entries = await loadTree("");
205
+ $.fileTree.innerHTML = "";
206
+ renderEntries(entries, $.fileTree, 0);
207
+ } catch {
208
+ $.fileTree.innerHTML = `<div class="file-tree-loading">Failed to load</div>`;
209
+ }
210
+ }
211
+
212
+ async function searchTree(query) {
213
+ const base = getProjectPath();
214
+ if (!base) return;
215
+
216
+ if (!query) {
217
+ // Restore normal tree view
218
+ if (searchMode) {
219
+ searchMode = false;
220
+ currentProject = null; // force reload
221
+ loadRootTree();
222
+ }
223
+ return;
224
+ }
225
+
226
+ searchMode = true;
227
+ $.fileTree.innerHTML = `<div class="file-tree-loading">Searching...</div>`;
228
+
229
+ try {
230
+ const results = await searchFiles(base, query);
231
+ $.fileTree.innerHTML = "";
232
+
233
+ if (!results || results.length === 0) {
234
+ $.fileTree.innerHTML = `<div class="file-tree-loading">No matches</div>`;
235
+ return;
236
+ }
237
+
238
+ for (const entry of results) {
239
+ const item = document.createElement("div");
240
+ item.className = "file-tree-item file-search-result";
241
+ item.dataset.path = entry.path;
242
+ item.dataset.type = entry.type;
243
+
244
+ const isDir = entry.type === "dir";
245
+ const icon = isDir ? FOLDER_SVG : FILE_SVG;
246
+
247
+ // Show filename prominent + relative path dimmed
248
+ const dirPart = entry.path.includes("/")
249
+ ? entry.path.slice(0, entry.path.lastIndexOf("/"))
250
+ : "";
251
+
252
+ item.innerHTML = `${icon}<span class="file-search-name">${escapeHtml(entry.name)}</span>${
253
+ dirPart ? `<span class="file-search-path">${escapeHtml(dirPart)}</span>` : ""
254
+ }`;
255
+
256
+ // Draggable
257
+ item.draggable = true;
258
+ item.addEventListener("dragstart", (e) => {
259
+ const fullPath = base ? `${base}/${entry.path}` : entry.path;
260
+ e.dataTransfer.setData("text/plain", fullPath);
261
+ e.dataTransfer.setData("application/x-file-path", fullPath);
262
+ e.dataTransfer.effectAllowed = "copy";
263
+ item.classList.add("dragging");
264
+ });
265
+ item.addEventListener("dragend", () => item.classList.remove("dragging"));
266
+
267
+ if (!isDir) {
268
+ item.addEventListener("click", () => openFilePreview(entry));
269
+ }
270
+
271
+ $.fileTree.appendChild(item);
272
+ }
273
+ } catch {
274
+ $.fileTree.innerHTML = `<div class="file-tree-loading">Search failed</div>`;
275
+ }
276
+ }
277
+
278
+ function resetExplorer() {
279
+ treeCache.clear();
280
+ currentProject = null;
281
+ activeFilePath = null;
282
+ $.fileTree.innerHTML = "";
283
+ closePreview();
284
+ }
285
+
286
+ async function refreshFileTree() {
287
+ $.fileRefreshBtn.classList.add("spinning");
288
+ try {
289
+ treeCache.clear();
290
+ currentProject = null;
291
+ searchMode = false;
292
+ $.fileExplorerSearch.value = "";
293
+ await loadRootTree();
294
+ } finally {
295
+ $.fileRefreshBtn.classList.remove("spinning");
296
+ }
297
+ }
298
+
299
+ function initFileExplorer() {
300
+ $.filePreviewClose.addEventListener("click", closePreview);
301
+ initPreviewResize();
302
+ $.fileRefreshBtn.addEventListener("click", () => refreshFileTree());
303
+
304
+ $.fileExplorerSearch.addEventListener("input", (e) => {
305
+ clearTimeout(searchDebounce);
306
+ const q = e.target.value.trim();
307
+ searchDebounce = setTimeout(() => searchTree(q), 250);
308
+ });
309
+
310
+ // Load tree when Files tab is opened
311
+ on("rightPanel:opened", (tab) => {
312
+ if (tab === "files") loadRootTree();
313
+ });
314
+ on("rightPanel:tabChanged", (tab) => {
315
+ if (tab === "files") loadRootTree();
316
+ });
317
+
318
+ // Reset and reload on project switch
319
+ $.projectSelect.addEventListener("change", () => {
320
+ resetExplorer();
321
+ if (isFilesTabActive()) loadRootTree();
322
+ });
323
+
324
+ // Load tree when projects data arrives (covers initial page load)
325
+ // setTimeout(0) defers until after loadProjects() finishes populating the select
326
+ onState("projectsData", () => {
327
+ setTimeout(() => {
328
+ if (isFilesTabActive()) loadRootTree();
329
+ }, 0);
330
+ });
331
+
332
+ // ── Context menu on right-click ──
333
+ initContextMenu();
334
+
335
+ // ── Drag-to-chat: drop file paths into message input ──
336
+ initDropTargets();
337
+ }
338
+
339
+ // ── Context Menu ────────────────────────────────────────
340
+ let ctxMenu = null;
341
+
342
+ function hideContextMenu() {
343
+ if (ctxMenu) {
344
+ ctxMenu.remove();
345
+ ctxMenu = null;
346
+ }
347
+ }
348
+
349
+ function showContextMenu(e, relPath) {
350
+ e.preventDefault();
351
+ hideContextMenu();
352
+
353
+ const base = getProjectPath() || "";
354
+ const fullPath = base ? `${base}/${relPath}` : relPath;
355
+
356
+ ctxMenu = document.createElement("div");
357
+ ctxMenu.className = "file-ctx-menu";
358
+ ctxMenu.innerHTML = `
359
+ <button data-action="full">Copy Full Path</button>
360
+ <button data-action="relative">Copy Relative Path</button>
361
+ `;
362
+
363
+ // Position at cursor
364
+ ctxMenu.style.left = e.clientX + "px";
365
+ ctxMenu.style.top = e.clientY + "px";
366
+ document.body.appendChild(ctxMenu);
367
+
368
+ // Keep menu within viewport
369
+ const rect = ctxMenu.getBoundingClientRect();
370
+ if (rect.right > window.innerWidth) ctxMenu.style.left = (e.clientX - rect.width) + "px";
371
+ if (rect.bottom > window.innerHeight) ctxMenu.style.top = (e.clientY - rect.height) + "px";
372
+
373
+ ctxMenu.addEventListener("click", (ev) => {
374
+ const action = ev.target.dataset.action;
375
+ if (action === "full") navigator.clipboard.writeText(fullPath);
376
+ if (action === "relative") navigator.clipboard.writeText(relPath);
377
+ hideContextMenu();
378
+ });
379
+ }
380
+
381
+ function initPreviewResize() {
382
+ const handle = document.createElement("div");
383
+ handle.className = "file-preview-resize";
384
+ $.filePreview.prepend(handle);
385
+
386
+ let startY, startH;
387
+
388
+ function onMouseMove(e) {
389
+ const delta = startY - e.clientY;
390
+ const pane = $.filePreview.closest(".right-panel-pane");
391
+ const maxH = pane ? pane.clientHeight - 40 : 600;
392
+ $.filePreview.style.height = Math.min(maxH, Math.max(80, startH + delta)) + "px";
393
+ }
394
+
395
+ function onMouseUp() {
396
+ handle.classList.remove("dragging");
397
+ document.removeEventListener("mousemove", onMouseMove);
398
+ document.removeEventListener("mouseup", onMouseUp);
399
+ }
400
+
401
+ handle.addEventListener("mousedown", (e) => {
402
+ e.preventDefault();
403
+ startY = e.clientY;
404
+ startH = $.filePreview.offsetHeight;
405
+ handle.classList.add("dragging");
406
+ document.addEventListener("mousemove", onMouseMove);
407
+ document.addEventListener("mouseup", onMouseUp);
408
+ });
409
+ }
410
+
411
+ function initContextMenu() {
412
+ // Delegate on file tree container
413
+ $.fileTree.addEventListener("contextmenu", (e) => {
414
+ const item = e.target.closest(".file-tree-item");
415
+ if (!item) return;
416
+ const relPath = item.dataset.path;
417
+ if (relPath) showContextMenu(e, relPath);
418
+ });
419
+
420
+ // Close on click outside or Escape
421
+ document.addEventListener("click", (e) => {
422
+ if (ctxMenu && !ctxMenu.contains(e.target)) hideContextMenu();
423
+ });
424
+ document.addEventListener("keydown", (e) => {
425
+ if (e.key === "Escape") hideContextMenu();
426
+ });
427
+ }
428
+
429
+ function initDropTargets() {
430
+ // Target the entire chat area so it works for both normal and parallel mode inputs
431
+ const chatArea = document.querySelector(".chat-area");
432
+ if (!chatArea) return;
433
+
434
+ chatArea.addEventListener("dragover", (e) => {
435
+ if (!e.dataTransfer.types.includes("application/x-file-path")) return;
436
+ e.preventDefault();
437
+ e.dataTransfer.dropEffect = "copy";
438
+ // Highlight the nearest textarea
439
+ const textarea = findNearestTextarea(e.target);
440
+ if (textarea) textarea.classList.add("file-drop-hover");
441
+ });
442
+
443
+ chatArea.addEventListener("dragleave", (e) => {
444
+ const textarea = findNearestTextarea(e.target);
445
+ if (textarea) textarea.classList.remove("file-drop-hover");
446
+ });
447
+
448
+ chatArea.addEventListener("drop", (e) => {
449
+ const filePath = e.dataTransfer.getData("application/x-file-path");
450
+ if (!filePath) return;
451
+ e.preventDefault();
452
+
453
+ // Find the closest textarea to drop into
454
+ let textarea = findNearestTextarea(e.target);
455
+ if (!textarea) textarea = $.messageInput;
456
+
457
+ textarea.classList.remove("file-drop-hover");
458
+
459
+ // Insert path at cursor position (or append)
460
+ const start = textarea.selectionStart;
461
+ const end = textarea.selectionEnd;
462
+ const current = textarea.value;
463
+ const insert = filePath;
464
+
465
+ // Add space before if there's already text and it doesn't end with space/newline
466
+ const prefix = current.length > 0 && start > 0 && !/[\s\n]$/.test(current.slice(0, start)) ? " " : "";
467
+
468
+ textarea.value = current.slice(0, start) + prefix + insert + current.slice(end);
469
+ textarea.selectionStart = textarea.selectionEnd = start + prefix.length + insert.length;
470
+ textarea.focus();
471
+
472
+ // Trigger input event for auto-resize
473
+ textarea.dispatchEvent(new Event("input", { bubbles: true }));
474
+ });
475
+ }
476
+
477
+ function findNearestTextarea(el) {
478
+ // Walk up to find a textarea (message input)
479
+ if (el?.tagName === "TEXTAREA") return el;
480
+ // Check within the input-bar or parallel pane
481
+ const bar = el?.closest?.(".input-bar, .parallel-pane");
482
+ if (bar) return bar.querySelector("textarea");
483
+ return null;
484
+ }
485
+
486
+ initFileExplorer();