@vibe-forge/client 0.2.0-alpha.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 (184) hide show
  1. package/LICENSE +21 -0
  2. package/cli.cjs +6 -0
  3. package/index.html +27 -0
  4. package/package.json +42 -0
  5. package/src/App.tsx +174 -0
  6. package/src/api.ts +241 -0
  7. package/src/components/ArchiveView.scss +168 -0
  8. package/src/components/ArchiveView.tsx +299 -0
  9. package/src/components/AutomationView/AutomationView.scss +26 -0
  10. package/src/components/AutomationView/RuleFormPanel.scss +129 -0
  11. package/src/components/AutomationView/RuleFormPanel.tsx +257 -0
  12. package/src/components/AutomationView/RuleSidebar.scss +219 -0
  13. package/src/components/AutomationView/RuleSidebar.tsx +258 -0
  14. package/src/components/AutomationView/RunHistoryPanel.scss +286 -0
  15. package/src/components/AutomationView/RunHistoryPanel.tsx +320 -0
  16. package/src/components/AutomationView/TaskList.scss +128 -0
  17. package/src/components/AutomationView/TaskList.tsx +79 -0
  18. package/src/components/AutomationView/TriggerList.scss +153 -0
  19. package/src/components/AutomationView/TriggerList.tsx +217 -0
  20. package/src/components/AutomationView/index.tsx +228 -0
  21. package/src/components/AutomationView/types.ts +21 -0
  22. package/src/components/Chat.scss +89 -0
  23. package/src/components/Chat.tsx +92 -0
  24. package/src/components/ConfigView.scss +185 -0
  25. package/src/components/ConfigView.tsx +258 -0
  26. package/src/components/NavRail.scss +71 -0
  27. package/src/components/NavRail.tsx +188 -0
  28. package/src/components/Sidebar.scss +112 -0
  29. package/src/components/Sidebar.tsx +291 -0
  30. package/src/components/chat/ChatHeader.scss +401 -0
  31. package/src/components/chat/ChatHeader.tsx +342 -0
  32. package/src/components/chat/ChatHistoryView.tsx +122 -0
  33. package/src/components/chat/ChatSettingsView.tsx +22 -0
  34. package/src/components/chat/ChatTimelineView.scss +53 -0
  35. package/src/components/chat/ChatTimelineView.tsx +158 -0
  36. package/src/components/chat/CodeBlock.scss +87 -0
  37. package/src/components/chat/CodeBlock.tsx +179 -0
  38. package/src/components/chat/CompletionMenu.scss +70 -0
  39. package/src/components/chat/CompletionMenu.tsx +58 -0
  40. package/src/components/chat/CurrentTodoList.scss +217 -0
  41. package/src/components/chat/CurrentTodoList.tsx +103 -0
  42. package/src/components/chat/MarkdownContent.tsx +43 -0
  43. package/src/components/chat/MessageFooter.tsx +48 -0
  44. package/src/components/chat/MessageItem.scss +251 -0
  45. package/src/components/chat/MessageItem.tsx +78 -0
  46. package/src/components/chat/NewSessionGuide.scss +186 -0
  47. package/src/components/chat/NewSessionGuide.tsx +167 -0
  48. package/src/components/chat/Sender.scss +367 -0
  49. package/src/components/chat/Sender.tsx +541 -0
  50. package/src/components/chat/SessionTimelinePanel/EventList.scss +58 -0
  51. package/src/components/chat/SessionTimelinePanel/EventList.tsx +212 -0
  52. package/src/components/chat/SessionTimelinePanel/gantt.ts +177 -0
  53. package/src/components/chat/SessionTimelinePanel/git-graph.ts +518 -0
  54. package/src/components/chat/SessionTimelinePanel/index.scss +28 -0
  55. package/src/components/chat/SessionTimelinePanel/index.tsx +121 -0
  56. package/src/components/chat/SessionTimelinePanel/mermaid.ts +4 -0
  57. package/src/components/chat/SessionTimelinePanel/types.ts +64 -0
  58. package/src/components/chat/SessionTimelinePanel/utils.ts +20 -0
  59. package/src/components/chat/ThinkingStatus.scss +70 -0
  60. package/src/components/chat/ThinkingStatus.tsx +13 -0
  61. package/src/components/chat/ToolCallBox.scss +137 -0
  62. package/src/components/chat/ToolCallBox.tsx +55 -0
  63. package/src/components/chat/ToolGroup.scss +154 -0
  64. package/src/components/chat/ToolGroup.tsx +102 -0
  65. package/src/components/chat/ToolRenderer.tsx +45 -0
  66. package/src/components/chat/messageUtils.ts +171 -0
  67. package/src/components/chat/safeSerialize.ts +84 -0
  68. package/src/components/chat/tools/DefaultTool.tsx +63 -0
  69. package/src/components/chat/tools/adapter-claude/BashTool.scss +71 -0
  70. package/src/components/chat/tools/adapter-claude/BashTool.tsx +82 -0
  71. package/src/components/chat/tools/adapter-claude/GlobTool.scss +88 -0
  72. package/src/components/chat/tools/adapter-claude/GlobTool.tsx +85 -0
  73. package/src/components/chat/tools/adapter-claude/GrepTool.scss +96 -0
  74. package/src/components/chat/tools/adapter-claude/GrepTool.tsx +114 -0
  75. package/src/components/chat/tools/adapter-claude/LSTool.scss +85 -0
  76. package/src/components/chat/tools/adapter-claude/LSTool.tsx +94 -0
  77. package/src/components/chat/tools/adapter-claude/ReadTool.scss +57 -0
  78. package/src/components/chat/tools/adapter-claude/ReadTool.tsx +87 -0
  79. package/src/components/chat/tools/adapter-claude/TodoTool.scss +78 -0
  80. package/src/components/chat/tools/adapter-claude/TodoTool.tsx +60 -0
  81. package/src/components/chat/tools/adapter-claude/WriteTool.scss +92 -0
  82. package/src/components/chat/tools/adapter-claude/WriteTool.tsx +86 -0
  83. package/src/components/chat/tools/adapter-claude/components/FileList.scss +65 -0
  84. package/src/components/chat/tools/adapter-claude/components/FileList.tsx +185 -0
  85. package/src/components/chat/tools/adapter-claude/index.ts +28 -0
  86. package/src/components/chat/tools/defineToolRender.ts +28 -0
  87. package/src/components/chat/tools/task/GetTaskInfoTool.scss +50 -0
  88. package/src/components/chat/tools/task/GetTaskInfoTool.tsx +88 -0
  89. package/src/components/chat/tools/task/ListTasksTool.scss +56 -0
  90. package/src/components/chat/tools/task/ListTasksTool.tsx +83 -0
  91. package/src/components/chat/tools/task/StartTasksTool.scss +56 -0
  92. package/src/components/chat/tools/task/StartTasksTool.tsx +96 -0
  93. package/src/components/chat/tools/task/components/TaskToolCard.scss +127 -0
  94. package/src/components/chat/tools/task/components/TaskToolCard.tsx +177 -0
  95. package/src/components/chat/tools/task/index.ts +15 -0
  96. package/src/components/chat/useChatModels.tsx +206 -0
  97. package/src/components/chat/useChatSession.ts +370 -0
  98. package/src/components/config/ConfigAboutSection.scss +111 -0
  99. package/src/components/config/ConfigAboutSection.tsx +86 -0
  100. package/src/components/config/ConfigDisplayValue.scss +22 -0
  101. package/src/components/config/ConfigDisplayValue.tsx +62 -0
  102. package/src/components/config/ConfigEditors.scss +65 -0
  103. package/src/components/config/ConfigEditors.tsx +98 -0
  104. package/src/components/config/ConfigFieldRow.scss +97 -0
  105. package/src/components/config/ConfigFieldRow.tsx +36 -0
  106. package/src/components/config/ConfigSectionForm.scss +94 -0
  107. package/src/components/config/ConfigSectionForm.tsx +436 -0
  108. package/src/components/config/ConfigSectionPanel.tsx +67 -0
  109. package/src/components/config/ConfigShortcutInput.scss +11 -0
  110. package/src/components/config/ConfigShortcutInput.tsx +52 -0
  111. package/src/components/config/ConfigSourceSwitch.tsx +57 -0
  112. package/src/components/config/configSchema.ts +319 -0
  113. package/src/components/config/configUtils.ts +83 -0
  114. package/src/components/config/index.tsx +5 -0
  115. package/src/components/config/recordEditors/BooleanRecordEditor.scss +1 -0
  116. package/src/components/config/recordEditors/BooleanRecordEditor.tsx +75 -0
  117. package/src/components/config/recordEditors/KeyValueEditor.scss +1 -0
  118. package/src/components/config/recordEditors/KeyValueEditor.tsx +97 -0
  119. package/src/components/config/recordEditors/McpServersRecordEditor.scss +1 -0
  120. package/src/components/config/recordEditors/McpServersRecordEditor.tsx +258 -0
  121. package/src/components/config/recordEditors/ModelServicesRecordEditor.scss +1 -0
  122. package/src/components/config/recordEditors/ModelServicesRecordEditor.tsx +233 -0
  123. package/src/components/config/recordEditors/RecordEditors.scss +117 -0
  124. package/src/components/config/recordEditors/RecordJsonEditor.scss +1 -0
  125. package/src/components/config/recordEditors/RecordJsonEditor.tsx +113 -0
  126. package/src/components/config/recordEditors/index.tsx +5 -0
  127. package/src/components/knowledge-base/KnowledgeBaseView.scss +19 -0
  128. package/src/components/knowledge-base/KnowledgeBaseView.tsx +186 -0
  129. package/src/components/knowledge-base/components/ActionButton.scss +5 -0
  130. package/src/components/knowledge-base/components/ActionButton.tsx +9 -0
  131. package/src/components/knowledge-base/components/EmptyState.scss +19 -0
  132. package/src/components/knowledge-base/components/EmptyState.tsx +42 -0
  133. package/src/components/knowledge-base/components/EntitiesTab.scss +5 -0
  134. package/src/components/knowledge-base/components/EntitiesTab.tsx +80 -0
  135. package/src/components/knowledge-base/components/EntityItem.scss +82 -0
  136. package/src/components/knowledge-base/components/EntityItem.tsx +79 -0
  137. package/src/components/knowledge-base/components/EntityList.scss +5 -0
  138. package/src/components/knowledge-base/components/EntityList.tsx +70 -0
  139. package/src/components/knowledge-base/components/FilterBar.scss +21 -0
  140. package/src/components/knowledge-base/components/FilterBar.tsx +51 -0
  141. package/src/components/knowledge-base/components/FlowsTab.scss +5 -0
  142. package/src/components/knowledge-base/components/FlowsTab.tsx +80 -0
  143. package/src/components/knowledge-base/components/KnowledgeBaseHeader.scss +27 -0
  144. package/src/components/knowledge-base/components/KnowledgeBaseHeader.tsx +29 -0
  145. package/src/components/knowledge-base/components/KnowledgeList.scss +19 -0
  146. package/src/components/knowledge-base/components/KnowledgeList.tsx +19 -0
  147. package/src/components/knowledge-base/components/LoadingState.scss +5 -0
  148. package/src/components/knowledge-base/components/LoadingState.tsx +11 -0
  149. package/src/components/knowledge-base/components/MetaList.scss +19 -0
  150. package/src/components/knowledge-base/components/MetaList.tsx +18 -0
  151. package/src/components/knowledge-base/components/RulesTab.scss +5 -0
  152. package/src/components/knowledge-base/components/RulesTab.tsx +49 -0
  153. package/src/components/knowledge-base/components/SectionHeader.scss +22 -0
  154. package/src/components/knowledge-base/components/SectionHeader.tsx +21 -0
  155. package/src/components/knowledge-base/components/SkillsTab.scss +5 -0
  156. package/src/components/knowledge-base/components/SkillsTab.tsx +49 -0
  157. package/src/components/knowledge-base/components/SpecItem.scss +138 -0
  158. package/src/components/knowledge-base/components/SpecItem.tsx +131 -0
  159. package/src/components/knowledge-base/components/SpecList.scss +5 -0
  160. package/src/components/knowledge-base/components/SpecList.tsx +70 -0
  161. package/src/components/knowledge-base/components/TabContent.scss +8 -0
  162. package/src/components/knowledge-base/components/TabContent.tsx +17 -0
  163. package/src/components/knowledge-base/components/TabLabel.scss +10 -0
  164. package/src/components/knowledge-base/components/TabLabel.tsx +15 -0
  165. package/src/components/knowledge-base/index.tsx +1 -0
  166. package/src/components/sidebar/SessionItem.scss +256 -0
  167. package/src/components/sidebar/SessionItem.tsx +265 -0
  168. package/src/components/sidebar/SessionList.scss +92 -0
  169. package/src/components/sidebar/SessionList.tsx +166 -0
  170. package/src/components/sidebar/SidebarHeader.scss +79 -0
  171. package/src/components/sidebar/SidebarHeader.tsx +128 -0
  172. package/src/connectionManager.ts +172 -0
  173. package/src/hooks/useGlobalShortcut.ts +26 -0
  174. package/src/hooks/useQueryParams.ts +54 -0
  175. package/src/i18n.ts +22 -0
  176. package/src/main.tsx +41 -0
  177. package/src/resources/locales/en.json +765 -0
  178. package/src/resources/locales/zh.json +766 -0
  179. package/src/store/index.ts +23 -0
  180. package/src/styles/global.scss +100 -0
  181. package/src/utils/shortcutUtils.ts +88 -0
  182. package/src/vite-env.d.ts +12 -0
  183. package/src/ws.ts +33 -0
  184. package/vite.config.ts +26 -0
@@ -0,0 +1,64 @@
1
+ export type TimelineEventType =
2
+ | 'tool__StartTasks'
3
+ | 'tool__AskUserQuestion'
4
+ | 'tool__Edit'
5
+ | 'tool__ResumeTask'
6
+ | 'user__Prompt'
7
+
8
+ interface TimelineBaseEvent {
9
+ startTime: string
10
+ endTime: string
11
+ }
12
+
13
+ export type TimelineEvent =
14
+ & TimelineBaseEvent
15
+ & (
16
+ | {
17
+ type: 'tool__StartTasks'
18
+ tasks?: Record<string, Task>
19
+ }
20
+ | {
21
+ type: 'tool__AskUserQuestion'
22
+ }
23
+ | {
24
+ type: 'tool__Edit'
25
+ }
26
+ | {
27
+ type: 'tool__ResumeTask'
28
+ }
29
+ | {
30
+ type: 'user__Prompt'
31
+ }
32
+ )
33
+
34
+ export interface Task {
35
+ startTime: string
36
+ endTime: string
37
+
38
+ description?: string
39
+ model?: string
40
+
41
+ events?: TimelineEvent[]
42
+ }
43
+
44
+ export type TimelineInteractionPayload =
45
+ | {
46
+ kind: 'event'
47
+ event: TimelineEvent
48
+ }
49
+ | {
50
+ kind: 'task'
51
+ name: string
52
+ task: Task
53
+ }
54
+
55
+ export interface TimelineInteraction {
56
+ id: string
57
+ label: string
58
+ payload: TimelineInteractionPayload
59
+ }
60
+
61
+ export interface TimelineDiagram {
62
+ code: string
63
+ interactions: TimelineInteraction[]
64
+ }
@@ -0,0 +1,20 @@
1
+ export function sanitizeId(value: string) {
2
+ return value.replace(/\W/g, '_')
3
+ }
4
+
5
+ export function parseTime(value: string) {
6
+ const [hours, minutes, seconds] = value.split(':').map(Number)
7
+ return (hours || 0) * 3600 + (minutes || 0) * 60 + (seconds || 0)
8
+ }
9
+
10
+ export function normalizeTime(value: string) {
11
+ const [hours, minutes, seconds] = value.split(':')
12
+ const normalizePart = (part?: string) => {
13
+ const parsed = Number.parseInt(part ?? '0', 10)
14
+ if (Number.isFinite(parsed)) {
15
+ return `${parsed}`.padStart(2, '0')
16
+ }
17
+ return '00'
18
+ }
19
+ return `${normalizePart(hours)}:${normalizePart(minutes)}:${normalizePart(seconds)}`
20
+ }
@@ -0,0 +1,70 @@
1
+ .chat-thinking-status {
2
+ display: flex;
3
+ align-items: center;
4
+ gap: 8px;
5
+ padding: 8px 0;
6
+ color: #9ca3af;
7
+ font-size: 12px;
8
+ animation: fadeIn .3s ease-in-out;
9
+
10
+ .dot-flashing {
11
+ position: relative;
12
+ width: 4px;
13
+ height: 4px;
14
+ border-radius: 5px;
15
+ background-color: #3b82f6;
16
+ color: #3b82f6;
17
+ animation: dotFlashing 1s infinite linear alternate;
18
+ animation-delay: .5s;
19
+ margin: 0 12px 0 10px;
20
+
21
+ &::before, &::after {
22
+ content: '';
23
+ display: inline-block;
24
+ position: absolute;
25
+ top: 0;
26
+ }
27
+
28
+ &::before {
29
+ left: -8px;
30
+ width: 4px;
31
+ height: 4px;
32
+ border-radius: 5px;
33
+ background-color: #3b82f6;
34
+ color: #3b82f6;
35
+ animation: dotFlashing 1s infinite linear alternate;
36
+ animation-delay: 0s;
37
+ }
38
+
39
+ &::after {
40
+ left: 8px;
41
+ width: 4px;
42
+ height: 4px;
43
+ border-radius: 5px;
44
+ background-color: #3b82f6;
45
+ color: #3b82f6;
46
+ animation: dotFlashing 1s infinite linear alternate;
47
+ animation-delay: 1s;
48
+ }
49
+ }
50
+ }
51
+
52
+ @keyframes dotFlashing {
53
+ 0% {
54
+ background-color: #3b82f6;
55
+ }
56
+ 50%, 100% {
57
+ background-color: #e5e7eb;
58
+ }
59
+ }
60
+
61
+ @keyframes fadeIn {
62
+ from {
63
+ opacity: 0;
64
+ transform: translateY(5px);
65
+ }
66
+ to {
67
+ opacity: 1;
68
+ transform: translateY(0);
69
+ }
70
+ }
@@ -0,0 +1,13 @@
1
+ import './ThinkingStatus.scss'
2
+ import React from 'react'
3
+ import { useTranslation } from 'react-i18next'
4
+
5
+ export function ThinkingStatus() {
6
+ const { t } = useTranslation()
7
+ return (
8
+ <div className='chat-thinking-status'>
9
+ <div className='dot-flashing'></div>
10
+ <span>{t('chat.thinking')}</span>
11
+ </div>
12
+ )
13
+ }
@@ -0,0 +1,137 @@
1
+ .tool-group {
2
+ margin: 0;
3
+ border: 1px solid var(--border-color);
4
+ border-radius: 6px;
5
+ overflow: hidden;
6
+ background-color: var(--bg-color);
7
+
8
+ .tool-call-box {
9
+ border: none;
10
+ margin: 0;
11
+ border-radius: 0;
12
+
13
+ &.result {
14
+ background-color: transparent;
15
+ }
16
+ }
17
+
18
+ .tool-call-header {
19
+ padding: 8px 12px;
20
+ height: 36px;
21
+ box-sizing: border-box;
22
+ cursor: pointer;
23
+ user-select: none;
24
+ display: flex;
25
+ align-items: center;
26
+ justify-content: space-between;
27
+ gap: 8px;
28
+ font-size: 13px;
29
+ font-weight: 500;
30
+ color: var(--text-color);
31
+ background-color: var(--tag-bg);
32
+ transition: text-shadow .4s ease;
33
+
34
+ &.collapsed {
35
+ cursor: default;
36
+ }
37
+
38
+ .tool-call-header-main {
39
+ display: flex;
40
+ align-items: center;
41
+ gap: 6px;
42
+ flex: 1;
43
+ min-width: 0;
44
+ }
45
+
46
+ &:hover {
47
+ text-shadow: 0 0 .5px currentColor;
48
+ }
49
+
50
+ .material-symbols-rounded {
51
+ font-size: 18px;
52
+ color: var(--sub-text-color);
53
+
54
+ &.expand-icon {
55
+ font-size: 16px;
56
+ }
57
+ }
58
+ }
59
+
60
+ .tool-result-header {
61
+ padding: 8px 12px;
62
+ height: 36px;
63
+ box-sizing: border-box;
64
+ cursor: pointer;
65
+ user-select: none;
66
+ border-top: 1px solid var(--border-color);
67
+ display: flex;
68
+ align-items: center;
69
+ gap: 8px;
70
+ font-size: 13px;
71
+ font-weight: 500;
72
+ color: #059669;
73
+ background-color: var(--tag-bg);
74
+
75
+ &.error {
76
+ color: #dc2626;
77
+ .material-symbols-rounded {
78
+ color: #dc2626;
79
+ }
80
+ }
81
+
82
+ &:hover {
83
+ opacity: .8;
84
+ }
85
+
86
+ .material-symbols-rounded {
87
+ font-size: 18px;
88
+ color: #059669;
89
+ }
90
+ }
91
+
92
+ .tool-call-box:first-child {
93
+ .tool-result-header {
94
+ border-top: none;
95
+ }
96
+ }
97
+
98
+ .tool-call-box.expanded {
99
+ .tool-call-header {
100
+ border-bottom: 1px solid var(--border-color);
101
+ }
102
+ .tool-result-header {
103
+ border-bottom: none;
104
+ }
105
+ }
106
+
107
+ .tool-call-box.collapsed {
108
+ .tool-call-header {
109
+ border-bottom: none;
110
+ }
111
+ .tool-result-header {
112
+ border-bottom: none;
113
+ }
114
+ }
115
+
116
+ .tool-call-body {
117
+ padding: 0;
118
+ overflow: hidden;
119
+ transition: all .2s ease-in-out;
120
+ background-color: var(--bg-color);
121
+
122
+ .tool-call-box.result & {
123
+ background-color: var(--bg-color);
124
+ border-top: 1px dashed var(--border-color);
125
+ }
126
+
127
+ .tool-content {
128
+ padding: 0;
129
+
130
+ // Override code block wrapper styles
131
+ .code-block-wrapper {
132
+ border: none;
133
+ border-radius: 0;
134
+ }
135
+ }
136
+ }
137
+ }
@@ -0,0 +1,55 @@
1
+ import './ToolCallBox.scss'
2
+ import React, { useState } from 'react'
3
+
4
+ export function ToolCallBox({
5
+ header,
6
+ content,
7
+ isError = false,
8
+ type = 'call',
9
+ defaultExpanded = false,
10
+ collapsible = true,
11
+ onDoubleClick
12
+ }: {
13
+ header: React.ReactNode
14
+ content: React.ReactNode
15
+ isError?: boolean
16
+ type?: 'call' | 'result'
17
+ defaultExpanded?: boolean
18
+ collapsible?: boolean
19
+ onDoubleClick?: (e: React.MouseEvent) => void
20
+ }) {
21
+ const [expanded, setExpanded] = useState(defaultExpanded || !collapsible)
22
+
23
+ const isExpanded = !collapsible || expanded
24
+
25
+ return (
26
+ <div
27
+ className={`tool-call-box ${type === 'result' ? 'result' : ''} ${isExpanded ? 'expanded' : 'collapsed'}`}
28
+ onDoubleClick={(e) => {
29
+ if (onDoubleClick) {
30
+ e.stopPropagation()
31
+ onDoubleClick(e)
32
+ }
33
+ }}
34
+ >
35
+ <div
36
+ className={`tool-call-header ${type === 'result' && isError ? 'error' : ''}`}
37
+ onClick={() => collapsible && setExpanded(!expanded)}
38
+ >
39
+ <div className='tool-call-header-main'>
40
+ {header}
41
+ </div>
42
+ {collapsible && (
43
+ <span className='material-symbols-rounded expand-icon'>
44
+ {isExpanded ? 'expand_less' : 'expand_more'}
45
+ </span>
46
+ )}
47
+ </div>
48
+ {isExpanded && (
49
+ <div className='tool-call-body'>
50
+ {content}
51
+ </div>
52
+ )}
53
+ </div>
54
+ )
55
+ }
@@ -0,0 +1,154 @@
1
+ .tool-group-container {
2
+ max-width: 100%;
3
+
4
+ .tool-group-wrapper {
5
+ margin: 0;
6
+
7
+ &.single-item {
8
+ // No special style for single item
9
+ }
10
+
11
+ &.card-style {
12
+ border: 1px solid var(--border-color);
13
+ border-radius: 8px;
14
+ background-color: var(--bg-color);
15
+ overflow: hidden;
16
+
17
+ .tool-group-header {
18
+ display: flex;
19
+ align-items: center;
20
+ justify-content: space-between;
21
+ padding: 8px 12px;
22
+ background-color: var(--tag-bg, #f9fafb);
23
+ border-bottom: 1px solid transparent;
24
+ cursor: pointer;
25
+ user-select: none;
26
+ transition: background-color .2s, text-shadow .4s ease;
27
+
28
+ .header-left {
29
+ display: flex;
30
+ align-items: center;
31
+ gap: 8px;
32
+ font-size: 13px;
33
+ font-weight: 500;
34
+ color: var(--text-color);
35
+
36
+ .material-symbols-rounded {
37
+ font-size: 18px;
38
+ color: var(--sub-text-color);
39
+ }
40
+ }
41
+
42
+ .expand-icon {
43
+ font-size: 20px;
44
+ color: var(--sub-text-color);
45
+ }
46
+
47
+ &:hover {
48
+ opacity: .8;
49
+ }
50
+ }
51
+
52
+ .tool-group-list {
53
+ display: flex;
54
+ flex-direction: column;
55
+ gap: 0;
56
+ padding: 0;
57
+ border-top: 1px solid var(--border-color);
58
+ background-color: var(--bg-color);
59
+ }
60
+
61
+ .tool-group-last-item {
62
+ background-color: var(--bg-color);
63
+ }
64
+
65
+ // Shared style reset for all tool groups inside the card
66
+ .tool-group-list, .tool-group-last-item {
67
+ .tool-group {
68
+ margin: 0;
69
+ border: none;
70
+ border-radius: 0;
71
+ background-color: transparent;
72
+ overflow: visible; // Allow timeline line to overflow
73
+
74
+ .tool-call-box {
75
+ margin: 0;
76
+ border: none;
77
+ border-radius: 0;
78
+
79
+ .tool-call-header {
80
+ padding: 8px 12px;
81
+ background-color: transparent;
82
+
83
+ &:hover {
84
+ text-shadow: 0 0 .5px currentColor;
85
+ }
86
+ }
87
+
88
+ .tool-call-body {
89
+ padding: 0;
90
+ background-color: transparent;
91
+ }
92
+ }
93
+ }
94
+ }
95
+
96
+ // Timeline gap style
97
+ .tool-group-list .tool-group {
98
+ position: relative;
99
+ margin-bottom: 12px; // Increased gap between items
100
+ border-bottom: none; // Remove border as we use gap + line
101
+
102
+ &::after {
103
+ content: '';
104
+ position: absolute;
105
+ left: 19px;
106
+ bottom: -11px; // Adjusted for larger margin
107
+ height: 10px; // Increased length for larger gap
108
+ width: 2px;
109
+ background-color: var(--sub-text-color);
110
+ z-index: 99;
111
+ border-radius: 4px;
112
+ opacity: .5;
113
+ }
114
+ }
115
+ }
116
+ }
117
+
118
+ .tool-group-footer-wrapper {
119
+ opacity: 0;
120
+ transition: opacity .2s ease;
121
+ margin-top: 4px;
122
+ padding-left: 2px;
123
+
124
+ .msg-footer {
125
+ display: flex;
126
+ align-items: center;
127
+ gap: 8px;
128
+ font-size: 11px;
129
+ color: var(--sub-text-color, #9ca3af);
130
+
131
+ .timestamp {
132
+ user-select: none;
133
+ cursor: pointer;
134
+ }
135
+ }
136
+ }
137
+
138
+ &:hover .tool-group-footer-wrapper {
139
+ opacity: 1;
140
+ }
141
+ }
142
+
143
+ html.dark .tool-group-container .tool-group-wrapper.card-style {
144
+ background-color: #1f2937; // Darker bg for card
145
+ .tool-group-header {
146
+ background-color: #374151;
147
+ &:hover {
148
+ text-shadow: 0 0 .5px currentColor;
149
+ }
150
+ }
151
+ .tool-group-list, .tool-group-last-item {
152
+ background-color: #1f2937;
153
+ }
154
+ }
@@ -0,0 +1,102 @@
1
+ import type { ChatMessage, ChatMessageContent } from '@vibe-forge/core'
2
+ import React, { useState } from 'react'
3
+ import { useTranslation } from 'react-i18next'
4
+ import { MessageFooter } from './MessageFooter'
5
+ import './ToolGroup.scss'
6
+ import { ToolRenderer } from './ToolRenderer'
7
+
8
+ export function ToolGroup({
9
+ items,
10
+ footer
11
+ }: {
12
+ items: {
13
+ item: Extract<ChatMessageContent, { type: 'tool_use' }>
14
+ resultItem?: Extract<ChatMessageContent, { type: 'tool_result' }>
15
+ }[]
16
+ footer?: {
17
+ model?: string
18
+ usage?: ChatMessage['usage']
19
+ createdAt: number
20
+ originalMessage: ChatMessage
21
+ }
22
+ }) {
23
+ const { t } = useTranslation()
24
+ const [expanded, setExpanded] = useState(false)
25
+
26
+ if (items.length === 0) return null
27
+
28
+ // If only one item, just render it directly (wrapped in container for footer)
29
+ if (items.length === 1) {
30
+ return (
31
+ <div className='tool-group-container'>
32
+ <div className='tool-group-wrapper single-item'>
33
+ <ToolRenderer
34
+ item={items[0].item}
35
+ resultItem={items[0].resultItem}
36
+ />
37
+ </div>
38
+ {footer && (
39
+ <div className='tool-group-footer-wrapper'>
40
+ <MessageFooter msg={footer.originalMessage} isUser={false} />
41
+ </div>
42
+ )}
43
+ </div>
44
+ )
45
+ }
46
+
47
+ const lastItem = items[items.length - 1]
48
+ const otherItems = items.slice(0, -1)
49
+
50
+ return (
51
+ <div className='tool-group-container'>
52
+ <div className='tool-group-wrapper card-style'>
53
+ <div
54
+ className='tool-group-header'
55
+ onClick={() => setExpanded(!expanded)}
56
+ >
57
+ <div className='header-left'>
58
+ <span className='material-symbols-rounded'>dataset</span>
59
+ <span>{t('chat.usedTools', { count: items.length })}</span>
60
+ </div>
61
+ <span className='material-symbols-rounded expand-icon'>
62
+ {expanded ? 'expand_less' : 'expand_more'}
63
+ </span>
64
+ </div>
65
+
66
+ {expanded && (
67
+ <div className='tool-group-list'>
68
+ {otherItems.map((it, idx) => (
69
+ <ToolRenderer
70
+ key={it.item.id || idx}
71
+ item={it.item}
72
+ resultItem={it.resultItem}
73
+ />
74
+ ))}
75
+ </div>
76
+ )}
77
+
78
+ {
79
+ /* Always show the last item, but if expanded, it's just part of the list visually.
80
+ If collapsed, it appears "below" the header.
81
+ Actually, to make it look like "part of the list", we should just put it in the flow.
82
+
83
+ When collapsed: Header + Last Item
84
+ When expanded: Header + Other Items + Last Item
85
+ */
86
+ }
87
+ <div className='tool-group-last-item'>
88
+ <ToolRenderer
89
+ item={lastItem.item}
90
+ resultItem={lastItem.resultItem}
91
+ />
92
+ </div>
93
+ </div>
94
+
95
+ {footer && (
96
+ <div className='tool-group-footer-wrapper'>
97
+ <MessageFooter msg={footer.originalMessage} isUser={false} />
98
+ </div>
99
+ )}
100
+ </div>
101
+ )
102
+ }
@@ -0,0 +1,45 @@
1
+ import React from 'react'
2
+
3
+ import type { ChatMessageContent } from '@vibe-forge/core'
4
+
5
+ import { DefaultTool } from './tools/DefaultTool'
6
+ import { BashTool, adapterClaudeToolRenders } from './tools/adapter-claude'
7
+ import { taskToolRenders } from './tools/task'
8
+
9
+ const TOOL_RENDERERS: Record<
10
+ string,
11
+ React.ComponentType<{
12
+ item: Extract<ChatMessageContent, { type: 'tool_use' }>
13
+ resultItem?: Extract<ChatMessageContent, { type: 'tool_result' }>
14
+ }>
15
+ > = {
16
+ ...taskToolRenders,
17
+ ...adapterClaudeToolRenders
18
+ }
19
+
20
+ export function ToolRenderer({
21
+ item,
22
+ resultItem
23
+ }: {
24
+ item: Extract<ChatMessageContent, { type: 'tool_use' }>
25
+ resultItem?: Extract<ChatMessageContent, { type: 'tool_result' }>
26
+ }) {
27
+ const toolName = item.name
28
+ const foundRenderer = TOOL_RENDERERS[toolName] ?? ToolRenderer.findRendererByInput(item)
29
+ const Renderer = foundRenderer ?? DefaultTool
30
+ return <Renderer item={item} resultItem={resultItem} />
31
+ }
32
+
33
+ ToolRenderer.findRendererByInput = (item: Extract<ChatMessageContent, { type: 'tool_use' }>) => {
34
+ // If input has 'command' and 'description', it's likely a bash tool even if name is different
35
+ const input = item.input as Record<string, unknown> | null
36
+ if (input != null && typeof input === 'object') {
37
+ if (
38
+ 'command' in input &&
39
+ (('description' in input && input.description != null) || ('reason' in input && input.reason != null))
40
+ ) {
41
+ return BashTool
42
+ }
43
+ }
44
+ return null
45
+ }