@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.
Files changed (109) hide show
  1. package/dist/assets/{arc-C1rWFTer.js → arc-M4HYfcHs.js} +1 -1
  2. package/dist/assets/{blockDiagram-c4efeb88-DlZ9x70F.js → blockDiagram-c4efeb88-CUrDjrxj.js} +1 -1
  3. package/dist/assets/{c4Diagram-c83219d4-BKKxi__y.js → c4Diagram-c83219d4-BMEtqlFp.js} +1 -1
  4. package/dist/assets/channel-Cj3Cp2OJ.js +1 -0
  5. package/dist/assets/{classDiagram-beda092f-CVGPySZq.js → classDiagram-beda092f-BOmDJ0Ml.js} +1 -1
  6. package/dist/assets/{classDiagram-v2-2358418a-7kp8GVVj.js → classDiagram-v2-2358418a-BODzX2MB.js} +1 -1
  7. package/dist/assets/clone-B7Q9B1dS.js +1 -0
  8. package/dist/assets/{createText-1719965b-Dykv8kT9.js → createText-1719965b-B9Dd8zcR.js} +1 -1
  9. package/dist/assets/{cssMode-B59COYVW.js → cssMode-DLxG92Ot.js} +1 -1
  10. package/dist/assets/{edges-96097737-CkZ1ZBro.js → edges-96097737-CuZFd43m.js} +1 -1
  11. package/dist/assets/{erDiagram-0228fc6a-281ADcRp.js → erDiagram-0228fc6a-8g9lu2-Z.js} +1 -1
  12. package/dist/assets/{flowDb-c6c81e3f-BQjX_flP.js → flowDb-c6c81e3f-BlBS1tdN.js} +1 -1
  13. package/dist/assets/{flowDiagram-50d868cf-DMHZTjES.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-CI3yz4z8.js → flowchart-elk-definition-6af322e1-BDqI2NFr.js} +1 -1
  16. package/dist/assets/{freemarker2-DWnWjibn.js → freemarker2-tVtpTMPu.js} +1 -1
  17. package/dist/assets/{ganttDiagram-a2739b55-B3IING9L.js → ganttDiagram-a2739b55-CDQjx9Wu.js} +1 -1
  18. package/dist/assets/{gitGraphDiagram-82fe8481-CnArIr_T.js → gitGraphDiagram-82fe8481-DUHFKRVA.js} +1 -1
  19. package/dist/assets/{graph-BZ1F0Yve.js → graph-2HKPi5B_.js} +1 -1
  20. package/dist/assets/{handlebars-C1QH9qTz.js → handlebars-D00tgNd8.js} +1 -1
  21. package/dist/assets/{html-D1NkqHjC.js → html-B-TDzBiR.js} +1 -1
  22. package/dist/assets/{htmlMode-DAZCE_rA.js → htmlMode-ClycqSTM.js} +1 -1
  23. package/dist/assets/{index-5325376f-Da9zSHjA.js → index-5325376f-DPrJpRQ-.js} +1 -1
  24. package/dist/assets/{index-C0vjF3D0.js → index-CAHZZEoo.js} +319 -323
  25. package/dist/assets/{index-vzEbM21t.css → index-Di7lePfb.css} +1 -1
  26. package/dist/assets/{infoDiagram-8eee0895-DYbFvRM7.js → infoDiagram-8eee0895-Co5tS1I5.js} +1 -1
  27. package/dist/assets/{javascript-CoMjGRHa.js → javascript-zbkwarmb.js} +1 -1
  28. package/dist/assets/{journeyDiagram-c64418c1-Boebox0b.js → journeyDiagram-c64418c1-k_qioHgy.js} +1 -1
  29. package/dist/assets/{jsonMode-D__gAvuz.js → jsonMode-C3CSpzBF.js} +1 -1
  30. package/dist/assets/{layout-CTcHNbHp.js → layout-CjOXKxvs.js} +1 -1
  31. package/dist/assets/{line-4AwinCz2.js → line-C-XnQrKR.js} +1 -1
  32. package/dist/assets/{linear-CeSMLzJW.js → linear-C7MMERzS.js} +1 -1
  33. package/dist/assets/{liquid-DZF6egdE.js → liquid-5G37EU6K.js} +1 -1
  34. package/dist/assets/{lspLanguageFeatures-6K4lv5S2.js → lspLanguageFeatures-zaDMuhCE.js} +1 -1
  35. package/dist/assets/{mdx-Cnt4ka6w.js → mdx-Bc-LY0gi.js} +1 -1
  36. package/dist/assets/{mermaid.core-B0yG5s4D.js → mermaid.core-CechbHof.js} +4 -4
  37. package/dist/assets/{mindmap-definition-8da855dc-KJEvXMKj.js → mindmap-definition-8da855dc-ejftCDGb.js} +1 -1
  38. package/dist/assets/{pieDiagram-a8764435-17nFAXPJ.js → pieDiagram-a8764435-DY__X3Qj.js} +1 -1
  39. package/dist/assets/{python-DA3TtjDv.js → python-vK2Ff2J5.js} +1 -1
  40. package/dist/assets/{quadrantDiagram-1e28029f-Dt4vubi-.js → quadrantDiagram-1e28029f-azIZCv_2.js} +1 -1
  41. package/dist/assets/{razor-CWDJgvX_.js → razor-BipjBJKu.js} +1 -1
  42. package/dist/assets/{requirementDiagram-08caed73-H6aDyDK-.js → requirementDiagram-08caed73-C4EB0Xs2.js} +1 -1
  43. package/dist/assets/{sankeyDiagram-a04cb91d-DxsVtbjI.js → sankeyDiagram-a04cb91d-PNhR6YWu.js} +1 -1
  44. package/dist/assets/{sequenceDiagram-c5b8d532-BHa148XJ.js → sequenceDiagram-c5b8d532-4c-qV-Ri.js} +1 -1
  45. package/dist/assets/{stateDiagram-1ecb1508-DgwBm8LO.js → stateDiagram-1ecb1508-CnURumPE.js} +1 -1
  46. package/dist/assets/{stateDiagram-v2-c2b004d7-BK7IQLVc.js → stateDiagram-v2-c2b004d7-DR2qHTPg.js} +1 -1
  47. package/dist/assets/{styles-b4e223ce-DzW27Bc-.js → styles-b4e223ce-B2PWXT_i.js} +1 -1
  48. package/dist/assets/{styles-ca3715f6-Dex2GiLT.js → styles-ca3715f6-DEhgVF5H.js} +1 -1
  49. package/dist/assets/{styles-d45a18b0-B6fGtDKS.js → styles-d45a18b0-DyzccA5F.js} +1 -1
  50. package/dist/assets/{svgDrawCommon-b86b1483-B4HYgfV5.js → svgDrawCommon-b86b1483-C_1tMhxp.js} +1 -1
  51. package/dist/assets/{timeline-definition-faaaa080--QSbWb25.js → timeline-definition-faaaa080-FdaC0dQH.js} +1 -1
  52. package/dist/assets/{tsMode-ZM7ocZCH.js → tsMode-CrMC5T3_.js} +1 -1
  53. package/dist/assets/{typescript-CKWDmBCc.js → typescript-CRfPu8v7.js} +1 -1
  54. package/dist/assets/{xml-DuEUAzPi.js → xml-jlRvQfFI.js} +1 -1
  55. package/dist/assets/{xychartDiagram-f5964ef8-D09Zkv2K.js → xychartDiagram-f5964ef8-sxjv75h9.js} +1 -1
  56. package/dist/assets/{yaml-DL7QPRYk.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 +22 -13
  60. package/src/components/chat/messages/MessageStatusNotice.scss +163 -0
  61. package/src/components/chat/messages/MessageStatusNotice.tsx +48 -0
  62. package/src/components/chat/messages/build-chat-history-status-notices.ts +138 -0
  63. package/src/components/chat/sender/@components/sender-body/SenderBody.tsx +0 -24
  64. package/src/components/chat/sender/@core/build-sender-controller-result.ts +0 -6
  65. package/src/components/chat/sender/@hooks/use-sender-controller.ts +0 -2
  66. package/src/components/chat/sender/@types/sender-props.ts +0 -3
  67. package/src/components/chat/sender/Sender.scss +0 -58
  68. package/src/components/chat/sender/Sender.tsx +0 -2
  69. package/src/components/chat/tools/DefaultTool.tsx +74 -207
  70. package/src/components/chat/tools/adapter-claude/ClaudeEditDiff.tsx +30 -0
  71. package/src/components/chat/tools/adapter-claude/GenericClaudeTool.scss +128 -0
  72. package/src/components/chat/tools/adapter-claude/GenericClaudeTool.tsx +133 -0
  73. package/src/components/chat/tools/adapter-claude/claude-tool-edit-builders.ts +102 -0
  74. package/src/components/chat/tools/adapter-claude/claude-tool-field-sections.tsx +168 -0
  75. package/src/components/chat/tools/adapter-claude/claude-tool-operation-builders.ts +135 -0
  76. package/src/components/chat/tools/adapter-claude/claude-tool-presentation.ts +61 -0
  77. package/src/components/chat/tools/adapter-claude/claude-tool-shared.ts +185 -0
  78. package/src/components/chat/tools/adapter-claude/claude-tool-summary.ts +76 -0
  79. package/src/components/chat/tools/adapter-claude/claude-tool-system-builders.ts +125 -0
  80. package/src/components/chat/tools/adapter-claude/claude-tool-task-builders.ts +148 -0
  81. package/src/components/chat/tools/adapter-claude/index.ts +24 -15
  82. package/src/components/chat/tools/core/ToolCallBox.scss +344 -36
  83. package/src/components/chat/tools/core/ToolCallBox.tsx +35 -13
  84. package/src/components/chat/tools/core/ToolDiffViewer.scss +138 -0
  85. package/src/components/chat/tools/core/ToolDiffViewer.tsx +180 -0
  86. package/src/components/chat/tools/core/ToolGroup.scss +52 -74
  87. package/src/components/chat/tools/core/ToolGroup.tsx +25 -40
  88. package/src/components/chat/tools/core/ToolRenderer.tsx +3 -3
  89. package/src/components/chat/tools/core/ToolResultContent.tsx +66 -0
  90. package/src/components/chat/tools/core/ToolSummaryHeader.tsx +67 -0
  91. package/src/components/chat/tools/core/tool-content-presence.ts +57 -0
  92. package/src/components/chat/tools/core/tool-display.ts +192 -0
  93. package/src/components/chat/tools/core/tool-result-content-utils.ts +171 -0
  94. package/src/components/chat/tools/core/tool-summary.ts +194 -0
  95. package/src/components/chat/tools/plugin-chrome-devtools/ChromeDevtoolsTool.tsx +66 -53
  96. package/src/components/chat/tools/task/GetTaskInfoTool.tsx +26 -9
  97. package/src/components/chat/tools/task/ListTasksTool.tsx +22 -9
  98. package/src/components/chat/tools/task/StartTasksTool.tsx +22 -9
  99. package/src/hooks/chat/interaction-state.ts +29 -9
  100. package/src/hooks/chat/use-chat-scroll.ts +2 -2
  101. package/src/hooks/chat/use-chat-session-messages.ts +24 -18
  102. package/src/hooks/chat/use-chat-session.ts +2 -2
  103. package/src/resources/locales/en.json +81 -0
  104. package/src/resources/locales/zh.json +81 -0
  105. package/src/routes/ChatRoute.tsx +24 -27
  106. package/src/utils/strip-ansi.ts +26 -0
  107. package/dist/assets/channel-F1aqMANO.js +0 -1
  108. package/dist/assets/clone-B-GCuXNo.js +0 -1
  109. package/dist/assets/flowDiagram-v2-4f6560a1-C5FzdVl1.js +0 -1
@@ -0,0 +1,180 @@
1
+ import './ToolDiffViewer.scss'
2
+
3
+ import { DiffEditor, loader } from '@monaco-editor/react'
4
+ import { Tooltip } from 'antd'
5
+ import type { editor as MonacoEditorNamespace } from 'monaco-editor'
6
+ import * as monacoApi from 'monaco-editor'
7
+ import EditorWorker from 'monaco-editor/esm/vs/editor/editor.worker?worker'
8
+ import React, { useEffect, useMemo, useState } from 'react'
9
+
10
+ import { TOOL_TOOLTIP_PROPS } from './tool-display'
11
+
12
+ const DIFF_LINE_HEIGHT = 18
13
+ const MIN_DIFF_HEIGHT = 96
14
+ const MAX_DIFF_HEIGHT = 360
15
+
16
+ const monacoRuntime = globalThis as typeof globalThis & {
17
+ MonacoEnvironment?: {
18
+ getWorker: () => Worker
19
+ }
20
+ }
21
+
22
+ if (monacoRuntime.MonacoEnvironment == null) {
23
+ monacoRuntime.MonacoEnvironment = {
24
+ getWorker: () => new EditorWorker()
25
+ }
26
+ }
27
+
28
+ loader.config({ monaco: monacoApi })
29
+
30
+ const getThemeName = () => (document.documentElement.classList.contains('dark') ? 'vs-dark' : 'vs')
31
+
32
+ const getEditorHeight = (original: string, modified: string) => {
33
+ const lineCount = Math.max(
34
+ original === '' ? 1 : original.split('\n').length,
35
+ modified === '' ? 1 : modified.split('\n').length
36
+ )
37
+
38
+ return Math.min(MAX_DIFF_HEIGHT, Math.max(MIN_DIFF_HEIGHT, lineCount * DIFF_LINE_HEIGHT + 28))
39
+ }
40
+
41
+ const getModeIcon = (mode: 'split' | 'inline') => (
42
+ <span className='material-symbols-rounded'>
43
+ {mode === 'split' ? 'splitscreen_right' : 'view_agenda'}
44
+ </span>
45
+ )
46
+
47
+ export interface ToolDiffMetaItem {
48
+ icon?: string
49
+ label: string
50
+ value?: string
51
+ tone?: 'default' | 'success' | 'muted'
52
+ }
53
+
54
+ function useMonacoTheme() {
55
+ const [themeName, setThemeName] = useState(getThemeName)
56
+
57
+ useEffect(() => {
58
+ const observer = new MutationObserver(() => {
59
+ setThemeName(getThemeName())
60
+ })
61
+
62
+ observer.observe(document.documentElement, {
63
+ attributes: true,
64
+ attributeFilter: ['class']
65
+ })
66
+
67
+ return () => {
68
+ observer.disconnect()
69
+ }
70
+ }, [])
71
+
72
+ return themeName
73
+ }
74
+
75
+ export function ToolDiffViewer({
76
+ original,
77
+ modified,
78
+ language,
79
+ metaItems = [],
80
+ splitLabel,
81
+ inlineLabel
82
+ }: {
83
+ original: string
84
+ modified: string
85
+ language?: string
86
+ metaItems?: ToolDiffMetaItem[]
87
+ splitLabel: string
88
+ inlineLabel: string
89
+ }) {
90
+ const [viewMode, setViewMode] = useState<'split' | 'inline'>('split')
91
+ const themeName = useMonacoTheme()
92
+ const height = useMemo(() => getEditorHeight(original, modified), [modified, original])
93
+
94
+ return (
95
+ <div className='tool-diff-viewer'>
96
+ <div className='tool-diff-viewer__toolbar'>
97
+ <div className='tool-diff-viewer__legend'>
98
+ {metaItems.map(item => (
99
+ <span
100
+ key={`${item.label}-${item.value ?? ''}`}
101
+ className={`tool-diff-viewer__meta-item ${
102
+ item.tone != null ? `tool-diff-viewer__meta-item--${item.tone}` : ''
103
+ }`}
104
+ >
105
+ {item.icon != null && item.icon !== '' && (
106
+ <span className='material-symbols-rounded'>{item.icon}</span>
107
+ )}
108
+ <span className='tool-diff-viewer__meta-label'>{item.label}</span>
109
+ {item.value != null && item.value !== '' && (
110
+ <span className='tool-diff-viewer__meta-value'>{item.value}</span>
111
+ )}
112
+ </span>
113
+ ))}
114
+ {language != null && language !== '' && (
115
+ <span className='tool-diff-viewer__lang'>{language}</span>
116
+ )}
117
+ </div>
118
+
119
+ <div className='tool-diff-viewer__mode-switch' role='tablist' aria-label='Diff view mode'>
120
+ {(['split', 'inline'] as const).map(mode => {
121
+ const label = mode === 'split' ? splitLabel : inlineLabel
122
+ return (
123
+ <Tooltip key={mode} title={label} {...TOOL_TOOLTIP_PROPS}>
124
+ <button
125
+ type='button'
126
+ className={`tool-diff-viewer__mode-button ${viewMode === mode ? 'is-active' : ''}`}
127
+ aria-label={label}
128
+ aria-pressed={viewMode === mode}
129
+ onClick={() => setViewMode(mode)}
130
+ >
131
+ {getModeIcon(mode)}
132
+ </button>
133
+ </Tooltip>
134
+ )
135
+ })}
136
+ </div>
137
+ </div>
138
+
139
+ <div
140
+ className='tool-diff-viewer__editor'
141
+ style={{ height: `${height}px` }}
142
+ aria-label='tool diff viewer'
143
+ >
144
+ <DiffEditor
145
+ original={original}
146
+ modified={modified}
147
+ originalLanguage={language ?? 'text'}
148
+ modifiedLanguage={language ?? 'text'}
149
+ theme={themeName}
150
+ loading={null}
151
+ options={{
152
+ automaticLayout: true,
153
+ contextmenu: false,
154
+ diffCodeLens: false,
155
+ fontFamily: 'ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace',
156
+ fontSize: 12,
157
+ glyphMargin: false,
158
+ hideUnchangedRegions: { enabled: false },
159
+ lineDecorationsWidth: 8,
160
+ lineHeight: DIFF_LINE_HEIGHT,
161
+ minimap: { enabled: false },
162
+ originalEditable: false,
163
+ overviewRulerBorder: false,
164
+ readOnly: true,
165
+ renderIndicators: true,
166
+ renderMarginRevertIcon: false,
167
+ renderOverviewRuler: false,
168
+ renderSideBySide: viewMode === 'split',
169
+ scrollBeyondLastLine: false,
170
+ scrollbar: {
171
+ alwaysConsumeMouseWheel: false,
172
+ useShadows: false
173
+ },
174
+ wordWrap: 'on'
175
+ } satisfies MonacoEditorNamespace.IDiffEditorConstructionOptions}
176
+ />
177
+ </div>
178
+ </div>
179
+ )
180
+ }
@@ -11,114 +11,97 @@
11
11
  .tool-group-wrapper {
12
12
  margin: 0;
13
13
 
14
- &.single-item {
15
- // No special style for single item
16
- }
17
-
18
14
  &.card-style {
19
- border: 1px solid var(--border-color);
20
- border-radius: 8px;
21
- background-color: var(--bg-color);
22
- overflow: hidden;
23
-
24
15
  .tool-group-header {
25
16
  display: flex;
26
17
  align-items: center;
27
- justify-content: space-between;
28
- padding: 8px 12px;
29
- background-color: var(--tag-bg, #f9fafb);
30
- border-bottom: 1px solid transparent;
18
+ justify-content: flex-start;
19
+ gap: 4px;
20
+ padding: 1px 0;
21
+ background: transparent;
31
22
  cursor: pointer;
32
23
  user-select: none;
33
- transition: background-color .2s, text-shadow .4s ease;
24
+ color: var(--sub-text-color);
25
+ transition: color .2s ease;
34
26
 
35
27
  .header-left {
36
- display: flex;
37
- align-items: center;
38
- gap: 8px;
39
- font-size: 13px;
28
+ min-width: 0;
29
+ flex: 0 1 auto;
30
+ font-size: 12px;
40
31
  font-weight: 500;
41
- color: var(--text-color);
42
-
43
- .material-symbols-rounded {
44
- font-size: 18px;
45
- color: var(--sub-text-color);
46
- }
32
+ line-height: 1.45;
33
+ white-space: nowrap;
34
+ overflow: hidden;
35
+ text-overflow: ellipsis;
47
36
  }
48
37
 
49
38
  .expand-icon {
50
- font-size: 20px;
51
- color: var(--sub-text-color);
39
+ font-size: 16px;
40
+ flex-shrink: 0;
41
+ transition: transform .22s ease, opacity .18s ease;
42
+ transform-origin: center;
43
+
44
+ &.is-expanded {
45
+ transform: rotate(90deg);
46
+ }
52
47
  }
53
48
 
54
49
  &:hover {
55
- opacity: .8;
50
+ color: var(--text-color);
51
+ }
52
+ }
53
+
54
+ .tool-group-list-shell {
55
+ display: grid;
56
+ grid-template-rows: 0fr;
57
+ opacity: 0;
58
+ visibility: hidden;
59
+ pointer-events: none;
60
+ transition:
61
+ grid-template-rows .22s ease,
62
+ opacity .18s ease,
63
+ visibility 0s linear .22s;
64
+
65
+ &.expanded {
66
+ grid-template-rows: 1fr;
67
+ opacity: 1;
68
+ visibility: visible;
69
+ pointer-events: auto;
70
+ transition: grid-template-rows .22s ease, opacity .18s ease;
56
71
  }
57
72
  }
58
73
 
59
74
  .tool-group-list {
75
+ min-height: 0;
76
+ overflow: hidden;
60
77
  display: flex;
61
78
  flex-direction: column;
62
- gap: 0;
63
- padding: 0;
64
- border-top: 1px solid var(--border-color);
65
- background-color: var(--bg-color);
66
- }
67
-
68
- .tool-group-last-item {
69
- background-color: var(--bg-color);
70
- }
79
+ gap: 2px;
80
+ padding: 4px 0 0;
71
81
 
72
- // Shared style reset for all tool groups inside the card
73
- .tool-group-list, .tool-group-last-item {
74
82
  .tool-group {
75
83
  margin: 0;
76
84
  border: none;
77
85
  border-radius: 0;
78
- background-color: transparent;
79
- overflow: visible; // Allow timeline line to overflow
86
+ background: transparent;
87
+ overflow: visible;
80
88
 
81
89
  .tool-call-box {
82
90
  margin: 0;
83
91
  border: none;
84
92
  border-radius: 0;
93
+ background: transparent;
85
94
 
86
95
  .tool-call-header {
87
- padding: 8px 12px;
88
- background-color: transparent;
89
-
90
- &:hover {
91
- text-shadow: 0 0 .5px currentColor;
92
- }
96
+ background: transparent;
93
97
  }
94
98
 
95
99
  .tool-call-body {
96
- padding: 0;
97
- background-color: transparent;
100
+ background: transparent;
98
101
  }
99
102
  }
100
103
  }
101
104
  }
102
-
103
- // Timeline gap style
104
- .tool-group-list .tool-group {
105
- position: relative;
106
- margin-bottom: 12px; // Increased gap between items
107
- border-bottom: none; // Remove border as we use gap + line
108
-
109
- &::after {
110
- content: '';
111
- position: absolute;
112
- left: 19px;
113
- bottom: -11px; // Adjusted for larger margin
114
- height: 10px; // Increased length for larger gap
115
- width: 2px;
116
- background-color: var(--sub-text-color);
117
- z-index: 99;
118
- border-radius: 4px;
119
- opacity: .5;
120
- }
121
- }
122
105
  }
123
106
  }
124
107
 
@@ -148,14 +131,9 @@
148
131
  }
149
132
 
150
133
  html.dark .tool-group-container .tool-group-wrapper.card-style {
151
- background-color: #1f2937; // Darker bg for card
152
134
  .tool-group-header {
153
- background-color: #374151;
154
135
  &:hover {
155
- text-shadow: 0 0 .5px currentColor;
136
+ color: #e5e7eb;
156
137
  }
157
138
  }
158
- .tool-group-list, .tool-group-last-item {
159
- background-color: #1f2937;
160
- }
161
139
  }
@@ -7,6 +7,7 @@ import { useSearchParams } from 'react-router-dom'
7
7
  import { MessageContextMenu } from '../../messages/MessageContextMenu'
8
8
  import { MessageFooter } from '../../messages/MessageFooter'
9
9
  import { ToolRenderer } from './ToolRenderer'
10
+ import { getToolGroupSummaryText } from './tool-summary'
10
11
 
11
12
  interface ToolGroupProps {
12
13
  anchorId: string
@@ -38,15 +39,14 @@ function ToolGroupComponent({
38
39
  const isDebugMode = searchParams.get('debug') === 'true'
39
40
  const [expanded, setExpanded] = useState(false)
40
41
 
41
- const lastItem = items[items.length - 1]
42
- const otherItems = items.slice(0, -1)
42
+ if (items.length === 0) return null
43
+
44
+ const summaryText = getToolGroupSummaryText(items, t)
43
45
  const shouldForceExpand = targetToolUseId != null &&
44
46
  targetToolUseId !== '' &&
45
- otherItems.some(item => item.item.id === targetToolUseId)
47
+ items.some(item => item.item.id === targetToolUseId)
46
48
  const isExpanded = expanded || shouldForceExpand
47
49
 
48
- if (items.length === 0) return null
49
-
50
50
  // If only one item, just render it directly (wrapped in container for footer)
51
51
  if (items.length === 1) {
52
52
  return (
@@ -96,48 +96,33 @@ function ToolGroupComponent({
96
96
  >
97
97
  <div id={anchorId} className='tool-group-container'>
98
98
  <div className='tool-group-wrapper card-style'>
99
- <div
100
- className='tool-group-header'
101
- onClick={() => setExpanded(!expanded)}
102
- >
103
- <div className='header-left'>
104
- <span className='material-symbols-rounded'>dataset</span>
105
- <span>{t('chat.usedTools', { count: items.length })}</span>
99
+ <div
100
+ className='tool-group-header'
101
+ aria-expanded={isExpanded}
102
+ onClick={() => setExpanded(!expanded)}
103
+ >
104
+ <div className='header-left'>
105
+ <span>{summaryText}</span>
106
+ </div>
107
+ <span className={`material-symbols-rounded expand-icon ${isExpanded ? 'is-expanded' : ''}`}>
108
+ chevron_right
109
+ </span>
106
110
  </div>
107
- <span className='material-symbols-rounded expand-icon'>
108
- {isExpanded ? 'expand_less' : 'expand_more'}
109
- </span>
110
- </div>
111
111
 
112
- {isExpanded && (
113
- <div className='tool-group-list'>
114
- {otherItems.map((it, idx) => (
115
- <div key={it.item.id || idx} data-tool-use-id={it.item.id}>
112
+ <div
113
+ className={`tool-group-list-shell ${isExpanded ? 'expanded' : 'collapsed'}`}
114
+ aria-hidden={!isExpanded}
115
+ >
116
+ <div className='tool-group-list'>
117
+ {items.map((it, idx) => (
116
118
  <ToolRenderer
119
+ key={it.item.id || idx}
117
120
  item={it.item}
118
121
  resultItem={it.resultItem}
119
122
  />
120
- </div>
121
- ))}
122
- </div>
123
- )}
124
-
125
- {
126
- /* Always show the last item, but if expanded, it's just part of the list visually.
127
- If collapsed, it appears "below" the header.
128
- Actually, to make it look like "part of the list", we should just put it in the flow.
129
-
130
- When collapsed: Header + Last Item
131
- When expanded: Header + Other Items + Last Item
132
- */
133
- }
134
- <div className='tool-group-last-item' data-tool-use-id={lastItem.item.id}>
135
- <ToolRenderer
136
- item={lastItem.item}
137
- resultItem={lastItem.resultItem}
138
- />
123
+ ))}
124
+ </div>
139
125
  </div>
140
-
141
126
  </div>
142
127
 
143
128
  {footer && isDebugMode && (
@@ -3,7 +3,7 @@ import React from 'react'
3
3
  import type { ChatMessageContent } from '@vibe-forge/core'
4
4
 
5
5
  import { DefaultTool } from '../DefaultTool'
6
- import { BashTool, adapterClaudeToolRenders } from '../adapter-claude'
6
+ import { GenericClaudeTool, adapterClaudeToolRenders, isClaudeToolName } from '../adapter-claude'
7
7
  import { chromeDevtoolsToolRenders } from '../plugin-chrome-devtools'
8
8
  import { taskToolRenders } from '../task'
9
9
 
@@ -28,7 +28,7 @@ export function ToolRenderer({
28
28
  }) {
29
29
  const toolName = item.name
30
30
  const foundRenderer = TOOL_RENDERERS[toolName] ?? ToolRenderer.findRendererByInput(item)
31
- const Renderer = foundRenderer ?? DefaultTool
31
+ const Renderer = foundRenderer ?? (isClaudeToolName(toolName) ? GenericClaudeTool : DefaultTool)
32
32
  return <Renderer item={item} resultItem={resultItem} />
33
33
  }
34
34
 
@@ -40,7 +40,7 @@ ToolRenderer.findRendererByInput = (item: Extract<ChatMessageContent, { type: 't
40
40
  'command' in input &&
41
41
  (('description' in input && input.description != null) || ('reason' in input && input.reason != null))
42
42
  ) {
43
- return BashTool
43
+ return GenericClaudeTool
44
44
  }
45
45
  }
46
46
  return null
@@ -0,0 +1,66 @@
1
+ import { CodeBlock } from '#~/components/CodeBlock'
2
+ import { MarkdownContent } from '#~/components/MarkdownContent'
3
+ import { safeJsonStringify } from '#~/utils/safe-serialize'
4
+
5
+ import { getStringList, getStructuredBlocks, looksLikeMarkdown } from './tool-result-content-utils'
6
+
7
+ export function ToolResultContent({
8
+ content,
9
+ preferMarkdown = false
10
+ }: {
11
+ content: unknown
12
+ preferMarkdown?: boolean
13
+ }) {
14
+ const structuredBlocks = getStructuredBlocks(content)
15
+ if (structuredBlocks != null) {
16
+ return (
17
+ <div className='tool-result-structured'>
18
+ {structuredBlocks.map((block, index) => (
19
+ block.type === 'text'
20
+ ? (
21
+ <div className='tool-result-text' key={`text-${index}`}>
22
+ {block.format === 'markdown'
23
+ ? <MarkdownContent content={block.text} />
24
+ : <div className='tool-result-text-content'>{block.text}</div>}
25
+ </div>
26
+ )
27
+ : (
28
+ <div className='tool-result-image-wrapper' key={`image-${index}`}>
29
+ <img
30
+ className='tool-result-image'
31
+ src={block.src}
32
+ alt={block.alt ?? ''}
33
+ width={block.width}
34
+ height={block.height}
35
+ />
36
+ {block.title != null && block.title !== '' && (
37
+ <div className='tool-result-image-caption'>{block.title}</div>
38
+ )}
39
+ </div>
40
+ )
41
+ ))}
42
+ </div>
43
+ )
44
+ }
45
+
46
+ const stringList = getStringList(content)
47
+ if (stringList != null) {
48
+ return (
49
+ <div className='tool-result-list'>
50
+ {stringList.map(item => (
51
+ <div className='tool-result-list-item' key={item}>{item}</div>
52
+ ))}
53
+ </div>
54
+ )
55
+ }
56
+
57
+ if (typeof content === 'string') {
58
+ if (content.startsWith('```') || (preferMarkdown && looksLikeMarkdown(content))) {
59
+ return <MarkdownContent content={content} />
60
+ }
61
+
62
+ return <CodeBlock code={content} lang='text' />
63
+ }
64
+
65
+ return <CodeBlock code={safeJsonStringify(content, 2)} lang='json' />
66
+ }
@@ -0,0 +1,67 @@
1
+ import { Tooltip } from 'antd'
2
+ import React from 'react'
3
+
4
+ import { TOOL_TOOLTIP_PROPS } from './tool-display'
5
+
6
+ export function ToolSummaryHeader({
7
+ icon,
8
+ title,
9
+ target,
10
+ targetTitle,
11
+ meta,
12
+ metaTitle,
13
+ expanded = false,
14
+ collapsible = false,
15
+ targetMonospace = false
16
+ }: {
17
+ icon?: React.ReactNode
18
+ title: React.ReactNode
19
+ target?: React.ReactNode
20
+ targetTitle?: string
21
+ meta?: React.ReactNode
22
+ metaTitle?: string
23
+ expanded?: boolean
24
+ collapsible?: boolean
25
+ targetMonospace?: boolean
26
+ }) {
27
+ const hasTarget = target != null && target !== ''
28
+ const targetNode = hasTarget
29
+ ? (
30
+ <span className={`tool-summary-header__target ${targetMonospace ? 'tool-summary-header__target--mono' : ''}`}>
31
+ {target}
32
+ </span>
33
+ )
34
+ : null
35
+
36
+ return (
37
+ <div className='tool-summary-header'>
38
+ <div className='tool-summary-header__lead'>
39
+ <span className='tool-summary-header__action'>
40
+ {icon != null && (
41
+ <span className='tool-summary-header__icon'>{icon}</span>
42
+ )}
43
+ <span className='tool-summary-header__title'>{title}</span>
44
+ </span>
45
+ {hasTarget && (
46
+ targetTitle != null && targetTitle !== ''
47
+ ? (
48
+ <Tooltip title={targetTitle} {...TOOL_TOOLTIP_PROPS}>
49
+ {targetNode}
50
+ </Tooltip>
51
+ )
52
+ : targetNode
53
+ )}
54
+ {collapsible && (
55
+ <span className={`material-symbols-rounded tool-summary-header__toggle ${expanded ? 'is-expanded' : ''}`}>
56
+ expand_more
57
+ </span>
58
+ )}
59
+ </div>
60
+ {meta != null && (
61
+ <span className='tool-summary-header__meta' title={metaTitle}>
62
+ {meta}
63
+ </span>
64
+ )}
65
+ </div>
66
+ )
67
+ }
@@ -0,0 +1,57 @@
1
+ import { toSerializable } from '#~/utils/safe-serialize'
2
+
3
+ import { getStringList, getStructuredBlocks } from './tool-result-content-utils'
4
+
5
+ const parseStructuredInput = (value: unknown) => {
6
+ if (typeof value !== 'string') {
7
+ return value
8
+ }
9
+
10
+ const trimmed = value.trim()
11
+ if (
12
+ (trimmed.startsWith('{') && trimmed.endsWith('}')) ||
13
+ (trimmed.startsWith('[') && trimmed.endsWith(']'))
14
+ ) {
15
+ try {
16
+ return JSON.parse(trimmed)
17
+ } catch {
18
+ return value
19
+ }
20
+ }
21
+
22
+ return value
23
+ }
24
+
25
+ export const hasMeaningfulToolValue = (value: unknown): boolean => {
26
+ const parsed = parseStructuredInput(toSerializable(value))
27
+
28
+ if (parsed == null) {
29
+ return false
30
+ }
31
+
32
+ if (typeof parsed === 'string') {
33
+ return parsed.trim() !== ''
34
+ }
35
+
36
+ if (typeof parsed === 'number' || typeof parsed === 'boolean') {
37
+ return true
38
+ }
39
+
40
+ if (Array.isArray(parsed)) {
41
+ return parsed.some(item => hasMeaningfulToolValue(item))
42
+ }
43
+
44
+ if (typeof parsed !== 'object') {
45
+ return false
46
+ }
47
+
48
+ if (getStructuredBlocks(parsed) != null) {
49
+ return true
50
+ }
51
+
52
+ if (getStringList(parsed) != null) {
53
+ return true
54
+ }
55
+
56
+ return Object.values(parsed as Record<string, unknown>).some(item => hasMeaningfulToolValue(item))
57
+ }