clay-server 2.26.0-beta.4 → 2.26.0-beta.6

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/public/app.js CHANGED
@@ -12,7 +12,7 @@ import { initInput, clearPendingImages, handleInputSync, autoResize, builtinComm
12
12
  import { initQrCode, triggerShare } from './modules/qrcode.js';
13
13
  import { initFileBrowser, loadRootDirectory, refreshTree, handleFsList, handleFsRead, handleDirChanged, refreshIfOpen, handleFileChanged, handleFileHistory, handleGitDiff, handleFileAt, getPendingNavigate, closeFileViewer, resetFileBrowser } from './modules/filebrowser.js';
14
14
  import { initTerminal, openTerminal, closeTerminal, resetTerminals, handleTermList, handleTermCreated, handleTermOutput, handleTermResized, handleTermExited, handleTermClosed, sendTerminalCommand } from './modules/terminal.js';
15
- import { initContextSources, updateTerminalList, handleContextSourcesState, getActiveSources, hasActiveSources } from './modules/context-sources.js';
15
+ import { initContextSources, updateTerminalList, updateBrowserTabList, handleContextSourcesState, getActiveSources, hasActiveSources } from './modules/context-sources.js';
16
16
  import { initStickyNotes, handleNotesList, handleNoteCreated, handleNoteUpdated, handleNoteDeleted, openArchive, closeArchive, isArchiveOpen, hideNotes, showNotes, isNotesVisible } from './modules/sticky-notes.js';
17
17
  import { initTheme, getThemeColor, getComputedVar, onThemeChange, getCurrentTheme } from './modules/theme.js';
18
18
  import { initTools, resetToolState, saveToolState, restoreToolState, renderAskUserQuestion, markAskUserAnswered, renderPermissionRequest, markPermissionResolved, markPermissionCancelled, renderElicitationRequest, markElicitationResolved, renderPlanBanner, renderPlanCard, handleTodoWrite, handleTaskCreate, handleTaskUpdate, startThinking, appendThinking, stopThinking, resetThinkingGroup, createToolItem, updateToolExecuting, updateToolResult, markAllToolsDone, addTurnMeta, resetTurnMetaCost, enableMainInput, getTools, getPlanContent, setPlanContent, isPlanFilePath, getTodoTools, updateSubagentActivity, addSubagentToolEntry, markSubagentDone, updateSubagentProgress, initSubagentStop, closeToolGroup, removeToolFromGroup } from './modules/tools.js';
@@ -2599,6 +2599,9 @@ import { initDebate, handleDebatePreparing, handleDebateStarted, handleDebateRes
2599
2599
  var contextMiniLabel = $("context-mini-label");
2600
2600
  var contextData = { contextWindow: 0, maxOutputTokens: 0, model: "-", cost: 0, input: 0, output: 0, cacheRead: 0, cacheWrite: 0, turns: 0 };
2601
2601
  var headerContextEl = null;
2602
+ var richContextUsage = null;
2603
+ var ctxPopoverEl = null;
2604
+ var ctxPopoverVisible = false;
2602
2605
 
2603
2606
  // Known context window sizes per model (fallback when SDK omits feature flag)
2604
2607
  var KNOWN_CONTEXT_WINDOWS = {
@@ -2646,6 +2649,14 @@ import { initDebate, handleDebatePreparing, handleDebateStarted, handleDebateRes
2646
2649
  headerContextEl.className = "header-context";
2647
2650
  headerContextEl.innerHTML = '<div class="header-context-bar"><div class="header-context-fill"></div></div><span class="header-context-label"></span>';
2648
2651
  statusArea.insertBefore(headerContextEl, statusArea.firstChild);
2652
+ headerContextEl.addEventListener("mouseenter", function() {
2653
+ if (richContextUsage) {
2654
+ showCtxPopover();
2655
+ }
2656
+ });
2657
+ headerContextEl.addEventListener("mouseleave", function() {
2658
+ ctxHoverTimer = setTimeout(hideCtxPopover, 120);
2659
+ });
2649
2660
  }
2650
2661
  if (headerContextEl) {
2651
2662
  var hFill = headerContextEl.querySelector(".header-context-fill");
@@ -2653,7 +2664,12 @@ import { initDebate, handleDebatePreparing, handleDebateStarted, handleDebateRes
2653
2664
  hFill.style.width = pct.toFixed(1) + "%";
2654
2665
  hFill.className = "header-context-fill" + cls;
2655
2666
  hLabel.textContent = pct.toFixed(0) + "%";
2656
- headerContextEl.dataset.tip = "Context window " + pct.toFixed(0) + "% used (" + formatTokens(used) + " / " + formatTokens(win) + " tokens)";
2667
+ // Use data-tip as fallback when rich data is not yet loaded
2668
+ if (richContextUsage) {
2669
+ headerContextEl.removeAttribute("data-tip");
2670
+ } else {
2671
+ headerContextEl.dataset.tip = "Context window " + pct.toFixed(0) + "% used (" + formatTokens(used) + " / " + formatTokens(win) + " tokens)";
2672
+ }
2657
2673
  }
2658
2674
  }
2659
2675
  contextUsedEl.textContent = formatTokens(used);
@@ -2718,9 +2734,158 @@ import { initDebate, handleDebatePreparing, handleDebateStarted, handleDebateRes
2718
2734
 
2719
2735
  function resetContextData() {
2720
2736
  contextData = { contextWindow: 0, maxOutputTokens: 0, model: "-", cost: 0, input: 0, output: 0, cacheRead: 0, cacheWrite: 0, turns: 0 };
2737
+ richContextUsage = null;
2738
+ hideCtxPopover();
2721
2739
  updateContextPanel();
2722
2740
  }
2723
2741
 
2742
+ // --- Rich context usage popover ---
2743
+
2744
+ var ctxHoverTimer = null;
2745
+
2746
+ function ensureCtxPopover() {
2747
+ if (ctxPopoverEl) return;
2748
+ ctxPopoverEl = document.createElement("div");
2749
+ ctxPopoverEl.className = "context-usage-popover hidden";
2750
+ // Keep popover open when hovering over it
2751
+ ctxPopoverEl.addEventListener("mouseenter", function() {
2752
+ if (ctxHoverTimer) { clearTimeout(ctxHoverTimer); ctxHoverTimer = null; }
2753
+ });
2754
+ ctxPopoverEl.addEventListener("mouseleave", function() {
2755
+ hideCtxPopover();
2756
+ });
2757
+ }
2758
+
2759
+ function showCtxPopover() {
2760
+ if (!headerContextEl || !richContextUsage) return;
2761
+ if (ctxHoverTimer) { clearTimeout(ctxHoverTimer); ctxHoverTimer = null; }
2762
+ ensureCtxPopover();
2763
+ headerContextEl.appendChild(ctxPopoverEl);
2764
+ renderCtxPopover();
2765
+ ctxPopoverEl.classList.remove("hidden");
2766
+ ctxPopoverVisible = true;
2767
+ }
2768
+
2769
+ function hideCtxPopover() {
2770
+ if (!ctxPopoverEl) return;
2771
+ ctxPopoverEl.classList.add("hidden");
2772
+ ctxPopoverVisible = false;
2773
+ }
2774
+
2775
+ // Categories to hide from the legend (noise, not actionable)
2776
+ var CTX_HIDDEN_CATS = { "Free space": 1, "Autocompact buffer": 1 };
2777
+
2778
+ function renderCtxPopover() {
2779
+ if (!ctxPopoverEl || !richContextUsage) return;
2780
+ var d = richContextUsage;
2781
+ var cats = d.categories || [];
2782
+ var total = d.totalTokens || 0;
2783
+ var max = d.maxTokens || 0;
2784
+ var pct = d.percentage != null ? d.percentage : (max > 0 ? (total / max) * 100 : 0);
2785
+
2786
+ var html = "";
2787
+
2788
+ // Header
2789
+ html += '<div class="ctx-pop-header">';
2790
+ html += '<span class="ctx-pop-model">' + escHtml(d.model || contextData.model || "-") + '</span>';
2791
+ html += '<span class="ctx-pop-pct">' + pct.toFixed(0) + '%';
2792
+ html += '<span class="ctx-pop-tokens">' + formatTokens(total) + ' / ' + formatTokens(max) + '</span>';
2793
+ html += '</span>';
2794
+ html += '</div>';
2795
+
2796
+ // Category emoji map
2797
+ var CTX_EMOJI = {
2798
+ "System prompt": "\ud83d\udcdc", "System tools": "\ud83d\udee0\ufe0f",
2799
+ "Memory files": "\ud83d\udcc1", "Skills": "\u26a1", "Messages": "\ud83d\udcac",
2800
+ "MCP tools": "\ud83d\udd0c", "Agents": "\ud83e\udd16", "Deferred tools": "\ud83d\udce6"
2801
+ };
2802
+
2803
+ // Stacked bar
2804
+ if (cats.length > 0 && max > 0) {
2805
+ html += '<div class="ctx-cat-bar">';
2806
+ for (var i = 0; i < cats.length; i++) {
2807
+ var cat = cats[i];
2808
+ if (cat.isDeferred || !cat.tokens || CTX_HIDDEN_CATS[cat.name]) continue;
2809
+ var w = Math.max(0.3, (cat.tokens / max) * 100);
2810
+ html += '<div style="width:' + w.toFixed(2) + '%;background:' + escHtml(cat.color) + '"></div>';
2811
+ }
2812
+ html += '</div>';
2813
+
2814
+ // Legend
2815
+ html += '<div class="ctx-cat-legend">';
2816
+ for (var j = 0; j < cats.length; j++) {
2817
+ var c = cats[j];
2818
+ if (c.isDeferred || !c.tokens || CTX_HIDDEN_CATS[c.name]) continue;
2819
+ var emoji = CTX_EMOJI[c.name] || "\ud83d\udcca";
2820
+ html += '<div class="ctx-cat-item">';
2821
+ html += '<span class="ctx-cat-name">' + em(emoji) + ' ' + escHtml(c.name) + '</span>';
2822
+ html += '<span class="ctx-cat-value">' + formatTokens(c.tokens) + '</span>';
2823
+ html += '</div>';
2824
+ }
2825
+ html += '</div>';
2826
+ }
2827
+
2828
+ // Message breakdown
2829
+ var mb = d.messageBreakdown;
2830
+ if (mb) {
2831
+ html += '<div class="ctx-pop-divider"></div>';
2832
+ html += '<div class="ctx-pop-section-label">' + em("\ud83d\udcac") + ' Messages</div>';
2833
+ if (mb.userMessageTokens) {
2834
+ html += '<div class="ctx-pop-row"><span class="ctx-pop-row-label">' + em("\ud83d\udc64") + ' User</span><span class="ctx-pop-row-value">' + formatTokens(mb.userMessageTokens) + '</span></div>';
2835
+ }
2836
+ if (mb.assistantMessageTokens) {
2837
+ html += '<div class="ctx-pop-row"><span class="ctx-pop-row-label">' + em("\ud83e\udd16") + ' Assistant</span><span class="ctx-pop-row-value">' + formatTokens(mb.assistantMessageTokens) + '</span></div>';
2838
+ }
2839
+ if (mb.toolCallTokens) {
2840
+ html += '<div class="ctx-pop-row"><span class="ctx-pop-row-label">' + em("\ud83d\udee0\ufe0f") + ' Tool calls</span><span class="ctx-pop-row-value">' + formatTokens(mb.toolCallTokens) + '</span></div>';
2841
+ }
2842
+ if (mb.toolResultTokens) {
2843
+ html += '<div class="ctx-pop-row"><span class="ctx-pop-row-label">' + em("\ud83d\udccb") + ' Tool results</span><span class="ctx-pop-row-value">' + formatTokens(mb.toolResultTokens) + '</span></div>';
2844
+ }
2845
+ if (mb.attachmentTokens) {
2846
+ html += '<div class="ctx-pop-row"><span class="ctx-pop-row-label">' + em("\ud83d\udcce") + ' Attachments</span><span class="ctx-pop-row-value">' + formatTokens(mb.attachmentTokens) + '</span></div>';
2847
+ }
2848
+ }
2849
+
2850
+ // Memory files
2851
+ var mf = d.memoryFiles;
2852
+ if (mf && mf.length > 0) {
2853
+ html += '<div class="ctx-pop-divider"></div>';
2854
+ html += '<div class="ctx-pop-section-label">' + em("\ud83d\udcc1") + ' Memory Files</div>';
2855
+ var baseCount = {};
2856
+ for (var mc = 0; mc < mf.length; mc++) {
2857
+ var bn = mf[mc].path.split("/").pop() || mf[mc].path;
2858
+ baseCount[bn] = (baseCount[bn] || 0) + 1;
2859
+ }
2860
+ for (var mi = 0; mi < mf.length; mi++) {
2861
+ var fpath = mf[mi].path;
2862
+ var fname = fpath.split("/").pop() || fpath;
2863
+ if (baseCount[fname] > 1) {
2864
+ var parts = fpath.split("/");
2865
+ fname = parts.length >= 2 ? parts[parts.length - 2] + "/" + fname : fpath;
2866
+ }
2867
+ html += '<div class="ctx-pop-row"><span class="ctx-pop-row-label">' + em("\ud83d\udcc4") + ' ' + escHtml(fname) + '</span><span class="ctx-pop-row-value">' + formatTokens(mf[mi].tokens) + '</span></div>';
2868
+ }
2869
+ }
2870
+
2871
+ // Auto-compact note
2872
+ if (d.isAutoCompactEnabled && d.autoCompactThreshold) {
2873
+ html += '<div class="ctx-pop-note">' + em("\u267b\ufe0f") + ' Auto-compact at ' + formatTokens(d.autoCompactThreshold) + '</div>';
2874
+ }
2875
+
2876
+ ctxPopoverEl.innerHTML = html;
2877
+ }
2878
+
2879
+ function escHtml(s) {
2880
+ var div = document.createElement("div");
2881
+ div.textContent = s;
2882
+ return div.innerHTML;
2883
+ }
2884
+
2885
+ function em(emoji) {
2886
+ return '<span class="ctx-emoji">' + emoji + '</span>';
2887
+ }
2888
+
2724
2889
  function resetContext() {
2725
2890
  resetContextData();
2726
2891
  // Keep view state, just reset data
@@ -4052,6 +4217,10 @@ import { initDebate, handleDebatePreparing, handleDebateStarted, handleDebateRes
4052
4217
 
4053
4218
  case "history_done":
4054
4219
  replayingHistory = false;
4220
+ // Restore cached rich context usage BEFORE updateContextPanel runs
4221
+ if (msg.contextUsage) {
4222
+ richContextUsage = msg.contextUsage;
4223
+ }
4055
4224
  // Restore accurate context data from the last result in full history
4056
4225
  if (msg.lastUsage || msg.lastModelUsage) {
4057
4226
  accumulateContext(msg.lastCost, msg.lastUsage, msg.lastModelUsage, msg.lastStreamInputTokens);
@@ -4419,6 +4588,59 @@ import { initDebate, handleDebatePreparing, handleDebateStarted, handleDebateRes
4419
4588
  }
4420
4589
  break;
4421
4590
 
4591
+ case "context_preview":
4592
+ // Show a Context Card with tab screenshot between user message and assistant response
4593
+ if (msg.tab) {
4594
+ var card = document.createElement("div");
4595
+ card.className = "context-card";
4596
+
4597
+ // Header
4598
+ var header = document.createElement("div");
4599
+ header.className = "context-card-header";
4600
+ var icon = document.createElement("span");
4601
+ icon.className = "context-card-icon";
4602
+ icon.textContent = "\uD83D\uDC41";
4603
+ header.appendChild(icon);
4604
+ var label = document.createElement("span");
4605
+ label.textContent = "Viewing tab";
4606
+ header.appendChild(label);
4607
+ card.appendChild(header);
4608
+
4609
+ // Screenshot
4610
+ if (msg.tab.screenshotUrl) {
4611
+ var img = document.createElement("img");
4612
+ img.className = "context-card-screenshot";
4613
+ img.src = msg.tab.screenshotUrl;
4614
+ img.loading = "lazy";
4615
+ img.addEventListener("click", function () { showImageModal(this.src); });
4616
+ card.appendChild(img);
4617
+ }
4618
+
4619
+ // Meta: title + domain
4620
+ var tabTitle = msg.tab.title || "";
4621
+ var tabDomain = "";
4622
+ try { tabDomain = new URL(msg.tab.url).hostname; } catch (e) {}
4623
+ if (tabTitle || tabDomain) {
4624
+ var meta = document.createElement("div");
4625
+ meta.className = "context-card-meta";
4626
+ var titleEl = document.createElement("span");
4627
+ titleEl.className = "context-card-title";
4628
+ titleEl.textContent = tabTitle;
4629
+ meta.appendChild(titleEl);
4630
+ if (tabDomain) {
4631
+ var domainEl = document.createElement("span");
4632
+ domainEl.className = "context-card-domain";
4633
+ domainEl.textContent = tabDomain;
4634
+ meta.appendChild(domainEl);
4635
+ }
4636
+ card.appendChild(meta);
4637
+ }
4638
+
4639
+ messagesEl.appendChild(card);
4640
+ scrollToBottom();
4641
+ }
4642
+ break;
4643
+
4422
4644
  case "status":
4423
4645
  if (msg.status === "processing") {
4424
4646
  setStatus("processing");
@@ -4614,6 +4836,14 @@ import { initDebate, handleDebatePreparing, handleDebateStarted, handleDebateRes
4614
4836
  accumulateContext(msg.cost, msg.usage, msg.modelUsage, msg.lastStreamInputTokens);
4615
4837
  break;
4616
4838
 
4839
+ case "context_usage":
4840
+ if (msg.data && !replayingHistory) {
4841
+ richContextUsage = msg.data;
4842
+ if (headerContextEl) headerContextEl.removeAttribute("data-tip");
4843
+ if (ctxPopoverVisible) renderCtxPopover();
4844
+ }
4845
+ break;
4846
+
4617
4847
  case "done":
4618
4848
  setActivity(null);
4619
4849
  stopThinking();
@@ -4795,6 +5025,10 @@ import { initDebate, handleDebatePreparing, handleDebateStarted, handleDebateRes
4795
5025
  handleContextSourcesState(msg);
4796
5026
  break;
4797
5027
 
5028
+ case "extension_command":
5029
+ sendExtensionCommand(msg.command, msg.args, msg.requestId);
5030
+ break;
5031
+
4798
5032
  case "term_created":
4799
5033
  handleTermCreated(msg);
4800
5034
  if (pendingTermCommand) {
@@ -5765,6 +5999,59 @@ import { initDebate, handleDebatePreparing, handleDebateStarted, handleDebateRes
5765
5999
  get connected() { return connected; },
5766
6000
  });
5767
6001
 
6002
+ // --- Chrome Extension Bridge ---
6003
+ var _extRequestCallbacks = {}; // requestId -> callback function
6004
+
6005
+ function sendExtensionCommand(command, args, requestId) {
6006
+ window.postMessage({
6007
+ source: "clay-page",
6008
+ payload: {
6009
+ type: "clay_ext_command",
6010
+ command: command,
6011
+ args: args,
6012
+ requestId: requestId
6013
+ }
6014
+ }, "*");
6015
+ }
6016
+
6017
+ function handleExtensionResult(requestId, result) {
6018
+ // Check local callback first (for server-initiated requests)
6019
+ var cb = _extRequestCallbacks[requestId];
6020
+ if (cb) {
6021
+ delete _extRequestCallbacks[requestId];
6022
+ cb(result);
6023
+ return;
6024
+ }
6025
+ // Forward to server
6026
+ if (ws && ws.readyState === 1) {
6027
+ ws.send(JSON.stringify({
6028
+ type: "extension_result",
6029
+ requestId: requestId,
6030
+ result: result
6031
+ }));
6032
+ }
6033
+ }
6034
+
6035
+ window.addEventListener("message", function(event) {
6036
+ if (event.source !== window) return;
6037
+ if (!event.data || event.data.source !== "clay-chrome-extension") return;
6038
+ var msg = event.data.payload;
6039
+
6040
+ if (msg.type === "clay_ext_tab_list") {
6041
+ updateBrowserTabList(msg.tabs);
6042
+ // Also inform server about tab list
6043
+ if (ws && ws.readyState === 1) {
6044
+ ws.send(JSON.stringify({
6045
+ type: "browser_tab_list",
6046
+ tabs: msg.tabs
6047
+ }));
6048
+ }
6049
+ }
6050
+ if (msg.type === "clay_ext_result") {
6051
+ handleExtensionResult(msg.requestId, msg.result);
6052
+ }
6053
+ });
6054
+
5768
6055
  // --- Playbook Engine ---
5769
6056
  initPlaybook();
5770
6057
 
@@ -328,6 +328,12 @@
328
328
  white-space: nowrap;
329
329
  border: 1px solid var(--border);
330
330
  transition: border-color 0.15s;
331
+ animation: chipIn 0.3s ease-out;
332
+ }
333
+
334
+ @keyframes chipIn {
335
+ from { opacity: 0; transform: translateY(6px) scale(0.95); }
336
+ to { opacity: 1; transform: translateY(0) scale(1); }
331
337
  }
332
338
 
333
339
  .context-chip-label {
@@ -368,6 +374,8 @@
368
374
  bottom: calc(100% + 4px);
369
375
  left: 0;
370
376
  min-width: 200px;
377
+ max-height: 320px;
378
+ overflow-y: auto;
371
379
  background: var(--sidebar-bg);
372
380
  border: 1px solid var(--border);
373
381
  border-radius: 10px;
@@ -430,6 +438,14 @@
430
438
  text-align: center;
431
439
  }
432
440
 
441
+ .context-picker-favicon {
442
+ width: 14px;
443
+ height: 14px;
444
+ border-radius: 2px;
445
+ flex-shrink: 0;
446
+ object-fit: contain;
447
+ }
448
+
433
449
  /* ==========================================================================
434
450
  Input Area — Claude-style unified container
435
451
  ========================================================================== */
@@ -123,6 +123,187 @@ button.top-bar-pill.pill-accent:hover { background: color-mix(in srgb, var(--acc
123
123
  }
124
124
  .pwa-standalone .top-bar-share-btn { display: none !important; }
125
125
 
126
+ /* Extension pill button — same style as share/install pills */
127
+ .ext-pill-wrap {
128
+ position: relative;
129
+ display: flex;
130
+ align-items: center;
131
+ }
132
+ .top-bar-ext-btn {
133
+ display: inline-flex;
134
+ align-items: center;
135
+ gap: 4px;
136
+ background: color-mix(in srgb, var(--text-muted) 10%, transparent);
137
+ color: var(--text-secondary);
138
+ border: none;
139
+ border-radius: 10px;
140
+ padding: 2px 10px;
141
+ font-family: inherit;
142
+ font-size: 11px;
143
+ font-weight: 600;
144
+ cursor: pointer;
145
+ white-space: nowrap;
146
+ line-height: 1;
147
+ transition: background 0.15s, color 0.15s;
148
+ }
149
+ .top-bar-ext-btn .lucide { width: 12px; height: 12px; }
150
+ .top-bar-ext-btn:hover { background: color-mix(in srgb, var(--text-muted) 18%, transparent); color: var(--text); }
151
+ @media (max-width: 768px) {
152
+ .top-bar-ext-btn span { display: none; }
153
+ }
154
+
155
+ /* Extension popover */
156
+ .ext-popover {
157
+ display: none;
158
+ position: absolute;
159
+ top: calc(100% + 8px);
160
+ left: 0;
161
+ background: var(--code-bg);
162
+ border: 1px solid var(--border);
163
+ border-radius: 12px;
164
+ padding: 16px;
165
+ z-index: 200;
166
+ box-shadow: 0 8px 32px rgba(var(--shadow-rgb), 0.35);
167
+ width: 340px;
168
+ }
169
+ .ext-popover.visible { display: block; }
170
+ .ext-popover-header { margin-bottom: 8px; }
171
+ .ext-popover-title {
172
+ font-size: 14px;
173
+ font-weight: 700;
174
+ color: var(--text);
175
+ }
176
+ .ext-experimental {
177
+ font-size: 10px;
178
+ font-weight: 600;
179
+ color: var(--warning, #f59e0b);
180
+ background: color-mix(in srgb, var(--warning, #f59e0b) 12%, transparent);
181
+ padding: 2px 6px;
182
+ border-radius: 6px;
183
+ vertical-align: middle;
184
+ letter-spacing: 0.3px;
185
+ text-transform: uppercase;
186
+ position: relative;
187
+ top: -1px;
188
+ }
189
+ .ext-popover-sub {
190
+ font-size: 11px;
191
+ color: var(--text-muted);
192
+ margin-top: 2px;
193
+ }
194
+ .ext-popover-sub a {
195
+ color: var(--accent);
196
+ text-decoration: none;
197
+ }
198
+ .ext-popover-sub a:hover { text-decoration: underline; }
199
+ .ext-popover-desc {
200
+ font-size: 12px;
201
+ color: var(--text-secondary);
202
+ line-height: 1.5;
203
+ margin-bottom: 12px;
204
+ }
205
+ .ext-popover-download {
206
+ display: flex;
207
+ align-items: center;
208
+ justify-content: center;
209
+ gap: 6px;
210
+ width: 100%;
211
+ padding: 8px 0;
212
+ background: var(--accent);
213
+ color: #fff;
214
+ border: none;
215
+ border-radius: 8px;
216
+ font-family: inherit;
217
+ font-size: 12.5px;
218
+ font-weight: 600;
219
+ cursor: pointer;
220
+ transition: opacity 0.15s;
221
+ }
222
+ .ext-popover-download .lucide { width: 14px; height: 14px; }
223
+ .ext-popover-download:hover { opacity: 0.85; }
224
+ .ext-popover-download:disabled { opacity: 0.5; cursor: default; }
225
+ .ext-popover-status {
226
+ font-size: 11px;
227
+ color: var(--accent);
228
+ text-align: center;
229
+ margin-top: 6px;
230
+ }
231
+ .ext-popover-status.hidden { display: none; }
232
+ .ext-popover-divider {
233
+ height: 1px;
234
+ background: var(--border);
235
+ margin: 12px 0;
236
+ }
237
+ .ext-popover-guide-title {
238
+ font-size: 11px;
239
+ font-weight: 700;
240
+ color: var(--text-muted);
241
+ text-transform: uppercase;
242
+ letter-spacing: 0.5px;
243
+ margin-bottom: 8px;
244
+ }
245
+ .ext-popover-steps {
246
+ display: flex;
247
+ flex-direction: column;
248
+ gap: 6px;
249
+ }
250
+ .ext-popover-step {
251
+ font-size: 12px;
252
+ color: var(--text-secondary);
253
+ line-height: 1.5;
254
+ display: flex;
255
+ gap: 8px;
256
+ align-items: baseline;
257
+ }
258
+ .ext-snum {
259
+ display: inline-flex;
260
+ align-items: center;
261
+ justify-content: center;
262
+ width: 18px;
263
+ height: 18px;
264
+ border-radius: 50%;
265
+ background: var(--accent);
266
+ color: #fff;
267
+ font-size: 10px;
268
+ font-weight: 700;
269
+ flex-shrink: 0;
270
+ position: relative;
271
+ top: 1px;
272
+ }
273
+ .ext-popover-code {
274
+ font-family: "Roboto Mono", monospace;
275
+ font-size: 11px;
276
+ background: var(--bg-tertiary);
277
+ padding: 1px 5px;
278
+ border-radius: 4px;
279
+ cursor: pointer;
280
+ }
281
+ .ext-popover-code:hover { background: color-mix(in srgb, var(--accent) 15%, var(--bg-tertiary)); }
282
+
283
+ /* Extension connected state */
284
+ .top-bar-ext-btn.ext-connected {
285
+ background: color-mix(in srgb, var(--success, #22c55e) 12%, transparent);
286
+ color: var(--success, #22c55e);
287
+ }
288
+ .top-bar-ext-btn.ext-connected:hover {
289
+ background: color-mix(in srgb, var(--success, #22c55e) 20%, transparent);
290
+ color: var(--success, #22c55e);
291
+ }
292
+ .ext-popover-connected {
293
+ display: flex;
294
+ align-items: center;
295
+ gap: 6px;
296
+ padding: 8px 10px;
297
+ background: color-mix(in srgb, var(--success, #22c55e) 10%, transparent);
298
+ color: var(--success, #22c55e);
299
+ border-radius: 8px;
300
+ font-size: 12.5px;
301
+ font-weight: 600;
302
+ margin-bottom: 12px;
303
+ }
304
+ .ext-popover-connected .lucide { width: 15px; height: 15px; }
305
+ .ext-popover-connected.hidden { display: none; }
306
+
126
307
  /* PWA install modal */
127
308
  .pwa-modal {
128
309
  position: fixed;
@@ -171,6 +171,85 @@
171
171
 
172
172
  .sys-msg.error .sys-text { color: var(--error); }
173
173
 
174
+ /* ==========================================================================
175
+ Context Card (browser tab preview shown between user msg and assistant response)
176
+ ========================================================================== */
177
+
178
+ .context-card {
179
+ max-width: 400px;
180
+ margin: 8px auto 12px;
181
+ padding: 12px;
182
+ background: var(--bg-alt);
183
+ border: 1px solid var(--border);
184
+ border-radius: 10px;
185
+ box-shadow: 0 1px 3px rgba(var(--shadow-rgb), 0.08);
186
+ }
187
+
188
+ .context-card-header {
189
+ display: flex;
190
+ align-items: center;
191
+ gap: 6px;
192
+ margin-bottom: 10px;
193
+ font-size: 11px;
194
+ color: var(--text-muted);
195
+ letter-spacing: 0.02em;
196
+ font-weight: 500;
197
+ }
198
+
199
+ .context-card-header .context-card-icon {
200
+ font-size: 13px;
201
+ opacity: 0.7;
202
+ }
203
+
204
+ .context-card-screenshot {
205
+ display: block;
206
+ width: 100%;
207
+ max-height: 200px;
208
+ object-fit: contain;
209
+ border-radius: 8px;
210
+ background: var(--bg);
211
+ cursor: pointer;
212
+ transition: transform 0.15s ease;
213
+ }
214
+
215
+ .context-card-screenshot:hover {
216
+ transform: scale(1.01);
217
+ }
218
+
219
+ .context-card-skeleton {
220
+ width: 100%;
221
+ height: 120px;
222
+ border-radius: 8px;
223
+ background: var(--bg);
224
+ animation: skeleton-shimmer 1.5s ease-in-out infinite;
225
+ }
226
+
227
+ .context-card-meta {
228
+ display: flex;
229
+ justify-content: space-between;
230
+ align-items: baseline;
231
+ margin-top: 8px;
232
+ gap: 12px;
233
+ }
234
+
235
+ .context-card-title {
236
+ font-size: 12px;
237
+ color: var(--text-muted);
238
+ white-space: nowrap;
239
+ overflow: hidden;
240
+ text-overflow: ellipsis;
241
+ flex: 1;
242
+ min-width: 0;
243
+ }
244
+
245
+ .context-card-domain {
246
+ font-size: 11px;
247
+ color: var(--text-dimmer);
248
+ font-family: monospace;
249
+ white-space: nowrap;
250
+ flex-shrink: 0;
251
+ }
252
+
174
253
  /* ==========================================================================
175
254
  Activity Indicator
176
255
  ========================================================================== */
@@ -906,3 +906,4 @@
906
906
  line-height: 1.45;
907
907
  margin-top: 2px;
908
908
  }
909
+