fix: simplify tool execution records in conversations

Replace full-width tool cards with compact disclosure rows in conversations and subagent progress. Remove redundant runtime summaries, keep expansion under user control, and show successful calls with green checks.

Prioritize errors and results, fold inputs, and provide original-text copying through application notifications. Detail text wraps in bounded scroll areas with no extra display controls.

Validation: 3927 tests passed, 66 skipped; typecheck and lint passed.

Release note: Tool activity is now more compact, with clearer status icons and less repeated text. Expand a record to read and copy results, errors, or input parameters.
This commit is contained in:
mesalogo
2026-09-11 14:54:11 +08:00
parent f6287589a3
commit fc36433d76
9 changed files with 371 additions and 83 deletions
+2
View File
@@ -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 内复制通知样式。
+7 -4
View File
@@ -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: "重新编辑并发送" }),
+3 -3
View File
@@ -824,7 +824,7 @@ function ChatHistoryPane({
conversationHtmlRenderingEnabled: boolean;
conversation: Conversation;
locale: TimeFormatLocale;
onCopyMessage: (content: string) => Promise<void>;
onCopyMessage: (content: string, kind?: 'tool') => Promise<void>;
onDownloadImage: (item: ImageViewerItem) => void;
onOpenCitationContext: (reference: KnowledgeSearchReference) => Promise<void>;
onOpenCitationSource: (reference: KnowledgeSearchReference) => Promise<void>;
@@ -6623,8 +6623,8 @@ function App(): React.JSX.Element {
};
const copyMessage = useCallback(
(content: string): Promise<void> =>
writeClipboardText(content, t("notices.messageCopied")),
(content: string, kind?: 'tool'): Promise<void> =>
writeClipboardText(content, t(kind === 'tool' ? "chat.tools.copied" : "notices.messageCopied")),
[t, writeClipboardText],
);
+110
View File
@@ -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(<ChatTimeline
artifactById={new Map()} conversationId="tools" hiddenMessageCount={0}
isUnusedConversation={false} locale="zh-CN" messageStartIndex={0}
{...callbacks} retryContent="" totalMessageCount={1}
messages={[{
id: 'tool-message', role: 'assistant', content: '', createdAt: 0, state: 'complete',
tools: [{ callId: 'call-1', name, summary, state: 'completed' }]
}]}
/>)
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(<ChatTimeline {...props} messages={[message]} />)
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(<ChatTimeline {...props} messages={[{
...message, tools: [{ ...message.tools![0]!, state: 'completed' }]
}]} />)
expect(card).toHaveAttribute('open')
expect(screen.getByText('执行完成,无返回内容。')).toBeVisible()
fireEvent.click(card.querySelector('summary')!)
view.rerender(<ChatTimeline {...props} messages={[{
...message, tools: [{ ...message.tools![0]!, output: 'late result' }]
}]} />)
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: '<div>partial output</div>', error: 'Read failed\nMore context' }]
}
const view = render(<ChatTimeline {...props} messages={[message]} />)
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', '<div>partial output</div>', 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('<div>partial output</div>', '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(<ChatTimeline {...props} messages={[{
...message, tools: [{ ...message.tools![0]!, input: '{"incomplete":' }]
}]} />)
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(),
+88 -41
View File
@@ -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<void>
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 (
<section className="tool-detail" aria-label={label}>
<header className="tool-detail__toolbar">
<strong>{label}</strong>
<button type="button" aria-label={t('chat.tools.copy', { label })} onClick={() => { void onCopy(content, 'tool') }}>
<Copy aria-hidden="true" size={13} />
{t('chat.tools.copyAction')}
</button>
</header>
<pre tabIndex={0} aria-label={label}>
{display}
</pre>
</section>
)
}
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"
>
<header className="tool-execution-list__header">
<TerminalSquare aria-hidden="true" size={15} />
<strong>{t('chat.tools.title')}</strong>
<small>{t('chat.tools.count', { count: tools.length })}</small>
</header>
<ol>
{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 (
<li key={tool.callId ?? tool.name}>
<details
className={`tool-execution tool-execution--${tool.state}`}
open={
tool.state === 'pending' || tool.state === 'running'
? true
: undefined
}
>
<summary>
<ChevronRight aria-hidden="true" size={14} className="tool-execution__chevron" />
<span className="tool-execution__identity">
<strong>{tool.name}</strong>
<span>{tool.summary}</span>
<strong title={tool.name}>{tool.name}</strong>
{hasSummary && <span title={tool.summary}>{tool.summary}</span>}
</span>
<small
aria-label={t(`chat.tools.states.${tool.state}`)}
title={t(`chat.tools.states.${tool.state}`)}
className="tool-execution__status"
>
{t(`chat.tools.states.${tool.state}`)}
<StateIcon aria-hidden="true" size={13} />
{(tool.state === 'failed' || tool.state === 'recoverable' || tool.state === 'cancelled') && t(`chat.tools.states.${tool.state}`)}
</small>
</summary>
<div className="tool-execution__details">
{tool.input && (
<section>
<strong>{t('chat.tools.input')}</strong>
<pre>{tool.input}</pre>
</section>
)}
{tool.output && (
<section>
<strong>{t('chat.tools.output')}</strong>
<pre>{tool.output}</pre>
</section>
)}
{hasSummary && <p className="tool-execution__full-summary">{tool.summary}</p>}
{tool.error && (
<section className="tool-execution__error">
<strong>{t('chat.tools.error')}</strong>
<pre>{tool.error}</pre>
</section>
<div className="tool-execution__error">
<ToolDetail label={t('chat.tools.error')} content={tool.error} onCopy={onCopy} />
</div>
)}
{tool.output ? (
<ToolDetail label={t('chat.tools.output')} content={tool.output} onCopy={onCopy} />
) : !tool.error && (
<p>{t(tool.state === 'running' ? 'chat.tools.waitingOutput'
: tool.state === 'pending' ? 'chat.tools.waitingStart'
: tool.state === 'completed' ? 'chat.tools.emptyOutput' : 'chat.tools.noDetails')}</p>
)}
{tool.input && (
<details className="tool-execution__input">
<summary>{t('chat.tools.input')}</summary>
<ToolDetail label={t('chat.tools.input')} content={tool.input} formatJson onCopy={onCopy} />
</details>
)}
{!hasDetails && <p>{t('chat.tools.noDetails')}</p>}
</div>
</details>
{tool.error && (
<p className="tool-execution__error-preview" title={tool.error}>{tool.error}</p>
)}
</li>
)
})}
@@ -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({
<div className="subagent-status-card__progress">
{groupMessageBlocks(progress).map((item) =>
item.kind === 'tools' ? (
<ToolExecutionList key={item.id} tools={item.tools.map((tool) =>
<ToolExecutionList key={item.id} onCopy={onCopy} tools={item.tools.map((tool) =>
(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}
/>
))}
</section>
@@ -435,7 +480,7 @@ type ChatMessageRowProps = {
message: Message
renderAssistantHtml?: boolean
onArticleRef: (messageId: string, element: HTMLElement | null) => void
onCopyMessage: (content: string) => Promise<void>
onCopyMessage: CopyContent
onDownloadImage: (item: ImageViewerItem) => void
onOpenCitationContext: (
reference: KnowledgeSearchReference
@@ -665,9 +710,10 @@ function ChatMessageRowView({
<div className="message-blocks">
{groupMessageBlocks(message.blocks).map((item) =>
item.kind === 'tools' ? (
<ToolExecutionList key={item.id} tools={item.tools} />
<ToolExecutionList key={item.id} tools={item.tools} onCopy={onCopyMessage} />
) : item.kind === 'subagents' ? (
<SubagentStatusList
onCopy={onCopyMessage}
key={item.id}
renderHtml={renderAssistantHtml}
subagents={item.childTaskIds.flatMap((childTaskId) => {
@@ -725,6 +771,7 @@ function ChatMessageRowView({
)}
{unorderedSubagents && unorderedSubagents.length > 0 && (
<SubagentStatusList
onCopy={onCopyMessage}
renderHtml={renderAssistantHtml}
subagents={unorderedSubagents}
/>
@@ -929,7 +976,7 @@ function ChatMessageRowView({
{(!message.blocks || message.blocks.length === 0) &&
message.tools &&
message.tools.length > 0 && (
<ToolExecutionList tools={message.tools} />
<ToolExecutionList tools={message.tools} onCopy={onCopyMessage} />
)}
{message.approval && (
<div className="approval-card">
@@ -1187,7 +1234,7 @@ type ChatTimelineProps = {
messageStartIndex: number
messages: Message[]
onArticleRef: (messageId: string, element: HTMLElement | null) => void
onCopyMessage: (content: string) => Promise<void>
onCopyMessage: CopyContent
onDownloadImage: (item: ImageViewerItem) => void
onOpenCitationContext: (
reference: KnowledgeSearchReference
@@ -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',
@@ -350,6 +350,12 @@ export const app = {
output: '执行结果',
error: '错误详情',
noDetails: '暂时没有可显示的执行详情。',
waitingStart: '等待执行,尚无结果。',
waitingOutput: '执行中,尚无结果。',
emptyOutput: '执行完成,无返回内容。',
copyAction: '复制',
copy: '复制{{label}}',
copied: '已复制工具详情',
states: {
pending: '等待中',
running: '进行中',
+141 -35
View File
@@ -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; }
+8
View File
@@ -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') {