FN-7541: remove chat markdown/plain-text toggle eye icon

Remove the chat header markdown/plain-text render toggle so chat messages always render as Markdown.

- Delete the chat-thread-header-render-toggle button (desktop + mobile) and its eye icon from ChatView.tsx
- Remove the associated showAllAsPlain state and related CSS rules from ChatView.css
- Drop the chat.showRenderedMarkdown/chat.showPlainText i18n keys from all locale files and resources.d.ts
- Update ChatView core/core-interactions/mobile tests to match the removed toggle
- Add a changeset documenting the breaking removal

Files changed:
 .changeset/FN-7541-remove-chat-eye-icon.md         |  7 +++
 packages/dashboard/app/components/ChatView.css     | 60 +---------------------
 packages/dashboard/app/components/ChatView.tsx     | 52 +++++--------------
 .../__tests__/ChatView.core-interactions.test.tsx  |  9 ++--
 .../components/__tests__/ChatView.core.test.tsx    | 35 ++++---------
 .../components/__tests__/ChatView.mobile.test.tsx  | 29 ++++-------
 packages/i18n/locales/en/app.json                  |  2 -
 packages/i18n/locales/es/app.json                  |  2 -
 packages/i18n/locales/fr/app.json                  |  2 -
 packages/i18n/locales/ko/app.json                  |  2 -
 packages/i18n/locales/zh-CN/app.json               |  2 -
 packages/i18n/locales/zh-TW/app.json               |  2 -
 packages/i18n/src/resources.d.ts                   |  2 -
 13 files changed, 46 insertions(+), 160 deletions(-)

Fusion-Task-Id: FN-7541

Fusion-Task-Lineage: 8eac79af-80b0-4ae0-ab86-c3599dd3612c

Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
This commit is contained in:
gsxdsm
2026-07-04 19:20:30 -07:00
parent bf688394a2
commit 635fca2409
13 changed files with 46 additions and 160 deletions

View File

@@ -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).

View File

@@ -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;
}

View File

@@ -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<PendingAttachment[]>([]);
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
<StandardChatMessageItem
key={message.id}
message={message}
forcePlain={showAllAsPlain}
forcePlain={false}
agentName={agentName}
hideAssistantIdentity={hideAssistantIdentity}
showAssistantModelTag={showAssistantModelTag}
@@ -2356,7 +2347,7 @@ export function ChatView({ projectId, addToast, floating = false, compactLayout
streamingText={streamingText}
streamingThinking={streamingThinking}
streamingToolCalls={streamingToolCalls}
forcePlain={showAllAsPlain}
forcePlain={false}
agentName={agentName}
hideAssistantIdentity={hideAssistantIdentity}
showAssistantModelTag={showAssistantModelTag}
@@ -2378,7 +2369,7 @@ export function ChatView({ projectId, addToast, floating = false, compactLayout
<StandardChatMessageItem
key={message.id}
message={message}
forcePlain={showAllAsPlain}
forcePlain={false}
agentName={agentName}
hideAssistantIdentity={hideAssistantIdentity}
showAssistantModelTag={showAssistantModelTag}
@@ -3234,7 +3225,7 @@ export function ChatView({ projectId, addToast, floating = false, compactLayout
<StandardChatMessageItem
key={message.id}
message={roomMessage}
forcePlain={showAllAsPlain}
forcePlain={false}
agentName={senderName}
hideAssistantIdentity={false}
showAssistantModelTag={false}
@@ -3377,6 +3368,10 @@ export function ChatView({ projectId, addToast, floating = false, compactLayout
) : (
<div ref={chatThreadRef} className="chat-thread">
{/* 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) && (
<div className="chat-thread-header">
<div className="chat-thread-header-identity" data-testid="chat-thread-header-identity">
@@ -3394,32 +3389,9 @@ export function ChatView({ projectId, addToast, floating = false, compactLayout
</span>
) : null}
</div>
{hasThreadInView && (
<button
type="button"
className={`chat-thread-header-render-toggle${showAllAsPlain ? " chat-thread-header-render-toggle--plain" : ""}`}
data-testid="chat-thread-render-toggle"
aria-label={showAllAsPlain ? t("chat.showRenderedMarkdown", "Show all messages as rendered Markdown") : t("chat.showPlainText", "Show all messages as plain text")}
onClick={toggleAllAsPlain}
>
{showAllAsPlain ? <EyeOff size={14} /> : <Eye size={14} />}
</button>
)}
</div>
)}
{isChatMobile && hasThreadInView && (
<button
type="button"
className={`chat-thread-header-render-toggle chat-thread-header-render-toggle--floating${showAllAsPlain ? " chat-thread-header-render-toggle--plain" : ""}`}
data-testid="chat-thread-render-toggle"
aria-label={showAllAsPlain ? t("chat.showRenderedMarkdown", "Show all messages as rendered Markdown") : t("chat.showPlainText", "Show all messages as plain text")}
onClick={toggleAllAsPlain}
>
{showAllAsPlain ? <EyeOff size={14} /> : <Eye size={14} />}
</button>
)}
{/* Messages + composer. CLI-backed chat sessions delegate this
region to <CliChatSurface> (transcript/raw-terminal toggle +
queued composer); generic-tier adapters render terminal-only. */}

View File

@@ -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);
});

View File

@@ -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();
});

View File

@@ -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(<ChatView projectId="proj-123" addToast={vi.fn()} />);
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 () => {

View File

@@ -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",

View File

@@ -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",

View File

@@ -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",

View File

@@ -1362,8 +1362,6 @@
"sendMessageFailed": "메시지 전송 실패",
"sendRoomMessageFailed": "방 메시지 전송 실패",
"sessionsGroupLabel": "세션",
"showPlainText": "모든 메시지를 일반 텍스트로 표시",
"showRenderedMarkdown": "모든 메시지를 렌더링된 Markdown으로 표시",
"skillSuggestions": "스킬 제안",
"startNewChat": "새 채팅 시작",
"startNewConversation": "새 대화 시작",

View File

@@ -1362,8 +1362,6 @@
"sendMessageFailed": "发送消息失败",
"sendRoomMessageFailed": "发送频道消息失败",
"sessionsGroupLabel": "会话",
"showPlainText": "显示纯文本",
"showRenderedMarkdown": "显示渲染后的 Markdown",
"skillSuggestions": "技能建议",
"startNewChat": "开始新聊天",
"startNewConversation": "开始新对话",

View File

@@ -1362,8 +1362,6 @@
"sendMessageFailed": "傳送訊息失敗",
"sendRoomMessageFailed": "傳送頻道訊息失敗",
"sessionsGroupLabel": "工作階段",
"showPlainText": "顯示純文字",
"showRenderedMarkdown": "顯示渲染後的 Markdown",
"skillSuggestions": "技能建議",
"startNewChat": "開始新聊天",
"startNewConversation": "開始新對話",

View File

@@ -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",