claude-relay 2.1.2 → 2.2.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.
@@ -9,6 +9,93 @@
9
9
  padding: 20px 0 12px;
10
10
  }
11
11
 
12
+ /* --- Sticky todo overlay --- */
13
+ #todo-sticky {
14
+ position: absolute;
15
+ top: 0;
16
+ left: 50%;
17
+ transform: translateX(-50%);
18
+ width: 100%;
19
+ max-width: var(--content-width);
20
+ padding: 8px 20px;
21
+ z-index: 10;
22
+ }
23
+ #todo-sticky.hidden { display: none; }
24
+
25
+ #todo-sticky .todo-sticky-inner {
26
+ background: var(--bg-alt);
27
+ border: 1px solid var(--border);
28
+ border-radius: 12px;
29
+ box-shadow: 0 4px 16px rgba(0, 0, 0, 0.3);
30
+ overflow: hidden;
31
+ }
32
+
33
+ #todo-sticky .todo-sticky-header {
34
+ display: flex;
35
+ align-items: center;
36
+ gap: 8px;
37
+ padding: 8px 14px;
38
+ cursor: pointer;
39
+ user-select: none;
40
+ }
41
+ #todo-sticky .todo-sticky-header:hover { background: rgba(255, 255, 255, 0.03); }
42
+ #todo-sticky .todo-sticky-icon { display: inline-flex; color: var(--accent); }
43
+ #todo-sticky .todo-sticky-icon .lucide { width: 14px; height: 14px; }
44
+ #todo-sticky .todo-sticky-title { font-size: 12px; font-weight: 600; color: var(--text-secondary); flex: 1; }
45
+ #todo-sticky .todo-sticky-count { font-size: 11px; color: var(--text-muted); font-family: "SF Mono", Menlo, Monaco, monospace; }
46
+ #todo-sticky .todo-sticky-chevron { display: inline-flex; color: var(--text-muted); transition: transform 0.2s; }
47
+ #todo-sticky .todo-sticky-chevron .lucide { width: 12px; height: 12px; }
48
+ #todo-sticky.collapsed .todo-sticky-chevron { transform: rotate(-90deg); }
49
+
50
+ #todo-sticky .todo-sticky-progress { height: 2px; background: var(--border); }
51
+ #todo-sticky .todo-sticky-progress-bar { height: 100%; background: var(--success); transition: width 0.3s ease; }
52
+
53
+ #todo-sticky .todo-sticky-items { padding: 0 4px 6px; }
54
+ #todo-sticky.collapsed .todo-sticky-items { display: none; }
55
+ #todo-sticky.collapsed .todo-sticky-progress { height: 3px; border-radius: 0 0 12px 12px; overflow: hidden; }
56
+
57
+ #todo-sticky .todo-sticky-item {
58
+ display: flex;
59
+ align-items: center;
60
+ gap: 8px;
61
+ padding: 4px 10px;
62
+ font-size: 12px;
63
+ line-height: 1.4;
64
+ }
65
+ #todo-sticky .todo-sticky-item-icon { display: inline-flex; flex-shrink: 0; }
66
+ #todo-sticky .todo-sticky-item.pending .todo-sticky-item-icon { color: var(--text-dimmer); }
67
+ #todo-sticky .todo-sticky-item.in-progress .todo-sticky-item-icon { color: var(--accent); }
68
+ #todo-sticky .todo-sticky-item.completed .todo-sticky-item-icon { color: var(--success); }
69
+ #todo-sticky .todo-sticky-item-text { flex: 1; }
70
+ #todo-sticky .todo-sticky-item.pending .todo-sticky-item-text { color: var(--text-muted); }
71
+ #todo-sticky .todo-sticky-item.in-progress .todo-sticky-item-text { color: var(--text); }
72
+ #todo-sticky .todo-sticky-item.completed .todo-sticky-item-text { color: var(--text-dimmer); text-decoration: line-through; }
73
+
74
+ /* --- Scroll-to-bottom floating button --- */
75
+ #new-msg-btn {
76
+ position: absolute;
77
+ bottom: 80px;
78
+ left: 50%;
79
+ transform: translateX(-50%);
80
+ background: var(--bg-alt);
81
+ color: var(--text-secondary);
82
+ border: 1px solid var(--border);
83
+ border-radius: 20px;
84
+ padding: 6px 16px;
85
+ font-size: 13px;
86
+ cursor: pointer;
87
+ z-index: 10;
88
+ transition: opacity 0.15s;
89
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3);
90
+ }
91
+ #new-msg-btn:hover {
92
+ background: var(--sidebar-hover);
93
+ color: var(--text);
94
+ }
95
+ #new-msg-btn.hidden {
96
+ display: none;
97
+ }
98
+
12
99
  /* --- User message --- */
13
100
  .msg-user {
14
101
  display: flex;
@@ -555,6 +642,99 @@ pre.mermaid-error {
555
642
  .diff-content .diff-file-header { color: #8B949E; font-weight: 600; }
556
643
  .diff-content .diff-ctx { color: var(--text-muted); }
557
644
 
645
+ /* --- Edit diff viewer --- */
646
+ .edit-diff-header {
647
+ display: flex;
648
+ align-items: center;
649
+ padding: 6px 14px;
650
+ font-size: 12px;
651
+ font-family: "SF Mono", Menlo, Monaco, monospace;
652
+ color: var(--text-muted);
653
+ background: rgba(255, 255, 255, 0.03);
654
+ border-bottom: 1px solid var(--border-subtle);
655
+ }
656
+
657
+ .edit-diff-path { flex: 1; }
658
+
659
+ .edit-diff-toggles {
660
+ display: inline-flex;
661
+ border: 1px solid var(--border);
662
+ border-radius: 6px;
663
+ overflow: hidden;
664
+ }
665
+
666
+ .edit-diff-toggle {
667
+ display: inline-flex;
668
+ align-items: center;
669
+ justify-content: center;
670
+ width: 28px;
671
+ height: 24px;
672
+ padding: 0;
673
+ border: none;
674
+ background: var(--bg-alt);
675
+ color: var(--text-dimmer);
676
+ cursor: pointer;
677
+ }
678
+ .edit-diff-toggle .lucide { width: 14px; height: 14px; }
679
+ .edit-diff-toggle:hover { color: var(--text-secondary); background: var(--sidebar-hover); }
680
+ .edit-diff-toggle.active { color: var(--text); background: var(--sidebar-active); }
681
+ .edit-diff-toggle + .edit-diff-toggle { border-left: 1px solid var(--border); }
682
+
683
+ .edit-diff-body {
684
+ display: flex;
685
+ max-height: 300px;
686
+ overflow-y: auto;
687
+ overflow-x: hidden;
688
+ }
689
+
690
+ .edit-diff-body pre {
691
+ margin: 0;
692
+ max-height: none;
693
+ overflow-y: visible;
694
+ font-family: "SF Mono", Menlo, Monaco, monospace;
695
+ font-size: 12px;
696
+ line-height: 1.55;
697
+ }
698
+
699
+ .edit-diff-gutter {
700
+ flex-shrink: 0;
701
+ padding: 12px 12px 12px 14px;
702
+ text-align: right;
703
+ color: var(--text-dimmer);
704
+ user-select: none;
705
+ -webkit-user-select: none;
706
+ min-width: 36px;
707
+ border-right: 1px solid var(--border-subtle);
708
+ }
709
+
710
+ .edit-diff-content {
711
+ flex: 1;
712
+ padding: 12px 14px;
713
+ white-space: pre-wrap;
714
+ word-break: break-all;
715
+ }
716
+
717
+ .edit-diff-content .diff-add {
718
+ color: #3FB950;
719
+ background: rgba(63, 185, 80, 0.08);
720
+ display: inline-block;
721
+ width: 100%;
722
+ }
723
+
724
+ .edit-diff-content .diff-del {
725
+ color: #F85149;
726
+ background: rgba(248, 81, 73, 0.08);
727
+ display: inline-block;
728
+ width: 100%;
729
+ }
730
+
731
+ /* Split view */
732
+ .edit-diff-split { overflow-x: auto; }
733
+ .edit-diff-split .edit-diff-side-old { border-right: 1px solid var(--border); }
734
+ .edit-diff-split .edit-diff-side-old,
735
+ .edit-diff-split .edit-diff-side-new { min-width: 0; flex: 1; }
736
+ .edit-diff-split .edit-diff-gutter { min-width: 30px; padding: 12px 8px 12px 10px; }
737
+
558
738
  /* --- Code viewer (Read tool) --- */
559
739
  .code-viewer {
560
740
  display: flex;
@@ -657,6 +837,9 @@ pre.mermaid-error {
657
837
  .plan-card-icon { display: inline-flex; color: var(--accent); }
658
838
  .plan-card-icon .lucide { width: 16px; height: 16px; }
659
839
  .plan-card-title { font-size: 14px; font-weight: 600; color: var(--accent); flex: 1; }
840
+ .plan-card-copy { display: inline-flex; align-items: center; justify-content: center; background: none; border: none; color: var(--text-dimmer); cursor: pointer; padding: 2px; border-radius: 4px; transition: color 0.15s; }
841
+ .plan-card-copy:hover { color: var(--text); }
842
+ .plan-card-copy .lucide { width: 14px; height: 14px; }
660
843
  .plan-card-chevron { display: inline-flex; color: var(--text-muted); transition: transform 0.2s; }
661
844
  .plan-card-chevron .lucide { width: 14px; height: 14px; }
662
845
  .plan-card.collapsed .plan-card-chevron { transform: rotate(-90deg); }
@@ -689,6 +872,14 @@ pre.mermaid-error {
689
872
  .plan-card-body strong { font-weight: 600; }
690
873
  .plan-card-body blockquote { border-left: 3px solid var(--border); padding-left: 12px; color: var(--text-muted); margin: 8px 0; }
691
874
 
875
+ /* --- Plan permission (ExitPlanMode) --- */
876
+ .plan-permission-header {
877
+ background: rgba(87, 171, 90, 0.08) !important;
878
+ border-color: rgba(87, 171, 90, 0.2) !important;
879
+ }
880
+ .plan-permission-header .permission-icon { color: var(--success) !important; }
881
+ .plan-permission-header .permission-title { color: var(--success) !important; }
882
+
692
883
  /* ==========================================================================
693
884
  Todo Widget
694
885
  ========================================================================== */
@@ -335,7 +335,10 @@
335
335
  }
336
336
 
337
337
  .session-list-header {
338
- padding: 24px 20px 4px;
338
+ display: flex;
339
+ align-items: center;
340
+ gap: 4px;
341
+ padding: 24px 12px 4px 20px;
339
342
  font-size: 11px;
340
343
  font-weight: 600;
341
344
  color: var(--text-dimmer);
@@ -343,6 +346,95 @@
343
346
  letter-spacing: 0.5px;
344
347
  }
345
348
 
349
+ #search-session-btn {
350
+ display: flex;
351
+ align-items: center;
352
+ justify-content: center;
353
+ width: 24px;
354
+ height: 24px;
355
+ border-radius: 6px;
356
+ border: none;
357
+ background: transparent;
358
+ color: var(--text-dimmer);
359
+ cursor: pointer;
360
+ padding: 0;
361
+ transition: color 0.15s, background 0.15s;
362
+ }
363
+
364
+ #search-session-btn .lucide { width: 14px; height: 14px; }
365
+ #search-session-btn:hover { color: var(--text); background: var(--sidebar-hover); }
366
+
367
+ /* --- Session search --- */
368
+ #session-search {
369
+ padding: 0 12px 8px;
370
+ }
371
+
372
+ #session-search.hidden {
373
+ display: none;
374
+ }
375
+
376
+ #session-search-input {
377
+ width: 100%;
378
+ padding: 7px 10px;
379
+ border-radius: 8px;
380
+ border: 1px solid var(--border);
381
+ background: var(--input-bg);
382
+ color: var(--text);
383
+ font-size: 13px;
384
+ font-family: inherit;
385
+ outline: none;
386
+ transition: border-color 0.15s;
387
+ }
388
+
389
+ #session-search-input:focus {
390
+ border-color: var(--accent);
391
+ }
392
+
393
+ #session-search-input::placeholder {
394
+ color: var(--text-dimmer);
395
+ }
396
+
397
+ #search-session-btn.active {
398
+ color: var(--accent);
399
+ }
400
+
401
+ .session-item.search-match {
402
+ background: rgba(218, 119, 86, 0.1);
403
+ color: var(--text);
404
+ }
405
+
406
+ .session-item.search-match .session-item-text {
407
+ color: var(--accent);
408
+ }
409
+
410
+ .session-item.search-dimmed {
411
+ opacity: 0.35;
412
+ }
413
+
414
+ .session-highlight {
415
+ background: rgba(218, 119, 86, 0.3);
416
+ color: var(--text);
417
+ border-radius: 2px;
418
+ padding: 0 1px;
419
+ }
420
+
421
+ /* --- Search hit timeline (right-side markers) --- */
422
+ .search-timeline {
423
+ position: absolute;
424
+ right: 8px;
425
+ z-index: 10;
426
+ pointer-events: none;
427
+ }
428
+
429
+ .search-blink {
430
+ animation: searchBlink 0.4s ease-in-out 3;
431
+ }
432
+
433
+ @keyframes searchBlink {
434
+ 0%, 100% { box-shadow: none; }
435
+ 50% { box-shadow: 0 0 0 2px var(--accent); border-radius: 12px; }
436
+ }
437
+
346
438
  #session-list {
347
439
  padding: 2px 8px;
348
440
  }
@@ -59,7 +59,13 @@
59
59
  <button id="file-browser-btn"><i data-lucide="folder-tree"></i> <span>File browser</span></button>
60
60
  <button id="terminal-sidebar-btn"><i data-lucide="square-terminal"></i> <span>Terminal</span></button>
61
61
  </div>
62
- <div class="session-list-header">Sessions</div>
62
+ <div class="session-list-header">
63
+ <span>Sessions</span>
64
+ <button id="search-session-btn" type="button" title="Search sessions"><i data-lucide="search"></i></button>
65
+ </div>
66
+ <div id="session-search" class="hidden">
67
+ <input id="session-search-input" type="text" placeholder="Search sessions..." autocomplete="off" spellcheck="false" />
68
+ </div>
63
69
  <div id="session-list"></div>
64
70
  </div>
65
71
  <div id="sidebar-panel-files" class="sidebar-panel hidden">
@@ -74,6 +80,9 @@
74
80
  <i data-lucide="ellipsis" style="width:14px;height:14px"></i>
75
81
  </button>
76
82
  <div id="sidebar-footer-menu" class="hidden">
83
+ <button class="sidebar-menu-item" id="footer-usage">
84
+ <i data-lucide="gauge"></i> <span>Usage</span>
85
+ </button>
77
86
  <a class="sidebar-menu-item" href="https://github.com/chadbyte/claude-relay" target="_blank" rel="noopener">
78
87
  <i data-lucide="github"></i> <span>GitHub</span>
79
88
  </a>
@@ -116,8 +125,9 @@
116
125
  </label>
117
126
  </div>
118
127
  </div>
119
- <button id="terminal-toggle-btn" title="Terminal"><i data-lucide="square-terminal"></i></button>
120
- <button id="qr-btn" title="QR Code"><i data-lucide="qr-code"></i></button>
128
+ <button id="usage-header-btn" class="hidden" title="Usage"><i data-lucide="dollar-sign"></i></button>
129
+ <button id="terminal-toggle-btn" title="Terminal"><i data-lucide="square-terminal"></i><span id="terminal-count" class="hidden"></span></button>
130
+ <button id="qr-btn" title="Share"><i data-lucide="share"></i></button>
121
131
  <div id="qr-overlay" class="hidden">
122
132
  <div id="qr-overlay-inner">
123
133
  <div id="qr-canvas"></div>
@@ -157,14 +167,72 @@
157
167
  </div>
158
168
  <div id="messages"></div>
159
169
 
170
+ <div id="usage-panel" class="hidden">
171
+ <div class="usage-panel-header">
172
+ <span>Usage</span>
173
+ <button id="usage-panel-close" aria-label="Close"><i data-lucide="x"></i></button>
174
+ </div>
175
+ <div class="usage-panel-body">
176
+ <div id="usage-rate-limits">
177
+ <div id="usage-loading" class="usage-loading">Loading...</div>
178
+ <div id="usage-error" class="usage-error hidden"></div>
179
+ <div id="usage-bars" class="hidden">
180
+ <div class="usage-bar-group" id="usage-bar-session">
181
+ <div class="usage-bar-label"><span>Current session</span><span class="usage-bar-pct" id="usage-pct-session"></span></div>
182
+ <div class="usage-bar-track"><div class="usage-bar-fill" id="usage-fill-session"></div></div>
183
+ <div class="usage-bar-reset" id="usage-reset-session"></div>
184
+ </div>
185
+ <div class="usage-bar-group" id="usage-bar-weekly">
186
+ <div class="usage-bar-label"><span>Current week (all models)</span><span class="usage-bar-pct" id="usage-pct-weekly"></span></div>
187
+ <div class="usage-bar-track"><div class="usage-bar-fill" id="usage-fill-weekly"></div></div>
188
+ <div class="usage-bar-reset" id="usage-reset-weekly"></div>
189
+ </div>
190
+ <div class="usage-bar-group" id="usage-bar-sonnet">
191
+ <div class="usage-bar-label"><span>Current week (Sonnet only)</span><span class="usage-bar-pct" id="usage-pct-sonnet"></span></div>
192
+ <div class="usage-bar-track"><div class="usage-bar-fill" id="usage-fill-sonnet"></div></div>
193
+ <div class="usage-bar-reset" id="usage-reset-sonnet"></div>
194
+ </div>
195
+ <div class="usage-bar-group hidden" id="usage-bar-extra">
196
+ <div class="usage-bar-label"><span>Extra usage</span><span class="usage-bar-pct" id="usage-pct-extra"></span></div>
197
+ <div class="usage-bar-track"><div class="usage-bar-fill usage-bar-fill-extra" id="usage-fill-extra"></div></div>
198
+ <div class="usage-bar-reset" id="usage-reset-extra"></div>
199
+ </div>
200
+ </div>
201
+ </div>
202
+ <div class="usage-divider" id="usage-session-divider"></div>
203
+ <div class="usage-section-label">Session</div>
204
+ <div class="usage-row"><span class="usage-label">Cost</span><span id="usage-cost" class="usage-value">$0.0000</span></div>
205
+ <div class="usage-row"><span class="usage-label">Input tokens</span><span id="usage-input" class="usage-value">0</span></div>
206
+ <div class="usage-row"><span class="usage-label">Output tokens</span><span id="usage-output" class="usage-value">0</span></div>
207
+ <div class="usage-row"><span class="usage-label">Cache read</span><span id="usage-cache-read" class="usage-value">0</span></div>
208
+ <div class="usage-row"><span class="usage-label">Cache write</span><span id="usage-cache-write" class="usage-value">0</span></div>
209
+ <div class="usage-row"><span class="usage-label">Turns</span><span id="usage-turns" class="usage-value">0</span></div>
210
+ </div>
211
+ </div>
212
+ <div id="todo-sticky" class="hidden"></div>
213
+ <button id="new-msg-btn" class="hidden" aria-label="Scroll to bottom"></button>
214
+ <button id="usage-fab" class="hidden" aria-label="Usage"><i data-lucide="gauge"></i></button>
160
215
  <div id="input-area">
161
216
  <div id="input-wrapper">
162
217
  <div id="slash-menu"></div>
163
218
  <div id="input-row">
164
219
  <div id="image-preview-bar"></div>
165
- <div id="input-inner">
166
- <textarea id="input" rows="1" placeholder="Message Claude Code..." enterkeyhint="send"></textarea>
167
- <button id="send-btn" disabled aria-label="Send"><i data-lucide="arrow-up"></i></button>
220
+ <textarea id="input" rows="1" placeholder="Message Claude Code..." enterkeyhint="send"></textarea>
221
+ <div id="input-bottom">
222
+ <div id="attach-wrap">
223
+ <button id="attach-btn" type="button" aria-label="Attach"><i data-lucide="plus"></i></button>
224
+ <div id="attach-menu" class="hidden">
225
+ <button class="attach-menu-item" id="attach-camera"><i data-lucide="camera"></i> <span>Take Photo</span></button>
226
+ <button class="attach-menu-item" id="attach-photos"><i data-lucide="image"></i> <span>Add Photos</span></button>
227
+ </div>
228
+ </div>
229
+ <div id="input-bottom-right">
230
+ <div id="model-menu-wrap" class="hidden">
231
+ <button id="model-btn" title="Model"><span id="model-label"></span> <i data-lucide="chevron-down"></i></button>
232
+ <div id="model-menu" class="hidden"></div>
233
+ </div>
234
+ <button id="send-btn" disabled aria-label="Send"><i data-lucide="arrow-up"></i></button>
235
+ </div>
168
236
  </div>
169
237
  </div>
170
238
  </div>
@@ -180,9 +248,22 @@
180
248
  <div class="file-viewer-body" id="file-viewer-body"></div>
181
249
  </div>
182
250
  <div id="terminal-container" class="hidden">
183
- <div class="file-viewer-header">
184
- <span class="file-viewer-path">Terminal</span>
185
- <button class="file-viewer-btn" id="terminal-close" title="Close"><i data-lucide="x"></i></button>
251
+ <div class="terminal-header">
252
+ <div id="terminal-tabs" class="terminal-tabs"></div>
253
+ <div class="terminal-header-actions">
254
+ <button class="file-viewer-btn" id="terminal-new-tab" title="New tab"><i data-lucide="plus"></i></button>
255
+ <button class="file-viewer-btn" id="terminal-close" title="Close panel"><i data-lucide="x"></i></button>
256
+ </div>
257
+ </div>
258
+ <div id="terminal-toolbar" class="hidden">
259
+ <button class="term-key" data-key="tab">Tab</button>
260
+ <button class="term-key term-key-toggle" data-key="ctrl">Ctrl</button>
261
+ <button class="term-key" data-key="esc">Esc</button>
262
+ <span class="term-key-spacer"></span>
263
+ <button class="term-key term-key-arrow" data-key="up">&#9650;</button>
264
+ <button class="term-key term-key-arrow" data-key="down">&#9660;</button>
265
+ <button class="term-key term-key-arrow" data-key="left">&#9664;</button>
266
+ <button class="term-key term-key-arrow" data-key="right">&#9654;</button>
186
267
  </div>
187
268
  <div id="terminal-body"></div>
188
269
  </div>
@@ -15,7 +15,7 @@ export function initFileBrowser(_ctx) {
15
15
 
16
16
  // Close button
17
17
  document.getElementById("file-viewer-close").addEventListener("click", function () {
18
- ctx.fileViewerEl.classList.add("hidden");
18
+ closeFileViewer();
19
19
  });
20
20
 
21
21
  // Copy button
@@ -33,11 +33,29 @@ export function initFileBrowser(_ctx) {
33
33
  // ESC to close
34
34
  document.addEventListener("keydown", function (e) {
35
35
  if (e.key === "Escape" && !ctx.fileViewerEl.classList.contains("hidden")) {
36
- ctx.fileViewerEl.classList.add("hidden");
36
+ closeFileViewer();
37
37
  }
38
38
  });
39
39
  }
40
40
 
41
+ // --- File watch helpers ---
42
+ function sendWatch(filePath) {
43
+ if (ctx.ws && ctx.connected) {
44
+ ctx.ws.send(JSON.stringify({ type: "fs_watch", path: filePath }));
45
+ }
46
+ }
47
+
48
+ function sendUnwatch() {
49
+ if (ctx.ws && ctx.connected) {
50
+ ctx.ws.send(JSON.stringify({ type: "fs_unwatch" }));
51
+ }
52
+ }
53
+
54
+ export function closeFileViewer() {
55
+ sendUnwatch();
56
+ ctx.fileViewerEl.classList.add("hidden");
57
+ }
58
+
41
59
  function renderBody() {
42
60
  var bodyEl = document.getElementById("file-viewer-body");
43
61
  var renderBtn = document.getElementById("file-viewer-render");
@@ -292,9 +310,22 @@ function showFileContent(msg) {
292
310
  }
293
311
 
294
312
  ctx.fileViewerEl.classList.remove("hidden");
313
+ sendWatch(msg.path);
295
314
  refreshIcons();
296
315
  }
297
316
 
317
+ export function handleFileChanged(msg) {
318
+ if (!msg.path || msg.path !== currentFilePath) return;
319
+ if (ctx.fileViewerEl.classList.contains("hidden")) return;
320
+ if (msg.content === currentContent) return;
321
+
322
+ var bodyEl = document.getElementById("file-viewer-body");
323
+ var scrollPos = bodyEl ? bodyEl.scrollTop : 0;
324
+ pendingRefresh = true;
325
+ showFileContent(msg);
326
+ if (bodyEl) bodyEl.scrollTop = scrollPos;
327
+ }
328
+
298
329
  function mapExtToLanguage(ext) {
299
330
  var map = {
300
331
  js: "javascript", ts: "typescript", jsx: "javascript", tsx: "typescript",
@@ -14,6 +14,7 @@ var isRemoteInput = false;
14
14
  var builtinCommands = [
15
15
  { name: "clear", desc: "Clear conversation" },
16
16
  { name: "rewind", desc: "Toggle rewind mode" },
17
+ { name: "usage", desc: "Toggle usage panel" },
17
18
  ];
18
19
 
19
20
  // --- Send ---
@@ -43,11 +44,18 @@ export function sendMessage() {
43
44
  return;
44
45
  }
45
46
 
47
+ if (text === "/usage") {
48
+ ctx.inputEl.value = "";
49
+ clearPendingImages();
50
+ autoResize();
51
+ if (ctx.toggleUsagePanel) ctx.toggleUsagePanel();
52
+ return;
53
+ }
54
+
46
55
  if (!ctx.connected) {
47
56
  ctx.addSystemMessage("Not connected — message not sent.", true);
48
57
  return;
49
58
  }
50
- if (ctx.processing) return;
51
59
 
52
60
  var pastes = pendingPastes.map(function (p) { return p.text; });
53
61
  ctx.addUserMessage(text, images.length > 0 ? images : null, pastes.length > 0 ? pastes : null);
@@ -263,10 +271,90 @@ export function handleInputSync(text) {
263
271
  isRemoteInput = false;
264
272
  }
265
273
 
274
+ // --- Attach menu ---
275
+ var attachMenuOpen = false;
276
+
277
+ function toggleAttachMenu() {
278
+ var menu = document.getElementById("attach-menu");
279
+ if (!menu) return;
280
+ attachMenuOpen = !attachMenuOpen;
281
+ menu.classList.toggle("hidden", !attachMenuOpen);
282
+ }
283
+
284
+ function closeAttachMenu() {
285
+ var menu = document.getElementById("attach-menu");
286
+ if (menu) menu.classList.add("hidden");
287
+ attachMenuOpen = false;
288
+ }
289
+
290
+ function createFileInput(accept, capture, multiple) {
291
+ var input = document.createElement("input");
292
+ input.type = "file";
293
+ input.accept = accept;
294
+ if (capture) input.setAttribute("capture", capture);
295
+ if (multiple) input.multiple = true;
296
+ input.style.display = "none";
297
+ document.body.appendChild(input);
298
+
299
+ input.addEventListener("change", function () {
300
+ if (input.files) {
301
+ for (var i = 0; i < input.files.length; i++) {
302
+ if (input.files[i].type.indexOf("image/") === 0) {
303
+ readImageBlob(input.files[i]);
304
+ }
305
+ }
306
+ }
307
+ document.body.removeChild(input);
308
+ });
309
+
310
+ input.click();
311
+ }
312
+
266
313
  // --- Init ---
267
314
  export function initInput(_ctx) {
268
315
  ctx = _ctx;
269
316
 
317
+ // Attach button
318
+ var isTouchDevice = "ontouchstart" in window;
319
+ var attachBtn = document.getElementById("attach-btn");
320
+ if (attachBtn) {
321
+ attachBtn.addEventListener("click", function (e) {
322
+ e.stopPropagation();
323
+ // Desktop: skip menu, open file picker directly
324
+ if (!isTouchDevice) {
325
+ createFileInput("image/*", null, true);
326
+ return;
327
+ }
328
+ toggleAttachMenu();
329
+ });
330
+ }
331
+
332
+ var cameraBtn = document.getElementById("attach-camera");
333
+ if (cameraBtn) {
334
+ cameraBtn.addEventListener("click", function () {
335
+ closeAttachMenu();
336
+ createFileInput("image/*", "environment");
337
+ });
338
+ }
339
+
340
+ var photosBtn = document.getElementById("attach-photos");
341
+ if (photosBtn) {
342
+ photosBtn.addEventListener("click", function () {
343
+ closeAttachMenu();
344
+ createFileInput("image/*", null, true);
345
+ });
346
+ }
347
+
348
+ // Close attach menu when clicking outside
349
+ document.addEventListener("click", function (e) {
350
+ if (attachMenuOpen) {
351
+ var wrap = document.getElementById("attach-wrap");
352
+ if (wrap && !wrap.contains(e.target)) {
353
+ closeAttachMenu();
354
+ }
355
+ }
356
+ });
357
+
270
358
  // Paste handler
271
359
  document.addEventListener("paste", function (e) {
272
360
  var cd = e.clipboardData;
@@ -370,11 +458,20 @@ export function initInput(_ctx) {
370
458
  }
371
459
 
372
460
  if (e.key === "Enter" && !e.shiftKey && !isComposing) {
461
+ // Mobile: Enter inserts newline, send via button only
462
+ if ("ontouchstart" in window) {
463
+ return;
464
+ }
373
465
  e.preventDefault();
374
466
  sendMessage();
375
467
  }
376
468
  });
377
469
 
470
+ // Mobile: switch enterkeyhint to "enter" so keyboard shows return key
471
+ if ("ontouchstart" in window) {
472
+ ctx.inputEl.setAttribute("enterkeyhint", "enter");
473
+ }
474
+
378
475
  // Send/Stop button
379
476
  ctx.sendBtn.addEventListener("click", function () {
380
477
  if (ctx.processing && ctx.connected) {