@vibe-forge/client 0.10.0 → 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.
Files changed (111) hide show
  1. package/dist/assets/{arc-CCXV7u3V.js → arc-M4HYfcHs.js} +1 -1
  2. package/dist/assets/{blockDiagram-c4efeb88-Bm52FmvT.js → blockDiagram-c4efeb88-CUrDjrxj.js} +1 -1
  3. package/dist/assets/{c4Diagram-c83219d4-C8tTEpcK.js → c4Diagram-c83219d4-BMEtqlFp.js} +1 -1
  4. package/dist/assets/channel-Cj3Cp2OJ.js +1 -0
  5. package/dist/assets/{classDiagram-beda092f-CNAIBAH1.js → classDiagram-beda092f-BOmDJ0Ml.js} +1 -1
  6. package/dist/assets/{classDiagram-v2-2358418a-BHeZAVdc.js → classDiagram-v2-2358418a-BODzX2MB.js} +1 -1
  7. package/dist/assets/clone-B7Q9B1dS.js +1 -0
  8. package/dist/assets/{createText-1719965b-BS2hLG8t.js → createText-1719965b-B9Dd8zcR.js} +1 -1
  9. package/dist/assets/{cssMode-WHcTFAOU.js → cssMode-DLxG92Ot.js} +1 -1
  10. package/dist/assets/{edges-96097737-C07f4iWA.js → edges-96097737-CuZFd43m.js} +1 -1
  11. package/dist/assets/{erDiagram-0228fc6a-BytsAWUs.js → erDiagram-0228fc6a-8g9lu2-Z.js} +1 -1
  12. package/dist/assets/{flowDb-c6c81e3f-CQJkOpAs.js → flowDb-c6c81e3f-BlBS1tdN.js} +1 -1
  13. package/dist/assets/{flowDiagram-50d868cf-CD5Tng2S.js → flowDiagram-50d868cf-u6mWflpF.js} +1 -1
  14. package/dist/assets/flowDiagram-v2-4f6560a1-G3v545eF.js +1 -0
  15. package/dist/assets/{flowchart-elk-definition-6af322e1-ylso-GWH.js → flowchart-elk-definition-6af322e1-BDqI2NFr.js} +1 -1
  16. package/dist/assets/{freemarker2-U_9Jyyr3.js → freemarker2-tVtpTMPu.js} +1 -1
  17. package/dist/assets/{ganttDiagram-a2739b55-Cg98bJx5.js → ganttDiagram-a2739b55-CDQjx9Wu.js} +1 -1
  18. package/dist/assets/{gitGraphDiagram-82fe8481-7Yp4hz0N.js → gitGraphDiagram-82fe8481-DUHFKRVA.js} +1 -1
  19. package/dist/assets/{graph-Ig3nvzvL.js → graph-2HKPi5B_.js} +1 -1
  20. package/dist/assets/{handlebars-DQyCBwHe.js → handlebars-D00tgNd8.js} +1 -1
  21. package/dist/assets/{html-CNC2AT5k.js → html-B-TDzBiR.js} +1 -1
  22. package/dist/assets/{htmlMode-DlATk4xW.js → htmlMode-ClycqSTM.js} +1 -1
  23. package/dist/assets/{index-5325376f-C4zed9sb.js → index-5325376f-DPrJpRQ-.js} +1 -1
  24. package/dist/assets/{index-Dbx0JG0p.js → index-CAHZZEoo.js} +319 -323
  25. package/dist/assets/{index-DRLsOoqb.css → index-Di7lePfb.css} +1 -1
  26. package/dist/assets/{infoDiagram-8eee0895-C8oSBaFs.js → infoDiagram-8eee0895-Co5tS1I5.js} +1 -1
  27. package/dist/assets/{javascript-9wv9uKW4.js → javascript-zbkwarmb.js} +1 -1
  28. package/dist/assets/{journeyDiagram-c64418c1-D5kJldvy.js → journeyDiagram-c64418c1-k_qioHgy.js} +1 -1
  29. package/dist/assets/{jsonMode-45dv39mU.js → jsonMode-C3CSpzBF.js} +1 -1
  30. package/dist/assets/{layout-DYNFLnIl.js → layout-CjOXKxvs.js} +1 -1
  31. package/dist/assets/{line-1gvOYQYZ.js → line-C-XnQrKR.js} +1 -1
  32. package/dist/assets/{linear-DHGm6Zdw.js → linear-C7MMERzS.js} +1 -1
  33. package/dist/assets/{liquid-BGoxrdXO.js → liquid-5G37EU6K.js} +1 -1
  34. package/dist/assets/{lspLanguageFeatures-CpCCXhrd.js → lspLanguageFeatures-zaDMuhCE.js} +1 -1
  35. package/dist/assets/{mdx-Dc2iMbEw.js → mdx-Bc-LY0gi.js} +1 -1
  36. package/dist/assets/{mermaid.core-Cq2bBFF1.js → mermaid.core-CechbHof.js} +4 -4
  37. package/dist/assets/{mindmap-definition-8da855dc-y5l6GRVh.js → mindmap-definition-8da855dc-ejftCDGb.js} +1 -1
  38. package/dist/assets/{pieDiagram-a8764435-PNROcv9y.js → pieDiagram-a8764435-DY__X3Qj.js} +1 -1
  39. package/dist/assets/{python-DjYAge7h.js → python-vK2Ff2J5.js} +1 -1
  40. package/dist/assets/{quadrantDiagram-1e28029f-CFJ3VPpp.js → quadrantDiagram-1e28029f-azIZCv_2.js} +1 -1
  41. package/dist/assets/{razor-OIY8fx_i.js → razor-BipjBJKu.js} +1 -1
  42. package/dist/assets/{requirementDiagram-08caed73-BpzDIINS.js → requirementDiagram-08caed73-C4EB0Xs2.js} +1 -1
  43. package/dist/assets/{sankeyDiagram-a04cb91d-CH69-iIn.js → sankeyDiagram-a04cb91d-PNhR6YWu.js} +1 -1
  44. package/dist/assets/{sequenceDiagram-c5b8d532-yBBEeVFU.js → sequenceDiagram-c5b8d532-4c-qV-Ri.js} +1 -1
  45. package/dist/assets/{stateDiagram-1ecb1508-BvF4sign.js → stateDiagram-1ecb1508-CnURumPE.js} +1 -1
  46. package/dist/assets/{stateDiagram-v2-c2b004d7-BeyT7Ghx.js → stateDiagram-v2-c2b004d7-DR2qHTPg.js} +1 -1
  47. package/dist/assets/{styles-b4e223ce-C58zxmK6.js → styles-b4e223ce-B2PWXT_i.js} +1 -1
  48. package/dist/assets/{styles-ca3715f6-DCE5sFi5.js → styles-ca3715f6-DEhgVF5H.js} +1 -1
  49. package/dist/assets/{styles-d45a18b0-CG-C1aM8.js → styles-d45a18b0-DyzccA5F.js} +1 -1
  50. package/dist/assets/{svgDrawCommon-b86b1483-F-K8GeDd.js → svgDrawCommon-b86b1483-C_1tMhxp.js} +1 -1
  51. package/dist/assets/{timeline-definition-faaaa080-DPv4uqVX.js → timeline-definition-faaaa080-FdaC0dQH.js} +1 -1
  52. package/dist/assets/{tsMode-BtU8ZELV.js → tsMode-CrMC5T3_.js} +1 -1
  53. package/dist/assets/{typescript-CJHgISWo.js → typescript-CRfPu8v7.js} +1 -1
  54. package/dist/assets/{xml-C_TJw4Bi.js → xml-jlRvQfFI.js} +1 -1
  55. package/dist/assets/{xychartDiagram-f5964ef8-BmXlhBzX.js → xychartDiagram-f5964ef8-sxjv75h9.js} +1 -1
  56. package/dist/assets/{yaml-BujeJOJ6.js → yaml-B47_IHOH.js} +1 -1
  57. package/dist/index.html +2 -2
  58. package/package.json +10 -10
  59. package/src/components/chat/ChatHistoryView.tsx +92 -14
  60. package/src/components/chat/messages/MessageItem.scss +10 -0
  61. package/src/components/chat/messages/MessageItem.tsx +5 -1
  62. package/src/components/chat/messages/MessageStatusNotice.scss +163 -0
  63. package/src/components/chat/messages/MessageStatusNotice.tsx +48 -0
  64. package/src/components/chat/messages/build-chat-history-status-notices.ts +138 -0
  65. package/src/components/chat/sender/@components/sender-body/SenderBody.tsx +0 -24
  66. package/src/components/chat/sender/@core/build-sender-controller-result.ts +0 -6
  67. package/src/components/chat/sender/@hooks/use-sender-controller.ts +0 -2
  68. package/src/components/chat/sender/@types/sender-props.ts +0 -3
  69. package/src/components/chat/sender/Sender.scss +0 -58
  70. package/src/components/chat/sender/Sender.tsx +0 -2
  71. package/src/components/chat/tools/DefaultTool.tsx +74 -207
  72. package/src/components/chat/tools/adapter-claude/ClaudeEditDiff.tsx +30 -0
  73. package/src/components/chat/tools/adapter-claude/GenericClaudeTool.scss +128 -0
  74. package/src/components/chat/tools/adapter-claude/GenericClaudeTool.tsx +133 -0
  75. package/src/components/chat/tools/adapter-claude/claude-tool-edit-builders.ts +102 -0
  76. package/src/components/chat/tools/adapter-claude/claude-tool-field-sections.tsx +168 -0
  77. package/src/components/chat/tools/adapter-claude/claude-tool-operation-builders.ts +135 -0
  78. package/src/components/chat/tools/adapter-claude/claude-tool-presentation.ts +61 -0
  79. package/src/components/chat/tools/adapter-claude/claude-tool-shared.ts +185 -0
  80. package/src/components/chat/tools/adapter-claude/claude-tool-summary.ts +76 -0
  81. package/src/components/chat/tools/adapter-claude/claude-tool-system-builders.ts +125 -0
  82. package/src/components/chat/tools/adapter-claude/claude-tool-task-builders.ts +148 -0
  83. package/src/components/chat/tools/adapter-claude/index.ts +24 -15
  84. package/src/components/chat/tools/core/ToolCallBox.scss +344 -36
  85. package/src/components/chat/tools/core/ToolCallBox.tsx +35 -13
  86. package/src/components/chat/tools/core/ToolDiffViewer.scss +138 -0
  87. package/src/components/chat/tools/core/ToolDiffViewer.tsx +180 -0
  88. package/src/components/chat/tools/core/ToolGroup.scss +52 -74
  89. package/src/components/chat/tools/core/ToolGroup.tsx +20 -26
  90. package/src/components/chat/tools/core/ToolRenderer.tsx +3 -3
  91. package/src/components/chat/tools/core/ToolResultContent.tsx +66 -0
  92. package/src/components/chat/tools/core/ToolSummaryHeader.tsx +67 -0
  93. package/src/components/chat/tools/core/tool-content-presence.ts +57 -0
  94. package/src/components/chat/tools/core/tool-display.ts +192 -0
  95. package/src/components/chat/tools/core/tool-result-content-utils.ts +171 -0
  96. package/src/components/chat/tools/core/tool-summary.ts +194 -0
  97. package/src/components/chat/tools/plugin-chrome-devtools/ChromeDevtoolsTool.tsx +66 -53
  98. package/src/components/chat/tools/task/GetTaskInfoTool.tsx +26 -9
  99. package/src/components/chat/tools/task/ListTasksTool.tsx +22 -9
  100. package/src/components/chat/tools/task/StartTasksTool.tsx +22 -9
  101. package/src/hooks/chat/interaction-state.ts +29 -9
  102. package/src/hooks/chat/use-chat-scroll.ts +2 -2
  103. package/src/hooks/chat/use-chat-session-messages.ts +24 -18
  104. package/src/hooks/chat/use-chat-session.ts +2 -2
  105. package/src/resources/locales/en.json +81 -0
  106. package/src/resources/locales/zh.json +81 -0
  107. package/src/routes/ChatRoute.tsx +40 -15
  108. package/src/utils/strip-ansi.ts +26 -0
  109. package/dist/assets/channel-gq_WMRvv.js +0 -1
  110. package/dist/assets/clone-XxGY7A5N.js +0 -1
  111. package/dist/assets/flowDiagram-v2-4f6560a1-DIBOANLV.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
- defaultExpanded={true}
26
- header={
27
- <div className='tool-header-content'>
28
- <i className='tool-header-icon chrome-devtools-tool__icon devicon-chrome-plain colored' />
29
- <span className='tool-header-title'>{displayName}</span>
30
- <span className='tool-header-hint'>{t('chat.tools.call')}</span>
31
- </div>
32
- }
33
- content={
34
- <div className='tool-content'>
35
- <CodeBlock
36
- code={safeJsonStringify(input, 2)}
37
- lang='json'
38
- />
39
- </div>
40
- }
41
- />
42
- {resultItem != null && (
43
- <ToolCallBox
44
- type='result'
45
- isError={resultItem.is_error}
46
- header={
47
- <div className='tool-header-content'>
48
- <span className='material-symbols-rounded tool-header-icon'>
49
- {resultItem.is_error === true ? 'error' : 'check_circle'}
50
- </span>
51
- <span className='tool-header-title'>{t('chat.result')}</span>
52
- </div>
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={resultItem.content}
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
  })