@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,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
@@ -16,6 +17,7 @@ interface ToolGroupProps {
16
17
  }[]
17
18
  originalMessage: ChatMessage
18
19
  sessionId?: string
20
+ targetToolUseId?: string
19
21
  footer?: {
20
22
  model?: string
21
23
  usage?: ChatMessage['usage']
@@ -29,6 +31,7 @@ function ToolGroupComponent({
29
31
  items,
30
32
  originalMessage,
31
33
  sessionId,
34
+ targetToolUseId,
32
35
  footer
33
36
  }: ToolGroupProps) {
34
37
  const { t } = useTranslation()
@@ -38,6 +41,12 @@ function ToolGroupComponent({
38
41
 
39
42
  if (items.length === 0) return null
40
43
 
44
+ const summaryText = getToolGroupSummaryText(items, t)
45
+ const shouldForceExpand = targetToolUseId != null &&
46
+ targetToolUseId !== '' &&
47
+ items.some(item => item.item.id === targetToolUseId)
48
+ const isExpanded = expanded || shouldForceExpand
49
+
41
50
  // If only one item, just render it directly (wrapped in container for footer)
42
51
  if (items.length === 1) {
43
52
  return (
@@ -55,7 +64,7 @@ function ToolGroupComponent({
55
64
  onStartEditing={() => {}}
56
65
  >
57
66
  <div id={anchorId} className='tool-group-container'>
58
- <div className='tool-group-wrapper single-item'>
67
+ <div className='tool-group-wrapper single-item' data-tool-use-id={items[0].item.id}>
59
68
  <ToolRenderer
60
69
  item={items[0].item}
61
70
  resultItem={items[0].resultItem}
@@ -71,9 +80,6 @@ function ToolGroupComponent({
71
80
  )
72
81
  }
73
82
 
74
- const lastItem = items[items.length - 1]
75
- const otherItems = items.slice(0, -1)
76
-
77
83
  return (
78
84
  <MessageContextMenu
79
85
  anchorId={anchorId}
@@ -92,20 +98,23 @@ function ToolGroupComponent({
92
98
  <div className='tool-group-wrapper card-style'>
93
99
  <div
94
100
  className='tool-group-header'
101
+ aria-expanded={isExpanded}
95
102
  onClick={() => setExpanded(!expanded)}
96
103
  >
97
104
  <div className='header-left'>
98
- <span className='material-symbols-rounded'>dataset</span>
99
- <span>{t('chat.usedTools', { count: items.length })}</span>
105
+ <span>{summaryText}</span>
100
106
  </div>
101
- <span className='material-symbols-rounded expand-icon'>
102
- {expanded ? 'expand_less' : 'expand_more'}
107
+ <span className={`material-symbols-rounded expand-icon ${isExpanded ? 'is-expanded' : ''}`}>
108
+ chevron_right
103
109
  </span>
104
110
  </div>
105
111
 
106
- {expanded && (
112
+ <div
113
+ className={`tool-group-list-shell ${isExpanded ? 'expanded' : 'collapsed'}`}
114
+ aria-hidden={!isExpanded}
115
+ >
107
116
  <div className='tool-group-list'>
108
- {otherItems.map((it, idx) => (
117
+ {items.map((it, idx) => (
109
118
  <ToolRenderer
110
119
  key={it.item.id || idx}
111
120
  item={it.item}
@@ -113,22 +122,6 @@ function ToolGroupComponent({
113
122
  />
114
123
  ))}
115
124
  </div>
116
- )}
117
-
118
- {
119
- /* Always show the last item, but if expanded, it's just part of the list visually.
120
- If collapsed, it appears "below" the header.
121
- Actually, to make it look like "part of the list", we should just put it in the flow.
122
-
123
- When collapsed: Header + Last Item
124
- When expanded: Header + Other Items + Last Item
125
- */
126
- }
127
- <div className='tool-group-last-item'>
128
- <ToolRenderer
129
- item={lastItem.item}
130
- resultItem={lastItem.resultItem}
131
- />
132
125
  </div>
133
126
  </div>
134
127
 
@@ -145,6 +138,7 @@ function ToolGroupComponent({
145
138
  const areToolGroupPropsEqual = (prev: ToolGroupProps, next: ToolGroupProps) => {
146
139
  if (prev.anchorId !== next.anchorId) return false
147
140
  if (prev.items.length !== next.items.length) return false
141
+ if (prev.targetToolUseId !== next.targetToolUseId) return false
148
142
  for (let i = 0; i < prev.items.length; i++) {
149
143
  if (prev.items[i].item !== next.items[i].item) return false
150
144
  if (prev.items[i].resultItem !== next.items[i].resultItem) return false
@@ -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
+ }