@vibe-forge/client 0.4.0 → 0.6.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 (142) hide show
  1. package/AGENTS.md +37 -0
  2. package/cli.cjs +1 -0
  3. package/dist/assets/{arc-DgIxeTMg.js → arc-CMAHd5G3.js} +1 -1
  4. package/dist/assets/{blockDiagram-c4efeb88-CEAob3X9.js → blockDiagram-c4efeb88-DKww-VCP.js} +1 -1
  5. package/dist/assets/{c4Diagram-c83219d4-DwIxpDKd.js → c4Diagram-c83219d4-DKrjVHyY.js} +1 -1
  6. package/dist/assets/channel-Bi4g8rj9.js +1 -0
  7. package/dist/assets/{classDiagram-beda092f-Cz1q8u_0.js → classDiagram-beda092f-BXx5rdo3.js} +1 -1
  8. package/dist/assets/{classDiagram-v2-2358418a-CImgTuwd.js → classDiagram-v2-2358418a-CnR3WLsr.js} +1 -1
  9. package/dist/assets/clone-DPrpP2ky.js +1 -0
  10. package/dist/assets/{createText-1719965b-C1_HJcCc.js → createText-1719965b-CmOsl1W7.js} +1 -1
  11. package/dist/assets/{edges-96097737-BU8qStzd.js → edges-96097737-CQeQgpjD.js} +1 -1
  12. package/dist/assets/{erDiagram-0228fc6a-DNA1Fz2L.js → erDiagram-0228fc6a-ZUNB-ucF.js} +1 -1
  13. package/dist/assets/{flowDb-c6c81e3f-DjiCStMN.js → flowDb-c6c81e3f-DuuKeSLX.js} +1 -1
  14. package/dist/assets/{flowDiagram-50d868cf-CSDi0-RD.js → flowDiagram-50d868cf-Bc6n85yR.js} +1 -1
  15. package/dist/assets/flowDiagram-v2-4f6560a1-BZqaeqoh.js +1 -0
  16. package/dist/assets/{flowchart-elk-definition-6af322e1-DrhIMas7.js → flowchart-elk-definition-6af322e1-cAG5afW9.js} +1 -1
  17. package/dist/assets/{ganttDiagram-a2739b55-CTZnUP5z.js → ganttDiagram-a2739b55-Dp6xhY5I.js} +1 -1
  18. package/dist/assets/{gitGraphDiagram-82fe8481-COOW7jTi.js → gitGraphDiagram-82fe8481-MlIIRBdG.js} +1 -1
  19. package/dist/assets/{graph-CIkpD4Kx.js → graph-D7Es8jZ-.js} +1 -1
  20. package/dist/assets/{index-5325376f-aVVRRTIu.js → index-5325376f-DC18ottv.js} +1 -1
  21. package/dist/assets/index-D37AbgPQ.js +545 -0
  22. package/dist/assets/{index-D1giUI7r.css → index-fcJ9v94I.css} +1 -1
  23. package/dist/assets/{infoDiagram-8eee0895-DQpZ1LVD.js → infoDiagram-8eee0895-CXk21kFp.js} +1 -1
  24. package/dist/assets/{journeyDiagram-c64418c1-DoKguIuk.js → journeyDiagram-c64418c1-899BKBHL.js} +1 -1
  25. package/dist/assets/{layout-Tnmha8Nh.js → layout-DLaxdy48.js} +1 -1
  26. package/dist/assets/{line-BQR2SOyl.js → line-_lw5YbRM.js} +1 -1
  27. package/dist/assets/{linear-DlG0eemV.js → linear-D5iu84ui.js} +1 -1
  28. package/dist/assets/{mermaid.core-BnwYO0He.js → mermaid.core-C6sW3GFM.js} +4 -4
  29. package/dist/assets/{mindmap-definition-8da855dc-BllYwDID.js → mindmap-definition-8da855dc-BS9Xy9KN.js} +1 -1
  30. package/dist/assets/{pieDiagram-a8764435-DwCkhPVc.js → pieDiagram-a8764435-DZt9cEgs.js} +1 -1
  31. package/dist/assets/{quadrantDiagram-1e28029f-c40GKTU0.js → quadrantDiagram-1e28029f-BTIeHOgn.js} +1 -1
  32. package/dist/assets/{requirementDiagram-08caed73-DnQp2Tk6.js → requirementDiagram-08caed73-BHJAKD2g.js} +1 -1
  33. package/dist/assets/{sankeyDiagram-a04cb91d-CnJrs13b.js → sankeyDiagram-a04cb91d-DnAkVOK8.js} +1 -1
  34. package/dist/assets/{sequenceDiagram-c5b8d532-1YBwnpKu.js → sequenceDiagram-c5b8d532-92tE3oFv.js} +1 -1
  35. package/dist/assets/{stateDiagram-1ecb1508-BFBxQ6Fh.js → stateDiagram-1ecb1508-DG0ObiMg.js} +1 -1
  36. package/dist/assets/{stateDiagram-v2-c2b004d7-Dmechvv2.js → stateDiagram-v2-c2b004d7-BKoJx2ci.js} +1 -1
  37. package/dist/assets/{styles-b4e223ce-DWWfWX8O.js → styles-b4e223ce-Ba6G4ri9.js} +1 -1
  38. package/dist/assets/{styles-ca3715f6-CKKvZxaU.js → styles-ca3715f6-Bn6RIIVW.js} +1 -1
  39. package/dist/assets/{styles-d45a18b0-dKMOUh9p.js → styles-d45a18b0-_dELBUI6.js} +1 -1
  40. package/dist/assets/{svgDrawCommon-b86b1483-CBgjChPM.js → svgDrawCommon-b86b1483-CRK-ZoIs.js} +1 -1
  41. package/dist/assets/{timeline-definition-faaaa080-NCt-HHmb.js → timeline-definition-faaaa080-DvQ_RA_i.js} +1 -1
  42. package/dist/assets/{xychartDiagram-f5964ef8-BJhXS4dG.js → xychartDiagram-f5964ef8-CJxeDLfg.js} +1 -1
  43. package/dist/index.html +2 -2
  44. package/package.json +10 -7
  45. package/src/App.tsx +20 -168
  46. package/src/api/base.ts +116 -7
  47. package/src/api/sessions.ts +3 -1
  48. package/src/api.ts +3 -1
  49. package/src/components/ArchiveView.tsx +5 -5
  50. package/src/components/ConfigView.tsx +28 -28
  51. package/src/components/{AutomationView → automation-view}/index.tsx +10 -9
  52. package/src/components/{BenchmarkView → benchmark-view}/index.tsx +5 -4
  53. package/src/components/chat/ChatHeader.tsx +6 -6
  54. package/src/components/chat/ChatHistoryView.tsx +74 -16
  55. package/src/components/chat/ChatTimelineView.tsx +3 -3
  56. package/src/components/chat/CurrentTodoList.scss +56 -27
  57. package/src/components/chat/{Sender → sender}/Sender.scss +278 -85
  58. package/src/components/chat/{Sender → sender}/Sender.tsx +125 -41
  59. package/src/components/chat/tools/core/ToolGroup.tsx +1 -1
  60. package/src/components/config/ConfigSectionForm.tsx +1 -1
  61. package/src/components/config/ConfigSourceSwitch.tsx +12 -34
  62. package/src/components/config/channelDefinitions.ts +2 -2
  63. package/src/components/layout/AppShell.scss +19 -0
  64. package/src/components/layout/AppShell.tsx +45 -0
  65. package/src/components/sidebar/SessionItem.scss +17 -0
  66. package/src/components/sidebar/SessionItem.tsx +21 -13
  67. package/src/hooks/chat/model-selector.ts +150 -0
  68. package/src/hooks/chat/use-chat-adapter.ts +93 -0
  69. package/src/hooks/chat/use-chat-models.tsx +126 -57
  70. package/src/hooks/chat/use-chat-permission-mode.ts +14 -10
  71. package/src/hooks/chat/use-chat-session-actions.ts +22 -13
  72. package/src/hooks/chat/use-chat-session-messages.ts +62 -10
  73. package/src/hooks/chat/use-chat-session.ts +49 -2
  74. package/src/hooks/use-app-preferences.ts +41 -0
  75. package/src/hooks/use-session-subscription.ts +101 -0
  76. package/src/hooks/use-sidebar-navigation.ts +35 -0
  77. package/src/resources/adapters.ts +20 -0
  78. package/src/resources/locales/en.json +6 -0
  79. package/src/resources/locales/zh.json +6 -0
  80. package/src/routes/AppRoutes.tsx +22 -0
  81. package/src/routes/ArchiveRoute.tsx +5 -0
  82. package/src/routes/AutomationRoute.tsx +5 -0
  83. package/src/routes/BenchmarkRoute.tsx +5 -0
  84. package/src/{components/Chat.scss → routes/ChatRoute.scss} +35 -0
  85. package/src/routes/ChatRoute.tsx +132 -0
  86. package/src/routes/ConfigRoute.tsx +5 -0
  87. package/src/routes/KnowledgeRoute.tsx +5 -0
  88. package/dist/assets/channel-DhtnrNJ6.js +0 -1
  89. package/dist/assets/clone-7bHB6YkC.js +0 -1
  90. package/dist/assets/flowDiagram-v2-4f6560a1-_13Sz5Wh.js +0 -1
  91. package/dist/assets/index-DRSI_ZIL.js +0 -514
  92. package/src/components/Chat.tsx +0 -100
  93. package/src/components/{AutomationView → automation-view}/RuleFormPanel.scss +0 -0
  94. package/src/components/{AutomationView → automation-view}/RuleFormPanel.tsx +0 -0
  95. package/src/components/{AutomationView → automation-view}/RuleSidebar.scss +0 -0
  96. package/src/components/{AutomationView → automation-view}/RuleSidebar.tsx +0 -0
  97. package/src/components/{AutomationView → automation-view}/RunHistoryPanel.scss +0 -0
  98. package/src/components/{AutomationView → automation-view}/RunHistoryPanel.tsx +0 -0
  99. package/src/components/{AutomationView → automation-view}/TaskList.scss +0 -0
  100. package/src/components/{AutomationView → automation-view}/TaskList.tsx +0 -0
  101. package/src/components/{AutomationView → automation-view}/TriggerList.scss +0 -0
  102. package/src/components/{AutomationView → automation-view}/TriggerList.tsx +0 -0
  103. package/src/components/{AutomationView/AutomationView.scss → automation-view/index.scss} +0 -0
  104. package/src/components/{AutomationView → automation-view}/types.ts +0 -0
  105. package/src/components/{BenchmarkView → benchmark-view}/BenchmarkCasePanel.scss +0 -0
  106. package/src/components/{BenchmarkView → benchmark-view}/BenchmarkCasePanel.tsx +0 -0
  107. package/src/components/{BenchmarkView → benchmark-view}/BenchmarkSidebar.scss +0 -0
  108. package/src/components/{BenchmarkView → benchmark-view}/BenchmarkSidebar.tsx +0 -0
  109. package/src/components/{BenchmarkView → benchmark-view}/BenchmarkView.scss +0 -0
  110. package/src/components/{BenchmarkView → benchmark-view}/types.ts +0 -0
  111. package/src/components/{BenchmarkView → benchmark-view}/utils.ts +0 -0
  112. package/src/components/chat/{Messages → messages}/MessageFooter.tsx +0 -0
  113. package/src/components/chat/{Messages → messages}/MessageItem.scss +0 -0
  114. package/src/components/chat/{Messages → messages}/MessageItem.tsx +0 -0
  115. package/src/components/chat/{Messages → messages}/message-utils.ts +0 -0
  116. package/src/components/chat/{Sender → sender}/CompletionMenu.scss +0 -0
  117. package/src/components/chat/{Sender → sender}/CompletionMenu.tsx +0 -0
  118. package/src/components/chat/{Sender → sender}/ThinkingStatus.scss +0 -0
  119. package/src/components/chat/{Sender → sender}/ThinkingStatus.tsx +0 -0
  120. package/src/components/chat/{SessionTimelinePanel → session-timeline-panel}/EventList.scss +0 -0
  121. package/src/components/chat/{SessionTimelinePanel → session-timeline-panel}/EventList.tsx +0 -0
  122. package/src/components/chat/{SessionTimelinePanel → session-timeline-panel}/gantt.ts +0 -0
  123. package/src/components/chat/{SessionTimelinePanel → session-timeline-panel}/git-graph.ts +0 -0
  124. package/src/components/chat/{SessionTimelinePanel → session-timeline-panel}/index.scss +0 -0
  125. package/src/components/chat/{SessionTimelinePanel → session-timeline-panel}/index.tsx +0 -0
  126. package/src/components/chat/{SessionTimelinePanel → session-timeline-panel}/mermaid.ts +0 -0
  127. package/src/components/chat/{SessionTimelinePanel → session-timeline-panel}/types.ts +0 -0
  128. package/src/components/chat/{SessionTimelinePanel → session-timeline-panel}/utils.ts +0 -0
  129. package/src/components/config/{recordEditors → record-editors}/BooleanRecordEditor.scss +0 -0
  130. package/src/components/config/{recordEditors → record-editors}/BooleanRecordEditor.tsx +0 -0
  131. package/src/components/config/{recordEditors → record-editors}/ChannelRecordEditor.scss +0 -0
  132. package/src/components/config/{recordEditors → record-editors}/ChannelRecordEditor.tsx +33 -33
  133. /package/src/components/config/{recordEditors → record-editors}/KeyValueEditor.scss +0 -0
  134. /package/src/components/config/{recordEditors → record-editors}/KeyValueEditor.tsx +0 -0
  135. /package/src/components/config/{recordEditors → record-editors}/McpServersRecordEditor.scss +0 -0
  136. /package/src/components/config/{recordEditors → record-editors}/McpServersRecordEditor.tsx +0 -0
  137. /package/src/components/config/{recordEditors → record-editors}/ModelServicesRecordEditor.scss +0 -0
  138. /package/src/components/config/{recordEditors → record-editors}/ModelServicesRecordEditor.tsx +0 -0
  139. /package/src/components/config/{recordEditors → record-editors}/RecordEditors.scss +0 -0
  140. /package/src/components/config/{recordEditors → record-editors}/RecordJsonEditor.scss +0 -0
  141. /package/src/components/config/{recordEditors → record-editors}/RecordJsonEditor.tsx +0 -0
  142. /package/src/components/config/{recordEditors → record-editors}/index.tsx +0 -0
@@ -1,10 +1,11 @@
1
1
  import { App } from 'antd'
2
- import { useEffect, useRef, useState } from 'react'
2
+ import { useCallback, useEffect, useRef, useState } from 'react'
3
+ import { useTranslation } from 'react-i18next'
3
4
  import { useSWRConfig } from 'swr'
4
5
 
5
- import type { AskUserQuestionParams, ChatMessage, Session, SessionInfo, WSEvent } from '@vibe-forge/core'
6
6
  import { getSessionMessages } from '#~/api.js'
7
7
  import { connectionManager } from '#~/connectionManager.js'
8
+ import type { AskUserQuestionParams, ChatMessage, Session, SessionInfo, WSEvent } from '@vibe-forge/core'
8
9
  import type { PermissionMode } from './use-chat-permission-mode'
9
10
 
10
11
  const applyMessageEvent = (currentMessages: ChatMessage[], data: WSEvent) => {
@@ -38,26 +39,41 @@ export function useChatSessionMessages({
38
39
  session,
39
40
  modelForQuery,
40
41
  permissionMode,
42
+ adapter,
41
43
  setInteractionRequest
42
44
  }: {
43
45
  session?: Session
44
46
  modelForQuery?: string
45
47
  permissionMode: PermissionMode
48
+ adapter?: string
46
49
  setInteractionRequest: (value: { id: string; payload: AskUserQuestionParams } | null) => void
47
50
  }) {
48
- const { message } = App.useApp()
51
+ const { t } = useTranslation()
49
52
  const { mutate } = useSWRConfig()
50
53
  const [messages, setMessages] = useState<ChatMessage[]>([])
51
54
  const [sessionInfo, setSessionInfo] = useState<SessionInfo | null>(null)
52
55
  const [isReady, setIsReady] = useState(false)
56
+ const [connectionError, setConnectionError] = useState<string | null>(null)
57
+ const [retryCount, setRetryCount] = useState(0)
53
58
  const isInitialLoadRef = useRef<boolean>(true)
54
59
  const lastConnectedModelRef = useRef<string | undefined>(undefined)
55
60
  const lastConnectedPermissionModeRef = useRef<string | undefined>(undefined)
61
+ const lastConnectedAdapterRef = useRef<string | undefined>(undefined)
62
+ const expectedCloseRef = useRef(false)
63
+
64
+ const retryConnection = useCallback(() => {
65
+ if (session?.id == null || session.id === '') return
66
+ expectedCloseRef.current = true
67
+ setConnectionError(null)
68
+ connectionManager.close(session.id)
69
+ setRetryCount((count) => count + 1)
70
+ }, [session?.id])
56
71
 
57
72
  useEffect(() => {
58
73
  setMessages([])
59
74
  setSessionInfo(null)
60
75
  setIsReady(false)
76
+ setConnectionError(null)
61
77
  setInteractionRequest(null)
62
78
  isInitialLoadRef.current = true
63
79
 
@@ -65,6 +81,7 @@ export function useChatSessionMessages({
65
81
  setIsReady(true)
66
82
  lastConnectedModelRef.current = undefined
67
83
  lastConnectedPermissionModeRef.current = undefined
84
+ lastConnectedAdapterRef.current = undefined
68
85
  return
69
86
  }
70
87
 
@@ -141,31 +158,44 @@ export function useChatSessionMessages({
141
158
  lastConnectedPermissionModeRef.current != null &&
142
159
  normalizedPermissionMode !== lastConnectedPermissionModeRef.current &&
143
160
  session?.status !== 'running'
144
- if (modelChanged || permissionModeChanged) {
161
+ const normalizedAdapter = adapter ?? ''
162
+ const adapterChanged = adapter != null &&
163
+ lastConnectedAdapterRef.current != null &&
164
+ normalizedAdapter !== lastConnectedAdapterRef.current &&
165
+ session?.status !== 'running'
166
+ if (modelChanged || permissionModeChanged || adapterChanged) {
167
+ expectedCloseRef.current = true
168
+ setConnectionError(null)
145
169
  connectionManager.send(session.id, { type: 'terminate_session' })
146
170
  connectionManager.close(session.id)
147
171
  }
148
172
  lastConnectedModelRef.current = normalizedModel
149
173
  lastConnectedPermissionModeRef.current = normalizedPermissionMode
174
+ lastConnectedAdapterRef.current = normalizedAdapter
150
175
 
151
176
  const timer = setTimeout(() => {
152
177
  if (isDisposed) return
153
178
 
154
179
  const connectionParams: Record<string, string> = {}
155
- if (modelForQuery) {
156
- connectionParams.model = modelForQuery
180
+ if (modelForQuery) {
181
+ connectionParams.model = modelForQuery
157
182
  }
158
183
  if (permissionMode) {
159
184
  connectionParams.permissionMode = permissionMode
160
185
  }
186
+ if (adapter) {
187
+ connectionParams.adapter = adapter
188
+ }
161
189
 
162
190
  cleanup = connectionManager.connect(session.id, {
163
191
  onOpen() {
192
+ expectedCloseRef.current = false
193
+ setConnectionError(null)
164
194
  },
165
195
  onMessage(data: WSEvent) {
166
196
  if (isDisposed) return
167
197
  if (data.type === 'error') {
168
- void message.error(data.message)
198
+ setConnectionError(data.message)
169
199
  return
170
200
  }
171
201
 
@@ -229,22 +259,44 @@ export function useChatSessionMessages({
229
259
  setInteractionRequest({ id: data.id, payload: data.payload })
230
260
  }
231
261
  },
262
+ onError() {
263
+ if (isDisposed) return
264
+ setConnectionError(t('chat.connectionError'))
265
+ },
232
266
  onClose() {
267
+ if (isDisposed) return
268
+ if (expectedCloseRef.current) {
269
+ expectedCloseRef.current = false
270
+ return
271
+ }
272
+ setConnectionError((current) => current ?? t('chat.connectionClosed'))
233
273
  }
234
274
  }, Object.keys(connectionParams).length > 0 ? connectionParams : undefined)
235
- }, modelChanged ? 200 : 100)
275
+ }, (modelChanged || permissionModeChanged || adapterChanged) ? 200 : 100)
236
276
 
237
277
  return () => {
238
278
  isDisposed = true
239
279
  clearTimeout(timer)
240
280
  cleanup?.()
241
281
  }
242
- }, [message, modelForQuery, mutate, permissionMode, session?.id, session?.status, setInteractionRequest])
282
+ }, [
283
+ adapter,
284
+ modelForQuery,
285
+ mutate,
286
+ permissionMode,
287
+ retryCount,
288
+ session?.id,
289
+ session?.status,
290
+ setInteractionRequest,
291
+ t
292
+ ])
243
293
 
244
294
  return {
245
295
  messages,
246
296
  setMessages,
247
297
  sessionInfo,
248
- isReady
298
+ isReady,
299
+ connectionError,
300
+ retryConnection
249
301
  }
250
302
  }
@@ -1,6 +1,8 @@
1
+ import { useEffect, useRef } from 'react'
1
2
  import { useTranslation } from 'react-i18next'
2
3
 
3
4
  import type { Session } from '@vibe-forge/core'
5
+ import { useChatAdapter } from './use-chat-adapter'
4
6
  import { useChatInteraction } from './use-chat-interaction'
5
7
  import { useChatModels } from './use-chat-models'
6
8
  import { useChatPermissionMode } from './use-chat-permission-mode'
@@ -13,31 +15,73 @@ export function useChatSession({
13
15
  session?: Session
14
16
  }) {
15
17
  const { t } = useTranslation()
18
+ const { selectedAdapter, setSelectedAdapter, adapterOptions } = useChatAdapter()
16
19
  const {
17
20
  selectedModel,
18
21
  selectedModelWithService,
19
22
  setSelectedModel,
20
23
  modelOptions,
21
24
  hasAvailableModels
22
- } = useChatModels()
25
+ } = useChatModels({ selectedAdapter })
23
26
  const { permissionMode, setPermissionMode, permissionModeOptions } = useChatPermissionMode()
24
27
  const { activeView, setActiveView } = useChatView()
25
28
  const { interactionRequest, setInteractionRequest, handleInteractionResponse } = useChatInteraction({
26
29
  sessionId: session?.id
27
30
  })
28
- const { messages, setMessages, sessionInfo, isReady } = useChatSessionMessages({
31
+ const { messages, setMessages, sessionInfo, isReady, connectionError, retryConnection } = useChatSessionMessages({
29
32
  session,
30
33
  modelForQuery: selectedModelWithService,
31
34
  permissionMode,
35
+ adapter: selectedAdapter,
32
36
  setInteractionRequest
33
37
  })
38
+ const lastObservedSessionRef = useRef<Pick<Session, 'id' | 'model' | 'permissionMode' | 'adapter'> | null>(null)
34
39
  const isThinking = session?.status === 'running'
35
40
 
41
+ useEffect(() => {
42
+ if (session?.id == null || session.id === '') {
43
+ lastObservedSessionRef.current = null
44
+ return
45
+ }
46
+
47
+ const previous = lastObservedSessionRef.current
48
+ const sessionChanged = previous?.id !== session.id
49
+
50
+ if (sessionChanged || previous?.model !== session.model) {
51
+ setSelectedModel(session.model)
52
+ }
53
+
54
+ if (sessionChanged || previous?.permissionMode !== session.permissionMode) {
55
+ setPermissionMode(session.permissionMode)
56
+ }
57
+
58
+ if (sessionChanged || previous?.adapter !== session.adapter) {
59
+ setSelectedAdapter(session.adapter)
60
+ }
61
+
62
+ lastObservedSessionRef.current = {
63
+ id: session.id,
64
+ model: session.model,
65
+ permissionMode: session.permissionMode,
66
+ adapter: session.adapter
67
+ }
68
+ }, [
69
+ session?.adapter,
70
+ session?.id,
71
+ session?.model,
72
+ session?.permissionMode,
73
+ setPermissionMode,
74
+ setSelectedAdapter,
75
+ setSelectedModel
76
+ ])
77
+
36
78
  return {
37
79
  messages,
38
80
  sessionInfo,
39
81
  interactionRequest,
40
82
  isReady,
83
+ connectionError,
84
+ retryConnection,
41
85
  isThinking,
42
86
  activeView,
43
87
  setActiveView,
@@ -51,6 +95,9 @@ export function useChatSession({
51
95
  permissionMode,
52
96
  setPermissionMode,
53
97
  permissionModeOptions,
98
+ selectedAdapter,
99
+ setSelectedAdapter,
100
+ adapterOptions,
54
101
  hasAvailableModels,
55
102
  modelUnavailable: !hasAvailableModels
56
103
  }
@@ -0,0 +1,41 @@
1
+ import { theme } from 'antd'
2
+ import { useAtomValue } from 'jotai'
3
+ import { useEffect, useMemo } from 'react'
4
+ import { useTranslation } from 'react-i18next'
5
+ import useSWR from 'swr'
6
+
7
+ import type { ConfigResponse } from '@vibe-forge/core'
8
+
9
+ import { getConfig } from '#~/api'
10
+ import { themeAtom } from '#~/store'
11
+
12
+ export function useAppPreferences() {
13
+ const { i18n } = useTranslation()
14
+ const themeMode = useAtomValue(themeAtom)
15
+ const { data: configRes } = useSWR<ConfigResponse>('/api/config', getConfig)
16
+ const interfaceLanguage = configRes?.sources?.merged?.general?.interfaceLanguage
17
+ const isDarkMode = themeMode === 'dark'
18
+ || (themeMode === 'system' && window.matchMedia('(prefers-color-scheme: dark)').matches)
19
+
20
+ useEffect(() => {
21
+ document.documentElement.classList.toggle('dark', isDarkMode)
22
+ }, [isDarkMode])
23
+
24
+ useEffect(() => {
25
+ if (interfaceLanguage && i18n.language !== interfaceLanguage) {
26
+ void i18n.changeLanguage(interfaceLanguage)
27
+ }
28
+ }, [i18n, interfaceLanguage])
29
+
30
+ const themeConfig = useMemo(() => ({
31
+ algorithm: isDarkMode ? theme.darkAlgorithm : theme.defaultAlgorithm,
32
+ token: {
33
+ colorPrimary: isDarkMode ? '#3b82f6' : '#000000'
34
+ }
35
+ }), [isDarkMode])
36
+
37
+ return {
38
+ isDarkMode,
39
+ themeConfig
40
+ }
41
+ }
@@ -0,0 +1,101 @@
1
+ import { useEffect } from 'react'
2
+ import { useSWRConfig } from 'swr'
3
+
4
+ import type { Session, WSEvent } from '@vibe-forge/core'
5
+
6
+ import { createSocket } from '#~/ws.js'
7
+
8
+ interface SessionListResponse {
9
+ sessions: Session[]
10
+ }
11
+ type SessionUpdate = Session | { id: string; isDeleted: boolean }
12
+
13
+ const sortSessions = (sessions: Session[]) => {
14
+ return [...sessions].sort((a, b) => {
15
+ const starredDelta = Number(b.isStarred === true) - Number(a.isStarred === true)
16
+ if (starredDelta !== 0) return starredDelta
17
+ return (b.createdAt ?? 0) - (a.createdAt ?? 0)
18
+ })
19
+ }
20
+
21
+ const mergeSessionList = (
22
+ prev: SessionListResponse | undefined,
23
+ updatedSession: SessionUpdate,
24
+ filter: 'active' | 'archived'
25
+ ) => {
26
+ if (prev?.sessions == null) return prev
27
+
28
+ if ('isDeleted' in updatedSession && updatedSession.isDeleted) {
29
+ return {
30
+ ...prev,
31
+ sessions: prev.sessions.filter((session) => session.id !== updatedSession.id)
32
+ }
33
+ }
34
+
35
+ const shouldInclude = filter === 'archived'
36
+ ? updatedSession.isArchived === true
37
+ : updatedSession.isArchived !== true
38
+ const existing = prev.sessions.find((session) => session.id === updatedSession.id)
39
+
40
+ if (!shouldInclude) {
41
+ return {
42
+ ...prev,
43
+ sessions: prev.sessions.filter((session) => session.id !== updatedSession.id)
44
+ }
45
+ }
46
+
47
+ const nextSessions = existing
48
+ ? prev.sessions.map((session) => session.id === updatedSession.id ? { ...session, ...updatedSession } : session)
49
+ : [updatedSession, ...prev.sessions]
50
+
51
+ return {
52
+ ...prev,
53
+ sessions: sortSessions(nextSessions)
54
+ }
55
+ }
56
+
57
+ export function useSessionSubscription() {
58
+ const { mutate } = useSWRConfig()
59
+
60
+ useEffect(() => {
61
+ let disposed = false
62
+ let socket: WebSocket | undefined
63
+ let reconnectTimer: ReturnType<typeof setTimeout> | undefined
64
+
65
+ const connect = () => {
66
+ if (disposed) return
67
+
68
+ socket = createSocket({
69
+ onMessage: (data: WSEvent) => {
70
+ if (data.type !== 'session_updated') return
71
+ const updatedSession = data.session as SessionUpdate
72
+
73
+ void mutate('/api/sessions', (prev: SessionListResponse | undefined) => {
74
+ return mergeSessionList(prev, updatedSession, 'active')
75
+ }, false)
76
+
77
+ void mutate('/api/sessions/archived', (prev: SessionListResponse | undefined) => {
78
+ return mergeSessionList(prev, updatedSession, 'archived')
79
+ }, false)
80
+ },
81
+ onClose: () => {
82
+ if (disposed) return
83
+ reconnectTimer = setTimeout(connect, 1000)
84
+ },
85
+ onError: () => {
86
+ socket?.close()
87
+ }
88
+ }, { subscribe: 'sessions' })
89
+ }
90
+
91
+ connect()
92
+
93
+ return () => {
94
+ disposed = true
95
+ if (reconnectTimer) {
96
+ clearTimeout(reconnectTimer)
97
+ }
98
+ socket?.close()
99
+ }
100
+ }, [mutate])
101
+ }
@@ -0,0 +1,35 @@
1
+ import { useAtomValue } from 'jotai'
2
+ import { useCallback } from 'react'
3
+ import { matchPath, useLocation, useNavigate } from 'react-router-dom'
4
+
5
+ import type { Session } from '@vibe-forge/core'
6
+
7
+ import { sidebarWidthAtom } from '#~/store'
8
+
9
+ const SESSION_ROUTE_PATTERN = '/session/:sessionId'
10
+
11
+ export function useSidebarNavigation() {
12
+ const navigate = useNavigate()
13
+ const location = useLocation()
14
+ const sidebarWidth = useAtomValue(sidebarWidthAtom)
15
+ const sessionMatch = matchPath({ path: SESSION_ROUTE_PATTERN, end: true }, location.pathname)
16
+ const activeSessionId = sessionMatch?.params.sessionId
17
+ const showSidebar = location.pathname === '/' || activeSessionId != null
18
+
19
+ const handleSelectSession = useCallback((session: Session, _isNew?: boolean) => {
20
+ void navigate(session.id === '' ? '/' : `/session/${session.id}`)
21
+ }, [navigate])
22
+
23
+ const handleDeletedSession = useCallback((deletedId: string, nextId?: string) => {
24
+ if (activeSessionId !== deletedId) return
25
+ void navigate(nextId ? `/session/${nextId}` : '/')
26
+ }, [activeSessionId, navigate])
27
+
28
+ return {
29
+ activeSessionId,
30
+ handleDeletedSession,
31
+ handleSelectSession,
32
+ showSidebar,
33
+ sidebarWidth
34
+ }
35
+ }
@@ -0,0 +1,20 @@
1
+ import { adapterDisplayName as claudeCodeDisplayName, adapterIcon as claudeCodeIcon } from '@vibe-forge/adapter-claude-code/icon'
2
+ import { adapterDisplayName as codexDisplayName, adapterIcon as codexIcon } from '@vibe-forge/adapter-codex/icon'
3
+
4
+ export const adapterDisplayMap = {
5
+ 'claude-code': {
6
+ title: claudeCodeDisplayName,
7
+ icon: claudeCodeIcon
8
+ },
9
+ codex: {
10
+ title: codexDisplayName,
11
+ icon: codexIcon
12
+ }
13
+ } as const
14
+
15
+ export const getAdapterDisplay = (adapterKey: string) => {
16
+ return adapterDisplayMap[adapterKey as keyof typeof adapterDisplayMap] ?? {
17
+ title: adapterKey,
18
+ icon: undefined
19
+ }
20
+ }
@@ -306,11 +306,17 @@
306
306
  "modelSearchPlaceholder": "Search models or services",
307
307
  "modelUnavailable": "No models available",
308
308
  "modelConfigRequired": "Add a model service in config before starting a session",
309
+ "connectionErrorTitle": "Connection error",
310
+ "connectionError": "WebSocket connection failed. Check the server and try again.",
311
+ "connectionClosed": "WebSocket connection closed. Try reconnecting.",
312
+ "retryConnection": "Retry",
309
313
  "imageTooLarge": "Image must be smaller than 5MB",
310
314
  "imageReadFailed": "Failed to read image",
311
315
  "imageNotSupportedInInteraction": "Images are not supported for this interaction",
312
316
  "modelGroupRecommended": "Recommended Models",
313
317
  "availableTools": "Available Tools",
318
+ "toolGroupChromeDevtools": "ChromeDevtools",
319
+ "toolGroupSystem": "System",
314
320
  "toolsCount": "{{count}} tools",
315
321
  "usedTools": "Used {{count}} tools",
316
322
  "viewInitInfo": "Double click to view initialization info",
@@ -307,11 +307,17 @@
307
307
  "modelSearchPlaceholder": "搜索模型或服务",
308
308
  "modelUnavailable": "暂无可用模型",
309
309
  "modelConfigRequired": "请先在配置中添加模型服务后再开始会话",
310
+ "connectionErrorTitle": "连接异常",
311
+ "connectionError": "WebSocket 连接失败,请检查服务状态后重试",
312
+ "connectionClosed": "WebSocket 连接已关闭,请重试",
313
+ "retryConnection": "重试连接",
310
314
  "imageTooLarge": "图片大小不能超过 5MB",
311
315
  "imageReadFailed": "图片读取失败",
312
316
  "imageNotSupportedInInteraction": "当前交互不支持图片",
313
317
  "modelGroupRecommended": "推荐模型",
314
318
  "availableTools": "可用工具",
319
+ "toolGroupChromeDevtools": "ChromeDevtools",
320
+ "toolGroupSystem": "系统",
315
321
  "toolsCount": "{{count}} 个工具",
316
322
  "usedTools": "使用了 {{count}} 个工具",
317
323
  "viewInitInfo": "双击查看初始化信息",
@@ -0,0 +1,22 @@
1
+ import { Route, Routes } from 'react-router-dom'
2
+
3
+ import { ArchiveRoute } from '#~/routes/ArchiveRoute'
4
+ import { AutomationRoute } from '#~/routes/AutomationRoute'
5
+ import { BenchmarkRoute } from '#~/routes/BenchmarkRoute'
6
+ import { ChatRoute } from '#~/routes/ChatRoute'
7
+ import { ConfigRoute } from '#~/routes/ConfigRoute'
8
+ import { KnowledgeRoute } from '#~/routes/KnowledgeRoute'
9
+
10
+ export function AppRoutes() {
11
+ return (
12
+ <Routes>
13
+ <Route path='/' element={<ChatRoute />} />
14
+ <Route path='/session/:sessionId' element={<ChatRoute />} />
15
+ <Route path='/archive' element={<ArchiveRoute />} />
16
+ <Route path='/benchmark' element={<BenchmarkRoute />} />
17
+ <Route path='/automation' element={<AutomationRoute />} />
18
+ <Route path='/knowledge' element={<KnowledgeRoute />} />
19
+ <Route path='/config' element={<ConfigRoute />} />
20
+ </Routes>
21
+ )
22
+ }
@@ -0,0 +1,5 @@
1
+ import { ArchiveView } from '#~/components/ArchiveView'
2
+
3
+ export function ArchiveRoute() {
4
+ return <ArchiveView />
5
+ }
@@ -0,0 +1,5 @@
1
+ import { AutomationView } from '#~/components/automation-view'
2
+
3
+ export function AutomationRoute() {
4
+ return <AutomationView />
5
+ }
@@ -0,0 +1,5 @@
1
+ import { BenchmarkView } from '#~/components/benchmark-view'
2
+
3
+ export function BenchmarkRoute() {
4
+ return <BenchmarkView />
5
+ }
@@ -43,6 +43,33 @@
43
43
  }
44
44
  }
45
45
 
46
+ .chat-messages-content {
47
+ display: flex;
48
+ flex-direction: column;
49
+ gap: 12px;
50
+ min-height: 100%;
51
+ }
52
+
53
+ .chat-pending-session-banner {
54
+ align-self: center;
55
+ display: inline-flex;
56
+ align-items: center;
57
+ gap: 8px;
58
+ margin: 0 auto 4px;
59
+ padding: 8px 14px;
60
+ border: 1px solid rgba(59, 130, 246, .18);
61
+ border-radius: 999px;
62
+ background: linear-gradient(135deg, rgba(239, 246, 255, .92), rgba(255, 255, 255, .96));
63
+ color: #2563eb;
64
+ font-size: 13px;
65
+ font-weight: 500;
66
+ box-shadow: 0 8px 24px rgba(59, 130, 246, .08);
67
+
68
+ .material-symbols-rounded {
69
+ font-size: 16px;
70
+ }
71
+ }
72
+
46
73
  .sender-container {
47
74
  flex: 0;
48
75
  display: flex;
@@ -87,3 +114,11 @@
87
114
  overflow: auto;
88
115
  padding: 20px 24px 24px;
89
116
  }
117
+
118
+ .chat-route__empty-state {
119
+ height: 100%;
120
+ display: grid;
121
+ place-items: center;
122
+ align-content: center;
123
+ gap: 16px;
124
+ }