clay-server 3.6.0 → 3.7.0-beta.2

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.
@@ -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("?");
@@ -34,7 +34,7 @@ function attachImage(ctx) {
34
34
  return hydrated;
35
35
  }
36
36
  if (!entry.imageRefs) return entry;
37
- if (entry.type !== "user_message" && entry.type !== "mention_user" && entry.type !== "user_mention") return entry;
37
+ if (entry.type !== "user_message" && entry.type !== "mention_user" && entry.type !== "user_mention" && entry.type !== "generated_image") return entry;
38
38
  var images = [];
39
39
  for (var ri = 0; ri < entry.imageRefs.length; ri++) {
40
40
  var ref = entry.imageRefs[ri];
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;
@@ -882,6 +883,7 @@ function createProjectContext(opts) {
882
883
  scheduleMessage: function (session, text, resetsAt) {
883
884
  scheduleMessage(session, text, resetsAt);
884
885
  },
886
+ saveImageFile: saveImageFile,
885
887
  getAutoContinueSetting: function (session) {
886
888
  // Per-user setting in multi-user mode
887
889
  if (usersModule.isMultiUser() && session && session.ownerId) {
@@ -1437,7 +1439,7 @@ function createProjectContext(opts) {
1437
1439
  IGNORED_DIRS: IGNORED_DIRS,
1438
1440
  BINARY_EXTS: BINARY_EXTS,
1439
1441
  IMAGE_EXTS: IMAGE_EXTS,
1440
- FS_MAX_SIZE: FS_MAX_SIZE,
1442
+ FS_MAX_SIZE: FS_VIEWER_MAX_SIZE,
1441
1443
  });
1442
1444
 
1443
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: #252525;
1971
- border-bottom: 1px solid #333;
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 #444;
1988
- background: #333;
1989
- color: #d4d4d4;
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: #555;
2002
- border-color: #666;
2111
+ background: var(--sidebar-hover);
2112
+ border-color: var(--text-dimmer);
2003
2113
  }
2004
2114
 
2005
2115
  .term-key-toggle.active {
@@ -0,0 +1,225 @@
1
+ .generated-image-row {
2
+ width: 100%;
3
+ max-width: var(--content-width);
4
+ margin: 4px auto 16px;
5
+ padding: 0 20px;
6
+ }
7
+
8
+ .generated-image-card {
9
+ width: min(560px, 100%);
10
+ overflow: hidden;
11
+ border: 1px solid var(--border);
12
+ border-radius: 14px;
13
+ background: var(--bg-alt);
14
+ box-shadow: 0 8px 28px rgba(var(--shadow-rgb), 0.16);
15
+ }
16
+
17
+ .generated-image-status {
18
+ display: flex;
19
+ align-items: center;
20
+ gap: 7px;
21
+ width: min(560px, 100%);
22
+ min-height: 30px;
23
+ padding: 0 2px 7px;
24
+ color: var(--text-muted);
25
+ font-size: 12px;
26
+ font-weight: 500;
27
+ }
28
+
29
+ .generated-image-status .lucide {
30
+ width: 14px;
31
+ height: 14px;
32
+ color: var(--accent);
33
+ animation: generated-image-sparkle 2.4s ease-in-out infinite;
34
+ }
35
+
36
+ .generated-image-status-dots::after {
37
+ content: "";
38
+ animation: generated-image-ellipsis 1.4s steps(4, end) infinite;
39
+ }
40
+
41
+ .generated-image-card--pending {
42
+ position: relative;
43
+ width: min(560px, 100%);
44
+ aspect-ratio: 1 / 1;
45
+ border-color: var(--border-subtle);
46
+ background: var(--bg-alt);
47
+ box-shadow: none;
48
+ }
49
+
50
+ .generated-image-particle-field {
51
+ position: absolute;
52
+ inset: 0;
53
+ overflow: hidden;
54
+ }
55
+
56
+ .generated-image-particle-field::before {
57
+ position: absolute;
58
+ inset: 11%;
59
+ content: "";
60
+ background-image: radial-gradient(circle, var(--text-dimmer) 1.4px, transparent 1.7px);
61
+ background-position: center;
62
+ background-size: 25px 25px;
63
+ opacity: 0.52;
64
+ -webkit-mask-image: radial-gradient(circle, #000 0%, rgba(0, 0, 0, 0.88) 38%, transparent 72%);
65
+ mask-image: radial-gradient(circle, #000 0%, rgba(0, 0, 0, 0.88) 38%, transparent 72%);
66
+ animation: generated-image-field 3.2s ease-in-out infinite;
67
+ }
68
+
69
+ .generated-image-particle-field::after {
70
+ position: absolute;
71
+ top: 50%;
72
+ left: 50%;
73
+ width: 36%;
74
+ aspect-ratio: 1 / 1;
75
+ border: 1px solid var(--accent-15);
76
+ border-radius: 50%;
77
+ content: "";
78
+ background: radial-gradient(circle, var(--accent-12), transparent 68%);
79
+ transform: translate(-50%, -50%);
80
+ animation: generated-image-pulse 3.2s ease-in-out infinite;
81
+ }
82
+
83
+ @keyframes generated-image-field {
84
+ 0%, 100% { opacity: 0.28; transform: scale(0.92) rotate(-2deg); }
85
+ 50% { opacity: 0.68; transform: scale(1.04) rotate(2deg); }
86
+ }
87
+
88
+ @keyframes generated-image-pulse {
89
+ 0%, 100% { opacity: 0.2; transform: translate(-50%, -50%) scale(0.72); }
90
+ 50% { opacity: 0.72; transform: translate(-50%, -50%) scale(1.08); }
91
+ }
92
+
93
+ @keyframes generated-image-sparkle {
94
+ 0%, 100% { opacity: 0.5; transform: scale(0.86) rotate(-8deg); }
95
+ 50% { opacity: 1; transform: scale(1.08) rotate(8deg); }
96
+ }
97
+
98
+ @keyframes generated-image-ellipsis {
99
+ 0% { content: ""; }
100
+ 25% { content: "."; }
101
+ 50% { content: ".."; }
102
+ 75%, 100% { content: "..."; }
103
+ }
104
+
105
+ @media (prefers-reduced-motion: reduce) {
106
+ .generated-image-status .lucide,
107
+ .generated-image-status-dots::after,
108
+ .generated-image-particle-field::before,
109
+ .generated-image-particle-field::after {
110
+ animation: none;
111
+ }
112
+ }
113
+
114
+ .generated-image-preview {
115
+ display: grid;
116
+ place-items: center;
117
+ min-height: 180px;
118
+ max-height: 680px;
119
+ overflow: hidden;
120
+ background:
121
+ linear-gradient(45deg, rgba(var(--overlay-rgb), 0.025) 25%, transparent 25%),
122
+ linear-gradient(-45deg, rgba(var(--overlay-rgb), 0.025) 25%, transparent 25%),
123
+ linear-gradient(45deg, transparent 75%, rgba(var(--overlay-rgb), 0.025) 75%),
124
+ linear-gradient(-45deg, transparent 75%, rgba(var(--overlay-rgb), 0.025) 75%);
125
+ background-position: 0 0, 0 8px, 8px -8px, -8px 0;
126
+ background-size: 16px 16px;
127
+ }
128
+
129
+ .generated-image-preview img {
130
+ display: block;
131
+ width: 100%;
132
+ max-height: 680px;
133
+ object-fit: contain;
134
+ cursor: zoom-in;
135
+ }
136
+
137
+ .generated-image-footer {
138
+ display: flex;
139
+ align-items: center;
140
+ justify-content: space-between;
141
+ gap: 14px;
142
+ min-height: 52px;
143
+ padding: 9px 10px 9px 14px;
144
+ border-top: 1px solid var(--border-subtle);
145
+ }
146
+
147
+ .generated-image-meta {
148
+ display: flex;
149
+ flex: 1;
150
+ min-width: 0;
151
+ flex-direction: column;
152
+ gap: 2px;
153
+ }
154
+
155
+ .generated-image-label {
156
+ display: flex;
157
+ align-items: center;
158
+ gap: 6px;
159
+ color: var(--text-secondary);
160
+ font-size: 12px;
161
+ font-weight: 600;
162
+ }
163
+
164
+ .generated-image-label .lucide {
165
+ width: 13px;
166
+ height: 13px;
167
+ color: var(--accent);
168
+ }
169
+
170
+ .generated-image-prompt {
171
+ overflow: hidden;
172
+ color: var(--text-muted);
173
+ font-size: 11px;
174
+ line-height: 1.35;
175
+ text-overflow: ellipsis;
176
+ white-space: nowrap;
177
+ }
178
+
179
+ .generated-image-actions {
180
+ display: flex;
181
+ flex-shrink: 0;
182
+ gap: 4px;
183
+ }
184
+
185
+ .generated-image-action {
186
+ display: inline-flex;
187
+ align-items: center;
188
+ gap: 5px;
189
+ height: 32px;
190
+ padding: 0 9px;
191
+ border: 1px solid transparent;
192
+ border-radius: 8px;
193
+ background: transparent;
194
+ color: var(--text-muted);
195
+ font: inherit;
196
+ font-size: 11px;
197
+ text-decoration: none;
198
+ cursor: pointer;
199
+ }
200
+
201
+ .generated-image-action:hover {
202
+ border-color: var(--border);
203
+ background: rgba(var(--overlay-rgb), 0.04);
204
+ color: var(--text);
205
+ }
206
+
207
+ .generated-image-action .lucide {
208
+ width: 14px;
209
+ height: 14px;
210
+ }
211
+
212
+ @media (max-width: 560px) {
213
+ .generated-image-row { padding: 0 12px; }
214
+ .generated-image-card { border-radius: 12px; }
215
+
216
+ .generated-image-action span {
217
+ display: none;
218
+ }
219
+
220
+ .generated-image-action {
221
+ width: 32px;
222
+ justify-content: center;
223
+ padding: 0;
224
+ }
225
+ }
@@ -762,7 +762,7 @@
762
762
  color: var(--text-muted);
763
763
  }
764
764
 
765
- /* --- Bottom toolbar (attach left, model+send right) --- */
765
+ /* --- Bottom toolbar (attach left, session config + send right) --- */
766
766
  #input-bottom {
767
767
  display: flex;
768
768
  align-items: center;
@@ -777,21 +777,7 @@
777
777
  flex-shrink: 0;
778
778
  }
779
779
 
780
- /* --- Vendor toggle (split toggle) --- */
781
- #vendor-toggle-wrap {
782
- display: flex;
783
- align-items: center;
784
- border: 1px solid var(--border);
785
- border-radius: 8px;
786
- overflow: hidden;
787
- height: 30px;
788
- flex-shrink: 0;
789
- }
790
- #vendor-toggle-wrap.hidden { display: none; }
791
-
792
- /* Compact session-vendor icon shown next to the config-chip when the
793
- full vendor-toggle is hidden (because the session's vendor is already
794
- committed). Visual only - not clickable. */
780
+ /* Compact committed-session vendor icon next to the config chip. */
795
781
  #active-vendor-indicator {
796
782
  display: inline-flex;
797
783
  align-items: center;
@@ -806,73 +792,13 @@
806
792
  border-radius: 4px;
807
793
  object-fit: cover;
808
794
  }
809
- #vendor-toggle-wrap.locked {
810
- border: none;
811
- height: auto;
812
- pointer-events: none;
813
- }
814
- #vendor-toggle-wrap.locked .vendor-toggle-btn:not(.active) { display: none; }
815
- #vendor-toggle-wrap.locked .vendor-toggle-btn.active {
816
- background: transparent;
817
- padding: 0;
818
- gap: 0;
819
- opacity: 0.7;
820
- }
821
- #vendor-toggle-wrap.locked .vendor-toggle-btn.active .vendor-toggle-label { display: none; }
822
- #vendor-toggle-wrap.locked .vendor-toggle-btn:first-child { border-right: none; }
823
- .vendor-toggle-btn {
824
- display: flex;
825
- align-items: center;
826
- gap: 4px;
827
- padding: 0 10px;
828
- height: 100%;
829
- border: none;
830
- background: transparent;
831
- cursor: pointer;
832
- font-size: 12px;
833
- color: var(--text-secondary);
834
- transition: background 0.15s, color 0.15s;
835
- white-space: nowrap;
836
- }
837
- .vendor-toggle-btn:first-child { border-right: 1px solid var(--border); }
838
- .vendor-toggle-btn.active {
839
- background: var(--accent-12);
840
- color: var(--accent);
841
- font-weight: 600;
842
- }
843
- .vendor-toggle-btn.disabled {
844
- opacity: 0.35;
845
- cursor: default;
846
- }
847
- .vendor-toggle-btn.disabled:not(.active) { display: none; }
848
- .vendor-toggle-icon {
849
- width: 16px;
850
- height: 16px;
851
- border-radius: 4px;
852
- object-fit: cover;
853
- }
854
- .vendor-toggle-label {
855
- pointer-events: none;
856
- }
857
- .vendor-toggle-btn.experimental::after,
858
795
  .vendor-experimental-badge {
859
- content: "🧪";
860
796
  font-size: 10px;
861
797
  line-height: 1;
862
798
  font-weight: 400;
863
799
  filter: saturate(0.85);
864
- }
865
- .vendor-experimental-badge {
866
800
  margin-left: auto;
867
801
  }
868
- @media (min-width: 901px) {
869
- .vendor-toggle-btn:not(.active) .vendor-toggle-label { display: none; }
870
- .vendor-toggle-btn:not(.active) { padding: 0 8px; }
871
- }
872
- @media (max-width: 900px) {
873
- .vendor-toggle-label { display: none; }
874
- .vendor-toggle-btn { padding: 0 8px; }
875
- }
876
802
 
877
803
  /* --- Attach buttons & menu --- */
878
804
  #attach-wrap {
@@ -1290,7 +1216,6 @@
1290
1216
  Attach (files/images -> path injection) and voice input stay available. */
1291
1217
  body.tui-composer-active #schedule-btn,
1292
1218
  body.tui-composer-active #ask-mate-btn,
1293
- body.tui-composer-active #vendor-toggle-wrap,
1294
1219
  body.tui-composer-active #active-vendor-indicator,
1295
1220
  body.tui-composer-active #config-chip-wrap {
1296
1221
  display: none !important;
@@ -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: #000;
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
-
@@ -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=20260828-clay-theme1">
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 */
@@ -524,48 +524,6 @@
524
524
  </div>
525
525
  </div>
526
526
  <div id="input-bottom-right">
527
- <div id="vendor-toggle-wrap">
528
- <button id="vendor-btn-claude" class="vendor-toggle-btn active" data-vendor="claude">
529
- <img src="/claude-code-avatar.png" class="vendor-toggle-icon" alt="Claude">
530
- <span class="vendor-toggle-label">Claude Code</span>
531
- </button>
532
- <button id="vendor-btn-codex" class="vendor-toggle-btn" data-vendor="codex">
533
- <img src="/codex-avatar.png" class="vendor-toggle-icon" alt="Codex">
534
- <span class="vendor-toggle-label">Codex</span>
535
- </button>
536
- <button id="vendor-btn-antigravity" class="vendor-toggle-btn" data-vendor="antigravity">
537
- <img src="/antigravity-avatar.png" class="vendor-toggle-icon" alt="Antigravity">
538
- <span class="vendor-toggle-label">Antigravity CLI</span>
539
- </button>
540
- <button id="vendor-btn-opencode" class="vendor-toggle-btn" data-vendor="opencode">
541
- <img src="/opencode-avatar.svg" class="vendor-toggle-icon" alt="OpenCode">
542
- <span class="vendor-toggle-label">OpenCode</span>
543
- </button>
544
- <button id="vendor-btn-kimi" class="vendor-toggle-btn" data-vendor="kimi">
545
- <img src="/kimi-avatar.svg" class="vendor-toggle-icon" alt="Kimi">
546
- <span class="vendor-toggle-label">Kimi Code</span>
547
- </button>
548
- <button id="vendor-btn-grok" class="vendor-toggle-btn" data-vendor="grok">
549
- <img src="/grok-avatar.svg" class="vendor-toggle-icon" alt="Grok">
550
- <span class="vendor-toggle-label">Grok Build</span>
551
- </button>
552
- <button id="vendor-btn-copilot" class="vendor-toggle-btn" data-vendor="copilot">
553
- <img src="/copilot-avatar.svg" class="vendor-toggle-icon" alt="GitHub Copilot">
554
- <span class="vendor-toggle-label">GitHub Copilot CLI</span>
555
- </button>
556
- <button id="vendor-btn-qwen" class="vendor-toggle-btn" data-vendor="qwen">
557
- <img src="/qwen-avatar.svg" class="vendor-toggle-icon" alt="Qwen">
558
- <span class="vendor-toggle-label">Qwen Code</span>
559
- </button>
560
- <button id="vendor-btn-junie" class="vendor-toggle-btn" data-vendor="junie">
561
- <img src="/junie-avatar.svg" class="vendor-toggle-icon" alt="Junie">
562
- <span class="vendor-toggle-label">Junie CLI</span>
563
- </button>
564
- <button id="vendor-btn-kiro" class="vendor-toggle-btn" data-vendor="kiro">
565
- <img src="/kiro-avatar.svg" class="vendor-toggle-icon" alt="Kiro">
566
- <span class="vendor-toggle-label">Kiro CLI</span>
567
- </button>
568
- </div>
569
527
  <div id="active-vendor-indicator" class="hidden" title="Session vendor">
570
528
  <img id="active-vendor-icon" alt="">
571
529
  </div>
@@ -703,6 +661,7 @@
703
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>
704
662
  <button class="file-viewer-btn hidden" id="file-viewer-history" title="Edit history"><i data-lucide="clock"></i></button>
705
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>
706
665
  <button class="file-viewer-btn" id="file-viewer-copy" title="Copy contents" aria-label="Copy contents"><i data-lucide="copy"></i></button>
707
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>
708
667
  </div>
@@ -2323,7 +2282,7 @@
2323
2282
  <script src="https://cdn.jsdelivr.net/npm/@xterm/addon-fit@0/lib/addon-fit.min.js"></script>
2324
2283
  <script src="https://cdn.jsdelivr.net/npm/@xterm/addon-web-links@0/lib/addon-web-links.min.js"></script>
2325
2284
  <script src="https://cdn.jsdelivr.net/npm/@xterm/addon-webgl@0/lib/addon-webgl.min.js"></script>
2326
- <script type="module" src="app.js?v=20260826a"></script>
2285
+ <script type="module" src="app.js?v=20260829-terminal-dark1"></script>
2327
2286
  <div id="pwa-install-modal" class="pwa-modal hidden">
2328
2287
  <div class="pwa-modal-backdrop"></div>
2329
2288
  <div class="pwa-modal-card">