@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,518 @@
1
+ import type { Task, TimelineDiagram, TimelineEvent, TimelineEventType, TimelineInteraction, TimelineInteractionPayload } from './types'
2
+ import { parseTime, sanitizeId } from './utils'
3
+
4
+ interface MermaidLabels {
5
+ mainStart: string
6
+ mainEnd: string
7
+ startTasks: string
8
+ allTasksDone: string
9
+ askUserQuestion: string
10
+ edit: string
11
+ resumeTask: string
12
+ userPrompt: string
13
+ userAnswer: string
14
+ receiveReply: string
15
+ taskStart: string
16
+ taskEnd: string
17
+ ganttTitle: string
18
+ ganttMainSection: string
19
+ ganttTasksSection: string
20
+ }
21
+
22
+ interface MermaidCommitTarget {
23
+ branchName?: string
24
+ priority?: number
25
+ }
26
+
27
+ type TimelineOp =
28
+ | {
29
+ kind: 'commit'
30
+ time: string
31
+ branch: string
32
+ commitId: string
33
+ action?: string
34
+ priority: number
35
+ seq: number
36
+ }
37
+ | {
38
+ kind: 'branch'
39
+ time: string
40
+ baseBranch: string
41
+ name: string
42
+ priority: number
43
+ seq: number
44
+ }
45
+ | {
46
+ kind: 'merge'
47
+ time: string
48
+ target: string
49
+ source: string
50
+ mergeId?: string
51
+ action?: string
52
+ priority: number
53
+ seq: number
54
+ }
55
+
56
+ interface MermaidLatestCommit {
57
+ id: string
58
+ branch: string
59
+ time: string
60
+ seq: number
61
+ }
62
+
63
+ interface MermaidRuntime {
64
+ curBranch: string | undefined
65
+ labels: MermaidLabels
66
+ lines: string[]
67
+ ops: TimelineOp[]
68
+ interactions: TimelineInteraction[]
69
+ commit: (commitId: string, action?: string) => string
70
+ branch: (name: string) => void
71
+ checkout: (name: string) => void
72
+ merge: (
73
+ name: string,
74
+ id?: string,
75
+ action?: string,
76
+ options?: {
77
+ branchName?: string
78
+ time?: string
79
+ seq?: number
80
+ }
81
+ ) => void
82
+ addCommit: (
83
+ timeId: string,
84
+ action?: string,
85
+ options?: MermaidCommitTarget,
86
+ payload?: TimelineInteractionPayload
87
+ ) => string
88
+ addBranch: (time: string, baseBranch: string, name: string, priority: number) => void
89
+ addMerge: (
90
+ time: string,
91
+ target: string,
92
+ source: string,
93
+ action: string,
94
+ priority: number,
95
+ payload?: TimelineInteractionPayload
96
+ ) => string
97
+ ensureBranch: (name: string) => void
98
+ normalizeCommitId: (value: string) => string
99
+ allocateCommitId: (baseId: string) => string
100
+ findLatestCommitId: (branchName?: string) => { id: string; branch: string } | undefined
101
+ resumeCommitIds: Map<string, string>
102
+ parentByBranch: Map<string, string>
103
+ }
104
+
105
+ function formatTag(action?: string) {
106
+ if (!action) return undefined
107
+ return action.replace(/"/g, "'")
108
+ }
109
+
110
+ function createCommitIdAllocator(usedCommitIds: Set<string>) {
111
+ const normalizeCommitId = (value: string) => value.replace(/\s+/g, '_')
112
+ const allocateCommitId = (baseId: string) => {
113
+ let finalId = baseId
114
+ let bump = 1
115
+ while (usedCommitIds.has(finalId)) {
116
+ finalId = `${baseId}_${bump}`
117
+ bump += 1
118
+ }
119
+ usedCommitIds.add(finalId)
120
+ return finalId
121
+ }
122
+ return {
123
+ normalizeCommitId,
124
+ allocateCommitId
125
+ }
126
+ }
127
+
128
+ function getEventTitle(labels: MermaidLabels, type: TimelineEventType) {
129
+ switch (type) {
130
+ case 'tool__StartTasks':
131
+ return labels.startTasks
132
+ case 'tool__AskUserQuestion':
133
+ return labels.askUserQuestion
134
+ case 'tool__Edit':
135
+ return labels.edit
136
+ case 'tool__ResumeTask':
137
+ return labels.resumeTask
138
+ case 'user__Prompt':
139
+ return labels.userPrompt
140
+ default:
141
+ return type
142
+ }
143
+ }
144
+
145
+ function getOpBranchName(op: TimelineOp) {
146
+ switch (op.kind) {
147
+ case 'merge':
148
+ return op.target
149
+ case 'branch':
150
+ return op.name
151
+ default:
152
+ return op.branch
153
+ }
154
+ }
155
+
156
+ function sortTimelineOps(ops: TimelineOp[]) {
157
+ return [...ops].sort((a, b) => {
158
+ const timeDiff = parseTime(a.time) - parseTime(b.time)
159
+ if (timeDiff !== 0) return timeDiff
160
+ if (a.priority !== b.priority) return a.priority - b.priority
161
+ const branchA = getOpBranchName(a)
162
+ const branchB = getOpBranchName(b)
163
+ if (branchA !== branchB) return branchA.localeCompare(branchB)
164
+ return a.seq - b.seq
165
+ })
166
+ }
167
+
168
+ function createMermaidRuntime(labels: MermaidLabels): MermaidRuntime {
169
+ const lines: string[] = ['gitGraph TB:']
170
+ const createdBranches = new Set<string>()
171
+ const usedCommitIds = new Set<string>()
172
+ const { normalizeCommitId, allocateCommitId } = createCommitIdAllocator(usedCommitIds)
173
+
174
+ const resumeCommitIds = new Map<string, string>()
175
+ const latestCommitIds = new Map<string, string>()
176
+ const commitBranchById = new Map<string, string>()
177
+ const parentByBranch = new Map<string, string>()
178
+ let globalLatestCommit: MermaidLatestCommit | undefined
179
+
180
+ let curBranch: string | undefined
181
+ let opSeq = 0
182
+ const ops: TimelineOp[] = []
183
+ const interactions: TimelineInteraction[] = []
184
+
185
+ const commit = (commitId: string, action?: string) => {
186
+ const safeAction = formatTag(action)
187
+ if (safeAction) {
188
+ lines.push(`commit id:"${commitId}" tag:"${safeAction}"`)
189
+ } else {
190
+ lines.push(`commit id:"${commitId}"`)
191
+ }
192
+ return commitId
193
+ }
194
+
195
+ const updateGlobalLatestCommit = (commitId: string, branchName: string, time: string, seq: number) => {
196
+ if (!globalLatestCommit) {
197
+ globalLatestCommit = {
198
+ id: commitId,
199
+ branch: branchName,
200
+ time,
201
+ seq
202
+ }
203
+ return
204
+ }
205
+ const timeDiff = parseTime(time) - parseTime(globalLatestCommit.time)
206
+ if (timeDiff > 0 || (timeDiff === 0 && seq > globalLatestCommit.seq)) {
207
+ globalLatestCommit = {
208
+ id: commitId,
209
+ branch: branchName,
210
+ time,
211
+ seq
212
+ }
213
+ }
214
+ }
215
+
216
+ const findLatestCommitIdByBranch = (branchName: string) => {
217
+ let cursor: string | undefined = branchName
218
+ while (cursor) {
219
+ const commitId = latestCommitIds.get(cursor)
220
+ if (commitId) {
221
+ return {
222
+ id: commitId,
223
+ branch: commitBranchById.get(commitId) ?? cursor
224
+ }
225
+ }
226
+ const parent = parentByBranch.get(cursor)
227
+ if (!parent || parent === cursor) break
228
+ cursor = parent
229
+ }
230
+ return undefined
231
+ }
232
+
233
+ const findLatestCommitId = (branchName?: string) => {
234
+ if (branchName) return findLatestCommitIdByBranch(branchName)
235
+ if (!globalLatestCommit) return undefined
236
+ return {
237
+ id: globalLatestCommit.id,
238
+ branch: globalLatestCommit.branch
239
+ }
240
+ }
241
+
242
+ const addCommit = (
243
+ timeId: string,
244
+ action?: string,
245
+ options?: MermaidCommitTarget,
246
+ payload?: TimelineInteractionPayload
247
+ ) => {
248
+ const baseId = normalizeCommitId(timeId)
249
+ const commitId = allocateCommitId(baseId)
250
+ if (options?.branchName && typeof options.priority === 'number') {
251
+ ops.push({
252
+ kind: 'commit',
253
+ time: timeId,
254
+ branch: options.branchName,
255
+ commitId,
256
+ action,
257
+ priority: options.priority,
258
+ seq: opSeq++
259
+ })
260
+ latestCommitIds.set(options.branchName, commitId)
261
+ commitBranchById.set(commitId, options.branchName)
262
+ updateGlobalLatestCommit(commitId, options.branchName, timeId, opSeq - 1)
263
+ if (payload) {
264
+ interactions.push({
265
+ id: commitId,
266
+ label: action ?? commitId,
267
+ payload
268
+ })
269
+ }
270
+ return commitId
271
+ }
272
+ if (payload) {
273
+ interactions.push({
274
+ id: commitId,
275
+ label: action ?? commitId,
276
+ payload
277
+ })
278
+ }
279
+ return commit(commitId, action)
280
+ }
281
+
282
+ const branch = (name: string) => {
283
+ if (createdBranches.has(name)) return
284
+ createdBranches.add(name)
285
+ curBranch = name
286
+ lines.push(`branch ${name}`)
287
+ }
288
+
289
+ const checkout = (name: string) => {
290
+ curBranch = name
291
+ lines.push(`checkout ${name}`)
292
+ }
293
+
294
+ const merge = (
295
+ name: string,
296
+ id?: string,
297
+ action?: string,
298
+ options?: {
299
+ branchName?: string
300
+ time?: string
301
+ seq?: number
302
+ }
303
+ ) => {
304
+ const items: string[] = []
305
+ if (id) {
306
+ items.push(`id:"${id}"`)
307
+ }
308
+ if (action) {
309
+ items.push(`tag:"${action}"`)
310
+ }
311
+ lines.push(`merge ${name} ${items.join(' ')}`)
312
+ if (id && options?.branchName && options.time && typeof options.seq === 'number') {
313
+ latestCommitIds.set(options.branchName, id)
314
+ commitBranchById.set(id, options.branchName)
315
+ updateGlobalLatestCommit(id, options.branchName, options.time, options.seq)
316
+ }
317
+ }
318
+
319
+ const addBranch = (time: string, baseBranch: string, name: string, priority: number) => {
320
+ ops.push({
321
+ kind: 'branch',
322
+ time,
323
+ baseBranch,
324
+ name,
325
+ priority,
326
+ seq: opSeq++
327
+ })
328
+ }
329
+
330
+ const addMerge = (
331
+ time: string,
332
+ target: string,
333
+ source: string,
334
+ action: string,
335
+ priority: number,
336
+ payload?: TimelineInteractionPayload
337
+ ) => {
338
+ const baseId = normalizeCommitId(time)
339
+ const mergeId = allocateCommitId(baseId)
340
+ ops.push({
341
+ kind: 'merge',
342
+ time,
343
+ target,
344
+ source,
345
+ action,
346
+ mergeId,
347
+ priority,
348
+ seq: opSeq++
349
+ })
350
+ if (payload) {
351
+ interactions.push({
352
+ id: mergeId,
353
+ label: action,
354
+ payload
355
+ })
356
+ }
357
+ return mergeId
358
+ }
359
+
360
+ const mainBranch = 'main'
361
+ const ensureBranch = (name: string) => {
362
+ if (name === mainBranch) return
363
+ branch(name)
364
+ }
365
+
366
+ return {
367
+ get curBranch() {
368
+ return curBranch
369
+ },
370
+ labels,
371
+ lines,
372
+ ops,
373
+ interactions,
374
+ commit,
375
+ branch,
376
+ checkout,
377
+ merge,
378
+ addCommit,
379
+ addBranch,
380
+ addMerge,
381
+ ensureBranch,
382
+ normalizeCommitId,
383
+ allocateCommitId,
384
+ findLatestCommitId,
385
+ resumeCommitIds,
386
+ parentByBranch
387
+ }
388
+ }
389
+
390
+ function walkTimelineEvents(
391
+ events: TimelineEvent[],
392
+ activeBranch: string,
393
+ parentBranch: string,
394
+ runtime: MermaidRuntime
395
+ ) {
396
+ const {
397
+ labels,
398
+ addCommit,
399
+ addBranch,
400
+ addMerge,
401
+ parentByBranch,
402
+ resumeCommitIds
403
+ } = runtime
404
+ for (const event of events) {
405
+ const { type, startTime, endTime } = event
406
+ switch (type) {
407
+ case 'tool__StartTasks': {
408
+ addCommit(startTime, labels.taskStart, { branchName: activeBranch, priority: 10 }, {
409
+ kind: 'event',
410
+ event
411
+ })
412
+ const { tasks = {} } = event
413
+ for (const [taskName, task] of Object.entries(tasks)) {
414
+ const taskBranch = sanitizeId(taskName)
415
+ parentByBranch.set(taskBranch, activeBranch)
416
+ addBranch(startTime, activeBranch, taskBranch, 15)
417
+ addCommit(task.startTime, labels.taskStart, { branchName: taskBranch, priority: 16 }, {
418
+ kind: 'task',
419
+ name: taskName,
420
+ task
421
+ })
422
+ const { events: taskEvents = [] } = task
423
+ if (taskEvents.length > 0) {
424
+ walkTimelineEvents(taskEvents, taskBranch, activeBranch, runtime)
425
+ }
426
+ addCommit(task.endTime, labels.taskEnd, { branchName: taskBranch, priority: 60 })
427
+ }
428
+ addCommit(endTime, labels.allTasksDone, { branchName: activeBranch, priority: 70 })
429
+ break
430
+ }
431
+ case 'tool__AskUserQuestion':
432
+ addCommit(startTime, labels.askUserQuestion, { branchName: activeBranch, priority: 20 }, {
433
+ kind: 'event',
434
+ event
435
+ })
436
+ addMerge(endTime, parentBranch, activeBranch, labels.userAnswer, 80, {
437
+ kind: 'event',
438
+ event
439
+ })
440
+ break
441
+ case 'tool__Edit':
442
+ addCommit(startTime, getEventTitle(labels, type), { branchName: activeBranch, priority: 30 }, {
443
+ kind: 'event',
444
+ event
445
+ })
446
+ break
447
+ case 'tool__ResumeTask': {
448
+ const commitId = addCommit(startTime, getEventTitle(labels, type), {
449
+ branchName: activeBranch,
450
+ priority: 40
451
+ }, {
452
+ kind: 'event',
453
+ event
454
+ })
455
+ resumeCommitIds.set(activeBranch, commitId)
456
+ break
457
+ }
458
+ case 'user__Prompt':
459
+ addMerge(startTime, activeBranch, '<prev_branch>', getEventTitle(labels, type), 50, {
460
+ kind: 'event',
461
+ event
462
+ })
463
+ break
464
+ default:
465
+ addCommit(endTime, getEventTitle(labels, type), { branchName: activeBranch, priority: 55 }, {
466
+ kind: 'event',
467
+ event
468
+ })
469
+ break
470
+ }
471
+ }
472
+ }
473
+
474
+ function applyTimelineOps(runtime: MermaidRuntime) {
475
+ const sortedOps = sortTimelineOps(runtime.ops)
476
+ for (const op of sortedOps) {
477
+ switch (op.kind) {
478
+ case 'commit':
479
+ runtime.ensureBranch(op.branch)
480
+ runtime.checkout(op.branch)
481
+ runtime.commit(op.commitId, op.action)
482
+ break
483
+ case 'branch':
484
+ runtime.checkout(op.baseBranch)
485
+ runtime.ensureBranch(op.name)
486
+ break
487
+ case 'merge': {
488
+ if (op.source === '<prev_branch>') {
489
+ op.source = runtime.curBranch!
490
+ }
491
+ runtime.ensureBranch(op.target)
492
+ runtime.ensureBranch(op.source)
493
+ runtime.checkout(op.target)
494
+ runtime.merge(op.source, op.mergeId, op.action, {
495
+ branchName: op.target,
496
+ time: op.time,
497
+ seq: op.seq
498
+ })
499
+ break
500
+ }
501
+ }
502
+ }
503
+ }
504
+
505
+ export function buildGitGraph(task: Task, labels: MermaidLabels): TimelineDiagram {
506
+ const runtime = createMermaidRuntime(labels)
507
+ const mainBranch = 'main'
508
+ runtime.checkout(mainBranch)
509
+ runtime.addCommit(task.startTime, labels.mainStart)
510
+ walkTimelineEvents(task.events ?? [], mainBranch, mainBranch, runtime)
511
+ applyTimelineOps(runtime)
512
+ runtime.checkout(mainBranch)
513
+ runtime.addCommit(task.endTime, labels.mainEnd)
514
+ return {
515
+ code: runtime.lines.join('\n'),
516
+ interactions: runtime.interactions
517
+ }
518
+ }
@@ -0,0 +1,28 @@
1
+ .session-timeline-panel {
2
+ display: flex;
3
+ align-content: center;
4
+ }
5
+
6
+ .session-timeline-mermaid__svg {
7
+ height: 100%;
8
+ white-space: nowrap;
9
+ overflow-x: auto;
10
+ user-select: none;
11
+ }
12
+
13
+ .session-timeline-mermaid__svg svg,
14
+ .session-timeline-mermaid__svg text,
15
+ .session-timeline-mermaid__svg tspan {
16
+ user-select: none;
17
+ pointer-events: none;
18
+ }
19
+
20
+ .session-timeline-mermaid__interactive {
21
+ cursor: pointer;
22
+ transition: filter 0.2s ease, opacity 0.2s ease;
23
+ }
24
+
25
+ .session-timeline-mermaid__interactive:hover {
26
+ filter: drop-shadow(0 0 6px var(--primary-color));
27
+ opacity: 0.9;
28
+ }
@@ -0,0 +1,121 @@
1
+ import './index.scss'
2
+
3
+ import React from 'react'
4
+ import { useTranslation } from 'react-i18next'
5
+
6
+ import { buildGantt, buildGitGraph } from './mermaid'
7
+ import type { Task } from './types'
8
+
9
+ export interface SessionTimelinePanelProps {
10
+ task: Task
11
+ viewMode: 'git' | 'gantt'
12
+ className?: string
13
+ style?: React.CSSProperties
14
+ }
15
+
16
+ export function SessionTimelinePanel(props: SessionTimelinePanelProps) {
17
+ const {
18
+ task,
19
+ viewMode,
20
+ className,
21
+ style
22
+ } = props
23
+ const containerRef = React.useRef<HTMLDivElement | null>(null)
24
+ const { t } = useTranslation()
25
+ const labels = React.useMemo(() => ({
26
+ mainStart: t('chat.timeline.mainStart'),
27
+ mainEnd: t('chat.timeline.mainEnd'),
28
+ startTasks: t('chat.timeline.startTasks'),
29
+ allTasksDone: t('chat.timeline.allTasksDone'),
30
+ askUserQuestion: t('chat.timeline.askUserQuestion'),
31
+ edit: t('chat.timeline.edit'),
32
+ resumeTask: t('chat.timeline.resumeTask'),
33
+ userPrompt: t('chat.timeline.userPrompt'),
34
+ userAnswer: t('chat.timeline.userAnswer'),
35
+ receiveReply: t('chat.timeline.receiveReply'),
36
+ taskStart: t('chat.timeline.taskStart'),
37
+ taskEnd: t('chat.timeline.taskEnd'),
38
+ ganttTitle: t('chat.timeline.ganttTitle'),
39
+ ganttMainSection: t('chat.timeline.ganttMainSection'),
40
+ ganttTasksSection: t('chat.timeline.ganttTasksSection')
41
+ }), [t])
42
+ const diagram = React.useMemo(() => {
43
+ if (viewMode === 'gantt') {
44
+ return buildGantt(task, labels)
45
+ }
46
+ return buildGitGraph(task, labels)
47
+ }, [labels, task, viewMode])
48
+ const diagramId = React.useId()
49
+ const safeDiagramId = React.useMemo(() => diagramId.replace(/\W/g, '_'), [diagramId])
50
+ const interactionsRef = React.useRef(new Map<string, { label: string; payload: unknown }>())
51
+ const mermaidCode = React.useMemo(() => diagram.code, [diagram.code])
52
+
53
+ React.useEffect(() => {
54
+ let cancelled = false
55
+ const render = async () => {
56
+ const mermaidModule = await import('mermaid')
57
+ const mermaid = mermaidModule.default
58
+ const fontFamily = getComputedStyle(document.body).fontFamily
59
+ mermaid.initialize({
60
+ startOnLoad: false,
61
+ securityLevel: 'loose',
62
+ themeVariables: {
63
+ fontFamily
64
+ },
65
+ gitGraph: {},
66
+ gantt: {
67
+ topAxis: true,
68
+ displayMode: 'compact',
69
+ gridLineStartPadding: 0,
70
+ leftPadding: 16,
71
+ rightPadding: 16
72
+ }
73
+ })
74
+ if (cancelled) return
75
+ const container = containerRef.current
76
+ if (!container) return
77
+ const { svg, bindFunctions } = await mermaid.render(safeDiagramId, mermaidCode)
78
+ if (cancelled) return
79
+ container.innerHTML = svg
80
+ const svgElement = container.querySelector('svg')
81
+ if (svgElement) {
82
+ svgElement.classList.add('session-timeline-mermaid__svg')
83
+ for (const interaction of diagram.interactions) {
84
+ const target = svgElement.querySelector<HTMLElement>(`#${CSS.escape(interaction.id)}`)
85
+ if (target) {
86
+ target.classList.add('session-timeline-mermaid__interactive')
87
+ target.addEventListener('click', () => {
88
+ const item = interactionsRef.current.get(interaction.id)
89
+ if (!item) return
90
+ console.warn(item.payload)
91
+ })
92
+ }
93
+ }
94
+ }
95
+ if (typeof bindFunctions === 'function') {
96
+ bindFunctions(container)
97
+ }
98
+ }
99
+ render()
100
+ return () => {
101
+ cancelled = true
102
+ }
103
+ }, [diagram.interactions, mermaidCode, safeDiagramId])
104
+
105
+ React.useEffect(() => {
106
+ interactionsRef.current = new Map(
107
+ diagram.interactions.map((interaction) => [
108
+ interaction.id,
109
+ { label: interaction.label, payload: interaction.payload }
110
+ ])
111
+ )
112
+ }, [diagram.interactions])
113
+
114
+ return (
115
+ <div
116
+ ref={containerRef}
117
+ className={`session-timeline-panel session-timeline-panel--${viewMode}${className ? ` ${className}` : ''}`}
118
+ style={style}
119
+ />
120
+ )
121
+ }
@@ -0,0 +1,4 @@
1
+ export * from './gantt'
2
+ export * from './git-graph'
3
+ export * from './types'
4
+ export * from './utils'