From aaf25159af03db6a8c4f3b994d5c83875b15bd13 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=A8=8B=E5=BA=8F=E5=91=98=E9=98=BF=E6=B1=9F=28Relakkes?= =?UTF-8?q?=29?= Date: Thu, 30 Apr 2026 14:30:20 +0800 Subject: [PATCH] fix: keep workspace review surfaces usable across sessions Workspace inspection now shares the same diff renderer between chat change cards and panel previews, supports scoped preview-tab closing, confirms checkpoint undo, resolves rewind checkpoints from the prompt cwd, and removes light-only workspace chrome so dark theme remains coherent. Constraint: Workspace review must keep working for existing transcript sessions and non-git session-derived changes. Rejected: Keep a separate chat diff preview | it drifted from panel rendering and dark theme behavior. Confidence: high Scope-risk: moderate Directive: Keep chat and workspace diff rendering shared so theme and truncation behavior do not diverge again. Tested: cd desktop && bun run test -- src/components/workspace/WorkspacePanel.test.tsx src/components/chat/MessageList.test.tsx src/stores/workspacePanelStore.test.ts Tested: cd desktop && bun run lint Tested: cd desktop && bun run build Tested: bun test src/server/__tests__/sessions.test.ts Tested: Browser dark workspace preview at http://127.0.0.1:59468/ with src/App.jsx diff Not-tested: macOS packaged app build --- .../components/chat/CurrentTurnChangeCard.tsx | 31 +- .../src/components/chat/MessageList.test.tsx | 12 +- desktop/src/components/chat/MessageList.tsx | 24 +- .../workspace/WorkspaceCodeSurface.tsx | 174 +++++++ .../workspace/WorkspacePanel.test.tsx | 80 +++ .../components/workspace/WorkspacePanel.tsx | 459 ++++++++---------- desktop/src/i18n/locales/en.ts | 3 + desktop/src/i18n/locales/zh.ts | 3 + .../src/stores/workspacePanelStore.test.ts | 57 +++ desktop/src/stores/workspacePanelStore.ts | 59 ++- src/server/__tests__/sessions.test.ts | 59 +++ src/server/index.ts | 2 + src/server/services/sessionRewindService.ts | 16 +- src/server/services/sessionService.ts | 12 + 14 files changed, 682 insertions(+), 309 deletions(-) create mode 100644 desktop/src/components/workspace/WorkspaceCodeSurface.tsx diff --git a/desktop/src/components/chat/CurrentTurnChangeCard.tsx b/desktop/src/components/chat/CurrentTurnChangeCard.tsx index 60326a83..23109e21 100644 --- a/desktop/src/components/chat/CurrentTurnChangeCard.tsx +++ b/desktop/src/components/chat/CurrentTurnChangeCard.tsx @@ -1,6 +1,7 @@ import { useCallback, useMemo, useState } from 'react' import { sessionsApi, type SessionRewindResponse } from '../../api/sessions' import { useTranslation } from '../../i18n' +import { WorkspaceDiffSurface } from '../workspace/WorkspaceCodeSurface' type DiffPreviewState = { loading: boolean @@ -75,7 +76,7 @@ export function CurrentTurnChangeCard({ return (
@@ -142,7 +143,11 @@ export function CurrentTurnChangeCard({ {diffState.error}
) : diffState?.diff ? ( - + ) : (
{t('chat.turnChangesDiffUnavailable')} @@ -164,28 +169,6 @@ export function CurrentTurnChangeCard({ ) } -function DiffPreview({ diff }: { diff: string }) { - const lines = diff.split('\n').slice(0, 220) - return ( -
-      {lines.map((line, index) => {
-        const colorClass = line.startsWith('+') && !line.startsWith('+++')
-          ? 'text-[var(--color-success)]'
-          : line.startsWith('-') && !line.startsWith('---')
-            ? 'text-[var(--color-error)]'
-            : line.startsWith('@@')
-              ? 'text-[var(--color-brand)]'
-              : 'text-[var(--color-code-fg)]'
-        return (
-          
- {line || ' '} -
- ) - })} -
- ) -} - function relativizeWorkspacePath(filePath: string, workDir: string | null): string { const normalizedPath = filePath.replace(/\\/g, '/') if (!workDir || !normalizedPath.startsWith('/')) return normalizedPath diff --git a/desktop/src/components/chat/MessageList.test.tsx b/desktop/src/components/chat/MessageList.test.tsx index 4597f680..f9e963b8 100644 --- a/desktop/src/components/chat/MessageList.test.tsx +++ b/desktop/src/components/chat/MessageList.test.tsx @@ -744,6 +744,7 @@ describe('MessageList nested tool calls', () => { render() expect(await screen.findByText('2 files changed')).toBeTruthy() + expect(screen.getByLabelText('Current turn changed files').className).toContain('w-full max-w-[860px]') expect(screen.getByText('+12')).toBeTruthy() expect(screen.getByText('-4')).toBeTruthy() expect(screen.getByText('src/App.tsx')).toBeTruthy() @@ -812,11 +813,12 @@ describe('MessageList nested tool calls', () => { fireEvent.click(await screen.findByRole('button', { name: 'Show diff for src/App.tsx' })) - expect(await screen.findByText('+new')).toBeTruthy() + const diffSurface = await screen.findByTestId('workspace-code') + expect(diffSurface.textContent).toContain('+new') expect(sessionsApi.getWorkspaceDiff).toHaveBeenCalledWith(ACTIVE_TAB, 'src/App.tsx') }) - it('undoes the current turn from the change card using checkpoint rewind', async () => { + it('confirms before undoing the current turn from the change card', async () => { vi.spyOn(sessionsApi, 'rewind') .mockResolvedValueOnce({ target: { @@ -889,6 +891,12 @@ describe('MessageList nested tool calls', () => { fireEvent.click(await screen.findByRole('button', { name: 'Undo current turn changes' })) + expect(sessionsApi.rewind).toHaveBeenCalledTimes(1) + const dialog = await screen.findByRole('dialog', { name: 'Undo current turn?' }) + expect(within(dialog).getByText('This will rewind the latest assistant response and restore tracked files for this turn.')).toBeTruthy() + + fireEvent.click(within(dialog).getByRole('button', { name: 'Undo current turn' })) + await waitFor(() => { expect(sessionsApi.rewind).toHaveBeenLastCalledWith(ACTIVE_TAB, { targetUserMessageId: 'user-1', diff --git a/desktop/src/components/chat/MessageList.tsx b/desktop/src/components/chat/MessageList.tsx index 3ec44029..278e12fe 100644 --- a/desktop/src/components/chat/MessageList.tsx +++ b/desktop/src/components/chat/MessageList.tsx @@ -21,6 +21,7 @@ import { CurrentTurnChangeCard } from './CurrentTurnChangeCard' import type { AgentTaskNotification, UIMessage } from '../../types/chat' import { Modal } from '../shared/Modal' import { Button } from '../shared/Button' +import { ConfirmDialog } from '../shared/ConfirmDialog' type ToolCall = Extract type ToolResult = Extract @@ -204,6 +205,7 @@ export function MessageList({ sessionId, compact = false }: MessageListProps = { const [currentTurnError, setCurrentTurnError] = useState(null) const [isLoadingCurrentTurnPreview, setIsLoadingCurrentTurnPreview] = useState(false) const [isUndoingCurrentTurn, setIsUndoingCurrentTurn] = useState(false) + const [currentTurnUndoConfirmOpen, setCurrentTurnUndoConfirmOpen] = useState(false) const updateAutoScrollState = useCallback(() => { const container = scrollContainerRef.current @@ -438,6 +440,7 @@ export function MessageList({ sessionId, compact = false }: MessageListProps = { }) setCurrentTurnPreview(null) + setCurrentTurnUndoConfirmOpen(false) } catch (error) { const message = error instanceof ApiError @@ -448,6 +451,7 @@ export function MessageList({ sessionId, compact = false }: MessageListProps = { ? error.message : String(error) setCurrentTurnError(message) + setCurrentTurnUndoConfirmOpen(false) } finally { setIsUndoingCurrentTurn(false) } @@ -545,13 +549,13 @@ export function MessageList({ sessionId, compact = false }: MessageListProps = { error={currentTurnError} isUndoing={isUndoingCurrentTurn} onUndo={() => { - void handleUndoCurrentTurn() + setCurrentTurnUndoConfirmOpen(true) }} /> )} {!currentTurnPreview && currentTurnError && ( -
+
{currentTurnError}
)} @@ -671,6 +675,22 @@ export function MessageList({ sessionId, compact = false }: MessageListProps = { )}
+ + { + if (!isUndoingCurrentTurn) { + setCurrentTurnUndoConfirmOpen(false) + } + }} + onConfirm={handleUndoCurrentTurn} + title={t('chat.turnChangesConfirmTitle')} + body={t('chat.turnChangesConfirmBody')} + confirmLabel={t('chat.turnChangesConfirmUndo')} + cancelLabel={t('common.cancel')} + confirmVariant="danger" + loading={isUndoingCurrentTurn} + />
) } diff --git a/desktop/src/components/workspace/WorkspaceCodeSurface.tsx b/desktop/src/components/workspace/WorkspaceCodeSurface.tsx new file mode 100644 index 00000000..601e4d2f --- /dev/null +++ b/desktop/src/components/workspace/WorkspaceCodeSurface.tsx @@ -0,0 +1,174 @@ +import { Highlight, type PrismTheme } from 'prism-react-renderer' +import { useTranslation } from '../../i18n' + +export const WORKSPACE_PREVIEW_LINE_LIMIT = 420 + +export const workspacePrismTheme: PrismTheme = { + plain: { + color: 'var(--color-code-fg)', + backgroundColor: 'transparent', + }, + styles: [ + { types: ['comment', 'prolog', 'doctype', 'cdata'], style: { color: 'var(--color-code-comment)', fontStyle: 'italic' } }, + { types: ['string', 'attr-value', 'template-string'], style: { color: 'var(--color-code-string)' } }, + { types: ['keyword', 'selector', 'important', 'atrule'], style: { color: 'var(--color-code-keyword)' } }, + { types: ['function'], style: { color: 'var(--color-code-function)' } }, + { types: ['tag'], style: { color: 'var(--color-code-keyword)' } }, + { types: ['number', 'boolean'], style: { color: 'var(--color-code-number)' } }, + { types: ['operator'], style: { color: 'var(--color-code-fg)' } }, + { types: ['punctuation'], style: { color: 'var(--color-code-punctuation)' } }, + { types: ['variable', 'parameter'], style: { color: 'var(--color-code-fg)' } }, + { types: ['property', 'attr-name'], style: { color: 'var(--color-code-property)' } }, + { types: ['builtin', 'class-name', 'constant', 'symbol'], style: { color: 'var(--color-code-type)' } }, + { types: ['inserted'], style: { color: 'var(--color-code-inserted)' } }, + { types: ['deleted'], style: { color: 'var(--color-code-deleted)' } }, + ], +} + +export function getFileExtension(name: string) { + const cleanName = name.split('/').pop() ?? name + const lastDot = cleanName.lastIndexOf('.') + if (lastDot <= 0 || lastDot === cleanName.length - 1) return '' + return cleanName.slice(lastDot + 1).toLowerCase() +} + +export function normalizePrismLanguage(language: string) { + const lower = language.toLowerCase() + const map: Record = { + text: 'text', + typescript: 'typescript', + ts: 'typescript', + tsx: 'tsx', + javascript: 'javascript', + js: 'javascript', + jsx: 'jsx', + markdown: 'markdown', + md: 'markdown', + html: 'markup', + xml: 'markup', + shell: 'bash', + sh: 'bash', + zsh: 'bash', + diff: 'diff', + } + return map[lower] ?? lower +} + +export function getLanguageFromPath(path: string) { + return normalizePrismLanguage(getFileExtension(path) || 'text') +} + +export function InlineHighlightedCode({ + value, + language, +}: { + value: string + language: string +}) { + return ( + + {({ tokens, getTokenProps }) => ( + <> + {(tokens[0] ?? []).map((token, tokenIndex) => { + const { key: tokenKey, ...tokenProps } = getTokenProps({ token, key: tokenIndex }) + return + })} + + )} + + ) +} + +export function WorkspaceDiffSurface({ + value, + path, + className = 'min-h-0 flex-1 overflow-auto bg-[var(--color-code-bg)]', + lineLimit = WORKSPACE_PREVIEW_LINE_LIMIT, +}: { + value: string + path: string + className?: string + lineLimit?: number +}) { + const t = useTranslation() + const lines = value.split('\n') + const visibleLines = lines.slice(0, lineLimit) + const hiddenLineCount = Math.max(0, lines.length - visibleLines.length) + const language = getLanguageFromPath(path) + + return ( +
+
+
+          {visibleLines.map((line, index) => {
+            const isFileHeader = line.startsWith('diff --') || line.startsWith('--- ') || line.startsWith('+++ ')
+            const isHunk = line.startsWith('@@')
+            const isAdded = line.startsWith('+') && !line.startsWith('+++')
+            const isRemoved = line.startsWith('-') && !line.startsWith('---')
+            const isCodeLine = isAdded || isRemoved || line.startsWith(' ')
+            const code = isCodeLine ? line.slice(1) : line
+            const prefix = isCodeLine ? line[0] : ' '
+
+            return (
+              
+ + {index + 1} + + + {prefix} + + + {isCodeLine ? ( + code ? : ' ' + ) : ( + code || ' ' + )} + +
+ ) + })} +
+ {hiddenLineCount > 0 && ( +
+ {t('workspace.previewLineLimit', { count: lineLimit })} +
+ )} +
+
+ ) +} diff --git a/desktop/src/components/workspace/WorkspacePanel.test.tsx b/desktop/src/components/workspace/WorkspacePanel.test.tsx index 5ace9434..e72d397f 100644 --- a/desktop/src/components/workspace/WorkspacePanel.test.tsx +++ b/desktop/src/components/workspace/WorkspacePanel.test.tsx @@ -587,6 +587,86 @@ describe('WorkspacePanel', () => { expect(view.queryByTestId('workspace-code')).toBeNull() }) + it('opens a context menu for preview tabs and closes tabs to the right', async () => { + await setWorkspaceState((state) => ({ + ...state, + panelBySession: { + ...state.panelBySession, + 'session-preview-menu': { + isOpen: true, + activeView: 'changed', + }, + }, + statusBySession: { + ...state.statusBySession, + 'session-preview-menu': { + state: 'ok', + workDir: '/repo', + repoName: 'repo', + branch: 'main', + isGitRepo: true, + changedFiles: [], + }, + }, + previewTabsBySession: { + ...state.previewTabsBySession, + 'session-preview-menu': [ + { + id: 'file:src/App.jsx', + path: 'src/App.jsx', + kind: 'file', + title: 'App.jsx', + language: 'jsx', + content: 'app', + state: 'ok', + size: 3, + }, + { + id: 'diff:vite.config.js', + path: 'vite.config.js', + kind: 'diff', + title: 'vite.config.js', + diff: '@@ -1 +1 @@', + state: 'ok', + size: 12, + }, + { + id: 'file:src/index.css', + path: 'src/index.css', + kind: 'file', + title: 'index.css', + language: 'css', + content: 'body{}', + state: 'ok', + size: 6, + }, + ], + }, + activePreviewTabIdBySession: { + ...state.activePreviewTabIdBySession, + 'session-preview-menu': 'diff:vite.config.js', + }, + })) + + const view = await renderPanel('session-preview-menu') + + await act(() => { + fireEvent.contextMenu(view.getByRole('tab', { name: /vite\.config\.js/i }), { + clientX: 320, + clientY: 42, + }) + }) + + await clickElement(view.getByRole('menuitem', { name: 'Close to the Right' })) + + expect(useWorkspacePanelStore.getState().previewTabsBySession['session-preview-menu']).toMatchObject([ + { id: 'file:src/App.jsx' }, + { id: 'diff:vite.config.js' }, + ]) + expect(useWorkspacePanelStore.getState().activePreviewTabIdBySession['session-preview-menu']).toBe('diff:vite.config.js') + expect(view.queryByRole('tab', { name: /index\.css/i })).toBeNull() + }) + it('uses the localized view menu label', async () => { await setSettingsState({ ...settingsInitialState, locale: 'zh' }) await setWorkspaceState((state) => ({ diff --git a/desktop/src/components/workspace/WorkspacePanel.tsx b/desktop/src/components/workspace/WorkspacePanel.tsx index 7f6d3b37..c1a8302c 100644 --- a/desktop/src/components/workspace/WorkspacePanel.tsx +++ b/desktop/src/components/workspace/WorkspacePanel.tsx @@ -1,5 +1,5 @@ -import { useEffect, useMemo, useState } from 'react' -import { Highlight, type PrismTheme } from 'prism-react-renderer' +import { useEffect, useMemo, useState, type MouseEvent } from 'react' +import { Highlight } from 'prism-react-renderer' import type { WorkspaceChangedFile, WorkspaceFileStatus, @@ -10,10 +10,18 @@ import { useTranslation } from '../../i18n' import { useShallow } from 'zustand/react/shallow' import { useWorkspacePanelStore, + type WorkspacePreviewCloseScope, type WorkspacePreviewKind, type WorkspacePreviewTab, } from '../../stores/workspacePanelStore' import { MarkdownRenderer } from '../markdown/MarkdownRenderer' +import { + getFileExtension, + normalizePrismLanguage, + WORKSPACE_PREVIEW_LINE_LIMIT, + WorkspaceDiffSurface, + workspacePrismTheme, +} from './WorkspaceCodeSurface' type WorkspacePanelProps = { sessionId: string @@ -71,44 +79,20 @@ const FILE_STATUS_META: Record = {} const EMPTY_PREVIEW_TABS: WorkspacePreviewTab[] = [] const EMPTY_EXPANDED_PATHS: string[] = [] -const PREVIEW_LINE_LIMIT = 420 - -const workspacePrismTheme: PrismTheme = { - plain: { - color: 'var(--color-code-fg)', - backgroundColor: 'transparent', - }, - styles: [ - { types: ['comment', 'prolog', 'doctype', 'cdata'], style: { color: 'var(--color-code-comment)', fontStyle: 'italic' } }, - { types: ['string', 'attr-value', 'template-string'], style: { color: 'var(--color-code-string)' } }, - { types: ['keyword', 'selector', 'important', 'atrule'], style: { color: 'var(--color-code-keyword)' } }, - { types: ['function'], style: { color: 'var(--color-code-function)' } }, - { types: ['tag'], style: { color: 'var(--color-code-keyword)' } }, - { types: ['number', 'boolean'], style: { color: 'var(--color-code-number)' } }, - { types: ['operator'], style: { color: 'var(--color-code-fg)' } }, - { types: ['punctuation'], style: { color: 'var(--color-code-punctuation)' } }, - { types: ['variable', 'parameter'], style: { color: 'var(--color-code-fg)' } }, - { types: ['property', 'attr-name'], style: { color: 'var(--color-code-property)' } }, - { types: ['builtin', 'class-name', 'constant', 'symbol'], style: { color: 'var(--color-code-type)' } }, - { types: ['inserted'], style: { color: 'var(--color-code-inserted)' } }, - { types: ['deleted'], style: { color: 'var(--color-code-deleted)' } }, - ], -} - const FILE_BADGE_META: Record = { - ts: { label: 'TS', className: 'bg-[#dff0ff] text-[#2b86c5]' }, - tsx: { label: 'TSX', className: 'bg-[#dff0ff] text-[#2b86c5]' }, - js: { label: 'JS', className: 'bg-[#fff3bf] text-[#8a6500]' }, - jsx: { label: 'JSX', className: 'bg-[#fff3bf] text-[#8a6500]' }, - json: { label: '{}', className: 'bg-[#eee9ff] text-[#6f4fb8]' }, - md: { label: 'MD', className: 'bg-[#e9eef3] text-[#5e6872]' }, - css: { label: 'CSS', className: 'bg-[#e4f2ff] text-[#246da6]' }, - html: { label: 'H', className: 'bg-[#ffe7dc] text-[#b9552d]' }, - png: { label: 'IMG', className: 'bg-[#e4f7ed] text-[#287747]' }, - jpg: { label: 'IMG', className: 'bg-[#e4f7ed] text-[#287747]' }, - jpeg: { label: 'IMG', className: 'bg-[#e4f7ed] text-[#287747]' }, - gif: { label: 'IMG', className: 'bg-[#e4f7ed] text-[#287747]' }, - svg: { label: 'SVG', className: 'bg-[#e4f7ed] text-[#287747]' }, + ts: { label: 'TS', className: 'bg-[var(--color-secondary)]/14 text-[var(--color-secondary)]' }, + tsx: { label: 'TSX', className: 'bg-[var(--color-secondary)]/14 text-[var(--color-secondary)]' }, + js: { label: 'JS', className: 'bg-[var(--color-warning)]/16 text-[var(--color-warning)]' }, + jsx: { label: 'JSX', className: 'bg-[var(--color-warning)]/16 text-[var(--color-warning)]' }, + json: { label: '{}', className: 'bg-[var(--color-tertiary)]/14 text-[var(--color-tertiary)]' }, + md: { label: 'MD', className: 'bg-[var(--color-text-tertiary)]/14 text-[var(--color-text-secondary)]' }, + css: { label: 'CSS', className: 'bg-[var(--color-secondary)]/14 text-[var(--color-secondary)]' }, + html: { label: 'H', className: 'bg-[var(--color-brand)]/14 text-[var(--color-brand)]' }, + png: { label: 'IMG', className: 'bg-[var(--color-success)]/14 text-[var(--color-success)]' }, + jpg: { label: 'IMG', className: 'bg-[var(--color-success)]/14 text-[var(--color-success)]' }, + jpeg: { label: 'IMG', className: 'bg-[var(--color-success)]/14 text-[var(--color-success)]' }, + gif: { label: 'IMG', className: 'bg-[var(--color-success)]/14 text-[var(--color-success)]' }, + svg: { label: 'SVG', className: 'bg-[var(--color-success)]/14 text-[var(--color-success)]' }, } function makeTreeStateKey(sessionId: string, path: string) { @@ -136,47 +120,14 @@ function getPreviewKindLabel( return kind === 'diff' ? t('workspace.previewKind.diff') : t('workspace.previewKind.file') } -function getFileExtension(name: string) { - const cleanName = name.split('/').pop() ?? name - const lastDot = cleanName.lastIndexOf('.') - if (lastDot <= 0 || lastDot === cleanName.length - 1) return '' - return cleanName.slice(lastDot + 1).toLowerCase() -} - function getFileBadgeMeta(name: string) { const extension = getFileExtension(name) return FILE_BADGE_META[extension] ?? { label: extension ? extension.slice(0, 3).toUpperCase() : 'TXT', - className: 'bg-[#eef0f2] text-[#747b83]', + className: 'bg-[var(--color-text-tertiary)]/12 text-[var(--color-text-secondary)]', } } -function normalizePrismLanguage(language: string) { - const lower = language.toLowerCase() - const map: Record = { - text: 'text', - typescript: 'typescript', - ts: 'typescript', - tsx: 'tsx', - javascript: 'javascript', - js: 'javascript', - jsx: 'jsx', - markdown: 'markdown', - md: 'markdown', - html: 'markup', - xml: 'markup', - shell: 'bash', - sh: 'bash', - zsh: 'bash', - diff: 'diff', - } - return map[lower] ?? lower -} - -function getLanguageFromPath(path: string) { - return normalizePrismLanguage(getFileExtension(path) || 'text') -} - function isMarkdownPreview(tab: WorkspacePreviewTab) { if (tab.kind !== 'file') return false const language = (tab.language ?? '').toLowerCase() @@ -196,31 +147,6 @@ function FileTypeBadge({ name, subtle = false }: { name: string; subtle?: boolea ) } -function InlineHighlightedCode({ - value, - language, -}: { - value: string - language: string -}) { - return ( - - {({ tokens, getTokenProps }) => ( - <> - {(tokens[0] ?? []).map((token, tokenIndex) => { - const { key: tokenKey, ...tokenProps } = getTokenProps({ token, key: tokenIndex }) - return - })} - - )} - - ) -} - function getInlineStateMessage( t: ReturnType, state: WorkspacePreviewTab['state'] | WorkspaceTreeResult['state'] | 'not_git_repo' | undefined, @@ -316,7 +242,7 @@ function ToolbarIconButton({ type="button" aria-label={label} onClick={onClick} - className="inline-flex h-8 w-8 items-center justify-center rounded-[9px] text-[#777c83] transition-colors hover:bg-[#f1f1f1] hover:text-[#272a2e] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[#0a96ff]/45" + className="inline-flex h-8 w-8 items-center justify-center rounded-[9px] text-[var(--color-text-tertiary)] transition-colors hover:bg-[var(--color-surface-hover)] hover:text-[var(--color-text-primary)] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--color-brand)]/35" > {icon} @@ -333,22 +259,22 @@ function WorkspaceFilterInput({ const t = useTranslation() return ( -
-