diff --git a/UI-DESIGN.md b/UI-DESIGN.md index 92ac6946..5b31d50e 100644 --- a/UI-DESIGN.md +++ b/UI-DESIGN.md @@ -611,6 +611,8 @@ GoodBuddy 是可调整窗口大小的桌面应用。响应式设计优先保证 - Runtime Prompt 快捷操作只把模板填入输入草稿,用户可以继续编辑;OpenCode Command 由 Runtime 原生 API 执行,输入框只承载可选参数,不以普通斜杠文本冒充执行。 - OpenCode 原生 Task 调用在本地项目和托管 SSH 项目中统一解析为子 Agent 状态卡,不显示为普通工具卡。状态卡持续显示 Agent 类型、任务说明、运行状态、完成输出或失败原因;身份徽标、Agent 名称和状态位于首层,任务说明作为主信息,Runtime 来源作为次级元数据,输出与错误在同一卡片内按需展开。父 Agent 的推理不冒充子 Agent 过程,必须与状态卡保持明确间距。状态不能只靠颜色表达。推理、子 Agent、工具和正文统一按事件首次出现顺序进入消息块;增量更新只更新原位置,Task 临时工具块重分类后必须原位替换,不能把子 Agent 卡统一提升到消息顶部。同一调用的增量事件按稳定调用 ID 更新,不能因 ACP 首帧尚无参数而永久保留重复工具卡或活动记录。一次请求内已观察到的子 Agent 均保留在消息与活动记录中;父请求取消、失败或意外中断时仍处于活动状态的卡片必须结算为对应终态,父请求成功结束但子 Agent 未报告终态时必须显示为失败而不是推断为完成。 - Agent 回复进行中锁定模型、专家角色、工作模式和 Runtime 定制选择器,并关闭已打开的上下文菜单;回复结束或停止后再恢复选择,避免界面状态与本次运行实际使用的上下文不一致。 +- 普通工具在主会话和子 Agent 过程中使用同一轻量折叠记录:不显示外层“工具执行”标题、计数、背景和边框,保留无障碍区域名称。记录纵向左对齐,头部按内容收宽且不超过消息宽度;展开箭头在左,工具名作为主标题,有额外信息的单行摘要弱化显示。右侧以绿色对勾显示成功完成,以图标显示运行和待执行状态,提供悬停文字和无障碍名称;失败和可恢复状态保留文字。仅重复工具名和执行状态的已知 Runtime 模板摘要在头部与详情中均隐藏;刷新重试、审批等额外提示保留。默认折叠,状态和输出更新不改变用户的展开选择;折叠时露出一行错误,展开详情使用消息可用宽度、浅背景和左侧细线,按错误、结果、独立折叠的输入参数阅读。详情保留原始文本,完整 JSON 输入可缩进显示,复制仍使用收到的原文,并通过应用通知反馈。详情工具栏只保留复制按钮,文本默认自动换行,长内容在固定最大高度内滚动,不提供换行切换和展开全部按钮。 +- 工具取消后保留“已取消”文字,避免与失败图标混淆。 - Agent 回复进行中不能禁用普通消息发送。新消息与到期 Scheduled Task 共用 Conversation 级待发送队列。队列位于 Composer 容器之外、与输入框等宽并保持独立间距;有待发送项时直接显示无标题栏的极简列表,空队列不保留占位。每项固定为一行,按进入顺序显示轻量来源图标和截断摘要;普通消息使用消息图标,Scheduled Task 统一使用淡出时钟图标。 - 待发送项默认在当前回复结束后顺序执行。每项提供明确的“立即中断并插入”(空闲时为“立即运行”)和删除操作;前者先取消当前 Conversation 的活动执行,再将所选项提升为下一项,不允许同一 Conversation 并发写入时间线。 - 队列区域向上增长并设置有界滚动高度,不提供展开、收起或冗余计数标题。宽容器中的操作采用弱化的行内样式,不渲染成强调卡片或胶囊;窄容器中保留来源、摘要和图标操作并隐藏冗长按钮文字。区域、来源和每个操作必须有可读无障碍名称。队列异步错误进入应用通知,不在 Composer 内复制通知样式。 diff --git a/src/renderer/src/App.test.tsx b/src/renderer/src/App.test.tsx index 98753b9d..11082776 100644 --- a/src/renderer/src/App.test.tsx +++ b/src/renderer/src/App.test.tsx @@ -3631,11 +3631,11 @@ describe("App", () => { expect(orderedBlocks).toEqual([ expect.stringContaining("这是回答内容"), expect.stringContaining("先检查项目结构"), - expect.stringContaining("OpenCode 工具:read"), + expect.stringContaining("read"), expect.stringContaining("再检查关键文件"), expect.stringContaining("最终结论"), ]); - expect(screen.getAllByText("OpenCode 工具:read")).toHaveLength(1); + expect(screen.queryByText("OpenCode 工具:read")).not.toBeInTheDocument(); expect( screen.getByRole("region", { name: "工具执行,共 2 项" }), ).toBeInTheDocument(); @@ -3648,13 +3648,16 @@ describe("App", () => { ).not.toBeVisible(); fireEvent.click(screen.getByText("read").closest("summary")!); expect(readTool).toHaveAttribute("open"); - expect(within(readTool!).getByText("调用参数")).toBeVisible(); + expect(within(readTool!).getByText("调用参数", { selector: "summary" })).toBeVisible(); expect( within(readTool!).getByText( (_, element) => element?.textContent === rawToolOutput, ), ).toBeVisible(); const activeReasoning = screen.getAllByText("正在推理"); + fireEvent.click(within(readTool!).getByRole("button", { name: "复制执行结果" })); + await waitFor(() => expect(api.clipboard.writeText).toHaveBeenCalledWith(rawToolOutput)); + expect(await screen.findByText("已复制工具详情")).toBeVisible(); expect(activeReasoning).toHaveLength(2); for (const reasoning of activeReasoning) { expect(reasoning.closest("details")).toHaveAttribute("open"); @@ -4652,7 +4655,7 @@ describe("App", () => { }); }); - expect(screen.getByText(toolError)).toBeInTheDocument(); + expect(screen.getByText(toolError, { selector: ".tool-execution__error-preview" })).toBeVisible(); expect(screen.queryByText(runtimeError)).not.toBeInTheDocument(); expect( screen.getByRole("button", { name: "重新编辑并发送" }), diff --git a/src/renderer/src/App.tsx b/src/renderer/src/App.tsx index 1764b2ee..1d67cfec 100644 --- a/src/renderer/src/App.tsx +++ b/src/renderer/src/App.tsx @@ -824,7 +824,7 @@ function ChatHistoryPane({ conversationHtmlRenderingEnabled: boolean; conversation: Conversation; locale: TimeFormatLocale; - onCopyMessage: (content: string) => Promise; + onCopyMessage: (content: string, kind?: 'tool') => Promise; onDownloadImage: (item: ImageViewerItem) => void; onOpenCitationContext: (reference: KnowledgeSearchReference) => Promise; onOpenCitationSource: (reference: KnowledgeSearchReference) => Promise; @@ -6623,8 +6623,8 @@ function App(): React.JSX.Element { }; const copyMessage = useCallback( - (content: string): Promise => - writeClipboardText(content, t("notices.messageCopied")), + (content: string, kind?: 'tool'): Promise => + writeClipboardText(content, t(kind === 'tool' ? "chat.tools.copied" : "notices.messageCopied")), [t, writeClipboardText], ); diff --git a/src/renderer/src/ChatTimeline.test.tsx b/src/renderer/src/ChatTimeline.test.tsx index 224ab887..aa55188a 100644 --- a/src/renderer/src/ChatTimeline.test.tsx +++ b/src/renderer/src/ChatTimeline.test.tsx @@ -61,6 +61,116 @@ function createMessages(): Message[] { } describe('ChatTimeline', () => { + it.each([ + ['read', 'OpenCode 工具:read', false], + ['read', 'Continue 工具:read', false], + ['read', 'DeepSeek Harness 工具:read', false], + ['read', '远端 Runtime 工具:read', false], + ['浏览器导航', '直连模型工具:浏览器导航', false], + ['浏览器导航', '正在执行直连模型工具:浏览器导航', false], + ['浏览器导航', '直连模型工具已完成:浏览器导航', false], + ['浏览器导航', '直连模型工具执行失败:浏览器导航', false], + ['read', ' OpenCode 工具: read ', false], + ['read', 'OpenCode 工具:read README.md', true], + ['浏览器导航', '直连模型工具需要刷新后重试:浏览器导航', true], + ['浏览器导航', '用户拒绝了直连模型工具:浏览器导航', true], + ['read', 'Custom tool: read', true] + ])('deduplicates the tool heading for %s / %s', (name, summary, hasSummary) => { + const view = render() + const card = view.container.querySelector('.tool-execution')! + expect(view.container.querySelector('.tool-execution-list > header')).toBeNull() + const status = card.querySelector('.tool-execution__status')! + expect(status).toHaveAttribute('aria-label', '已完成') + expect(status).toHaveAttribute('title', '已完成') + expect(status.textContent).toBe('') + expect(card.querySelector('.tool-execution__identity strong')).toHaveTextContent(name) + expect(Boolean(card.querySelector('.tool-execution__identity > span'))).toBe(hasSummary) + fireEvent.click(card.querySelector('summary')!) + expect(Boolean(card.querySelector('.tool-execution__full-summary'))).toBe(hasSummary) + }) + + it('constrains tool grid columns so long summaries cannot clip controls', () => { + for (const selector of ['.tool-execution-list', '.tool-execution-list > ol', + '.tool-execution__details', '.tool-execution__details section']) { + const rule = stylesheet.slice(stylesheet.indexOf(`${selector} {`)).split('}')[0] + expect(rule).toContain('grid-template-columns: minmax(0, 1fr)') + } + }) + + it('keeps tool expansion under user control through streaming and completion', () => { + const props = { + artifactById: new Map(), conversationId: 'tools', + hiddenMessageCount: 0, isUnusedConversation: false, locale: 'zh-CN' as const, + messageStartIndex: 0, ...callbacks, retryContent: '', totalMessageCount: 1 + } + const message: Message = { + id: 'tool-message', role: 'assistant', content: '', createdAt: 0, state: 'streaming', + tools: [{ callId: 'read-1', name: 'read', summary: 'read', state: 'running' }] + } + const view = render() + const card = view.container.querySelector('.tool-execution')! + expect(card).not.toHaveAttribute('open') + expect(within(card as HTMLElement).getAllByText('read')).toHaveLength(1) + fireEvent.click(card.querySelector('summary')!) + expect(card).toHaveAttribute('open') + expect(screen.getByText('执行中,尚无结果。')).toBeVisible() + view.rerender() + expect(card).toHaveAttribute('open') + expect(screen.getByText('执行完成,无返回内容。')).toBeVisible() + fireEvent.click(card.querySelector('summary')!) + view.rerender() + expect(card).not.toHaveAttribute('open') + }) + + it('prioritizes errors and results, folds JSON inputs and copies original content', () => { + const input = '{"path":"README.md","limit":20}' + const props = { + artifactById: new Map(), conversationId: 'tools', + hiddenMessageCount: 0, isUnusedConversation: false, locale: 'zh-CN' as const, + messageStartIndex: 0, ...callbacks, retryContent: '', totalMessageCount: 1 + } + const message: Message = { + id: 'tool-message', role: 'assistant', content: '', createdAt: 0, state: 'complete', + tools: [{ callId: 'read-1', name: 'read', summary: 'Read README.md', state: 'failed', + input, output: '
partial output
', error: 'Read failed\nMore context' }] + } + const view = render() + const card = view.container.querySelector('.tool-execution')! + const preview = view.container.querySelector('.tool-execution__error-preview')! + expect(preview).toHaveTextContent('Read failed') + expect(card.querySelector('.tool-execution__status')).toHaveTextContent('失败') + fireEvent.click(card.querySelector('summary')!) + expect(Array.from(card.querySelectorAll('pre')).map((pre) => pre.textContent)).toEqual([ + 'Read failed\nMore context', '
partial output
', JSON.stringify(JSON.parse(input), null, 2) + ]) + const inputCard = card.querySelector('.tool-execution__input')! + expect(inputCard).not.toHaveAttribute('open') + fireEvent.click(screen.getByRole('button', { name: '复制执行结果' })) + expect(callbacks.onCopyMessage).toHaveBeenLastCalledWith('
partial output
', 'tool') + const result = screen.getByRole('region', { name: '执行结果' }) + expect(within(result).getAllByRole('button')).toHaveLength(1) + expect(within(result).getByRole('button', { name: '复制执行结果' })).toBeVisible() + fireEvent.click(inputCard.querySelector('summary')!) + fireEvent.click(screen.getByRole('button', { name: '复制调用参数' })) + expect(callbacks.onCopyMessage).toHaveBeenLastCalledWith(input, 'tool') + view.rerender() + expect(inputCard.querySelector('pre')?.textContent).toBe('{"incomplete":') + }) + it('shows an image context limitation as a quiet persistent footer, not an alert', () => { const message: Message = { id: 'image-message', role: 'assistant', content: '', createdAt: Date.now(), diff --git a/src/renderer/src/ChatTimeline.tsx b/src/renderer/src/ChatTimeline.tsx index 5c54912b..83bee3ea 100644 --- a/src/renderer/src/ChatTimeline.tsx +++ b/src/renderer/src/ChatTimeline.tsx @@ -1,5 +1,6 @@ import { Bot, + Check, CheckCircle2, ChevronRight, CircleHelp, @@ -11,7 +12,6 @@ import { ClockFading, LoaderCircle, ShieldCheck, - TerminalSquare, UserRound, XCircle } from 'lucide-react' @@ -181,10 +181,46 @@ function MessageReasoning({ ) } +type CopyContent = (content: string, kind?: 'tool') => Promise + +function ToolDetail({ label, content, formatJson = false, onCopy }: { + label: string + content: string + formatJson?: boolean + onCopy: CopyContent +}): React.JSX.Element { + const { t } = useTranslation('app') + let display = content + if (formatJson) { + try { + display = JSON.stringify(JSON.parse(content), null, 2) + } catch { + // Truncated JSON and plain-text arguments remain readable as received. + } + } + + return ( +
+
+ {label} + +
+
+        {display}
+      
+
+ ) +} + function ToolExecutionList({ - tools + tools, + onCopy }: { tools: ToolActivity[] + onCopy: CopyContent }): React.JSX.Element { const { t } = useTranslation('app') @@ -193,59 +229,63 @@ function ToolExecutionList({ aria-label={t('chat.tools.region', { count: tools.length })} className="tool-execution-list" > -
-
    {tools.map((tool) => { - const hasDetails = Boolean( - tool.input || tool.output || tool.error + const summary = tool.summary.trim() + // Only suppress known boilerplate; recovery hints and custom summaries remain visible. + const summaryIdentity = summary.replace( + /^(?:(?:OpenCode|Continue|DeepSeek Harness|远端 Runtime) 工具|直连模型工具(?:已完成|执行失败)?|正在执行直连模型工具)\s*[::]\s*/u, + '' ) + const hasSummary = summary.length > 0 && summaryIdentity !== tool.name.trim() + const StateIcon = tool.state === 'running' ? LoaderCircle + : tool.state === 'pending' ? Clock3 + : tool.state === 'completed' ? Check : XCircle return (
  1. +
    - {tool.input && ( -
    - {t('chat.tools.input')} -
    {tool.input}
    -
    - )} - {tool.output && ( -
    - {t('chat.tools.output')} -
    {tool.output}
    -
    - )} + {hasSummary &&

    {tool.summary}

    } {tool.error && ( -
    - {t('chat.tools.error')} -
    {tool.error}
    -
    +
    + +
    + )} + {tool.output ? ( + + ) : !tool.error && ( +

    {t(tool.state === 'running' ? 'chat.tools.waitingOutput' + : tool.state === 'pending' ? 'chat.tools.waitingStart' + : tool.state === 'completed' ? 'chat.tools.emptyOutput' : 'chat.tools.noDetails')}

    + )} + {tool.input && ( +
    + {t('chat.tools.input')} + +
    )} - {!hasDetails &&

    {t('chat.tools.noDetails')}

    }
    + {tool.error && ( +

    {tool.error}

    + )}
  2. ) })} @@ -256,10 +296,12 @@ function ToolExecutionList({ const SubagentStatusCard = memo(function SubagentStatusCard({ renderHtml, - subagent + subagent, + onCopy }: { renderHtml: boolean subagent: SubagentActivity + onCopy: CopyContent }): React.JSX.Element { const { t } = useTranslation('app') const [expanded, setExpanded] = useState(false) @@ -349,7 +391,7 @@ const SubagentStatusCard = memo(function SubagentStatusCard({
    {groupMessageBlocks(progress).map((item) => item.kind === 'tools' ? ( - + (tool.state === 'pending' || tool.state === 'running') && subagent.state !== 'queued' && subagent.state !== 'running' ? { ...tool, state: subagent.state === 'cancelled' ? 'cancelled' : 'interrupted' } @@ -403,10 +445,12 @@ const SubagentStatusCard = memo(function SubagentStatusCard({ function SubagentStatusList({ renderHtml, - subagents + subagents, + onCopy }: { renderHtml: boolean subagents: SubagentActivity[] + onCopy: CopyContent }): React.JSX.Element { const { t } = useTranslation('app') @@ -420,6 +464,7 @@ function SubagentStatusList({ key={subagent.childTaskId} renderHtml={renderHtml} subagent={subagent} + onCopy={onCopy} /> ))} @@ -435,7 +480,7 @@ type ChatMessageRowProps = { message: Message renderAssistantHtml?: boolean onArticleRef: (messageId: string, element: HTMLElement | null) => void - onCopyMessage: (content: string) => Promise + onCopyMessage: CopyContent onDownloadImage: (item: ImageViewerItem) => void onOpenCitationContext: ( reference: KnowledgeSearchReference @@ -665,9 +710,10 @@ function ChatMessageRowView({
    {groupMessageBlocks(message.blocks).map((item) => item.kind === 'tools' ? ( - + ) : item.kind === 'subagents' ? ( { @@ -725,6 +771,7 @@ function ChatMessageRowView({ )} {unorderedSubagents && unorderedSubagents.length > 0 && ( @@ -929,7 +976,7 @@ function ChatMessageRowView({ {(!message.blocks || message.blocks.length === 0) && message.tools && message.tools.length > 0 && ( - + )} {message.approval && (
    @@ -1187,7 +1234,7 @@ type ChatTimelineProps = { messageStartIndex: number messages: Message[] onArticleRef: (messageId: string, element: HTMLElement | null) => void - onCopyMessage: (content: string) => Promise + onCopyMessage: CopyContent onDownloadImage: (item: ImageViewerItem) => void onOpenCitationContext: ( reference: KnowledgeSearchReference diff --git a/src/renderer/src/i18n/locales/en-US/app.ts b/src/renderer/src/i18n/locales/en-US/app.ts index 170db42e..d97f2bf3 100644 --- a/src/renderer/src/i18n/locales/en-US/app.ts +++ b/src/renderer/src/i18n/locales/en-US/app.ts @@ -368,6 +368,12 @@ export const app = { output: 'Result', error: 'Error details', noDetails: 'No execution details are available yet.', + waitingStart: 'Waiting to start; no result yet.', + waitingOutput: 'Running; no result yet.', + emptyOutput: 'Completed with no returned content.', + copyAction: 'Copy', + copy: 'Copy {{label}}', + copied: 'Tool details copied', states: { pending: 'Pending', running: 'Running', diff --git a/src/renderer/src/i18n/locales/zh-CN/app.ts b/src/renderer/src/i18n/locales/zh-CN/app.ts index 98ef1cd1..d171cc1d 100644 --- a/src/renderer/src/i18n/locales/zh-CN/app.ts +++ b/src/renderer/src/i18n/locales/zh-CN/app.ts @@ -350,6 +350,12 @@ export const app = { output: '执行结果', error: '错误详情', noDetails: '暂时没有可显示的执行详情。', + waitingStart: '等待执行,尚无结果。', + waitingOutput: '执行中,尚无结果。', + emptyOutput: '执行完成,无返回内容。', + copyAction: '复制', + copy: '复制{{label}}', + copied: '已复制工具详情', states: { pending: '等待中', running: '进行中', diff --git a/src/renderer/src/styles.css b/src/renderer/src/styles.css index dfd84b3c..9ef6195f 100644 --- a/src/renderer/src/styles.css +++ b/src/renderer/src/styles.css @@ -6163,53 +6163,35 @@ button > svg { .tool-execution-list { display: grid; + grid-template-columns: minmax(0, 1fr); + min-width: 0; margin-top: var(--space-2); - overflow: hidden; - border: 1px solid var(--border-default); - border-radius: var(--radius-card); - background: var(--surface-subtle); color: var(--text-secondary); } -.tool-execution-list__header { - display: flex; - align-items: center; - min-height: 32px; - padding: var(--space-2) var(--space-3); - gap: var(--space-2); -} - -.tool-execution-list__header strong { - color: var(--text-primary); - font-size: var(--font-body); -} - -.tool-execution-list__header small { - margin-left: auto; - color: var(--text-muted); - font-size: var(--font-caption); -} - .tool-execution-list > ol { display: grid; + grid-template-columns: minmax(0, 1fr); padding: 0; - border-top: 1px solid var(--border-subtle); margin: 0; list-style: none; } -.tool-execution-list > ol > li + li { - border-top: 1px solid var(--border-subtle); +.tool-execution-list > ol > li { + min-width: 0; } .tool-execution > summary { display: flex; align-items: center; + width: fit-content; + max-width: 100%; min-height: 36px; - padding: var(--space-2) var(--space-3); + padding: var(--space-2); + border-radius: var(--radius-control); cursor: pointer; - gap: var(--space-3); - list-style-position: inside; + gap: var(--space-2); + list-style: none; transition: background var(--motion-fast) ease-out; } @@ -6223,21 +6205,36 @@ button > svg { } .tool-execution__identity { - display: grid; + display: flex; + align-items: center; flex: 1; min-width: 0; - gap: var(--space-1); + gap: var(--space-2); } .tool-execution__identity strong { + flex: 0 1 auto; + max-width: 60%; color: var(--text-primary); - font-family: var(--font-family-mono); font-size: var(--font-body); - overflow-wrap: anywhere; + font-weight: 500; +} + +.tool-execution__identity strong:only-child { + max-width: 100%; +} + +.tool-execution__identity > *, +.tool-execution__error-preview { + min-width: 0; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; } .tool-execution__identity > span { - color: var(--text-muted); + flex: 1; + color: var(--text-secondary); font-size: var(--font-caption); } @@ -6254,12 +6251,20 @@ button > svg { .tool-execution__details { display: grid; - padding: 0 var(--space-3) var(--space-3); + grid-template-columns: minmax(0, 1fr); + min-width: 0; + margin: var(--space-1) 0 var(--space-2); + padding: var(--space-3); + border-left: 2px solid var(--border-subtle); + border-radius: var(--radius-control); + background: var(--surface-subtle); gap: var(--space-3); } .tool-execution__details section { display: grid; + grid-template-columns: minmax(0, 1fr); + min-width: 0; gap: var(--space-2); } @@ -17530,6 +17535,107 @@ details.settings-section > :not(summary) + :not(summary) { min-height: min(80px, 50%); max-height: max(50%, calc(100% - 80px)); } + +.tool-execution > summary::-webkit-details-marker { + display: none; +} + +.tool-execution__status { + display: inline-flex; + flex: none; + align-items: center; + gap: var(--space-1); +} + +.tool-execution--running > summary small { + color: var(--accent); +} + +.tool-execution--running .tool-execution__status svg { + animation: subagent-status-spin 1s linear infinite; +} + +.tool-execution__chevron { + flex: none; + transition: transform var(--motion-fast) ease-out; +} + +.tool-execution[open] > summary .tool-execution__chevron { + transform: rotate(90deg); +} + +.tool-execution__error-preview { + margin: 0; + padding: 0 var(--space-3) var(--space-2); + color: var(--danger); + font-size: var(--font-caption); +} + +.tool-execution[open] + .tool-execution__error-preview { + display: none; +} + +.tool-execution__full-summary { + white-space: pre-wrap; + overflow-wrap: anywhere; +} + +.tool-execution__input > summary { + padding: var(--space-2) 0; + cursor: pointer; + font-size: var(--font-caption); +} + +.tool-detail__toolbar { + display: flex; + flex-wrap: wrap; + align-items: center; + gap: var(--space-1); +} + +.tool-detail__toolbar strong { + margin-right: auto; +} + +.tool-detail__toolbar button { + display: inline-flex; + align-items: center; + min-height: 28px; + padding: var(--space-1) var(--space-2); + border: 1px solid transparent; + border-radius: var(--radius-control); + background: transparent; + color: var(--text-secondary); + font-size: var(--font-caption); + gap: var(--space-1); + cursor: pointer; +} + +.tool-detail__toolbar button:hover { + background: var(--accent-subtle); + color: var(--accent); +} + +.tool-detail__toolbar button:focus-visible, +.tool-execution__input > summary:focus-visible, +.tool-detail pre:focus-visible { + outline: 2px solid var(--accent); + outline-offset: -2px; +} + +.tool-execution--completed .tool-execution__status { + color: var(--success); +} + +@media (prefers-reduced-motion: reduce) { + .tool-execution--running .tool-execution__status svg { + animation: none; + } + + .tool-execution__chevron { + transition: none; + } +} .workspace-git__history { display: flex; flex-direction: column; flex: 1; min-height: 0; } .workspace-git__history[hidden] { display: none; } .workspace-git__history-dock > [role="separator"] { height: var(--space-2); flex: 0 0 auto; cursor: ns-resize; touch-action: none; } diff --git a/src/renderer/src/test-setup.ts b/src/renderer/src/test-setup.ts index 0d735671..b62a29d3 100644 --- a/src/renderer/src/test-setup.ts +++ b/src/renderer/src/test-setup.ts @@ -7,6 +7,14 @@ if (typeof Element !== 'undefined') { Element.prototype.scrollTo = vi.fn() } +if (typeof ResizeObserver === 'undefined') { + globalThis.ResizeObserver = class { + observe = vi.fn() + unobserve = vi.fn() + disconnect = vi.fn() + } +} + beforeEach(async () => { clearSshHostRemoteEnvironmentCache() if (typeof localStorage === 'undefined' || typeof document === 'undefined') {