claudeck 1.2.0 → 1.3.1

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 (42) hide show
  1. package/README.md +64 -5
  2. package/cli.js +53 -4
  3. package/package.json +3 -2
  4. package/public/css/core/responsive.css +2 -2
  5. package/public/css/ui/file-picker.css +243 -17
  6. package/public/css/ui/messages.css +72 -9
  7. package/public/css/ui/toolbox.css +43 -0
  8. package/public/index.html +80 -745
  9. package/public/js/components/add-project-modal.js +27 -0
  10. package/public/js/components/agent-modal.js +73 -0
  11. package/public/js/components/agent-monitor-modal.js +19 -0
  12. package/public/js/components/bg-confirm-modal.js +22 -0
  13. package/public/js/components/chain-modal.js +52 -0
  14. package/public/js/components/cost-dashboard-modal.js +39 -0
  15. package/public/js/components/dag-editor-modal.js +55 -0
  16. package/public/js/components/file-picker-modal.js +45 -0
  17. package/public/js/components/linear-create-modal.js +43 -0
  18. package/public/js/components/mcp-modal.js +58 -0
  19. package/public/js/components/orchestrate-modal.js +40 -0
  20. package/public/js/components/permission-modal.js +30 -0
  21. package/public/js/components/prompt-modal.js +31 -0
  22. package/public/js/components/shortcuts-modal.js +45 -0
  23. package/public/js/components/status-bar.js +97 -0
  24. package/public/js/components/system-prompt-modal.js +29 -0
  25. package/public/js/components/telegram-modal.js +84 -0
  26. package/public/js/components/welcome-overlay.js +60 -0
  27. package/public/js/components/workflow-modal.js +41 -0
  28. package/public/js/core/api.js +10 -0
  29. package/public/js/core/dom.js +3 -2
  30. package/public/js/core/ws.js +7 -1
  31. package/public/js/features/attachments.js +226 -23
  32. package/public/js/features/projects.js +7 -0
  33. package/public/js/main.js +22 -0
  34. package/public/js/ui/shortcuts.js +4 -8
  35. package/public/login.html +470 -0
  36. package/public/offline.html +300 -168
  37. package/public/sw.js +10 -2
  38. package/server/agent-loop.js +1 -0
  39. package/server/auth.js +141 -0
  40. package/server/orchestrator.js +1 -0
  41. package/server/ws-handler.js +2 -0
  42. package/server.js +14 -3
@@ -7,6 +7,45 @@ import { registerCommand } from '../ui/commands.js';
7
7
  const SUPPORTED_IMAGE_TYPES = ["image/png", "image/jpeg", "image/gif", "image/webp"];
8
8
  const MAX_IMAGE_SIZE = 5 * 1024 * 1024; // 5MB
9
9
 
10
+ // ── File type categorization ─────────────────────────────
11
+ const FILE_TYPES = {
12
+ code: new Set(['js','jsx','ts','tsx','mjs','cjs','py','go','rs','java','rb','php','c','cpp','h','hpp','swift','kt','scala','sh','bash','zsh','lua','r','pl','ex','exs','elm','hs','clj','dart','cs','vue','svelte']),
13
+ config: new Set(['json','yaml','yml','toml','env','ini','lock','conf','cfg','properties','editorconfig','gitignore','dockerignore','eslintrc','prettierrc','babelrc','nvmrc']),
14
+ markup: new Set(['html','htm','css','scss','sass','less','xml','svg','astro','njk','ejs','hbs','pug','styl']),
15
+ docs: new Set(['md','txt','rst','adoc','tex','org','log']),
16
+ data: new Set(['csv','sql','graphql','gql','prisma','tsv']),
17
+ };
18
+
19
+ const BINARY_EXTENSIONS = new Set([
20
+ 'png','jpg','jpeg','gif','webp','bmp','ico','tiff','tif',
21
+ 'pdf','zip','gz','tar','rar','7z','bz2','xz',
22
+ 'exe','dll','so','dylib','o','a','wasm','bin','dat',
23
+ 'db','sqlite','sqlite3',
24
+ 'mp3','mp4','avi','mov','flv','wmv','wav','ogg','flac','aac','m4a',
25
+ 'ttf','otf','woff','woff2','eot',
26
+ 'class','jar','pyc','pyo',
27
+ 'DS_Store',
28
+ ]);
29
+
30
+ function getFileExt(filePath) {
31
+ const dot = filePath.lastIndexOf('.');
32
+ if (dot < 0) return '';
33
+ return filePath.slice(dot + 1).toLowerCase();
34
+ }
35
+
36
+ function getFileCategory(filePath) {
37
+ const ext = getFileExt(filePath);
38
+ if (BINARY_EXTENSIONS.has(ext)) return 'binary';
39
+ for (const [cat, exts] of Object.entries(FILE_TYPES)) {
40
+ if (exts.has(ext)) return cat;
41
+ }
42
+ return 'default';
43
+ }
44
+
45
+ function isBinaryFile(filePath) {
46
+ return BINARY_EXTENSIONS.has(getFileExt(filePath));
47
+ }
48
+
10
49
  // ── Badge ────────────────────────────────────────────────
11
50
  export function updateAttachmentBadge() {
12
51
  const attachedFiles = getState("attachedFiles");
@@ -20,7 +59,59 @@ export function updateAttachmentBadge() {
20
59
  }
21
60
  }
22
61
 
23
- // ── File picker (existing) ───────────────────────────────
62
+ // ── Selected chips ───────────────────────────────────────
63
+ function renderSelectedChips() {
64
+ const attachedFiles = getState("attachedFiles");
65
+ $.fpSelected.innerHTML = "";
66
+
67
+ if (attachedFiles.length === 0) {
68
+ $.fpSelected.classList.add("hidden");
69
+ return;
70
+ }
71
+
72
+ $.fpSelected.classList.remove("hidden");
73
+ for (const file of attachedFiles) {
74
+ const chip = document.createElement("div");
75
+ chip.className = "fp-chip";
76
+
77
+ const name = document.createElement("span");
78
+ name.className = "fp-chip-name";
79
+ name.textContent = file.path;
80
+ name.title = file.path;
81
+
82
+ const removeBtn = document.createElement("button");
83
+ removeBtn.className = "fp-chip-remove";
84
+ removeBtn.textContent = "\u00d7";
85
+ removeBtn.title = "Remove";
86
+ removeBtn.addEventListener("click", (e) => {
87
+ e.stopPropagation();
88
+ removeFileByPath(file.path);
89
+ });
90
+
91
+ chip.appendChild(name);
92
+ chip.appendChild(removeBtn);
93
+ $.fpSelected.appendChild(chip);
94
+ }
95
+ }
96
+
97
+ function removeFileByPath(path) {
98
+ const attachedFiles = getState("attachedFiles").filter(f => f.path !== path);
99
+ setState("attachedFiles", attachedFiles);
100
+ renderSelectedChips();
101
+ updateFooterCount();
102
+ updateAttachmentBadge();
103
+ // Update item in list if visible
104
+ const items = $.fpList.querySelectorAll(".file-picker-item");
105
+ for (const item of items) {
106
+ if (item.dataset.path === path) {
107
+ item.classList.remove("selected");
108
+ const check = item.querySelector(".fp-check");
109
+ if (check) check.style.opacity = "0";
110
+ }
111
+ }
112
+ }
113
+
114
+ // ── File picker ──────────────────────────────────────────
24
115
  export async function openFilePicker() {
25
116
  const cwd = $.projectSelect.value;
26
117
  if (!cwd) return;
@@ -48,38 +139,144 @@ export function renderFilePicker(filter) {
48
139
  ? allProjectFiles.filter((f) => f.toLowerCase().includes(lower))
49
140
  : allProjectFiles;
50
141
 
51
- for (const filePath of filtered.slice(0, 200)) {
142
+ const visible = filtered.slice(0, 200);
143
+
144
+ // Show/hide empty state
145
+ if (visible.length === 0) {
146
+ $.fpEmpty.classList.remove("hidden");
147
+ $.fpList.style.display = "none";
148
+ } else {
149
+ $.fpEmpty.classList.add("hidden");
150
+ $.fpList.style.display = "";
151
+ }
152
+
153
+ for (const filePath of visible) {
154
+ const category = getFileCategory(filePath);
155
+ const binary = category === 'binary';
156
+ const isSelected = attachedFiles.some((f) => f.path === filePath);
157
+
52
158
  const item = document.createElement("div");
53
159
  item.className = "file-picker-item";
54
- const isSelected = attachedFiles.some((f) => f.path === filePath);
160
+ item.dataset.path = filePath;
55
161
  if (isSelected) item.classList.add("selected");
56
- item.textContent = filePath;
57
- item.addEventListener("click", () => toggleFileAttachment(filePath, item));
162
+ if (binary) item.classList.add("binary-warn");
163
+
164
+ // Type dot
165
+ const dot = document.createElement("span");
166
+ dot.className = `fp-type-dot type-${category}`;
167
+ item.appendChild(dot);
168
+
169
+ // File path
170
+ const pathEl = document.createElement("span");
171
+ pathEl.className = "fp-path";
172
+ pathEl.textContent = filePath;
173
+ item.appendChild(pathEl);
174
+
175
+ if (binary) {
176
+ // Binary label
177
+ const label = document.createElement("span");
178
+ label.className = "fp-binary-label";
179
+ label.textContent = "binary";
180
+ item.appendChild(label);
181
+ } else {
182
+ // Checkmark
183
+ const check = document.createElement("span");
184
+ check.className = "fp-check";
185
+ check.textContent = "\u2713";
186
+ item.appendChild(check);
187
+
188
+ item.addEventListener("click", () => toggleFileAttachment(filePath, item));
189
+ }
190
+
58
191
  $.fpList.appendChild(item);
59
192
  }
193
+
194
+ renderSelectedChips();
195
+ updateFooterCount();
196
+ }
197
+
198
+ function updateFooterCount() {
199
+ const attachedFiles = getState("attachedFiles");
200
+ const count = attachedFiles.length;
201
+ $.fpCount.textContent = `${count} file${count !== 1 ? "s" : ""} selected`;
60
202
  }
61
203
 
62
204
  async function toggleFileAttachment(filePath, itemEl) {
63
205
  const attachedFiles = [...getState("attachedFiles")];
64
206
  const idx = attachedFiles.findIndex((f) => f.path === filePath);
207
+
65
208
  if (idx >= 0) {
209
+ // Deselect
66
210
  attachedFiles.splice(idx, 1);
67
211
  setState("attachedFiles", attachedFiles);
68
212
  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
- }
213
+ renderSelectedChips();
214
+ updateFooterCount();
215
+ updateAttachmentBadge();
216
+ return;
80
217
  }
81
- $.fpCount.textContent = `${attachedFiles.length} file${attachedFiles.length !== 1 ? "s" : ""} selected`;
82
- updateAttachmentBadge();
218
+
219
+ // Show loading state
220
+ itemEl.classList.add("loading");
221
+ const check = itemEl.querySelector(".fp-check");
222
+ if (check) check.style.display = "none";
223
+ const spinner = document.createElement("span");
224
+ spinner.className = "fp-spinner";
225
+ itemEl.appendChild(spinner);
226
+
227
+ try {
228
+ const cwd = $.projectSelect.value;
229
+ const data = await api.fetchFileContent(cwd, filePath);
230
+ // Remove spinner, show selected
231
+ spinner.remove();
232
+ if (check) check.style.display = "";
233
+ itemEl.classList.remove("loading");
234
+ itemEl.classList.add("selected");
235
+
236
+ const updated = [...getState("attachedFiles")];
237
+ updated.push({ path: filePath, content: data.content });
238
+ setState("attachedFiles", updated);
239
+ renderSelectedChips();
240
+ updateFooterCount();
241
+ updateAttachmentBadge();
242
+ } catch (err) {
243
+ // Remove spinner, show error
244
+ spinner.remove();
245
+ if (check) check.style.display = "";
246
+ itemEl.classList.remove("loading");
247
+ showItemError(itemEl, parseFileError(err.message));
248
+ }
249
+ }
250
+
251
+ function parseFileError(message) {
252
+ if (message.includes("50KB") || message.includes("too large") || message.includes("413")) {
253
+ return "Too large (50KB limit)";
254
+ }
255
+ if (message.includes("ENOENT") || message.includes("not found")) {
256
+ return "File not found";
257
+ }
258
+ if (message.includes("EACCES") || message.includes("permission")) {
259
+ return "Permission denied";
260
+ }
261
+ return "Cannot read file";
262
+ }
263
+
264
+ function showItemError(itemEl, message) {
265
+ itemEl.classList.add("error");
266
+ // Remove existing error msg if any
267
+ const existing = itemEl.querySelector(".fp-error-msg");
268
+ if (existing) existing.remove();
269
+
270
+ const errorMsg = document.createElement("span");
271
+ errorMsg.className = "fp-error-msg";
272
+ errorMsg.textContent = message;
273
+ itemEl.appendChild(errorMsg);
274
+
275
+ // Auto-clear after 3s
276
+ setTimeout(() => {
277
+ itemEl.classList.remove("error");
278
+ errorMsg.remove();
279
+ }, 3000);
83
280
  }
84
281
 
85
282
  function closeFilePicker() {
@@ -89,11 +286,11 @@ function closeFilePicker() {
89
286
  // ── Image attachments ────────────────────────────────────
90
287
  export function addImageAttachment(file) {
91
288
  if (!SUPPORTED_IMAGE_TYPES.includes(file.type)) {
92
- showImageError(`Unsupported image type: ${file.type}. Use PNG, JPEG, GIF, or WebP.`);
289
+ showImageError(`Unsupported type: ${file.type || "unknown"}. Use PNG, JPEG, GIF, or WebP.`);
93
290
  return;
94
291
  }
95
292
  if (file.size > MAX_IMAGE_SIZE) {
96
- showImageError(`Image too large (${(file.size / 1024 / 1024).toFixed(1)}MB). Max 5MB.`);
293
+ showImageError(`Image too large (${(file.size / 1024 / 1024).toFixed(1)} MB). Max 5 MB.`);
97
294
  return;
98
295
  }
99
296
 
@@ -145,7 +342,7 @@ function renderImagePreview() {
145
342
  const imgEl = document.createElement("img");
146
343
  imgEl.src = `data:${img.mimeType};base64,${img.data}`;
147
344
  imgEl.alt = img.name;
148
- imgEl.title = img.name;
345
+ imgEl.title = `${img.name}`;
149
346
 
150
347
  const removeBtn = document.createElement("button");
151
348
  removeBtn.className = "image-preview-remove";
@@ -162,7 +359,6 @@ function renderImagePreview() {
162
359
  }
163
360
 
164
361
  function showImageError(message) {
165
- // Use toast container if available
166
362
  const container = document.getElementById("toast-container");
167
363
  if (container) {
168
364
  const toast = document.createElement("div");
@@ -203,7 +399,6 @@ $.imageFileInput.addEventListener("change", () => {
203
399
 
204
400
  // Paste handler — detect images in clipboard
205
401
  document.addEventListener("paste", (e) => {
206
- // Only handle when focus is in the chat input area
207
402
  const active = document.activeElement;
208
403
  if (active !== $.messageInput && !$.messageInput.contains(active)) return;
209
404
 
@@ -234,11 +429,19 @@ $.messageInput.addEventListener("drop", (e) => {
234
429
  $.messageInput.classList.remove("drag-highlight");
235
430
  if (!e.dataTransfer.files.length) return;
236
431
  e.preventDefault();
432
+
433
+ let hasUnsupported = false;
237
434
  for (const file of e.dataTransfer.files) {
238
435
  if (SUPPORTED_IMAGE_TYPES.includes(file.type)) {
239
436
  addImageAttachment(file);
437
+ } else {
438
+ hasUnsupported = true;
240
439
  }
241
440
  }
441
+
442
+ if (hasUnsupported) {
443
+ showImageError("Only images (PNG, JPEG, GIF, WebP) can be dropped here. Use the attach button for code files.");
444
+ }
242
445
  });
243
446
 
244
447
  // ── Commands ─────────────────────────────────────────────
@@ -8,6 +8,7 @@ import { panes } from '../ui/parallel.js';
8
8
  import { loadSessions } from './sessions.js';
9
9
  import { loadStats } from './cost-dashboard.js';
10
10
  import { showWhalyPlaceholder, addSkillUsedMessage } from '../ui/messages.js';
11
+ import { updateAttachmentBadge, clearImageAttachments } from './attachments.js';
11
12
 
12
13
  export async function loadProjects() {
13
14
  try {
@@ -362,6 +363,12 @@ $.projectSelect.addEventListener("change", async () => {
362
363
  if ($.projectSelect.value) {
363
364
  setState("view", "chat");
364
365
  }
366
+ // Clear attachments and input on project switch
367
+ setState("attachedFiles", []);
368
+ setState("allProjectFiles", []);
369
+ clearImageAttachments();
370
+ updateAttachmentBadge();
371
+ $.messageInput.value = "";
365
372
  updateSystemPromptIndicator();
366
373
  updateHeaderProjectName();
367
374
  updateSessionControls();
package/public/js/main.js CHANGED
@@ -1,4 +1,26 @@
1
1
  // Entry point — imports all modules and runs boot sequence
2
+
3
+ // Web Components — must load before dom.js so getElementById finds their elements
4
+ import './components/welcome-overlay.js';
5
+ import './components/orchestrate-modal.js';
6
+ import './components/agent-monitor-modal.js';
7
+ import './components/dag-editor-modal.js';
8
+ import './components/chain-modal.js';
9
+ import './components/agent-modal.js';
10
+ import './components/workflow-modal.js';
11
+ import './components/prompt-modal.js';
12
+ import './components/system-prompt-modal.js';
13
+ import './components/file-picker-modal.js';
14
+ import './components/shortcuts-modal.js';
15
+ import './components/cost-dashboard-modal.js';
16
+ import './components/bg-confirm-modal.js';
17
+ import './components/permission-modal.js';
18
+ import './components/linear-create-modal.js';
19
+ import './components/telegram-modal.js';
20
+ import './components/mcp-modal.js';
21
+ import './components/add-project-modal.js';
22
+ import './components/status-bar.js';
23
+
2
24
  import './core/store.js';
3
25
  import './core/dom.js';
4
26
  import './core/constants.js';
@@ -11,12 +11,8 @@ function closeAllModals() {
11
11
  document.querySelectorAll(".modal-overlay:not([data-persistent])").forEach((m) => m.classList.add("hidden"));
12
12
  }
13
13
 
14
- document.getElementById("shortcuts-modal-close").addEventListener("click", () => {
15
- $.shortcutsModal.classList.add("hidden");
16
- });
17
- $.shortcutsModal.addEventListener("click", (e) => {
18
- if (e.target === $.shortcutsModal) $.shortcutsModal.classList.add("hidden");
19
- });
14
+ // Shortcuts modal ref — rendered by <claudeck-shortcuts-modal> web component
15
+ const shortcutsModal = document.getElementById("shortcuts-modal");
20
16
 
21
17
  document.addEventListener("keydown", (e) => {
22
18
  const isMeta = e.metaKey || e.ctrlKey;
@@ -43,7 +39,7 @@ document.addEventListener("keydown", (e) => {
43
39
 
44
40
  if (isMeta && e.key === "/") {
45
41
  e.preventDefault();
46
- $.shortcutsModal.classList.toggle("hidden");
42
+ shortcutsModal.classList.toggle("hidden");
47
43
  return;
48
44
  }
49
45
 
@@ -113,7 +109,7 @@ registerCommand("shortcuts", {
113
109
  category: "app",
114
110
  description: "Show keyboard shortcuts",
115
111
  execute() {
116
- $.shortcutsModal.classList.remove("hidden");
112
+ shortcutsModal.classList.remove("hidden");
117
113
  },
118
114
  });
119
115