@vibe-forge/client 0.10.1 → 0.11.1

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 (133) hide show
  1. package/dist/assets/{arc-C1rWFTer.js → arc-CSepokz3.js} +1 -1
  2. package/dist/assets/{blockDiagram-c4efeb88-DlZ9x70F.js → blockDiagram-c4efeb88-D0ARcoNf.js} +1 -1
  3. package/dist/assets/{c4Diagram-c83219d4-BKKxi__y.js → c4Diagram-c83219d4-BysYF9kP.js} +1 -1
  4. package/dist/assets/channel-CeKPk6Nd.js +1 -0
  5. package/dist/assets/{classDiagram-beda092f-CVGPySZq.js → classDiagram-beda092f-BG1GhIOL.js} +1 -1
  6. package/dist/assets/{classDiagram-v2-2358418a-7kp8GVVj.js → classDiagram-v2-2358418a-Dd08uGSH.js} +1 -1
  7. package/dist/assets/clone-CrkD2PuD.js +1 -0
  8. package/dist/assets/{createText-1719965b-Dykv8kT9.js → createText-1719965b-CigPEIEn.js} +1 -1
  9. package/dist/assets/{cssMode-B59COYVW.js → cssMode-MjflyEfm.js} +1 -1
  10. package/dist/assets/{edges-96097737-CkZ1ZBro.js → edges-96097737-DuTBJJRv.js} +1 -1
  11. package/dist/assets/{erDiagram-0228fc6a-281ADcRp.js → erDiagram-0228fc6a-Cp1bL7Y7.js} +1 -1
  12. package/dist/assets/{flowDb-c6c81e3f-BQjX_flP.js → flowDb-c6c81e3f-BfKbhiq5.js} +1 -1
  13. package/dist/assets/{flowDiagram-50d868cf-DMHZTjES.js → flowDiagram-50d868cf-m7gGc3PK.js} +1 -1
  14. package/dist/assets/flowDiagram-v2-4f6560a1-4ZU4bdp1.js +1 -0
  15. package/dist/assets/{flowchart-elk-definition-6af322e1-CI3yz4z8.js → flowchart-elk-definition-6af322e1-EVeTDRRK.js} +1 -1
  16. package/dist/assets/{freemarker2-DWnWjibn.js → freemarker2-Bb3-QAIN.js} +1 -1
  17. package/dist/assets/{ganttDiagram-a2739b55-B3IING9L.js → ganttDiagram-a2739b55-DslB2U0R.js} +1 -1
  18. package/dist/assets/{gitGraphDiagram-82fe8481-CnArIr_T.js → gitGraphDiagram-82fe8481-C-KFWMXL.js} +1 -1
  19. package/dist/assets/{graph-BZ1F0Yve.js → graph-CukaUc0o.js} +1 -1
  20. package/dist/assets/{handlebars-C1QH9qTz.js → handlebars-C4le-2Y6.js} +1 -1
  21. package/dist/assets/{html-D1NkqHjC.js → html-CjNiRs5S.js} +1 -1
  22. package/dist/assets/{htmlMode-DAZCE_rA.js → htmlMode-B73_3-We.js} +1 -1
  23. package/dist/assets/{index-5325376f-Da9zSHjA.js → index-5325376f-CVISZFPw.js} +1 -1
  24. package/dist/assets/{index-C0vjF3D0.js → index-BZosmb5_.js} +336 -336
  25. package/dist/assets/index-C1oh0w9H.css +32 -0
  26. package/dist/assets/{infoDiagram-8eee0895-DYbFvRM7.js → infoDiagram-8eee0895-DoirLE1K.js} +1 -1
  27. package/dist/assets/{javascript-CoMjGRHa.js → javascript-BDjnqJFP.js} +1 -1
  28. package/dist/assets/{journeyDiagram-c64418c1-Boebox0b.js → journeyDiagram-c64418c1-Ckn-p2CM.js} +1 -1
  29. package/dist/assets/{jsonMode-D__gAvuz.js → jsonMode-C-ftOc5j.js} +1 -1
  30. package/dist/assets/{layout-CTcHNbHp.js → layout-Z7yUG7hB.js} +1 -1
  31. package/dist/assets/{line-4AwinCz2.js → line-DPG_cfAy.js} +1 -1
  32. package/dist/assets/{linear-CeSMLzJW.js → linear--GSeVfMi.js} +1 -1
  33. package/dist/assets/{liquid-DZF6egdE.js → liquid-COiLZ9py.js} +1 -1
  34. package/dist/assets/{lspLanguageFeatures-6K4lv5S2.js → lspLanguageFeatures-DGmhryFq.js} +1 -1
  35. package/dist/assets/{mdx-Cnt4ka6w.js → mdx-BpL87Gej.js} +1 -1
  36. package/dist/assets/{mermaid.core-B0yG5s4D.js → mermaid.core-Cg1CCDo6.js} +4 -4
  37. package/dist/assets/{mindmap-definition-8da855dc-KJEvXMKj.js → mindmap-definition-8da855dc-CKDof1lD.js} +1 -1
  38. package/dist/assets/{pieDiagram-a8764435-17nFAXPJ.js → pieDiagram-a8764435-DwvCaZVE.js} +1 -1
  39. package/dist/assets/{python-DA3TtjDv.js → python-63dBmWV_.js} +1 -1
  40. package/dist/assets/{quadrantDiagram-1e28029f-Dt4vubi-.js → quadrantDiagram-1e28029f-CkzYBQpy.js} +1 -1
  41. package/dist/assets/{razor-CWDJgvX_.js → razor-C50tBqEZ.js} +1 -1
  42. package/dist/assets/{requirementDiagram-08caed73-H6aDyDK-.js → requirementDiagram-08caed73-Brgdjqf4.js} +1 -1
  43. package/dist/assets/{sankeyDiagram-a04cb91d-DxsVtbjI.js → sankeyDiagram-a04cb91d-CGkYexrs.js} +1 -1
  44. package/dist/assets/{sequenceDiagram-c5b8d532-BHa148XJ.js → sequenceDiagram-c5b8d532-D0wE-_J8.js} +1 -1
  45. package/dist/assets/{stateDiagram-1ecb1508-DgwBm8LO.js → stateDiagram-1ecb1508-BYb3NCXZ.js} +1 -1
  46. package/dist/assets/{stateDiagram-v2-c2b004d7-BK7IQLVc.js → stateDiagram-v2-c2b004d7-DrPqi4Pt.js} +1 -1
  47. package/dist/assets/{styles-b4e223ce-DzW27Bc-.js → styles-b4e223ce-DD66TIO4.js} +1 -1
  48. package/dist/assets/{styles-ca3715f6-Dex2GiLT.js → styles-ca3715f6-iy02LHIV.js} +1 -1
  49. package/dist/assets/{styles-d45a18b0-B6fGtDKS.js → styles-d45a18b0-BgqAgJyW.js} +1 -1
  50. package/dist/assets/{svgDrawCommon-b86b1483-B4HYgfV5.js → svgDrawCommon-b86b1483-CDq7ugnw.js} +1 -1
  51. package/dist/assets/{timeline-definition-faaaa080--QSbWb25.js → timeline-definition-faaaa080-DzcLLjK0.js} +1 -1
  52. package/dist/assets/{tsMode-ZM7ocZCH.js → tsMode-BFRFI4ct.js} +1 -1
  53. package/dist/assets/{typescript-CKWDmBCc.js → typescript-CBZQRAPv.js} +1 -1
  54. package/dist/assets/{xml-DuEUAzPi.js → xml-BpWm6upt.js} +1 -1
  55. package/dist/assets/{xychartDiagram-f5964ef8-D09Zkv2K.js → xychartDiagram-f5964ef8-zBN8FmLQ.js} +1 -1
  56. package/dist/assets/{yaml-DL7QPRYk.js → yaml-CqbJPiIP.js} +1 -1
  57. package/dist/index.html +2 -2
  58. package/package.json +10 -10
  59. package/src/api/git.ts +78 -0
  60. package/src/api.ts +24 -0
  61. package/src/components/chat/ChatHeader.tsx +4 -0
  62. package/src/components/chat/ChatHistoryView.tsx +22 -13
  63. package/src/components/chat/git-controls/BranchSwitcherDropdown.tsx +157 -0
  64. package/src/components/chat/git-controls/ChatGitControls.scss +616 -0
  65. package/src/components/chat/git-controls/ChatGitControls.tsx +151 -0
  66. package/src/components/chat/git-controls/GitCommitModal.tsx +199 -0
  67. package/src/components/chat/git-controls/GitCommitModalParts.tsx +151 -0
  68. package/src/components/chat/git-controls/GitOperationsDropdown.tsx +123 -0
  69. package/src/components/chat/git-controls/GitPushModal.tsx +106 -0
  70. package/src/components/chat/git-controls/GitWorktreeDropdown.tsx +68 -0
  71. package/src/components/chat/git-controls/git-branch-utils.ts +88 -0
  72. package/src/components/chat/git-controls/git-commit-utils.ts +79 -0
  73. package/src/components/chat/git-controls/git-mutation-utils.ts +69 -0
  74. package/src/components/chat/git-controls/git-operation-utils.ts +98 -0
  75. package/src/components/chat/git-controls/git-worktree-utils.ts +49 -0
  76. package/src/components/chat/git-controls/use-chat-git-commit.ts +185 -0
  77. package/src/components/chat/git-controls/use-chat-git-controls.ts +200 -0
  78. package/src/components/chat/git-controls/use-chat-git-push-state.ts +19 -0
  79. package/src/components/chat/git-controls/use-chat-git-worktrees.ts +39 -0
  80. package/src/components/chat/messages/MessageStatusNotice.scss +163 -0
  81. package/src/components/chat/messages/MessageStatusNotice.tsx +48 -0
  82. package/src/components/chat/messages/build-chat-history-status-notices.ts +138 -0
  83. package/src/components/chat/sender/@components/sender-body/SenderBody.tsx +0 -24
  84. package/src/components/chat/sender/@core/build-sender-controller-result.ts +0 -6
  85. package/src/components/chat/sender/@hooks/use-sender-controller.ts +0 -2
  86. package/src/components/chat/sender/@types/sender-props.ts +0 -3
  87. package/src/components/chat/sender/Sender.scss +0 -58
  88. package/src/components/chat/sender/Sender.tsx +0 -2
  89. package/src/components/chat/tools/DefaultTool.tsx +84 -208
  90. package/src/components/chat/tools/adapter-claude/ClaudeEditDiff.tsx +30 -0
  91. package/src/components/chat/tools/adapter-claude/GenericClaudeTool.scss +128 -0
  92. package/src/components/chat/tools/adapter-claude/GenericClaudeTool.tsx +119 -0
  93. package/src/components/chat/tools/adapter-claude/claude-tool-edit-builders.ts +109 -0
  94. package/src/components/chat/tools/adapter-claude/claude-tool-field-sections.tsx +83 -0
  95. package/src/components/chat/tools/adapter-claude/claude-tool-operation-builders.ts +135 -0
  96. package/src/components/chat/tools/adapter-claude/claude-tool-presentation.ts +61 -0
  97. package/src/components/chat/tools/adapter-claude/claude-tool-shared.ts +185 -0
  98. package/src/components/chat/tools/adapter-claude/claude-tool-summary.ts +76 -0
  99. package/src/components/chat/tools/adapter-claude/claude-tool-system-builders.ts +125 -0
  100. package/src/components/chat/tools/adapter-claude/claude-tool-task-builders.ts +148 -0
  101. package/src/components/chat/tools/adapter-claude/index.ts +24 -15
  102. package/src/components/chat/tools/core/ToolCallBox.scss +362 -36
  103. package/src/components/chat/tools/core/ToolCallBox.tsx +35 -13
  104. package/src/components/chat/tools/core/ToolDiffViewer.scss +138 -0
  105. package/src/components/chat/tools/core/ToolDiffViewer.tsx +180 -0
  106. package/src/components/chat/tools/core/ToolGroup.scss +52 -74
  107. package/src/components/chat/tools/core/ToolGroup.tsx +25 -40
  108. package/src/components/chat/tools/core/ToolRenderer.tsx +3 -3
  109. package/src/components/chat/tools/core/ToolResultContent.tsx +66 -0
  110. package/src/components/chat/tools/core/ToolSummaryHeader.tsx +67 -0
  111. package/src/components/chat/tools/core/generic-tool-presentation.ts +661 -0
  112. package/src/components/chat/tools/core/tool-content-presence.ts +57 -0
  113. package/src/components/chat/tools/core/tool-display.ts +203 -0
  114. package/src/components/chat/tools/core/tool-field-sections.tsx +132 -0
  115. package/src/components/chat/tools/core/tool-result-content-utils.ts +171 -0
  116. package/src/components/chat/tools/core/tool-summary.ts +206 -0
  117. package/src/components/chat/tools/plugin-chrome-devtools/ChromeDevtoolsTool.tsx +59 -53
  118. package/src/components/chat/tools/task/GetTaskInfoTool.tsx +26 -9
  119. package/src/components/chat/tools/task/ListTasksTool.tsx +22 -9
  120. package/src/components/chat/tools/task/StartTasksTool.tsx +22 -9
  121. package/src/hooks/chat/interaction-state.ts +29 -9
  122. package/src/hooks/chat/session-view-cache.ts +80 -0
  123. package/src/hooks/chat/use-chat-scroll.ts +2 -2
  124. package/src/hooks/chat/use-chat-session-messages.ts +139 -39
  125. package/src/hooks/chat/use-chat-session.ts +2 -2
  126. package/src/resources/locales/en.json +149 -0
  127. package/src/resources/locales/zh.json +149 -0
  128. package/src/routes/ChatRoute.tsx +24 -27
  129. package/src/utils/strip-ansi.ts +26 -0
  130. package/dist/assets/channel-F1aqMANO.js +0 -1
  131. package/dist/assets/clone-B-GCuXNo.js +0 -1
  132. package/dist/assets/flowDiagram-v2-4f6560a1-C5FzdVl1.js +0 -1
  133. package/dist/assets/index-vzEbM21t.css +0 -32
@@ -0,0 +1,661 @@
1
+ /* eslint-disable max-lines */
2
+
3
+ import type { ToolDiffMetaItem } from './ToolDiffViewer'
4
+ import type { ToolFieldView } from './tool-field-sections'
5
+
6
+ const WINDOWS_PATH_RE = /^[a-z]:[\\/]+/i
7
+ const PATH_KEY_SET = new Set(['cwd', 'dir', 'directory', 'filePath', 'file_path', 'path', 'workdir'])
8
+ const MULTILINE_CODE_KEY_SET = new Set([
9
+ 'command',
10
+ 'content',
11
+ 'input',
12
+ 'newString',
13
+ 'new_string',
14
+ 'oldString',
15
+ 'old_string',
16
+ 'output',
17
+ 'patch',
18
+ 'stderr',
19
+ 'stdout'
20
+ ])
21
+ const LIST_KEY_SET = new Set(['changes', 'entries', 'files', 'matches', 'results', 'todos'])
22
+
23
+ interface ToolMeta {
24
+ titleKey?: string
25
+ fallbackTitle: string
26
+ icon: string
27
+ }
28
+
29
+ interface GenericToolDiffView {
30
+ original: string
31
+ modified: string
32
+ language?: string
33
+ metaItems?: Array<{
34
+ labelKey: string
35
+ fallbackLabel: string
36
+ value?: string
37
+ tone?: ToolDiffMetaItem['tone']
38
+ icon?: string
39
+ }>
40
+ }
41
+
42
+ export interface GenericToolPresentation {
43
+ titleKey?: string
44
+ fallbackTitle: string
45
+ icon: string
46
+ primary?: string
47
+ inlineFields: ToolFieldView[]
48
+ blockFields: ToolFieldView[]
49
+ diff?: GenericToolDiffView
50
+ suppressSuccessResult?: boolean
51
+ }
52
+
53
+ const isRecord = (value: unknown): value is Record<string, unknown> => (
54
+ value != null && typeof value === 'object' && !Array.isArray(value)
55
+ )
56
+
57
+ const asString = (value: unknown) => (
58
+ typeof value === 'string' && value.trim() !== '' ? value : undefined
59
+ )
60
+
61
+ const asNumber = (value: unknown) => (
62
+ typeof value === 'number' && Number.isFinite(value) ? value : undefined
63
+ )
64
+
65
+ const asBoolean = (value: unknown) => (
66
+ typeof value === 'boolean' ? value : undefined
67
+ )
68
+
69
+ const normalizeToolPath = (value: string) => value.replace(/\\/g, '/')
70
+
71
+ const getFileInfo = (filePath?: string) => {
72
+ const resolvedPath = (filePath != null && filePath !== '') ? filePath : 'unknown file'
73
+ const normalizedPath = normalizeToolPath(resolvedPath)
74
+ const lastSlashIndex = normalizedPath.lastIndexOf('/')
75
+ const fileName = lastSlashIndex >= 0 ? normalizedPath.slice(lastSlashIndex + 1) : normalizedPath
76
+ const dirPath = lastSlashIndex >= 0 ? normalizedPath.slice(0, lastSlashIndex) : ''
77
+
78
+ return { filePath: normalizedPath, fileName, dirPath }
79
+ }
80
+
81
+ const getLanguageFromPath = (path: string) => {
82
+ const extPart = path.split('.').pop()
83
+ const ext = (extPart != null && extPart !== '') ? extPart.toLowerCase() : ''
84
+ const langMap: Record<string, string> = {
85
+ 'css': 'css',
86
+ 'diff': 'diff',
87
+ 'html': 'html',
88
+ 'js': 'javascript',
89
+ 'json': 'json',
90
+ 'jsx': 'jsx',
91
+ 'md': 'markdown',
92
+ 'py': 'python',
93
+ 'scss': 'scss',
94
+ 'sh': 'bash',
95
+ 'sql': 'sql',
96
+ 'ts': 'typescript',
97
+ 'tsx': 'tsx',
98
+ 'yaml': 'yaml',
99
+ 'yml': 'yaml'
100
+ }
101
+ return langMap[ext] ?? 'text'
102
+ }
103
+
104
+ const humanizeToolSegment = (value: string) => (
105
+ value
106
+ .replace(/[_:-]+/g, ' ')
107
+ .replace(/([a-z0-9])([A-Z])/g, '$1 $2')
108
+ .trim()
109
+ )
110
+
111
+ const formatToolName = (name: string) => {
112
+ if (name.startsWith('mcp__ChromeDevtools__')) {
113
+ return name.replace('mcp__ChromeDevtools__', '')
114
+ }
115
+
116
+ const namespaceSegments = name.includes('__') ? name.split('__').filter(Boolean) : []
117
+ const lastSegment = namespaceSegments.length > 0
118
+ ? namespaceSegments[namespaceSegments.length - 1]
119
+ : name.split(':').pop() ?? name
120
+ return humanizeToolSegment(lastSegment)
121
+ }
122
+
123
+ const normalizeToolKey = (name: string) => (
124
+ formatToolName(name).replace(/[^a-z0-9]+/gi, '').toLowerCase()
125
+ )
126
+
127
+ const toFieldKey = (value: string) => (
128
+ value
129
+ .replace(/^[^a-z0-9]+/i, '')
130
+ .replace(/[_-]+([a-z0-9])/gi, (_, char: string) => char.toUpperCase())
131
+ )
132
+
133
+ const toFallbackLabel = (value: string) => {
134
+ const humanized = humanizeToolSegment(value)
135
+ if (humanized === '') return value
136
+ return humanized.charAt(0).toUpperCase() + humanized.slice(1)
137
+ }
138
+
139
+ const getFieldDescriptor = (key: string, fallbackLabel?: string) => {
140
+ const normalizedKey = toFieldKey(key)
141
+ return {
142
+ labelKey: `chat.tools.fields.${normalizedKey}`,
143
+ fallbackLabel: fallbackLabel ?? toFallbackLabel(key)
144
+ }
145
+ }
146
+
147
+ const pushField = (
148
+ fields: ToolFieldView[],
149
+ usedKeys: Set<string>,
150
+ key: string,
151
+ field: ToolFieldView
152
+ ) => {
153
+ const { value } = field
154
+ if (value == null || value === '') {
155
+ return
156
+ }
157
+ if (Array.isArray(value) && value.length === 0) {
158
+ return
159
+ }
160
+ if (isRecord(value) && Object.keys(value).length === 0) {
161
+ return
162
+ }
163
+
164
+ usedKeys.add(key)
165
+ fields.push(field)
166
+ }
167
+
168
+ const isPathLikeValue = (value: string) => (
169
+ value.startsWith('/') ||
170
+ value.startsWith('./') ||
171
+ value.startsWith('../') ||
172
+ WINDOWS_PATH_RE.test(value) ||
173
+ (value.includes('/') && !value.includes(' '))
174
+ )
175
+
176
+ const getFirstLine = (value: string | undefined) => value?.split('\n')[0]?.trim()
177
+
178
+ const looksLikeCode = (key: string, value: string) => {
179
+ if (MULTILINE_CODE_KEY_SET.has(key)) return true
180
+ if (value.includes('*** Begin Patch')) return true
181
+ return /[{}();=<>]|^diff --git/m.test(value)
182
+ }
183
+
184
+ const isPrimitiveList = (value: unknown[]) =>
185
+ value.every(item => (
186
+ typeof item === 'string' || typeof item === 'number' || typeof item === 'boolean'
187
+ ))
188
+
189
+ const getChangeLines = (value: unknown) => {
190
+ if (!Array.isArray(value)) return undefined
191
+ const lines = value.flatMap((change) => {
192
+ if (!isRecord(change)) return []
193
+ const kind = asString(change.kind) ?? 'change'
194
+ const path = asString(change.path) ?? asString(change.file_path) ?? asString(change.filePath)
195
+ if (path == null) return []
196
+ return [`${kind} ${path}`]
197
+ })
198
+ return lines.length > 0 ? lines : undefined
199
+ }
200
+
201
+ const getPatchTargets = (patch: string) => {
202
+ const matches = Array.from(
203
+ patch.matchAll(/^\*\*\* (?:(?:Add|Update|Delete) File|Move to): (.+)$/gm),
204
+ match => match[1]?.trim()
205
+ ).filter((value): value is string => typeof value === 'string' && value !== '')
206
+
207
+ if (matches.length > 0) {
208
+ return matches
209
+ }
210
+
211
+ return Array.from(
212
+ patch.matchAll(/^\+\+\+ b\/(.+)$/gm),
213
+ match => match[1]?.trim()
214
+ ).filter((value): value is string => typeof value === 'string' && value !== '')
215
+ }
216
+
217
+ const getGenericToolMeta = (name: string): ToolMeta => {
218
+ const normalizedKey = normalizeToolKey(name)
219
+
220
+ if (normalizedKey === 'applypatch') {
221
+ return { fallbackTitle: 'Apply Patch', icon: 'difference' }
222
+ }
223
+ if (normalizedKey === 'bash' || normalizedKey === 'execcommand') {
224
+ return { titleKey: 'chat.tools.bash', fallbackTitle: 'Bash', icon: 'terminal' }
225
+ }
226
+ if (normalizedKey === 'edit' || normalizedKey === 'editfile') {
227
+ return { titleKey: 'chat.tools.editTool', fallbackTitle: 'Edit File', icon: 'edit' }
228
+ }
229
+ if (normalizedKey === 'filechange') {
230
+ return { fallbackTitle: 'File Change', icon: 'difference' }
231
+ }
232
+ if (normalizedKey === 'glob') {
233
+ return { titleKey: 'chat.tools.globTool', fallbackTitle: 'Glob', icon: 'search' }
234
+ }
235
+ if (normalizedKey === 'grep') {
236
+ return { titleKey: 'chat.tools.grepTool', fallbackTitle: 'Grep', icon: 'find_in_page' }
237
+ }
238
+ if (normalizedKey === 'listdir' || normalizedKey === 'ls') {
239
+ return { titleKey: 'chat.tools.lsTool', fallbackTitle: 'List Directory', icon: 'folder_open' }
240
+ }
241
+ if (normalizedKey === 'read' || normalizedKey === 'readfile') {
242
+ return { titleKey: 'chat.tools.read', fallbackTitle: 'Read File', icon: 'visibility' }
243
+ }
244
+ if (normalizedKey === 'viewimage') {
245
+ return { fallbackTitle: 'View Image', icon: 'image' }
246
+ }
247
+ if (normalizedKey === 'webfetch') {
248
+ return { titleKey: 'chat.tools.webFetch', fallbackTitle: 'Web Fetch', icon: 'language' }
249
+ }
250
+ if (normalizedKey === 'websearch') {
251
+ return { titleKey: 'chat.tools.webSearch', fallbackTitle: 'Web Search', icon: 'travel_explore' }
252
+ }
253
+ if (normalizedKey === 'write' || normalizedKey === 'writefile') {
254
+ return { titleKey: 'chat.tools.write', fallbackTitle: 'Write File', icon: 'edit_note' }
255
+ }
256
+ if (name.startsWith('mcp__') || name.includes(':mcp:')) {
257
+ return { fallbackTitle: formatToolName(name), icon: 'extension' }
258
+ }
259
+ if (name.startsWith('plugin__')) {
260
+ return { fallbackTitle: formatToolName(name), icon: 'extension' }
261
+ }
262
+
263
+ return { fallbackTitle: formatToolName(name), icon: 'build' }
264
+ }
265
+
266
+ const getPrimaryFromRecord = (record: Record<string, unknown>) => {
267
+ const preferredKeys = [
268
+ 'file_path',
269
+ 'filePath',
270
+ 'path',
271
+ 'url',
272
+ 'query',
273
+ 'pattern',
274
+ 'command',
275
+ 'selector',
276
+ 'taskId',
277
+ 'subject',
278
+ 'title'
279
+ ]
280
+
281
+ for (const key of preferredKeys) {
282
+ const value = asString(record[key])
283
+ if (value != null) {
284
+ return getFirstLine(value)
285
+ }
286
+ }
287
+
288
+ for (const value of Object.values(record)) {
289
+ if (typeof value === 'string' && value.trim() !== '') {
290
+ const firstLine = getFirstLine(value)
291
+ if (firstLine != null && firstLine !== '' && !firstLine.startsWith('*** Begin Patch')) {
292
+ return firstLine
293
+ }
294
+ }
295
+ }
296
+
297
+ return undefined
298
+ }
299
+
300
+ const addRemainingRecordFields = (
301
+ blockFields: ToolFieldView[],
302
+ inlineFields: ToolFieldView[],
303
+ record: Record<string, unknown>,
304
+ usedKeys: Set<string>
305
+ ) => {
306
+ for (const [key, value] of Object.entries(record)) {
307
+ if (usedKeys.has(key) || value == null) {
308
+ continue
309
+ }
310
+
311
+ const descriptor = getFieldDescriptor(key)
312
+ if (typeof value === 'string') {
313
+ const trimmed = value.trim()
314
+ if (trimmed === '') {
315
+ continue
316
+ }
317
+
318
+ if (!trimmed.includes('\n') && trimmed.length <= 80) {
319
+ pushField(inlineFields, usedKeys, key, {
320
+ ...descriptor,
321
+ format: 'inline',
322
+ value: trimmed
323
+ })
324
+ continue
325
+ }
326
+
327
+ pushField(blockFields, usedKeys, key, {
328
+ ...descriptor,
329
+ format: looksLikeCode(key, trimmed) ? 'code' : 'text',
330
+ value: trimmed,
331
+ lang: key === 'patch' ? 'diff' : undefined
332
+ })
333
+ continue
334
+ }
335
+
336
+ if (typeof value === 'number' || typeof value === 'boolean') {
337
+ pushField(inlineFields, usedKeys, key, {
338
+ ...descriptor,
339
+ format: 'inline',
340
+ value
341
+ })
342
+ continue
343
+ }
344
+
345
+ if (Array.isArray(value)) {
346
+ if (value.length === 0) {
347
+ continue
348
+ }
349
+
350
+ const changeLines = key === 'changes' ? getChangeLines(value) : undefined
351
+ if (changeLines != null) {
352
+ pushField(blockFields, usedKeys, key, {
353
+ ...descriptor,
354
+ format: 'list',
355
+ value: changeLines
356
+ })
357
+ continue
358
+ }
359
+
360
+ if (LIST_KEY_SET.has(key) && isPrimitiveList(value)) {
361
+ pushField(blockFields, usedKeys, key, {
362
+ ...descriptor,
363
+ format: 'list',
364
+ value: value.map(item => String(item))
365
+ })
366
+ continue
367
+ }
368
+
369
+ pushField(blockFields, usedKeys, key, {
370
+ ...descriptor,
371
+ format: 'json',
372
+ value
373
+ })
374
+ continue
375
+ }
376
+
377
+ if (isRecord(value)) {
378
+ pushField(blockFields, usedKeys, key, {
379
+ ...descriptor,
380
+ format: 'json',
381
+ value
382
+ })
383
+ }
384
+ }
385
+ }
386
+
387
+ const buildPrimitiveInputPresentation = (
388
+ input: unknown,
389
+ meta: ToolMeta
390
+ ): GenericToolPresentation => {
391
+ const blockFields: ToolFieldView[] = []
392
+
393
+ if (typeof input === 'string' && input.trim() !== '') {
394
+ const trimmed = input.trim()
395
+ pushField(blockFields, new Set<string>(), 'input', {
396
+ ...getFieldDescriptor('input', 'Input'),
397
+ format: trimmed.includes('\n') || looksLikeCode('input', trimmed) ? 'code' : 'text',
398
+ value: trimmed
399
+ })
400
+ } else if (Array.isArray(input) && input.length > 0) {
401
+ pushField(blockFields, new Set<string>(), 'input', {
402
+ ...getFieldDescriptor('input', 'Input'),
403
+ format: isPrimitiveList(input) ? 'list' : 'json',
404
+ value: isPrimitiveList(input) ? input.map(item => String(item)) : input
405
+ })
406
+ } else if (input != null) {
407
+ pushField(blockFields, new Set<string>(), 'input', {
408
+ ...getFieldDescriptor('input', 'Input'),
409
+ format: 'json',
410
+ value: input
411
+ })
412
+ }
413
+
414
+ return {
415
+ ...meta,
416
+ primary: typeof input === 'string' && input.trim() !== '' ? getFirstLine(input.trim()) : undefined,
417
+ inlineFields: [],
418
+ blockFields
419
+ }
420
+ }
421
+
422
+ export function buildGenericToolPresentation(name: string, input: unknown): GenericToolPresentation {
423
+ const meta = getGenericToolMeta(name)
424
+ if (!isRecord(input)) {
425
+ return buildPrimitiveInputPresentation(input, meta)
426
+ }
427
+
428
+ const record = input
429
+ const inlineFields: ToolFieldView[] = []
430
+ const blockFields: ToolFieldView[] = []
431
+ const usedKeys = new Set<string>()
432
+ const normalizedKey = normalizeToolKey(name)
433
+ let primary: string | undefined
434
+ let diff: GenericToolDiffView | undefined
435
+ let suppressSuccessResult = false
436
+
437
+ if (normalizedKey === 'bash' || normalizedKey === 'execcommand') {
438
+ const command = asString(record.command)
439
+ primary = getFirstLine(command)
440
+ pushField(inlineFields, usedKeys, 'cwd', {
441
+ ...getFieldDescriptor('cwd', 'Cwd'),
442
+ format: 'inline',
443
+ value: asString(record.cwd)
444
+ })
445
+ pushField(inlineFields, usedKeys, 'workdir', {
446
+ ...getFieldDescriptor('workdir', 'Workdir'),
447
+ format: 'inline',
448
+ value: asString(record.workdir)
449
+ })
450
+ pushField(inlineFields, usedKeys, 'timeoutMs', {
451
+ ...getFieldDescriptor('timeoutMs', 'Timeout'),
452
+ format: 'inline',
453
+ value: asNumber(record.timeoutMs)
454
+ })
455
+ pushField(inlineFields, usedKeys, 'yield_time_ms', {
456
+ ...getFieldDescriptor('yieldTimeMs', 'Yield Time'),
457
+ format: 'inline',
458
+ value: asNumber(record.yield_time_ms)
459
+ })
460
+ pushField(inlineFields, usedKeys, 'max_output_tokens', {
461
+ ...getFieldDescriptor('maxOutputTokens', 'Max Output Tokens'),
462
+ format: 'inline',
463
+ value: asNumber(record.max_output_tokens)
464
+ })
465
+ if (command != null && command.includes('\n')) {
466
+ pushField(blockFields, usedKeys, 'command', {
467
+ ...getFieldDescriptor('command'),
468
+ format: 'code',
469
+ value: command,
470
+ lang: 'bash'
471
+ })
472
+ } else {
473
+ usedKeys.add('command')
474
+ }
475
+ } else if (normalizedKey === 'read' || normalizedKey === 'readfile' || normalizedKey === 'viewimage') {
476
+ const pathValue = asString(record.path) ?? asString(record.file_path)
477
+ primary = pathValue != null ? getFileInfo(pathValue).filePath : undefined
478
+ pushField(inlineFields, usedKeys, 'startLine', {
479
+ ...getFieldDescriptor('startLine', 'Start Line'),
480
+ format: 'inline',
481
+ value: asNumber(record.startLine)
482
+ })
483
+ pushField(inlineFields, usedKeys, 'endLine', {
484
+ ...getFieldDescriptor('endLine', 'End Line'),
485
+ format: 'inline',
486
+ value: asNumber(record.endLine)
487
+ })
488
+ pushField(inlineFields, usedKeys, 'offset', {
489
+ ...getFieldDescriptor('offset'),
490
+ format: 'inline',
491
+ value: asNumber(record.offset)
492
+ })
493
+ pushField(inlineFields, usedKeys, 'limit', {
494
+ ...getFieldDescriptor('limit'),
495
+ format: 'inline',
496
+ value: asNumber(record.limit)
497
+ })
498
+ usedKeys.add('path')
499
+ usedKeys.add('file_path')
500
+ } else if (normalizedKey === 'write' || normalizedKey === 'writefile') {
501
+ const pathValue = asString(record.path) ?? asString(record.file_path)
502
+ primary = pathValue != null ? getFileInfo(pathValue).filePath : undefined
503
+ const content = asString(record.content)
504
+ if (content != null) {
505
+ pushField(blockFields, usedKeys, 'content', {
506
+ ...getFieldDescriptor('content'),
507
+ format: 'code',
508
+ value: content,
509
+ lang: getLanguageFromPath(primary ?? '')
510
+ })
511
+ }
512
+ usedKeys.add('path')
513
+ usedKeys.add('file_path')
514
+ suppressSuccessResult = true
515
+ } else if (normalizedKey === 'edit' || normalizedKey === 'editfile') {
516
+ const pathValue = asString(record.path) ?? asString(record.file_path)
517
+ primary = pathValue != null ? getFileInfo(pathValue).filePath : undefined
518
+ const original = asString(record.oldString) ?? asString(record.old_string) ?? ''
519
+ const modified = asString(record.newString) ?? asString(record.new_string) ?? ''
520
+ const replaceAll = asBoolean(record.replaceAll) ?? asBoolean(record.replace_all)
521
+ if (original !== '' || modified !== '') {
522
+ diff = {
523
+ original,
524
+ modified,
525
+ language: getLanguageFromPath(primary ?? ''),
526
+ metaItems: replaceAll == null
527
+ ? []
528
+ : [{
529
+ ...getFieldDescriptor('replaceAll'),
530
+ icon: 'select_all',
531
+ value: replaceAll ? 'true' : 'false',
532
+ tone: replaceAll ? 'success' : 'muted'
533
+ }]
534
+ }
535
+ usedKeys.add('oldString')
536
+ usedKeys.add('old_string')
537
+ usedKeys.add('newString')
538
+ usedKeys.add('new_string')
539
+ }
540
+ pushField(inlineFields, usedKeys, 'replaceAll', {
541
+ ...getFieldDescriptor('replaceAll'),
542
+ format: 'inline',
543
+ value: replaceAll != null ? String(replaceAll) : undefined
544
+ })
545
+ usedKeys.add('path')
546
+ usedKeys.add('file_path')
547
+ suppressSuccessResult = true
548
+ } else if (normalizedKey === 'applypatch') {
549
+ const patch = asString(record.patch)
550
+ const targets = patch != null ? getPatchTargets(patch) : []
551
+ primary = targets.length === 1
552
+ ? getFileInfo(targets[0]).filePath
553
+ : targets.length > 1
554
+ ? `${targets.length} files`
555
+ : undefined
556
+ if (patch != null) {
557
+ pushField(blockFields, usedKeys, 'patch', {
558
+ ...getFieldDescriptor('patch', 'Patch'),
559
+ format: 'code',
560
+ value: patch,
561
+ lang: 'diff'
562
+ })
563
+ }
564
+ pushField(inlineFields, usedKeys, 'cwd', {
565
+ ...getFieldDescriptor('cwd', 'Cwd'),
566
+ format: 'inline',
567
+ value: asString(record.cwd)
568
+ })
569
+ suppressSuccessResult = true
570
+ } else if (normalizedKey === 'glob') {
571
+ primary = asString(record.pattern)
572
+ pushField(inlineFields, usedKeys, 'cwd', {
573
+ ...getFieldDescriptor('cwd', 'Cwd'),
574
+ format: 'inline',
575
+ value: asString(record.cwd)
576
+ })
577
+ usedKeys.add('pattern')
578
+ } else if (normalizedKey === 'grep') {
579
+ primary = asString(record.pattern)
580
+ pushField(inlineFields, usedKeys, 'path', {
581
+ ...getFieldDescriptor('path'),
582
+ format: 'inline',
583
+ value: asString(record.path)
584
+ })
585
+ pushField(inlineFields, usedKeys, 'glob', {
586
+ ...getFieldDescriptor('glob'),
587
+ format: 'inline',
588
+ value: asString(record.glob)
589
+ })
590
+ pushField(inlineFields, usedKeys, 'ignoreCase', {
591
+ ...getFieldDescriptor('ignoreCase', 'Ignore Case'),
592
+ format: 'inline',
593
+ value: asBoolean(record.ignoreCase) != null ? String(record.ignoreCase) : undefined
594
+ })
595
+ pushField(inlineFields, usedKeys, 'contextLines', {
596
+ ...getFieldDescriptor('contextLines', 'Context Lines'),
597
+ format: 'inline',
598
+ value: asNumber(record.contextLines)
599
+ })
600
+ usedKeys.add('pattern')
601
+ } else if (normalizedKey === 'ls' || normalizedKey === 'listdir') {
602
+ const pathValue = asString(record.path)
603
+ primary = pathValue != null ? getFileInfo(pathValue).filePath : undefined
604
+ usedKeys.add('path')
605
+ } else if (normalizedKey === 'websearch') {
606
+ primary = asString(record.query)
607
+ usedKeys.add('query')
608
+ } else if (normalizedKey === 'webfetch') {
609
+ primary = asString(record.url)
610
+ usedKeys.add('url')
611
+ } else if (normalizedKey === 'filechange') {
612
+ const changeLines = getChangeLines(record.changes)
613
+ primary = changeLines?.length === 1
614
+ ? getFileInfo(changeLines[0].split(' ').slice(1).join(' ')).filePath
615
+ : changeLines != null && changeLines.length > 1
616
+ ? `${changeLines.length} files`
617
+ : undefined
618
+ pushField(inlineFields, usedKeys, 'status', {
619
+ ...getFieldDescriptor('status'),
620
+ format: 'inline',
621
+ value: asString(record.status)
622
+ })
623
+ pushField(blockFields, usedKeys, 'changes', {
624
+ ...getFieldDescriptor('changes', 'Changes'),
625
+ format: 'list',
626
+ value: changeLines
627
+ })
628
+ suppressSuccessResult = true
629
+ }
630
+
631
+ if (primary == null) {
632
+ const genericPrimary = getPrimaryFromRecord(record)
633
+ primary = genericPrimary
634
+ }
635
+
636
+ for (const key of ['path', 'file_path', 'url', 'query', 'pattern', 'command']) {
637
+ if (key in record && usedKeys.has(key)) {
638
+ continue
639
+ }
640
+ const value = asString(record[key])
641
+ if (value == null) continue
642
+ if (PATH_KEY_SET.has(key) || isPathLikeValue(value)) {
643
+ primary = primary ?? (PATH_KEY_SET.has(key) ? getFileInfo(value).filePath : value)
644
+ } else if (key === 'command') {
645
+ primary = primary ?? getFirstLine(value)
646
+ } else {
647
+ primary = primary ?? value
648
+ }
649
+ }
650
+
651
+ addRemainingRecordFields(blockFields, inlineFields, record, usedKeys)
652
+
653
+ return {
654
+ ...meta,
655
+ primary,
656
+ inlineFields,
657
+ blockFields,
658
+ diff,
659
+ suppressSuccessResult
660
+ }
661
+ }