diff --git a/packages/types/src/global-settings.ts b/packages/types/src/global-settings.ts index 692798d00d..44d1c6fb00 100644 --- a/packages/types/src/global-settings.ts +++ b/packages/types/src/global-settings.ts @@ -264,6 +264,19 @@ export const globalSettingsSchema = z.object({ * @default "send" */ enterBehavior: z.enum(["send", "newline"]).optional(), + /** + * Controls the border effect on the chat input box while AI is working. + * - "marquee": A conic-gradient light beam orbits the ring (default) + * - "breathing": A pulsing glow + 1px blue border ring + * @default "marquee" + */ + chatInputEffect: z.enum(["marquee", "breathing"]).optional(), + /** + * Opt-in zebra striping (alternating background colors) for rows in + * markdown tables rendered in the chat. + * @default false + */ + tableStriped: z.boolean().optional(), profileThresholds: z.record(z.string(), z.number()).optional(), hasOpenedModeSelector: z.boolean().optional(), lastModeExportPath: z.string().optional(), diff --git a/packages/types/src/vscode-extension-host.ts b/packages/types/src/vscode-extension-host.ts index 5f6b579779..5dc28c0767 100644 --- a/packages/types/src/vscode-extension-host.ts +++ b/packages/types/src/vscode-extension-host.ts @@ -324,6 +324,8 @@ export type ExtensionState = Pick< | "reasoningBlockCollapsed" | "chatFontSize" | "enterBehavior" + | "chatInputEffect" + | "tableStriped" | "includeCurrentTime" | "includeCurrentCost" | "maxGitStatusFiles" diff --git a/src/core/webview/ClineProvider.ts b/src/core/webview/ClineProvider.ts index 34848ab860..b6d0d810ca 100644 --- a/src/core/webview/ClineProvider.ts +++ b/src/core/webview/ClineProvider.ts @@ -2612,6 +2612,8 @@ export class ClineProvider reasoningBlockCollapsed, chatFontSize, enterBehavior, + chatInputEffect, + tableStriped, cloudUserInfo, cloudIsAuthenticated, sharingEnabled, @@ -2790,6 +2792,8 @@ export class ClineProvider reasoningBlockCollapsed: reasoningBlockCollapsed ?? true, chatFontSize, enterBehavior: enterBehavior ?? "send", + chatInputEffect: chatInputEffect ?? "marquee", + tableStriped: tableStriped ?? false, cloudUserInfo, cloudIsAuthenticated: cloudIsAuthenticated ?? false, cloudAuthSkipModel: this.context.globalState.get("roo-auth-skip-model") ?? false, @@ -3019,6 +3023,8 @@ export class ClineProvider reasoningBlockCollapsed: stateValues.reasoningBlockCollapsed ?? true, chatFontSize: stateValues.chatFontSize, enterBehavior: stateValues.enterBehavior ?? "send", + chatInputEffect: stateValues.chatInputEffect ?? "marquee", + tableStriped: stateValues.tableStriped ?? false, cloudUserInfo, cloudIsAuthenticated, sharingEnabled, diff --git a/src/core/webview/__tests__/ClineProvider.spec.ts b/src/core/webview/__tests__/ClineProvider.spec.ts index f76d0944a7..a9b51a2432 100644 --- a/src/core/webview/__tests__/ClineProvider.spec.ts +++ b/src/core/webview/__tests__/ClineProvider.spec.ts @@ -1650,6 +1650,54 @@ describe("ClineProvider", () => { expect(state.destructiveCommandGuardEnabled).toBe(false) }) + describe("chat input effect and table striping settings", () => { + it("defaults to marquee and false when unset in getState and getStateToPostToWebview", async () => { + await provider.resolveWebviewView(mockWebviewView) + + const state = await provider.getState() + const postedState = await provider.getStateToPostToWebview() + + expect(state.chatInputEffect).toBe("marquee") + expect(state.tableStriped).toBe(false) + expect(postedState.chatInputEffect).toBe("marquee") + expect(postedState.tableStriped).toBe(false) + }) + + it("preserves an explicit marquee chat input effect", async () => { + await provider.resolveWebviewView(mockWebviewView) + await provider.contextProxy.setValue("chatInputEffect", "marquee") + + const state = await provider.getState() + const postedState = await provider.getStateToPostToWebview() + + expect(state.chatInputEffect).toBe("marquee") + expect(postedState.chatInputEffect).toBe("marquee") + }) + + it("preserves a breathing chat input effect", async () => { + await provider.resolveWebviewView(mockWebviewView) + await provider.contextProxy.setValue("chatInputEffect", "breathing") + + const state = await provider.getState() + const postedState = await provider.getStateToPostToWebview() + + expect(state.chatInputEffect).toBe("breathing") + expect(postedState.chatInputEffect).toBe("breathing") + }) + + it("preserves explicit true and false table striping", async () => { + await provider.resolveWebviewView(mockWebviewView) + + await provider.contextProxy.setValue("tableStriped", true) + expect((await provider.getState()).tableStriped).toBe(true) + expect((await provider.getStateToPostToWebview()).tableStriped).toBe(true) + + await provider.contextProxy.setValue("tableStriped", false) + expect((await provider.getState()).tableStriped).toBe(false) + expect((await provider.getStateToPostToWebview()).tableStriped).toBe(false) + }) + }) + test("language is set to VSCode language", async () => { // Mock VSCode language as Spanish ;(vscode.env as any).language = "pt-BR" diff --git a/webview-ui/src/components/chat/ApiConfigSelector.tsx b/webview-ui/src/components/chat/ApiConfigSelector.tsx index ef5151a00d..b7aad4ab57 100644 --- a/webview-ui/src/components/chat/ApiConfigSelector.tsx +++ b/webview-ui/src/components/chat/ApiConfigSelector.tsx @@ -97,14 +97,26 @@ export const ApiConfigSelector = ({ return (
handleSelect(config.id)} className={cn( - "px-3 py-1.5 text-sm cursor-pointer flex items-center group", - "hover:bg-vscode-list-hoverBackground", + "px-3 py-1.5 text-sm flex items-center group", isCurrentConfig && "bg-vscode-list-activeSelectionBackground text-vscode-list-activeSelectionForeground", )}> -
+
+
{isCurrentConfig && (
@@ -180,8 +192,10 @@ export const ApiConfigSelector = ({ /> {searchValue.length > 0 && (
- setSearchValue("")} />
@@ -203,18 +217,18 @@ export const ApiConfigSelector = ({ {/* Pinned configs - sticky header */} {pinnedConfigs.length > 0 && (
0 && "border-b border-vscode-dropdown-foreground/10", - )} - aria-label="Pinned configurations"> + )}> {pinnedConfigs.map((config) => renderConfigItem(config, true))}
)} {/* Unpinned configs */} {unpinnedConfigs.length > 0 && ( -
+
{unpinnedConfigs.map((config) => renderConfigItem(config, false))}
)} diff --git a/webview-ui/src/components/chat/ChatRow.tsx b/webview-ui/src/components/chat/ChatRow.tsx index 952322084f..3fc4ac062d 100644 --- a/webview-ui/src/components/chat/ChatRow.tsx +++ b/webview-ui/src/components/chat/ChatRow.tsx @@ -58,7 +58,6 @@ import { Eye, FileDiff, ListTree, - User, Edit, Trash2, MessageCircleQuestionMark, @@ -67,7 +66,6 @@ import { PocketKnife, FolderTree, SquareTerminal, - MessageCircle, Repeat2, Split, ArrowRight, @@ -1208,13 +1206,10 @@ export const ChatRowContent = ({ case "text": return (
-
- - {t("chat:text.rooSaid")} -
- -
-
+
+
+ +
{message.images && message.images.length > 0 && (
@@ -1228,17 +1223,13 @@ export const ChatRowContent = ({ ) case "user_feedback": return ( -
-
- - {t("chat:feedback.youSaid")} -
+
{isEditing ? (
@@ -1261,50 +1252,56 @@ export const ChatRowContent = ({ />
) : ( -
-
{ - e.stopPropagation() - if (!isStreaming) { - handleEditClick() - } - }} - title={t("chat:queuedMessages.clickToEdit")}> - -
-
-
{ - e.stopPropagation() - handleEditClick() - }}> - -
-
{ - e.stopPropagation() - vscode.postMessage({ type: "deleteMessage", value: message.ts }) - }}> - -
-
+
{ + e.stopPropagation() + if (!isStreaming) { + handleEditClick() + } + }} + title={t("chat:queuedMessages.clickToEdit")}> +
)} {!isEditing && message.images && message.images.length > 0 && ( )}
+ {!isEditing && ( +
+ + +
+ )}
) case "user_feedback_diff": const tool = safeJsonParse(message.text) return ( -
+
( clineMessages, commands, enterBehavior, + chatInputEffect, lockApiConfigAcrossModes, } = useExtensionState() @@ -1028,6 +1029,36 @@ export const ChatTextArea = forwardRef( "overflow-hidden", "rounded-lg", )}> + {/* Streaming border effect — marquee (conic-gradient light beam) or breathing (pulsing glow + 1px ring). + Selected via Settings → UI → chatInputEffect. Only shown while AI is executing. */} + {!isDraggingOver && isStreaming && (chatInputEffect ?? "marquee") === "marquee" && ( +
+
+
+ )} + {/* Breathing border effect — pulsing glow + 1px blue ring. No overflow-hidden needed (box-shadow would be clipped). */} + {!isDraggingOver && isStreaming && chatInputEffect === "breathing" && ( +
+
+
+
+ )}
( ? "border border-vscode-focusBorder outline outline-vscode-focusBorder" : isDraggingOver ? "border-2 border-dashed border-vscode-focusBorder" - : "border border-transparent", + : "border border-vscode-input-border", "pl-2", "py-2", isEditMode ? "pr-20" : "pr-9", @@ -1110,7 +1141,7 @@ export const ChatTextArea = forwardRef( ? "border border-vscode-focusBorder outline outline-vscode-focusBorder" : isDraggingOver ? "border-2 border-dashed border-vscode-focusBorder" - : "border border-transparent", + : "border border-vscode-input-border", isDraggingOver ? "bg-[color-mix(in_srgb,var(--vscode-input-background)_95%,var(--vscode-focusBorder))]" : "bg-vscode-input-background", diff --git a/webview-ui/src/components/chat/Markdown.tsx b/webview-ui/src/components/chat/Markdown.tsx index 87780d5df8..5edf0433e1 100644 --- a/webview-ui/src/components/chat/Markdown.tsx +++ b/webview-ui/src/components/chat/Markdown.tsx @@ -2,11 +2,13 @@ import { memo, useState } from "react" import { VSCodeButton } from "@vscode/webview-ui-toolkit/react" import { useCopyToClipboard } from "@src/utils/clipboard" +import { useExtensionState } from "@src/context/ExtensionStateContext" import { StandardTooltip } from "@src/components/ui" import MarkdownBlock from "../common/MarkdownBlock" export const Markdown = memo(({ markdown, partial }: { markdown?: string; partial?: boolean }) => { + const { tableStriped } = useExtensionState() const [isHovering, setIsHovering] = useState(false) // Shorter feedback duration for copy button flash. @@ -22,7 +24,7 @@ export const Markdown = memo(({ markdown, partial }: { markdown?: string; partia onMouseLeave={() => setIsHovering(false)} style={{ position: "relative" }}>
- +
{markdown && !partial && isHovering && (
{ const { t } = useTranslation() - const { reasoningBlockCollapsed } = useExtensionState() + const { reasoningBlockCollapsed, tableStriped } = useExtensionState() const [isCollapsed, setIsCollapsed] = useState(reasoningBlockCollapsed) @@ -51,7 +51,13 @@ export const ReasoningBlock = ({ content, isStreaming, isLast }: ReasoningBlockP onClick={handleToggle}>
- {t("chat:reasoning.thinking")} + + {t("chat:reasoning.thinking")} + {elapsed > 0 && ( {secondsLabel} )} @@ -69,7 +75,7 @@ export const ReasoningBlock = ({ content, isStreaming, isLast }: ReasoningBlockP
- +
)}
diff --git a/webview-ui/src/components/chat/__tests__/ApiConfigSelector.spec.tsx b/webview-ui/src/components/chat/__tests__/ApiConfigSelector.spec.tsx index 420249b365..0512657616 100644 --- a/webview-ui/src/components/chat/__tests__/ApiConfigSelector.spec.tsx +++ b/webview-ui/src/components/chat/__tests__/ApiConfigSelector.spec.tsx @@ -1,4 +1,4 @@ -import { render, screen, fireEvent, waitFor } from "@/utils/test-utils" +import { render, screen, fireEvent, waitFor, within } from "@/utils/test-utils" import { vscode } from "@/utils/vscode" import { providerIdentifiers } from "@roo-code/types" @@ -237,11 +237,10 @@ describe("ApiConfigSelector", () => { expect(searchInput.value).toBe("test") - // Find and click the X button - const clearButton = screen.getByTestId("popover-content").querySelector(".cursor-pointer") - if (clearButton) { - fireEvent.click(clearButton) - } + // Find and click the clear (X) button, which is now a semantic button + const clearButton = screen.getByLabelText("common:ui.clear_search") + expect(clearButton.tagName).toBe("BUTTON") + fireEvent.click(clearButton) await waitFor(() => { expect(searchInput.value).toBe("") @@ -260,6 +259,39 @@ describe("ApiConfigSelector", () => { expect(mockOnChange).toHaveBeenCalledWith("config2") }) + test("renders each config with a dedicated, keyboard-operable selection button", () => { + render() + fireEvent.click(screen.getByTestId("dropdown-trigger")) + + const content = screen.getByTestId("popover-content") + + // Selection is a dedicated, keyboard-focusable button (not a bare div/span). + const selectedButton = within(content).getByText("Config 1").closest("button") as HTMLButtonElement + expect(selectedButton).toBeTruthy() + expect(selectedButton.tagName).toBe("BUTTON") + // The selected state is announced on the control. + expect(selectedButton.getAttribute("aria-current")).toBe("true") + + const otherButton = within(content).getByText("Config 2").closest("button") as HTMLButtonElement + expect(otherButton).toBeTruthy() + expect(otherButton.getAttribute("aria-current")).toBeNull() + }) + + test("selects a config with the keyboard", () => { + render() + fireEvent.click(screen.getByTestId("dropdown-trigger")) + + const content = screen.getByTestId("popover-content") + const selectionButton = within(content).getByText("Config 2").closest("button") as HTMLButtonElement + expect(selectionButton).toBeTruthy() + + // The selection control is keyboard-focusable (native button tab order). + expect(selectionButton.tabIndex).toBe(0) + + fireEvent.keyDown(selectionButton, { key: "Enter" }) + expect(mockOnChange).toHaveBeenCalledWith("config2") + }) + test("shows check mark for selected config", () => { render() @@ -428,9 +460,11 @@ describe("ApiConfigSelector", () => { const searchInput = screen.getByPlaceholderText("common:ui.search_placeholder") as HTMLInputElement fireEvent.change(searchInput, { target: { value: "Config" } }) - // Pin a config - const config2Row = screen.getByText("Config 2").closest("div") - const pinButton = config2Row?.querySelector("button") + // Pin a config (target the pin button, not the selection button) + const config2Row = screen.getByText("Config 2").closest('[role="group"]') as HTMLElement | null + const pinButton = Array.from(config2Row?.querySelectorAll("button") ?? []).find((btn) => + btn.querySelector(".codicon-pin"), + ) if (pinButton) { fireEvent.click(pinButton) } @@ -471,7 +505,6 @@ describe("ApiConfigSelector", () => { // Check for pinned configs sticky header const pinnedStickyHeader = scrollContainer?.querySelector(".sticky.top-0.z-10.bg-vscode-dropdown-background") expect(pinnedStickyHeader).toBeInTheDocument() - expect(pinnedStickyHeader).toHaveAttribute("aria-label", "Pinned configurations") // Check for Config 1, 2, 3 being visible in the sticky header (pinned) expect(screen.getAllByText("Config 1").length).toBeGreaterThan(0) @@ -491,7 +524,7 @@ describe("ApiConfigSelector", () => { } // Check for unpinned configs section - const unpinnedSection = scrollContainer?.querySelector('[aria-label="All configurations"]') + const unpinnedSection = scrollContainer?.querySelector('[data-testid="unpinned-configs"]') expect(unpinnedSection).toBeInTheDocument() // Verify separator exists as border on pinned section when unpinned configs exist @@ -527,7 +560,7 @@ describe("ApiConfigSelector", () => { expect(pinnedSection).not.toBeInTheDocument() // Should have unpinned configs section with all configs - const unpinnedSection = scrollContainer?.querySelector('[aria-label="All configurations"]') + const unpinnedSection = scrollContainer?.querySelector('[data-testid="unpinned-configs"]') expect(unpinnedSection).toBeInTheDocument() // All configs should be in the unpinned section diff --git a/webview-ui/src/components/chat/__tests__/ChatRow.user-feedback-bubble.spec.tsx b/webview-ui/src/components/chat/__tests__/ChatRow.user-feedback-bubble.spec.tsx new file mode 100644 index 0000000000..01876ecebb --- /dev/null +++ b/webview-ui/src/components/chat/__tests__/ChatRow.user-feedback-bubble.spec.tsx @@ -0,0 +1,197 @@ +import React from "react" +import { fireEvent, renderWithExtensionState, screen } from "@/utils/test-utils" +import type { ClineMessage } from "@roo-code/types" +import { ChatRowContent } from "../ChatRow" + +const mockPostMessage = vi.fn() + +vi.mock("@src/utils/vscode", () => ({ + vscode: { + postMessage: (...args: unknown[]) => mockPostMessage(...args), + }, +})) + +// Mock i18n +vi.mock("react-i18next", () => ({ + useTranslation: () => ({ + t: (key: string) => key, + }), + Trans: ({ children }: { children?: React.ReactNode }) => <>{children}, + initReactI18next: { type: "3rdParty", init: () => {} }, +})) + +// Mock CodeBlock (avoid ESM/highlighter costs) +vi.mock("@src/components/common/CodeBlock", () => ({ + default: () => null, +})) + +// Mock useSelectedModel so the hook has a stable default +vi.mock("@src/components/ui/hooks/useSelectedModel", () => ({ + useSelectedModel: () => ({ info: { supportsImages: true } }), +})) + +const makeUserFeedback = (): ClineMessage => + ({ ts: 1, type: "say", say: "user_feedback", text: "hello bubble" }) as ClineMessage + +function renderRow(message: ClineMessage, isStreaming = false) { + return renderWithExtensionState( + {}} + onSuggestionClick={() => {}} + onBatchFileResponse={() => {}} + onFollowUpUnmount={() => {}} + isFollowUpAnswered={false} + />, + ) +} + +describe("ChatRow - user feedback bubble layout & contrast", () => { + beforeEach(() => { + vi.clearAllMocks() + mockPostMessage.mockClear() + }) + + it("lays the user feedback row out as a right-aligned bubble", () => { + const { container } = renderRow(makeUserFeedback()) + + // The wrapper is the bubble container + const bubbleContainer = container.querySelector(".ml-auto") as HTMLElement | null + expect(bubbleContainer).toBeTruthy() + expect(bubbleContainer!.className).toContain("w-fit") + expect(bubbleContainer!.className).toContain("max-w-[70%]") + expect(bubbleContainer!.className).toContain("items-end") + expect(bubbleContainer!.className).toContain("flex-col") + }) + + it("uses a soft themed background instead of the inverted foreground color", () => { + const { container } = renderRow(makeUserFeedback()) + const bubble = container.querySelector(".cursor-text") as HTMLElement | null + + expect(bubble).toBeTruthy() + expect(bubble!.className).toContain("bg-vscode-list-hoverBackground") + expect(bubble!.className).toContain("text-vscode-foreground") + // The previous implementation inverted foreground/background, causing harsh contrast. + expect(bubble!.className).not.toContain("bg-vscode-editor-foreground/70") + expect(bubble!.className).not.toContain("text-vscode-editor-background") + }) + + it("still renders the message text", () => { + const { container } = renderRow(makeUserFeedback()) + const bubble = container.querySelector(".cursor-text") as HTMLElement | null + + expect(bubble).toBeTruthy() + expect(bubble!.textContent).toContain("hello bubble") + }) + + it("places the action bar as the bubble's immediate next sibling, outside the bubble", () => { + const { container } = renderRow(makeUserFeedback()) + + const bubble = container.querySelector(".cursor-text") as HTMLElement | null + const actionBar = Array.from(container.querySelectorAll("div")).find( + (el) => el.className.includes("flex") && el.className.includes("gap-2") && el.className.includes("pr-1"), + ) as HTMLElement | undefined + + expect(bubble).toBeTruthy() + expect(actionBar).toBeTruthy() + + // The action bar must share the bubble's parent and immediately follow it. + expect(actionBar!.parentElement).toBe(bubble!.parentElement) + expect(bubble!.nextElementSibling).toBe(actionBar) + + expect(actionBar!.querySelector('[aria-label="chat:edit"]')).toBeTruthy() + expect(actionBar!.querySelector('[aria-label="common:confirmation.deleteMessage"]')).toBeTruthy() + + // The bubble must NOT contain the edit/delete controls + expect(bubble!.querySelector('[aria-label="chat:edit"]')).toBeFalsy() + expect(bubble!.querySelector('[aria-label="common:confirmation.deleteMessage"]')).toBeFalsy() + }) + + it("uses the editor background for the bubble while editing", () => { + const { container } = renderRow(makeUserFeedback()) + + // Enter edit mode by clicking the message text (the clickable inner bubble) + const bubble = container.querySelector('[title="chat:queuedMessages.clickToEdit"]') as HTMLElement | null + expect(bubble).toBeTruthy() + fireEvent.click(bubble!) + + // In edit mode the bubble switches to editor background/foreground and + // no longer uses the soft list-hover treatment. + const editBubble = container.querySelector(".border.rounded-sm") as HTMLElement | null + expect(editBubble).toBeTruthy() + expect(editBubble!.className).toContain("bg-vscode-editor-background") + expect(editBubble!.className).toContain("text-vscode-editor-foreground") + expect(editBubble!.className).not.toContain("bg-vscode-list-hoverBackground") + }) + + it("renders the feedback bubble without a header label", () => { + renderRow(makeUserFeedback()) + // The header label ("you said") must not be rendered for user feedback + expect(screen.queryByText("chat:feedback.youSaid")).not.toBeInTheDocument() + expect(screen.queryByLabelText("User icon")).not.toBeInTheDocument() + }) + + it("renders the edit/delete actions as keyboard-focusable buttons", () => { + const { container } = renderRow(makeUserFeedback()) + + const editButton = container.querySelector('[aria-label="chat:edit"]') as HTMLButtonElement | null + const deleteButton = container.querySelector( + '[aria-label="common:confirmation.deleteMessage"]', + ) as HTMLButtonElement | null + + expect(editButton?.tagName).toBe("BUTTON") + expect(deleteButton?.tagName).toBe("BUTTON") + expect(editButton?.getAttribute("type")).toBe("button") + expect(deleteButton?.getAttribute("type")).toBe("button") + }) + + it("enters edit mode when the edit action is clicked", () => { + const { container } = renderRow(makeUserFeedback()) + + const editButton = container.querySelector('[aria-label="chat:edit"]') as HTMLElement | null + expect(editButton).toBeTruthy() + fireEvent.click(editButton!) + + // Clicking edit enters inline edit mode for the feedback message + const textarea = container.querySelector("textarea") as HTMLTextAreaElement | null + expect(textarea).toBeTruthy() + }) + + it("deletes the message when the delete action is clicked", () => { + mockPostMessage.mockClear() + const { container } = renderRow(makeUserFeedback()) + + const deleteButton = container.querySelector( + '[aria-label="common:confirmation.deleteMessage"]', + ) as HTMLElement | null + expect(deleteButton).toBeTruthy() + fireEvent.click(deleteButton!) + + expect(mockPostMessage).toHaveBeenCalledWith({ type: "deleteMessage", value: 1 }) + }) + + it("disables the edit/delete actions while streaming and sends no message when activated", () => { + const { container } = renderRow(makeUserFeedback(), true) + + const editButton = container.querySelector('[aria-label="chat:edit"]') as HTMLButtonElement | null + const deleteButton = container.querySelector( + '[aria-label="common:confirmation.deleteMessage"]', + ) as HTMLButtonElement | null + + expect(editButton).toBeTruthy() + expect(deleteButton).toBeTruthy() + expect(editButton!.disabled).toBe(true) + expect(deleteButton!.disabled).toBe(true) + + mockPostMessage.mockClear() + fireEvent.click(editButton!) + fireEvent.click(deleteButton!) + + // No edit mode entered and no delete message posted while streaming. + expect(container.querySelector("textarea")).not.toBeInTheDocument() + expect(mockPostMessage).not.toHaveBeenCalled() + }) +}) diff --git a/webview-ui/src/components/chat/__tests__/ChatTextArea.spec.tsx b/webview-ui/src/components/chat/__tests__/ChatTextArea.spec.tsx index aae9ac3cc6..dba24b4644 100644 --- a/webview-ui/src/components/chat/__tests__/ChatTextArea.spec.tsx +++ b/webview-ui/src/components/chat/__tests__/ChatTextArea.spec.tsx @@ -1497,4 +1497,100 @@ describe("ChatTextArea", () => { expect(defaultProps.setInputValue).toHaveBeenCalledWith("abc/some/path def") }) }) + + describe("streaming border animation", () => { + it("renders marquee border overlay only when AI is streaming", () => { + const { container } = render() + + const borderDiv = container.querySelector('[data-testid="streaming-border"]') + expect(borderDiv).toBeInTheDocument() + // Marquee effect renders a border-spin ring + expect(borderDiv!.querySelector('[class*="border-spin"]')).toBeInTheDocument() + }) + + it("does not render border overlay when AI is not streaming", () => { + const { container } = render() + + const borderDiv = container.querySelector('[data-testid="streaming-border"]') + expect(borderDiv).not.toBeInTheDocument() + }) + + it("renders marquee border overlay while streaming even when focused", () => { + const { container } = render() + + const textarea = container.querySelector("textarea")! + fireEvent.focus(textarea) + + const borderDiv = container.querySelector('[data-testid="streaming-border"]') + expect(borderDiv).toBeInTheDocument() + }) + + it("renders breathing border overlay when chatInputEffect is breathing", () => { + ;(useExtensionState as ReturnType).mockReturnValue({ + filePaths: [], + openedTabs: [], + apiConfiguration: { + apiProvider: providerIdentifiers.anthropic, + }, + taskHistory: [], + cwd: "/test/workspace", + chatInputEffect: "breathing", + }) + const { container } = render() + + const borderDiv = container.querySelector('[data-testid="streaming-border"]') + expect(borderDiv).toBeInTheDocument() + // Breathing effect renders streaming-glow + border-breathe classes + expect(borderDiv!.querySelector('[class*="streaming-glow"]')).toBeInTheDocument() + expect(borderDiv!.querySelector('[class*="border-breathe"]')).toBeInTheDocument() + }) + + it("does not render breathing border when chatInputEffect is marquee", () => { + ;(useExtensionState as ReturnType).mockReturnValue({ + filePaths: [], + openedTabs: [], + apiConfiguration: { + apiProvider: providerIdentifiers.anthropic, + }, + taskHistory: [], + cwd: "/test/workspace", + chatInputEffect: "marquee", + }) + const { container } = render() + + const borderDiv = container.querySelector('[data-testid="streaming-border"]') + expect(borderDiv).toBeInTheDocument() + // Marquee effect should NOT have breathing classes + expect(borderDiv!.querySelector('[class*="streaming-glow"]')).not.toBeInTheDocument() + expect(borderDiv!.querySelector('[class*="border-breathe"]')).not.toBeInTheDocument() + }) + + it("applies reduced-motion fallbacks to the streaming animation layers", () => { + const { container } = render() + + const borderDiv = container.querySelector('[data-testid="streaming-border"]') + expect(borderDiv).toBeInTheDocument() + const spinLayer = borderDiv!.querySelector('[class*="border-spin"]') as HTMLElement | null + expect(spinLayer).toBeTruthy() + expect(spinLayer!.className).toContain("motion-reduce:animate-none") + }) + }) + + describe("input border state", () => { + it("keeps a shallow border on the textarea when it is not focused (not transparent)", () => { + const { container } = render() + + const textarea = container.querySelector("textarea")! + expect(textarea).toHaveClass("border-vscode-input-border") + expect(textarea).not.toHaveClass("border-transparent") + }) + + it("shows the focus border on the textarea when it is focused", () => { + const { container } = render() + + const textarea = container.querySelector("textarea")! + fireEvent.focus(textarea) + expect(textarea).toHaveClass("border-vscode-focusBorder") + }) + }) }) diff --git a/webview-ui/src/components/chat/__tests__/Markdown.spec.tsx b/webview-ui/src/components/chat/__tests__/Markdown.spec.tsx new file mode 100644 index 0000000000..c5447ab832 --- /dev/null +++ b/webview-ui/src/components/chat/__tests__/Markdown.spec.tsx @@ -0,0 +1,53 @@ +import React from "react" +import { fireEvent, renderWithExtensionState, screen } from "@/utils/test-utils" +import { Markdown } from "../Markdown" + +const mockMarkdownBlock = vi.fn() + +vi.mock("@src/components/common/MarkdownBlock", () => ({ + default: (props: { markdown?: string; striped?: boolean }) => { + mockMarkdownBlock(props) + return
{props.markdown}
+ }, +})) + +// Mock i18n +vi.mock("react-i18next", () => ({ + useTranslation: () => ({ + t: (key: string) => key, + }), + Trans: ({ children }: { children?: React.ReactNode }) => <>{children}, + initReactI18next: { type: "3rdParty", init: () => {} }, +})) + +describe("Markdown", () => { + beforeEach(() => { + mockMarkdownBlock.mockClear() + }) + + it("renders null when no markdown is provided", () => { + const { container } = renderWithExtensionState() + expect(container.firstChild).toBeNull() + }) + + it("passes the tableStriped setting down to MarkdownBlock", () => { + renderWithExtensionState(, { + state: { tableStriped: true } as never, + }) + expect(screen.getByText("hello")).toBeInTheDocument() + expect(mockMarkdownBlock).toHaveBeenCalledWith(expect.objectContaining({ striped: true })) + }) + + it("defaults striped to false when tableStriped is unset", () => { + renderWithExtensionState() + expect(mockMarkdownBlock).toHaveBeenCalledWith(expect.objectContaining({ striped: false })) + }) + + it("shows the copy button on hover", () => { + renderWithExtensionState() + const container = screen.getByText("hello").closest("div") as HTMLElement + fireEvent.mouseEnter(container) + + expect(screen.getByRole("button")).toBeTruthy() + }) +}) diff --git a/webview-ui/src/components/chat/__tests__/ReasoningBlock.spec.tsx b/webview-ui/src/components/chat/__tests__/ReasoningBlock.spec.tsx new file mode 100644 index 0000000000..4e7a801157 --- /dev/null +++ b/webview-ui/src/components/chat/__tests__/ReasoningBlock.spec.tsx @@ -0,0 +1,68 @@ +import React from "react" +import { render, screen } from "@testing-library/react" + +import { ReasoningBlock } from "../ReasoningBlock" + +// Keep the markdown renderer out of the way; this suite focuses on the +// "Thinking..." label and its shimmer state. +vi.mock("../../common/MarkdownBlock", () => ({ + default: ({ markdown }: { markdown: string }) =>
{markdown}
, +})) + +vi.mock("react-i18next", () => ({ + useTranslation: () => ({ + t: (key: string, options?: { count?: number }) => { + if (key === "chat:reasoning.thinking") { + return "Thinking..." + } + if (key === "chat:reasoning.seconds" && options?.count !== undefined) { + return `${options.count}s` + } + return key + }, + }), +})) + +vi.mock("@src/context/ExtensionStateContext", () => ({ + useExtensionState: () => ({ + reasoningBlockCollapsed: false, + }), +})) + +const renderReasoningBlock = (props: Partial> = {}) => { + return render( + , + ) +} + +describe("ReasoningBlock thinking shimmer", () => { + it("applies the shimmer animation to the latest streaming block", () => { + renderReasoningBlock({ isStreaming: true, isLast: true }) + + const label = screen.getByText("Thinking...") + expect(label).toHaveClass("animate-thinking-shine") + expect(label).not.toHaveClass("text-vscode-foreground") + }) + + it("keeps historical reasoning blocks static while another block is streaming", () => { + renderReasoningBlock({ isStreaming: true, isLast: false }) + + const label = screen.getByText("Thinking...") + expect(label).not.toHaveClass("animate-thinking-shine") + expect(label).toHaveClass("text-vscode-foreground") + }) + + it("keeps completed reasoning blocks static once streaming has finished", () => { + renderReasoningBlock({ isStreaming: false, isLast: true }) + + const label = screen.getByText("Thinking...") + expect(label).not.toHaveClass("animate-thinking-shine") + expect(label).toHaveClass("text-vscode-foreground") + }) + + it("always keeps the bold styling on the thinking label", () => { + renderReasoningBlock({ isStreaming: true, isLast: true }) + + expect(screen.getByText("Thinking...")).toHaveClass("font-bold") + }) +}) diff --git a/webview-ui/src/components/common/MarkdownBlock.tsx b/webview-ui/src/components/common/MarkdownBlock.tsx index 02f696553f..3eb8c44f60 100644 --- a/webview-ui/src/components/common/MarkdownBlock.tsx +++ b/webview-ui/src/components/common/MarkdownBlock.tsx @@ -32,6 +32,8 @@ const ALERT_LABELS: Record = { interface MarkdownBlockProps { markdown?: string + /** Opt-in zebra striping for markdown tables (Settings → UI → tableStriped). */ + striped?: boolean } const StyledMarkdown = styled.div` @@ -213,7 +215,9 @@ const StyledMarkdown = styled.div` color: var(--vscode-foreground); } - tr:nth-child(even) { + /* Opt-in zebra striping (Settings → UI). The ampersand keeps the class on + this element; without it styled-components would require a descendant class. */ + &.table-striped tr:nth-child(even) { background-color: var(--vscode-editor-inactiveSelectionBackground); } @@ -273,7 +277,7 @@ const StyledMarkdown = styled.div` } ` -const MarkdownBlock = memo(({ markdown }: MarkdownBlockProps) => { +const MarkdownBlock = memo(({ markdown, striped = false }: MarkdownBlockProps) => { const components = useMemo( () => ({ table: ({ children, ...props }: any) => { @@ -395,7 +399,7 @@ const MarkdownBlock = memo(({ markdown }: MarkdownBlockProps) => { ) return ( - + { expect(screen.getByText("Third level ordered")).toBeInTheDocument() expect(screen.getByText("Back to first level")).toBeInTheDocument() }) + + it("should not apply table striping by default", () => { + const { container } = render() + + const root = container.firstElementChild as HTMLElement + expect(root).not.toHaveClass("table-striped") + }) + + it("should apply table striping when the striped prop is enabled", () => { + const { container } = render() + + const root = container.firstElementChild as HTMLElement + expect(root).toHaveClass("table-striped") + }) }) diff --git a/webview-ui/src/components/settings/SettingsView.tsx b/webview-ui/src/components/settings/SettingsView.tsx index b270ff342d..854495cee2 100644 --- a/webview-ui/src/components/settings/SettingsView.tsx +++ b/webview-ui/src/components/settings/SettingsView.tsx @@ -213,6 +213,8 @@ const SettingsView = forwardRef(({ onDone, t reasoningBlockCollapsed, chatFontSize, enterBehavior, + chatInputEffect, + tableStriped, includeCurrentTime, includeCurrentCost, maxGitStatusFiles, @@ -439,6 +441,8 @@ const SettingsView = forwardRef(({ onDone, t reasoningBlockCollapsed: reasoningBlockCollapsed ?? true, chatFontSize: chatFontSize ?? null, enterBehavior: enterBehavior ?? "send", + chatInputEffect: chatInputEffect ?? "marquee", + tableStriped: tableStriped ?? false, includeCurrentTime: includeCurrentTime ?? true, includeCurrentCost: includeCurrentCost ?? true, maxGitStatusFiles: maxGitStatusFiles ?? 0, @@ -937,7 +941,9 @@ const SettingsView = forwardRef(({ onDone, t { reasoningBlockCollapsed: boolean enterBehavior: "send" | "newline" + chatInputEffect: "marquee" | "breathing" chatFontSize?: number + tableStriped?: boolean autoCloseZooOpenedFiles?: boolean autoCloseZooOpenedFilesAfterUserEdited?: boolean autoCloseZooOpenedNewFiles?: boolean @@ -32,7 +34,9 @@ interface UISettingsProps extends HTMLAttributes { export const UISettings = ({ reasoningBlockCollapsed, enterBehavior, + chatInputEffect, chatFontSize, + tableStriped, autoCloseZooOpenedFiles, autoCloseZooOpenedFilesAfterUserEdited, autoCloseZooOpenedNewFiles, @@ -82,6 +86,25 @@ export const UISettings = ({ telemetryClient.capture("ui_settings_chat_font_size_reset") } + // VSCodeDropdown's onChange is typed as an intersection of a native Event + // handler and a FormEventHandler, so we accept `unknown` (assignable to both) + // and narrow to the actual onChange?.({ target: { value: e.target.value } })} + data-testid={dataTestId}> + {children} + + ), + VSCodeOption: ({ value, children }: any) => , })) vi.mock("../../../components/common/Tab", () => ({ diff --git a/webview-ui/src/components/settings/__tests__/UISettings.spec.tsx b/webview-ui/src/components/settings/__tests__/UISettings.spec.tsx index 4e61f0fcff..a81a881444 100644 --- a/webview-ui/src/components/settings/__tests__/UISettings.spec.tsx +++ b/webview-ui/src/components/settings/__tests__/UISettings.spec.tsx @@ -11,6 +11,7 @@ describe("UISettings", () => { const defaultProps = { reasoningBlockCollapsed: false, enterBehavior: "send" as const, + chatInputEffect: "marquee" as const, setCachedStateField: vi.fn(), } @@ -176,4 +177,45 @@ describe("UISettings", () => { }) }) }) + + describe("chat input effect", () => { + it("renders the chat input effect dropdown", () => { + const { getByTestId } = render() + const dropdown = getByTestId("chat-input-effect-dropdown") as HTMLSelectElement + expect(dropdown).toBeTruthy() + expect(dropdown.value).toBe("marquee") + }) + + it("calls setCachedStateField when chat input effect is changed", async () => { + const setCachedStateField = vi.fn() + const { getByTestId } = render() + + const dropdown = getByTestId("chat-input-effect-dropdown") + fireEvent.change(dropdown, { target: { value: "breathing" } }) + + await waitFor(() => { + expect(setCachedStateField).toHaveBeenCalledWith("chatInputEffect", "breathing") + }) + }) + }) + + describe("table striping", () => { + it("renders the table striping checkbox unchecked by default", () => { + const { getByTestId } = render() + const checkbox = getByTestId("table-striped-checkbox") as HTMLInputElement + expect(checkbox).toBeTruthy() + expect(checkbox.checked).toBe(false) + }) + + it("calls setCachedStateField when the table striping checkbox is toggled", async () => { + const setCachedStateField = vi.fn() + const { getByTestId } = render() + + fireEvent.click(getByTestId("table-striped-checkbox")) + + await waitFor(() => { + expect(setCachedStateField).toHaveBeenCalledWith("tableStriped", true) + }) + }) + }) }) diff --git a/webview-ui/src/components/settings/__tests__/UISettings.visual.fixture.tsx b/webview-ui/src/components/settings/__tests__/UISettings.visual.fixture.tsx index 09481ce498..76f0437bb4 100644 --- a/webview-ui/src/components/settings/__tests__/UISettings.visual.fixture.tsx +++ b/webview-ui/src/components/settings/__tests__/UISettings.visual.fixture.tsx @@ -8,6 +8,7 @@ import { AppProviders } from "../../../../playwright/AppProviders" interface UIState { reasoningBlockCollapsed: boolean enterBehavior: "send" | "newline" + chatInputEffect: "marquee" | "breathing" chatFontSize?: number autoCloseZooOpenedFiles?: boolean autoCloseZooOpenedFilesAfterUserEdited?: boolean @@ -18,6 +19,7 @@ export function UISettingsStory() { const [state, setState] = useState({ reasoningBlockCollapsed: true, enterBehavior: "send", + chatInputEffect: "marquee", chatFontSize: 14, autoCloseZooOpenedFiles: true, autoCloseZooOpenedFilesAfterUserEdited: true, @@ -33,6 +35,8 @@ export function UISettingsStory() { return { ...current, [field]: Boolean(value) } case "enterBehavior": return { ...current, enterBehavior: value === "newline" ? "newline" : "send" } + case "chatInputEffect": + return { ...current, chatInputEffect: value === "breathing" ? "breathing" : "marquee" } case "chatFontSize": return { ...current, chatFontSize: typeof value === "number" ? value : undefined } default: diff --git a/webview-ui/src/components/settings/__tests__/__screenshots__/ui-settings-dark.png b/webview-ui/src/components/settings/__tests__/__screenshots__/ui-settings-dark.png index 2773ec6614..4f15277345 100644 Binary files a/webview-ui/src/components/settings/__tests__/__screenshots__/ui-settings-dark.png and b/webview-ui/src/components/settings/__tests__/__screenshots__/ui-settings-dark.png differ diff --git a/webview-ui/src/components/settings/__tests__/__screenshots__/ui-settings-high-contrast-light.png b/webview-ui/src/components/settings/__tests__/__screenshots__/ui-settings-high-contrast-light.png index ba27c7b912..5aff7a3cc2 100644 Binary files a/webview-ui/src/components/settings/__tests__/__screenshots__/ui-settings-high-contrast-light.png and b/webview-ui/src/components/settings/__tests__/__screenshots__/ui-settings-high-contrast-light.png differ diff --git a/webview-ui/src/components/settings/__tests__/__screenshots__/ui-settings-high-contrast.png b/webview-ui/src/components/settings/__tests__/__screenshots__/ui-settings-high-contrast.png index 2eb48a95dc..2dfc24889b 100644 Binary files a/webview-ui/src/components/settings/__tests__/__screenshots__/ui-settings-high-contrast.png and b/webview-ui/src/components/settings/__tests__/__screenshots__/ui-settings-high-contrast.png differ diff --git a/webview-ui/src/components/settings/__tests__/__screenshots__/ui-settings-light.png b/webview-ui/src/components/settings/__tests__/__screenshots__/ui-settings-light.png index f454bb2d51..16a618a66d 100644 Binary files a/webview-ui/src/components/settings/__tests__/__screenshots__/ui-settings-light.png and b/webview-ui/src/components/settings/__tests__/__screenshots__/ui-settings-light.png differ diff --git a/webview-ui/src/i18n/locales/ca/common.json b/webview-ui/src/i18n/locales/ca/common.json index 56e9a3745e..a41749fb6d 100644 --- a/webview-ui/src/i18n/locales/ca/common.json +++ b/webview-ui/src/i18n/locales/ca/common.json @@ -21,6 +21,7 @@ }, "ui": { "search_placeholder": "Cerca...", + "clear_search": "Neteja la cerca", "no_results": "No s'han trobat resultats" }, "mermaid": { diff --git a/webview-ui/src/i18n/locales/ca/settings.json b/webview-ui/src/i18n/locales/ca/settings.json index 762a1290af..12f2ff5c5d 100644 --- a/webview-ui/src/i18n/locales/ca/settings.json +++ b/webview-ui/src/i18n/locales/ca/settings.json @@ -1122,6 +1122,12 @@ "label": "Requereix {{primaryMod}}+Intro per enviar missatges", "description": "Quan estigui activat, has de prémer {{primaryMod}}+Intro per enviar missatges en lloc de només Intro" }, + "chatInputEffect": { + "label": "Efecte de la caixa d'entrada mentre l'IA treballa", + "marquee": "Efecte marquesina (vora giratòria)", + "breathing": "Efecte de respiració (brillantor pulsant)", + "description": "Tria l'efecte d'animació de la vora de la caixa d'entrada mentre l'IA treballa" + }, "chatFontSize": { "label": "Mida de lletra del xat", "description": "Defineix la mida de la lletra (en píxels) del xat de Zoo Code. Deixa-ho al valor per defecte per coincidir amb la mida de lletra del VS Code.", @@ -1138,6 +1144,10 @@ "autoCloseZooOpenedNewFiles": { "label": "Tanca automaticament els fitxers nous creats", "description": "Quan esta activat, les pestanyes dels fitxers que Zoo ha creat (en lloc de modificar) tambe es tanquen automaticament despres d'acceptar el canvi." + }, + "tableStriped": { + "label": "Files de taula ratllades", + "description": "Aplica colors de fons alterns a les files de les taules de markdown" } }, "skills": { diff --git a/webview-ui/src/i18n/locales/de/common.json b/webview-ui/src/i18n/locales/de/common.json index ab8bd6d240..8e02dd035a 100644 --- a/webview-ui/src/i18n/locales/de/common.json +++ b/webview-ui/src/i18n/locales/de/common.json @@ -21,6 +21,7 @@ }, "ui": { "search_placeholder": "Suchen...", + "clear_search": "Suche löschen", "no_results": "Keine Ergebnisse gefunden" }, "mermaid": { diff --git a/webview-ui/src/i18n/locales/de/settings.json b/webview-ui/src/i18n/locales/de/settings.json index f56984bc73..4f83c6bbf6 100644 --- a/webview-ui/src/i18n/locales/de/settings.json +++ b/webview-ui/src/i18n/locales/de/settings.json @@ -1122,6 +1122,12 @@ "label": "{{primaryMod}}+Enter zum Senden erfordern", "description": "Wenn aktiviert, musst du {{primaryMod}}+Enter drücken, um Nachrichten zu senden, anstatt nur Enter" }, + "chatInputEffect": { + "label": "Effekt für das Eingabefeld, während die KI arbeitet", + "marquee": "Laufschrift (rotierender Rand)", + "breathing": "Atmung (pulsierendes Leuchten)", + "description": "Wähle den Rahmenanimationseffekt für das Eingabefeld, während die KI arbeitet" + }, "chatFontSize": { "label": "Chat-Schriftgröße", "description": "Lege die Schriftgröße (in Pixeln) für den Zoo Code-Chat fest. Belasse den Standardwert, um die Schriftgröße von VS Code zu übernehmen.", @@ -1138,6 +1144,10 @@ "autoCloseZooOpenedNewFiles": { "label": "Neu erstellte Dateien automatisch schließen", "description": "Wenn aktiviert, werden Tabs für Dateien, die Zoo neu erstellt hat (anstatt sie zu bearbeiten), nach dem Akzeptieren der Änderung ebenfalls automatisch geschlossen." + }, + "tableStriped": { + "label": "Gestreifte Tabellenzeilen", + "description": "Abwechselnde Hintergrundfarben auf Zeilen in Markdown-Tabellen anwenden" } }, "skills": { diff --git a/webview-ui/src/i18n/locales/en/common.json b/webview-ui/src/i18n/locales/en/common.json index 981eaeec75..9132625ccb 100644 --- a/webview-ui/src/i18n/locales/en/common.json +++ b/webview-ui/src/i18n/locales/en/common.json @@ -21,6 +21,7 @@ }, "ui": { "search_placeholder": "Search...", + "clear_search": "Clear search", "no_results": "No results found" }, "mermaid": { diff --git a/webview-ui/src/i18n/locales/en/settings.json b/webview-ui/src/i18n/locales/en/settings.json index 77f8a4f86d..7391504dc0 100644 --- a/webview-ui/src/i18n/locales/en/settings.json +++ b/webview-ui/src/i18n/locales/en/settings.json @@ -226,6 +226,12 @@ "label": "Require {{primaryMod}}+Enter to send messages", "description": "When enabled, you must press {{primaryMod}}+Enter to send messages instead of just Enter" }, + "chatInputEffect": { + "label": "AI Working Input Box Effect", + "marquee": "Marquee (rotating border)", + "breathing": "Breathing (pulsing glow)", + "description": "Choose the border animation effect on the chat input box while AI is working" + }, "chatFontSize": { "label": "Chat font size", "description": "Set the font size (in pixels) for the Zoo Code chat. Leave at the default to match VS Code's font size.", @@ -242,6 +248,10 @@ "autoCloseZooOpenedNewFiles": { "label": "Auto-close newly created files", "description": "When enabled, tabs for files that Zoo created (rather than modified) will also be closed automatically after the edit is accepted." + }, + "tableStriped": { + "label": "Striped table rows", + "description": "Apply alternating background colors to rows in markdown tables" } }, "prompts": { diff --git a/webview-ui/src/i18n/locales/es/common.json b/webview-ui/src/i18n/locales/es/common.json index 03455b7cad..919d7f954d 100644 --- a/webview-ui/src/i18n/locales/es/common.json +++ b/webview-ui/src/i18n/locales/es/common.json @@ -21,6 +21,7 @@ }, "ui": { "search_placeholder": "Buscar...", + "clear_search": "Borrar búsqueda", "no_results": "No se encontraron resultados" }, "mermaid": { diff --git a/webview-ui/src/i18n/locales/es/settings.json b/webview-ui/src/i18n/locales/es/settings.json index e730483fa0..4dbcb25a78 100644 --- a/webview-ui/src/i18n/locales/es/settings.json +++ b/webview-ui/src/i18n/locales/es/settings.json @@ -1122,6 +1122,12 @@ "label": "Requerir {{primaryMod}}+Enter para enviar mensajes", "description": "Cuando está activado, debes presionar {{primaryMod}}+Enter para enviar mensajes en lugar de solo Enter" }, + "chatInputEffect": { + "label": "Efecto de la caja de entrada mientras la IA trabaja", + "marquee": "Cinta (borde giratorio)", + "breathing": "Respiración (brillo pulsante)", + "description": "Elige el efecto de animación del borde de la caja de entrada mientras la IA trabaja" + }, "chatFontSize": { "label": "Tamaño de fuente del chat", "description": "Establece el tamaño de fuente (en píxeles) del chat de Zoo Code. Déjalo en el valor predeterminado para que coincida con el tamaño de fuente de VS Code.", @@ -1138,6 +1144,10 @@ "autoCloseZooOpenedNewFiles": { "label": "Cerrar automaticamente los archivos nuevos creados", "description": "Cuando esta opcion esta activada, las pestanas de archivos que Zoo ha creado (en lugar de modificar) tambien se cierran automaticamente despues de aceptar la modificacion." + }, + "tableStriped": { + "label": "Filas de tabla con rayas", + "description": "Aplica colores de fondo alternos a las filas de las tablas de markdown" } }, "skills": { diff --git a/webview-ui/src/i18n/locales/fr/common.json b/webview-ui/src/i18n/locales/fr/common.json index def93ad6c5..0da8a05cd4 100644 --- a/webview-ui/src/i18n/locales/fr/common.json +++ b/webview-ui/src/i18n/locales/fr/common.json @@ -21,6 +21,7 @@ }, "ui": { "search_placeholder": "Rechercher...", + "clear_search": "Effacer la recherche", "no_results": "Aucun résultat trouvé" }, "mermaid": { diff --git a/webview-ui/src/i18n/locales/fr/settings.json b/webview-ui/src/i18n/locales/fr/settings.json index 78770da21d..ce2e35aa15 100644 --- a/webview-ui/src/i18n/locales/fr/settings.json +++ b/webview-ui/src/i18n/locales/fr/settings.json @@ -1122,6 +1122,12 @@ "label": "Exiger {{primaryMod}}+Entrée pour envoyer les messages", "description": "Lorsqu'activé, tu dois appuyer sur {{primaryMod}}+Entrée pour envoyer des messages au lieu de simplement Entrée" }, + "chatInputEffect": { + "label": "Effet de la zone de saisie pendant le travail de l'IA", + "marquee": "Défilement (bordure rotative)", + "breathing": "Respiration (lueur pulsante)", + "description": "Choisissez l'effet d'animation de la bordure de la zone de saisie pendant que l'IA travaille" + }, "chatFontSize": { "label": "Taille de police du chat", "description": "Définissez la taille de police (en pixels) du chat Zoo Code. Laissez la valeur par défaut pour correspondre à la taille de police de VS Code.", @@ -1138,6 +1144,10 @@ "autoCloseZooOpenedNewFiles": { "label": "Fermer automatiquement les nouveaux fichiers créés", "description": "Quand cette option est activée, les onglets des fichiers que Zoo a créés (plutôt que modifiés) sont également fermés automatiquement après l'acceptation de la modification." + }, + "tableStriped": { + "label": "Lignes de tableau alternées", + "description": "Appliquer des couleurs d'arrière-plan alternées aux lignes des tableaux markdown" } }, "skills": { diff --git a/webview-ui/src/i18n/locales/hi/common.json b/webview-ui/src/i18n/locales/hi/common.json index 076530e6b0..186ec84add 100644 --- a/webview-ui/src/i18n/locales/hi/common.json +++ b/webview-ui/src/i18n/locales/hi/common.json @@ -21,6 +21,7 @@ }, "ui": { "search_placeholder": "खोजें...", + "clear_search": "खोज साफ़ करें", "no_results": "कोई परिणाम नहीं मिला" }, "mermaid": { diff --git a/webview-ui/src/i18n/locales/hi/settings.json b/webview-ui/src/i18n/locales/hi/settings.json index 413d3515bc..6e8bb4df3a 100644 --- a/webview-ui/src/i18n/locales/hi/settings.json +++ b/webview-ui/src/i18n/locales/hi/settings.json @@ -1122,6 +1122,12 @@ "label": "संदेश भेजने के लिए {{primaryMod}}+Enter की आवश्यकता है", "description": "जब सक्षम हो, तो आपको केवल Enter के बजाय संदेश भेजने के लिए {{primaryMod}}+Enter दबाना होगा" }, + "chatInputEffect": { + "label": "AI कार्य करते समय इनपुट बॉक्स प्रभाव", + "marquee": "मार्की (घूमती हुई सीमा)", + "breathing": "साँस (स्पंदित चमक)", + "description": "AI कार्य करते समय चैट इनपुट बॉक्स पर बॉर्डर एनीमेशन प्रभाव चुनें" + }, "chatFontSize": { "label": "चैट फ़ॉन्ट आकार", "description": "Zoo Code चैट के लिए फ़ॉन्ट आकार (पिक्सेल में) सेट करें। VS Code के फ़ॉन्ट आकार से मेल खाने के लिए डिफ़ॉल्ट पर छोड़ दें।", @@ -1138,6 +1144,10 @@ "autoCloseZooOpenedNewFiles": { "label": "Nai banayi gayi files ko svachalit band karo", "description": "Saksham hone par, Zoo dvara banaayi gayi (sanshodhit nahin) files ki tabs ko bhi parivartan svikaar hone ke baad svachalit roop se band kar diya jaata hai." + }, + "tableStriped": { + "label": "धारीदार तालिका पंक्तियाँ", + "description": "मार्कडाउन तालिकाओं की पंक्तियों पर वैकल्पिक पृष्ठभूमि रंग लागू करें" } }, "skills": { diff --git a/webview-ui/src/i18n/locales/id/common.json b/webview-ui/src/i18n/locales/id/common.json index a65295f28d..81a2a9bf33 100644 --- a/webview-ui/src/i18n/locales/id/common.json +++ b/webview-ui/src/i18n/locales/id/common.json @@ -21,6 +21,7 @@ }, "ui": { "search_placeholder": "Cari...", + "clear_search": "Hapus pencarian", "no_results": "Tidak ada hasil yang ditemukan" }, "mermaid": { diff --git a/webview-ui/src/i18n/locales/id/settings.json b/webview-ui/src/i18n/locales/id/settings.json index 9b9928da64..737f8376dc 100644 --- a/webview-ui/src/i18n/locales/id/settings.json +++ b/webview-ui/src/i18n/locales/id/settings.json @@ -1122,6 +1122,12 @@ "label": "Memerlukan {{primaryMod}}+Enter untuk mengirim pesan", "description": "Ketika diaktifkan, kamu harus menekan {{primaryMod}}+Enter untuk mengirim pesan alih-alih hanya Enter" }, + "chatInputEffect": { + "label": "Efek kotak input saat AI bekerja", + "marquee": "Marquee (batas berputar)", + "breathing": "Bernapas (cahaya berdenyut)", + "description": "Pilih efek animasi batas pada kotak input chat saat AI bekerja" + }, "chatFontSize": { "label": "Ukuran font obrolan", "description": "Atur ukuran font (dalam piksel) untuk obrolan Zoo Code. Biarkan pada nilai default agar sesuai dengan ukuran font VS Code.", @@ -1138,6 +1144,10 @@ "autoCloseZooOpenedNewFiles": { "label": "Tutup otomatis file baru yang dibuat", "description": "Saat diaktifkan, tab untuk file yang dibuat Zoo (bukan dimodifikasi) juga ditutup secara otomatis setelah perubahan diterima." + }, + "tableStriped": { + "label": "Baris tabel bergaris", + "description": "Terapkan warna latar belakang bergantian pada baris di tabel markdown" } }, "skills": { diff --git a/webview-ui/src/i18n/locales/it/common.json b/webview-ui/src/i18n/locales/it/common.json index 9b801628f4..bf358a6484 100644 --- a/webview-ui/src/i18n/locales/it/common.json +++ b/webview-ui/src/i18n/locales/it/common.json @@ -21,6 +21,7 @@ }, "ui": { "search_placeholder": "Cerca...", + "clear_search": "Cancella ricerca", "no_results": "Nessun risultato trovato" }, "mermaid": { diff --git a/webview-ui/src/i18n/locales/it/settings.json b/webview-ui/src/i18n/locales/it/settings.json index 8a43ec3d35..7b17b950f3 100644 --- a/webview-ui/src/i18n/locales/it/settings.json +++ b/webview-ui/src/i18n/locales/it/settings.json @@ -1122,6 +1122,12 @@ "label": "Richiedi {{primaryMod}}+Invio per inviare messaggi", "description": "Quando abilitato, devi premere {{primaryMod}}+Invio per inviare messaggi invece di solo Invio" }, + "chatInputEffect": { + "label": "Effetto della casella di input mentre l'IA lavora", + "marquee": "Scorrimento (bordo rotante)", + "breathing": "Respirazione (bagliore pulsante)", + "description": "Scegli l'effetto di animazione del bordo sulla casella di input della chat mentre l'IA lavora" + }, "chatFontSize": { "label": "Dimensione carattere della chat", "description": "Imposta la dimensione del carattere (in pixel) per la chat di Zoo Code. Lascia il valore predefinito per adattarsi alla dimensione del carattere di VS Code.", @@ -1138,6 +1144,10 @@ "autoCloseZooOpenedNewFiles": { "label": "Chiudi automaticamente i nuovi file creati", "description": "Quando abilitata, le schede dei file che Zoo ha creato (invece di modificare) vengono chiuse automaticamente dopo l'accettazione della modifica." + }, + "tableStriped": { + "label": "Righe della tabella a strisce", + "description": "Applica colori di sfondo alternati alle righe delle tabelle markdown" } }, "skills": { diff --git a/webview-ui/src/i18n/locales/ja/common.json b/webview-ui/src/i18n/locales/ja/common.json index b3b9d462e0..0c98597d39 100644 --- a/webview-ui/src/i18n/locales/ja/common.json +++ b/webview-ui/src/i18n/locales/ja/common.json @@ -21,6 +21,7 @@ }, "ui": { "search_placeholder": "検索...", + "clear_search": "検索をクリア", "no_results": "結果が見つかりません" }, "mermaid": { diff --git a/webview-ui/src/i18n/locales/ja/settings.json b/webview-ui/src/i18n/locales/ja/settings.json index b2cfbe977e..f3de08a204 100644 --- a/webview-ui/src/i18n/locales/ja/settings.json +++ b/webview-ui/src/i18n/locales/ja/settings.json @@ -1122,6 +1122,12 @@ "label": "メッセージを送信するには{{primaryMod}}+Enterが必要", "description": "有効にすると、Enterだけでなく{{primaryMod}}+Enterを押してメッセージを送信する必要があります" }, + "chatInputEffect": { + "label": "AI作業中の入力ボックスエフェクト", + "marquee": "マーキー(回転する枠線)", + "breathing": "ブリージング(脈動する光)", + "description": "AI作業中にチャット入力ボックスの枠線アニメーション効果を選択" + }, "chatFontSize": { "label": "チャットのフォントサイズ", "description": "Zoo Code チャットのフォントサイズ(ピクセル単位)を設定します。VS Code のフォントサイズに合わせるには既定値のままにしてください。", @@ -1138,6 +1144,10 @@ "autoCloseZooOpenedNewFiles": { "label": "新しく作成されたファイルを自動的に閉じる", "description": "有効にすると、Zooが作成した(変更ではなく)ファイルのタブも、変更を承認した後に自動的に閉じられます。" + }, + "tableStriped": { + "label": "表の行を縞模様にする", + "description": "Markdown テーブルの行に交互の背景色を適用します" } }, "skills": { diff --git a/webview-ui/src/i18n/locales/ko/common.json b/webview-ui/src/i18n/locales/ko/common.json index d7120e2520..74da3c4f33 100644 --- a/webview-ui/src/i18n/locales/ko/common.json +++ b/webview-ui/src/i18n/locales/ko/common.json @@ -21,6 +21,7 @@ }, "ui": { "search_placeholder": "검색...", + "clear_search": "검색 지우기", "no_results": "결과를 찾을 수 없습니다" }, "mermaid": { diff --git a/webview-ui/src/i18n/locales/ko/settings.json b/webview-ui/src/i18n/locales/ko/settings.json index 1ff9addeb4..dbb8315beb 100644 --- a/webview-ui/src/i18n/locales/ko/settings.json +++ b/webview-ui/src/i18n/locales/ko/settings.json @@ -1122,6 +1122,12 @@ "label": "메시지를 보내려면 {{primaryMod}}+Enter가 필요", "description": "활성화하면 Enter만으로는 안 되고 {{primaryMod}}+Enter를 눌러야 메시지를 보낼 수 있습니다" }, + "chatInputEffect": { + "label": "AI 작업 중 입력 상자 효과", + "marquee": "마퀴 (회전 테두리)", + "breathing": "호흡 (맥동하는 광채)", + "description": "AI가 작업하는 동안 채팅 입력 상자에 표시할 테두리 애니메이션 효과를 선택하세요" + }, "chatFontSize": { "label": "채팅 글꼴 크기", "description": "Zoo Code 채팅의 글꼴 크기(픽셀)를 설정합니다. VS Code 글꼴 크기에 맞추려면 기본값으로 두세요.", @@ -1138,6 +1144,10 @@ "autoCloseZooOpenedNewFiles": { "label": "새로 만든 파일 자동 닫기", "description": "활성화 시, Zoo가 수정한 것이 아니라 새로 만든 파일의 탭도 변경 사항을 승인한 후 자동으로 닫힙니다." + }, + "tableStriped": { + "label": "표 행 줄무늬", + "description": "마크다운 표의 행에 교차 배경색을 적용합니다" } }, "skills": { diff --git a/webview-ui/src/i18n/locales/nl/common.json b/webview-ui/src/i18n/locales/nl/common.json index ec6cf89ccb..11bf8fb9b5 100644 --- a/webview-ui/src/i18n/locales/nl/common.json +++ b/webview-ui/src/i18n/locales/nl/common.json @@ -21,6 +21,7 @@ }, "ui": { "search_placeholder": "Zoeken...", + "clear_search": "Zoekopdracht wissen", "no_results": "Geen resultaten gevonden" }, "mermaid": { diff --git a/webview-ui/src/i18n/locales/nl/settings.json b/webview-ui/src/i18n/locales/nl/settings.json index 4361d091a1..0cd0761283 100644 --- a/webview-ui/src/i18n/locales/nl/settings.json +++ b/webview-ui/src/i18n/locales/nl/settings.json @@ -1122,6 +1122,12 @@ "label": "Vereist {{primaryMod}}+Enter om berichten te versturen", "description": "Wanneer ingeschakeld, moet je {{primaryMod}}+Enter indrukken om berichten te versturen in plaats van alleen Enter" }, + "chatInputEffect": { + "label": "Invoervak-effect terwijl AI werkt", + "marquee": "Marquee (draaiende rand)", + "breathing": "Ademen (pulserende gloed)", + "description": "Kies het randanimatie-effect op het chat-invoervak terwijl AI werkt" + }, "chatFontSize": { "label": "Lettergrootte van chat", "description": "Stel de lettergrootte (in pixels) in voor de Zoo Code-chat. Laat op de standaardwaarde staan om overeen te komen met de lettergrootte van VS Code.", @@ -1138,6 +1144,10 @@ "autoCloseZooOpenedNewFiles": { "label": "Automatisch sluiten van nieuw gemaakte bestanden", "description": "Wanneer ingeschakeld, worden tabbladen van bestanden die Zoo heeft aangemaakt (in plaats van gewijzigd) ook automatisch gesloten nadat de wijziging is geaccepteerd." + }, + "tableStriped": { + "label": "Gestreepte tabelrijen", + "description": "Pas afwisselende achtergrondkleuren toe op rijen in markdown-tabellen" } }, "skills": { diff --git a/webview-ui/src/i18n/locales/pl/common.json b/webview-ui/src/i18n/locales/pl/common.json index 419aa83af1..86477855c3 100644 --- a/webview-ui/src/i18n/locales/pl/common.json +++ b/webview-ui/src/i18n/locales/pl/common.json @@ -21,6 +21,7 @@ }, "ui": { "search_placeholder": "Szukaj...", + "clear_search": "Wyczyść wyszukiwanie", "no_results": "Nie znaleziono wyników" }, "mermaid": { diff --git a/webview-ui/src/i18n/locales/pl/settings.json b/webview-ui/src/i18n/locales/pl/settings.json index 277bcaa470..0203be2542 100644 --- a/webview-ui/src/i18n/locales/pl/settings.json +++ b/webview-ui/src/i18n/locales/pl/settings.json @@ -1122,6 +1122,12 @@ "label": "Wymagaj {{primaryMod}}+Enter do wysyłania wiadomości", "description": "Po włączeniu musisz nacisnąć {{primaryMod}}+Enter, aby wysłać wiadomości, zamiast tylko Enter" }, + "chatInputEffect": { + "label": "Efekt pola wprowadzania podczas pracy AI", + "marquee": "Marquee (obracająca się ramka)", + "breathing": "Oddychanie (pulsująca poświata)", + "description": "Wybierz efekt animacji ramki pola wprowadzania czatu podczas pracy AI" + }, "chatFontSize": { "label": "Rozmiar czcionki czatu", "description": "Ustaw rozmiar czcionki (w pikselach) dla czatu Zoo Code. Pozostaw wartość domyślną, aby dopasować ją do rozmiaru czcionki VS Code.", @@ -1138,6 +1144,10 @@ "autoCloseZooOpenedNewFiles": { "label": "Automatycznie zamykaj nowo utworzone pliki", "description": "Gdy wlaczone, karty plikow, ktore Zoo utworzylo (zamiast zmodyfikowalo), sa rowniez automatycznie zamykane po zaakceptowaniu zmiany." + }, + "tableStriped": { + "label": "Pasiaste wiersze tabeli", + "description": "Zastosuj naprzemienne kolory tła do wierszy w tabelach markdown" } }, "skills": { diff --git a/webview-ui/src/i18n/locales/pt-BR/common.json b/webview-ui/src/i18n/locales/pt-BR/common.json index 4990796976..482e0f9a83 100644 --- a/webview-ui/src/i18n/locales/pt-BR/common.json +++ b/webview-ui/src/i18n/locales/pt-BR/common.json @@ -21,6 +21,7 @@ }, "ui": { "search_placeholder": "Pesquisar...", + "clear_search": "Limpar pesquisa", "no_results": "Nenhum resultado encontrado" }, "mermaid": { diff --git a/webview-ui/src/i18n/locales/pt-BR/settings.json b/webview-ui/src/i18n/locales/pt-BR/settings.json index 8ce67bcd48..29e1cf7e2c 100644 --- a/webview-ui/src/i18n/locales/pt-BR/settings.json +++ b/webview-ui/src/i18n/locales/pt-BR/settings.json @@ -1122,6 +1122,12 @@ "label": "Requer {{primaryMod}}+Enter para enviar mensagens", "description": "Quando ativado, você deve pressionar {{primaryMod}}+Enter para enviar mensagens em vez de apenas Enter" }, + "chatInputEffect": { + "label": "Efeito da caixa de entrada enquanto a IA trabalha", + "marquee": "Marquee (borda giratória)", + "breathing": "Respiração (brilho pulsante)", + "description": "Escolha o efeito de animação da borda na caixa de entrada do chat enquanto a IA trabalha" + }, "chatFontSize": { "label": "Tamanho da fonte do chat", "description": "Defina o tamanho da fonte (em pixels) do chat do Zoo Code. Deixe no padrão para corresponder ao tamanho da fonte do VS Code.", @@ -1138,6 +1144,10 @@ "autoCloseZooOpenedNewFiles": { "label": "Fechar automaticamente novos arquivos criados", "description": "Quando ativada, as abas de arquivos que o Zoo criou (em vez de modificar) tambem sao fechadas automaticamente apos aceitar a modificacao." + }, + "tableStriped": { + "label": "Linhas de tabela listradas", + "description": "Aplica cores de fundo alternadas às linhas em tabelas markdown" } }, "skills": { diff --git a/webview-ui/src/i18n/locales/ru/common.json b/webview-ui/src/i18n/locales/ru/common.json index f66384a693..6070cf1f6e 100644 --- a/webview-ui/src/i18n/locales/ru/common.json +++ b/webview-ui/src/i18n/locales/ru/common.json @@ -21,6 +21,7 @@ }, "ui": { "search_placeholder": "Поиск...", + "clear_search": "Очистить поиск", "no_results": "Результатов не найдено" }, "mermaid": { diff --git a/webview-ui/src/i18n/locales/ru/settings.json b/webview-ui/src/i18n/locales/ru/settings.json index 0ac516c190..4318a03457 100644 --- a/webview-ui/src/i18n/locales/ru/settings.json +++ b/webview-ui/src/i18n/locales/ru/settings.json @@ -1122,6 +1122,12 @@ "label": "Требовать {{primaryMod}}+Enter для отправки сообщений", "description": "Если включено, необходимо нажать {{primaryMod}}+Enter для отправки сообщений вместо простого Enter" }, + "chatInputEffect": { + "label": "Эффект поля ввода во время работы ИИ", + "marquee": "Бегущая строка (вращающаяся рамка)", + "breathing": "Дыхание (пульсирующее свечение)", + "description": "Выберите эффект анимации рамки поля ввода чата во время работы ИИ" + }, "chatFontSize": { "label": "Размер шрифта чата", "description": "Задайте размер шрифта (в пикселях) для чата Zoo Code. Оставьте значение по умолчанию, чтобы соответствовать размеру шрифта VS Code.", @@ -1138,6 +1144,10 @@ "autoCloseZooOpenedNewFiles": { "label": "Автоматически закрывать новые созданные файлы", "description": "Если включено, вкладки файлов, которые Zoo создал (а не изменил), также автоматически закрываются после принятия изменения." + }, + "tableStriped": { + "label": "Полосатые строки таблицы", + "description": "Применять чередующиеся цвета фона к строкам таблиц markdown" } }, "skills": { diff --git a/webview-ui/src/i18n/locales/tr/common.json b/webview-ui/src/i18n/locales/tr/common.json index db9e991cd5..34cb747373 100644 --- a/webview-ui/src/i18n/locales/tr/common.json +++ b/webview-ui/src/i18n/locales/tr/common.json @@ -21,6 +21,7 @@ }, "ui": { "search_placeholder": "Ara...", + "clear_search": "Aramayı temizle", "no_results": "Sonuç bulunamadı" }, "mermaid": { diff --git a/webview-ui/src/i18n/locales/tr/settings.json b/webview-ui/src/i18n/locales/tr/settings.json index 5d3a5cb89a..c7dfc1b72c 100644 --- a/webview-ui/src/i18n/locales/tr/settings.json +++ b/webview-ui/src/i18n/locales/tr/settings.json @@ -1122,6 +1122,12 @@ "label": "Mesaj göndermek için {{primaryMod}}+Enter gerekli", "description": "Etkinleştirildiğinde, sadece Enter yerine mesaj göndermek için {{primaryMod}}+Enter'a basmalısınız" }, + "chatInputEffect": { + "label": "AI çalışırken giriş kutusu efekti", + "marquee": "Kayan yazı (dönen kenarlık)", + "breathing": "Nefes alma (nabız gibi parlayan)", + "description": "AI çalışırken sohbet giriş kutusundaki kenarlık animasyon efektini seçin" + }, "chatFontSize": { "label": "Sohbet yazı tipi boyutu", "description": "Zoo Code sohbeti için yazı tipi boyutunu (piksel cinsinden) ayarlayın. VS Code yazı tipi boyutuyla eşleşmesi için varsayılan değerde bırakın.", @@ -1138,6 +1144,10 @@ "autoCloseZooOpenedNewFiles": { "label": "Yeni olusturulan dosyalari otomatik kapat", "description": "Etkinlestirildiginde, Zoo'nun olusturdugu (degistirmedigi) dosyalarin sekmeleri de degisiklik kabul edildikten sonra otomatik olarak kapatilir." + }, + "tableStriped": { + "label": "Çizgili tablo satırları", + "description": "Markdown tablolarındaki satırlara alternatif arka plan renkleri uygula" } }, "skills": { diff --git a/webview-ui/src/i18n/locales/vi/common.json b/webview-ui/src/i18n/locales/vi/common.json index 57eb31fafa..bd8e5e1487 100644 --- a/webview-ui/src/i18n/locales/vi/common.json +++ b/webview-ui/src/i18n/locales/vi/common.json @@ -21,6 +21,7 @@ }, "ui": { "search_placeholder": "Tìm kiếm...", + "clear_search": "Xóa tìm kiếm", "no_results": "Không tìm thấy kết quả nào" }, "mermaid": { diff --git a/webview-ui/src/i18n/locales/vi/settings.json b/webview-ui/src/i18n/locales/vi/settings.json index adb1be64e3..35700b0391 100644 --- a/webview-ui/src/i18n/locales/vi/settings.json +++ b/webview-ui/src/i18n/locales/vi/settings.json @@ -1122,6 +1122,12 @@ "label": "Yêu cầu {{primaryMod}}+Enter để gửi tin nhắn", "description": "Khi được bật, bạn phải nhấn {{primaryMod}}+Enter để gửi tin nhắn thay vì chỉ nhấn Enter" }, + "chatInputEffect": { + "label": "Hiệu ứng ô nhập khi AI đang làm việc", + "marquee": "Marquee (viền xoay)", + "breathing": "Thở (ánh sáng nhấp nháy)", + "description": "Chọn hiệu ứng hoạt hình viền trên ô nhập chat khi AI đang làm việc" + }, "chatFontSize": { "label": "Cỡ chữ trò chuyện", "description": "Đặt cỡ chữ (tính bằng pixel) cho cuộc trò chuyện Zoo Code. Để ở giá trị mặc định để khớp với cỡ chữ của VS Code.", @@ -1138,6 +1144,10 @@ "autoCloseZooOpenedNewFiles": { "label": "Tự động đóng các tệp mới tạo", "description": "Khi bật, các tab của các tệp mà Zoo đã tạo (thay vì chỉnh sửa) cũng sẽ tự động đóng sau khi chấp nhận thay đổi." + }, + "tableStriped": { + "label": "Hàng bảng sọc", + "description": "Áp dụng màu nền xen kẽ cho các hàng trong bảng markdown" } }, "skills": { diff --git a/webview-ui/src/i18n/locales/zh-CN/common.json b/webview-ui/src/i18n/locales/zh-CN/common.json index 10df089333..2f2f9d25b2 100644 --- a/webview-ui/src/i18n/locales/zh-CN/common.json +++ b/webview-ui/src/i18n/locales/zh-CN/common.json @@ -21,6 +21,7 @@ }, "ui": { "search_placeholder": "搜索...", + "clear_search": "清除搜索", "no_results": "未找到结果" }, "mermaid": { diff --git a/webview-ui/src/i18n/locales/zh-CN/settings.json b/webview-ui/src/i18n/locales/zh-CN/settings.json index 916f629efe..8559b50d44 100644 --- a/webview-ui/src/i18n/locales/zh-CN/settings.json +++ b/webview-ui/src/i18n/locales/zh-CN/settings.json @@ -1122,6 +1122,12 @@ "label": "需要 {{primaryMod}}+Enter 发送消息", "description": "启用后,必须按 {{primaryMod}}+Enter 发送消息,而不仅仅是 Enter" }, + "chatInputEffect": { + "label": "AI工作时输入框动效", + "marquee": "走马灯效果(旋转光晕)", + "breathing": "呼吸效果(脉冲光晕)", + "description": "选择 AI 工作时输入框的边框动画效果" + }, "chatFontSize": { "label": "聊天字体大小", "description": "设置 Zoo Code 聊天的字体大小(以像素为单位)。保留默认值以匹配 VS Code 的字体大小。", @@ -1138,6 +1144,10 @@ "autoCloseZooOpenedNewFiles": { "label": "自动关闭新建的文件", "description": "启用后,Zoo 新建(而非修改)的文件标签页在接受更改后也会自动关闭。" + }, + "tableStriped": { + "label": "表格隔行着色", + "description": "为 Markdown 表格的行应用交替背景色" } }, "skills": { diff --git a/webview-ui/src/i18n/locales/zh-TW/common.json b/webview-ui/src/i18n/locales/zh-TW/common.json index 558609c157..9190301326 100644 --- a/webview-ui/src/i18n/locales/zh-TW/common.json +++ b/webview-ui/src/i18n/locales/zh-TW/common.json @@ -21,6 +21,7 @@ }, "ui": { "search_placeholder": "搜尋...", + "clear_search": "清除搜尋", "no_results": "找不到結果" }, "mermaid": { diff --git a/webview-ui/src/i18n/locales/zh-TW/settings.json b/webview-ui/src/i18n/locales/zh-TW/settings.json index d1f9258dcf..66efcbb553 100644 --- a/webview-ui/src/i18n/locales/zh-TW/settings.json +++ b/webview-ui/src/i18n/locales/zh-TW/settings.json @@ -173,6 +173,12 @@ "label": "需要 {{primaryMod}}+Enter 傳送訊息", "description": "啟用後,必須按 {{primaryMod}}+Enter 傳送訊息,而不只是 Enter" }, + "chatInputEffect": { + "label": "AI 工作時的輸入框動效", + "marquee": "跑馬燈效果(旋轉光暈)", + "breathing": "呼吸效果(脈動光暈)", + "description": "選擇 AI 工作時聊天輸入框的邊框動畫效果" + }, "chatFontSize": { "label": "聊天字型大小", "description": "設定 Zoo Code 聊天的字型大小(以像素為單位)。保留預設值以符合 VS Code 的字型大小。", @@ -189,6 +195,10 @@ "autoCloseZooOpenedNewFiles": { "label": "自動關閉新建立的檔案", "description": "啟用後,Zoo 新建(而非修改)的檔案分頁在接受變更後也會自動關閉。" + }, + "tableStriped": { + "label": "表格隔行著色", + "description": "為 Markdown 表格的列套用交替背景色" } }, "prompts": { diff --git a/webview-ui/src/index.css b/webview-ui/src/index.css index bccbdf7031..bafc7740ae 100644 --- a/webview-ui/src/index.css +++ b/webview-ui/src/index.css @@ -472,7 +472,7 @@ input[cmdk-input]:focus { } /* - * Use geometric precision for codicons to avoid blurriness + * Use geometric precision for codicons to avoid blurriness */ .codicon[class*="codicon-"] { @@ -603,3 +603,106 @@ input[cmdk-input]:focus { padding: 8px; margin: -8px; } + +/* ChatTextArea marquee border — conic-gradient light beam orbits the ring. + mask clips center (content-box) away → text never blocked. + 1px ring, dark blue beam. Shown only while AI is executing. */ +@property --angle { + syntax: ""; + initial-value: 0deg; + inherits: false; +} + +@keyframes border-spin { + from { + --angle: 0deg; + } + to { + --angle: 360deg; + } +} + +/* ChatTextArea breathing border — pulsing glow + 1px ring. + Shown only while AI is executing (isStreaming gate in ChatTextArea) + when chatInputEffect is set to "breathing". */ +@keyframes streaming-glow { + 0%, + 100% { + box-shadow: 0 0 2px color-mix(in srgb, var(--vscode-focusBorder, #03638f) 8%, transparent); + } + 50% { + box-shadow: + 0 0 12px color-mix(in srgb, var(--vscode-focusBorder, #04567b) 45%, transparent), + 0 0 24px color-mix(in srgb, var(--vscode-focusBorder, #012434) 25%, transparent); + } +} + +.animate-streaming-glow { + animation: streaming-glow 2s ease-in-out infinite; +} + +/* Breathing border ring — 1px blue ring with pulsing opacity */ +@keyframes border-breathe { + 0%, + 100% { + opacity: 0.15; + } + 50% { + opacity: 1; + } +} + +/* "Thinking..." shimmer — VS Code chat progress style, continuous narrow band. + Ported from workbench chat's chat-thinking-shimmer: a light band sweeps + continuously across the text on a 2s loop. The workbench-only variable + --vscode-chat-thinkingShimmer is not available inside the webview, so the + highlight band is synthesized from the foreground color instead. */ +@keyframes thinking-shine { + 0% { + background-position: 100% 0; + } + 100% { + background-position: -100% 0; + } +} + +.animate-thinking-shine { + background-image: linear-gradient( + 90deg, + color-mix(in srgb, var(--vscode-descriptionForeground) 45%, transparent) 0%, + color-mix(in srgb, var(--vscode-descriptionForeground) 45%, transparent) 30%, + color-mix(in srgb, var(--vscode-foreground) 100%, #ffffff 40%) 50%, + color-mix(in srgb, var(--vscode-descriptionForeground) 45%, transparent) 70%, + color-mix(in srgb, var(--vscode-descriptionForeground) 45%, transparent) 100% + ); + background-size: 400% auto; + background-clip: text; + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; + color: transparent; + animation: thinking-shine 2s linear infinite; + will-change: background-position; +} + +/* Respect the OS-level "reduce motion" accessibility preference: users who + request reduced motion must not see the continuously looping effects above. + The shimmer keeps its static gradient so the text stays legible. */ +@media (prefers-reduced-motion: reduce) { + .animate-smooth-bounce, + .animate-slide-in-right, + .animate-fade-in, + .animate-pulse, + .animate-ground-slide, + .animate-sun, + .animate-streaming-glow, + .animate-thinking-shine { + animation: none; + } + + .animate-thinking-shine { + background-image: none; + -webkit-text-fill-color: var(--vscode-descriptionForeground); + color: var(--vscode-descriptionForeground); + will-change: auto; + } +}