diff --git a/.changeset/FN-7541-remove-chat-eye-icon.md b/.changeset/FN-7541-remove-chat-eye-icon.md new file mode 100644 index 0000000000..abf2336a76 --- /dev/null +++ b/.changeset/FN-7541-remove-chat-eye-icon.md @@ -0,0 +1,7 @@ +--- +"@runfusion/fusion": patch +--- + +summary: Remove the eye icon markdown/plain toggle from chat; messages always render as Markdown. +category: breaking +dev: Removed ChatView `chat-thread-header-render-toggle` (desktop + mobile), `showAllAsPlain` state, and `chat.showRenderedMarkdown`/`chat.showPlainText` i18n keys (FN-7541). diff --git a/packages/dashboard/app/components/ChatView.css b/packages/dashboard/app/components/ChatView.css index 7e55c8fe74..820d77d008 100644 --- a/packages/dashboard/app/components/ChatView.css +++ b/packages/dashboard/app/components/ChatView.css @@ -940,61 +940,9 @@ In the narrow/mobile chat layout there is no room for an expand/maximize afforda } /* -FNXC:ChatRenderMode 2026-07-02-00:00: -Chat keeps one thread-wide Markdown/plain control: desktop/tablet anchors it in the thread identity header, while mobile direct-thread view floats the same button above the transcript/composer so it does not reintroduce a second header row or per-message toggles. +FNXC:ChatRenderToggle 2026-07-04-00:00: +The thread-wide Markdown/plain render toggle (`.chat-thread-header-render-toggle*`, desktop + mobile floating variants) was removed per FN-7541. Chat always renders Markdown now; the button and its CSS shells no longer exist. */ -.chat-thread-header-render-toggle { - margin-left: auto; - flex-shrink: 0; - display: inline-flex; - align-items: center; - justify-content: center; - width: calc(var(--space-md) * 3); - height: calc(var(--space-md) * 3); - min-width: calc(var(--space-md) * 3); - min-height: calc(var(--space-md) * 3); - padding: 0; - border: none; - border-radius: var(--radius-sm); - color: var(--text-muted); - background: transparent; - cursor: pointer; - outline: none; - transition: background var(--transition-fast), color var(--transition-fast); -} - -.chat-thread-header-render-toggle:hover { - background: var(--surface-hover); - color: var(--text); -} - -.chat-thread-header-render-toggle:focus-visible { - outline: none; - box-shadow: var(--focus-ring-strong); -} - -.chat-thread-header-render-toggle--plain { - color: var(--text); -} - -.chat-thread-header-render-toggle--floating { - position: absolute; - right: var(--space-md); - bottom: calc((var(--space-lg) * 2.5) + (var(--space-md) * 2) + var(--space-sm)); - z-index: 3; - width: calc(var(--space-lg) * 2.25); - height: calc(var(--space-lg) * 2.25); - min-width: calc(var(--space-lg) * 2.25); - min-height: calc(var(--space-lg) * 2.25); - border: var(--btn-border-width) solid var(--border); - border-radius: var(--radius-full); - background: var(--surface); - box-shadow: var(--shadow-md); -} - -.chat-thread-header-render-toggle--floating:hover { - background: var(--surface-hover); -} /* Messages */ .chat-messages { @@ -2238,10 +2186,6 @@ Queued-message banners stack above the composer input with a capped scroll area, text-overflow: ellipsis; } - .chat-thread-header-render-toggle { - flex-shrink: 0; - } - .chat-thread-header-context { display: none; } diff --git a/packages/dashboard/app/components/ChatView.tsx b/packages/dashboard/app/components/ChatView.tsx index 8e66988323..175803a404 100644 --- a/packages/dashboard/app/components/ChatView.tsx +++ b/packages/dashboard/app/components/ChatView.tsx @@ -10,8 +10,6 @@ import { Pencil, ChevronLeft, Bot, - Eye, - EyeOff, Paperclip, ChevronDown, Copy, @@ -545,11 +543,9 @@ export function ChatView({ projectId, addToast, floating = false, compactLayout const [mentionPopupVisible, setMentionPopupVisible] = useState(false); const [mentionHighlightIndex, setMentionHighlightIndex] = useState(0); const [mentionStartPos, setMentionStartPos] = useState(-1); - // Single thread-wide toggle: when true, all assistant content (including the - // streaming bubble) renders as plain text instead of Markdown. Replaces the - // earlier per-message toggle so the chat header owns this control instead - // of every reply having its own button. - const [showAllAsPlain, setShowAllAsPlain] = useState(false); + // FNXC:ChatRenderToggle 2026-07-04-00:00: The markdown/plain eye toggle + // (showAllAsPlain / toggleAllAsPlain) was removed per FN-7541. Chat always + // renders Markdown now; forcePlain is hardcoded to false everywhere below. // Attachment state mirrors QuickEntryBox: pending files selected before send. const [pendingAttachments, setPendingAttachments] = useState([]); const [isDragOver, setIsDragOver] = useState(false); @@ -2179,18 +2175,13 @@ export function ChatView({ projectId, addToast, floating = false, compactLayout // In model-only chats (no real agent picked) the agent identity *is* the // model name, which is already in the thread header. Repeating it on every - // assistant bubble is noise. Hide the per-message identity row entirely; - // the render-mode toggle still appears in a slim toolbar. + // assistant bubble is noise. Hide the per-message identity row entirely. const hideAssistantIdentity = activeSession?.agentId === FN_AGENT_ID; const getPendingPreview = (message: string) => message.length > 50 ? `${message.slice(0, 50)}…` : message; - const toggleAllAsPlain = useCallback(() => { - setShowAllAsPlain((value) => !value); - }, []); - useEffect(() => { if (!mobileSessionMenuOpen) { return; @@ -2336,7 +2327,7 @@ export function ChatView({ projectId, addToast, floating = false, compactLayout {/* Header - desktop/tablet keeps the thread identity row; mobile direct-thread controls move into ViewHeader. */} + {/* FNXC:ChatRenderToggle 2026-07-04-00:00: The markdown/plain eye toggle + button (desktop `.chat-thread-header-render-toggle` and the mobile + floating `--floating` variant) was removed per FN-7541. Chat now + always renders Markdown (forcePlain is hardcoded to false). */} {!isChatMobile && (hasThreadInView || !isChatMobile) && (
@@ -3394,32 +3389,9 @@ export function ChatView({ projectId, addToast, floating = false, compactLayout ) : null}
- {hasThreadInView && ( - - )}
)} - {isChatMobile && hasThreadInView && ( - - )} - {/* Messages + composer. CLI-backed chat sessions delegate this region to (transcript/raw-terminal toggle + queued composer); generic-tier adapters render terminal-only. */} diff --git a/packages/dashboard/app/components/__tests__/ChatView.core-interactions.test.tsx b/packages/dashboard/app/components/__tests__/ChatView.core-interactions.test.tsx index 74a4e55a22..96a7419eaa 100644 --- a/packages/dashboard/app/components/__tests__/ChatView.core-interactions.test.tsx +++ b/packages/dashboard/app/components/__tests__/ChatView.core-interactions.test.tsx @@ -1223,7 +1223,10 @@ describe("ChatView core interactions", () => { expect(headerModelTag).toBeNull(); }); - it("keeps provider identity text grouped in header while render toggle stays on the same row", async () => { + // FNXC:ChatRenderToggle 2026-07-04-00:00: the render toggle previously + // asserted here was removed per FN-7541; keep the identity-grouping + // assertions and add a regression check that no toggle remains. + it("keeps provider identity text grouped in header with no render toggle present", async () => { setupMockChat({ activeSession: { id: "session-001", @@ -1243,7 +1246,6 @@ describe("ChatView core interactions", () => { const header = document.querySelector(".chat-thread-header") as HTMLElement | null; const identity = screen.getByTestId("chat-thread-header-identity"); - const toggle = screen.getByTestId("chat-thread-render-toggle"); const providerIcon = identity.querySelector(".provider-icon"); const modelTag = identity.querySelector(".chat-model-tag"); const newChatButton = screen.getByTestId("chat-new-btn"); @@ -1254,8 +1256,7 @@ describe("ChatView core interactions", () => { expect(within(identity).getByText("Agent Chat")).toBeInTheDocument(); expect(modelTag).toBeInTheDocument(); expect(modelTag).toHaveTextContent("Claude Sonnet 4.5"); - expect(toggle).toBeInTheDocument(); - expect(header?.children[header.children.length - 1]).toBe(toggle); + expect(screen.queryByTestId("chat-thread-render-toggle")).not.toBeInTheDocument(); expect(document.querySelectorAll(".chat-thread-header .chat-model-tag")).toHaveLength(1); }); diff --git a/packages/dashboard/app/components/__tests__/ChatView.core.test.tsx b/packages/dashboard/app/components/__tests__/ChatView.core.test.tsx index 16c81a2901..936945cc01 100644 --- a/packages/dashboard/app/components/__tests__/ChatView.core.test.tsx +++ b/packages/dashboard/app/components/__tests__/ChatView.core.test.tsx @@ -501,7 +501,12 @@ describe("ChatView", () => { expect(screen.queryByTestId("chat-render-mode-plain")).not.toBeInTheDocument(); }); - it("thread-header toggle flips every assistant bubble between rendered Markdown and plain text", async () => { + // FNXC:ChatRenderToggle 2026-07-04-00:00: The thread-header markdown/plain + // toggle was removed per FN-7541. Chat now always renders Markdown + // (forcePlain={false} everywhere), so these regression tests assert the + // toggle is gone and Markdown rendering stays intact for both persisted + // and streaming assistant bubbles, on the desktop surface. + it("has no thread-header render toggle and still renders assistant Markdown", async () => { setupMockChat({ activeSession: { id: "session-001", agentId: "agent-001", status: "active", title: "Test Chat", createdAt: "2026-04-08T00:00:00.000Z", updatedAt: "2026-04-08T00:00:00.000Z" }, messages: [ @@ -514,27 +519,14 @@ describe("ChatView", () => { const firstBubble = screen.getByTestId("chat-message-msg-001"); const secondBubble = screen.getByTestId("chat-message-msg-002"); - const headerToggle = screen.getByTestId("chat-thread-render-toggle"); - // Per-message toggles were intentionally removed; only the single - // thread-level toggle should exist. + expect(screen.queryByTestId("chat-thread-render-toggle")).not.toBeInTheDocument(); expect(screen.queryAllByTestId("chat-message-render-toggle")).toHaveLength(0); expect(within(firstBubble).getByText("First", { selector: "strong" })).toBeInTheDocument(); expect(within(secondBubble).getByText("Second", { selector: "strong" })).toBeInTheDocument(); - - await userEvent.click(headerToggle); - - expect(within(firstBubble).getByText(/\*\*First\*\* item/)).toBeInTheDocument(); - expect(within(firstBubble).queryByText("First", { selector: "strong" })).toBeNull(); - expect(within(secondBubble).getByText(/\*\*Second\*\* item/)).toBeInTheDocument(); - expect(within(secondBubble).queryByText("Second", { selector: "strong" })).toBeNull(); - - await userEvent.click(headerToggle); - expect(within(firstBubble).getByText("First", { selector: "strong" })).toBeInTheDocument(); - expect(within(secondBubble).getByText("Second", { selector: "strong" })).toBeInTheDocument(); }); - it("thread-header toggle also drives the streaming bubble", async () => { + it("has no thread-header render toggle and still renders the streaming bubble as Markdown", async () => { setupMockChat({ activeSession: { id: "session-001", agentId: "agent-001", status: "active", title: "Test Chat", createdAt: "2026-04-08T00:00:00.000Z", updatedAt: "2026-04-08T00:00:00.000Z" }, messages: [{ id: "msg-001", sessionId: "session-001", role: "assistant", content: "**Persisted**", createdAt: "2026-04-08T00:00:00.000Z" }], @@ -546,17 +538,8 @@ describe("ChatView", () => { const persistedBubble = screen.getByTestId("chat-message-msg-001"); const streamingBubble = document.querySelector(".chat-message--streaming") as HTMLElement; - const headerToggle = screen.getByTestId("chat-thread-render-toggle"); - expect(within(streamingBubble).getByText("Live", { selector: "strong" })).toBeInTheDocument(); - expect(within(persistedBubble).getByText("Persisted", { selector: "strong" })).toBeInTheDocument(); - - await userEvent.click(headerToggle); - - expect(within(streamingBubble).getByText(/\*\*Live\*\* stream/)).toBeInTheDocument(); - expect(within(persistedBubble).getByText(/\*\*Persisted\*\*/)).toBeInTheDocument(); - - await userEvent.click(headerToggle); + expect(screen.queryByTestId("chat-thread-render-toggle")).not.toBeInTheDocument(); expect(within(streamingBubble).getByText("Live", { selector: "strong" })).toBeInTheDocument(); expect(within(persistedBubble).getByText("Persisted", { selector: "strong" })).toBeInTheDocument(); }); diff --git a/packages/dashboard/app/components/__tests__/ChatView.mobile.test.tsx b/packages/dashboard/app/components/__tests__/ChatView.mobile.test.tsx index a2eea7ba96..2b12096b17 100644 --- a/packages/dashboard/app/components/__tests__/ChatView.mobile.test.tsx +++ b/packages/dashboard/app/components/__tests__/ChatView.mobile.test.tsx @@ -286,7 +286,6 @@ describe("ChatView mobile behavior", () => { const headerTitle = viewHeader.querySelector(".view-header__title") as HTMLElement; const backButton = screen.getByTestId("chat-back-btn"); const sessionTrigger = screen.getByTestId("chat-mobile-session-trigger"); - const renderToggle = screen.getByTestId("chat-thread-render-toggle"); expect(viewHeader).toContainElement(backButton); expect(viewHeader).toContainElement(sessionTrigger); @@ -301,8 +300,9 @@ describe("ChatView mobile behavior", () => { expect(sessionTrigger).not.toHaveTextContent("MiniMax M3"); expect(within(sessionTrigger).getByTestId("minimax-icon")).toBeInTheDocument(); expect(sessionTrigger.querySelector(".chat-model-tag")).not.toBeInTheDocument(); - expect(viewHeader).not.toContainElement(renderToggle); - expect(renderToggle).toHaveClass("chat-thread-header-render-toggle--floating"); + // FNXC:ChatRenderToggle 2026-07-04-00:00: the floating markdown/plain + // toggle was removed per FN-7541; confirm no such control remains. + expect(screen.queryByTestId("chat-thread-render-toggle")).not.toBeInTheDocument(); expect(screen.getAllByTestId("chat-back-btn")).toHaveLength(1); expect(screen.getAllByTestId("chat-mobile-session-trigger")).toHaveLength(1); expect(screen.getByRole("heading", { name: "Chat" })).toBeInTheDocument(); @@ -343,7 +343,11 @@ describe("ChatView mobile behavior", () => { } }); - it("mobile mode: floating render toggle flips persisted and streaming assistant output", async () => { + // FNXC:ChatRenderToggle 2026-07-04-00:00: the floating markdown/plain + // toggle was removed per FN-7541; mobile chat always renders Markdown + // now (forcePlain={false}), so this regression test asserts the toggle + // is absent and persisted + streaming bubbles both still render Markdown. + it("mobile mode: has no floating render toggle and still renders persisted and streaming assistant output as Markdown", async () => { const restoreMatchMedia = mockMobileViewport(); try { setupMockChat({ @@ -355,19 +359,13 @@ describe("ChatView mobile behavior", () => { await renderWithAct(); - const toggle = screen.getByTestId("chat-thread-render-toggle"); const persistedBubble = screen.getByTestId("chat-message-msg-001"); const streamingBubble = document.querySelector(".chat-message--streaming") as HTMLElement; - expect(toggle).toHaveClass("chat-thread-header-render-toggle--floating"); + expect(screen.queryByTestId("chat-thread-render-toggle")).not.toBeInTheDocument(); expect(screen.queryAllByTestId("chat-message-render-toggle")).toHaveLength(0); expect(within(persistedBubble).getByText("Persisted", { selector: "strong" })).toBeInTheDocument(); expect(within(streamingBubble).getByText("Live", { selector: "strong" })).toBeInTheDocument(); - - await userEvent.click(toggle); - - expect(within(persistedBubble).getByText(/\*\*Persisted\*\* reply/)).toBeInTheDocument(); - expect(within(streamingBubble).getByText(/\*\*Live\*\* reply/)).toBeInTheDocument(); } finally { restoreMatchMedia.mockRestore(); } @@ -2212,7 +2210,6 @@ describe("ChatView mobile CSS contract", () => { const actionsRule = css.match(/\.chat-view--mobile-direct-thread\s*>\s*\.view-header\s+\.view-header__actions\s*\{([^}]*)\}/)?.[1] ?? ""; const menuRule = css.match(/\.chat-view--mobile-direct-thread\s+\.chat-mobile-session-menu\s*\{([^}]*)\}/)?.[1] ?? ""; const triggerRule = css.match(/\.chat-mobile-session-trigger\s*\{([^}]*)\}/)?.[1] ?? ""; - const floatingToggleRule = css.match(/\.chat-thread-header-render-toggle--floating\s*\{([^}]*)\}/)?.[1] ?? ""; expect(headerRule).toContain("flex-wrap: nowrap"); expect(directTitleRule).toContain("position: absolute"); @@ -2227,12 +2224,8 @@ describe("ChatView mobile CSS contract", () => { expect(triggerRule).toContain("min-width: 0"); expect(triggerRule).toContain("gap: var(--space-sm)"); expect(triggerRule).not.toMatch(/#[0-9a-fA-F]{3,8}|rgb\(/); - expect(floatingToggleRule).toContain("position: absolute"); - expect(floatingToggleRule).toContain("right: var(--space-md)"); - expect(floatingToggleRule).toContain("bottom: calc((var(--space-lg) * 2.5) + (var(--space-md) * 2) + var(--space-sm))"); - expect(floatingToggleRule).toContain("min-width: calc(var(--space-lg) * 2.25)"); - expect(floatingToggleRule).toContain("min-height: calc(var(--space-lg) * 2.25)"); - expect(floatingToggleRule).toContain("box-shadow: var(--shadow-md)"); + // FNXC:ChatRenderToggle 2026-07-04-00:00: the floating render-toggle + // button and its CSS were removed per FN-7541; no rule to assert here. }); it("FN-4352: response copy action stays compact on mobile", async () => { diff --git a/packages/i18n/locales/en/app.json b/packages/i18n/locales/en/app.json index 2e621166f0..f6b4d8534d 100644 --- a/packages/i18n/locales/en/app.json +++ b/packages/i18n/locales/en/app.json @@ -1374,8 +1374,6 @@ "sendMessageFailed": "Failed to send message", "sendRoomMessageFailed": "Failed to send room message", "sessionsGroupLabel": "Sessions", - "showPlainText": "Show all messages as plain text", - "showRenderedMarkdown": "Show all messages as rendered Markdown", "skillSuggestions": "Skill suggestions", "startNewChat": "Start a new chat", "startNewConversation": "Start a new conversation", diff --git a/packages/i18n/locales/es/app.json b/packages/i18n/locales/es/app.json index 2eb6efea7d..f7f44ed940 100644 --- a/packages/i18n/locales/es/app.json +++ b/packages/i18n/locales/es/app.json @@ -1362,8 +1362,6 @@ "sendMessageFailed": "Error al enviar el mensaje", "sendRoomMessageFailed": "Error al enviar el mensaje al canal", "sessionsGroupLabel": "Sesiones", - "showPlainText": "Mostrar texto sin formato", - "showRenderedMarkdown": "Mostrar markdown renderizado", "skillSuggestions": "Sugerencias de habilidades", "startNewChat": "Iniciar un nuevo chat", "startNewConversation": "Iniciar una nueva conversación", diff --git a/packages/i18n/locales/fr/app.json b/packages/i18n/locales/fr/app.json index 88c6809298..e689a39d2f 100644 --- a/packages/i18n/locales/fr/app.json +++ b/packages/i18n/locales/fr/app.json @@ -1362,8 +1362,6 @@ "sendMessageFailed": "Échec de l'envoi du message", "sendRoomMessageFailed": "Échec de l'envoi du message dans le canal", "sessionsGroupLabel": "Sessions", - "showPlainText": "Afficher le texte brut", - "showRenderedMarkdown": "Afficher le markdown rendu", "skillSuggestions": "Suggestions de compétences", "startNewChat": "Démarrer une nouvelle conversation", "startNewConversation": "Commencer une nouvelle conversation", diff --git a/packages/i18n/locales/ko/app.json b/packages/i18n/locales/ko/app.json index bd67143a2e..36a8292cd9 100644 --- a/packages/i18n/locales/ko/app.json +++ b/packages/i18n/locales/ko/app.json @@ -1362,8 +1362,6 @@ "sendMessageFailed": "메시지 전송 실패", "sendRoomMessageFailed": "방 메시지 전송 실패", "sessionsGroupLabel": "세션", - "showPlainText": "모든 메시지를 일반 텍스트로 표시", - "showRenderedMarkdown": "모든 메시지를 렌더링된 Markdown으로 표시", "skillSuggestions": "스킬 제안", "startNewChat": "새 채팅 시작", "startNewConversation": "새 대화 시작", diff --git a/packages/i18n/locales/zh-CN/app.json b/packages/i18n/locales/zh-CN/app.json index f2c35f17cd..27354f327d 100644 --- a/packages/i18n/locales/zh-CN/app.json +++ b/packages/i18n/locales/zh-CN/app.json @@ -1362,8 +1362,6 @@ "sendMessageFailed": "发送消息失败", "sendRoomMessageFailed": "发送频道消息失败", "sessionsGroupLabel": "会话", - "showPlainText": "显示纯文本", - "showRenderedMarkdown": "显示渲染后的 Markdown", "skillSuggestions": "技能建议", "startNewChat": "开始新聊天", "startNewConversation": "开始新对话", diff --git a/packages/i18n/locales/zh-TW/app.json b/packages/i18n/locales/zh-TW/app.json index c7c46932fd..11fc22bfd2 100644 --- a/packages/i18n/locales/zh-TW/app.json +++ b/packages/i18n/locales/zh-TW/app.json @@ -1362,8 +1362,6 @@ "sendMessageFailed": "傳送訊息失敗", "sendRoomMessageFailed": "傳送頻道訊息失敗", "sessionsGroupLabel": "工作階段", - "showPlainText": "顯示純文字", - "showRenderedMarkdown": "顯示渲染後的 Markdown", "skillSuggestions": "技能建議", "startNewChat": "開始新聊天", "startNewConversation": "開始新對話", diff --git a/packages/i18n/src/resources.d.ts b/packages/i18n/src/resources.d.ts index 8506205ee5..7449e9e944 100644 --- a/packages/i18n/src/resources.d.ts +++ b/packages/i18n/src/resources.d.ts @@ -1365,8 +1365,6 @@ export default interface Resources { "sendMessageFailed": "Failed to send message", "sendRoomMessageFailed": "Failed to send room message", "sessionsGroupLabel": "Sessions", - "showPlainText": "Show all messages as plain text", - "showRenderedMarkdown": "Show all messages as rendered Markdown", "skillSuggestions": "Skill suggestions", "startNewChat": "Start a new chat", "startNewConversation": "Start a new conversation",