feat: improve runtime visibility and browser interaction
This commit is contained in:
+102
-10
@@ -94,6 +94,7 @@ const api: DesktopApi = {
|
||||
})
|
||||
},
|
||||
browser: {
|
||||
interact: vi.fn(async () => {}),
|
||||
stop: vi.fn(async () => {}),
|
||||
onState: vi.fn((listener) => {
|
||||
browserListener = listener
|
||||
@@ -828,7 +829,18 @@ describe('App', () => {
|
||||
callId: 'call-1',
|
||||
name: 'read',
|
||||
state: 'running',
|
||||
summary: 'OpenCode 工具:read'
|
||||
summary: 'OpenCode 工具:read',
|
||||
input: '{"path":"README.md"}'
|
||||
})
|
||||
agentListener?.({
|
||||
requestId: request.requestId,
|
||||
type: 'tool',
|
||||
callId: 'call-2',
|
||||
name: 'grep',
|
||||
state: 'completed',
|
||||
summary: 'OpenCode 工具:grep',
|
||||
input: '{"pattern":"runtime"}',
|
||||
output: 'src/main/agent/runtime.ts'
|
||||
})
|
||||
agentListener?.({
|
||||
requestId: request.requestId,
|
||||
@@ -846,7 +858,10 @@ describe('App', () => {
|
||||
callId: 'call-1',
|
||||
name: 'read',
|
||||
state: 'completed',
|
||||
summary: 'OpenCode 工具:read'
|
||||
summary: 'OpenCode 工具:read',
|
||||
input: '{"path":"README.md"}',
|
||||
output:
|
||||
'README contents\nAuthorization: Bearer visible-token'
|
||||
})
|
||||
})
|
||||
|
||||
@@ -866,6 +881,26 @@ describe('App', () => {
|
||||
expect(
|
||||
screen.getAllByText('OpenCode 工具:read')
|
||||
).toHaveLength(1)
|
||||
expect(
|
||||
screen.getByRole('region', { name: '工具执行,共 2 项' })
|
||||
).toBeInTheDocument()
|
||||
const readTool = screen.getByText('read').closest('details')
|
||||
const rawToolOutput =
|
||||
'README contents\nAuthorization: Bearer visible-token'
|
||||
expect(readTool).not.toHaveAttribute('open')
|
||||
expect(
|
||||
screen.getByText(
|
||||
(_, element) => element?.textContent === rawToolOutput
|
||||
)
|
||||
).not.toBeVisible()
|
||||
fireEvent.click(screen.getByText('read').closest('summary')!)
|
||||
expect(readTool).toHaveAttribute('open')
|
||||
expect(within(readTool!).getByText('调用参数')).toBeVisible()
|
||||
expect(
|
||||
within(readTool!).getByText(
|
||||
(_, element) => element?.textContent === rawToolOutput
|
||||
)
|
||||
).toBeVisible()
|
||||
|
||||
act(() => {
|
||||
if (!request) {
|
||||
@@ -1040,8 +1075,19 @@ describe('App', () => {
|
||||
render(<App />)
|
||||
|
||||
fireEvent.click(await screen.findByLabelText('添加附件'))
|
||||
expect(await screen.findByText('需求说明.md')).toBeInTheDocument()
|
||||
expect(screen.getByText('页面截图.png')).toBeInTheDocument()
|
||||
const composer = screen
|
||||
.getByLabelText('向 GoodBuddy 提问')
|
||||
.closest<HTMLElement>('.composer')
|
||||
expect(composer).not.toBeNull()
|
||||
if (!composer) {
|
||||
return
|
||||
}
|
||||
expect(
|
||||
await within(composer).findByText('需求说明.md')
|
||||
).toBeInTheDocument()
|
||||
expect(
|
||||
within(composer).getByText('页面截图.png')
|
||||
).toBeInTheDocument()
|
||||
fireEvent.change(screen.getByLabelText('向 GoodBuddy 提问'), {
|
||||
target: { value: '分析这些附件' }
|
||||
})
|
||||
@@ -1130,8 +1176,17 @@ describe('App', () => {
|
||||
render(<App />)
|
||||
|
||||
fireEvent.click(await screen.findByLabelText('添加附件'))
|
||||
const composer = screen
|
||||
.getByLabelText('向 GoodBuddy 提问')
|
||||
.closest<HTMLElement>('.composer')
|
||||
expect(composer).not.toBeNull()
|
||||
if (!composer) {
|
||||
return
|
||||
}
|
||||
await waitFor(() =>
|
||||
expect(screen.getAllByText(/^参考图-\d\.png$/u)).toHaveLength(5)
|
||||
expect(
|
||||
within(composer).getAllByText(/^参考图-\d\.png$/u)
|
||||
).toHaveLength(5)
|
||||
)
|
||||
fireEvent.change(screen.getByLabelText('向 GoodBuddy 提问'), {
|
||||
target: { value: '比较这五张图片' }
|
||||
@@ -1188,8 +1243,15 @@ describe('App', () => {
|
||||
await waitFor(() =>
|
||||
expect(api.context.captureWindow).toHaveBeenCalledWith('window-2')
|
||||
)
|
||||
const composer = screen
|
||||
.getByLabelText('向 GoodBuddy 提问')
|
||||
.closest<HTMLElement>('.composer')
|
||||
expect(composer).not.toBeNull()
|
||||
if (!composer) {
|
||||
return
|
||||
}
|
||||
expect(
|
||||
await screen.findByText('窗口-Browser.jpg')
|
||||
await within(composer).findByText('窗口-Browser.jpg')
|
||||
).toBeInTheDocument()
|
||||
})
|
||||
|
||||
@@ -1335,10 +1397,22 @@ describe('App', () => {
|
||||
expect(
|
||||
await screen.findByRole('heading', { name: '工作区说明' })
|
||||
).toBeInTheDocument()
|
||||
expect(
|
||||
screen.getByRole('tab', { name: '工作区' })
|
||||
).toHaveAttribute('aria-selected', 'true')
|
||||
expect(
|
||||
screen.queryByRole('tab', { name: '预览' })
|
||||
).not.toBeInTheDocument()
|
||||
expect(api.workspace.readFile).toHaveBeenCalledWith(
|
||||
projectId,
|
||||
'README.md'
|
||||
)
|
||||
fireEvent.click(
|
||||
screen.getByRole('button', { name: '返回工作区' })
|
||||
)
|
||||
expect(
|
||||
await screen.findByRole('button', { name: 'README.md' })
|
||||
).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('opens workspace entries from their row actions', async () => {
|
||||
@@ -2986,19 +3060,31 @@ describe('App', () => {
|
||||
fireEvent.click(screen.getByLabelText('切换助手工作栏'))
|
||||
expect(sidebar).toHaveClass('assistant-sidebar--open')
|
||||
|
||||
expect(
|
||||
screen.getByRole('tab', { name: '任务中心' })
|
||||
).toHaveAttribute('aria-selected', 'true')
|
||||
expect(screen.getByText('自动化')).toBeInTheDocument()
|
||||
expect(screen.queryByText('最近任务')).not.toBeInTheDocument()
|
||||
fireEvent.click(screen.getByRole('tab', { name: '上下文' }))
|
||||
expect(
|
||||
screen.getByText('尚未添加文件、截图或剪贴板内容。')
|
||||
).toBeInTheDocument()
|
||||
fireEvent.click(screen.getByRole('tab', { name: '任务中心' }))
|
||||
fireEvent.click(screen.getByRole('tab', { name: '工作区' }))
|
||||
expect(
|
||||
screen.getByText(/查看当前和最近请求的运行状态/)
|
||||
screen.getByText(/选择文件后在当前工作区内预览/)
|
||||
).toBeInTheDocument()
|
||||
fireEvent.click(screen.getByRole('tab', { name: '成果库' }))
|
||||
fireEvent.click(screen.getByRole('tab', { name: '浏览器' }))
|
||||
expect(
|
||||
screen.getByText(/Agent 打开网页后/)
|
||||
).toBeInTheDocument()
|
||||
fireEvent.click(screen.getByRole('tab', { name: '成果' }))
|
||||
expect(screen.getByText('对话与导入成果')).toBeInTheDocument()
|
||||
expect(
|
||||
screen.getByText(/保存并预览由对话生成或手动导入/)
|
||||
screen.getByText(/查看并预览由对话生成或手动导入/)
|
||||
).toBeInTheDocument()
|
||||
expect(
|
||||
screen.queryByRole('tab', { name: '预览' })
|
||||
).not.toBeInTheDocument()
|
||||
fireEvent.click(screen.getByLabelText('关闭助手工作栏'))
|
||||
expect(sidebar).not.toHaveClass('assistant-sidebar--open')
|
||||
})
|
||||
@@ -3036,6 +3122,12 @@ describe('App', () => {
|
||||
'src',
|
||||
'data:image/jpeg;base64,/9j/2Q=='
|
||||
)
|
||||
fireEvent.click(
|
||||
screen.getByRole('button', { name: '交互' })
|
||||
)
|
||||
await waitFor(() =>
|
||||
expect(api.browser.interact).toHaveBeenCalledWith(conversationId)
|
||||
)
|
||||
fireEvent.click(
|
||||
screen.getByRole('button', { name: '停止浏览器' })
|
||||
)
|
||||
|
||||
+150
-74
@@ -471,6 +471,107 @@ function terminalizeMessageToolBlocks(
|
||||
)
|
||||
}
|
||||
|
||||
type MessageBlockRenderItem =
|
||||
| {
|
||||
kind: 'block'
|
||||
block: Exclude<ConversationMessageBlock, { type: 'tool' }>
|
||||
}
|
||||
| {
|
||||
kind: 'tools'
|
||||
id: string
|
||||
tools: ToolActivity[]
|
||||
}
|
||||
|
||||
function groupMessageBlocks(
|
||||
blocks: ConversationMessageBlock[]
|
||||
): MessageBlockRenderItem[] {
|
||||
const items: MessageBlockRenderItem[] = []
|
||||
for (const block of blocks) {
|
||||
if (block.type !== 'tool') {
|
||||
items.push({ kind: 'block', block })
|
||||
continue
|
||||
}
|
||||
const previous = items.at(-1)
|
||||
if (previous?.kind === 'tools') {
|
||||
previous.tools.push(block.tool)
|
||||
} else {
|
||||
items.push({
|
||||
kind: 'tools',
|
||||
id: block.id,
|
||||
tools: [block.tool]
|
||||
})
|
||||
}
|
||||
}
|
||||
return items
|
||||
}
|
||||
|
||||
function ToolExecutionList({
|
||||
tools
|
||||
}: {
|
||||
tools: ToolActivity[]
|
||||
}): React.JSX.Element {
|
||||
return (
|
||||
<section
|
||||
aria-label={`工具执行,共 ${tools.length} 项`}
|
||||
className="tool-execution-list"
|
||||
>
|
||||
<header className="tool-execution-list__header">
|
||||
<TerminalSquare aria-hidden="true" size={15} />
|
||||
<strong>工具执行</strong>
|
||||
<small>{tools.length} 项</small>
|
||||
</header>
|
||||
<ol>
|
||||
{tools.map((tool) => {
|
||||
const hasDetails = Boolean(
|
||||
tool.input || tool.output || tool.error
|
||||
)
|
||||
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>
|
||||
<span className="tool-execution__identity">
|
||||
<strong>{tool.name}</strong>
|
||||
<span>{tool.summary}</span>
|
||||
</span>
|
||||
<small>{toolStateLabels[tool.state]}</small>
|
||||
</summary>
|
||||
<div className="tool-execution__details">
|
||||
{tool.input && (
|
||||
<section>
|
||||
<strong>调用参数</strong>
|
||||
<pre>{tool.input}</pre>
|
||||
</section>
|
||||
)}
|
||||
{tool.output && (
|
||||
<section>
|
||||
<strong>执行结果</strong>
|
||||
<pre>{tool.output}</pre>
|
||||
</section>
|
||||
)}
|
||||
{tool.error && (
|
||||
<section className="tool-execution__error">
|
||||
<strong>错误详情</strong>
|
||||
<pre>{tool.error}</pre>
|
||||
</section>
|
||||
)}
|
||||
{!hasDetails && <p>暂时没有可显示的执行详情。</p>}
|
||||
</div>
|
||||
</details>
|
||||
</li>
|
||||
)
|
||||
})}
|
||||
</ol>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
|
||||
function createConversation(
|
||||
projectId?: string,
|
||||
runtimeSelection?: AgentRuntimeSelection
|
||||
@@ -1829,6 +1930,8 @@ function App(): React.JSX.Element {
|
||||
name: event.name,
|
||||
state: event.state,
|
||||
summary: event.summary,
|
||||
input: event.input,
|
||||
output: event.output,
|
||||
error: event.error
|
||||
}
|
||||
if (index >= 0) {
|
||||
@@ -2241,7 +2344,7 @@ function App(): React.JSX.Element {
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
if (assistantSidebarTab !== 'changes') {
|
||||
if (assistantSidebarTab !== 'workspace') {
|
||||
return
|
||||
}
|
||||
const timeout = setTimeout(() => {
|
||||
@@ -4151,14 +4254,20 @@ function App(): React.JSX.Element {
|
||||
)}
|
||||
{message.blocks && message.blocks.length > 0 ? (
|
||||
<div className="message-blocks">
|
||||
{message.blocks.map((block) =>
|
||||
block.type === 'reasoning' ? (
|
||||
{groupMessageBlocks(message.blocks).map((item) =>
|
||||
item.kind === 'tools' ? (
|
||||
<ToolExecutionList
|
||||
key={item.id}
|
||||
tools={item.tools}
|
||||
/>
|
||||
) : item.block.type === 'reasoning' ? (
|
||||
<details
|
||||
className="message-reasoning"
|
||||
key={block.id}
|
||||
key={item.block.id}
|
||||
open={
|
||||
message.state === 'streaming' &&
|
||||
message.blocks?.at(-1)?.id === block.id
|
||||
message.blocks?.at(-1)?.id ===
|
||||
item.block.id
|
||||
}
|
||||
>
|
||||
<summary>
|
||||
@@ -4168,34 +4277,18 @@ function App(): React.JSX.Element {
|
||||
</summary>
|
||||
<div className="markdown-content message-reasoning__content">
|
||||
<MarkdownRenderer>
|
||||
{block.content}
|
||||
{item.block.content}
|
||||
</MarkdownRenderer>
|
||||
</div>
|
||||
</details>
|
||||
) : block.type === 'text' ? (
|
||||
<div
|
||||
className="markdown-content message__content"
|
||||
key={block.id}
|
||||
>
|
||||
<MarkdownRenderer>
|
||||
{block.content}
|
||||
</MarkdownRenderer>
|
||||
</div>
|
||||
) : (
|
||||
<div
|
||||
className="tool-activity"
|
||||
key={block.id}
|
||||
className="markdown-content message__content"
|
||||
key={item.block.id}
|
||||
>
|
||||
<TerminalSquare size={15} />
|
||||
<div className="tool-activity__content">
|
||||
<span>{block.tool.summary}</span>
|
||||
{block.tool.error && (
|
||||
<code>{block.tool.error}</code>
|
||||
)}
|
||||
</div>
|
||||
<small>
|
||||
{toolStateLabels[block.tool.state]}
|
||||
</small>
|
||||
<MarkdownRenderer>
|
||||
{item.block.content}
|
||||
</MarkdownRenderer>
|
||||
</div>
|
||||
)
|
||||
)}
|
||||
@@ -4347,19 +4440,10 @@ function App(): React.JSX.Element {
|
||||
</details>
|
||||
)}
|
||||
{(!message.blocks || message.blocks.length === 0) &&
|
||||
message.tools?.map((tool) => (
|
||||
<div
|
||||
className="tool-activity"
|
||||
key={tool.callId ?? tool.name}
|
||||
>
|
||||
<TerminalSquare size={15} />
|
||||
<div className="tool-activity__content">
|
||||
<span>{tool.summary}</span>
|
||||
{tool.error && <code>{tool.error}</code>}
|
||||
</div>
|
||||
<small>{toolStateLabels[tool.state]}</small>
|
||||
</div>
|
||||
))}
|
||||
message.tools &&
|
||||
message.tools.length > 0 && (
|
||||
<ToolExecutionList tools={message.tools} />
|
||||
)}
|
||||
{message.subagents && message.subagents.length > 0 && (
|
||||
<section
|
||||
aria-label="子专家状态"
|
||||
@@ -5292,17 +5376,29 @@ function App(): React.JSX.Element {
|
||||
</div>
|
||||
)}
|
||||
<RightAssistantSidebar
|
||||
activities={activityRecords}
|
||||
approvals={pendingSidebarApprovals}
|
||||
artifacts={sidebarArtifacts}
|
||||
attachments={attachments}
|
||||
browserState={browserStates[activeId]}
|
||||
enabledLibraries={enabledSidebarLibraries}
|
||||
experts={assistantExperts}
|
||||
heartbeatEntries={heartbeatEntries}
|
||||
heartbeats={assistantHeartbeats}
|
||||
memories={assistantMemories}
|
||||
schedules={assistantSchedules}
|
||||
onClose={() => setAssistantSidebarOpen(false)}
|
||||
onInteractBrowser={async () => {
|
||||
if (!activeId) {
|
||||
return
|
||||
}
|
||||
const browserApi = window.goodbuddy.browser
|
||||
if (!browserApi) {
|
||||
notify({
|
||||
tone: 'error',
|
||||
message: '浏览器控制组件尚未加载,请重启 GoodBuddy'
|
||||
})
|
||||
return
|
||||
}
|
||||
await browserApi.interact(activeId)
|
||||
}}
|
||||
onStopBrowser={async () => {
|
||||
if (!activeId) {
|
||||
return
|
||||
@@ -5324,16 +5420,6 @@ function App(): React.JSX.Element {
|
||||
})
|
||||
}
|
||||
}}
|
||||
onOpenHeartbeat={() => setView('heartbeat')}
|
||||
onCreateMemory={async (content) => {
|
||||
const memory = await window.goodbuddy.memory.create({
|
||||
scope: activeProjectId ? 'project' : 'global',
|
||||
scopeId: activeProjectId || undefined,
|
||||
type: 'preference',
|
||||
content
|
||||
})
|
||||
setAssistantMemories((current) => [memory, ...current])
|
||||
}}
|
||||
onCreateHeartbeat={createHeartbeat}
|
||||
onCreateSchedule={async (input) => {
|
||||
const schedule = await window.goodbuddy.schedules.create({
|
||||
@@ -5342,7 +5428,6 @@ function App(): React.JSX.Element {
|
||||
})
|
||||
setAssistantSchedules((current) => [schedule, ...current])
|
||||
}}
|
||||
onOpenConversation={openActivityConversation}
|
||||
onImportArtifacts={async () => {
|
||||
const imported = await window.goodbuddy.artifacts.importFiles(
|
||||
activeProjectId || undefined
|
||||
@@ -5352,7 +5437,7 @@ function App(): React.JSX.Element {
|
||||
...imported,
|
||||
...current
|
||||
])
|
||||
setAssistantSidebarTab('artifacts')
|
||||
setAssistantSidebarTab('results')
|
||||
}
|
||||
}}
|
||||
onLoadArtifact={async (artifactId) => {
|
||||
@@ -5367,13 +5452,6 @@ function App(): React.JSX.Element {
|
||||
)
|
||||
}}
|
||||
onRemoveAttachment={removeAttachment}
|
||||
onRemoveMemory={async (memoryId) => {
|
||||
await window.goodbuddy.memory.remove(memoryId)
|
||||
setAssistantMemories((current) =>
|
||||
current.filter((memory) => memory.id !== memoryId)
|
||||
)
|
||||
}}
|
||||
onSetMemoryStatus={setMemoryStatus}
|
||||
onRemoveHeartbeat={removeHeartbeat}
|
||||
onRemoveSchedule={async (scheduleId) => {
|
||||
await window.goodbuddy.schedules.remove(scheduleId)
|
||||
@@ -5381,16 +5459,6 @@ function App(): React.JSX.Element {
|
||||
current.filter((schedule) => schedule.id !== scheduleId)
|
||||
)
|
||||
}}
|
||||
onRunSchedule={async (scheduleId) => {
|
||||
await window.goodbuddy.schedules.runNow(scheduleId)
|
||||
notify({ tone: 'success', message: '定时任务已开始执行' })
|
||||
}}
|
||||
onRunHeartbeat={runHeartbeat}
|
||||
onSetHeartbeatPaused={setHeartbeatPaused}
|
||||
onListWorkspaceDirectory={listWorkspaceDirectory}
|
||||
onLoadWorkspaceFile={loadWorkspaceFile}
|
||||
onOpenWorkspaceEntry={openWorkspaceEntry}
|
||||
onRefreshChanges={refreshWorkspaceChanges}
|
||||
onRespondApproval={(approval, decision) => {
|
||||
void respondToApproval(
|
||||
approval.conversationId,
|
||||
@@ -5399,13 +5467,21 @@ function App(): React.JSX.Element {
|
||||
decision
|
||||
)
|
||||
}}
|
||||
onRunHeartbeat={runHeartbeat}
|
||||
onRunSchedule={async (scheduleId) => {
|
||||
await window.goodbuddy.schedules.runNow(scheduleId)
|
||||
notify({ tone: 'success', message: '定时任务已开始执行' })
|
||||
}}
|
||||
onSetHeartbeatPaused={setHeartbeatPaused}
|
||||
onListWorkspaceDirectory={listWorkspaceDirectory}
|
||||
onLoadWorkspaceFile={loadWorkspaceFile}
|
||||
onOpenWorkspaceEntry={openWorkspaceEntry}
|
||||
onRefreshChanges={refreshWorkspaceChanges}
|
||||
onTabChange={setAssistantSidebarTab}
|
||||
open={
|
||||
assistantSidebarOpen && view === 'chat'
|
||||
}
|
||||
schedules={assistantSchedules}
|
||||
tab={assistantSidebarTab}
|
||||
tasks={assistantTasks}
|
||||
workspaceChanges={workspaceChanges}
|
||||
workspaceProjectId={activeProjectId || undefined}
|
||||
/>
|
||||
|
||||
@@ -319,10 +319,14 @@ export function McpSettingsSection(): React.JSX.Element {
|
||||
</div>
|
||||
|
||||
<p className="settings-notice">
|
||||
内置工具由 GoodBuddy 提供,不属于 MCP Server。外部 MCP Server
|
||||
及其工具具有当前用户权限,请仅添加可信服务;远程访问令牌将由系统安全存储加密。
|
||||
当前版本仅由直连模型在 Execute 模式加载这些工具,并在每次调用前请求
|
||||
GoodBuddy 审批。
|
||||
自定义 MCP 当前仅用于直连模型,新建时默认分配给直连模型,并仅在 Execute
|
||||
模式加载。内置共享 MCP 当前仅有知识库搜索,可供直连模型、OpenCode 和
|
||||
Continue 使用。Runtime 自有 MCP 配置不在此处管理。
|
||||
</p>
|
||||
<p className="settings-notice">
|
||||
内置工具由 GoodBuddy 提供,不属于 MCP Server。自定义 MCP Server
|
||||
及其工具具有当前用户权限,请仅添加可信服务;远程访问令牌将由系统安全存储加密,
|
||||
工具调用前仍需 GoodBuddy 审批。
|
||||
</p>
|
||||
{error && !editor && <p className="settings-warning">{error}</p>}
|
||||
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import { cleanup, fireEvent, render, screen } from '@testing-library/react'
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import type {
|
||||
AssistantExpert,
|
||||
AssistantTask
|
||||
} from '../../shared/assistant-contracts'
|
||||
import { RightAssistantSidebar } from './RightAssistantSidebar'
|
||||
import {
|
||||
RightAssistantSidebar,
|
||||
type AssistantSidebarTab,
|
||||
type SidebarArtifact
|
||||
} from './RightAssistantSidebar'
|
||||
|
||||
afterEach(cleanup)
|
||||
|
||||
@@ -16,58 +16,48 @@ beforeEach(() => {
|
||||
})
|
||||
|
||||
function renderSidebar({
|
||||
tasks = [],
|
||||
experts = [],
|
||||
tab = 'context',
|
||||
onCreateSchedule = vi.fn(async () => undefined)
|
||||
artifacts = [],
|
||||
onLoadArtifact = vi.fn(async () => undefined)
|
||||
}: {
|
||||
tasks?: AssistantTask[]
|
||||
experts?: AssistantExpert[]
|
||||
tab?: 'tasks' | 'context'
|
||||
onCreateSchedule?: () => Promise<void>
|
||||
tab?: AssistantSidebarTab
|
||||
artifacts?: SidebarArtifact[]
|
||||
onLoadArtifact?: (artifactId: string) => Promise<void>
|
||||
} = {}): HTMLElement {
|
||||
render(
|
||||
<RightAssistantSidebar
|
||||
activities={[]}
|
||||
approvals={[]}
|
||||
artifacts={[]}
|
||||
artifacts={artifacts}
|
||||
attachments={[]}
|
||||
enabledLibraries={[]}
|
||||
experts={experts}
|
||||
heartbeatEntries={[]}
|
||||
heartbeats={[]}
|
||||
memories={[]}
|
||||
schedules={[]}
|
||||
onClose={vi.fn()}
|
||||
onCreateHeartbeat={vi.fn(async () => undefined)}
|
||||
onCreateMemory={vi.fn(async () => undefined)}
|
||||
onCreateSchedule={onCreateSchedule}
|
||||
onCreateSchedule={vi.fn(async () => undefined)}
|
||||
onImportArtifacts={vi.fn(async () => undefined)}
|
||||
onListWorkspaceDirectory={vi.fn(async (path: string) => ({
|
||||
path,
|
||||
entries: [],
|
||||
truncated: false
|
||||
}))}
|
||||
onLoadArtifact={vi.fn(async () => undefined)}
|
||||
onLoadArtifact={onLoadArtifact}
|
||||
onLoadWorkspaceFile={vi.fn()}
|
||||
onOpenWorkspaceEntry={vi.fn(async () => undefined)}
|
||||
onOpenConversation={vi.fn()}
|
||||
onOpenHeartbeat={vi.fn()}
|
||||
onInteractBrowser={vi.fn(async () => undefined)}
|
||||
onRefreshChanges={vi.fn(async () => undefined)}
|
||||
onRemoveAttachment={vi.fn()}
|
||||
onRemoveHeartbeat={vi.fn(async () => undefined)}
|
||||
onRemoveMemory={vi.fn(async () => undefined)}
|
||||
onRemoveSchedule={vi.fn(async () => undefined)}
|
||||
onRespondApproval={vi.fn()}
|
||||
onRunHeartbeat={vi.fn(async () => undefined)}
|
||||
onRunSchedule={vi.fn(async () => undefined)}
|
||||
onSetHeartbeatPaused={vi.fn(async () => undefined)}
|
||||
onSetMemoryStatus={vi.fn(async () => undefined)}
|
||||
onStopBrowser={vi.fn(async () => undefined)}
|
||||
onTabChange={vi.fn()}
|
||||
open
|
||||
schedules={[]}
|
||||
tab={tab}
|
||||
tasks={tasks}
|
||||
/>
|
||||
)
|
||||
|
||||
@@ -165,80 +155,55 @@ describe('RightAssistantSidebar resizing', () => {
|
||||
).toBe('424px')
|
||||
})
|
||||
|
||||
it('indents child tasks and names their expert and routing mode', () => {
|
||||
const parentTask: AssistantTask = {
|
||||
id: 'parent-task',
|
||||
conversationId: 'conversation-1',
|
||||
title: '分析发布计划',
|
||||
instructions: '分析发布计划',
|
||||
origin: 'user',
|
||||
status: 'running',
|
||||
createdAt: '2026-08-01T00:00:00.000Z'
|
||||
}
|
||||
const childTask: AssistantTask = {
|
||||
id: 'child-task',
|
||||
conversationId: 'conversation-1',
|
||||
parentTaskId: parentTask.id,
|
||||
expertId: 'expert-1',
|
||||
routingMode: 'smart',
|
||||
title: '研究子任务',
|
||||
instructions: '收集资料',
|
||||
origin: 'subagent',
|
||||
status: 'completed',
|
||||
createdAt: '2026-08-01T00:01:00.000Z'
|
||||
}
|
||||
renderSidebar({
|
||||
tab: 'tasks',
|
||||
tasks: [childTask, parentTask],
|
||||
experts: [
|
||||
{
|
||||
id: 'expert-1',
|
||||
name: '研究专家',
|
||||
description: '分析证据',
|
||||
systemInstructions: 'Analyze evidence.',
|
||||
routingKeywords: ['研究'],
|
||||
enabled: true,
|
||||
createdAt: '2026-08-01T00:00:00.000Z',
|
||||
updatedAt: '2026-08-01T00:00:00.000Z'
|
||||
}
|
||||
]
|
||||
})
|
||||
it('exposes the task center and four reusable work surfaces', () => {
|
||||
renderSidebar()
|
||||
|
||||
const taskButtons = screen.getAllByRole('button', {
|
||||
name: /分析发布计划|研究子任务/u
|
||||
})
|
||||
expect(taskButtons[0]).toHaveTextContent('分析发布计划')
|
||||
expect(taskButtons[1]).toHaveClass('assistant-sidebar__row--subtask')
|
||||
expect(taskButtons[1]).toHaveTextContent('子专家:研究专家 · 智能路由')
|
||||
expect(
|
||||
screen.getAllByRole('tab').map((tab) => tab.textContent)
|
||||
).toEqual(['任务中心', '上下文', '工作区', '浏览器', '成果'])
|
||||
expect(
|
||||
screen.queryByRole('tab', { name: '预览' })
|
||||
).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('preserves schedule input and reports a failed action', async () => {
|
||||
const onCreateSchedule = vi.fn(async () => {
|
||||
throw new Error('定时服务不可用')
|
||||
})
|
||||
renderSidebar({ tab: 'tasks', onCreateSchedule })
|
||||
it('keeps automation in the task center without recent tasks', () => {
|
||||
renderSidebar({ tab: 'tasks' })
|
||||
|
||||
fireEvent.change(screen.getByLabelText('定时任务标题'), {
|
||||
target: { value: '每日摘要' }
|
||||
})
|
||||
fireEvent.change(screen.getByLabelText('定时任务内容'), {
|
||||
target: { value: '总结今天的工作' }
|
||||
})
|
||||
fireEvent.change(screen.getByLabelText('定时任务时间'), {
|
||||
target: { value: '2026-08-06T09:00' }
|
||||
})
|
||||
fireEvent.click(
|
||||
screen.getByRole('button', { name: '添加定时任务' })
|
||||
)
|
||||
expect(screen.getByText('等待审批')).toBeInTheDocument()
|
||||
expect(screen.getByText('自动化')).toBeInTheDocument()
|
||||
expect(
|
||||
screen.getByLabelText('定时任务标题')
|
||||
).toBeInTheDocument()
|
||||
expect(screen.queryByText('最近任务')).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
expect(await screen.findByRole('alert')).toHaveTextContent(
|
||||
'定时服务不可用'
|
||||
)
|
||||
expect(screen.getByLabelText('定时任务标题')).toHaveValue(
|
||||
'每日摘要'
|
||||
)
|
||||
expect(screen.getByLabelText('定时任务内容')).toHaveValue(
|
||||
'总结今天的工作'
|
||||
)
|
||||
it('previews a result without switching to a separate tab', () => {
|
||||
const onLoadArtifact = vi.fn(async () => undefined)
|
||||
renderSidebar({
|
||||
tab: 'results',
|
||||
artifacts: [
|
||||
{
|
||||
id: 'artifact-1',
|
||||
title: '发布说明',
|
||||
content: '# 发布说明',
|
||||
createdAt: Date.now(),
|
||||
mimeType: 'text/markdown'
|
||||
}
|
||||
],
|
||||
onLoadArtifact
|
||||
})
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: /发布说明/u }))
|
||||
|
||||
expect(onLoadArtifact).toHaveBeenCalledWith('artifact-1')
|
||||
expect(
|
||||
screen.getByRole('tab', { name: '成果' })
|
||||
).toHaveAttribute('aria-selected', 'true')
|
||||
expect(
|
||||
screen.getByRole('button', { name: '返回成果列表' })
|
||||
).toBeInTheDocument()
|
||||
expect(
|
||||
screen.queryByRole('tab', { name: '预览' })
|
||||
).not.toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,29 +1,25 @@
|
||||
import {
|
||||
CheckCircle2,
|
||||
ChevronLeft,
|
||||
ChevronRight,
|
||||
FileDiff,
|
||||
ExternalLink,
|
||||
FileText,
|
||||
FolderTree,
|
||||
Hourglass,
|
||||
Monitor,
|
||||
PanelRightClose,
|
||||
PlayCircle,
|
||||
RefreshCw,
|
||||
ShieldAlert,
|
||||
Upload,
|
||||
X,
|
||||
XCircle
|
||||
X
|
||||
} from 'lucide-react'
|
||||
import { useEffect, useMemo, useRef, useState } from 'react'
|
||||
import { useEffect, useRef, useState } from 'react'
|
||||
import type {
|
||||
AssistantHeartbeatConfig,
|
||||
AssistantMemory,
|
||||
AssistantSchedule,
|
||||
AssistantHeartbeatConfig,
|
||||
AssistantHeartbeatEntry,
|
||||
AssistantExpert,
|
||||
HeartbeatCreateInput,
|
||||
ScheduleCreateInput,
|
||||
AssistantTask,
|
||||
WorkspaceChanges,
|
||||
WorkspaceDirectoryListing,
|
||||
WorkspaceFilePreview
|
||||
@@ -35,17 +31,15 @@ import type {
|
||||
ContextAttachment,
|
||||
KnowledgeLibrary
|
||||
} from '../../shared/contracts'
|
||||
import type { ActivityRecord } from './activity-store'
|
||||
import { HeartbeatSettings } from './HeartbeatSettings'
|
||||
import { WorkspaceFilesPanel } from './WorkspaceFilesPanel'
|
||||
|
||||
export type AssistantSidebarTab =
|
||||
| 'tasks'
|
||||
| 'context'
|
||||
| 'artifacts'
|
||||
| 'changes'
|
||||
| 'workspace'
|
||||
| 'browser'
|
||||
| 'preview'
|
||||
| 'results'
|
||||
|
||||
export type SidebarArtifact = {
|
||||
id: string
|
||||
@@ -67,38 +61,24 @@ export type PendingSidebarApproval = {
|
||||
type RightAssistantSidebarProps = {
|
||||
open: boolean
|
||||
tab: AssistantSidebarTab
|
||||
activities: ActivityRecord[]
|
||||
tasks: AssistantTask[]
|
||||
experts?: AssistantExpert[]
|
||||
approvals: PendingSidebarApproval[]
|
||||
artifacts: SidebarArtifact[]
|
||||
attachments: ContextAttachment[]
|
||||
enabledLibraries: KnowledgeLibrary[]
|
||||
approvals: PendingSidebarApproval[]
|
||||
heartbeats: AssistantHeartbeatConfig[]
|
||||
memories: AssistantMemory[]
|
||||
schedules: AssistantSchedule[]
|
||||
heartbeats: AssistantHeartbeatConfig[]
|
||||
heartbeatEntries: AssistantHeartbeatEntry[]
|
||||
workspaceChanges?: WorkspaceChanges
|
||||
workspaceProjectId?: string
|
||||
browserState?: BrowserLiveState
|
||||
onClose: () => void
|
||||
onInteractBrowser: () => Promise<void>
|
||||
onStopBrowser: () => Promise<void>
|
||||
onOpenHeartbeat: () => void
|
||||
onOpenConversation: (conversationId: string) => void
|
||||
onCreateHeartbeat: (input: HeartbeatCreateInput) => Promise<void>
|
||||
onCreateSchedule: (input: ScheduleCreateInput) => Promise<void>
|
||||
onImportArtifacts: () => Promise<void>
|
||||
onLoadArtifact: (artifactId: string) => Promise<void>
|
||||
onRemoveAttachment: (attachmentId: string) => void
|
||||
onCreateMemory: (content: string) => Promise<void>
|
||||
onCreateSchedule: (input: ScheduleCreateInput) => Promise<void>
|
||||
onCreateHeartbeat: (input: HeartbeatCreateInput) => Promise<void>
|
||||
onSetHeartbeatPaused: (
|
||||
heartbeatId: string,
|
||||
paused: boolean
|
||||
) => Promise<void>
|
||||
onRemoveHeartbeat: (heartbeatId: string) => Promise<void>
|
||||
onRunHeartbeat: (heartbeatId: string) => Promise<void>
|
||||
onRemoveSchedule: (scheduleId: string) => Promise<void>
|
||||
onRunSchedule: (scheduleId: string) => Promise<void>
|
||||
onRefreshChanges: () => Promise<void>
|
||||
onListWorkspaceDirectory: (
|
||||
path: string
|
||||
@@ -108,15 +88,18 @@ type RightAssistantSidebarProps = {
|
||||
path: string,
|
||||
type: 'file' | 'directory'
|
||||
) => Promise<void>
|
||||
onRemoveMemory: (memoryId: string) => Promise<void>
|
||||
onSetMemoryStatus: (
|
||||
memoryId: string,
|
||||
status: AssistantMemory['status']
|
||||
) => Promise<void>
|
||||
onRemoveHeartbeat: (heartbeatId: string) => Promise<void>
|
||||
onRemoveSchedule: (scheduleId: string) => Promise<void>
|
||||
onRespondApproval: (
|
||||
approval: PendingSidebarApproval,
|
||||
decision: ApprovalDecision
|
||||
) => void
|
||||
onRunHeartbeat: (heartbeatId: string) => Promise<void>
|
||||
onRunSchedule: (scheduleId: string) => Promise<void>
|
||||
onSetHeartbeatPaused: (
|
||||
heartbeatId: string,
|
||||
paused: boolean
|
||||
) => Promise<void>
|
||||
onTabChange: (tab: AssistantSidebarTab) => void
|
||||
}
|
||||
|
||||
@@ -128,22 +111,17 @@ const tabs: Array<{
|
||||
{
|
||||
id: 'tasks',
|
||||
label: '任务中心',
|
||||
description: '查看运行状态、处理审批并安排自动化'
|
||||
description: '处理待审批操作并管理自动化'
|
||||
},
|
||||
{
|
||||
id: 'context',
|
||||
label: '上下文',
|
||||
description: '管理本次对话的附件、知识库与长期记忆'
|
||||
description: '查看本次对话使用的附件、知识库与记忆'
|
||||
},
|
||||
{
|
||||
id: 'artifacts',
|
||||
label: '成果库',
|
||||
description: '集中保存和打开对话生成或手动导入的内容'
|
||||
},
|
||||
{
|
||||
id: 'changes',
|
||||
id: 'workspace',
|
||||
label: '工作区',
|
||||
description: '浏览项目文件与工具活动'
|
||||
description: '浏览项目文件、Git 变更与文件内容'
|
||||
},
|
||||
{
|
||||
id: 'browser',
|
||||
@@ -151,9 +129,9 @@ const tabs: Array<{
|
||||
description: '查看 Agent 操作网页时的实时画面'
|
||||
},
|
||||
{
|
||||
id: 'preview',
|
||||
label: '预览',
|
||||
description: '预览选中的成果或工作区文件'
|
||||
id: 'results',
|
||||
label: '成果',
|
||||
description: '查看对话生成或手动导入的内容'
|
||||
}
|
||||
]
|
||||
const emptyChangedFiles: WorkspaceChanges['files'] = []
|
||||
@@ -189,87 +167,37 @@ function clampSidebarWidth(width: number, viewportWidth: number): number {
|
||||
return Math.min(limits.maximum, Math.max(limits.minimum, width))
|
||||
}
|
||||
|
||||
function formatTime(timestamp: number | string): string {
|
||||
return sidebarTimeFormatter.format(new Date(timestamp))
|
||||
}
|
||||
|
||||
export function orderTasksWithChildren(
|
||||
tasks: readonly AssistantTask[]
|
||||
): AssistantTask[] {
|
||||
const childIds = new Set(
|
||||
tasks.flatMap((task) => (task.parentTaskId ? [task.id] : []))
|
||||
)
|
||||
const childrenByParent = new Map<string, AssistantTask[]>()
|
||||
for (const task of tasks) {
|
||||
if (!task.parentTaskId) {
|
||||
continue
|
||||
}
|
||||
const children = childrenByParent.get(task.parentTaskId) ?? []
|
||||
children.push(task)
|
||||
childrenByParent.set(task.parentTaskId, children)
|
||||
}
|
||||
const ordered: AssistantTask[] = []
|
||||
const included = new Set<string>()
|
||||
const append = (task: AssistantTask): void => {
|
||||
if (included.has(task.id)) {
|
||||
return
|
||||
}
|
||||
included.add(task.id)
|
||||
ordered.push(task)
|
||||
for (const child of childrenByParent.get(task.id) ?? []) {
|
||||
append(child)
|
||||
}
|
||||
}
|
||||
for (const task of tasks) {
|
||||
if (!childIds.has(task.id)) {
|
||||
append(task)
|
||||
}
|
||||
}
|
||||
for (const task of tasks) {
|
||||
append(task)
|
||||
}
|
||||
return ordered
|
||||
}
|
||||
|
||||
export function RightAssistantSidebar({
|
||||
open,
|
||||
tab,
|
||||
activities,
|
||||
tasks,
|
||||
experts = [],
|
||||
approvals,
|
||||
artifacts,
|
||||
attachments,
|
||||
enabledLibraries,
|
||||
approvals,
|
||||
heartbeats,
|
||||
memories,
|
||||
schedules,
|
||||
heartbeats,
|
||||
heartbeatEntries,
|
||||
workspaceChanges,
|
||||
workspaceProjectId,
|
||||
browserState,
|
||||
onClose,
|
||||
onInteractBrowser,
|
||||
onStopBrowser,
|
||||
onOpenHeartbeat,
|
||||
onOpenConversation,
|
||||
onCreateHeartbeat,
|
||||
onCreateSchedule,
|
||||
onImportArtifacts,
|
||||
onLoadArtifact,
|
||||
onRemoveAttachment,
|
||||
onCreateMemory,
|
||||
onCreateSchedule,
|
||||
onCreateHeartbeat,
|
||||
onSetHeartbeatPaused,
|
||||
onRemoveHeartbeat,
|
||||
onRunHeartbeat,
|
||||
onRemoveSchedule,
|
||||
onRunSchedule,
|
||||
onRefreshChanges,
|
||||
onListWorkspaceDirectory,
|
||||
onLoadWorkspaceFile,
|
||||
onOpenWorkspaceEntry,
|
||||
onRemoveMemory,
|
||||
onSetMemoryStatus,
|
||||
onRemoveHeartbeat,
|
||||
onRemoveSchedule,
|
||||
onRespondApproval,
|
||||
onRunHeartbeat,
|
||||
onRunSchedule,
|
||||
onSetHeartbeatPaused,
|
||||
onTabChange
|
||||
}: RightAssistantSidebarProps): React.JSX.Element {
|
||||
const [viewportWidth, setViewportWidth] = useState(window.innerWidth)
|
||||
@@ -303,7 +231,6 @@ export function RightAssistantSidebar({
|
||||
}
|
||||
>()
|
||||
const workspacePreviewRequest = useRef(0)
|
||||
const [memoryDraft, setMemoryDraft] = useState('')
|
||||
const [workspaceRefreshVersion, setWorkspaceRefreshVersion] = useState(0)
|
||||
const [scheduleTitle, setScheduleTitle] = useState('')
|
||||
const [schedulePrompt, setSchedulePrompt] = useState('')
|
||||
@@ -312,31 +239,11 @@ export function RightAssistantSidebar({
|
||||
ScheduleCreateInput['recurrence']
|
||||
>('once')
|
||||
const [actionError, setActionError] = useState('')
|
||||
const recentTasks = useMemo(
|
||||
() =>
|
||||
activities
|
||||
.filter((activity) => activity.kind === 'request')
|
||||
.slice(0, 20),
|
||||
[activities]
|
||||
)
|
||||
const orderedTasks = useMemo(
|
||||
() => orderTasksWithChildren(tasks),
|
||||
[tasks]
|
||||
)
|
||||
const expertNames = useMemo(
|
||||
() => new Map(experts.map((expert) => [expert.id, expert.name])),
|
||||
[experts]
|
||||
)
|
||||
const changes = useMemo(
|
||||
() =>
|
||||
activities
|
||||
.filter((activity) => activity.kind === 'tool')
|
||||
.slice(0, 30),
|
||||
[activities]
|
||||
const activeMemories = memories.filter(
|
||||
(memory) => memory.status === 'confirmed'
|
||||
)
|
||||
const artifactPreview =
|
||||
artifacts.find((artifact) => artifact.id === selectedArtifactId) ??
|
||||
artifacts[0]
|
||||
artifacts.find((artifact) => artifact.id === selectedArtifactId)
|
||||
const currentWorkspacePreview =
|
||||
workspacePreview?.projectId === workspaceProjectId
|
||||
? workspacePreview
|
||||
@@ -428,7 +335,6 @@ export function RightAssistantSidebar({
|
||||
const projectId = workspaceProjectId
|
||||
setWorkspacePreview({ projectId, path, state: 'loading' })
|
||||
setActionError('')
|
||||
onTabChange('preview')
|
||||
void onLoadWorkspaceFile(path)
|
||||
.then((file) => {
|
||||
if (workspacePreviewRequest.current === requestId) {
|
||||
@@ -624,103 +530,49 @@ export function RightAssistantSidebar({
|
||||
{tab === 'tasks' && (
|
||||
<section className="assistant-sidebar__section">
|
||||
<p className="assistant-sidebar__section-description">
|
||||
查看当前和最近请求的运行状态、处理待审批操作,并安排定时任务与智能心跳。
|
||||
处理当前待审批操作,并创建和管理自动化任务。
|
||||
</p>
|
||||
{approvals.length > 0 && (
|
||||
<>
|
||||
<h3>
|
||||
<ShieldAlert size={15} />
|
||||
等待审批
|
||||
</h3>
|
||||
{approvals.map((approval) => (
|
||||
<article
|
||||
className="assistant-sidebar__approval"
|
||||
key={approval.approvalId}
|
||||
>
|
||||
<strong>{approval.title}</strong>
|
||||
<p>{approval.description}</p>
|
||||
{approval.toolName && <code>{approval.toolName}</code>}
|
||||
<div className="assistant-sidebar__approval-actions">
|
||||
<button
|
||||
className="secondary-button"
|
||||
onClick={() =>
|
||||
onRespondApproval(approval, 'deny')
|
||||
}
|
||||
type="button"
|
||||
>
|
||||
拒绝
|
||||
</button>
|
||||
<button
|
||||
className="primary-button"
|
||||
onClick={() =>
|
||||
onRespondApproval(approval, 'once')
|
||||
}
|
||||
type="button"
|
||||
>
|
||||
仅此次允许
|
||||
</button>
|
||||
</div>
|
||||
</article>
|
||||
))}
|
||||
</>
|
||||
)}
|
||||
|
||||
<h3>
|
||||
<PlayCircle size={15} />
|
||||
最近任务
|
||||
<ShieldAlert size={15} />
|
||||
等待审批
|
||||
</h3>
|
||||
{tasks.length === 0 && recentTasks.length === 0 ? (
|
||||
{approvals.length === 0 ? (
|
||||
<p className="assistant-sidebar__empty">
|
||||
发送请求后,任务状态会显示在这里。
|
||||
当前没有等待审批的操作。
|
||||
</p>
|
||||
) : (
|
||||
(orderedTasks.length > 0 ? orderedTasks : recentTasks).map((task) => (
|
||||
<button
|
||||
className={
|
||||
'parentTaskId' in task && task.parentTaskId
|
||||
? 'assistant-sidebar__row assistant-sidebar__row--subtask'
|
||||
: 'assistant-sidebar__row'
|
||||
}
|
||||
key={task.id}
|
||||
onClick={() => {
|
||||
if (task.conversationId) {
|
||||
onOpenConversation(task.conversationId)
|
||||
}
|
||||
}}
|
||||
type="button"
|
||||
approvals.map((approval) => (
|
||||
<article
|
||||
className="assistant-sidebar__approval"
|
||||
key={approval.approvalId}
|
||||
>
|
||||
{task.status === 'running' ||
|
||||
task.status === 'pending' ? (
|
||||
<Hourglass size={15} />
|
||||
) : task.status === 'failed' ||
|
||||
task.status === 'denied' ? (
|
||||
<XCircle size={15} />
|
||||
) : (
|
||||
<CheckCircle2 size={15} />
|
||||
)}
|
||||
<span>
|
||||
<strong>{task.title}</strong>
|
||||
<small>
|
||||
{formatTime(task.createdAt)} · {task.status}
|
||||
</small>
|
||||
{'parentTaskId' in task && task.parentTaskId && (
|
||||
<small className="assistant-sidebar__subtask-meta">
|
||||
子专家:
|
||||
{task.expertId
|
||||
? expertNames.get(task.expertId) ??
|
||||
task.title
|
||||
: task.title}
|
||||
{' · '}
|
||||
{task.routingMode === 'smart'
|
||||
? '智能路由'
|
||||
: '手动指定'}
|
||||
</small>
|
||||
)}
|
||||
</span>
|
||||
<ChevronRight size={14} />
|
||||
</button>
|
||||
<strong>{approval.title}</strong>
|
||||
<p>{approval.description}</p>
|
||||
{approval.toolName && <code>{approval.toolName}</code>}
|
||||
<div className="assistant-sidebar__approval-actions">
|
||||
<button
|
||||
className="secondary-button"
|
||||
onClick={() =>
|
||||
onRespondApproval(approval, 'deny')
|
||||
}
|
||||
type="button"
|
||||
>
|
||||
拒绝
|
||||
</button>
|
||||
<button
|
||||
className="primary-button"
|
||||
onClick={() =>
|
||||
onRespondApproval(approval, 'once')
|
||||
}
|
||||
type="button"
|
||||
>
|
||||
仅此次允许
|
||||
</button>
|
||||
</div>
|
||||
</article>
|
||||
))
|
||||
)}
|
||||
|
||||
<h3>
|
||||
<Hourglass size={15} />
|
||||
自动化
|
||||
@@ -775,9 +627,7 @@ export function RightAssistantSidebar({
|
||||
prompt: schedulePrompt.trim(),
|
||||
workMode: 'ask',
|
||||
recurrence: scheduleRecurrence,
|
||||
nextRunAt: new Date(
|
||||
scheduleTime
|
||||
).toISOString()
|
||||
nextRunAt: new Date(scheduleTime).toISOString()
|
||||
}),
|
||||
'添加定时任务失败',
|
||||
() => {
|
||||
@@ -843,6 +693,9 @@ export function RightAssistantSidebar({
|
||||
|
||||
{tab === 'context' && (
|
||||
<section className="assistant-sidebar__section">
|
||||
<p className="assistant-sidebar__section-description">
|
||||
查看当前对话实际使用的附件、知识库与已确认记忆。
|
||||
</p>
|
||||
<h3>
|
||||
<FileText size={15} />
|
||||
本次附件
|
||||
@@ -892,200 +745,76 @@ export function RightAssistantSidebar({
|
||||
)}
|
||||
<h3>
|
||||
<CheckCircle2 size={15} />
|
||||
长期记忆
|
||||
已确认记忆
|
||||
</h3>
|
||||
<div className="assistant-sidebar__memory-form">
|
||||
<input
|
||||
aria-label="新增长期记忆"
|
||||
maxLength={8_000}
|
||||
onChange={(event) => setMemoryDraft(event.target.value)}
|
||||
placeholder="例如:我偏好简洁的中文回复"
|
||||
value={memoryDraft}
|
||||
/>
|
||||
<button
|
||||
className="primary-button"
|
||||
disabled={!memoryDraft.trim()}
|
||||
onClick={() => {
|
||||
const content = memoryDraft.trim()
|
||||
runAction(
|
||||
() => onCreateMemory(content),
|
||||
'保存长期记忆失败',
|
||||
() => setMemoryDraft('')
|
||||
)
|
||||
}}
|
||||
type="button"
|
||||
>
|
||||
记住
|
||||
</button>
|
||||
</div>
|
||||
{memories.length === 0 ? (
|
||||
{activeMemories.length === 0 ? (
|
||||
<p className="assistant-sidebar__empty">
|
||||
尚无已确认的长期记忆。
|
||||
当前范围没有已确认的长期记忆。
|
||||
</p>
|
||||
) : (
|
||||
memories.map((memory) => (
|
||||
activeMemories.map((memory) => (
|
||||
<article
|
||||
className="assistant-sidebar__memory"
|
||||
key={memory.id}
|
||||
>
|
||||
<span>
|
||||
{memory.content}
|
||||
{memory.status === 'proposed' && (
|
||||
<small>智能心跳建议,等待确认</small>
|
||||
)}
|
||||
</span>
|
||||
<div>
|
||||
{memory.status === 'proposed' && (
|
||||
<>
|
||||
<button
|
||||
onClick={() =>
|
||||
runAction(
|
||||
() =>
|
||||
onSetMemoryStatus(
|
||||
memory.id,
|
||||
'confirmed'
|
||||
),
|
||||
'确认长期记忆失败'
|
||||
)
|
||||
}
|
||||
type="button"
|
||||
>
|
||||
确认
|
||||
</button>
|
||||
<button
|
||||
onClick={() =>
|
||||
runAction(
|
||||
() =>
|
||||
onSetMemoryStatus(
|
||||
memory.id,
|
||||
'rejected'
|
||||
),
|
||||
'忽略长期记忆失败'
|
||||
)
|
||||
}
|
||||
type="button"
|
||||
>
|
||||
忽略
|
||||
</button>
|
||||
</>
|
||||
)}
|
||||
<button
|
||||
aria-label={`删除记忆 ${memory.content.slice(0, 24)}`}
|
||||
className="icon-button"
|
||||
onClick={() =>
|
||||
runAction(
|
||||
() => onRemoveMemory(memory.id),
|
||||
'删除长期记忆失败'
|
||||
)
|
||||
}
|
||||
type="button"
|
||||
>
|
||||
<X size={13} />
|
||||
</button>
|
||||
</div>
|
||||
</article>
|
||||
))
|
||||
)}
|
||||
<h3>
|
||||
<RefreshCw size={15} />
|
||||
智能心跳
|
||||
<button
|
||||
aria-label="打开智能心跳中心"
|
||||
className="icon-button"
|
||||
onClick={onOpenHeartbeat}
|
||||
type="button"
|
||||
>
|
||||
<ChevronRight size={14} />
|
||||
</button>
|
||||
</h3>
|
||||
{heartbeatEntries.length === 0 ? (
|
||||
<p className="assistant-sidebar__empty">
|
||||
完成智能心跳后,最近的成长摘要会显示在这里。
|
||||
</p>
|
||||
) : (
|
||||
heartbeatEntries.slice(0, 10).map((entry) => (
|
||||
<article
|
||||
className="assistant-sidebar__schedule"
|
||||
key={entry.id}
|
||||
>
|
||||
<span>
|
||||
<strong>
|
||||
{new Date(entry.createdAt).toLocaleString('zh-CN')}
|
||||
</strong>
|
||||
<small>
|
||||
{entry.proposedMemoryIds.length} 条记忆建议 ·{' '}
|
||||
{entry.followUpTaskIds.length} 个后续任务
|
||||
</small>
|
||||
</span>
|
||||
<p>{entry.summary}</p>
|
||||
<span>{memory.content}</span>
|
||||
</article>
|
||||
))
|
||||
)}
|
||||
</section>
|
||||
)}
|
||||
|
||||
{tab === 'artifacts' && (
|
||||
<section className="assistant-sidebar__section">
|
||||
<p className="assistant-sidebar__section-description">
|
||||
保存并预览由对话生成或手动导入的文本、图片、PDF 与网页内容。
|
||||
</p>
|
||||
<h3>
|
||||
<FileText size={15} />
|
||||
对话与导入成果
|
||||
</h3>
|
||||
<button
|
||||
className="secondary-button assistant-sidebar__import"
|
||||
onClick={() =>
|
||||
runAction(
|
||||
onImportArtifacts,
|
||||
'导入成果失败'
|
||||
)
|
||||
}
|
||||
type="button"
|
||||
>
|
||||
<Upload size={13} />
|
||||
导入 PDF、图片或网页
|
||||
</button>
|
||||
{artifacts.length === 0 ? (
|
||||
<p className="assistant-sidebar__empty">
|
||||
完成的回复会作为可预览成果显示在这里。
|
||||
</p>
|
||||
) : (
|
||||
artifacts.map((artifact) => (
|
||||
{tab === 'workspace' && (
|
||||
currentWorkspacePreview ? (
|
||||
<section className="assistant-sidebar__preview">
|
||||
<header>
|
||||
<button
|
||||
className="assistant-sidebar__row"
|
||||
key={artifact.id}
|
||||
aria-label="返回工作区"
|
||||
className="assistant-sidebar__back"
|
||||
onClick={() => {
|
||||
workspacePreviewRequest.current += 1
|
||||
setWorkspacePreview(undefined)
|
||||
setSelectedArtifactId(artifact.id)
|
||||
setActionError('')
|
||||
onTabChange('preview')
|
||||
runAction(
|
||||
() => onLoadArtifact(artifact.id),
|
||||
'加载成果失败'
|
||||
)
|
||||
}}
|
||||
type="button"
|
||||
>
|
||||
<FileText size={15} />
|
||||
<span>
|
||||
<strong>{artifact.title}</strong>
|
||||
<small>{formatTime(artifact.createdAt)}</small>
|
||||
</span>
|
||||
<ChevronRight size={14} />
|
||||
<ChevronLeft size={14} />
|
||||
工作区
|
||||
</button>
|
||||
))
|
||||
)}
|
||||
</section>
|
||||
)}
|
||||
|
||||
{tab === 'changes' && (
|
||||
<>
|
||||
<span>
|
||||
<strong>{currentWorkspacePreview.path}</strong>
|
||||
<small>
|
||||
{currentWorkspacePreview.state === 'ready'
|
||||
? `${currentWorkspacePreview.file.size.toLocaleString('zh-CN')} 字节`
|
||||
: '项目工作区文件'}
|
||||
</small>
|
||||
</span>
|
||||
</header>
|
||||
{currentWorkspacePreview.state === 'loading' ? (
|
||||
<p className="assistant-sidebar__empty">
|
||||
正在读取文件…
|
||||
</p>
|
||||
) : currentWorkspacePreview.state === 'error' ? (
|
||||
<p className="assistant-sidebar__empty" role="alert">
|
||||
{currentWorkspacePreview.error}
|
||||
</p>
|
||||
) : (
|
||||
<div className="markdown-body markdown-content">
|
||||
{currentWorkspacePreview.file.mimeType ===
|
||||
'text/markdown' ? (
|
||||
<MarkdownRenderer>
|
||||
{currentWorkspacePreview.file.content}
|
||||
</MarkdownRenderer>
|
||||
) : (
|
||||
<pre>{currentWorkspacePreview.file.content}</pre>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</section>
|
||||
) : (
|
||||
<section className="assistant-sidebar__section">
|
||||
<p className="assistant-sidebar__section-description">
|
||||
浏览当前项目文件,并查看 Agent 的工具活动。Git
|
||||
项目还会显示未提交更改。
|
||||
浏览当前项目文件与 Git 变更;选择文件后在当前工作区内预览。
|
||||
</p>
|
||||
<h3>
|
||||
<FolderTree size={15} />
|
||||
@@ -1132,33 +861,119 @@ export function RightAssistantSidebar({
|
||||
</details>
|
||||
)}
|
||||
</section>
|
||||
)
|
||||
)}
|
||||
|
||||
{tab === 'results' && (
|
||||
artifactPreview ? (
|
||||
<section className="assistant-sidebar__preview">
|
||||
<header>
|
||||
<button
|
||||
aria-label="返回成果列表"
|
||||
className="assistant-sidebar__back"
|
||||
onClick={() => {
|
||||
setSelectedArtifactId(undefined)
|
||||
setActionError('')
|
||||
}}
|
||||
type="button"
|
||||
>
|
||||
<ChevronLeft size={14} />
|
||||
成果
|
||||
</button>
|
||||
<span>
|
||||
<strong>{artifactPreview.title}</strong>
|
||||
<small>
|
||||
{sidebarTimeFormatter.format(
|
||||
new Date(artifactPreview.createdAt)
|
||||
)}
|
||||
</small>
|
||||
</span>
|
||||
</header>
|
||||
<div className="markdown-body markdown-content">
|
||||
{artifactPreview.mimeType.startsWith('image/') ? (
|
||||
artifactPreview.content ? (
|
||||
<img
|
||||
alt={artifactPreview.title}
|
||||
className="assistant-sidebar__image-preview"
|
||||
src={artifactPreview.content}
|
||||
/>
|
||||
) : (
|
||||
<p className="assistant-sidebar__empty">
|
||||
正在加载图片…
|
||||
</p>
|
||||
)
|
||||
) : artifactPreview.mimeType === 'text/html' ? (
|
||||
<iframe
|
||||
className="assistant-sidebar__web-preview"
|
||||
sandbox=""
|
||||
srcDoc={artifactPreview.content}
|
||||
title={artifactPreview.title}
|
||||
/>
|
||||
) : artifactPreview.mimeType === 'application/json' ? (
|
||||
<pre>{artifactPreview.content}</pre>
|
||||
) : (
|
||||
<MarkdownRenderer>
|
||||
{artifactPreview.content}
|
||||
</MarkdownRenderer>
|
||||
)}
|
||||
</div>
|
||||
</section>
|
||||
) : (
|
||||
<section className="assistant-sidebar__section">
|
||||
<h3>工具活动</h3>
|
||||
{changes.length === 0 ? (
|
||||
<p className="assistant-sidebar__section-description">
|
||||
查看并预览由对话生成或手动导入的文本、图片、PDF 与网页内容。
|
||||
</p>
|
||||
<h3>
|
||||
<FileText size={15} />
|
||||
对话与导入成果
|
||||
</h3>
|
||||
<button
|
||||
className="secondary-button assistant-sidebar__import"
|
||||
onClick={() =>
|
||||
runAction(
|
||||
onImportArtifacts,
|
||||
'导入成果失败'
|
||||
)
|
||||
}
|
||||
type="button"
|
||||
>
|
||||
<Upload size={13} />
|
||||
导入 PDF、图片或网页
|
||||
</button>
|
||||
{artifacts.length === 0 ? (
|
||||
<p className="assistant-sidebar__empty">
|
||||
Agent 调用工具后,相关记录会显示在这里。
|
||||
完成的回复会作为可预览成果显示在这里。
|
||||
</p>
|
||||
) : (
|
||||
changes.map((change) => (
|
||||
artifacts.map((artifact) => (
|
||||
<button
|
||||
className="assistant-sidebar__row"
|
||||
key={change.id}
|
||||
onClick={() =>
|
||||
onOpenConversation(change.conversationId)
|
||||
}
|
||||
key={artifact.id}
|
||||
onClick={() => {
|
||||
setSelectedArtifactId(artifact.id)
|
||||
setActionError('')
|
||||
runAction(
|
||||
() => onLoadArtifact(artifact.id),
|
||||
'加载成果失败'
|
||||
)
|
||||
}}
|
||||
type="button"
|
||||
>
|
||||
<FileDiff size={15} />
|
||||
<FileText size={15} />
|
||||
<span>
|
||||
<strong>{change.title}</strong>
|
||||
<small>{change.detail || change.status}</small>
|
||||
<strong>{artifact.title}</strong>
|
||||
<small>
|
||||
{sidebarTimeFormatter.format(
|
||||
new Date(artifact.createdAt)
|
||||
)}
|
||||
</small>
|
||||
</span>
|
||||
<ChevronRight size={14} />
|
||||
</button>
|
||||
))
|
||||
)}
|
||||
</section>
|
||||
</>
|
||||
)
|
||||
)}
|
||||
|
||||
{tab === 'browser' && (
|
||||
@@ -1170,18 +985,39 @@ export function RightAssistantSidebar({
|
||||
</span>
|
||||
{browserState &&
|
||||
browserState.status !== 'stopped' && (
|
||||
<button
|
||||
className="secondary-button"
|
||||
onClick={() =>
|
||||
runAction(
|
||||
onStopBrowser,
|
||||
'停止浏览器失败'
|
||||
)
|
||||
}
|
||||
type="button"
|
||||
>
|
||||
停止浏览器
|
||||
</button>
|
||||
<div className="assistant-sidebar__browser-actions">
|
||||
<button
|
||||
className="secondary-button"
|
||||
disabled={
|
||||
browserState.status === 'creating' ||
|
||||
browserState.status === 'interactive'
|
||||
}
|
||||
onClick={() =>
|
||||
runAction(
|
||||
onInteractBrowser,
|
||||
'打开浏览器交互窗口失败'
|
||||
)
|
||||
}
|
||||
type="button"
|
||||
>
|
||||
<ExternalLink aria-hidden="true" size={12} />
|
||||
{browserState.status === 'interactive'
|
||||
? '交互中'
|
||||
: '交互'}
|
||||
</button>
|
||||
<button
|
||||
className="secondary-button"
|
||||
onClick={() =>
|
||||
runAction(
|
||||
onStopBrowser,
|
||||
'停止浏览器失败'
|
||||
)
|
||||
}
|
||||
type="button"
|
||||
>
|
||||
停止浏览器
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</header>
|
||||
{!browserState ? (
|
||||
@@ -1201,6 +1037,8 @@ export function RightAssistantSidebar({
|
||||
? '正在加载页面…'
|
||||
: browserState.status === 'acting'
|
||||
? 'Agent 正在操作页面…'
|
||||
: browserState.status === 'interactive'
|
||||
? '用户正在辅助操作页面…'
|
||||
: browserState.status === 'ready'
|
||||
? '浏览器已就绪'
|
||||
: browserState.status === 'failed'
|
||||
@@ -1235,82 +1073,6 @@ export function RightAssistantSidebar({
|
||||
)}
|
||||
</section>
|
||||
)}
|
||||
|
||||
{tab === 'preview' && (
|
||||
<section className="assistant-sidebar__preview">
|
||||
{currentWorkspacePreview ? (
|
||||
<>
|
||||
<header>
|
||||
<strong>{currentWorkspacePreview.path}</strong>
|
||||
<small>
|
||||
{currentWorkspacePreview.state === 'ready'
|
||||
? `${currentWorkspacePreview.file.size.toLocaleString('zh-CN')} 字节`
|
||||
: '项目工作区文件'}
|
||||
</small>
|
||||
</header>
|
||||
{currentWorkspacePreview.state === 'loading' ? (
|
||||
<p className="assistant-sidebar__empty">
|
||||
正在读取文件…
|
||||
</p>
|
||||
) : currentWorkspacePreview.state === 'error' ? (
|
||||
<p className="assistant-sidebar__empty" role="alert">
|
||||
{currentWorkspacePreview.error}
|
||||
</p>
|
||||
) : (
|
||||
<div className="markdown-body markdown-content">
|
||||
{currentWorkspacePreview.file.mimeType ===
|
||||
'text/markdown' ? (
|
||||
<MarkdownRenderer>
|
||||
{currentWorkspacePreview.file.content}
|
||||
</MarkdownRenderer>
|
||||
) : (
|
||||
<pre>{currentWorkspacePreview.file.content}</pre>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
) : artifactPreview ? (
|
||||
<>
|
||||
<header>
|
||||
<strong>{artifactPreview.title}</strong>
|
||||
<small>{formatTime(artifactPreview.createdAt)}</small>
|
||||
</header>
|
||||
<div className="markdown-body markdown-content">
|
||||
{artifactPreview.mimeType.startsWith('image/') ? (
|
||||
artifactPreview.content ? (
|
||||
<img
|
||||
alt={artifactPreview.title}
|
||||
className="assistant-sidebar__image-preview"
|
||||
src={artifactPreview.content}
|
||||
/>
|
||||
) : (
|
||||
<p className="assistant-sidebar__empty">
|
||||
正在加载图片…
|
||||
</p>
|
||||
)
|
||||
) : artifactPreview.mimeType === 'text/html' ? (
|
||||
<iframe
|
||||
className="assistant-sidebar__web-preview"
|
||||
sandbox=""
|
||||
srcDoc={artifactPreview.content}
|
||||
title={artifactPreview.title}
|
||||
/>
|
||||
) : artifactPreview.mimeType === 'application/json' ? (
|
||||
<pre>{artifactPreview.content}</pre>
|
||||
) : (
|
||||
<MarkdownRenderer>
|
||||
{artifactPreview.content}
|
||||
</MarkdownRenderer>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<p className="assistant-sidebar__empty">
|
||||
选择成果后可在这里预览。
|
||||
</p>
|
||||
)}
|
||||
</section>
|
||||
)}
|
||||
</div>
|
||||
</aside>
|
||||
)
|
||||
|
||||
@@ -1657,6 +1657,14 @@ describe('SettingsPanel runtime files', () => {
|
||||
|
||||
fireEvent.click(screen.getByRole('tab', { name: 'Skills' }))
|
||||
expect(await screen.findByText('文档写作')).toBeInTheDocument()
|
||||
expect(
|
||||
screen.getByText('支持直连模型、OpenCode 和 Continue')
|
||||
).toBeInTheDocument()
|
||||
expect(
|
||||
screen.getByText(/新导入的 Skill 默认启用/)
|
||||
).toHaveTextContent(
|
||||
'分配给直连模型、OpenCode 和 Continue'
|
||||
)
|
||||
fireEvent.click(
|
||||
screen.getByRole('button', { name: '导入 Skill 目录' })
|
||||
)
|
||||
@@ -1677,6 +1685,15 @@ describe('SettingsPanel runtime files', () => {
|
||||
|
||||
fireEvent.click(screen.getByRole('tab', { name: 'MCP' }))
|
||||
expect(await screen.findByText('电脑控制能力')).toBeInTheDocument()
|
||||
expect(
|
||||
screen.getByText(/自定义 MCP 当前仅用于直连模型/)
|
||||
).toHaveTextContent('新建时默认分配给直连模型')
|
||||
expect(
|
||||
screen.getByText(/内置共享 MCP 当前仅有知识库搜索/)
|
||||
).toHaveTextContent('直连模型、OpenCode 和 Continue')
|
||||
expect(
|
||||
screen.getByText(/Runtime 自有 MCP 配置不在此处管理/)
|
||||
).toBeInTheDocument()
|
||||
expect(screen.getAllByText('托管浏览器配置').length).toBeGreaterThan(0)
|
||||
expect(screen.getByLabelText('启用 Linux 桌面控制')).toBeDisabled()
|
||||
fireEvent.click(screen.getByLabelText('启用 浏览器控制'))
|
||||
|
||||
@@ -61,7 +61,7 @@ export function SkillsSettingsSection(): React.JSX.Element {
|
||||
<BookOpen size={17} />
|
||||
<div>
|
||||
<strong>Skills</strong>
|
||||
<small>离线能力说明,可独立分配给各 Agent Runtime</small>
|
||||
<small>支持直连模型、OpenCode 和 Continue</small>
|
||||
</div>
|
||||
<button
|
||||
className="secondary-button"
|
||||
@@ -91,6 +91,10 @@ export function SkillsSettingsSection(): React.JSX.Element {
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<p className="settings-notice">
|
||||
Skill 以本地能力说明注入所选目标,不会写入 Runtime
|
||||
自有配置。新导入的 Skill 默认启用,并分配给直连模型、OpenCode 和 Continue。
|
||||
</p>
|
||||
{error && <p className="settings-warning">{error}</p>}
|
||||
{!snapshot && !error && <p className="settings-empty">正在读取 Skills…</p>}
|
||||
<div className="capability-list">
|
||||
|
||||
+173
-86
@@ -704,7 +704,7 @@ textarea:focus-visible {
|
||||
padding: 8px;
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
gap: 3px;
|
||||
grid-template-columns: repeat(6, minmax(0, 1fr));
|
||||
grid-template-columns: repeat(5, minmax(0, 1fr));
|
||||
}
|
||||
|
||||
.assistant-sidebar__tab {
|
||||
@@ -733,8 +733,8 @@ textarea:focus-visible {
|
||||
padding: 0 4px;
|
||||
place-items: center;
|
||||
border-radius: 8px;
|
||||
background: #ff4d4f;
|
||||
color: #fff;
|
||||
background: var(--danger-solid);
|
||||
color: var(--text-on-accent);
|
||||
font-size: 8px;
|
||||
}
|
||||
|
||||
@@ -960,18 +960,6 @@ textarea:focus-visible {
|
||||
background: #e6f4ff;
|
||||
}
|
||||
|
||||
.assistant-sidebar__row--subtask {
|
||||
width: calc(100% - var(--space-4));
|
||||
margin-left: var(--space-4);
|
||||
border-left: 2px solid var(--accent-selected);
|
||||
background: var(--surface-subtle);
|
||||
}
|
||||
|
||||
.assistant-sidebar__subtask-meta {
|
||||
color: var(--text-secondary) !important;
|
||||
white-space: normal !important;
|
||||
}
|
||||
|
||||
.assistant-sidebar__row span,
|
||||
.assistant-sidebar__context span,
|
||||
.assistant-sidebar__library {
|
||||
@@ -1002,9 +990,9 @@ textarea:focus-visible {
|
||||
|
||||
.assistant-sidebar__approval {
|
||||
padding: 11px;
|
||||
border: 1px solid #ffd591;
|
||||
border-radius: 9px;
|
||||
background: #fff7e6;
|
||||
border: 1px solid var(--danger-border);
|
||||
border-radius: var(--radius-control);
|
||||
background: var(--danger-subtle);
|
||||
}
|
||||
|
||||
.assistant-sidebar__approval strong {
|
||||
@@ -1014,13 +1002,13 @@ textarea:focus-visible {
|
||||
|
||||
.assistant-sidebar__approval p {
|
||||
margin: 5px 0;
|
||||
color: #595959;
|
||||
color: var(--text-secondary);
|
||||
font-size: 10px;
|
||||
line-height: 1.45;
|
||||
}
|
||||
|
||||
.assistant-sidebar__approval code {
|
||||
color: #ad6800;
|
||||
color: var(--danger);
|
||||
font-size: 9px;
|
||||
}
|
||||
|
||||
@@ -1053,24 +1041,6 @@ textarea:focus-visible {
|
||||
background: #fafafa;
|
||||
}
|
||||
|
||||
.assistant-sidebar__memory-form {
|
||||
display: grid;
|
||||
gap: 6px;
|
||||
grid-template-columns: minmax(0, 1fr) auto;
|
||||
}
|
||||
|
||||
.assistant-sidebar__memory-form input {
|
||||
min-width: 0;
|
||||
padding: 8px 9px;
|
||||
border: 1px solid #d9d9d9;
|
||||
border-radius: 8px;
|
||||
font-size: 10px;
|
||||
}
|
||||
|
||||
.assistant-sidebar__memory-form button {
|
||||
padding: 0 10px;
|
||||
}
|
||||
|
||||
.assistant-sidebar__memory {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
@@ -1096,21 +1066,6 @@ textarea:focus-visible {
|
||||
font-size: 8px;
|
||||
}
|
||||
|
||||
.assistant-sidebar__memory > div {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 5px;
|
||||
}
|
||||
|
||||
.assistant-sidebar__memory > div > button:not(.icon-button) {
|
||||
padding: 2px 4px;
|
||||
border: 0;
|
||||
background: transparent;
|
||||
color: #1677ff;
|
||||
cursor: pointer;
|
||||
font-size: 8px;
|
||||
}
|
||||
|
||||
.assistant-sidebar__schedule-form {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
@@ -1122,18 +1077,19 @@ textarea:focus-visible {
|
||||
.assistant-sidebar__schedule-form select {
|
||||
width: 100%;
|
||||
padding: 8px 9px;
|
||||
border: 1px solid #d9d9d9;
|
||||
border-radius: 8px;
|
||||
background: #fff;
|
||||
border: 1px solid var(--border-control);
|
||||
border-radius: var(--radius-control);
|
||||
background: var(--surface-raised);
|
||||
color: var(--text-primary);
|
||||
font-size: 9px;
|
||||
resize: vertical;
|
||||
}
|
||||
|
||||
.assistant-sidebar__schedule {
|
||||
padding: 9px 10px;
|
||||
border: 1px solid #d9d9d9;
|
||||
border-radius: 9px;
|
||||
background: #fafafa;
|
||||
border: 1px solid var(--border-default);
|
||||
border-radius: var(--radius-control);
|
||||
background: var(--surface-subtle);
|
||||
}
|
||||
|
||||
.assistant-sidebar__schedule > span {
|
||||
@@ -1147,7 +1103,7 @@ textarea:focus-visible {
|
||||
}
|
||||
|
||||
.assistant-sidebar__schedule small {
|
||||
color: #8c8c8c;
|
||||
color: var(--text-muted);
|
||||
font-size: 8px;
|
||||
}
|
||||
|
||||
@@ -1160,18 +1116,11 @@ textarea:focus-visible {
|
||||
.assistant-sidebar__schedule button {
|
||||
padding: 3px 0;
|
||||
background: transparent;
|
||||
color: #1677ff;
|
||||
color: var(--accent);
|
||||
cursor: pointer;
|
||||
font-size: 8px;
|
||||
}
|
||||
|
||||
.assistant-sidebar__schedule p {
|
||||
margin: 7px 0 0;
|
||||
color: #595959;
|
||||
font-size: 9px;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.heartbeat-settings {
|
||||
display: flex;
|
||||
min-width: 0;
|
||||
@@ -1371,11 +1320,20 @@ textarea:focus-visible {
|
||||
gap: var(--space-2);
|
||||
}
|
||||
|
||||
.assistant-sidebar__browser-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--space-2);
|
||||
}
|
||||
|
||||
.assistant-sidebar__browser > header .secondary-button {
|
||||
display: inline-flex;
|
||||
min-height: 28px;
|
||||
align-items: center;
|
||||
padding: 4px 8px;
|
||||
flex: none;
|
||||
font-size: 10px;
|
||||
gap: var(--space-1);
|
||||
}
|
||||
|
||||
.assistant-sidebar__browser-status {
|
||||
@@ -1433,9 +1391,34 @@ textarea:focus-visible {
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
margin-bottom: 12px;
|
||||
flex-direction: column;
|
||||
gap: var(--space-2);
|
||||
}
|
||||
|
||||
.assistant-sidebar__preview > header > span {
|
||||
display: flex;
|
||||
min-width: 0;
|
||||
flex-direction: column;
|
||||
gap: 3px;
|
||||
}
|
||||
|
||||
.assistant-sidebar__back {
|
||||
display: flex;
|
||||
width: fit-content;
|
||||
align-items: center;
|
||||
padding: var(--space-1) var(--space-2);
|
||||
border-radius: var(--radius-control);
|
||||
background: transparent;
|
||||
color: var(--text-secondary);
|
||||
cursor: pointer;
|
||||
font-size: var(--font-caption);
|
||||
gap: var(--space-1);
|
||||
}
|
||||
|
||||
.assistant-sidebar__back:hover {
|
||||
background: var(--surface-subtle);
|
||||
color: var(--accent);
|
||||
}
|
||||
|
||||
.assistant-sidebar__preview > header strong {
|
||||
font-size: 12px;
|
||||
}
|
||||
@@ -1949,7 +1932,7 @@ textarea:focus-visible {
|
||||
}
|
||||
|
||||
.message-blocks .message-reasoning,
|
||||
.message-blocks .tool-activity {
|
||||
.message-blocks .tool-execution-list {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
@@ -2087,11 +2070,13 @@ textarea:focus-visible {
|
||||
|
||||
.message__status {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
align-items: flex-start;
|
||||
margin-top: 8px;
|
||||
color: #8c8c8c;
|
||||
font-size: 10px;
|
||||
gap: 7px;
|
||||
overflow-wrap: anywhere;
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
|
||||
.message__status--error {
|
||||
@@ -2099,44 +2084,147 @@ textarea:focus-visible {
|
||||
}
|
||||
|
||||
.thinking-dot {
|
||||
flex: 0 0 auto;
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
margin-top: 4px;
|
||||
border-radius: 50%;
|
||||
animation: pulse 1.1s ease-in-out infinite;
|
||||
background: #1677ff;
|
||||
}
|
||||
|
||||
.tool-activity {
|
||||
.tool-execution-list {
|
||||
display: grid;
|
||||
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;
|
||||
padding: 9px 11px;
|
||||
border: 1px solid #d9d9d9;
|
||||
border-radius: 9px;
|
||||
margin-top: 8px;
|
||||
background: #fafafa;
|
||||
color: #595959;
|
||||
font-size: 10px;
|
||||
gap: 8px;
|
||||
min-height: 32px;
|
||||
padding: var(--space-2) var(--space-3);
|
||||
gap: var(--space-2);
|
||||
}
|
||||
|
||||
.tool-activity__content {
|
||||
.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;
|
||||
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 > summary {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
min-height: 36px;
|
||||
padding: var(--space-2) var(--space-3);
|
||||
cursor: pointer;
|
||||
gap: var(--space-3);
|
||||
list-style-position: inside;
|
||||
transition: background var(--motion-fast) ease-out;
|
||||
}
|
||||
|
||||
.tool-execution > summary:hover {
|
||||
background: var(--accent-subtle);
|
||||
}
|
||||
|
||||
.tool-execution > summary:focus-visible {
|
||||
outline: 2px solid var(--accent);
|
||||
outline-offset: -2px;
|
||||
}
|
||||
|
||||
.tool-execution__identity {
|
||||
display: grid;
|
||||
flex: 1;
|
||||
gap: var(--space-1);
|
||||
min-width: 0;
|
||||
gap: var(--space-1);
|
||||
}
|
||||
|
||||
.tool-activity__content code {
|
||||
.tool-execution__identity strong {
|
||||
color: var(--text-primary);
|
||||
font-family: var(--font-family-mono);
|
||||
font-size: var(--font-body);
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
|
||||
.tool-execution__identity > span {
|
||||
color: var(--text-muted);
|
||||
font-size: var(--font-caption);
|
||||
}
|
||||
|
||||
.tool-execution > summary small {
|
||||
color: var(--text-muted);
|
||||
font-size: var(--font-caption);
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.tool-execution--failed > summary small,
|
||||
.tool-execution--recoverable > summary small {
|
||||
color: var(--danger);
|
||||
}
|
||||
|
||||
.tool-execution__details {
|
||||
display: grid;
|
||||
padding: 0 var(--space-3) var(--space-3);
|
||||
gap: var(--space-3);
|
||||
}
|
||||
|
||||
.tool-execution__details section {
|
||||
display: grid;
|
||||
gap: var(--space-2);
|
||||
}
|
||||
|
||||
.tool-execution__details strong {
|
||||
color: var(--text-secondary);
|
||||
font-size: var(--font-caption);
|
||||
}
|
||||
|
||||
.tool-execution__details pre {
|
||||
max-height: 320px;
|
||||
padding: var(--space-3);
|
||||
overflow: auto;
|
||||
border: 1px solid var(--border-subtle);
|
||||
border-radius: var(--radius-control);
|
||||
margin: 0;
|
||||
background: var(--surface-raised);
|
||||
color: var(--text-primary);
|
||||
font-family: var(--font-family-mono);
|
||||
font-size: var(--font-caption);
|
||||
line-height: 1.5;
|
||||
overflow-wrap: anywhere;
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
|
||||
.tool-activity small {
|
||||
color: #8c8c8c;
|
||||
text-transform: uppercase;
|
||||
.tool-execution__details p {
|
||||
margin: 0;
|
||||
color: var(--text-muted);
|
||||
font-size: var(--font-caption);
|
||||
}
|
||||
|
||||
.tool-execution__error pre {
|
||||
border-color: var(--danger-border);
|
||||
color: var(--danger);
|
||||
}
|
||||
|
||||
.subagent-status-list {
|
||||
@@ -6281,7 +6369,6 @@ details.settings-section > :not(summary) {
|
||||
.settings-panel__footer,
|
||||
.assistant-sidebar__row,
|
||||
.assistant-sidebar__library,
|
||||
.assistant-sidebar__schedule,
|
||||
.assistant-sidebar__diff,
|
||||
.assistant-sidebar__preview pre,
|
||||
.tool-activity,
|
||||
|
||||
Reference in New Issue
Block a user