@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,265 @@
1
+ import './SessionItem.scss'
2
+
3
+ import type { Session, SessionStatus } from '@vibe-forge/core'
4
+ import { Badge, Button, Checkbox, List, Tag, Tooltip } from 'antd'
5
+ import dayjs from 'dayjs'
6
+ import relativeTime from 'dayjs/plugin/relativeTime'
7
+ import React, { useMemo } from 'react'
8
+ import { useTranslation } from 'react-i18next'
9
+ import 'dayjs/locale/zh-cn'
10
+
11
+ dayjs.extend(relativeTime)
12
+
13
+ interface SessionItemProps {
14
+ session: Session
15
+ isActive: boolean
16
+ isBatchMode: boolean
17
+ isSelected: boolean
18
+ onSelect: (session: Session) => void
19
+ onArchive: (id: string) => void | Promise<void>
20
+ onDelete: (id: string) => void | Promise<void>
21
+ onStar: (id: string, isStarred: boolean) => void | Promise<void>
22
+ onUpdateTags: (id: string, tags: string[]) => void | Promise<void>
23
+ onToggleSelect: (id: string) => void
24
+ }
25
+
26
+ export function SessionItem({
27
+ session,
28
+ isActive,
29
+ isBatchMode,
30
+ isSelected,
31
+ onSelect,
32
+ onArchive,
33
+ onDelete,
34
+ onStar,
35
+ onUpdateTags,
36
+ onToggleSelect
37
+ }: SessionItemProps) {
38
+ const { t, i18n } = useTranslation()
39
+ const [isAddingTag, setIsAddingTag] = React.useState(false)
40
+ const [newTag, setNewTag] = React.useState('')
41
+ const automationPrefix = 'automation:'
42
+
43
+ const timeDisplay = useMemo(() => {
44
+ const d = dayjs(session.createdAt)
45
+ if (i18n.language === 'zh') {
46
+ d.locale('zh-cn')
47
+ } else {
48
+ d.locale('en')
49
+ }
50
+ return {
51
+ relative: d.fromNow(),
52
+ full: d.format('YYYY-MM-DD HH:mm:ss')
53
+ }
54
+ }, [session.createdAt, i18n.language])
55
+
56
+ const displayTitle = (session.title != null && session.title !== '')
57
+ ? session.title
58
+ : (session.lastUserMessage != null && session.lastUserMessage !== '')
59
+ ? session.lastUserMessage
60
+ : t('common.newChat')
61
+ const messageCount = session.messageCount ?? 0
62
+
63
+ const lastMessageSnippet = useMemo(() => {
64
+ if (session.lastMessage == null || session.lastMessage === '') return null
65
+ return session.lastMessage.length > 60 ? `${session.lastMessage.slice(0, 60)}...` : session.lastMessage
66
+ }, [session.lastMessage])
67
+
68
+ const parseAutomationTag = useMemo(() => {
69
+ return (tag: string) => {
70
+ if (!tag.startsWith(automationPrefix)) return null
71
+ const parts = tag.split(':')
72
+ if (parts.length < 3) return null
73
+ return {
74
+ ruleId: parts[1],
75
+ ruleTitle: parts.slice(2).join(':')
76
+ }
77
+ }
78
+ }, [automationPrefix])
79
+
80
+ const getStatusIcon = (status?: SessionStatus) => {
81
+ if (!status) {
82
+ return <div className={`status-dot ${isActive ? 'active' : ''}`} />
83
+ }
84
+
85
+ let icon = ''
86
+ let color = ''
87
+ const title = t(`common.status.${status}`)
88
+
89
+ switch (status) {
90
+ case 'completed':
91
+ icon = 'check_circle'
92
+ color = '#52c41a' // Green
93
+ break
94
+ case 'terminated':
95
+ icon = 'remove_circle'
96
+ color = '#bfbfbf' // Grey
97
+ break
98
+ case 'failed':
99
+ icon = 'cancel'
100
+ color = '#ff4d4f' // Red
101
+ break
102
+ case 'running':
103
+ icon = 'sync'
104
+ color = '#1890ff' // Blue
105
+ break
106
+ case 'waiting_input':
107
+ return (
108
+ <Tooltip title={title}>
109
+ <div className='waiting-input-indicator' />
110
+ </Tooltip>
111
+ )
112
+ default:
113
+ return <div className={`status-dot ${isActive ? 'active' : ''}`} />
114
+ }
115
+
116
+ return (
117
+ <Tooltip title={title}>
118
+ <span
119
+ className={`material-symbols-rounded status-icon ${status === 'running' ? 'spin' : ''}`}
120
+ style={{
121
+ color,
122
+ fontSize: '16px',
123
+ lineHeight: '16px',
124
+ display: 'flex',
125
+ alignItems: 'center',
126
+ justifyContent: 'center'
127
+ }}
128
+ >
129
+ {icon}
130
+ </span>
131
+ </Tooltip>
132
+ )
133
+ }
134
+
135
+ return (
136
+ <List.Item
137
+ onClick={() => isBatchMode ? onToggleSelect(session.id) : onSelect(session)}
138
+ onDoubleClick={() => {
139
+ // eslint-disable-next-line no-console
140
+ console.log('Session Details:', session)
141
+ }}
142
+ className={`session-item ${isActive ? 'active' : ''} ${isSelected ? 'selected' : ''} ${
143
+ session.isStarred ? 'starred' : ''
144
+ }`}
145
+ >
146
+ <div className='session-item-content'>
147
+ {!isBatchMode && (
148
+ <div className='status-indicator'>
149
+ {getStatusIcon(session.status)}
150
+ </div>
151
+ )}
152
+ {isBatchMode && (
153
+ <div className='batch-checkbox-wrapper'>
154
+ <Checkbox
155
+ checked={isSelected}
156
+ onChange={() => onToggleSelect(session.id)}
157
+ onClick={(e) => e.stopPropagation()}
158
+ />
159
+ </div>
160
+ )}
161
+ <div className={`session-info ${isBatchMode ? '' : 'with-actions'}`}>
162
+ <div className='session-header'>
163
+ <div className='session-title'>
164
+ <span className='session-title-text'>
165
+ {displayTitle}
166
+ </span>
167
+ </div>
168
+ {!isBatchMode && (
169
+ <div className='session-item-actions'>
170
+ <Tooltip title={session.isStarred ? t('common.unstar') : t('common.star')}>
171
+ <Button
172
+ type='text'
173
+ size='small'
174
+ className={`action-btn ${session.isStarred ? 'starred' : ''}`}
175
+ onClick={(e) => {
176
+ e.stopPropagation()
177
+ void onStar(session.id, !session.isStarred)
178
+ }}
179
+ icon={
180
+ <span
181
+ className={`material-symbols-rounded ${session.isStarred ? 'filled' : ''}`}
182
+ >
183
+ star
184
+ </span>
185
+ }
186
+ />
187
+ </Tooltip>
188
+ <Tooltip title={t('common.archive')}>
189
+ <Button
190
+ type='text'
191
+ size='small'
192
+ className='action-btn archive-btn'
193
+ onClick={(e) => {
194
+ e.stopPropagation()
195
+ void onArchive(session.id)
196
+ }}
197
+ icon={<span className='material-symbols-rounded'>archive</span>}
198
+ />
199
+ </Tooltip>
200
+ </div>
201
+ )}
202
+ </div>
203
+ {lastMessageSnippet != null && (
204
+ <div className='last-message'>
205
+ {lastMessageSnippet}
206
+ </div>
207
+ )}
208
+ <div className='session-meta'>
209
+ {session.status && (
210
+ <span
211
+ className='status-text'
212
+ style={{ fontSize: '11px', color: 'var(--sub-text-color)', marginRight: '8px' }}
213
+ >
214
+ {t(`common.status.${session.status}`)}
215
+ </span>
216
+ )}
217
+ <Tooltip title={timeDisplay.full}>
218
+ <span className='time-display'>
219
+ {timeDisplay.relative}
220
+ </span>
221
+ </Tooltip>
222
+ </div>
223
+ <div className='tags-container'>
224
+ {session.tags?.map(tag => {
225
+ const automationTag = parseAutomationTag(tag)
226
+ if (automationTag) {
227
+ const href = `/automation?rule=${encodeURIComponent(automationTag.ruleId)}`
228
+ return (
229
+ <Tag
230
+ key={tag}
231
+ className='session-tag session-tag--automation'
232
+ onClick={(event) => event.stopPropagation()}
233
+ >
234
+ <a
235
+ className='session-tag__link'
236
+ href={href}
237
+ onClick={(event) => event.stopPropagation()}
238
+ >
239
+ {automationTag.ruleTitle}
240
+ </a>
241
+ </Tag>
242
+ )
243
+ }
244
+ return (
245
+ <Tag
246
+ key={tag}
247
+ className='session-tag'
248
+ >
249
+ {tag}
250
+ </Tag>
251
+ )
252
+ })}
253
+ </div>
254
+ </div>
255
+ </div>
256
+
257
+ {messageCount > 0 && (
258
+ <Badge
259
+ count={messageCount > 99 ? '99+' : messageCount}
260
+ className='session-item-badge'
261
+ />
262
+ )}
263
+ </List.Item>
264
+ )
265
+ }
@@ -0,0 +1,92 @@
1
+ .session-list-container {
2
+ flex: 1;
3
+ min-height: 0;
4
+ display: flex;
5
+ flex-direction: column;
6
+
7
+ .session-list-scroll {
8
+ flex: 1;
9
+ overflow-y: auto;
10
+
11
+ .session-list {
12
+ padding: 12px 16px;
13
+ display: flex;
14
+ flex-direction: column;
15
+ gap: 8px;
16
+
17
+ .ant-list-items {
18
+ display: flex;
19
+ flex-direction: column;
20
+ gap: 8px;
21
+ }
22
+
23
+ .session-row {
24
+ position: relative;
25
+
26
+ &.has-parent {
27
+ &::before {
28
+ content: '';
29
+ position: absolute;
30
+ top: -8px;
31
+ bottom: -8px;
32
+ left: calc((var(--session-depth, 0) - 1) * 22px + 8px);
33
+ width: 3px;
34
+ background-color: var(--primary-color);
35
+ border-radius: 2px;
36
+ }
37
+ }
38
+
39
+ .session-item {
40
+ margin-left: calc(var(--session-depth, 0) * 22px);
41
+ }
42
+
43
+ .session-collapse-row {
44
+ margin-top: 6px;
45
+ margin-left: calc(var(--session-depth, 0) * 22px);
46
+ display: flex;
47
+ align-items: center;
48
+ gap: 6px;
49
+ color: var(--sub-text-color);
50
+
51
+ .session-tree-toggle {
52
+ width: 20px;
53
+ height: 20px;
54
+ padding: 0;
55
+ display: flex;
56
+ align-items: center;
57
+ justify-content: center;
58
+ color: var(--sub-text-color);
59
+
60
+ .material-symbols-rounded {
61
+ font-size: 18px;
62
+ transition: transform .2s ease;
63
+ }
64
+
65
+ &.collapsed {
66
+ .material-symbols-rounded {
67
+ transform: rotate(-90deg);
68
+ }
69
+ }
70
+ }
71
+
72
+ .session-collapse-label {
73
+ font-size: 11px;
74
+ }
75
+
76
+ .session-collapse-summary {
77
+ font-size: 11px;
78
+ color: var(--sub-text-color);
79
+ opacity: .8;
80
+ }
81
+ }
82
+ }
83
+ }
84
+ }
85
+
86
+ .empty-text {
87
+ padding: 32px 16px;
88
+ text-align: center;
89
+ color: var(--sub-text-color);
90
+ font-size: 14px;
91
+ }
92
+ }
@@ -0,0 +1,166 @@
1
+ import './SessionList.scss'
2
+
3
+ import type { Session } from '@vibe-forge/core'
4
+ import { Button, List } from 'antd'
5
+ import React, { useMemo, useState } from 'react'
6
+ import { useTranslation } from 'react-i18next'
7
+ import { SessionItem } from './SessionItem'
8
+
9
+ interface SessionListProps {
10
+ sessions: Session[]
11
+ activeId?: string
12
+ isBatchMode: boolean
13
+ selectedIds: Set<string>
14
+ searchQuery?: string
15
+ onSelectSession: (session: Session) => void
16
+ onArchiveSession: (id: string) => void | Promise<void>
17
+ onDeleteSession: (id: string) => void | Promise<void>
18
+ onStarSession: (id: string, isStarred: boolean) => void | Promise<void>
19
+ onUpdateTags: (id: string, tags: string[]) => void | Promise<void>
20
+ onToggleSelect: (id: string) => void
21
+ }
22
+
23
+ export function SessionList({
24
+ sessions,
25
+ activeId,
26
+ isBatchMode,
27
+ selectedIds,
28
+ searchQuery,
29
+ onSelectSession,
30
+ onArchiveSession,
31
+ onDeleteSession,
32
+ onStarSession,
33
+ onUpdateTags,
34
+ onToggleSelect
35
+ }: SessionListProps) {
36
+ const { t } = useTranslation()
37
+ const [collapsedIds, setCollapsedIds] = useState<Set<string>>(new Set())
38
+
39
+ const { flattenedSessions, childSummaryMap } = useMemo(() => {
40
+ const sessionMap = new Map(sessions.map((s) => [s.id, s]))
41
+ const childrenMap = new Map<string, Session[]>()
42
+ for (const session of sessions) {
43
+ if (session.parentSessionId) {
44
+ const list = childrenMap.get(session.parentSessionId) ?? []
45
+ list.push(session)
46
+ childrenMap.set(session.parentSessionId, list)
47
+ }
48
+ }
49
+
50
+ const roots = sessions.filter((s) => {
51
+ if (!s.parentSessionId) return true
52
+ return !sessionMap.has(s.parentSessionId)
53
+ })
54
+
55
+ const childSummary = new Map<string, string>()
56
+ for (const [parentId, children] of childrenMap.entries()) {
57
+ const total = children.length
58
+ const statusCount = new Map<string, number>()
59
+ for (const child of children) {
60
+ const status = child.status ?? 'running'
61
+ statusCount.set(status, (statusCount.get(status) ?? 0) + 1)
62
+ }
63
+ const parts = [t('common.childSummary', { count: total })]
64
+ for (const [status, count] of statusCount.entries()) {
65
+ parts.push(t('common.childStatusItem', { count, status: t(`common.status.${status}`) }))
66
+ }
67
+ childSummary.set(parentId, parts.join(' · '))
68
+ }
69
+
70
+ const result: Array<{ session: Session; depth: number; hasChildren: boolean }> = []
71
+ const visited = new Set<string>()
72
+
73
+ const walk = (session: Session, depth: number) => {
74
+ if (visited.has(session.id)) return
75
+ visited.add(session.id)
76
+ const children = childrenMap.get(session.id) ?? []
77
+ result.push({
78
+ session,
79
+ depth,
80
+ hasChildren: children.length > 0
81
+ })
82
+ if (collapsedIds.has(session.id)) return
83
+ for (const child of children) {
84
+ walk(child, depth + 1)
85
+ }
86
+ }
87
+
88
+ for (const root of roots) {
89
+ walk(root, 0)
90
+ }
91
+
92
+ return {
93
+ flattenedSessions: result,
94
+ childSummaryMap: childSummary
95
+ }
96
+ }, [sessions, collapsedIds, t])
97
+
98
+ const toggleCollapse = (id: string) => {
99
+ setCollapsedIds((prev) => {
100
+ const next = new Set(prev)
101
+ if (next.has(id)) {
102
+ next.delete(id)
103
+ } else {
104
+ next.add(id)
105
+ }
106
+ return next
107
+ })
108
+ }
109
+
110
+ return (
111
+ <div className='session-list-container'>
112
+ <div className='session-list-scroll'>
113
+ <List
114
+ className='session-list'
115
+ size='small'
116
+ locale={{
117
+ emptyText: <div className='empty-text'>
118
+ {searchQuery ? t('common.noSessions') : t('common.startNewChat')}
119
+ </div>
120
+ }}
121
+ dataSource={flattenedSessions}
122
+ renderItem={({ session: s, depth, hasChildren }) => (
123
+ <div
124
+ className={`session-row ${depth > 0 ? 'has-parent' : ''}`}
125
+ style={{ '--session-depth': depth } as React.CSSProperties}
126
+ >
127
+ <SessionItem
128
+ session={s}
129
+ isActive={activeId === s.id}
130
+ isBatchMode={isBatchMode}
131
+ isSelected={selectedIds.has(s.id)}
132
+ onSelect={onSelectSession}
133
+ onArchive={onArchiveSession}
134
+ onDelete={onDeleteSession}
135
+ onStar={onStarSession}
136
+ onUpdateTags={onUpdateTags}
137
+ onToggleSelect={onToggleSelect}
138
+ />
139
+ {hasChildren && !isBatchMode && (
140
+ <div
141
+ className='session-collapse-row'
142
+ style={{ '--session-depth': depth } as React.CSSProperties}
143
+ onClick={(e) => e.stopPropagation()}
144
+ >
145
+ <Button
146
+ type='text'
147
+ size='small'
148
+ className={`session-tree-toggle ${collapsedIds.has(s.id) ? 'collapsed' : ''}`}
149
+ onClick={() => toggleCollapse(s.id)}
150
+ icon={<span className='material-symbols-rounded'>chevron_right</span>}
151
+ />
152
+ <span className='session-collapse-label'>
153
+ {collapsedIds.has(s.id) ? t('common.expandChildren') : t('common.collapseChildren')}
154
+ </span>
155
+ <span className='session-collapse-summary'>
156
+ {childSummaryMap.get(s.id) ?? t('common.childSummary', { count: 0 })}
157
+ </span>
158
+ </div>
159
+ )}
160
+ </div>
161
+ )}
162
+ />
163
+ </div>
164
+ </div>
165
+ )
166
+ }
@@ -0,0 +1,79 @@
1
+ .sidebar-header {
2
+ padding: 12px 16px;
3
+ display: flex;
4
+ flex-direction: column;
5
+ gap: 12px;
6
+ flex-shrink: 0;
7
+
8
+ .header-bottom {
9
+ display: flex;
10
+ gap: 8px;
11
+ align-items: center;
12
+
13
+ .batch-select-wrapper {
14
+ width: 32px;
15
+ height: 32px;
16
+ display: flex;
17
+ align-items: center;
18
+ justify-content: center;
19
+ flex-shrink: 0;
20
+ }
21
+
22
+ .search-input {
23
+ border-radius: 6px;
24
+ flex: 1;
25
+ height: 36px;
26
+
27
+ .search-icon {
28
+ font-size: 18px;
29
+ color: var(--sub-text-color);
30
+ display: inline-flex;
31
+ align-items: center;
32
+ line-height: 1;
33
+ }
34
+ }
35
+
36
+ .batch-actions {
37
+ display: flex;
38
+ gap: 8px;
39
+ align-items: center;
40
+
41
+ .action-btn {
42
+ display: flex;
43
+ align-items: center;
44
+ justify-content: center;
45
+ width: 32px;
46
+ height: 32px;
47
+ padding: 0;
48
+ flex-shrink: 0;
49
+ color: var(--sub-text-color);
50
+
51
+ &.active {
52
+ color: var(--primary-color);
53
+ }
54
+
55
+ .material-symbols-rounded {
56
+ font-size: 20px;
57
+ display: inline-flex;
58
+ align-items: center;
59
+ line-height: 1;
60
+ }
61
+ }
62
+ }
63
+
64
+ .sidebar-collapse-btn, .sidebar-new-chat-btn {
65
+ display: flex;
66
+ align-items: center;
67
+ justify-content: center;
68
+ width: 32px;
69
+ height: 32px;
70
+ padding: 0;
71
+ color: var(--sub-text-color);
72
+ flex-shrink: 0;
73
+
74
+ .material-symbols-rounded {
75
+ font-size: 20px;
76
+ }
77
+ }
78
+ }
79
+ }