+
{localize('com_ui_output')}
-
diff --git a/client/src/components/Chat/Messages/Content/ProgressCircle.tsx b/client/src/components/Chat/Messages/Content/ProgressCircle.tsx
deleted file mode 100644
index be4cd3be151..00000000000
--- a/client/src/components/Chat/Messages/Content/ProgressCircle.tsx
+++ /dev/null
@@ -1,38 +0,0 @@
-export default function ProgressCircle({
- radius,
- circumference,
- offset,
-}: {
- radius: number;
- circumference: number;
- offset: number;
-}) {
- return (
-
- );
-}
diff --git a/client/src/components/Chat/Messages/Content/RetrievalIcon.tsx b/client/src/components/Chat/Messages/Content/RetrievalIcon.tsx
deleted file mode 100644
index fd9dfec07d9..00000000000
--- a/client/src/components/Chat/Messages/Content/RetrievalIcon.tsx
+++ /dev/null
@@ -1,80 +0,0 @@
-export default function RetrievalIcon() {
- return (
-
- );
-}
diff --git a/client/src/components/Chat/Messages/Content/WrenchIcon.tsx b/client/src/components/Chat/Messages/Content/WrenchIcon.tsx
deleted file mode 100644
index 9343f04b404..00000000000
--- a/client/src/components/Chat/Messages/Content/WrenchIcon.tsx
+++ /dev/null
@@ -1,49 +0,0 @@
-import React, { useState, useEffect } from 'react';
-
-export default function WrenchIcon() {
- const [rotate, setRotate] = useState(false);
-
- useEffect(() => {
- const timer = setInterval(() => {
- setRotate((r) => !r);
- }, 2000); // Change 2000 to the duration you want for each pause
-
- return () => clearInterval(timer);
- }, []);
-
- return (
-
- );
-}
diff --git a/e2e/specs/mock/scenarios/code-window-header.spec.ts b/e2e/specs/mock/scenarios/code-window-header.spec.ts
new file mode 100644
index 00000000000..96637abe473
--- /dev/null
+++ b/e2e/specs/mock/scenarios/code-window-header.spec.ts
@@ -0,0 +1,75 @@
+import { expect, test } from '@playwright/test';
+import {
+ MOCK_ENDPOINTS,
+ NEW_CHAT_PATH,
+ enableSkills,
+ fetchJson,
+ getAccessToken,
+ messagesView,
+ requestJson,
+ selectMockEndpoint,
+ sendMessage,
+} from '../helpers';
+
+/**
+ * The code window header used to repaint its panel with a per-mode fill that matched the panel
+ * in every bundled theme. It now paints nothing, so the header always shows the panel's own
+ * `surface-secondary`, including in a theme where the two roles differ. A skill file created by
+ * the model opens a file-authoring card, whose preview carries the header.
+ */
+type SkillSummary = { _id: string; name: string };
+
+test('the code window header shows its panel surface @scenario:code-window-header-shows-its-panel', async ({
+ page,
+}) => {
+ test.setTimeout(120_000);
+ const skillName = `e2e-code-header-${Date.now()}-${Math.floor(Math.random() * 1e4)}`;
+ await page.goto(NEW_CHAT_PATH, { timeout: 10_000 });
+ await selectMockEndpoint(page, MOCK_ENDPOINTS[0]);
+ await enableSkills(page);
+
+ const response = await sendMessage(
+ page,
+ [`E2E_CREATE_SKILL:${skillName}`, 'Create the skill file using host file authoring only.'].join(
+ '\n',
+ ),
+ );
+ expect(response.ok()).toBeTruthy();
+
+ try {
+ const card = messagesView(page).getByRole('button', { name: /^Created / });
+ await expect(card).toBeVisible({ timeout: 30_000 });
+ await card.click();
+
+ const copy = messagesView(page).getByRole('button', { name: 'Copy code', exact: true }).first();
+ await expect(copy).toBeVisible({ timeout: 15_000 });
+
+ const paint = await copy.evaluate((button) => {
+ const header = button.closest('div.justify-between') as HTMLElement;
+ const panel = header.parentElement as HTMLElement;
+ const probe = document.createElement('div');
+ probe.style.backgroundColor = 'rgb(var(--surface-secondary))';
+ document.body.appendChild(probe);
+ const surfaceSecondary = getComputedStyle(probe).backgroundColor;
+ probe.remove();
+ return {
+ header: getComputedStyle(header).backgroundColor,
+ panel: getComputedStyle(panel).backgroundColor,
+ surfaceSecondary,
+ };
+ });
+ expect(paint.header).toBe('rgba(0, 0, 0, 0)');
+ expect(paint.panel).toBe(paint.surfaceSecondary);
+ } finally {
+ const token = await getAccessToken(page);
+ const listed = await fetchJson<{ skills?: SkillSummary[] }>(
+ page,
+ `/api/skills?search=${encodeURIComponent(skillName)}&limit=10`,
+ token,
+ );
+ const skill = listed.skills?.find((item) => item.name === skillName);
+ if (skill) {
+ await requestJson(page, { path: `/api/skills/${skill._id}`, token, method: 'DELETE' });
+ }
+ }
+});
diff --git a/eslint-suppressions.json b/eslint-suppressions.json
index 27d7fcfa3a8..5b5892571cc 100644
--- a/eslint-suppressions.json
+++ b/eslint-suppressions.json
@@ -408,14 +408,6 @@
"count": 4
}
},
- "client/src/components/Chat/Messages/Content/ActionIcon.tsx": {
- "shadcn/no-inline-styles": {
- "count": 13
- },
- "shadcn/no-raw-colors": {
- "count": 9
- }
- },
"client/src/components/Chat/Messages/Content/ActivityPhaseGroup.tsx": {
"shadcn/no-arbitrary-values": {
"count": 1
@@ -660,11 +652,6 @@
"count": 6
}
},
- "client/src/components/Chat/Messages/Content/ProgressCircle.tsx": {
- "shadcn/no-arbitrary-values": {
- "count": 1
- }
- },
"client/src/components/Chat/Messages/Content/ProgressText.tsx": {
"shadcn/no-inline-styles": {
"count": 2
@@ -681,14 +668,6 @@
"count": 1
}
},
- "client/src/components/Chat/Messages/Content/RetrievalIcon.tsx": {
- "shadcn/no-inline-styles": {
- "count": 5
- },
- "shadcn/no-raw-colors": {
- "count": 7
- }
- },
"client/src/components/Chat/Messages/Content/SearchContent.tsx": {
"shadcn/no-unknown-classes": {
"count": 1
@@ -768,14 +747,6 @@
"count": 16
}
},
- "client/src/components/Chat/Messages/Content/WrenchIcon.tsx": {
- "shadcn/no-inline-styles": {
- "count": 1
- },
- "shadcn/no-raw-colors": {
- "count": 1
- }
- },
"client/src/components/Chat/Messages/Content/animate.tsx": {
"shadcn/no-inline-styles": {
"count": 1
diff --git a/packages/client/src/theme/allowlist.md b/packages/client/src/theme/allowlist.md
index 5d7c48398ec..6eaf706be4d 100644
--- a/packages/client/src/theme/allowlist.md
+++ b/packages/client/src/theme/allowlist.md
@@ -39,3 +39,15 @@ is a role, not an exception.
| --------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `packages/client/src/components/Dropdown.css` | Popover shadows in black alpha. The app paints `.popover-ui` from its own copy of these rules in `client/src/style.css`, so moving either copy to `--theme-shadow-lg` needs both edited together. |
| `packages/client/src/components/Tooltip.css` | Tooltip shadows are smaller than any step of the theme shadow scale; black alpha ink, dropped in high contrast where the border takes over. |
+
+## Chat: media, artwork and documents that leave the app
+
+| File | Why |
+| ----------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
+| `client/src/components/Chat/Input/Files/DragDropOverlay.tsx` | The drop-zone illustration's fills are artwork. Its backdrop is black at 40% in both modes; the theme scrim is gray-500 in default light, so it moves to `surface-overlay` once a theme can set scrim opacity per mode (berry-13/LibreChat#181). |
+| `client/src/components/Chat/Input/Files/SourceIcon.tsx` | The OpenAI chip (white/75, black/65 in dark) and the code-execution badge (a black terminal mark with white ink) sit over the file's own preview: media overlays, like the upload progress. |
+| `client/src/components/Chat/Input/Files/ProgressCircle.tsx` | Upload progress drawn over the user's own image: a media scrim, so it stays black and white whatever the theme paints around the image. |
+| `client/src/utils/markdown.ts` | The downloaded HTML document's GitHub Primer stylesheet; it is read outside the app, where no theme variables exist. Its high-contrast branch already reads the roles. |
+| `client/src/utils/mermaid.ts` | The mermaid artifact document and its zoom controls render in a sandboxed iframe without theme variables; the token reads' fallbacks and the luminance repair ink are literals by necessity. |
+| `client/src/utils/richtext.ts` | Inline styles on HTML copied to the clipboard for pasting into other apps, which must not carry the sender's theme. |
+| `client/src/utils/officePreview.ts`, `client/src/hooks/ScreenshotContext.tsx` | Fallbacks for a theme role read at runtime, used only when the variable is unset. |