@vibe-forge/client 0.10.1 → 0.11.0
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/dist/assets/{arc-C1rWFTer.js → arc-M4HYfcHs.js} +1 -1
- package/dist/assets/{blockDiagram-c4efeb88-DlZ9x70F.js → blockDiagram-c4efeb88-CUrDjrxj.js} +1 -1
- package/dist/assets/{c4Diagram-c83219d4-BKKxi__y.js → c4Diagram-c83219d4-BMEtqlFp.js} +1 -1
- package/dist/assets/channel-Cj3Cp2OJ.js +1 -0
- package/dist/assets/{classDiagram-beda092f-CVGPySZq.js → classDiagram-beda092f-BOmDJ0Ml.js} +1 -1
- package/dist/assets/{classDiagram-v2-2358418a-7kp8GVVj.js → classDiagram-v2-2358418a-BODzX2MB.js} +1 -1
- package/dist/assets/clone-B7Q9B1dS.js +1 -0
- package/dist/assets/{createText-1719965b-Dykv8kT9.js → createText-1719965b-B9Dd8zcR.js} +1 -1
- package/dist/assets/{cssMode-B59COYVW.js → cssMode-DLxG92Ot.js} +1 -1
- package/dist/assets/{edges-96097737-CkZ1ZBro.js → edges-96097737-CuZFd43m.js} +1 -1
- package/dist/assets/{erDiagram-0228fc6a-281ADcRp.js → erDiagram-0228fc6a-8g9lu2-Z.js} +1 -1
- package/dist/assets/{flowDb-c6c81e3f-BQjX_flP.js → flowDb-c6c81e3f-BlBS1tdN.js} +1 -1
- package/dist/assets/{flowDiagram-50d868cf-DMHZTjES.js → flowDiagram-50d868cf-u6mWflpF.js} +1 -1
- package/dist/assets/flowDiagram-v2-4f6560a1-G3v545eF.js +1 -0
- package/dist/assets/{flowchart-elk-definition-6af322e1-CI3yz4z8.js → flowchart-elk-definition-6af322e1-BDqI2NFr.js} +1 -1
- package/dist/assets/{freemarker2-DWnWjibn.js → freemarker2-tVtpTMPu.js} +1 -1
- package/dist/assets/{ganttDiagram-a2739b55-B3IING9L.js → ganttDiagram-a2739b55-CDQjx9Wu.js} +1 -1
- package/dist/assets/{gitGraphDiagram-82fe8481-CnArIr_T.js → gitGraphDiagram-82fe8481-DUHFKRVA.js} +1 -1
- package/dist/assets/{graph-BZ1F0Yve.js → graph-2HKPi5B_.js} +1 -1
- package/dist/assets/{handlebars-C1QH9qTz.js → handlebars-D00tgNd8.js} +1 -1
- package/dist/assets/{html-D1NkqHjC.js → html-B-TDzBiR.js} +1 -1
- package/dist/assets/{htmlMode-DAZCE_rA.js → htmlMode-ClycqSTM.js} +1 -1
- package/dist/assets/{index-5325376f-Da9zSHjA.js → index-5325376f-DPrJpRQ-.js} +1 -1
- package/dist/assets/{index-C0vjF3D0.js → index-CAHZZEoo.js} +319 -323
- package/dist/assets/{index-vzEbM21t.css → index-Di7lePfb.css} +1 -1
- package/dist/assets/{infoDiagram-8eee0895-DYbFvRM7.js → infoDiagram-8eee0895-Co5tS1I5.js} +1 -1
- package/dist/assets/{javascript-CoMjGRHa.js → javascript-zbkwarmb.js} +1 -1
- package/dist/assets/{journeyDiagram-c64418c1-Boebox0b.js → journeyDiagram-c64418c1-k_qioHgy.js} +1 -1
- package/dist/assets/{jsonMode-D__gAvuz.js → jsonMode-C3CSpzBF.js} +1 -1
- package/dist/assets/{layout-CTcHNbHp.js → layout-CjOXKxvs.js} +1 -1
- package/dist/assets/{line-4AwinCz2.js → line-C-XnQrKR.js} +1 -1
- package/dist/assets/{linear-CeSMLzJW.js → linear-C7MMERzS.js} +1 -1
- package/dist/assets/{liquid-DZF6egdE.js → liquid-5G37EU6K.js} +1 -1
- package/dist/assets/{lspLanguageFeatures-6K4lv5S2.js → lspLanguageFeatures-zaDMuhCE.js} +1 -1
- package/dist/assets/{mdx-Cnt4ka6w.js → mdx-Bc-LY0gi.js} +1 -1
- package/dist/assets/{mermaid.core-B0yG5s4D.js → mermaid.core-CechbHof.js} +4 -4
- package/dist/assets/{mindmap-definition-8da855dc-KJEvXMKj.js → mindmap-definition-8da855dc-ejftCDGb.js} +1 -1
- package/dist/assets/{pieDiagram-a8764435-17nFAXPJ.js → pieDiagram-a8764435-DY__X3Qj.js} +1 -1
- package/dist/assets/{python-DA3TtjDv.js → python-vK2Ff2J5.js} +1 -1
- package/dist/assets/{quadrantDiagram-1e28029f-Dt4vubi-.js → quadrantDiagram-1e28029f-azIZCv_2.js} +1 -1
- package/dist/assets/{razor-CWDJgvX_.js → razor-BipjBJKu.js} +1 -1
- package/dist/assets/{requirementDiagram-08caed73-H6aDyDK-.js → requirementDiagram-08caed73-C4EB0Xs2.js} +1 -1
- package/dist/assets/{sankeyDiagram-a04cb91d-DxsVtbjI.js → sankeyDiagram-a04cb91d-PNhR6YWu.js} +1 -1
- package/dist/assets/{sequenceDiagram-c5b8d532-BHa148XJ.js → sequenceDiagram-c5b8d532-4c-qV-Ri.js} +1 -1
- package/dist/assets/{stateDiagram-1ecb1508-DgwBm8LO.js → stateDiagram-1ecb1508-CnURumPE.js} +1 -1
- package/dist/assets/{stateDiagram-v2-c2b004d7-BK7IQLVc.js → stateDiagram-v2-c2b004d7-DR2qHTPg.js} +1 -1
- package/dist/assets/{styles-b4e223ce-DzW27Bc-.js → styles-b4e223ce-B2PWXT_i.js} +1 -1
- package/dist/assets/{styles-ca3715f6-Dex2GiLT.js → styles-ca3715f6-DEhgVF5H.js} +1 -1
- package/dist/assets/{styles-d45a18b0-B6fGtDKS.js → styles-d45a18b0-DyzccA5F.js} +1 -1
- package/dist/assets/{svgDrawCommon-b86b1483-B4HYgfV5.js → svgDrawCommon-b86b1483-C_1tMhxp.js} +1 -1
- package/dist/assets/{timeline-definition-faaaa080--QSbWb25.js → timeline-definition-faaaa080-FdaC0dQH.js} +1 -1
- package/dist/assets/{tsMode-ZM7ocZCH.js → tsMode-CrMC5T3_.js} +1 -1
- package/dist/assets/{typescript-CKWDmBCc.js → typescript-CRfPu8v7.js} +1 -1
- package/dist/assets/{xml-DuEUAzPi.js → xml-jlRvQfFI.js} +1 -1
- package/dist/assets/{xychartDiagram-f5964ef8-D09Zkv2K.js → xychartDiagram-f5964ef8-sxjv75h9.js} +1 -1
- package/dist/assets/{yaml-DL7QPRYk.js → yaml-B47_IHOH.js} +1 -1
- package/dist/index.html +2 -2
- package/package.json +10 -10
- package/src/components/chat/ChatHistoryView.tsx +22 -13
- package/src/components/chat/messages/MessageStatusNotice.scss +163 -0
- package/src/components/chat/messages/MessageStatusNotice.tsx +48 -0
- package/src/components/chat/messages/build-chat-history-status-notices.ts +138 -0
- package/src/components/chat/sender/@components/sender-body/SenderBody.tsx +0 -24
- package/src/components/chat/sender/@core/build-sender-controller-result.ts +0 -6
- package/src/components/chat/sender/@hooks/use-sender-controller.ts +0 -2
- package/src/components/chat/sender/@types/sender-props.ts +0 -3
- package/src/components/chat/sender/Sender.scss +0 -58
- package/src/components/chat/sender/Sender.tsx +0 -2
- package/src/components/chat/tools/DefaultTool.tsx +74 -207
- package/src/components/chat/tools/adapter-claude/ClaudeEditDiff.tsx +30 -0
- package/src/components/chat/tools/adapter-claude/GenericClaudeTool.scss +128 -0
- package/src/components/chat/tools/adapter-claude/GenericClaudeTool.tsx +133 -0
- package/src/components/chat/tools/adapter-claude/claude-tool-edit-builders.ts +102 -0
- package/src/components/chat/tools/adapter-claude/claude-tool-field-sections.tsx +168 -0
- package/src/components/chat/tools/adapter-claude/claude-tool-operation-builders.ts +135 -0
- package/src/components/chat/tools/adapter-claude/claude-tool-presentation.ts +61 -0
- package/src/components/chat/tools/adapter-claude/claude-tool-shared.ts +185 -0
- package/src/components/chat/tools/adapter-claude/claude-tool-summary.ts +76 -0
- package/src/components/chat/tools/adapter-claude/claude-tool-system-builders.ts +125 -0
- package/src/components/chat/tools/adapter-claude/claude-tool-task-builders.ts +148 -0
- package/src/components/chat/tools/adapter-claude/index.ts +24 -15
- package/src/components/chat/tools/core/ToolCallBox.scss +344 -36
- package/src/components/chat/tools/core/ToolCallBox.tsx +35 -13
- package/src/components/chat/tools/core/ToolDiffViewer.scss +138 -0
- package/src/components/chat/tools/core/ToolDiffViewer.tsx +180 -0
- package/src/components/chat/tools/core/ToolGroup.scss +52 -74
- package/src/components/chat/tools/core/ToolGroup.tsx +25 -40
- package/src/components/chat/tools/core/ToolRenderer.tsx +3 -3
- package/src/components/chat/tools/core/ToolResultContent.tsx +66 -0
- package/src/components/chat/tools/core/ToolSummaryHeader.tsx +67 -0
- package/src/components/chat/tools/core/tool-content-presence.ts +57 -0
- package/src/components/chat/tools/core/tool-display.ts +192 -0
- package/src/components/chat/tools/core/tool-result-content-utils.ts +171 -0
- package/src/components/chat/tools/core/tool-summary.ts +194 -0
- package/src/components/chat/tools/plugin-chrome-devtools/ChromeDevtoolsTool.tsx +66 -53
- package/src/components/chat/tools/task/GetTaskInfoTool.tsx +26 -9
- package/src/components/chat/tools/task/ListTasksTool.tsx +22 -9
- package/src/components/chat/tools/task/StartTasksTool.tsx +22 -9
- package/src/hooks/chat/interaction-state.ts +29 -9
- package/src/hooks/chat/use-chat-scroll.ts +2 -2
- package/src/hooks/chat/use-chat-session-messages.ts +24 -18
- package/src/hooks/chat/use-chat-session.ts +2 -2
- package/src/resources/locales/en.json +81 -0
- package/src/resources/locales/zh.json +81 -0
- package/src/routes/ChatRoute.tsx +24 -27
- package/src/utils/strip-ansi.ts +26 -0
- package/dist/assets/channel-F1aqMANO.js +0 -1
- package/dist/assets/clone-B-GCuXNo.js +0 -1
- package/dist/assets/flowDiagram-v2-4f6560a1-C5FzdVl1.js +0 -1
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
import { safeJsonStringify } from '#~/utils/safe-serialize'
|
|
2
|
+
|
|
3
|
+
export const TOOL_TOOLTIP_PROPS = {
|
|
4
|
+
arrow: false,
|
|
5
|
+
mouseEnterDelay: .2,
|
|
6
|
+
mouseLeaveDelay: .06
|
|
7
|
+
} as const
|
|
8
|
+
|
|
9
|
+
const PATH_TOOLTIP_MARKERS = ['apps', 'packages', 'src', 'scripts', '.ai', 'docs', 'tests', 'test', 'changelog']
|
|
10
|
+
const WINDOWS_PATH_RE = /^[a-z]:[\\/]+/i
|
|
11
|
+
const URL_RE = /^https?:\/\//i
|
|
12
|
+
|
|
13
|
+
const TOOL_FIELD_ICON_MAP: Record<string, string> = {
|
|
14
|
+
activeForm: 'description',
|
|
15
|
+
addBlockedBy: 'link_off',
|
|
16
|
+
addBlocks: 'block',
|
|
17
|
+
allowedDomains: 'travel_explore',
|
|
18
|
+
allowedPrompts: 'chat',
|
|
19
|
+
answers: 'fact_check',
|
|
20
|
+
args: 'segment',
|
|
21
|
+
blockedDomains: 'domain_disabled',
|
|
22
|
+
cellId: 'tag',
|
|
23
|
+
cellType: 'grid_view',
|
|
24
|
+
command: 'terminal',
|
|
25
|
+
content: 'notes',
|
|
26
|
+
description: 'subject',
|
|
27
|
+
disableSandbox: 'shield',
|
|
28
|
+
details: 'info',
|
|
29
|
+
editMode: 'edit',
|
|
30
|
+
glob: 'data_object',
|
|
31
|
+
ignore: 'visibility_off',
|
|
32
|
+
limit: 'filter_alt',
|
|
33
|
+
maxTurns: 'repeat',
|
|
34
|
+
metadata: 'badge',
|
|
35
|
+
model: 'neurology',
|
|
36
|
+
mode: 'tune',
|
|
37
|
+
newSource: 'note_stack',
|
|
38
|
+
newString: 'edit_note',
|
|
39
|
+
oldString: 'history',
|
|
40
|
+
owner: 'person',
|
|
41
|
+
offset: 'format_indent_increase',
|
|
42
|
+
path: 'folder',
|
|
43
|
+
prompt: 'chat',
|
|
44
|
+
pushToRemote: 'upload',
|
|
45
|
+
questions: 'quiz',
|
|
46
|
+
remoteSession: 'devices',
|
|
47
|
+
remoteSessionUrl: 'link',
|
|
48
|
+
replaceAll: 'select_all',
|
|
49
|
+
resume: 'play_circle',
|
|
50
|
+
runInBackground: 'background_dot_large',
|
|
51
|
+
status: 'flag',
|
|
52
|
+
subagentType: 'hub',
|
|
53
|
+
subject: 'title',
|
|
54
|
+
timeout: 'timer',
|
|
55
|
+
todos: 'checklist'
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const TOOL_FORMAT_ICON_MAP: Record<string, string> = {
|
|
59
|
+
code: 'code',
|
|
60
|
+
inline: 'label',
|
|
61
|
+
json: 'data_object',
|
|
62
|
+
list: 'format_list_bulleted',
|
|
63
|
+
questions: 'quiz',
|
|
64
|
+
text: 'article'
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export const getToolSectionIcon = (section: 'call' | 'details' | 'result') => {
|
|
68
|
+
if (section === 'call') return 'tune'
|
|
69
|
+
if (section === 'result') return 'assignment_turned_in'
|
|
70
|
+
return 'info'
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export const getToolFieldIcon = (labelKey: string, format: string) => {
|
|
74
|
+
const key = labelKey.split('.').pop() ?? labelKey
|
|
75
|
+
return TOOL_FIELD_ICON_MAP[key] ?? TOOL_FORMAT_ICON_MAP[format] ?? 'label'
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export const getToolValueText = (value: unknown) => {
|
|
79
|
+
if (typeof value === 'string') {
|
|
80
|
+
return value
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
if (typeof value === 'number' || typeof value === 'boolean') {
|
|
84
|
+
return String(value)
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
return safeJsonStringify(value, 2)
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export const getToolInlineValueText = (value: unknown) => (
|
|
91
|
+
getToolValueText(value).replace(/\s+/g, ' ').trim()
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
export const shouldUseMonospaceTarget = (value: string | undefined) => {
|
|
95
|
+
if (value == null || value === '') {
|
|
96
|
+
return false
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
return (
|
|
100
|
+
value.startsWith('/') ||
|
|
101
|
+
value.startsWith('./') ||
|
|
102
|
+
value.startsWith('../') ||
|
|
103
|
+
value.startsWith('http://') ||
|
|
104
|
+
value.startsWith('https://') ||
|
|
105
|
+
value.startsWith('#') ||
|
|
106
|
+
value.includes('/')
|
|
107
|
+
)
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const normalizeToolPath = (value: string) => value.replace(/\\/g, '/')
|
|
111
|
+
|
|
112
|
+
const looksLikeToolPath = (value: string) => {
|
|
113
|
+
if (URL_RE.test(value)) {
|
|
114
|
+
return false
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const normalized = normalizeToolPath(value)
|
|
118
|
+
if (normalized.startsWith('/') || normalized.startsWith('./') || normalized.startsWith('../')) {
|
|
119
|
+
return true
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
if (WINDOWS_PATH_RE.test(value)) {
|
|
123
|
+
return true
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
if (!normalized.includes('/')) {
|
|
127
|
+
return false
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
return !normalized.includes(' ')
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const getPathLabel = (value: string) => {
|
|
134
|
+
const normalized = normalizeToolPath(value)
|
|
135
|
+
const segments = normalized.split('/').filter(Boolean)
|
|
136
|
+
return segments.at(-1) ?? normalized
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
const getPathTooltip = (value: string) => {
|
|
140
|
+
const normalized = normalizeToolPath(value)
|
|
141
|
+
const segments = normalized.split('/').filter(Boolean)
|
|
142
|
+
const markerIndex = segments.findIndex(segment => PATH_TOOLTIP_MARKERS.includes(segment))
|
|
143
|
+
|
|
144
|
+
if (markerIndex >= 0) {
|
|
145
|
+
return segments.slice(markerIndex).join('/')
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
return normalized
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
export const getToolTargetPresentation = (value: string | undefined) => {
|
|
152
|
+
if (value == null || value.trim() === '') {
|
|
153
|
+
return {
|
|
154
|
+
text: undefined,
|
|
155
|
+
title: undefined,
|
|
156
|
+
monospace: false
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
const normalized = value.trim()
|
|
161
|
+
|
|
162
|
+
if (looksLikeToolPath(normalized)) {
|
|
163
|
+
return {
|
|
164
|
+
text: getPathLabel(normalized),
|
|
165
|
+
title: getPathTooltip(normalized),
|
|
166
|
+
monospace: true
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
if (URL_RE.test(normalized)) {
|
|
171
|
+
try {
|
|
172
|
+
const url = new URL(normalized)
|
|
173
|
+
return {
|
|
174
|
+
text: `${url.hostname}${url.pathname}${url.search}${url.hash}`,
|
|
175
|
+
title: normalized,
|
|
176
|
+
monospace: false
|
|
177
|
+
}
|
|
178
|
+
} catch {
|
|
179
|
+
return {
|
|
180
|
+
text: normalized,
|
|
181
|
+
title: normalized,
|
|
182
|
+
monospace: false
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
return {
|
|
188
|
+
text: normalized,
|
|
189
|
+
title: undefined,
|
|
190
|
+
monospace: shouldUseMonospaceTarget(normalized)
|
|
191
|
+
}
|
|
192
|
+
}
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
import { toSerializable } from '#~/utils/safe-serialize'
|
|
2
|
+
|
|
3
|
+
interface StructuredTextBlock {
|
|
4
|
+
type: 'text'
|
|
5
|
+
text: string
|
|
6
|
+
format: 'text' | 'markdown'
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
interface StructuredImageBlock {
|
|
10
|
+
type: 'image'
|
|
11
|
+
src: string
|
|
12
|
+
alt?: string
|
|
13
|
+
title?: string
|
|
14
|
+
width?: number
|
|
15
|
+
height?: number
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export type StructuredBlock = StructuredTextBlock | StructuredImageBlock
|
|
19
|
+
|
|
20
|
+
const parseStructuredInput = (value: unknown) => {
|
|
21
|
+
if (typeof value !== 'string') {
|
|
22
|
+
return value
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const trimmed = value.trim()
|
|
26
|
+
if (
|
|
27
|
+
(trimmed.startsWith('{') && trimmed.endsWith('}')) ||
|
|
28
|
+
(trimmed.startsWith('[') && trimmed.endsWith(']'))
|
|
29
|
+
) {
|
|
30
|
+
try {
|
|
31
|
+
return JSON.parse(trimmed)
|
|
32
|
+
} catch {
|
|
33
|
+
return value
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
return value
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const resolveImageSource = (value: Record<string, unknown>) => {
|
|
41
|
+
const directUrl = typeof value.url === 'string'
|
|
42
|
+
? value.url
|
|
43
|
+
: typeof value.src === 'string'
|
|
44
|
+
? value.src
|
|
45
|
+
: typeof value.image_url === 'string'
|
|
46
|
+
? value.image_url
|
|
47
|
+
: typeof value.imageUrl === 'string'
|
|
48
|
+
? value.imageUrl
|
|
49
|
+
: typeof value.dataUrl === 'string'
|
|
50
|
+
? value.dataUrl
|
|
51
|
+
: null
|
|
52
|
+
if (directUrl != null) {
|
|
53
|
+
return directUrl
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const source = value.source != null && typeof value.source === 'object'
|
|
57
|
+
? value.source as Record<string, unknown>
|
|
58
|
+
: null
|
|
59
|
+
const data = typeof value.data === 'string'
|
|
60
|
+
? value.data
|
|
61
|
+
: typeof value.base64 === 'string'
|
|
62
|
+
? value.base64
|
|
63
|
+
: source != null && typeof source.data === 'string'
|
|
64
|
+
? source.data
|
|
65
|
+
: null
|
|
66
|
+
if (data == null || data === '') {
|
|
67
|
+
return null
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const mimeType = typeof value.mimeType === 'string'
|
|
71
|
+
? value.mimeType
|
|
72
|
+
: typeof value.mime_type === 'string'
|
|
73
|
+
? value.mime_type
|
|
74
|
+
: source != null && typeof source.media_type === 'string'
|
|
75
|
+
? source.media_type
|
|
76
|
+
: source != null && typeof source.mimeType === 'string'
|
|
77
|
+
? source.mimeType
|
|
78
|
+
: source != null && typeof source.mime_type === 'string'
|
|
79
|
+
? source.mime_type
|
|
80
|
+
: 'image/png'
|
|
81
|
+
|
|
82
|
+
return `data:${mimeType};base64,${data}`
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const parseBlock = (value: unknown): StructuredBlock | null => {
|
|
86
|
+
if (value == null || typeof value !== 'object') {
|
|
87
|
+
return null
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const obj = value as Record<string, unknown>
|
|
91
|
+
const rawType = typeof obj.type === 'string' ? obj.type.toLowerCase() : ''
|
|
92
|
+
if (rawType === 'text' || rawType === 'markdown' || rawType === 'md') {
|
|
93
|
+
const text = typeof obj.text === 'string'
|
|
94
|
+
? obj.text
|
|
95
|
+
: typeof obj.content === 'string'
|
|
96
|
+
? obj.content
|
|
97
|
+
: null
|
|
98
|
+
if (text == null) {
|
|
99
|
+
return null
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const rawFormat = typeof obj.format === 'string' ? obj.format.toLowerCase() : 'markdown'
|
|
103
|
+
return {
|
|
104
|
+
type: 'text',
|
|
105
|
+
text,
|
|
106
|
+
format: rawType === 'text' && (rawFormat === 'text' || rawFormat === 'plain') ? 'text' : 'markdown'
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
if (rawType !== 'image') {
|
|
111
|
+
return null
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const src = resolveImageSource(obj)
|
|
115
|
+
if (src == null) {
|
|
116
|
+
return null
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
return {
|
|
120
|
+
type: 'image',
|
|
121
|
+
src,
|
|
122
|
+
alt: typeof obj.alt === 'string' ? obj.alt : undefined,
|
|
123
|
+
title: typeof obj.title === 'string' ? obj.title : undefined,
|
|
124
|
+
width: typeof obj.width === 'number' ? obj.width : undefined,
|
|
125
|
+
height: typeof obj.height === 'number' ? obj.height : undefined
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
export const getStructuredBlocks = (value: unknown): StructuredBlock[] | null => {
|
|
130
|
+
const parsed = parseStructuredInput(toSerializable(value))
|
|
131
|
+
if (Array.isArray(parsed)) {
|
|
132
|
+
const blocks = parsed.map(parseBlock)
|
|
133
|
+
return blocks.every(Boolean) ? blocks as StructuredBlock[] : null
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
if (parsed != null && typeof parsed === 'object') {
|
|
137
|
+
const container = parsed as Record<string, unknown>
|
|
138
|
+
const content = container.content ?? container.items ?? container.blocks
|
|
139
|
+
if (Array.isArray(content)) {
|
|
140
|
+
const blocks = content.map(parseBlock)
|
|
141
|
+
return blocks.every(Boolean) ? blocks as StructuredBlock[] : null
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
const single = parseBlock(parsed)
|
|
146
|
+
return single != null ? [single] : null
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
export const getStringList = (value: unknown): string[] | null => {
|
|
150
|
+
const parsed = parseStructuredInput(toSerializable(value))
|
|
151
|
+
if (!Array.isArray(parsed) || !parsed.every(item => ['string', 'number', 'boolean'].includes(typeof item))) {
|
|
152
|
+
return null
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
return parsed.map(item => String(item))
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
export const looksLikeMarkdown = (value: string) => {
|
|
159
|
+
const trimmed = value.trim()
|
|
160
|
+
if (trimmed === '') {
|
|
161
|
+
return false
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
return (
|
|
165
|
+
trimmed.startsWith('```') ||
|
|
166
|
+
/^(?:#{1,6}\s|[-*+]\s|>\s|\d+\.\s)/m.test(trimmed) ||
|
|
167
|
+
/\[[^\]]+\]\([^)]+\)/.test(trimmed) ||
|
|
168
|
+
/!\[[^\]]*\]\([^)]+\)/.test(trimmed) ||
|
|
169
|
+
/\|.+\|/.test(trimmed)
|
|
170
|
+
)
|
|
171
|
+
}
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
import type { ChatMessageContent } from '@vibe-forge/core'
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
buildClaudeToolPresentation,
|
|
5
|
+
getClaudeToolBaseName,
|
|
6
|
+
isClaudeToolName
|
|
7
|
+
} from '../adapter-claude/claude-tool-presentation'
|
|
8
|
+
import { getClaudeToolSummaryText } from '../adapter-claude/claude-tool-summary'
|
|
9
|
+
|
|
10
|
+
export type ToolUseItem = Extract<ChatMessageContent, { type: 'tool_use' }>
|
|
11
|
+
type Translate = (key: string, options?: Record<string, unknown>) => string
|
|
12
|
+
|
|
13
|
+
const HUMANIZED_SEGMENT_SEPARATOR = /[_:-]+/g
|
|
14
|
+
const GENERIC_TOOL_NAMESPACE_PREFIXES = new Set(['adapter', 'agent', 'mcp', 'plugin', 'tool'])
|
|
15
|
+
|
|
16
|
+
const humanizeToolSegment = (value: string) =>
|
|
17
|
+
value
|
|
18
|
+
.replace(HUMANIZED_SEGMENT_SEPARATOR, ' ')
|
|
19
|
+
.replace(/([a-z0-9])([A-Z])/g, '$1 $2')
|
|
20
|
+
.trim()
|
|
21
|
+
|
|
22
|
+
const getToolNameSegments = (name: string) => (
|
|
23
|
+
name.includes('__') ? name.split('__').filter(Boolean) : name.split(':').filter(Boolean)
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
export const formatToolName = (name: string) => {
|
|
27
|
+
if (name.startsWith('mcp__ChromeDevtools__')) {
|
|
28
|
+
return name.replace('mcp__ChromeDevtools__', '')
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const namespaceSegments = name.split('__').filter(Boolean)
|
|
32
|
+
const lastSegment = namespaceSegments.length > 0
|
|
33
|
+
? namespaceSegments[namespaceSegments.length - 1]
|
|
34
|
+
: name.split(':').pop() ?? name
|
|
35
|
+
return humanizeToolSegment(lastSegment)
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export const getToolInputPreview = (input: unknown) => {
|
|
39
|
+
if (input == null || typeof input !== 'object' || Array.isArray(input)) {
|
|
40
|
+
return undefined
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const record = input as Record<string, unknown>
|
|
44
|
+
const preferredKeys = [
|
|
45
|
+
'file_path',
|
|
46
|
+
'path',
|
|
47
|
+
'url',
|
|
48
|
+
'query',
|
|
49
|
+
'pattern',
|
|
50
|
+
'command',
|
|
51
|
+
'selector',
|
|
52
|
+
'taskId',
|
|
53
|
+
'subject',
|
|
54
|
+
'skill',
|
|
55
|
+
'title'
|
|
56
|
+
]
|
|
57
|
+
|
|
58
|
+
for (const key of preferredKeys) {
|
|
59
|
+
const value = record[key]
|
|
60
|
+
if (typeof value === 'string' && value.trim() !== '') {
|
|
61
|
+
return value.trim().split('\n')[0]
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
for (const value of Object.values(record)) {
|
|
66
|
+
if (typeof value === 'string' && value.trim() !== '') {
|
|
67
|
+
return value.trim().split('\n')[0]
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
return undefined
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export function getToolSummaryText(item: ToolUseItem, t: Translate) {
|
|
75
|
+
if (isClaudeToolName(item.name)) {
|
|
76
|
+
return getClaudeToolSummaryText(item.name, item.input, t)
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const displayName = formatToolName(item.name)
|
|
80
|
+
const preview = getToolInputPreview(item.input)
|
|
81
|
+
return preview != null && preview !== '' ? `${displayName} ${preview}` : displayName
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export function getToolTitleText(item: ToolUseItem, t: Translate) {
|
|
85
|
+
if (isClaudeToolName(item.name)) {
|
|
86
|
+
const presentation = buildClaudeToolPresentation(item.name, item.input)
|
|
87
|
+
return t(presentation.titleKey, { defaultValue: presentation.fallbackTitle })
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
return formatToolName(item.name)
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export function getToolPrimaryText(item: ToolUseItem) {
|
|
94
|
+
if (isClaudeToolName(item.name)) {
|
|
95
|
+
return buildClaudeToolPresentation(item.name, item.input).primary
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
return getToolInputPreview(item.input)
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const getToolNamespaceLabel = (name: string) => {
|
|
102
|
+
const namespaceSegments = getToolNameSegments(name)
|
|
103
|
+
.slice(0, -1)
|
|
104
|
+
.filter((segment, index) => !(index === 0 && GENERIC_TOOL_NAMESPACE_PREFIXES.has(segment.toLowerCase())))
|
|
105
|
+
.map(humanizeToolSegment)
|
|
106
|
+
.filter(Boolean)
|
|
107
|
+
|
|
108
|
+
return namespaceSegments.length > 0 ? namespaceSegments.join(' ') : undefined
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const getToolQualifiedLabel = (name: string, fallbackLabel: string) => {
|
|
112
|
+
const namespaceLabel = getToolNamespaceLabel(name)
|
|
113
|
+
return namespaceLabel != null && namespaceLabel !== ''
|
|
114
|
+
? `${namespaceLabel} ${fallbackLabel}`
|
|
115
|
+
: fallbackLabel
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
interface ToolGroupDescriptor {
|
|
119
|
+
key: string
|
|
120
|
+
label: string
|
|
121
|
+
qualifiedLabel: string
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function getToolGroupDescriptor(item: ToolUseItem, t: Translate): ToolGroupDescriptor {
|
|
125
|
+
if (isClaudeToolName(item.name)) {
|
|
126
|
+
const baseName = getClaudeToolBaseName(item.name)
|
|
127
|
+
const presentation = buildClaudeToolPresentation(item.name, item.input)
|
|
128
|
+
const label = t(presentation.titleKey, { defaultValue: presentation.fallbackTitle })
|
|
129
|
+
return {
|
|
130
|
+
key: `claude:${baseName}`,
|
|
131
|
+
label,
|
|
132
|
+
qualifiedLabel: label
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
const label = formatToolName(item.name)
|
|
137
|
+
return {
|
|
138
|
+
key: item.name,
|
|
139
|
+
label,
|
|
140
|
+
qualifiedLabel: getToolQualifiedLabel(item.name, label)
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
export function getToolGroupSummaryText(
|
|
145
|
+
items: Array<{
|
|
146
|
+
item: ToolUseItem
|
|
147
|
+
}>,
|
|
148
|
+
t: Translate
|
|
149
|
+
) {
|
|
150
|
+
const groupedTools = new Map<string, { label: string; qualifiedLabel: string; count: number }>()
|
|
151
|
+
|
|
152
|
+
for (const { item } of items) {
|
|
153
|
+
const descriptor = getToolGroupDescriptor(item, t)
|
|
154
|
+
const label = descriptor.label
|
|
155
|
+
if (label === '') continue
|
|
156
|
+
|
|
157
|
+
const current = groupedTools.get(descriptor.key)
|
|
158
|
+
if (current != null) {
|
|
159
|
+
current.count += 1
|
|
160
|
+
continue
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
groupedTools.set(descriptor.key, {
|
|
164
|
+
label,
|
|
165
|
+
qualifiedLabel: descriptor.qualifiedLabel,
|
|
166
|
+
count: 1
|
|
167
|
+
})
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
const groups = Array.from(groupedTools.values())
|
|
171
|
+
const labelCounts = groups.reduce((map, group) => {
|
|
172
|
+
map.set(group.label, (map.get(group.label) ?? 0) + 1)
|
|
173
|
+
return map
|
|
174
|
+
}, new Map<string, number>())
|
|
175
|
+
|
|
176
|
+
if (groups.length === 0) {
|
|
177
|
+
return t('chat.usedTools', { count: items.length })
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
const summaries = groups.map((group) => {
|
|
181
|
+
const name = (labelCounts.get(group.label) ?? 0) > 1 ? group.qualifiedLabel : group.label
|
|
182
|
+
return {
|
|
183
|
+
count: group.count,
|
|
184
|
+
text: t('chat.tools.groupSummaryCount', { name, count: group.count })
|
|
185
|
+
}
|
|
186
|
+
})
|
|
187
|
+
|
|
188
|
+
const visible = summaries.slice(0, 2).map(summary => summary.text)
|
|
189
|
+
const hiddenCallCount = summaries.slice(2).reduce((count, summary) => count + summary.count, 0)
|
|
190
|
+
|
|
191
|
+
return hiddenCallCount > 0
|
|
192
|
+
? [...visible, t('chat.tools.groupSummaryMoreCount', { count: hiddenCallCount })].join(' · ')
|
|
193
|
+
: visible.join(' · ')
|
|
194
|
+
}
|
|
@@ -1,75 +1,88 @@
|
|
|
1
1
|
import './ChromeDevtoolsTool.scss'
|
|
2
|
+
import { Tooltip } from 'antd'
|
|
2
3
|
import React from 'react'
|
|
3
4
|
import { useTranslation } from 'react-i18next'
|
|
4
5
|
|
|
5
6
|
import { CodeBlock } from '#~/components/CodeBlock'
|
|
6
7
|
import { safeJsonStringify } from '#~/utils/safe-serialize'
|
|
7
8
|
import { ToolCallBox } from '../core/ToolCallBox'
|
|
9
|
+
import { ToolResultContent } from '../core/ToolResultContent'
|
|
10
|
+
import { ToolSummaryHeader } from '../core/ToolSummaryHeader'
|
|
11
|
+
import { hasMeaningfulToolValue } from '../core/tool-content-presence'
|
|
12
|
+
import { TOOL_TOOLTIP_PROPS, getToolSectionIcon, getToolTargetPresentation } from '../core/tool-display'
|
|
13
|
+
import { getToolPrimaryText, getToolTitleText } from '../core/tool-summary'
|
|
8
14
|
import { defineToolRender } from '../defineToolRender'
|
|
9
15
|
|
|
10
|
-
const formatToolName = (name: string) => {
|
|
11
|
-
if (name.startsWith('mcp__ChromeDevtools__')) {
|
|
12
|
-
return name.replace('mcp__ChromeDevtools__', '')
|
|
13
|
-
}
|
|
14
|
-
return name
|
|
15
|
-
}
|
|
16
|
-
|
|
17
16
|
export const ChromeDevtoolsTool = defineToolRender(({ item, resultItem }) => {
|
|
18
17
|
const { t } = useTranslation()
|
|
19
|
-
const displayName = formatToolName(item.name)
|
|
20
18
|
const input = item.input != null ? item.input : {}
|
|
19
|
+
const hasCallDetails = hasMeaningfulToolValue(input)
|
|
20
|
+
const hasResultDetails = resultItem != null && hasMeaningfulToolValue(resultItem.content)
|
|
21
|
+
const hasDetails = hasCallDetails || hasResultDetails
|
|
22
|
+
const titleText = getToolTitleText(item, t)
|
|
23
|
+
const targetPresentation = getToolTargetPresentation(getToolPrimaryText(item))
|
|
24
|
+
const errorMeta = resultItem?.is_error === true
|
|
25
|
+
? (
|
|
26
|
+
<span className='tool-status tool-status--error'>
|
|
27
|
+
<span className='material-symbols-rounded'>error</span>
|
|
28
|
+
</span>
|
|
29
|
+
)
|
|
30
|
+
: undefined
|
|
21
31
|
|
|
22
32
|
return (
|
|
23
|
-
<div className='tool-group chrome-devtools-tool'>
|
|
33
|
+
<div className='tool-group tool-group--compact chrome-devtools-tool'>
|
|
24
34
|
<ToolCallBox
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
<
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
}
|
|
54
|
-
content={
|
|
55
|
-
<div className='tool-content'>
|
|
56
|
-
{typeof resultItem.content === 'string'
|
|
57
|
-
? (
|
|
35
|
+
variant='inline'
|
|
36
|
+
defaultExpanded={false}
|
|
37
|
+
collapsible={hasDetails}
|
|
38
|
+
header={({ isExpanded, isCollapsible }) => (
|
|
39
|
+
<ToolSummaryHeader
|
|
40
|
+
icon={<i className='devicon-chrome-plain colored' />}
|
|
41
|
+
title={titleText}
|
|
42
|
+
target={targetPresentation.text}
|
|
43
|
+
targetTitle={targetPresentation.title}
|
|
44
|
+
targetMonospace={targetPresentation.monospace}
|
|
45
|
+
expanded={isExpanded}
|
|
46
|
+
collapsible={isCollapsible}
|
|
47
|
+
meta={errorMeta}
|
|
48
|
+
metaTitle={errorMeta == null ? undefined : t('chat.result')}
|
|
49
|
+
/>
|
|
50
|
+
)}
|
|
51
|
+
content={hasDetails
|
|
52
|
+
? (
|
|
53
|
+
<div className='tool-detail-sections'>
|
|
54
|
+
{hasCallDetails && (
|
|
55
|
+
<div className='tool-detail-section'>
|
|
56
|
+
<div className='tool-detail-section__header'>
|
|
57
|
+
<Tooltip title={t('chat.tools.call')} {...TOOL_TOOLTIP_PROPS}>
|
|
58
|
+
<span className='tool-detail-section__icon material-symbols-rounded'>
|
|
59
|
+
{getToolSectionIcon('call')}
|
|
60
|
+
</span>
|
|
61
|
+
</Tooltip>
|
|
62
|
+
</div>
|
|
58
63
|
<CodeBlock
|
|
59
|
-
code={
|
|
60
|
-
lang='text'
|
|
61
|
-
/>
|
|
62
|
-
)
|
|
63
|
-
: (
|
|
64
|
-
<CodeBlock
|
|
65
|
-
code={safeJsonStringify(resultItem.content, 2)}
|
|
64
|
+
code={safeJsonStringify(input, 2)}
|
|
66
65
|
lang='json'
|
|
66
|
+
hideHeader={true}
|
|
67
67
|
/>
|
|
68
|
-
|
|
68
|
+
</div>
|
|
69
|
+
)}
|
|
70
|
+
{hasResultDetails && resultItem != null && (
|
|
71
|
+
<div className='tool-detail-section'>
|
|
72
|
+
<div className='tool-detail-section__header'>
|
|
73
|
+
<Tooltip title={t('chat.result')} {...TOOL_TOOLTIP_PROPS}>
|
|
74
|
+
<span className='tool-detail-section__icon material-symbols-rounded'>
|
|
75
|
+
{getToolSectionIcon('result')}
|
|
76
|
+
</span>
|
|
77
|
+
</Tooltip>
|
|
78
|
+
</div>
|
|
79
|
+
<ToolResultContent content={resultItem.content} />
|
|
80
|
+
</div>
|
|
81
|
+
)}
|
|
69
82
|
</div>
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
83
|
+
)
|
|
84
|
+
: null}
|
|
85
|
+
/>
|
|
73
86
|
</div>
|
|
74
87
|
)
|
|
75
88
|
})
|