@allbluecn/web-app 0.23.3 → 0.23.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +3 -3
- package/src/components/ai/global-harness-sidebar/__tests__/global-ai-prompts.test.ts +2 -0
- package/src/components/desktop/__tests__/cloud-login-rollback.test.tsx +161 -0
- package/src/components/desktop/cloud-login.tsx +29 -26
- package/src/components/desktop-agent/__tests__/agent-chat.test.tsx +194 -0
- package/src/components/desktop-agent/__tests__/agent-tool-card.test.tsx +138 -0
- package/src/components/desktop-agent/__tests__/agent-workspace-card.test.tsx +189 -0
- package/src/components/desktop-agent/agent-chat.tsx +221 -0
- package/src/components/desktop-agent/agent-tool-card.tsx +185 -0
- package/src/components/desktop-agent/agent-workspace-card.tsx +176 -0
- package/src/components/layout/sidebar-03/app-sidebar.tsx +9 -2
- package/src/config/sidebar-routes.tsx +14 -1
- package/src/lib/changelog/sdk-versions.ts +1 -1
- package/src/lib/desktop-agent/__tests__/connection.test.ts +223 -0
- package/src/lib/desktop-agent/__tests__/errors.test.ts +43 -0
- package/src/lib/desktop-agent/__tests__/tool-result.test.ts +163 -0
- package/src/lib/desktop-agent/__tests__/use-desktop-agent-presence.test.tsx +181 -0
- package/src/lib/desktop-agent/availability.ts +39 -0
- package/src/lib/desktop-agent/connection.ts +72 -0
- package/src/lib/desktop-agent/errors.ts +21 -0
- package/src/lib/desktop-agent/tool-result.ts +170 -0
- package/src/lib/desktop-agent/use-desktop-agent-presence.ts +64 -0
- package/src/lib/desktop-bridge-types.ts +22 -1
- package/src/lib/hotkeys/__tests__/registry.test.ts +11 -4
- package/src/lib/hotkeys/registry.ts +9 -1
- package/src/lib/queries/README.md +1 -0
- package/src/lib/queries/__tests__/desktop-agent-queries.test.ts +80 -0
- package/src/lib/queries/desktop-agent.ts +25 -0
- package/src/paraglide/messages/_index.d.ts +42 -0
- package/src/paraglide/messages/_index.js +42 -0
- package/src/paraglide/messages/agent_desktop_chatcwd1.d.ts +19 -0
- package/src/paraglide/messages/agent_desktop_chatcwd1.js +30 -0
- package/src/paraglide/messages/agent_desktop_chatemptyhint2.d.ts +17 -0
- package/src/paraglide/messages/agent_desktop_chatemptyhint2.js +30 -0
- package/src/paraglide/messages/agent_desktop_chatemptytitle2.d.ts +17 -0
- package/src/paraglide/messages/agent_desktop_chatemptytitle2.js +30 -0
- package/src/paraglide/messages/agent_desktop_chatindependenthint2.d.ts +17 -0
- package/src/paraglide/messages/agent_desktop_chatindependenthint2.js +30 -0
- package/src/paraglide/messages/agent_desktop_chatnoworkspace2.d.ts +17 -0
- package/src/paraglide/messages/agent_desktop_chatnoworkspace2.js +30 -0
- package/src/paraglide/messages/agent_desktop_chatplaceholder1.d.ts +17 -0
- package/src/paraglide/messages/agent_desktop_chatplaceholder1.js +30 -0
- package/src/paraglide/messages/agent_desktop_chatsend1.d.ts +17 -0
- package/src/paraglide/messages/agent_desktop_chatsend1.js +30 -0
- package/src/paraglide/messages/agent_desktop_chatstop1.d.ts +17 -0
- package/src/paraglide/messages/agent_desktop_chatstop1.js +30 -0
- package/src/paraglide/messages/agent_desktop_chatthinking1.d.ts +17 -0
- package/src/paraglide/messages/agent_desktop_chatthinking1.js +30 -0
- package/src/paraglide/messages/agent_desktop_errorconnection1.d.ts +17 -0
- package/src/paraglide/messages/agent_desktop_errorconnection1.js +30 -0
- package/src/paraglide/messages/agent_desktop_errorforbidden1.d.ts +17 -0
- package/src/paraglide/messages/agent_desktop_errorforbidden1.js +30 -0
- package/src/paraglide/messages/agent_desktop_errorgeneric1.d.ts +19 -0
- package/src/paraglide/messages/agent_desktop_errorgeneric1.js +30 -0
- package/src/paraglide/messages/agent_desktop_errorunauthorized1.d.ts +17 -0
- package/src/paraglide/messages/agent_desktop_errorunauthorized1.js +30 -0
- package/src/paraglide/messages/agent_desktop_errorunavailable1.d.ts +17 -0
- package/src/paraglide/messages/agent_desktop_errorunavailable1.js +30 -0
- package/src/paraglide/messages/agent_desktop_pagedescription1.d.ts +17 -0
- package/src/paraglide/messages/agent_desktop_pagedescription1.js +30 -0
- package/src/paraglide/messages/agent_desktop_title.d.ts +16 -0
- package/src/paraglide/messages/agent_desktop_title.js +29 -0
- package/src/paraglide/messages/agent_desktop_toolcountfiles2.d.ts +19 -0
- package/src/paraglide/messages/agent_desktop_toolcountfiles2.js +30 -0
- package/src/paraglide/messages/agent_desktop_toolcounthits2.d.ts +19 -0
- package/src/paraglide/messages/agent_desktop_toolcounthits2.js +30 -0
- package/src/paraglide/messages/agent_desktop_toolcountlines2.d.ts +19 -0
- package/src/paraglide/messages/agent_desktop_toolcountlines2.js +30 -0
- package/src/paraglide/messages/agent_desktop_toolemptydir2.d.ts +17 -0
- package/src/paraglide/messages/agent_desktop_toolemptydir2.js +30 -0
- package/src/paraglide/messages/agent_desktop_toolerror1.d.ts +17 -0
- package/src/paraglide/messages/agent_desktop_toolerror1.js +30 -0
- package/src/paraglide/messages/agent_desktop_toolfind1.d.ts +19 -0
- package/src/paraglide/messages/agent_desktop_toolfind1.js +30 -0
- package/src/paraglide/messages/agent_desktop_toolgrep1.d.ts +19 -0
- package/src/paraglide/messages/agent_desktop_toolgrep1.js +30 -0
- package/src/paraglide/messages/agent_desktop_toolls1.d.ts +19 -0
- package/src/paraglide/messages/agent_desktop_toolls1.js +30 -0
- package/src/paraglide/messages/agent_desktop_toolmore1.d.ts +19 -0
- package/src/paraglide/messages/agent_desktop_toolmore1.js +30 -0
- package/src/paraglide/messages/agent_desktop_toolnomatches2.d.ts +17 -0
- package/src/paraglide/messages/agent_desktop_toolnomatches2.js +30 -0
- package/src/paraglide/messages/agent_desktop_toolother1.d.ts +19 -0
- package/src/paraglide/messages/agent_desktop_toolother1.js +30 -0
- package/src/paraglide/messages/agent_desktop_toolread1.d.ts +19 -0
- package/src/paraglide/messages/agent_desktop_toolread1.js +30 -0
- package/src/paraglide/messages/agent_desktop_toolrunning1.d.ts +17 -0
- package/src/paraglide/messages/agent_desktop_toolrunning1.js +30 -0
- package/src/paraglide/messages/agent_desktop_tooltruncated1.d.ts +17 -0
- package/src/paraglide/messages/agent_desktop_tooltruncated1.js +30 -0
- package/src/paraglide/messages/agent_desktop_workspaceadd1.d.ts +17 -0
- package/src/paraglide/messages/agent_desktop_workspaceadd1.js +30 -0
- package/src/paraglide/messages/agent_desktop_workspaceaddfailed2.d.ts +17 -0
- package/src/paraglide/messages/agent_desktop_workspaceaddfailed2.js +30 -0
- package/src/paraglide/messages/agent_desktop_workspacedescription1.d.ts +17 -0
- package/src/paraglide/messages/agent_desktop_workspacedescription1.js +30 -0
- package/src/paraglide/messages/agent_desktop_workspaceempty1.d.ts +17 -0
- package/src/paraglide/messages/agent_desktop_workspaceempty1.js +30 -0
- package/src/paraglide/messages/agent_desktop_workspaceemptyhint2.d.ts +17 -0
- package/src/paraglide/messages/agent_desktop_workspaceemptyhint2.js +30 -0
- package/src/paraglide/messages/agent_desktop_workspaceloadfailed2.d.ts +17 -0
- package/src/paraglide/messages/agent_desktop_workspaceloadfailed2.js +30 -0
- package/src/paraglide/messages/agent_desktop_workspaceremove1.d.ts +17 -0
- package/src/paraglide/messages/agent_desktop_workspaceremove1.js +30 -0
- package/src/paraglide/messages/agent_desktop_workspaceremovefailed2.d.ts +17 -0
- package/src/paraglide/messages/agent_desktop_workspaceremovefailed2.js +30 -0
- package/src/paraglide/messages/agent_desktop_workspacetitle1.d.ts +17 -0
- package/src/paraglide/messages/agent_desktop_workspacetitle1.js +30 -0
- package/src/paraglide/messages/agent_desktop_workspaceunsupported1.d.ts +17 -0
- package/src/paraglide/messages/agent_desktop_workspaceunsupported1.js +30 -0
- package/src/paraglide/messages/common_hotkey_desc_agentabort1.d.ts +17 -0
- package/src/paraglide/messages/common_hotkey_desc_agentabort1.js +30 -0
- package/src/paraglide/messages/sync_bind_rolled_back.d.ts +16 -0
- package/src/paraglide/messages/sync_bind_rolled_back.js +29 -0
- package/src/routeTree.gen.ts +23 -0
- package/src/routes/-tests/agent-gate.test.ts +45 -0
- package/src/routes/_nav/agent/route.lazy.tsx +63 -0
- package/src/routes/_nav/agent/route.tsx +21 -0
- package/src/routes/api/sync/account-status.ts +4 -2
- package/src/routes/api/sync/push.ts +11 -3
- package/src/server/__tests__/desktop-cloud-identity.test.ts +331 -0
- package/src/server/__tests__/desktop-session-merge.test.ts +42 -0
- package/src/server/__tests__/exchange-remote-token.test.ts +92 -10
- package/src/server/__tests__/remote-auth.test.ts +39 -0
- package/src/server/desktop-bootstrap.ts +16 -5
- package/src/server/desktop-cloud-identity.ts +247 -0
- package/src/server/remote-auth.ts +33 -1
- package/src/server/serverFns/auth/session.ts +27 -2
- package/src/server/serverFns/sync/transfer.ts +28 -27
- package/src/server/serverFns/workspace-kb.ts +10 -0
- package/src/server/sync/__tests__/ownership-transfer.test.ts +261 -48
- package/src/server/sync/__tests__/sync-first-push.e2e.test.ts +349 -0
- package/src/server/sync/account-status-cloud.ts +41 -0
- package/src/server/sync/ownership-transfer.ts +88 -49
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
// SPDX-License-Identifier: AGPL-3.0-or-later
|
|
2
|
+
// AllBlue - 理想之海
|
|
3
|
+
// Copyright (C) 2026 AllBlue Contributors
|
|
4
|
+
|
|
5
|
+
// 本地文件助手 · 授权目录卡(桌面本地优先 P3-B5):真实 QueryClient + 可控 fake
|
|
6
|
+
// bridge——listWorkspaces 走 queryOptions 工厂;pickDirectory 成功 → invalidate
|
|
7
|
+
// 重取清单 + 选中新目录;取消(null)不动;removeWorkspace 成功 → invalidate;
|
|
8
|
+
// 旧壳(无三方法)降级提示;agent 终态不可用(presence absent)同样降级;门控
|
|
9
|
+
// 探测/重试中(pending)按加载态;IPC 失败 toast。
|
|
10
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
|
11
|
+
import { cleanup, fireEvent, render, screen, waitFor } from '@testing-library/react'
|
|
12
|
+
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
|
|
13
|
+
import { TooltipProvider } from '@/components/ui/tooltip'
|
|
14
|
+
import { m } from '@/paraglide/messages.js'
|
|
15
|
+
|
|
16
|
+
const state = vi.hoisted(() => ({
|
|
17
|
+
bridge: null as unknown,
|
|
18
|
+
toastError: vi.fn(),
|
|
19
|
+
presence: 'present' as 'pending' | 'present' | 'absent',
|
|
20
|
+
}))
|
|
21
|
+
|
|
22
|
+
vi.mock('@/lib/desktop-bridge-types', () => ({
|
|
23
|
+
getDesktopBridge: () => state.bridge,
|
|
24
|
+
}))
|
|
25
|
+
// 门控判定(agentPort 装配)在 availability 侧的用例见 use-desktop-agent-presence.test.tsx;
|
|
26
|
+
// 本文件固定 presence 值,专注卡片自身的降级/交互。
|
|
27
|
+
vi.mock('@/lib/desktop-agent/use-desktop-agent-presence', () => ({
|
|
28
|
+
useDesktopAgentPresence: () => state.presence,
|
|
29
|
+
}))
|
|
30
|
+
vi.mock('@/components/ui/toast', () => ({
|
|
31
|
+
toast: { error: state.toastError, success: vi.fn() },
|
|
32
|
+
}))
|
|
33
|
+
|
|
34
|
+
import { AgentWorkspaceCard } from '../agent-workspace-card'
|
|
35
|
+
|
|
36
|
+
function makeBridge(initial: string[]) {
|
|
37
|
+
let workspaces = [...initial]
|
|
38
|
+
return {
|
|
39
|
+
listWorkspaces: vi.fn(async () => [...workspaces]),
|
|
40
|
+
pickDirectory: vi.fn(async (): Promise<string | null> => null),
|
|
41
|
+
removeWorkspace: vi.fn(async (path: string) => {
|
|
42
|
+
workspaces = workspaces.filter((w) => w !== path)
|
|
43
|
+
}),
|
|
44
|
+
/** 测试辅助:模拟 main 侧 pickDirectory 写入清单 */
|
|
45
|
+
authorize(path: string) {
|
|
46
|
+
workspaces = [...workspaces, path]
|
|
47
|
+
},
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function renderCard(props: { selected?: string | null; onSelect?: (p: string) => void; disabled?: boolean } = {}) {
|
|
52
|
+
const client = new QueryClient({ defaultOptions: { queries: { retry: false } } })
|
|
53
|
+
const onSelect = props.onSelect ?? vi.fn()
|
|
54
|
+
const { container } = render(
|
|
55
|
+
<QueryClientProvider client={client}>
|
|
56
|
+
<TooltipProvider>
|
|
57
|
+
<AgentWorkspaceCard selected={props.selected ?? null} onSelect={onSelect} disabled={props.disabled} />
|
|
58
|
+
</TooltipProvider>
|
|
59
|
+
</QueryClientProvider>,
|
|
60
|
+
)
|
|
61
|
+
return { onSelect, container }
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
beforeEach(() => {
|
|
65
|
+
state.toastError.mockReset()
|
|
66
|
+
})
|
|
67
|
+
|
|
68
|
+
afterEach(() => {
|
|
69
|
+
cleanup()
|
|
70
|
+
state.bridge = null
|
|
71
|
+
state.presence = 'present'
|
|
72
|
+
})
|
|
73
|
+
|
|
74
|
+
describe('AgentWorkspaceCard', () => {
|
|
75
|
+
it('渲染授权清单:目录名 + 完整路径,当前目录单选选中', async () => {
|
|
76
|
+
state.bridge = makeBridge(['/Users/x/proj-a', '/Users/x/proj-b'])
|
|
77
|
+
renderCard({ selected: '/Users/x/proj-b' })
|
|
78
|
+
expect(await screen.findByText('proj-a')).toBeInTheDocument()
|
|
79
|
+
expect(screen.getByText('/Users/x/proj-a')).toBeInTheDocument()
|
|
80
|
+
expect(screen.getByText('proj-b')).toBeInTheDocument()
|
|
81
|
+
const radios = screen.getAllByRole('radio')
|
|
82
|
+
expect(radios).toHaveLength(2)
|
|
83
|
+
expect(radios[1]).toHaveAttribute('aria-checked', 'true')
|
|
84
|
+
expect(radios[0]).toHaveAttribute('aria-checked', 'false')
|
|
85
|
+
})
|
|
86
|
+
|
|
87
|
+
it('点选其他目录 → onSelect(该路径)', async () => {
|
|
88
|
+
state.bridge = makeBridge(['/Users/x/proj-a', '/Users/x/proj-b'])
|
|
89
|
+
const { onSelect } = renderCard({ selected: '/Users/x/proj-a' })
|
|
90
|
+
await screen.findByText('proj-b')
|
|
91
|
+
fireEvent.click(screen.getAllByRole('radio')[1]!)
|
|
92
|
+
await waitFor(() => expect(onSelect).toHaveBeenCalledWith('/Users/x/proj-b'))
|
|
93
|
+
})
|
|
94
|
+
|
|
95
|
+
it('添加目录:pickDirectory 成功 → 清单重取(invalidate)且选中新目录', async () => {
|
|
96
|
+
const bridge = makeBridge(['/Users/x/proj-a'])
|
|
97
|
+
bridge.pickDirectory.mockImplementation(async () => {
|
|
98
|
+
bridge.authorize('/Users/x/new-proj')
|
|
99
|
+
return '/Users/x/new-proj'
|
|
100
|
+
})
|
|
101
|
+
state.bridge = bridge
|
|
102
|
+
const { onSelect } = renderCard({ selected: '/Users/x/proj-a' })
|
|
103
|
+
await screen.findByText('proj-a')
|
|
104
|
+
expect(bridge.listWorkspaces).toHaveBeenCalledTimes(1)
|
|
105
|
+
fireEvent.click(screen.getByRole('button', { name: m.agent_desktop_workspaceAdd() }))
|
|
106
|
+
expect(await screen.findByText('new-proj')).toBeInTheDocument()
|
|
107
|
+
expect(bridge.pickDirectory).toHaveBeenCalledTimes(1)
|
|
108
|
+
expect(bridge.listWorkspaces).toHaveBeenCalledTimes(2)
|
|
109
|
+
expect(onSelect).toHaveBeenCalledWith('/Users/x/new-proj')
|
|
110
|
+
})
|
|
111
|
+
|
|
112
|
+
it('取消目录对话框(pickDirectory → null):不重取、不改选', async () => {
|
|
113
|
+
const bridge = makeBridge(['/Users/x/proj-a'])
|
|
114
|
+
state.bridge = bridge
|
|
115
|
+
const { onSelect } = renderCard({ selected: '/Users/x/proj-a' })
|
|
116
|
+
await screen.findByText('proj-a')
|
|
117
|
+
fireEvent.click(screen.getByRole('button', { name: m.agent_desktop_workspaceAdd() }))
|
|
118
|
+
await waitFor(() => expect(bridge.pickDirectory).toHaveBeenCalledTimes(1))
|
|
119
|
+
await waitFor(() =>
|
|
120
|
+
expect(screen.getByRole('button', { name: m.agent_desktop_workspaceAdd() })).not.toBeDisabled(),
|
|
121
|
+
)
|
|
122
|
+
expect(bridge.listWorkspaces).toHaveBeenCalledTimes(1)
|
|
123
|
+
expect(onSelect).not.toHaveBeenCalled()
|
|
124
|
+
})
|
|
125
|
+
|
|
126
|
+
it('撤销授权:removeWorkspace(路径) → 清单重取后该目录消失', async () => {
|
|
127
|
+
const bridge = makeBridge(['/Users/x/proj-a', '/Users/x/proj-b'])
|
|
128
|
+
state.bridge = bridge
|
|
129
|
+
renderCard({ selected: '/Users/x/proj-a' })
|
|
130
|
+
await screen.findByText('proj-b')
|
|
131
|
+
fireEvent.click(screen.getAllByRole('button', { name: m.agent_desktop_workspaceRemove() })[1]!)
|
|
132
|
+
await waitFor(() => expect(screen.queryByText('proj-b')).not.toBeInTheDocument())
|
|
133
|
+
expect(bridge.removeWorkspace).toHaveBeenCalledWith('/Users/x/proj-b')
|
|
134
|
+
expect(screen.getByText('proj-a')).toBeInTheDocument()
|
|
135
|
+
})
|
|
136
|
+
|
|
137
|
+
it('空清单:空态提示(引导先添加目录)', async () => {
|
|
138
|
+
state.bridge = makeBridge([])
|
|
139
|
+
renderCard()
|
|
140
|
+
expect(await screen.findByText(m.agent_desktop_workspaceEmpty())).toBeInTheDocument()
|
|
141
|
+
expect(screen.queryAllByRole('radio')).toHaveLength(0)
|
|
142
|
+
})
|
|
143
|
+
|
|
144
|
+
it('旧壳(无 pickDirectory/listWorkspaces/removeWorkspace):升级提示,无添加按钮', () => {
|
|
145
|
+
state.bridge = { appInfo: vi.fn() }
|
|
146
|
+
renderCard()
|
|
147
|
+
expect(screen.getByText(m.agent_desktop_workspaceUnsupported())).toBeInTheDocument()
|
|
148
|
+
expect(screen.queryByRole('button', { name: m.agent_desktop_workspaceAdd() })).toBeNull()
|
|
149
|
+
})
|
|
150
|
+
|
|
151
|
+
it('agent 终态不可用(presence absent):即使三方法齐备也降级为升级提示,且不发起清单查询', () => {
|
|
152
|
+
state.presence = 'absent'
|
|
153
|
+
const bridge = makeBridge(['/Users/x/proj-a'])
|
|
154
|
+
state.bridge = bridge
|
|
155
|
+
renderCard()
|
|
156
|
+
expect(screen.getByText(m.agent_desktop_workspaceUnsupported())).toBeInTheDocument()
|
|
157
|
+
expect(screen.queryByRole('button', { name: m.agent_desktop_workspaceAdd() })).toBeNull()
|
|
158
|
+
expect(bridge.listWorkspaces).not.toHaveBeenCalled()
|
|
159
|
+
})
|
|
160
|
+
|
|
161
|
+
it('门控探测 / 冷启动重试中(presence pending):按加载态渲染,不提前报升级、不查清单', () => {
|
|
162
|
+
state.presence = 'pending'
|
|
163
|
+
const bridge = makeBridge(['/Users/x/proj-a'])
|
|
164
|
+
state.bridge = bridge
|
|
165
|
+
const { container } = renderCard()
|
|
166
|
+
expect(screen.queryByText(m.agent_desktop_workspaceUnsupported())).toBeNull()
|
|
167
|
+
expect(container.querySelectorAll('[data-slot="skeleton"]').length).toBeGreaterThan(0)
|
|
168
|
+
expect(bridge.listWorkspaces).not.toHaveBeenCalled()
|
|
169
|
+
})
|
|
170
|
+
|
|
171
|
+
it('添加失败(IPC 抛错)→ toast 错误提示', async () => {
|
|
172
|
+
const bridge = makeBridge(['/Users/x/proj-a'])
|
|
173
|
+
bridge.pickDirectory.mockRejectedValue(new Error('dialog failed'))
|
|
174
|
+
state.bridge = bridge
|
|
175
|
+
renderCard({ selected: '/Users/x/proj-a' })
|
|
176
|
+
await screen.findByText('proj-a')
|
|
177
|
+
fireEvent.click(screen.getByRole('button', { name: m.agent_desktop_workspaceAdd() }))
|
|
178
|
+
await waitFor(() => expect(state.toastError).toHaveBeenCalledWith(m.agent_desktop_workspaceAddFailed()))
|
|
179
|
+
})
|
|
180
|
+
|
|
181
|
+
it('运行中锁定(disabled):单选与撤销不可操作', async () => {
|
|
182
|
+
state.bridge = makeBridge(['/Users/x/proj-a', '/Users/x/proj-b'])
|
|
183
|
+
renderCard({ selected: '/Users/x/proj-a', disabled: true })
|
|
184
|
+
await screen.findByText('proj-b')
|
|
185
|
+
for (const btn of screen.getAllByRole('button', { name: m.agent_desktop_workspaceRemove() })) {
|
|
186
|
+
expect(btn).toBeDisabled()
|
|
187
|
+
}
|
|
188
|
+
})
|
|
189
|
+
})
|
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
// SPDX-License-Identifier: AGPL-3.0-or-later
|
|
2
|
+
// AllBlue - 理想之海
|
|
3
|
+
// Copyright (C) 2026 AllBlue Contributors
|
|
4
|
+
|
|
5
|
+
// 本地文件助手 · 对话区(桌面本地优先 P3-B5):useChat 直连 Pro desktop 域
|
|
6
|
+
// AgentService 的 `POST /prompt`(AG-UI SSE)。
|
|
7
|
+
// - connection:lib/desktop-agent/connection 包装——每次 connect 懒取 appInfo 的
|
|
8
|
+
// agentPort/agentToken(backend 重启换端口/令牌后下一问即用新值),引用稳定。
|
|
9
|
+
// - threadId:mount 级稳定值(useChat 1.0 起必填,变化即重建 ChatClient)。B4 端点
|
|
10
|
+
// 每次 POST 新建 pi 会话,threadId 仅作 run 归属回显——一期不保留多轮上下文,
|
|
11
|
+
// 输入区常驻提示告知。
|
|
12
|
+
// - cwd:经 forwardedProps 进 RunAgentInput.forwardedProps(端点 extractCwd 取值位,
|
|
13
|
+
// 服务端再按 workspaces.json 复核授权);父级以 key={cwd} 让每个目录一份会话。
|
|
14
|
+
// - 消息:user 气泡 / assistant Markdown + 工具调用折叠卡(同 toolCallId 的
|
|
15
|
+
// tool-result 已由 processor 挂到 tool-call part.output,不重复渲染);pi 对
|
|
16
|
+
// user / toolResult 消息也发 message_start/end,processor 会建空 assistant 消息,
|
|
17
|
+
// 渲染层跳过(B3B4 报告疑虑 3)。
|
|
18
|
+
// - 输入:Enter 发送 / Shift+Enter 换行 / 输入法组合中的 Enter 不发送。
|
|
19
|
+
// - 中止:运行中发送按钮换成「中止」+ 应用热键 agent-abort(Mod+.,T23 声明即绑定,
|
|
20
|
+
// 仅运行中 enabled)。
|
|
21
|
+
import { useEffect, useMemo, useRef, useState, type KeyboardEvent, type UIEvent } from 'react'
|
|
22
|
+
import { useChat } from '@tanstack/ai-react'
|
|
23
|
+
import type { UIMessage } from '@tanstack/ai-client'
|
|
24
|
+
import { useAppHotkeys, useHotkey } from '@tanstack/react-hotkeys'
|
|
25
|
+
import { ArrowUp, CircleAlert, FolderSearch, Loader2, MessageSquareText, Square } from 'lucide-react'
|
|
26
|
+
import { AgentToolCard, type AgentToolResultPart } from '@/components/desktop-agent/agent-tool-card'
|
|
27
|
+
import { MarkdownContent } from '@/components/harness/ai-message/components/markdown-content'
|
|
28
|
+
import { ShortcutBadge } from '@/components/hotkeys/shortcut-badge'
|
|
29
|
+
import { Empty, EmptyDescription, EmptyHeader, EmptyMedia, EmptyTitle } from '@/components/ui/empty'
|
|
30
|
+
import {
|
|
31
|
+
InputGroup,
|
|
32
|
+
InputGroupAddon,
|
|
33
|
+
InputGroupButton,
|
|
34
|
+
InputGroupTextarea,
|
|
35
|
+
} from '@/components/ui/input-group'
|
|
36
|
+
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip'
|
|
37
|
+
import { createDesktopAgentConnection } from '@/lib/desktop-agent/connection'
|
|
38
|
+
import { describeDesktopAgentError } from '@/lib/desktop-agent/errors'
|
|
39
|
+
import { formatAppHotkeyForBadge } from '@/lib/hotkeys/registry'
|
|
40
|
+
import { m } from '@/paraglide/messages.js'
|
|
41
|
+
|
|
42
|
+
/** 距底部小于该值视为「贴底」:新内容到达时自动滚到底;用户上翻阅读时不打断 */
|
|
43
|
+
const STICK_TO_BOTTOM_PX = 80
|
|
44
|
+
|
|
45
|
+
/** 有可见内容:user 有正文;assistant 有非空正文或工具调用(空消息对跳过) */
|
|
46
|
+
function hasVisibleContent(message: UIMessage): boolean {
|
|
47
|
+
return message.parts.some((part) =>
|
|
48
|
+
part.type === 'text' ? part.content.trim() !== '' : part.type === 'tool-call',
|
|
49
|
+
)
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function AgentMessage({ message }: { message: UIMessage }) {
|
|
53
|
+
if (message.role === 'user') {
|
|
54
|
+
const text = message.parts.map((part) => (part.type === 'text' ? part.content : '')).join('')
|
|
55
|
+
return (
|
|
56
|
+
<div className="flex justify-end" data-testid="agent-message" data-role="user">
|
|
57
|
+
<div className="max-w-xl rounded-2xl rounded-br-sm bg-primary px-3.5 py-2 text-sm whitespace-pre-wrap text-primary-foreground">
|
|
58
|
+
{text}
|
|
59
|
+
</div>
|
|
60
|
+
</div>
|
|
61
|
+
)
|
|
62
|
+
}
|
|
63
|
+
const results = new Map<string, AgentToolResultPart>()
|
|
64
|
+
for (const part of message.parts) {
|
|
65
|
+
if (part.type === 'tool-result') results.set(part.toolCallId, part)
|
|
66
|
+
}
|
|
67
|
+
return (
|
|
68
|
+
<div className="flex flex-col gap-2 text-sm" data-testid="agent-message" data-role="assistant">
|
|
69
|
+
{message.parts.map((part, index) => {
|
|
70
|
+
if (part.type === 'text') {
|
|
71
|
+
// parts 流式只追加不重排,index key 稳定
|
|
72
|
+
return part.content.trim() === '' ? null : (
|
|
73
|
+
<MarkdownContent key={index} content={part.content} skipCodeHighlighting />
|
|
74
|
+
)
|
|
75
|
+
}
|
|
76
|
+
if (part.type === 'tool-call') {
|
|
77
|
+
return <AgentToolCard key={part.id || index} part={part} result={results.get(part.id)} />
|
|
78
|
+
}
|
|
79
|
+
return null
|
|
80
|
+
})}
|
|
81
|
+
</div>
|
|
82
|
+
)
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export interface AgentChatProps {
|
|
86
|
+
/** 当前工作目录(授权清单选中项);null = 尚未授权/选择,输入禁用 */
|
|
87
|
+
cwd: string | null
|
|
88
|
+
/** 运行态变化通知父级(运行中锁定目录切换);须传稳定引用(如 setState) */
|
|
89
|
+
onRunningChange?: (running: boolean) => void
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export function AgentChat({ cwd, onRunningChange }: AgentChatProps) {
|
|
93
|
+
const [connection] = useState(() => createDesktopAgentConnection())
|
|
94
|
+
const [threadId] = useState(() => crypto.randomUUID())
|
|
95
|
+
const forwardedProps = useMemo(() => ({ cwd: cwd ?? '' }), [cwd])
|
|
96
|
+
const { messages, sendMessage, stop, isLoading, error } = useChat({ connection, threadId, forwardedProps })
|
|
97
|
+
|
|
98
|
+
const [input, setInput] = useState('')
|
|
99
|
+
const scrollRef = useRef<HTMLDivElement>(null)
|
|
100
|
+
const stickToBottomRef = useRef(true)
|
|
101
|
+
|
|
102
|
+
// 声明即绑定:agent-abort 单一来源 lib/hotkeys/registry.ts
|
|
103
|
+
const hk = useAppHotkeys()
|
|
104
|
+
useHotkey(hk['agent-abort'], () => stop(), { enabled: isLoading })
|
|
105
|
+
|
|
106
|
+
useEffect(() => {
|
|
107
|
+
onRunningChange?.(isLoading)
|
|
108
|
+
}, [isLoading, onRunningChange])
|
|
109
|
+
// 运行中被卸载(离开页面):解除父级锁定
|
|
110
|
+
useEffect(() => () => onRunningChange?.(false), [onRunningChange])
|
|
111
|
+
|
|
112
|
+
const visible = useMemo(() => messages.filter(hasVisibleContent), [messages])
|
|
113
|
+
|
|
114
|
+
useEffect(() => {
|
|
115
|
+
const el = scrollRef.current
|
|
116
|
+
if (el && stickToBottomRef.current) el.scrollTop = el.scrollHeight
|
|
117
|
+
}, [visible, isLoading])
|
|
118
|
+
|
|
119
|
+
const onScroll = (event: UIEvent<HTMLDivElement>) => {
|
|
120
|
+
const el = event.currentTarget
|
|
121
|
+
stickToBottomRef.current = el.scrollHeight - el.scrollTop - el.clientHeight < STICK_TO_BOTTOM_PX
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
const canSend = cwd !== null && input.trim() !== '' && !isLoading
|
|
125
|
+
|
|
126
|
+
const submit = () => {
|
|
127
|
+
if (!canSend) return
|
|
128
|
+
const text = input.trim()
|
|
129
|
+
setInput('')
|
|
130
|
+
stickToBottomRef.current = true
|
|
131
|
+
// 失败由 useChat error 态渲染(describeDesktopAgentError),这里只吞 unhandledrejection
|
|
132
|
+
Promise.resolve(sendMessage(text)).catch(() => {})
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const onKeyDown = (event: KeyboardEvent<HTMLTextAreaElement>) => {
|
|
136
|
+
if (event.key !== 'Enter' || event.shiftKey || event.nativeEvent.isComposing) return
|
|
137
|
+
event.preventDefault()
|
|
138
|
+
submit()
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
return (
|
|
142
|
+
<section className="flex min-h-0 flex-1 flex-col overflow-hidden rounded-xl border bg-card">
|
|
143
|
+
<div className="flex items-center gap-2 border-b px-4 py-2.5 text-xs text-muted-foreground">
|
|
144
|
+
<FolderSearch className="size-3.5 shrink-0" />
|
|
145
|
+
<span className="min-w-0 truncate" data-testid="agent-chat-cwd">
|
|
146
|
+
{cwd ? m.agent_desktop_chatCwd({ path: cwd }) : m.agent_desktop_chatNoWorkspace()}
|
|
147
|
+
</span>
|
|
148
|
+
</div>
|
|
149
|
+
|
|
150
|
+
<div ref={scrollRef} onScroll={onScroll} className="min-h-0 flex-1 overflow-y-auto overscroll-contain px-4 py-4">
|
|
151
|
+
{visible.length === 0 && !isLoading ? (
|
|
152
|
+
<Empty className="h-full">
|
|
153
|
+
<EmptyHeader>
|
|
154
|
+
<EmptyMedia variant="icon">
|
|
155
|
+
<MessageSquareText />
|
|
156
|
+
</EmptyMedia>
|
|
157
|
+
<EmptyTitle>{m.agent_desktop_chatEmptyTitle()}</EmptyTitle>
|
|
158
|
+
<EmptyDescription>{m.agent_desktop_chatEmptyHint()}</EmptyDescription>
|
|
159
|
+
</EmptyHeader>
|
|
160
|
+
</Empty>
|
|
161
|
+
) : (
|
|
162
|
+
<div className="mx-auto flex w-full max-w-3xl flex-col gap-5">
|
|
163
|
+
{visible.map((message) => (
|
|
164
|
+
<AgentMessage key={message.id} message={message} />
|
|
165
|
+
))}
|
|
166
|
+
{isLoading && (
|
|
167
|
+
<div className="flex items-center gap-2 text-sm text-muted-foreground" data-testid="agent-chat-thinking">
|
|
168
|
+
<Loader2 className="size-4 animate-spin" />
|
|
169
|
+
{m.agent_desktop_chatThinking()}
|
|
170
|
+
</div>
|
|
171
|
+
)}
|
|
172
|
+
</div>
|
|
173
|
+
)}
|
|
174
|
+
</div>
|
|
175
|
+
|
|
176
|
+
{error && (
|
|
177
|
+
<div
|
|
178
|
+
role="alert"
|
|
179
|
+
className="mx-4 mb-2 flex items-start gap-2 rounded-lg border border-destructive/40 bg-destructive/5 px-3 py-2 text-xs text-destructive"
|
|
180
|
+
>
|
|
181
|
+
<CircleAlert className="mt-0.5 size-3.5 shrink-0" />
|
|
182
|
+
<span className="min-w-0 wrap-break-word">{describeDesktopAgentError(error)}</span>
|
|
183
|
+
</div>
|
|
184
|
+
)}
|
|
185
|
+
|
|
186
|
+
<div className="border-t p-3">
|
|
187
|
+
<InputGroup>
|
|
188
|
+
<InputGroupTextarea
|
|
189
|
+
value={input}
|
|
190
|
+
onChange={(event) => setInput(event.target.value)}
|
|
191
|
+
onKeyDown={onKeyDown}
|
|
192
|
+
disabled={cwd === null}
|
|
193
|
+
placeholder={cwd ? m.agent_desktop_chatPlaceholder() : m.agent_desktop_chatNoWorkspace()}
|
|
194
|
+
className="max-h-40"
|
|
195
|
+
/>
|
|
196
|
+
<InputGroupAddon align="block-end">
|
|
197
|
+
<span className="min-w-0 truncate text-xs font-normal">{m.agent_desktop_chatIndependentHint()}</span>
|
|
198
|
+
{isLoading ? (
|
|
199
|
+
<Tooltip>
|
|
200
|
+
<TooltipTrigger
|
|
201
|
+
render={<InputGroupButton variant="outline" size="sm" className="ml-auto" onClick={() => stop()} />}
|
|
202
|
+
>
|
|
203
|
+
<Square className="fill-current" />
|
|
204
|
+
{m.agent_desktop_chatStop()}
|
|
205
|
+
</TooltipTrigger>
|
|
206
|
+
<TooltipContent>
|
|
207
|
+
<ShortcutBadge keys={formatAppHotkeyForBadge(hk['agent-abort'].hotkey)} />
|
|
208
|
+
</TooltipContent>
|
|
209
|
+
</Tooltip>
|
|
210
|
+
) : (
|
|
211
|
+
<InputGroupButton variant="default" size="sm" className="ml-auto" disabled={!canSend} onClick={submit}>
|
|
212
|
+
<ArrowUp />
|
|
213
|
+
{m.agent_desktop_chatSend()}
|
|
214
|
+
</InputGroupButton>
|
|
215
|
+
)}
|
|
216
|
+
</InputGroupAddon>
|
|
217
|
+
</InputGroup>
|
|
218
|
+
</div>
|
|
219
|
+
</section>
|
|
220
|
+
)
|
|
221
|
+
}
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
// SPDX-License-Identifier: AGPL-3.0-or-later
|
|
2
|
+
// AllBlue - 理想之海
|
|
3
|
+
// Copyright (C) 2026 AllBlue Contributors
|
|
4
|
+
|
|
5
|
+
// 本地文件助手 · 工具调用折叠卡(桌面本地优先 P3-B5):pi 只读四件套的每次调用
|
|
6
|
+
// 渲染为一张默认折叠的卡——头部「工具 + 目标 + 计数」,展开后列出文件名(ls /
|
|
7
|
+
// find)、命中行(grep:文件:行号 + 行内容)或文件内容(read)。风格对齐 hextaui
|
|
8
|
+
// AI 分组 / 站内 components/ai/tool-cards(rounded-lg border bg-muted/40 text-xs +
|
|
9
|
+
// 折叠头),折叠用 shadcn Collapsible(Base UI)。解析逻辑在 lib/desktop-agent/tool-result。
|
|
10
|
+
import { useState } from 'react'
|
|
11
|
+
import type { UIMessage } from '@tanstack/ai-client'
|
|
12
|
+
import {
|
|
13
|
+
ChevronRight,
|
|
14
|
+
FileSearch,
|
|
15
|
+
FileText,
|
|
16
|
+
Folder,
|
|
17
|
+
FolderTree,
|
|
18
|
+
Loader2,
|
|
19
|
+
Search,
|
|
20
|
+
Wrench,
|
|
21
|
+
type LucideIcon,
|
|
22
|
+
} from 'lucide-react'
|
|
23
|
+
import { Badge } from '@/components/ui/badge'
|
|
24
|
+
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from '@/components/ui/collapsible'
|
|
25
|
+
import {
|
|
26
|
+
extractAgentToolText,
|
|
27
|
+
parseAgentToolArgs,
|
|
28
|
+
summarizeAgentTool,
|
|
29
|
+
type AgentToolKind,
|
|
30
|
+
type AgentToolSummary,
|
|
31
|
+
} from '@/lib/desktop-agent/tool-result'
|
|
32
|
+
import { cn } from '@/lib/utils'
|
|
33
|
+
import { m } from '@/paraglide/messages.js'
|
|
34
|
+
|
|
35
|
+
type MessagePart = UIMessage['parts'][number]
|
|
36
|
+
export type AgentToolCallPart = Extract<MessagePart, { type: 'tool-call' }>
|
|
37
|
+
export type AgentToolResultPart = Extract<MessagePart, { type: 'tool-result' }>
|
|
38
|
+
|
|
39
|
+
const KIND_ICON: Record<AgentToolKind, LucideIcon> = {
|
|
40
|
+
read: FileText,
|
|
41
|
+
grep: Search,
|
|
42
|
+
find: FileSearch,
|
|
43
|
+
ls: FolderTree,
|
|
44
|
+
other: Wrench,
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// 文案在渲染时解析(paraglide 按当前 locale);非导出函数保 Fast Refresh 纯组件边界
|
|
48
|
+
function titleOf(summary: AgentToolSummary, toolName: string): string {
|
|
49
|
+
const target = summary.target ?? '…'
|
|
50
|
+
switch (summary.kind) {
|
|
51
|
+
case 'read':
|
|
52
|
+
return m.agent_desktop_toolRead({ target })
|
|
53
|
+
case 'grep':
|
|
54
|
+
return m.agent_desktop_toolGrep({ target })
|
|
55
|
+
case 'find':
|
|
56
|
+
return m.agent_desktop_toolFind({ target })
|
|
57
|
+
case 'ls':
|
|
58
|
+
return m.agent_desktop_toolLs({ target })
|
|
59
|
+
default:
|
|
60
|
+
return m.agent_desktop_toolOther({ name: toolName })
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function countOf(summary: AgentToolSummary): string | null {
|
|
65
|
+
if (summary.empty) return summary.kind === 'ls' ? m.agent_desktop_toolEmptyDir() : m.agent_desktop_toolNoMatches()
|
|
66
|
+
if (summary.total === 0) return null
|
|
67
|
+
switch (summary.kind) {
|
|
68
|
+
case 'grep':
|
|
69
|
+
return m.agent_desktop_toolCountHits({ count: summary.total })
|
|
70
|
+
case 'ls':
|
|
71
|
+
case 'find':
|
|
72
|
+
return m.agent_desktop_toolCountFiles({ count: summary.total })
|
|
73
|
+
default:
|
|
74
|
+
return m.agent_desktop_toolCountLines({ count: summary.total })
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function ToolEntries({ summary }: { summary: AgentToolSummary }) {
|
|
79
|
+
const hidden = summary.total - summary.entries.length
|
|
80
|
+
return (
|
|
81
|
+
<div className="flex flex-col gap-1.5">
|
|
82
|
+
{summary.kind === 'read' || summary.kind === 'other' ? (
|
|
83
|
+
<pre className="max-h-60 overflow-auto rounded bg-background p-2 font-mono whitespace-pre-wrap break-all text-foreground">
|
|
84
|
+
{summary.entries.map((entry) => (entry.kind === 'text' ? entry.text : '')).join('\n')}
|
|
85
|
+
</pre>
|
|
86
|
+
) : (
|
|
87
|
+
<ul className="flex max-h-60 flex-col gap-0.5 overflow-auto font-mono">
|
|
88
|
+
{summary.entries.map((entry, index) =>
|
|
89
|
+
entry.kind === 'hit' ? (
|
|
90
|
+
<li key={index} className="flex min-w-0 gap-2 rounded px-1.5 py-0.5 hover:bg-background">
|
|
91
|
+
<span className="shrink-0 text-muted-foreground">{`${entry.path}:${entry.line}`}</span>
|
|
92
|
+
<span className="min-w-0 truncate">{entry.text}</span>
|
|
93
|
+
</li>
|
|
94
|
+
) : entry.kind === 'file' ? (
|
|
95
|
+
<li key={index} className="flex min-w-0 items-center gap-1.5 rounded px-1.5 py-0.5 hover:bg-background">
|
|
96
|
+
{entry.path.endsWith('/') ? (
|
|
97
|
+
<Folder className="size-3 shrink-0 text-muted-foreground" />
|
|
98
|
+
) : (
|
|
99
|
+
<FileText className="size-3 shrink-0 text-muted-foreground" />
|
|
100
|
+
)}
|
|
101
|
+
<span className="min-w-0 truncate">{entry.path}</span>
|
|
102
|
+
</li>
|
|
103
|
+
) : null,
|
|
104
|
+
)}
|
|
105
|
+
</ul>
|
|
106
|
+
)}
|
|
107
|
+
{hidden > 0 && <p className="text-muted-foreground">{m.agent_desktop_toolMore({ count: hidden })}</p>}
|
|
108
|
+
</div>
|
|
109
|
+
)
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export interface AgentToolCardProps {
|
|
113
|
+
part: AgentToolCallPart
|
|
114
|
+
/** 同 toolCallId 的 tool-result part(output 缺席时的内容回落 + 错误原文) */
|
|
115
|
+
result?: AgentToolResultPart
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
export function AgentToolCard({ part, result }: AgentToolCardProps) {
|
|
119
|
+
const [open, setOpen] = useState(false)
|
|
120
|
+
const isError = part.state === 'error' || result?.state === 'error'
|
|
121
|
+
const settled = isError || part.output !== undefined || (result !== undefined && result.state !== 'streaming')
|
|
122
|
+
const text = settled ? extractAgentToolText(part.output, result?.content) : null
|
|
123
|
+
const summary = summarizeAgentTool(part.name, parseAgentToolArgs(part), isError ? null : text)
|
|
124
|
+
const errorText = isError ? (result?.error ?? text) : null
|
|
125
|
+
const expandable = isError ? Boolean(errorText) : summary.entries.length > 0
|
|
126
|
+
const count = settled && !isError ? countOf(summary) : null
|
|
127
|
+
const Icon = KIND_ICON[summary.kind]
|
|
128
|
+
const expanded = open && expandable
|
|
129
|
+
|
|
130
|
+
return (
|
|
131
|
+
<Collapsible
|
|
132
|
+
open={expanded}
|
|
133
|
+
onOpenChange={setOpen}
|
|
134
|
+
disabled={!expandable}
|
|
135
|
+
data-tool={part.name}
|
|
136
|
+
className={cn(
|
|
137
|
+
'rounded-lg border text-xs',
|
|
138
|
+
isError ? 'border-destructive/40 bg-destructive/5' : 'border-border bg-muted/40',
|
|
139
|
+
)}
|
|
140
|
+
>
|
|
141
|
+
<CollapsibleTrigger
|
|
142
|
+
data-testid="agent-tool-card-trigger"
|
|
143
|
+
className="flex w-full items-center gap-2 px-2.5 py-2 text-left"
|
|
144
|
+
>
|
|
145
|
+
<ChevronRight
|
|
146
|
+
className={cn(
|
|
147
|
+
'size-3.5 shrink-0 text-muted-foreground transition-transform',
|
|
148
|
+
expanded && 'rotate-90',
|
|
149
|
+
!expandable && 'opacity-0',
|
|
150
|
+
)}
|
|
151
|
+
/>
|
|
152
|
+
<Icon className="size-3.5 shrink-0 text-muted-foreground" />
|
|
153
|
+
<span className="min-w-0 flex-1 truncate font-medium">{titleOf(summary, part.name)}</span>
|
|
154
|
+
{!settled && (
|
|
155
|
+
<span className="flex shrink-0 items-center gap-1 text-muted-foreground">
|
|
156
|
+
<Loader2 className="size-3 animate-spin" />
|
|
157
|
+
{m.agent_desktop_toolRunning()}
|
|
158
|
+
</span>
|
|
159
|
+
)}
|
|
160
|
+
{isError && (
|
|
161
|
+
<Badge variant="destructive" className="shrink-0">
|
|
162
|
+
{m.agent_desktop_toolError()}
|
|
163
|
+
</Badge>
|
|
164
|
+
)}
|
|
165
|
+
{summary.truncated && (
|
|
166
|
+
<Badge variant="outline" className="shrink-0">
|
|
167
|
+
{m.agent_desktop_toolTruncated()}
|
|
168
|
+
</Badge>
|
|
169
|
+
)}
|
|
170
|
+
{count && <span className="shrink-0 text-muted-foreground">{count}</span>}
|
|
171
|
+
</CollapsibleTrigger>
|
|
172
|
+
<CollapsibleContent>
|
|
173
|
+
<div className="border-t px-2.5 py-2">
|
|
174
|
+
{isError ? (
|
|
175
|
+
<pre className="max-h-60 overflow-auto font-mono whitespace-pre-wrap break-all text-destructive">
|
|
176
|
+
{errorText}
|
|
177
|
+
</pre>
|
|
178
|
+
) : (
|
|
179
|
+
<ToolEntries summary={summary} />
|
|
180
|
+
)}
|
|
181
|
+
</div>
|
|
182
|
+
</CollapsibleContent>
|
|
183
|
+
</Collapsible>
|
|
184
|
+
)
|
|
185
|
+
}
|