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.
- package/LICENSE +21 -0
- package/README.md +233 -0
- package/cli.js +2 -0
- package/config/agent-chains.json +16 -0
- package/config/agent-dags.json +16 -0
- package/config/agents.json +46 -0
- package/config/bot-prompt.json +3 -0
- package/config/folders.json +66 -0
- package/config/prompts.json +92 -0
- package/config/repos.json +86 -0
- package/config/telegram-config.json +17 -0
- package/config/workflows.json +90 -0
- package/db.js +1198 -0
- package/package.json +55 -0
- package/plugins/claude-editor/client.css +171 -0
- package/plugins/claude-editor/client.js +183 -0
- package/plugins/event-stream/client.css +207 -0
- package/plugins/event-stream/client.js +271 -0
- package/plugins/linear/client.css +345 -0
- package/plugins/linear/client.js +380 -0
- package/plugins/linear/config.json +5 -0
- package/plugins/linear/server.js +312 -0
- package/plugins/repos/client.css +549 -0
- package/plugins/repos/client.js +663 -0
- package/plugins/repos/server.js +232 -0
- package/plugins/sudoku/client.css +196 -0
- package/plugins/sudoku/client.js +329 -0
- package/plugins/tasks/client.css +414 -0
- package/plugins/tasks/client.js +394 -0
- package/plugins/tasks/server.js +116 -0
- package/plugins/tic-tac-toe/client.css +167 -0
- package/plugins/tic-tac-toe/client.js +241 -0
- package/public/css/core/components.css +232 -0
- package/public/css/core/layout.css +330 -0
- package/public/css/core/print.css +18 -0
- package/public/css/core/reset.css +36 -0
- package/public/css/core/responsive.css +378 -0
- package/public/css/core/theme.css +116 -0
- package/public/css/core/variables.css +93 -0
- package/public/css/features/agent-monitor.css +297 -0
- package/public/css/features/agent-sidebar.css +525 -0
- package/public/css/features/agents.css +996 -0
- package/public/css/features/analytics.css +181 -0
- package/public/css/features/background-sessions.css +321 -0
- package/public/css/features/cost-dashboard.css +168 -0
- package/public/css/features/home.css +313 -0
- package/public/css/features/retro-terminal.css +88 -0
- package/public/css/features/telegram.css +127 -0
- package/public/css/features/tour.css +148 -0
- package/public/css/features/voice-input.css +60 -0
- package/public/css/features/welcome.css +241 -0
- package/public/css/panels/assistant-bot.css +442 -0
- package/public/css/panels/dev-docs.css +292 -0
- package/public/css/panels/file-explorer.css +322 -0
- package/public/css/panels/git-panel.css +221 -0
- package/public/css/panels/mcp-manager.css +199 -0
- package/public/css/panels/tips-feed.css +353 -0
- package/public/css/ui/commands.css +273 -0
- package/public/css/ui/context-gauge.css +76 -0
- package/public/css/ui/file-picker.css +69 -0
- package/public/css/ui/image-attachments.css +106 -0
- package/public/css/ui/messages.css +884 -0
- package/public/css/ui/modals.css +122 -0
- package/public/css/ui/parallel.css +217 -0
- package/public/css/ui/permissions.css +110 -0
- package/public/css/ui/right-panel.css +481 -0
- package/public/css/ui/sessions.css +689 -0
- package/public/css/ui/status-bar.css +425 -0
- package/public/css/ui/toolbox.css +206 -0
- package/public/data/tips.json +218 -0
- package/public/icons/favicon.png +0 -0
- package/public/icons/icon-192.png +0 -0
- package/public/icons/icon-512.png +0 -0
- package/public/icons/whaly.png +0 -0
- package/public/index.html +1140 -0
- package/public/js/core/api.js +591 -0
- package/public/js/core/constants.js +3 -0
- package/public/js/core/dom.js +270 -0
- package/public/js/core/events.js +10 -0
- package/public/js/core/plugin-loader.js +153 -0
- package/public/js/core/store.js +39 -0
- package/public/js/core/utils.js +25 -0
- package/public/js/core/ws.js +64 -0
- package/public/js/features/agent-monitor.js +222 -0
- package/public/js/features/agents.js +1209 -0
- package/public/js/features/analytics.js +397 -0
- package/public/js/features/attachments.js +251 -0
- package/public/js/features/background-sessions.js +475 -0
- package/public/js/features/chat.js +589 -0
- package/public/js/features/cost-dashboard.js +152 -0
- package/public/js/features/dag-editor.js +399 -0
- package/public/js/features/easter-egg.js +46 -0
- package/public/js/features/home.js +270 -0
- package/public/js/features/projects.js +372 -0
- package/public/js/features/prompts.js +228 -0
- package/public/js/features/sessions.js +332 -0
- package/public/js/features/telegram.js +131 -0
- package/public/js/features/tour.js +210 -0
- package/public/js/features/voice-input.js +185 -0
- package/public/js/features/welcome.js +43 -0
- package/public/js/features/workflows.js +277 -0
- package/public/js/main.js +51 -0
- package/public/js/panels/assistant-bot.js +445 -0
- package/public/js/panels/dev-docs.js +380 -0
- package/public/js/panels/file-explorer.js +486 -0
- package/public/js/panels/git-panel.js +285 -0
- package/public/js/panels/mcp-manager.js +311 -0
- package/public/js/panels/tips-feed.js +303 -0
- package/public/js/ui/commands.js +114 -0
- package/public/js/ui/context-gauge.js +100 -0
- package/public/js/ui/diff.js +124 -0
- package/public/js/ui/disabled-tools.js +36 -0
- package/public/js/ui/export.js +74 -0
- package/public/js/ui/formatting.js +206 -0
- package/public/js/ui/header-dropdowns.js +72 -0
- package/public/js/ui/input-meta.js +71 -0
- package/public/js/ui/max-turns.js +21 -0
- package/public/js/ui/messages.js +387 -0
- package/public/js/ui/model-selector.js +20 -0
- package/public/js/ui/notifications.js +232 -0
- package/public/js/ui/parallel.js +176 -0
- package/public/js/ui/permissions.js +168 -0
- package/public/js/ui/right-panel.js +173 -0
- package/public/js/ui/shortcuts.js +143 -0
- package/public/js/ui/sidebar-toggle.js +29 -0
- package/public/js/ui/status-bar.js +172 -0
- package/public/js/ui/tab-sdk.js +623 -0
- package/public/js/ui/theme.js +38 -0
- package/public/manifest.json +13 -0
- package/public/offline.html +190 -0
- package/public/style.css +42 -0
- package/public/sw.js +91 -0
- package/server/agent-loop.js +385 -0
- package/server/dag-executor.js +265 -0
- package/server/orchestrator.js +514 -0
- package/server/paths.js +61 -0
- package/server/plugin-mount.js +56 -0
- package/server/push-sender.js +31 -0
- package/server/routes/agents.js +294 -0
- package/server/routes/bot.js +45 -0
- package/server/routes/exec.js +35 -0
- package/server/routes/files.js +218 -0
- package/server/routes/mcp.js +82 -0
- package/server/routes/messages.js +36 -0
- package/server/routes/notifications.js +37 -0
- package/server/routes/projects.js +207 -0
- package/server/routes/prompts.js +53 -0
- package/server/routes/sessions.js +103 -0
- package/server/routes/stats.js +143 -0
- package/server/routes/telegram.js +71 -0
- package/server/routes/tips.js +135 -0
- package/server/routes/workflows.js +81 -0
- package/server/summarizer.js +55 -0
- package/server/telegram-poller.js +205 -0
- package/server/telegram-sender.js +304 -0
- package/server/ws-handler.js +926 -0
- 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();
|