clay-server 3.7.0-beta.1 → 3.7.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/lib/project-http.js +51 -0
- package/lib/project.js +2 -1
- package/lib/public/css/filebrowser.css +117 -7
- package/lib/public/css/tui-attention.css +1 -2
- package/lib/public/index.html +3 -2
- package/lib/public/modules/filebrowser-context-menu.js +148 -0
- package/lib/public/modules/filebrowser.js +41 -4
- package/lib/public/modules/input.js +1 -1
- package/lib/public/modules/session-tui-view.js +2 -3
- package/lib/public/modules/theme.js +4 -2
- package/lib/public/modules/tui-attention.js +5 -8
- package/lib/public/style.css +2 -2
- package/package.json +1 -1
package/lib/project-http.js
CHANGED
|
@@ -254,6 +254,57 @@ function attachHTTP(ctx) {
|
|
|
254
254
|
return true;
|
|
255
255
|
}
|
|
256
256
|
|
|
257
|
+
// File browser: download project files
|
|
258
|
+
if (req.method === "GET" && urlPath.startsWith("/api/file/download?")) {
|
|
259
|
+
if (usersModule.isMultiUser()) {
|
|
260
|
+
var downloadUser = req._clayUser;
|
|
261
|
+
var downloadPermissions = downloadUser ? usersModule.getEffectivePermissions(downloadUser, osUsers) : null;
|
|
262
|
+
if (!downloadPermissions || !downloadPermissions.fileBrowser) {
|
|
263
|
+
res.writeHead(403, { "Content-Type": "text/plain; charset=utf-8" });
|
|
264
|
+
res.end("File browser access is not permitted");
|
|
265
|
+
return true;
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
var downloadQueryIndex = urlPath.indexOf("?");
|
|
270
|
+
var downloadParams = new URLSearchParams(urlPath.substring(downloadQueryIndex));
|
|
271
|
+
var downloadPath = downloadParams.get("path");
|
|
272
|
+
if (!downloadPath) { res.writeHead(400); res.end("Missing path"); return true; }
|
|
273
|
+
var downloadFile = safePath(cwd, downloadPath);
|
|
274
|
+
if (!downloadFile && getOsUserInfoForReq(req)) {
|
|
275
|
+
downloadFile = safeAbsPath(downloadPath);
|
|
276
|
+
}
|
|
277
|
+
if (!downloadFile) { res.writeHead(403); res.end("Access denied"); return true; }
|
|
278
|
+
|
|
279
|
+
try {
|
|
280
|
+
var downloadUserInfo = getOsUserInfoForReq(req);
|
|
281
|
+
var downloadContent;
|
|
282
|
+
if (downloadUserInfo) {
|
|
283
|
+
downloadContent = fsAsUser("read_binary", { file: downloadFile }, downloadUserInfo).buffer;
|
|
284
|
+
} else {
|
|
285
|
+
downloadContent = fs.readFileSync(downloadFile);
|
|
286
|
+
}
|
|
287
|
+
var downloadName = path.basename(downloadPath).replace(/[\x00-\x1f\x7f"\\]/g, "_") || "download";
|
|
288
|
+
var asciiDownloadName = downloadName.replace(/[^\x20-\x7e]/g, "_");
|
|
289
|
+
var encodedDownloadName = encodeURIComponent(downloadName).replace(/[!'()*]/g, function (character) {
|
|
290
|
+
return "%" + character.charCodeAt(0).toString(16).toUpperCase();
|
|
291
|
+
});
|
|
292
|
+
var contentDisposition = "attachment; filename=\"" + asciiDownloadName + "\"; filename*=UTF-8''" + encodedDownloadName;
|
|
293
|
+
res.writeHead(200, {
|
|
294
|
+
"Content-Type": "application/octet-stream",
|
|
295
|
+
"Content-Length": downloadContent.length,
|
|
296
|
+
"Content-Disposition": contentDisposition,
|
|
297
|
+
"Cache-Control": "no-store",
|
|
298
|
+
"X-Content-Type-Options": "nosniff",
|
|
299
|
+
});
|
|
300
|
+
res.end(downloadContent);
|
|
301
|
+
} catch (e) {
|
|
302
|
+
res.writeHead(404);
|
|
303
|
+
res.end("Not found");
|
|
304
|
+
}
|
|
305
|
+
return true;
|
|
306
|
+
}
|
|
307
|
+
|
|
257
308
|
// File browser: serve project images
|
|
258
309
|
if (req.method === "GET" && urlPath.startsWith("/api/file?")) {
|
|
259
310
|
var qIdx = urlPath.indexOf("?");
|
package/lib/project.js
CHANGED
|
@@ -111,6 +111,7 @@ var BINARY_EXTS = new Set([
|
|
|
111
111
|
]);
|
|
112
112
|
var IMAGE_EXTS = new Set([".png", ".jpg", ".jpeg", ".gif", ".webp", ".svg", ".bmp", ".ico"]);
|
|
113
113
|
var FS_MAX_SIZE = 512 * 1024;
|
|
114
|
+
var FS_VIEWER_MAX_SIZE = 5 * 1024 * 1024;
|
|
114
115
|
function safePath(base, requested) {
|
|
115
116
|
var resolved = path.resolve(base, requested);
|
|
116
117
|
if (resolved !== base && !resolved.startsWith(base + path.sep)) return null;
|
|
@@ -1438,7 +1439,7 @@ function createProjectContext(opts) {
|
|
|
1438
1439
|
IGNORED_DIRS: IGNORED_DIRS,
|
|
1439
1440
|
BINARY_EXTS: BINARY_EXTS,
|
|
1440
1441
|
IMAGE_EXTS: IMAGE_EXTS,
|
|
1441
|
-
FS_MAX_SIZE:
|
|
1442
|
+
FS_MAX_SIZE: FS_VIEWER_MAX_SIZE,
|
|
1442
1443
|
});
|
|
1443
1444
|
|
|
1444
1445
|
// --- MCP bridge handler for Codex and session-bound Kiro tools ---
|
|
@@ -259,6 +259,11 @@
|
|
|
259
259
|
|
|
260
260
|
.file-tree-item:hover { background: var(--sidebar-hover); color: var(--text); }
|
|
261
261
|
.file-tree-item.active { background: var(--sidebar-active); color: var(--text); }
|
|
262
|
+
.file-tree-item.context-open {
|
|
263
|
+
background: var(--sidebar-active);
|
|
264
|
+
color: var(--text);
|
|
265
|
+
box-shadow: inset 0 0 0 1px var(--border);
|
|
266
|
+
}
|
|
262
267
|
/* Keyboard focus ring (arrow-key navigation) — separate from .active
|
|
263
268
|
so the currently-opened file stays visually marked even while the
|
|
264
269
|
user is exploring elsewhere with the arrows. */
|
|
@@ -324,6 +329,65 @@
|
|
|
324
329
|
color: var(--error);
|
|
325
330
|
}
|
|
326
331
|
|
|
332
|
+
/* --- File tree context menu --- */
|
|
333
|
+
.file-tree-context-menu {
|
|
334
|
+
position: fixed;
|
|
335
|
+
z-index: 10040;
|
|
336
|
+
min-width: 164px;
|
|
337
|
+
padding: 5px;
|
|
338
|
+
border: 1px solid var(--border);
|
|
339
|
+
border-radius: 10px;
|
|
340
|
+
background: var(--sidebar-bg);
|
|
341
|
+
box-shadow: 0 12px 34px rgba(var(--shadow-rgb), 0.34);
|
|
342
|
+
animation: file-tree-context-in 0.12s ease-out both;
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
.file-tree-context-item {
|
|
346
|
+
display: flex;
|
|
347
|
+
align-items: center;
|
|
348
|
+
gap: 9px;
|
|
349
|
+
width: 100%;
|
|
350
|
+
min-height: 34px;
|
|
351
|
+
padding: 7px 10px;
|
|
352
|
+
border: 0;
|
|
353
|
+
border-radius: 7px;
|
|
354
|
+
background: transparent;
|
|
355
|
+
color: var(--text-secondary);
|
|
356
|
+
font: inherit;
|
|
357
|
+
font-size: 13px;
|
|
358
|
+
text-align: left;
|
|
359
|
+
cursor: pointer;
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
.file-tree-context-item:hover,
|
|
363
|
+
.file-tree-context-item:focus-visible {
|
|
364
|
+
outline: none;
|
|
365
|
+
background: rgba(var(--overlay-rgb), 0.06);
|
|
366
|
+
color: var(--text);
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
.file-tree-context-item .lucide {
|
|
370
|
+
width: 15px;
|
|
371
|
+
height: 15px;
|
|
372
|
+
flex-shrink: 0;
|
|
373
|
+
color: var(--text-muted);
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
.file-tree-context-separator {
|
|
377
|
+
height: 1px;
|
|
378
|
+
margin: 4px 5px;
|
|
379
|
+
background: var(--border-subtle);
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
@keyframes file-tree-context-in {
|
|
383
|
+
from { opacity: 0; transform: translateY(-3px) scale(0.98); }
|
|
384
|
+
to { opacity: 1; transform: translateY(0) scale(1); }
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
@media (prefers-reduced-motion: reduce) {
|
|
388
|
+
.file-tree-context-menu { animation: none; }
|
|
389
|
+
}
|
|
390
|
+
|
|
327
391
|
/* --- Panel fullscreen --- */
|
|
328
392
|
@media (min-width: 1024px) {
|
|
329
393
|
#main-column:has(.panel-fullscreen:not(.hidden)) > .title-bar-content { display: none; }
|
|
@@ -1695,6 +1759,51 @@
|
|
|
1695
1759
|
padding: 0;
|
|
1696
1760
|
}
|
|
1697
1761
|
|
|
1762
|
+
.file-viewer-large-text {
|
|
1763
|
+
min-height: 100%;
|
|
1764
|
+
}
|
|
1765
|
+
|
|
1766
|
+
.file-viewer-large-notice {
|
|
1767
|
+
position: sticky;
|
|
1768
|
+
top: 0;
|
|
1769
|
+
left: 0;
|
|
1770
|
+
z-index: 2;
|
|
1771
|
+
display: flex;
|
|
1772
|
+
align-items: center;
|
|
1773
|
+
gap: 7px;
|
|
1774
|
+
width: fit-content;
|
|
1775
|
+
margin: 10px 12px 0;
|
|
1776
|
+
padding: 6px 9px;
|
|
1777
|
+
color: var(--text-muted);
|
|
1778
|
+
background: color-mix(in srgb, var(--bg) 92%, var(--accent));
|
|
1779
|
+
border: 1px solid var(--border-subtle);
|
|
1780
|
+
border-radius: 7px;
|
|
1781
|
+
font-size: 12px;
|
|
1782
|
+
line-height: 1.35;
|
|
1783
|
+
}
|
|
1784
|
+
|
|
1785
|
+
.file-viewer-large-notice svg {
|
|
1786
|
+
width: 14px;
|
|
1787
|
+
height: 14px;
|
|
1788
|
+
flex-shrink: 0;
|
|
1789
|
+
}
|
|
1790
|
+
|
|
1791
|
+
.file-viewer-large-text pre {
|
|
1792
|
+
width: max-content;
|
|
1793
|
+
min-width: 100%;
|
|
1794
|
+
margin: 0;
|
|
1795
|
+
padding: 12px 14px;
|
|
1796
|
+
box-sizing: border-box;
|
|
1797
|
+
white-space: pre;
|
|
1798
|
+
tab-size: 2;
|
|
1799
|
+
}
|
|
1800
|
+
|
|
1801
|
+
.file-viewer-large-text code {
|
|
1802
|
+
font-family: var(--font-mono);
|
|
1803
|
+
font-size: 12px;
|
|
1804
|
+
line-height: 1.55;
|
|
1805
|
+
}
|
|
1806
|
+
|
|
1698
1807
|
.file-viewer-binary {
|
|
1699
1808
|
display: flex;
|
|
1700
1809
|
align-items: center;
|
|
@@ -1948,6 +2057,7 @@
|
|
|
1948
2057
|
min-height: 0;
|
|
1949
2058
|
overflow: hidden;
|
|
1950
2059
|
position: relative;
|
|
2060
|
+
background: #141412;
|
|
1951
2061
|
}
|
|
1952
2062
|
|
|
1953
2063
|
.terminal-tab-body {
|
|
@@ -1967,8 +2077,8 @@
|
|
|
1967
2077
|
align-items: center;
|
|
1968
2078
|
gap: 6px;
|
|
1969
2079
|
padding: 6px 8px;
|
|
1970
|
-
background:
|
|
1971
|
-
border-bottom: 1px solid
|
|
2080
|
+
background: var(--bg-alt);
|
|
2081
|
+
border-bottom: 1px solid var(--border);
|
|
1972
2082
|
flex-shrink: 0;
|
|
1973
2083
|
overflow-x: auto;
|
|
1974
2084
|
-webkit-overflow-scrolling: touch;
|
|
@@ -1984,9 +2094,9 @@
|
|
|
1984
2094
|
min-width: 40px;
|
|
1985
2095
|
padding: 0 10px;
|
|
1986
2096
|
border-radius: 6px;
|
|
1987
|
-
border: 1px solid
|
|
1988
|
-
background:
|
|
1989
|
-
color:
|
|
2097
|
+
border: 1px solid var(--border);
|
|
2098
|
+
background: var(--sidebar-active);
|
|
2099
|
+
color: var(--text-secondary);
|
|
1990
2100
|
font-family: "Roboto Mono", monospace;
|
|
1991
2101
|
font-size: 12px;
|
|
1992
2102
|
font-weight: 500;
|
|
@@ -1998,8 +2108,8 @@
|
|
|
1998
2108
|
}
|
|
1999
2109
|
|
|
2000
2110
|
.term-key:active {
|
|
2001
|
-
background:
|
|
2002
|
-
border-color:
|
|
2111
|
+
background: var(--sidebar-hover);
|
|
2112
|
+
border-color: var(--text-dimmer);
|
|
2003
2113
|
}
|
|
2004
2114
|
|
|
2005
2115
|
.term-key-toggle.active {
|
|
@@ -282,7 +282,7 @@ body.tui-suspended #tui-resume-bar {
|
|
|
282
282
|
.tui-modal {
|
|
283
283
|
width: min(960px, 100%);
|
|
284
284
|
height: min(640px, 100%);
|
|
285
|
-
background:
|
|
285
|
+
background: var(--bg);
|
|
286
286
|
border-radius: 12px;
|
|
287
287
|
overflow: hidden;
|
|
288
288
|
display: flex;
|
|
@@ -683,4 +683,3 @@ body.tui-suspended #tui-resume-bar {
|
|
|
683
683
|
.wna-content { padding: 32px 20px 80px; }
|
|
684
684
|
.wna-title { font-size: 26px; }
|
|
685
685
|
}
|
|
686
|
-
|
package/lib/public/index.html
CHANGED
|
@@ -30,7 +30,7 @@
|
|
|
30
30
|
(function(){try{var k="clay-theme-vars",v=localStorage.getItem(k),r=document.documentElement;if(v){var o=JSON.parse(v),p;for(p in o)r.style.setProperty(p,o[p]);var vt=localStorage.getItem(k.replace("-vars","-variant"));if(vt==="light"){r.classList.add("light-theme");r.classList.remove("dark-theme")}else{r.classList.add("dark-theme");r.classList.remove("light-theme")}var m=document.querySelector('meta[name="theme-color"]');if(m&&o["--bg"])m.setAttribute("content",o["--bg"])}else{var sl=window.matchMedia&&window.matchMedia("(prefers-color-scheme: light)").matches;if(sl){r.classList.add("light-theme");r.classList.remove("dark-theme")}}}catch(e){}})();
|
|
31
31
|
</script>
|
|
32
32
|
<script>if(window.navigator.standalone||window.matchMedia("(display-mode:standalone)").matches){document.documentElement.classList.add("pwa-standalone")}</script>
|
|
33
|
-
<link rel="stylesheet" href="style.css?v=20260829-
|
|
33
|
+
<link rel="stylesheet" href="style.css?v=20260829-terminal-canvas1">
|
|
34
34
|
<style>
|
|
35
35
|
@media(max-width:768px){
|
|
36
36
|
/* User messages: vertical stack, avatar on top, right-aligned */
|
|
@@ -661,6 +661,7 @@
|
|
|
661
661
|
<button class="file-viewer-btn file-viewer-slide-level hidden" id="file-viewer-slide-level" title="Split slides by heading level" aria-haspopup="menu" aria-expanded="false"><span>H1</span><i data-lucide="chevron-down"></i></button>
|
|
662
662
|
<button class="file-viewer-btn hidden" id="file-viewer-history" title="Edit history"><i data-lucide="clock"></i></button>
|
|
663
663
|
<button class="file-viewer-btn" id="file-viewer-refresh" title="Refresh"><i data-lucide="refresh-cw"></i></button>
|
|
664
|
+
<button class="file-viewer-btn" id="file-viewer-download" title="Download file" aria-label="Download file"><i data-lucide="download"></i></button>
|
|
664
665
|
<button class="file-viewer-btn" id="file-viewer-copy" title="Copy contents" aria-label="Copy contents"><i data-lucide="copy"></i></button>
|
|
665
666
|
<button class="file-viewer-btn hidden" id="file-viewer-copy-formatted" title="Copy Markdown formatting" aria-label="Copy Markdown formatting"><i data-lucide="clipboard-copy"></i></button>
|
|
666
667
|
</div>
|
|
@@ -2281,7 +2282,7 @@
|
|
|
2281
2282
|
<script src="https://cdn.jsdelivr.net/npm/@xterm/addon-fit@0/lib/addon-fit.min.js"></script>
|
|
2282
2283
|
<script src="https://cdn.jsdelivr.net/npm/@xterm/addon-web-links@0/lib/addon-web-links.min.js"></script>
|
|
2283
2284
|
<script src="https://cdn.jsdelivr.net/npm/@xterm/addon-webgl@0/lib/addon-webgl.min.js"></script>
|
|
2284
|
-
<script type="module" src="app.js?v=20260829-
|
|
2285
|
+
<script type="module" src="app.js?v=20260829-terminal-dark1"></script>
|
|
2285
2286
|
<div id="pwa-install-modal" class="pwa-modal hidden">
|
|
2286
2287
|
<div class="pwa-modal-backdrop"></div>
|
|
2287
2288
|
<div class="pwa-modal-card">
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
// File-tree context menu and shared file download action.
|
|
2
|
+
|
|
3
|
+
import { iconHtml, refreshIcons } from './icons.js';
|
|
4
|
+
import { insertTextAtCursor } from './input.js';
|
|
5
|
+
import { copyToClipboard, showToast } from './utils.js';
|
|
6
|
+
|
|
7
|
+
var MAX_COPY_CONTENT_BYTES = 1024 * 1024;
|
|
8
|
+
|
|
9
|
+
function closeFileContextMenu() {
|
|
10
|
+
var menu = document.getElementById('file-tree-context-menu');
|
|
11
|
+
if (menu) menu.remove();
|
|
12
|
+
var activeRows = document.querySelectorAll('.file-tree-item.context-open');
|
|
13
|
+
for (var i = 0; i < activeRows.length; i++) activeRows[i].classList.remove('context-open');
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function downloadProjectFile(filePath) {
|
|
17
|
+
if (!filePath) return;
|
|
18
|
+
var link = document.createElement('a');
|
|
19
|
+
link.href = 'api/file/download?path=' + encodeURIComponent(filePath);
|
|
20
|
+
link.download = filePath.split('/').pop() || 'download';
|
|
21
|
+
document.body.appendChild(link);
|
|
22
|
+
link.click();
|
|
23
|
+
link.remove();
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function copyProjectFileContents(filePath) {
|
|
27
|
+
var url = 'api/file/download?path=' + encodeURIComponent(filePath);
|
|
28
|
+
fetch(url, { credentials: 'same-origin', cache: 'no-store' })
|
|
29
|
+
.then(function (response) {
|
|
30
|
+
if (!response.ok) throw new Error('Could not read file');
|
|
31
|
+
var contentLength = parseInt(response.headers.get('content-length') || '0', 10);
|
|
32
|
+
if (contentLength > MAX_COPY_CONTENT_BYTES) {
|
|
33
|
+
if (response.body && response.body.cancel) response.body.cancel();
|
|
34
|
+
throw new Error('File is too large to copy');
|
|
35
|
+
}
|
|
36
|
+
return response.arrayBuffer();
|
|
37
|
+
})
|
|
38
|
+
.then(function (buffer) {
|
|
39
|
+
if (buffer.byteLength > MAX_COPY_CONTENT_BYTES) throw new Error('File is too large to copy');
|
|
40
|
+
var bytes = new Uint8Array(buffer);
|
|
41
|
+
for (var i = 0; i < bytes.length; i++) {
|
|
42
|
+
if (bytes[i] === 0) throw new Error('Binary files cannot be copied as text');
|
|
43
|
+
}
|
|
44
|
+
var decoder = new TextDecoder('utf-8', { fatal: true });
|
|
45
|
+
var text;
|
|
46
|
+
try { text = decoder.decode(buffer); } catch (e) { throw new Error('Binary files cannot be copied as text'); }
|
|
47
|
+
return copyToClipboard(text);
|
|
48
|
+
})
|
|
49
|
+
.catch(function (error) {
|
|
50
|
+
var message = error && error.message ? error.message : 'Could not copy file contents';
|
|
51
|
+
showToast(message, 'error');
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function menuItem(icon, label, handler) {
|
|
56
|
+
var item = document.createElement('button');
|
|
57
|
+
item.type = 'button';
|
|
58
|
+
item.className = 'file-tree-context-item';
|
|
59
|
+
item.setAttribute('role', 'menuitem');
|
|
60
|
+
item.innerHTML = iconHtml(icon) + '<span>' + label + '</span>';
|
|
61
|
+
item.addEventListener('click', handler);
|
|
62
|
+
return item;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function positionMenu(menu, clientX, clientY) {
|
|
66
|
+
var edge = 8;
|
|
67
|
+
var rect = menu.getBoundingClientRect();
|
|
68
|
+
var left = Math.max(edge, Math.min(clientX, window.innerWidth - rect.width - edge));
|
|
69
|
+
var top = Math.max(edge, Math.min(clientY, window.innerHeight - rect.height - edge));
|
|
70
|
+
menu.style.left = left + 'px';
|
|
71
|
+
menu.style.top = top + 'px';
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function showFileContextMenu(event, row) {
|
|
75
|
+
closeFileContextMenu();
|
|
76
|
+
row.classList.add('context-open');
|
|
77
|
+
|
|
78
|
+
var menu = document.createElement('div');
|
|
79
|
+
menu.id = 'file-tree-context-menu';
|
|
80
|
+
menu.className = 'file-tree-context-menu';
|
|
81
|
+
menu.setAttribute('role', 'menu');
|
|
82
|
+
menu.setAttribute('aria-label', 'File actions');
|
|
83
|
+
|
|
84
|
+
var mention = menuItem('at-sign', 'Mention in chat', function (clickEvent) {
|
|
85
|
+
clickEvent.stopPropagation();
|
|
86
|
+
var filePath = row.dataset.path;
|
|
87
|
+
closeFileContextMenu();
|
|
88
|
+
insertTextAtCursor(filePath + ' ');
|
|
89
|
+
});
|
|
90
|
+
menu.appendChild(mention);
|
|
91
|
+
|
|
92
|
+
var copyPath = menuItem('copy', 'Copy path', function (clickEvent) {
|
|
93
|
+
clickEvent.stopPropagation();
|
|
94
|
+
var filePath = row.dataset.path;
|
|
95
|
+
closeFileContextMenu();
|
|
96
|
+
copyToClipboard(filePath).catch(function () { showToast('Could not copy path', 'error'); });
|
|
97
|
+
});
|
|
98
|
+
menu.appendChild(copyPath);
|
|
99
|
+
|
|
100
|
+
var copyContents = menuItem('clipboard-copy', 'Copy contents', function (clickEvent) {
|
|
101
|
+
clickEvent.stopPropagation();
|
|
102
|
+
var filePath = row.dataset.path;
|
|
103
|
+
closeFileContextMenu();
|
|
104
|
+
copyProjectFileContents(filePath);
|
|
105
|
+
});
|
|
106
|
+
menu.appendChild(copyContents);
|
|
107
|
+
|
|
108
|
+
var separator = document.createElement('div');
|
|
109
|
+
separator.className = 'file-tree-context-separator';
|
|
110
|
+
separator.setAttribute('role', 'separator');
|
|
111
|
+
menu.appendChild(separator);
|
|
112
|
+
|
|
113
|
+
var download = menuItem('download', 'Download', function (clickEvent) {
|
|
114
|
+
clickEvent.stopPropagation();
|
|
115
|
+
var filePath = row.dataset.path;
|
|
116
|
+
closeFileContextMenu();
|
|
117
|
+
downloadProjectFile(filePath);
|
|
118
|
+
});
|
|
119
|
+
menu.appendChild(download);
|
|
120
|
+
document.body.appendChild(menu);
|
|
121
|
+
refreshIcons(menu);
|
|
122
|
+
positionMenu(menu, event.clientX, event.clientY);
|
|
123
|
+
try { mention.focus({ preventScroll: true }); } catch (e) { mention.focus(); }
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
export function initFileBrowserContextMenu(treeEl) {
|
|
127
|
+
if (!treeEl || treeEl.dataset.contextMenuReady === 'true') return;
|
|
128
|
+
treeEl.dataset.contextMenuReady = 'true';
|
|
129
|
+
|
|
130
|
+
treeEl.addEventListener('contextmenu', function (event) {
|
|
131
|
+
var row = event.target && event.target.closest ? event.target.closest('.file-tree-item') : null;
|
|
132
|
+
if (!row || !treeEl.contains(row) || row.dataset.entryType !== 'file' || !row.dataset.path) return;
|
|
133
|
+
event.preventDefault();
|
|
134
|
+
event.stopPropagation();
|
|
135
|
+
showFileContextMenu(event, row);
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
document.addEventListener('pointerdown', function (event) {
|
|
139
|
+
var menu = document.getElementById('file-tree-context-menu');
|
|
140
|
+
if (menu && !menu.contains(event.target)) closeFileContextMenu();
|
|
141
|
+
});
|
|
142
|
+
document.addEventListener('keydown', function (event) {
|
|
143
|
+
if (event.key === 'Escape') closeFileContextMenu();
|
|
144
|
+
});
|
|
145
|
+
window.addEventListener('resize', closeFileContextMenu);
|
|
146
|
+
window.addEventListener('blur', closeFileContextMenu);
|
|
147
|
+
treeEl.addEventListener('scroll', closeFileContextMenu, { passive: true });
|
|
148
|
+
}
|
|
@@ -10,6 +10,7 @@ import { animateMarkdownChange, beginMarkdownPresentation, cancelMarkdownFollow,
|
|
|
10
10
|
import { store } from './store.js';
|
|
11
11
|
import { enterMarkdownSlides, exitMarkdownSlides, handleMarkdownSlideKey, syncMarkdownSlidesButton, toggleMarkdownSlideLevelMenu } from './markdown-slides.js';
|
|
12
12
|
import { initFileViewerTabs, openFileViewerTab, previewFileViewerTab, updateFileViewerTab, closeFileViewerTab, focusedFileViewerTab, clearFileViewerTabs } from './filebrowser-tabs.js';
|
|
13
|
+
import { initFileBrowserContextMenu, downloadProjectFile } from './filebrowser-context-menu.js';
|
|
13
14
|
|
|
14
15
|
var ctx;
|
|
15
16
|
var showDropHint = function () {};
|
|
@@ -29,6 +30,7 @@ var gitDiffCache = {}; // hash -> diff text
|
|
|
29
30
|
var pendingGitDiff = null; // callback for pending git diff
|
|
30
31
|
var fileAtCache = {}; // hash -> file content
|
|
31
32
|
var pendingFileAt = null; // callback for pending file-at
|
|
33
|
+
var FILE_RICH_PREVIEW_MAX_BYTES = 1024 * 1024;
|
|
32
34
|
|
|
33
35
|
export function initFileBrowser(_ctx) {
|
|
34
36
|
ctx = _ctx;
|
|
@@ -55,6 +57,7 @@ export function initFileBrowser(_ctx) {
|
|
|
55
57
|
// collapses/expands folders and ascends/descends into them. The tree
|
|
56
58
|
// gets a tabindex so it can receive focus and keydown events.
|
|
57
59
|
if (ctx.fileTreeEl) {
|
|
60
|
+
initFileBrowserContextMenu(ctx.fileTreeEl);
|
|
58
61
|
ctx.fileTreeEl.setAttribute('tabindex', '0');
|
|
59
62
|
ctx.fileTreeEl.addEventListener('keydown', handleTreeKeyDown);
|
|
60
63
|
// When the user clicks any tree row, promote it to keyboard focus
|
|
@@ -180,6 +183,11 @@ export function initFileBrowser(_ctx) {
|
|
|
180
183
|
});
|
|
181
184
|
});
|
|
182
185
|
|
|
186
|
+
document.getElementById("file-viewer-download").addEventListener("click", function () {
|
|
187
|
+
if (!currentFilePath) return;
|
|
188
|
+
downloadProjectFile(currentFilePath);
|
|
189
|
+
});
|
|
190
|
+
|
|
183
191
|
// Markdown render toggle
|
|
184
192
|
document.getElementById("file-viewer-render").addEventListener("click", function () {
|
|
185
193
|
if (!currentContent || (!currentIsMarkdown && !currentIsSvg)) return;
|
|
@@ -754,6 +762,7 @@ function renderFilteredTree(container, tree, depth, query) {
|
|
|
754
762
|
|
|
755
763
|
var row = document.createElement("div");
|
|
756
764
|
row.className = "file-tree-item" + (isDir ? " expanded" : "");
|
|
765
|
+
row.dataset.entryType = isDir ? "dir" : "file";
|
|
757
766
|
row.style.paddingLeft = (8 + depth * 16) + "px";
|
|
758
767
|
if (entry) {
|
|
759
768
|
row.draggable = true;
|
|
@@ -972,6 +981,7 @@ function renderEntries(container, entries, depth) {
|
|
|
972
981
|
var entry = sorted[i];
|
|
973
982
|
var row = document.createElement("div");
|
|
974
983
|
row.className = "file-tree-item";
|
|
984
|
+
row.dataset.entryType = entry.type;
|
|
975
985
|
row.style.paddingLeft = (8 + depth * 16) + "px";
|
|
976
986
|
|
|
977
987
|
row.draggable = true;
|
|
@@ -1079,6 +1089,7 @@ function showFileContent(msg) {
|
|
|
1079
1089
|
var previousContent = currentContent;
|
|
1080
1090
|
var previousPath = currentFilePath;
|
|
1081
1091
|
var previousWasMarkdown = currentIsMarkdown;
|
|
1092
|
+
var lightweightPreview = false;
|
|
1082
1093
|
var refreshSlideIndex = pendingRefresh && store.get('markdownSlidesActive')
|
|
1083
1094
|
? store.get('markdownSlideIndex') || 0
|
|
1084
1095
|
: null;
|
|
@@ -1116,8 +1127,10 @@ function showFileContent(msg) {
|
|
|
1116
1127
|
} else {
|
|
1117
1128
|
currentContent = msg.content;
|
|
1118
1129
|
var ext = requestedExt;
|
|
1119
|
-
|
|
1120
|
-
|
|
1130
|
+
lightweightPreview = (msg.size || 0) > FILE_RICH_PREVIEW_MAX_BYTES;
|
|
1131
|
+
currentIsMarkdown = !lightweightPreview && (ext === "md" || ext === "mdx");
|
|
1132
|
+
currentIsSvg = !lightweightPreview && ext === "svg";
|
|
1133
|
+
if (lightweightPreview) isRendered = false;
|
|
1121
1134
|
if (pendingRenderedOpen && currentIsMarkdown) isRendered = true;
|
|
1122
1135
|
|
|
1123
1136
|
if (currentIsMarkdown || currentIsSvg) {
|
|
@@ -1129,7 +1142,9 @@ function showFileContent(msg) {
|
|
|
1129
1142
|
}
|
|
1130
1143
|
|
|
1131
1144
|
// Markdown starts as source; SVG starts as a safe image preview.
|
|
1132
|
-
if (
|
|
1145
|
+
if (lightweightPreview) {
|
|
1146
|
+
renderLargeTextFile(bodyEl, msg.content, msg.size);
|
|
1147
|
+
} else if (currentIsMarkdown) {
|
|
1133
1148
|
var transitionFrom = keepRenderState && prevRendered && previousWasMarkdown &&
|
|
1134
1149
|
isFollowingMarkdown(msg.path) && pathsReferToSameFile(previousPath, msg.path)
|
|
1135
1150
|
? (previousContent == null ? "" : previousContent)
|
|
@@ -1149,7 +1164,9 @@ function showFileContent(msg) {
|
|
|
1149
1164
|
refreshIcons();
|
|
1150
1165
|
|
|
1151
1166
|
// If opened with a diff request, show full-file split diff in wide mode
|
|
1152
|
-
if (pendingOpenMode &&
|
|
1167
|
+
if (pendingOpenMode && lightweightPreview) {
|
|
1168
|
+
pendingOpenMode = null;
|
|
1169
|
+
} else if (pendingOpenMode && pendingOpenMode.type === "diff" && currentContent != null) {
|
|
1153
1170
|
var diffOpts = pendingOpenMode;
|
|
1154
1171
|
pendingOpenMode = null;
|
|
1155
1172
|
historyVisible = false;
|
|
@@ -1444,6 +1461,26 @@ function renderCodeWithLineNumbers(bodyEl, content, ext) {
|
|
|
1444
1461
|
}
|
|
1445
1462
|
}
|
|
1446
1463
|
|
|
1464
|
+
function renderLargeTextFile(bodyEl, content, size) {
|
|
1465
|
+
var viewer = document.createElement("div");
|
|
1466
|
+
viewer.className = "file-viewer-large-text";
|
|
1467
|
+
|
|
1468
|
+
var notice = document.createElement("div");
|
|
1469
|
+
notice.className = "file-viewer-large-notice";
|
|
1470
|
+
notice.innerHTML = iconHtml("file-text") +
|
|
1471
|
+
"<span>Large file (" + formatSize(size || 0) + ") — showing plain text for performance</span>";
|
|
1472
|
+
|
|
1473
|
+
var codeWrap = document.createElement("pre");
|
|
1474
|
+
var codeEl = document.createElement("code");
|
|
1475
|
+
codeEl.textContent = content;
|
|
1476
|
+
codeWrap.appendChild(codeEl);
|
|
1477
|
+
|
|
1478
|
+
viewer.appendChild(notice);
|
|
1479
|
+
viewer.appendChild(codeWrap);
|
|
1480
|
+
bodyEl.innerHTML = "";
|
|
1481
|
+
bodyEl.appendChild(viewer);
|
|
1482
|
+
}
|
|
1483
|
+
|
|
1447
1484
|
function formatSize(bytes) {
|
|
1448
1485
|
if (bytes < 1024) return bytes + " B";
|
|
1449
1486
|
if (bytes < 1048576) return (bytes / 1024).toFixed(1) + " KB";
|
|
@@ -428,7 +428,7 @@ function extractFilePaths(cd) {
|
|
|
428
428
|
}
|
|
429
429
|
|
|
430
430
|
// --- Insert text at cursor in textarea ---
|
|
431
|
-
function insertTextAtCursor(text) {
|
|
431
|
+
export function insertTextAtCursor(text) {
|
|
432
432
|
var el = ctx.inputEl;
|
|
433
433
|
el.focus();
|
|
434
434
|
var start = el.selectionStart;
|
|
@@ -23,9 +23,8 @@ import { attachTuiGrab, detachTuiGrab } from './tui-grab.js';
|
|
|
23
23
|
import { createKeyToolbar, TERMINAL_TOOLBAR_HTML } from './terminal-toolbar.js';
|
|
24
24
|
import { refreshIcons, iconHtml } from './icons.js';
|
|
25
25
|
|
|
26
|
-
// Claude TUI
|
|
27
|
-
//
|
|
28
|
-
// setTuiSessionTheme() below, which theme.js calls from applyTheme.
|
|
26
|
+
// Claude TUI sessions always use Clay Studio Dark via getTerminalTheme().
|
|
27
|
+
// Theme switches reapply that fixed palette through setTuiSessionTheme().
|
|
29
28
|
|
|
30
29
|
var hostEl = null; // container div mounted over #messages
|
|
31
30
|
var xtermContainerEl = null;
|
|
@@ -262,7 +262,7 @@ export function getComputedVar(varName) {
|
|
|
262
262
|
}
|
|
263
263
|
|
|
264
264
|
export function getTerminalTheme() {
|
|
265
|
-
return computeTerminalTheme(
|
|
265
|
+
return computeTerminalTheme(getTheme(DEFAULT_DARK_THEME_ID) || defaultDarkFallback);
|
|
266
266
|
}
|
|
267
267
|
|
|
268
268
|
export function getMermaidThemeVars() {
|
|
@@ -305,7 +305,9 @@ export function applyTheme(themeId, fromPicker) {
|
|
|
305
305
|
|
|
306
306
|
try { updateMascotSvgs(vars, isLight); } catch (e) {}
|
|
307
307
|
|
|
308
|
-
|
|
308
|
+
// Terminals deliberately keep Clay Studio Dark for predictable ANSI
|
|
309
|
+
// contrast, even while the surrounding workspace uses a light theme.
|
|
310
|
+
var termTheme = getTerminalTheme();
|
|
309
311
|
try { setTerminalTheme(termTheme); } catch (e) {}
|
|
310
312
|
try { setTuiSessionTheme(termTheme); } catch (e) {}
|
|
311
313
|
try { setTuiAttentionTheme(termTheme); } catch (e) {}
|
|
@@ -16,9 +16,9 @@
|
|
|
16
16
|
import { getTerminalTheme } from './theme.js';
|
|
17
17
|
import { getTerminalFontFamily, getTerminalFontSize, onTerminalFontChange } from './terminal-prefs.js';
|
|
18
18
|
|
|
19
|
-
// TUI attention modal
|
|
20
|
-
// getTerminalTheme()
|
|
21
|
-
// which theme.js calls from applyTheme.
|
|
19
|
+
// The TUI attention modal always uses Clay Studio Dark via
|
|
20
|
+
// getTerminalTheme(). Theme switches reapply that fixed palette through
|
|
21
|
+
// setTuiAttentionTheme(), which theme.js calls from applyTheme().
|
|
22
22
|
|
|
23
23
|
var modalEl = null;
|
|
24
24
|
var modalXterm = null;
|
|
@@ -266,9 +266,8 @@ export function tuiModalHandleTermResized() { return false; }
|
|
|
266
266
|
export function tuiModalHandleTermExited() { return false; }
|
|
267
267
|
export function tuiModalHandleTermClosed() { return false; }
|
|
268
268
|
|
|
269
|
-
//
|
|
270
|
-
//
|
|
271
|
-
// so the frame doesn't stay black when a light theme is active.
|
|
269
|
+
// Theme update hook for the attention modal. The supplied palette remains
|
|
270
|
+
// Clay Studio Dark even when the surrounding workspace switches themes.
|
|
272
271
|
export function setTuiAttentionTheme(xtermTheme) {
|
|
273
272
|
if (modalXterm) {
|
|
274
273
|
try { modalXterm.options.theme = xtermTheme; } catch (e) {}
|
|
@@ -276,8 +275,6 @@ export function setTuiAttentionTheme(xtermTheme) {
|
|
|
276
275
|
if (modalEl && xtermTheme && xtermTheme.background) {
|
|
277
276
|
var bodyEl = modalEl.querySelector(".tui-modal-body");
|
|
278
277
|
if (bodyEl) bodyEl.style.background = xtermTheme.background;
|
|
279
|
-
var frameEl = modalEl.querySelector(".tui-modal");
|
|
280
|
-
if (frameEl) frameEl.style.background = xtermTheme.background;
|
|
281
278
|
}
|
|
282
279
|
}
|
|
283
280
|
|
package/lib/public/style.css
CHANGED
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
@import url("css/generated-images.css?v=20260829c");
|
|
12
12
|
@import url("css/rewind.css");
|
|
13
13
|
@import url("css/input.css?v=20260828-composer-depth1");
|
|
14
|
-
@import url("css/filebrowser.css?v=
|
|
14
|
+
@import url("css/filebrowser.css?v=20260829-terminal-canvas1");
|
|
15
15
|
@import url("css/git-panel.css");
|
|
16
16
|
@import url("css/diff.css");
|
|
17
17
|
@import url("css/highlight.css");
|
|
@@ -36,5 +36,5 @@
|
|
|
36
36
|
@import url("css/mention.css");
|
|
37
37
|
@import url("css/debate.css");
|
|
38
38
|
@import url("css/notifications-center.css");
|
|
39
|
-
@import url("css/tui-attention.css");
|
|
39
|
+
@import url("css/tui-attention.css?v=20260829-terminal-canvas1");
|
|
40
40
|
@import url("css/pwa-mobile.css?v=20260824f");
|
package/package.json
CHANGED