@vibe-forge/client 0.3.0 → 0.5.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 (163) hide show
  1. package/cli.cjs +2 -1
  2. package/dist/assets/{arc-CwMXUVsq.js → arc-C4ymrcSQ.js} +1 -1
  3. package/dist/assets/{blockDiagram-c4efeb88-CGxJV7KJ.js → blockDiagram-c4efeb88-CeB7-kgP.js} +1 -1
  4. package/dist/assets/{c4Diagram-c83219d4-BKhin7cY.js → c4Diagram-c83219d4-C935Im8S.js} +1 -1
  5. package/dist/assets/channel-84s1ACzD.js +1 -0
  6. package/dist/assets/{classDiagram-beda092f-BASmn22R.js → classDiagram-beda092f-B9IV13KI.js} +1 -1
  7. package/dist/assets/{classDiagram-v2-2358418a-BUk9rNBX.js → classDiagram-v2-2358418a-CXF_K4fE.js} +1 -1
  8. package/dist/assets/clone-B2E8tddE.js +1 -0
  9. package/dist/assets/{createText-1719965b-2XqnWjQY.js → createText-1719965b-DwX8iC5F.js} +1 -1
  10. package/dist/assets/devicon-BWlTeAUU.woff +0 -0
  11. package/dist/assets/devicon-CirD-cQx.ttf +0 -0
  12. package/dist/assets/devicon-Dg8iWy0i.svg +1211 -0
  13. package/dist/assets/devicon-TqfHp33-.eot +0 -0
  14. package/dist/assets/{edges-96097737-B7e32Jeg.js → edges-96097737-9P1uH1RE.js} +1 -1
  15. package/dist/assets/{erDiagram-0228fc6a-CCR2or72.js → erDiagram-0228fc6a-ixeGTFvg.js} +1 -1
  16. package/dist/assets/{flowDb-c6c81e3f-B72HWT9x.js → flowDb-c6c81e3f-G1gSTTBI.js} +1 -1
  17. package/dist/assets/{flowDiagram-50d868cf-WOi0KARY.js → flowDiagram-50d868cf-CzrG99nD.js} +1 -1
  18. package/dist/assets/flowDiagram-v2-4f6560a1-CJfJYbME.js +1 -0
  19. package/dist/assets/{flowchart-elk-definition-6af322e1-i_Yd0LCE.js → flowchart-elk-definition-6af322e1-sFCoysWa.js} +1 -1
  20. package/dist/assets/{ganttDiagram-a2739b55-CFH9zF14.js → ganttDiagram-a2739b55-Ccsk_Lru.js} +1 -1
  21. package/dist/assets/{gitGraphDiagram-82fe8481-DglKfMze.js → gitGraphDiagram-82fe8481-CwathJ6H.js} +1 -1
  22. package/dist/assets/{graph-BKbBNGPf.js → graph-DRCU-8Rz.js} +1 -1
  23. package/dist/assets/{index-5325376f-BK7F9nSl.js → index-5325376f-Bq-fg2i_.js} +1 -1
  24. package/dist/assets/index-CHMuZ5-1.css +1 -0
  25. package/dist/assets/index-cGZvDhhU.js +542 -0
  26. package/dist/assets/{infoDiagram-8eee0895-BLFL77_D.js → infoDiagram-8eee0895-JBcUkJ6T.js} +1 -1
  27. package/dist/assets/{journeyDiagram-c64418c1-CS9XctDL.js → journeyDiagram-c64418c1-DsdQU-R8.js} +1 -1
  28. package/dist/assets/{layout-By3JZZGt.js → layout-s0slG1OL.js} +1 -1
  29. package/dist/assets/{line-9GUsXbwv.js → line-CymFqgW6.js} +1 -1
  30. package/dist/assets/{linear-DzGV4E9N.js → linear-lDQVZ6aQ.js} +1 -1
  31. package/dist/assets/{mermaid.core-CG3Ib42Q.js → mermaid.core-Cmlqga_E.js} +6 -6
  32. package/dist/assets/{mindmap-definition-8da855dc-WQ3LPKJU.js → mindmap-definition-8da855dc-CqqTDJn_.js} +1 -1
  33. package/dist/assets/{pieDiagram-a8764435-DHVIUZiN.js → pieDiagram-a8764435-BL2Ajx7Z.js} +1 -1
  34. package/dist/assets/{quadrantDiagram-1e28029f-C3G9Ye8-.js → quadrantDiagram-1e28029f-ClL_3ASt.js} +1 -1
  35. package/dist/assets/{requirementDiagram-08caed73-C9ES1D5G.js → requirementDiagram-08caed73-CB1RgE3K.js} +1 -1
  36. package/dist/assets/{sankeyDiagram-a04cb91d-B4BKXclQ.js → sankeyDiagram-a04cb91d-tgleEYiD.js} +1 -1
  37. package/dist/assets/{sequenceDiagram-c5b8d532-DrgEb25G.js → sequenceDiagram-c5b8d532-DlatQT5R.js} +1 -1
  38. package/dist/assets/{stateDiagram-1ecb1508-CF1XWARJ.js → stateDiagram-1ecb1508-B--MLqRs.js} +1 -1
  39. package/dist/assets/{stateDiagram-v2-c2b004d7-IO3i3yXv.js → stateDiagram-v2-c2b004d7-CRMZ6Dpx.js} +1 -1
  40. package/dist/assets/{styles-b4e223ce-DACN9aSc.js → styles-b4e223ce-CPiYHfUz.js} +1 -1
  41. package/dist/assets/{styles-ca3715f6-bekm2WLP.js → styles-ca3715f6-B9UKPAzX.js} +1 -1
  42. package/dist/assets/{styles-d45a18b0-OzTDVBb8.js → styles-d45a18b0-BC1Ak1So.js} +1 -1
  43. package/dist/assets/{svgDrawCommon-b86b1483-BWroJerr.js → svgDrawCommon-b86b1483-DV8R0g-n.js} +1 -1
  44. package/dist/assets/{timeline-definition-faaaa080-CCfRNigO.js → timeline-definition-faaaa080-CiqGS5DC.js} +1 -1
  45. package/dist/assets/{xychartDiagram-f5964ef8-C3cbfVqN.js → xychartDiagram-f5964ef8-h6VSD3GE.js} +1 -1
  46. package/dist/index.html +2 -7
  47. package/index.html +0 -5
  48. package/package.json +12 -6
  49. package/src/App.tsx +2 -0
  50. package/src/api/README.md +26 -0
  51. package/src/api/automation.ts +88 -0
  52. package/src/api/base.ts +54 -0
  53. package/src/api/benchmark.ts +45 -0
  54. package/src/api/config.ts +24 -0
  55. package/src/api/knowledge.ts +72 -0
  56. package/src/api/projects.ts +15 -0
  57. package/src/api/sessions.ts +84 -0
  58. package/src/api/types.ts +20 -0
  59. package/src/api.ts +44 -269
  60. package/src/components/AutomationView/AutomationView.scss +5 -1
  61. package/src/components/AutomationView/RuleFormPanel.tsx +3 -2
  62. package/src/components/AutomationView/TaskList.scss +4 -6
  63. package/src/components/AutomationView/TaskList.tsx +2 -1
  64. package/src/components/AutomationView/TriggerList.scss +4 -1
  65. package/src/components/BenchmarkView/BenchmarkCasePanel.scss +267 -0
  66. package/src/components/BenchmarkView/BenchmarkCasePanel.tsx +309 -0
  67. package/src/components/BenchmarkView/BenchmarkSidebar.scss +182 -0
  68. package/src/components/BenchmarkView/BenchmarkSidebar.tsx +262 -0
  69. package/src/components/BenchmarkView/BenchmarkView.scss +78 -0
  70. package/src/components/BenchmarkView/index.tsx +197 -0
  71. package/src/components/BenchmarkView/types.ts +10 -0
  72. package/src/components/BenchmarkView/utils.ts +21 -0
  73. package/src/components/Chat.tsx +43 -29
  74. package/src/components/{chat/CodeBlock.tsx → CodeBlock.tsx} +3 -1
  75. package/src/components/ConfigView.tsx +32 -25
  76. package/src/components/{chat/MarkdownContent.tsx → MarkdownContent.tsx} +1 -1
  77. package/src/components/NavRail.tsx +7 -0
  78. package/src/components/chat/ChatHeader.scss +37 -19
  79. package/src/components/chat/ChatHeader.tsx +6 -9
  80. package/src/components/chat/ChatHistoryView.tsx +99 -45
  81. package/src/components/chat/CurrentTodoList.tsx +10 -9
  82. package/src/components/chat/{MessageItem.scss → Messages/MessageItem.scss} +14 -0
  83. package/src/components/chat/{MessageItem.tsx → Messages/MessageItem.tsx} +30 -8
  84. package/src/components/chat/{messageUtils.ts → Messages/message-utils.ts} +1 -1
  85. package/src/components/chat/{Sender.scss → Sender/Sender.scss} +146 -3
  86. package/src/components/chat/{Sender.tsx → Sender/Sender.tsx} +183 -5
  87. package/src/components/chat/tools/DefaultTool.tsx +184 -21
  88. package/src/components/chat/tools/adapter-claude/BashTool.scss +67 -51
  89. package/src/components/chat/tools/adapter-claude/BashTool.tsx +83 -49
  90. package/src/components/chat/tools/adapter-claude/GlobTool.scss +0 -79
  91. package/src/components/chat/tools/adapter-claude/GlobTool.tsx +16 -36
  92. package/src/components/chat/tools/adapter-claude/GrepTool.scss +0 -87
  93. package/src/components/chat/tools/adapter-claude/GrepTool.tsx +22 -41
  94. package/src/components/chat/tools/adapter-claude/LSTool.scss +0 -79
  95. package/src/components/chat/tools/adapter-claude/LSTool.tsx +15 -15
  96. package/src/components/chat/tools/adapter-claude/ReadTool.scss +0 -55
  97. package/src/components/chat/tools/adapter-claude/ReadTool.tsx +20 -42
  98. package/src/components/chat/tools/adapter-claude/TodoTool.scss +8 -23
  99. package/src/components/chat/tools/adapter-claude/TodoTool.tsx +24 -11
  100. package/src/components/chat/tools/adapter-claude/WriteTool.scss +21 -69
  101. package/src/components/chat/tools/adapter-claude/WriteTool.tsx +22 -58
  102. package/src/components/chat/tools/adapter-claude/index.ts +4 -10
  103. package/src/components/chat/tools/adapter-claude/utils.ts +54 -0
  104. package/src/components/chat/tools/core/ToolCallBox.scss +356 -0
  105. package/src/components/chat/{ToolGroup.tsx → tools/core/ToolGroup.tsx} +26 -7
  106. package/src/components/chat/{ToolRenderer.tsx → tools/core/ToolRenderer.tsx} +6 -4
  107. package/src/components/chat/tools/plugin-chrome-devtools/ChromeDevtoolsTool.scss +11 -0
  108. package/src/components/chat/tools/plugin-chrome-devtools/ChromeDevtoolsTool.tsx +75 -0
  109. package/src/components/chat/tools/plugin-chrome-devtools/index.ts +45 -0
  110. package/src/components/chat/tools/task/GetTaskInfoTool.scss +2 -27
  111. package/src/components/chat/tools/task/GetTaskInfoTool.tsx +48 -38
  112. package/src/components/chat/tools/task/ListTasksTool.scss +3 -28
  113. package/src/components/chat/tools/task/ListTasksTool.tsx +11 -8
  114. package/src/components/chat/tools/task/StartTasksTool.scss +3 -28
  115. package/src/components/chat/tools/task/StartTasksTool.tsx +14 -17
  116. package/src/components/chat/tools/task/components/TaskRow.scss +105 -0
  117. package/src/components/chat/tools/task/components/TaskRow.tsx +163 -0
  118. package/src/components/chat/tools/task/components/TaskToolCard.scss +15 -15
  119. package/src/components/chat/tools/task/components/TaskToolCard.tsx +8 -6
  120. package/src/components/config/ConfigSectionForm.tsx +12 -1
  121. package/src/components/config/ConfigSourceSwitch.tsx +12 -34
  122. package/src/components/config/channelDefinitions.ts +6 -0
  123. package/src/components/config/configSchema.ts +10 -1
  124. package/src/components/config/recordEditors/ChannelRecordEditor.scss +1 -0
  125. package/src/components/config/recordEditors/ChannelRecordEditor.tsx +397 -0
  126. package/src/components/config/recordEditors/index.tsx +1 -0
  127. package/src/components/knowledge-base/components/RuleItem.tsx +1 -1
  128. package/src/components/knowledge-base/components/SpecItem.tsx +1 -1
  129. package/src/components/sidebar/SessionItem.scss +17 -0
  130. package/src/components/sidebar/SessionItem.tsx +21 -13
  131. package/src/hooks/chat/use-chat-adapter.ts +81 -0
  132. package/src/hooks/chat/use-chat-interaction.ts +26 -0
  133. package/src/{components/chat/useChatModels.tsx → hooks/chat/use-chat-models.tsx} +117 -22
  134. package/src/hooks/chat/use-chat-permission-mode.ts +47 -0
  135. package/src/hooks/chat/use-chat-scroll.ts +51 -0
  136. package/src/hooks/chat/use-chat-session-actions.ts +153 -0
  137. package/src/hooks/chat/use-chat-session-messages.ts +262 -0
  138. package/src/hooks/chat/use-chat-session.ts +63 -0
  139. package/src/hooks/chat/use-chat-view.ts +39 -0
  140. package/src/main.tsx +10 -13
  141. package/src/resources/adapters.ts +20 -0
  142. package/src/resources/locales/en.json +66 -0
  143. package/src/resources/locales/zh.json +66 -0
  144. package/src/runtime-config.ts +52 -0
  145. package/src/vite-env.d.ts +11 -0
  146. package/src/ws.ts +5 -3
  147. package/vite.config.ts +12 -4
  148. package/dist/assets/channel-jbCEHqbG.js +0 -1
  149. package/dist/assets/clone-CCRKqS4L.js +0 -1
  150. package/dist/assets/flowDiagram-v2-4f6560a1-Baslbgn4.js +0 -1
  151. package/dist/assets/index-B0qfCb1G.css +0 -1
  152. package/dist/assets/index-CNo75dYr.js +0 -497
  153. package/src/components/chat/ToolCallBox.scss +0 -137
  154. package/src/components/chat/useChatSession.ts +0 -370
  155. /package/src/components/{chat/CodeBlock.scss → CodeBlock.scss} +0 -0
  156. /package/src/components/chat/{MessageFooter.tsx → Messages/MessageFooter.tsx} +0 -0
  157. /package/src/components/chat/{CompletionMenu.scss → Sender/CompletionMenu.scss} +0 -0
  158. /package/src/components/chat/{CompletionMenu.tsx → Sender/CompletionMenu.tsx} +0 -0
  159. /package/src/components/chat/{ThinkingStatus.scss → Sender/ThinkingStatus.scss} +0 -0
  160. /package/src/components/chat/{ThinkingStatus.tsx → Sender/ThinkingStatus.tsx} +0 -0
  161. /package/src/components/chat/{ToolCallBox.tsx → tools/core/ToolCallBox.tsx} +0 -0
  162. /package/src/components/chat/{ToolGroup.scss → tools/core/ToolGroup.scss} +0 -0
  163. /package/src/{components/chat/safeSerialize.ts → utils/safe-serialize.ts} +0 -0
@@ -0,0 +1,81 @@
1
+ import { createElement, type ReactNode } from 'react'
2
+ import { useEffect, useMemo, useState } from 'react'
3
+ import useSWR from 'swr'
4
+
5
+ import { getConfig } from '#~/api.js'
6
+ import type { ConfigResponse } from '@vibe-forge/core'
7
+ import { getAdapterDisplay } from '#~/resources/adapters.js'
8
+
9
+ const ADAPTER_STORAGE_KEY = 'vf_chat_adapter'
10
+
11
+ export function useChatAdapter() {
12
+ const [selectedAdapter, setSelectedAdapter] = useState<string | undefined>(() => {
13
+ try {
14
+ const raw = localStorage.getItem(ADAPTER_STORAGE_KEY)
15
+ return raw == null || raw.trim() === '' ? undefined : raw
16
+ } catch {
17
+ return undefined
18
+ }
19
+ })
20
+
21
+ const { data: configRes } = useSWR<ConfigResponse>('/api/config', getConfig)
22
+
23
+ const mergedAdapters = useMemo(() => {
24
+ return configRes?.sources?.merged?.adapters ?? {}
25
+ }, [configRes?.sources?.merged?.adapters])
26
+
27
+ const defaultAdapter = configRes?.sources?.merged?.general?.defaultAdapter
28
+
29
+ const adapterOptions = useMemo<Array<{ value: string; label: ReactNode }>>(() => {
30
+ const keys = Object.keys(mergedAdapters)
31
+ return keys.map((key) => {
32
+ const display = getAdapterDisplay(key)
33
+ return {
34
+ value: key,
35
+ label: createElement('span', { className: 'adapter-option' }, [
36
+ display.icon != null
37
+ ? createElement('img', {
38
+ key: 'icon',
39
+ className: 'adapter-option__icon',
40
+ src: display.icon,
41
+ alt: '',
42
+ 'aria-hidden': true
43
+ })
44
+ : null,
45
+ createElement('span', { key: 'text', className: 'adapter-option__text' }, display.title)
46
+ ])
47
+ }
48
+ })
49
+ }, [mergedAdapters])
50
+
51
+ // Auto-select: use stored value if valid, else config default, else first available
52
+ useEffect(() => {
53
+ const keys = Object.keys(mergedAdapters)
54
+ if (keys.length === 0) {
55
+ setSelectedAdapter(undefined)
56
+ return
57
+ }
58
+ setSelectedAdapter((prev) => {
59
+ if (prev != null && keys.includes(prev)) return prev
60
+ if (defaultAdapter && keys.includes(defaultAdapter as string)) return defaultAdapter as string
61
+ return keys[0]
62
+ })
63
+ }, [defaultAdapter, mergedAdapters])
64
+
65
+ // Persist to localStorage
66
+ useEffect(() => {
67
+ try {
68
+ if (selectedAdapter == null || selectedAdapter.trim() === '') {
69
+ localStorage.removeItem(ADAPTER_STORAGE_KEY)
70
+ } else {
71
+ localStorage.setItem(ADAPTER_STORAGE_KEY, selectedAdapter)
72
+ }
73
+ } catch {}
74
+ }, [selectedAdapter])
75
+
76
+ return {
77
+ selectedAdapter,
78
+ setSelectedAdapter,
79
+ adapterOptions
80
+ }
81
+ }
@@ -0,0 +1,26 @@
1
+ import { useCallback, useState } from 'react'
2
+
3
+ import type { AskUserQuestionParams } from '@vibe-forge/core'
4
+ import { connectionManager } from '#~/connectionManager.js'
5
+
6
+ export function useChatInteraction({ sessionId }: { sessionId?: string }) {
7
+ const [interactionRequest, setInteractionRequest] = useState<{ id: string; payload: AskUserQuestionParams } | null>(
8
+ null
9
+ )
10
+
11
+ const handleInteractionResponse = useCallback((id: string, data: string | string[]) => {
12
+ if (!sessionId) return
13
+ connectionManager.send(sessionId, {
14
+ type: 'interaction_response',
15
+ id,
16
+ data
17
+ })
18
+ setInteractionRequest(null)
19
+ }, [sessionId])
20
+
21
+ return {
22
+ interactionRequest,
23
+ setInteractionRequest,
24
+ handleInteractionResponse
25
+ }
26
+ }
@@ -1,22 +1,26 @@
1
- import React, { useEffect, useMemo, useState } from 'react'
1
+ import React, { useCallback, useEffect, useMemo, useState } from 'react'
2
2
  import { useTranslation } from 'react-i18next'
3
3
  import useSWR from 'swr'
4
4
 
5
- import type { ConfigResponse, ModelServiceConfig, RecommendedModelConfig } from '@vibe-forge/core'
6
- import { getConfig } from '../../api'
5
+ import type { AdapterBuiltinModel, ConfigResponse, ModelServiceConfig, RecommendedModelConfig } from '@vibe-forge/core'
6
+ import { getConfig } from '#~/api.js'
7
7
 
8
- interface ModelSelectOption {
8
+ export interface ModelSelectOption {
9
9
  value: string
10
10
  label: React.ReactNode
11
11
  searchText: string
12
12
  }
13
13
 
14
- interface ModelSelectGroup {
14
+ export interface ModelSelectGroup {
15
15
  label: React.ReactNode
16
16
  options: ModelSelectOption[]
17
17
  }
18
18
 
19
- export function useChatModels() {
19
+ export function useChatModels({
20
+ selectedAdapter
21
+ }: {
22
+ selectedAdapter?: string
23
+ } = {}) {
20
24
  const { t } = useTranslation()
21
25
  const [selectedModel, setSelectedModel] = useState<string | undefined>(() => {
22
26
  try {
@@ -41,12 +45,38 @@ export function useChatModels() {
41
45
  ))
42
46
  }, [configRes?.sources?.merged?.general?.recommendedModels])
43
47
 
48
+ const adapterBuiltinModels = useMemo(() => {
49
+ const raw = configRes?.sources?.merged?.adapterBuiltinModels
50
+ return (raw ?? {}) as Record<string, AdapterBuiltinModel[]>
51
+ }, [configRes?.sources?.merged?.adapterBuiltinModels])
52
+
53
+ const activeBuiltinModels = useMemo(() => {
54
+ if (selectedAdapter && adapterBuiltinModels[selectedAdapter]) {
55
+ return { [selectedAdapter]: adapterBuiltinModels[selectedAdapter] }
56
+ }
57
+ return adapterBuiltinModels
58
+ }, [adapterBuiltinModels, selectedAdapter])
59
+
60
+ const activeBuiltinModelValues = useMemo(() => (
61
+ Object.values(activeBuiltinModels).flat().map(model => model.value)
62
+ ), [activeBuiltinModels])
63
+
64
+ const builtinModelSet = useMemo(() => {
65
+ const set = new Set<string>()
66
+ for (const models of Object.values(activeBuiltinModels)) {
67
+ for (const m of models) set.add(m.value)
68
+ }
69
+ return set
70
+ }, [activeBuiltinModels])
71
+
44
72
  const modelServiceEntries = useMemo(() => Object.entries(mergedModelServices), [mergedModelServices])
45
73
 
46
74
  const availableModels = useMemo(() => {
47
75
  const list: Array<{ model: string; serviceKey: string; serviceTitle: string }> = []
48
76
  for (const [serviceKey, serviceValue] of modelServiceEntries) {
49
- const service = (serviceValue != null && typeof serviceValue === 'object') ? serviceValue as ModelServiceConfig : undefined
77
+ const service = (serviceValue != null && typeof serviceValue === 'object')
78
+ ? serviceValue as ModelServiceConfig
79
+ : undefined
50
80
  const serviceTitle = service?.title?.trim() !== '' ? service?.title ?? '' : serviceKey
51
81
  const models = Array.isArray(service?.models) ? service?.models.filter(item => typeof item === 'string') : []
52
82
  for (const model of models) {
@@ -59,19 +89,55 @@ export function useChatModels() {
59
89
  const availableModelValues = useMemo(() => availableModels.map(item => item.model), [availableModels])
60
90
  const availableModelKey = useMemo(() => availableModelValues.join('|'), [availableModelValues])
61
91
  const availableModelSet = useMemo(() => new Set(availableModelValues), [availableModelKey])
62
- const hasAvailableModels = availableModelValues.length > 0
92
+ const hasAvailableModels = availableModelValues.length > 0 || builtinModelSet.size > 0
93
+ const modelToService = useMemo(() => {
94
+ const map = new Map<string, { key: string; title: string }>()
95
+ for (const entry of availableModels) {
96
+ if (!map.has(entry.model)) {
97
+ map.set(entry.model, { key: entry.serviceKey, title: entry.serviceTitle })
98
+ }
99
+ }
100
+ return map
101
+ }, [availableModels])
63
102
  const defaultModelService = configRes?.sources?.merged?.general?.defaultModelService
64
103
  const defaultModel = configRes?.sources?.merged?.general?.defaultModel
104
+ const formatModelWithService = useCallback((model: string | undefined) => {
105
+ const normalizedModel = typeof model === 'string' ? model.trim() : ''
106
+ if (normalizedModel === '') return undefined
107
+ // Builtin adapter models pass through as-is (no service prefix)
108
+ if (builtinModelSet.has(normalizedModel)) return normalizedModel
109
+ if (normalizedModel.includes(',')) return normalizedModel
110
+ const resolvedService = modelToService.get(normalizedModel)?.key ?? defaultModelService
111
+ return resolvedService ? `${resolvedService},${normalizedModel}` : normalizedModel
112
+ }, [builtinModelSet, defaultModelService, modelToService])
65
113
  const resolvedDefaultModel = useMemo(() => {
66
- if (!hasAvailableModels) return undefined
114
+ if (defaultModel && builtinModelSet.has(defaultModel)) return defaultModel
115
+ if (activeBuiltinModelValues.length > 0) {
116
+ return activeBuiltinModelValues[0]
117
+ }
67
118
  if (defaultModel && availableModelSet.has(defaultModel)) return defaultModel
68
119
  if (defaultModelService && mergedModelServices[defaultModelService]) {
69
120
  const service = mergedModelServices[defaultModelService]
70
121
  const models = Array.isArray(service?.models) ? service?.models.filter(item => typeof item === 'string') : []
71
122
  if (models.length > 0) return models[0]
72
123
  }
73
- return availableModelValues[0]
74
- }, [availableModelSet, availableModelValues, defaultModel, defaultModelService, hasAvailableModels, mergedModelServices])
124
+ if (availableModelValues.length > 0) return availableModelValues[0]
125
+ // Fall back to first builtin model from the active adapter
126
+ const firstBuiltin = Object.values(activeBuiltinModels).flat()[0]
127
+ return firstBuiltin?.value
128
+ }, [
129
+ activeBuiltinModels,
130
+ activeBuiltinModelValues,
131
+ availableModelSet,
132
+ availableModelValues,
133
+ builtinModelSet,
134
+ defaultModel,
135
+ defaultModelService,
136
+ mergedModelServices
137
+ ])
138
+ const selectedModelWithService = useMemo(() => (
139
+ formatModelWithService(selectedModel)
140
+ ), [formatModelWithService, selectedModel])
75
141
 
76
142
  useEffect(() => {
77
143
  if (!hasAvailableModels) {
@@ -79,10 +145,13 @@ export function useChatModels() {
79
145
  return
80
146
  }
81
147
  setSelectedModel((prev) => {
82
- if (prev != null && availableModelSet.has(prev)) return prev
148
+ if (prev != null) {
149
+ const isValid = availableModelSet.has(prev) || builtinModelSet.has(prev)
150
+ if (isValid) return prev
151
+ }
83
152
  return resolvedDefaultModel
84
153
  })
85
- }, [availableModelSet, hasAvailableModels, resolvedDefaultModel])
154
+ }, [availableModelSet, builtinModelSet, hasAvailableModels, resolvedDefaultModel, selectedAdapter])
86
155
 
87
156
  useEffect(() => {
88
157
  try {
@@ -126,13 +195,6 @@ export function useChatModels() {
126
195
  }
127
196
  }
128
197
 
129
- const modelToService = new Map<string, { key: string; title: string }>()
130
- for (const entry of availableModels) {
131
- if (!modelToService.has(entry.model)) {
132
- modelToService.set(entry.model, { key: entry.serviceKey, title: entry.serviceTitle })
133
- }
134
- }
135
-
136
198
  const resolveFirstAlias = (modelsAlias: Record<string, string[]> | undefined, model: string) => {
137
199
  if (!modelsAlias) return undefined
138
200
  for (const [alias, aliasModels] of Object.entries(modelsAlias)) {
@@ -144,7 +206,9 @@ export function useChatModels() {
144
206
 
145
207
  const serviceGroups = modelServiceEntries
146
208
  .map(([serviceKey, serviceValue]) => {
147
- const service = (serviceValue != null && typeof serviceValue === 'object') ? serviceValue as ModelServiceConfig : undefined
209
+ const service = (serviceValue != null && typeof serviceValue === 'object')
210
+ ? serviceValue as ModelServiceConfig
211
+ : undefined
148
212
  const serviceTitle = service?.title?.trim() !== '' ? service?.title ?? '' : serviceKey
149
213
  const groupTitle = serviceTitle?.trim() !== '' ? serviceTitle : serviceKey
150
214
  const serviceDescription = service?.description
@@ -212,11 +276,42 @@ export function useChatModels() {
212
276
  options: recommendedOptions
213
277
  })
214
278
  }
279
+
280
+ // Adapter builtin model groups (filtered to active adapter)
281
+ for (const [adapterKey, models] of Object.entries(activeBuiltinModels)) {
282
+ if (!Array.isArray(models) || models.length === 0) continue
283
+ const adapterTitle = t('chat.modelGroupBuiltin', {
284
+ adapter: adapterKey,
285
+ defaultValue: `${adapterKey} (Default)`
286
+ })
287
+ groups.push({
288
+ label: (
289
+ <div className='model-group-label'>
290
+ <div className='model-group-title'>{adapterTitle}</div>
291
+ </div>
292
+ ),
293
+ options: models.map(m => buildOption({
294
+ value: m.value,
295
+ title: m.title,
296
+ description: m.description
297
+ }))
298
+ })
299
+ }
300
+
215
301
  return [...groups, ...serviceGroups]
216
- }, [availableModelSet, availableModels, mergedModelServices, modelServiceEntries, recommendedModels, t])
302
+ }, [
303
+ activeBuiltinModels,
304
+ availableModelSet,
305
+ modelToService,
306
+ mergedModelServices,
307
+ modelServiceEntries,
308
+ recommendedModels,
309
+ t
310
+ ])
217
311
 
218
312
  return {
219
313
  selectedModel,
314
+ selectedModelWithService,
220
315
  setSelectedModel,
221
316
  modelOptions,
222
317
  hasAvailableModels
@@ -0,0 +1,47 @@
1
+ import { useEffect, useMemo, useState } from 'react'
2
+ import type { ReactNode } from 'react'
3
+
4
+ export type PermissionMode = 'default' | 'acceptEdits' | 'plan' | 'dontAsk' | 'bypassPermissions'
5
+
6
+ const PERMISSION_MODE_STORAGE_KEY = 'vf_chat_permission_mode'
7
+
8
+ const isPermissionMode = (value: string): value is PermissionMode => {
9
+ return value === 'default'
10
+ || value === 'acceptEdits'
11
+ || value === 'plan'
12
+ || value === 'dontAsk'
13
+ || value === 'bypassPermissions'
14
+ }
15
+
16
+ export function useChatPermissionMode() {
17
+ const [permissionMode, setPermissionMode] = useState<PermissionMode>('default')
18
+
19
+ useEffect(() => {
20
+ try {
21
+ const raw = localStorage.getItem(PERMISSION_MODE_STORAGE_KEY)
22
+ if (raw && isPermissionMode(raw)) {
23
+ setPermissionMode(raw)
24
+ }
25
+ } catch {}
26
+ }, [])
27
+
28
+ useEffect(() => {
29
+ try {
30
+ localStorage.setItem(PERMISSION_MODE_STORAGE_KEY, permissionMode)
31
+ } catch {}
32
+ }, [permissionMode])
33
+
34
+ const permissionModeOptions = useMemo<Array<{ value: PermissionMode; label: ReactNode }>>(() => ([
35
+ { value: 'default', label: '默认' },
36
+ { value: 'acceptEdits', label: '接受编辑' },
37
+ { value: 'plan', label: '计划' },
38
+ { value: 'dontAsk', label: '不询问' },
39
+ { value: 'bypassPermissions', label: '跳过权限' }
40
+ ]), [])
41
+
42
+ return {
43
+ permissionMode,
44
+ setPermissionMode,
45
+ permissionModeOptions
46
+ }
47
+ }
@@ -0,0 +1,51 @@
1
+ import { useCallback, useEffect, useRef, useState } from 'react'
2
+
3
+ const SCROLL_THRESHOLD = 80
4
+
5
+ export function useChatScroll({ messagesLength }: { messagesLength: number }) {
6
+ const messagesEndRef = useRef<HTMLDivElement>(null)
7
+ const messagesContainerRef = useRef<HTMLDivElement>(null)
8
+ const messagesContentRef = useRef<HTMLDivElement>(null)
9
+ const [showScrollBottom, setShowScrollBottom] = useState(false)
10
+
11
+ const updateScrollState = useCallback(() => {
12
+ const container = messagesContainerRef.current
13
+ if (!container) return
14
+ const distanceToBottom = container.scrollHeight - (container.scrollTop + container.clientHeight)
15
+ setShowScrollBottom(distanceToBottom > SCROLL_THRESHOLD)
16
+ }, [])
17
+
18
+ const scrollToBottom = useCallback((behavior: ScrollBehavior = 'smooth') => {
19
+ setTimeout(() => {
20
+ if (messagesContainerRef.current) {
21
+ messagesContainerRef.current.scrollTo({
22
+ top: messagesContainerRef.current.scrollHeight,
23
+ behavior
24
+ })
25
+ }
26
+ }, 50)
27
+ }, [])
28
+
29
+ useEffect(() => {
30
+ const container = messagesContainerRef.current
31
+ if (!container) return
32
+ updateScrollState()
33
+ const handleScroll = () => updateScrollState()
34
+ container.addEventListener('scroll', handleScroll, { passive: true })
35
+ return () => {
36
+ container.removeEventListener('scroll', handleScroll)
37
+ }
38
+ }, [updateScrollState])
39
+
40
+ useEffect(() => {
41
+ updateScrollState()
42
+ }, [updateScrollState, messagesLength])
43
+
44
+ return {
45
+ messagesEndRef,
46
+ messagesContainerRef,
47
+ messagesContentRef,
48
+ showScrollBottom,
49
+ scrollToBottom
50
+ }
51
+ }
@@ -0,0 +1,153 @@
1
+ import { App } from 'antd'
2
+ import { useCallback, useState } from 'react'
3
+ import { useTranslation } from 'react-i18next'
4
+ import { useNavigate } from 'react-router-dom'
5
+ import { useSWRConfig } from 'swr'
6
+
7
+ import { createSession } from '#~/api.js'
8
+ import { connectionManager } from '#~/connectionManager.js'
9
+ import type { ChatMessageContent, Session } from '@vibe-forge/core'
10
+ import type { PermissionMode } from './use-chat-permission-mode'
11
+
12
+ export function useChatSessionActions({
13
+ session,
14
+ modelForQuery,
15
+ hasAvailableModels,
16
+ permissionMode,
17
+ adapter,
18
+ onClearMessages
19
+ }: {
20
+ session?: Session
21
+ modelForQuery?: string
22
+ hasAvailableModels: boolean
23
+ permissionMode: PermissionMode
24
+ adapter?: string
25
+ onClearMessages: () => void
26
+ }) {
27
+ const { message } = App.useApp()
28
+ const { t } = useTranslation()
29
+ const navigate = useNavigate()
30
+ const { mutate } = useSWRConfig()
31
+ const [isCreating, setIsCreating] = useState(false)
32
+ const isThinking = isCreating || session?.status === 'running'
33
+
34
+ const send = useCallback(async (text: string) => {
35
+ if (text.trim() === '' || isThinking) return
36
+ if (!hasAvailableModels) {
37
+ void message.warning(t('chat.modelConfigRequired'))
38
+ return
39
+ }
40
+
41
+ if (!session?.id) {
42
+ setIsCreating(true)
43
+ try {
44
+ const { session: newSession } = await createSession(undefined, text.trim(), undefined, modelForQuery, {
45
+ permissionMode,
46
+ adapter
47
+ })
48
+
49
+ await mutate('/api/sessions', (prev: { sessions: Session[] } | undefined) => {
50
+ if (!prev?.sessions) return { sessions: [newSession] }
51
+ return {
52
+ ...prev,
53
+ sessions: [newSession, ...prev.sessions]
54
+ }
55
+ }, false)
56
+
57
+ void navigate(`/session/${newSession.id}`)
58
+ } catch (err) {
59
+ console.error(err)
60
+ setIsCreating(false)
61
+ void message.error('Failed to create session')
62
+ }
63
+ return
64
+ }
65
+
66
+ connectionManager.send(session.id, {
67
+ type: 'user_message',
68
+ text: text.trim()
69
+ })
70
+ }, [
71
+ adapter,
72
+ hasAvailableModels,
73
+ isThinking,
74
+ message,
75
+ mutate,
76
+ navigate,
77
+ permissionMode,
78
+ modelForQuery,
79
+ session?.id,
80
+ t
81
+ ])
82
+
83
+ const sendContent = useCallback(async (content: ChatMessageContent[]) => {
84
+ if (content.length === 0 || isThinking) return
85
+ if (!hasAvailableModels) {
86
+ void message.warning(t('chat.modelConfigRequired'))
87
+ return
88
+ }
89
+
90
+ if (!session?.id) {
91
+ setIsCreating(true)
92
+ try {
93
+ const { session: newSession } = await createSession(undefined, undefined, content, modelForQuery, {
94
+ permissionMode,
95
+ adapter
96
+ })
97
+
98
+ await mutate('/api/sessions', (prev: { sessions: Session[] } | undefined) => {
99
+ if (!prev?.sessions) return { sessions: [newSession] }
100
+ return {
101
+ ...prev,
102
+ sessions: [newSession, ...prev.sessions]
103
+ }
104
+ }, false)
105
+
106
+ void navigate(`/session/${newSession.id}`)
107
+ setIsCreating(false)
108
+ } catch (err) {
109
+ console.error(err)
110
+ setIsCreating(false)
111
+ void message.error('Failed to create session')
112
+ }
113
+ return
114
+ }
115
+
116
+ connectionManager.send(session.id, {
117
+ type: 'user_message',
118
+ content
119
+ })
120
+ }, [
121
+ adapter,
122
+ hasAvailableModels,
123
+ isThinking,
124
+ message,
125
+ mutate,
126
+ navigate,
127
+ permissionMode,
128
+ modelForQuery,
129
+ session?.id,
130
+ t
131
+ ])
132
+
133
+ const interrupt = useCallback(() => {
134
+ if (!session?.id || isThinking === false) return
135
+ connectionManager.send(session.id, {
136
+ type: 'interrupt'
137
+ })
138
+ }, [isThinking, session?.id])
139
+
140
+ const clearMessages = useCallback(() => {
141
+ onClearMessages()
142
+ void message.success('Messages cleared')
143
+ }, [message, onClearMessages])
144
+
145
+ return {
146
+ isCreating,
147
+ isThinking,
148
+ send,
149
+ sendContent,
150
+ interrupt,
151
+ clearMessages
152
+ }
153
+ }