@vibe-forge/client 0.2.0-alpha.9 → 0.4.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 (166) hide show
  1. package/cli.cjs +1 -1
  2. package/dist/assets/{arc-CybT1Fs2.js → arc-DgIxeTMg.js} +1 -1
  3. package/dist/assets/{blockDiagram-c4efeb88-BY5Aoa-D.js → blockDiagram-c4efeb88-CEAob3X9.js} +1 -1
  4. package/dist/assets/{c4Diagram-c83219d4-F42hTbzS.js → c4Diagram-c83219d4-DwIxpDKd.js} +1 -1
  5. package/dist/assets/channel-DhtnrNJ6.js +1 -0
  6. package/dist/assets/{classDiagram-beda092f-D-tIPp-3.js → classDiagram-beda092f-Cz1q8u_0.js} +1 -1
  7. package/dist/assets/{classDiagram-v2-2358418a-J57aCe6u.js → classDiagram-v2-2358418a-CImgTuwd.js} +1 -1
  8. package/dist/assets/clone-7bHB6YkC.js +1 -0
  9. package/dist/assets/{createText-1719965b-ByfEqOF-.js → createText-1719965b-C1_HJcCc.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-CMEArkOa.js → edges-96097737-BU8qStzd.js} +1 -1
  15. package/dist/assets/{erDiagram-0228fc6a-Cf8mX2aj.js → erDiagram-0228fc6a-DNA1Fz2L.js} +1 -1
  16. package/dist/assets/{flowDb-c6c81e3f-DG6WKyo7.js → flowDb-c6c81e3f-DjiCStMN.js} +1 -1
  17. package/dist/assets/{flowDiagram-50d868cf-CstUxz-w.js → flowDiagram-50d868cf-CSDi0-RD.js} +1 -1
  18. package/dist/assets/flowDiagram-v2-4f6560a1-_13Sz5Wh.js +1 -0
  19. package/dist/assets/{flowchart-elk-definition-6af322e1--4CRoQ-H.js → flowchart-elk-definition-6af322e1-DrhIMas7.js} +1 -1
  20. package/dist/assets/{ganttDiagram-a2739b55-DYgHcKd-.js → ganttDiagram-a2739b55-CTZnUP5z.js} +1 -1
  21. package/dist/assets/{gitGraphDiagram-82fe8481-DDSVpfsd.js → gitGraphDiagram-82fe8481-COOW7jTi.js} +1 -1
  22. package/dist/assets/{graph-CRWF39gX.js → graph-CIkpD4Kx.js} +1 -1
  23. package/dist/assets/{index-5325376f-W1hft795.js → index-5325376f-aVVRRTIu.js} +1 -1
  24. package/dist/assets/index-D1giUI7r.css +1 -0
  25. package/dist/assets/index-DRSI_ZIL.js +514 -0
  26. package/dist/assets/{infoDiagram-8eee0895-D4SHcix6.js → infoDiagram-8eee0895-DQpZ1LVD.js} +1 -1
  27. package/dist/assets/{journeyDiagram-c64418c1-MWgCkVoE.js → journeyDiagram-c64418c1-DoKguIuk.js} +1 -1
  28. package/dist/assets/{layout-C88ObkCf.js → layout-Tnmha8Nh.js} +1 -1
  29. package/dist/assets/{line-C7WAYMt5.js → line-BQR2SOyl.js} +1 -1
  30. package/dist/assets/{linear-C4msxfcU.js → linear-DlG0eemV.js} +1 -1
  31. package/dist/assets/{mermaid.core-Cabag9SZ.js → mermaid.core-BnwYO0He.js} +6 -6
  32. package/dist/assets/{mindmap-definition-8da855dc-CeS8ETXx.js → mindmap-definition-8da855dc-BllYwDID.js} +1 -1
  33. package/dist/assets/{pieDiagram-a8764435-BvjyKnq5.js → pieDiagram-a8764435-DwCkhPVc.js} +1 -1
  34. package/dist/assets/{quadrantDiagram-1e28029f-DzYvpbNM.js → quadrantDiagram-1e28029f-c40GKTU0.js} +1 -1
  35. package/dist/assets/{requirementDiagram-08caed73-DHIoDbyo.js → requirementDiagram-08caed73-DnQp2Tk6.js} +1 -1
  36. package/dist/assets/{sankeyDiagram-a04cb91d-BFSGnQGs.js → sankeyDiagram-a04cb91d-CnJrs13b.js} +1 -1
  37. package/dist/assets/{sequenceDiagram-c5b8d532-_LM3BJ5-.js → sequenceDiagram-c5b8d532-1YBwnpKu.js} +1 -1
  38. package/dist/assets/{stateDiagram-1ecb1508-DwORjOzl.js → stateDiagram-1ecb1508-BFBxQ6Fh.js} +1 -1
  39. package/dist/assets/{stateDiagram-v2-c2b004d7-B4cAWWz1.js → stateDiagram-v2-c2b004d7-Dmechvv2.js} +1 -1
  40. package/dist/assets/{styles-b4e223ce-D_rmV3B_.js → styles-b4e223ce-DWWfWX8O.js} +1 -1
  41. package/dist/assets/{styles-ca3715f6-BFx4VuFc.js → styles-ca3715f6-CKKvZxaU.js} +1 -1
  42. package/dist/assets/{styles-d45a18b0-BE3106vL.js → styles-d45a18b0-dKMOUh9p.js} +1 -1
  43. package/dist/assets/{svgDrawCommon-b86b1483-DwDTO1op.js → svgDrawCommon-b86b1483-CBgjChPM.js} +1 -1
  44. package/dist/assets/{timeline-definition-faaaa080-C4b8qUQZ.js → timeline-definition-faaaa080-NCt-HHmb.js} +1 -1
  45. package/dist/assets/{xychartDiagram-f5964ef8-BRJ9Z4u-.js → xychartDiagram-f5964ef8-BJhXS4dG.js} +1 -1
  46. package/dist/index.html +2 -7
  47. package/index.html +0 -5
  48. package/package.json +11 -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 +82 -0
  58. package/src/api/types.ts +20 -0
  59. package/src/api.ts +44 -241
  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 +37 -29
  74. package/src/components/{chat/CodeBlock.tsx → CodeBlock.tsx} +3 -1
  75. package/src/components/ConfigView.tsx +13 -1
  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 +89 -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/NewSessionGuide.scss +35 -13
  86. package/src/components/chat/NewSessionGuide.tsx +20 -10
  87. package/src/components/chat/{Sender.scss → Sender/Sender.scss} +80 -0
  88. package/src/components/chat/{Sender.tsx → Sender/Sender.tsx} +161 -5
  89. package/src/components/chat/tools/DefaultTool.tsx +184 -21
  90. package/src/components/chat/tools/adapter-claude/BashTool.scss +67 -51
  91. package/src/components/chat/tools/adapter-claude/BashTool.tsx +83 -49
  92. package/src/components/chat/tools/adapter-claude/GlobTool.scss +0 -79
  93. package/src/components/chat/tools/adapter-claude/GlobTool.tsx +16 -36
  94. package/src/components/chat/tools/adapter-claude/GrepTool.scss +0 -87
  95. package/src/components/chat/tools/adapter-claude/GrepTool.tsx +22 -41
  96. package/src/components/chat/tools/adapter-claude/LSTool.scss +0 -79
  97. package/src/components/chat/tools/adapter-claude/LSTool.tsx +15 -15
  98. package/src/components/chat/tools/adapter-claude/ReadTool.scss +0 -55
  99. package/src/components/chat/tools/adapter-claude/ReadTool.tsx +20 -42
  100. package/src/components/chat/tools/adapter-claude/TodoTool.scss +8 -23
  101. package/src/components/chat/tools/adapter-claude/TodoTool.tsx +24 -11
  102. package/src/components/chat/tools/adapter-claude/WriteTool.scss +21 -69
  103. package/src/components/chat/tools/adapter-claude/WriteTool.tsx +22 -58
  104. package/src/components/chat/tools/adapter-claude/index.ts +4 -10
  105. package/src/components/chat/tools/adapter-claude/utils.ts +54 -0
  106. package/src/components/chat/tools/core/ToolCallBox.scss +356 -0
  107. package/src/components/chat/{ToolGroup.tsx → tools/core/ToolGroup.tsx} +26 -7
  108. package/src/components/chat/{ToolRenderer.tsx → tools/core/ToolRenderer.tsx} +6 -4
  109. package/src/components/chat/tools/plugin-chrome-devtools/ChromeDevtoolsTool.scss +11 -0
  110. package/src/components/chat/tools/plugin-chrome-devtools/ChromeDevtoolsTool.tsx +75 -0
  111. package/src/components/chat/tools/plugin-chrome-devtools/index.ts +45 -0
  112. package/src/components/chat/tools/task/GetTaskInfoTool.scss +2 -27
  113. package/src/components/chat/tools/task/GetTaskInfoTool.tsx +48 -38
  114. package/src/components/chat/tools/task/ListTasksTool.scss +3 -28
  115. package/src/components/chat/tools/task/ListTasksTool.tsx +11 -8
  116. package/src/components/chat/tools/task/StartTasksTool.scss +3 -28
  117. package/src/components/chat/tools/task/StartTasksTool.tsx +14 -17
  118. package/src/components/chat/tools/task/components/TaskRow.scss +105 -0
  119. package/src/components/chat/tools/task/components/TaskRow.tsx +163 -0
  120. package/src/components/chat/tools/task/components/TaskToolCard.scss +15 -15
  121. package/src/components/chat/tools/task/components/TaskToolCard.tsx +8 -6
  122. package/src/components/config/AppSettingsPanel.tsx +33 -0
  123. package/src/components/config/ConfigSectionForm.tsx +12 -1
  124. package/src/components/config/channelDefinitions.ts +6 -0
  125. package/src/components/config/configSchema.ts +10 -1
  126. package/src/components/config/recordEditors/ChannelRecordEditor.scss +1 -0
  127. package/src/components/config/recordEditors/ChannelRecordEditor.tsx +397 -0
  128. package/src/components/config/recordEditors/index.tsx +1 -0
  129. package/src/components/knowledge-base/KnowledgeBaseView.tsx +51 -3
  130. package/src/components/knowledge-base/components/RuleItem.tsx +79 -0
  131. package/src/components/knowledge-base/components/RuleList.scss +5 -0
  132. package/src/components/knowledge-base/components/RuleList.tsx +70 -0
  133. package/src/components/knowledge-base/components/RulesTab.tsx +32 -7
  134. package/src/components/knowledge-base/components/SpecItem.tsx +1 -1
  135. package/src/hooks/chat/use-chat-interaction.ts +26 -0
  136. package/src/{components/chat/useChatModels.tsx → hooks/chat/use-chat-models.tsx} +65 -16
  137. package/src/hooks/chat/use-chat-permission-mode.ts +47 -0
  138. package/src/hooks/chat/use-chat-scroll.ts +51 -0
  139. package/src/hooks/chat/use-chat-session-actions.ts +147 -0
  140. package/src/hooks/chat/use-chat-session-messages.ts +250 -0
  141. package/src/hooks/chat/use-chat-session.ts +57 -0
  142. package/src/hooks/chat/use-chat-view.ts +39 -0
  143. package/src/main.tsx +10 -13
  144. package/src/resources/locales/en.json +73 -0
  145. package/src/resources/locales/zh.json +73 -0
  146. package/src/runtime-config.ts +52 -0
  147. package/src/store/index.ts +2 -0
  148. package/src/vite-env.d.ts +11 -0
  149. package/src/ws.ts +5 -3
  150. package/vite.config.ts +12 -4
  151. package/dist/assets/channel-DrWdSpqV.js +0 -1
  152. package/dist/assets/clone-D0cC8LLB.js +0 -1
  153. package/dist/assets/flowDiagram-v2-4f6560a1-Bf_DH7dp.js +0 -1
  154. package/dist/assets/index-CNMzWvKV.js +0 -497
  155. package/dist/assets/index-PEmISxiy.css +0 -1
  156. package/src/components/chat/ToolCallBox.scss +0 -137
  157. package/src/components/chat/useChatSession.ts +0 -370
  158. /package/src/components/{chat/CodeBlock.scss → CodeBlock.scss} +0 -0
  159. /package/src/components/chat/{MessageFooter.tsx → Messages/MessageFooter.tsx} +0 -0
  160. /package/src/components/chat/{CompletionMenu.scss → Sender/CompletionMenu.scss} +0 -0
  161. /package/src/components/chat/{CompletionMenu.tsx → Sender/CompletionMenu.tsx} +0 -0
  162. /package/src/components/chat/{ThinkingStatus.scss → Sender/ThinkingStatus.scss} +0 -0
  163. /package/src/components/chat/{ThinkingStatus.tsx → Sender/ThinkingStatus.tsx} +0 -0
  164. /package/src/components/chat/{ToolCallBox.tsx → tools/core/ToolCallBox.tsx} +0 -0
  165. /package/src/components/chat/{ToolGroup.scss → tools/core/ToolGroup.scss} +0 -0
  166. /package/src/{components/chat/safeSerialize.ts → utils/safe-serialize.ts} +0 -0
@@ -0,0 +1,79 @@
1
+ import { Button, Empty, Tag, Tooltip } from 'antd'
2
+ import React from 'react'
3
+ import { useTranslation } from 'react-i18next'
4
+ import useSWR from 'swr'
5
+
6
+ import type { RuleDetail, RuleSummary } from '#~/api.js'
7
+ import { getRuleDetail } from '#~/api.js'
8
+ import { MarkdownContent } from '#~/components/MarkdownContent'
9
+ import { LoadingState } from './LoadingState'
10
+
11
+ type RuleItemProps = {
12
+ rule: RuleSummary
13
+ }
14
+
15
+ export function RuleItem({ rule }: RuleItemProps) {
16
+ const { t } = useTranslation()
17
+ const [expanded, setExpanded] = React.useState(false)
18
+ const { data, isLoading } = useSWR<{ rule: RuleDetail }>(
19
+ expanded ? ['rule-detail', rule.id] : null,
20
+ () => getRuleDetail(rule.id)
21
+ )
22
+ const detail = data?.rule
23
+ const body = detail?.body ?? ''
24
+ const globList = rule.globs ?? []
25
+
26
+ return (
27
+ <div className='knowledge-base-view__item'>
28
+ <div className='knowledge-base-view__item-row'>
29
+ <div className='knowledge-base-view__item-main'>
30
+ <div className='knowledge-base-view__item-title'>
31
+ <span className='material-symbols-rounded knowledge-base-view__item-icon'>gavel</span>
32
+ <span>{rule.name}</span>
33
+ </div>
34
+ <div className='knowledge-base-view__item-desc'>{rule.description}</div>
35
+ {globList.length > 0 && (
36
+ <div className='knowledge-base-view__tag-list'>
37
+ {globList.map(glob => (
38
+ <Tag key={glob} className='knowledge-base-view__tag'>
39
+ <span className='material-symbols-rounded knowledge-base-view__tag-icon'>folder</span>
40
+ <span>{glob}</span>
41
+ </Tag>
42
+ ))}
43
+ </div>
44
+ )}
45
+ </div>
46
+ <div className='knowledge-base-view__item-meta'>
47
+ {rule.always && (
48
+ <Tag color='blue'>{t('knowledge.meta.always')}</Tag>
49
+ )}
50
+ <Tooltip title={expanded ? t('knowledge.actions.collapse') : t('knowledge.actions.expand')}>
51
+ <Button
52
+ type='text'
53
+ className='knowledge-base-view__icon-button'
54
+ onClick={() => setExpanded(prev => !prev)}
55
+ icon={
56
+ <span className='material-symbols-rounded'>
57
+ {expanded ? 'expand_less' : 'expand_more'}
58
+ </span>
59
+ }
60
+ />
61
+ </Tooltip>
62
+ </div>
63
+ </div>
64
+ {expanded && (
65
+ <div className='knowledge-base-view__detail'>
66
+ {isLoading && <LoadingState />}
67
+ {!isLoading && body.trim() === '' && (
68
+ <Empty image={Empty.PRESENTED_IMAGE_SIMPLE} description={t('knowledge.rules.empty')} />
69
+ )}
70
+ {!isLoading && body.trim() !== '' && (
71
+ <div className='knowledge-base-view__markdown'>
72
+ <MarkdownContent content={body} />
73
+ </div>
74
+ )}
75
+ </div>
76
+ )}
77
+ </div>
78
+ )
79
+ }
@@ -0,0 +1,5 @@
1
+ .knowledge-base-view__rule-list {
2
+ display: flex;
3
+ flex-direction: column;
4
+ gap: 12px;
5
+ }
@@ -0,0 +1,70 @@
1
+ import './RuleList.scss'
2
+
3
+ import { List } from 'antd'
4
+ import { useTranslation } from 'react-i18next'
5
+
6
+ import type { RuleSummary } from '#~/api.js'
7
+ import { EmptyState } from './EmptyState'
8
+ import { KnowledgeList } from './KnowledgeList'
9
+ import { LoadingState } from './LoadingState'
10
+ import { RuleItem } from './RuleItem'
11
+
12
+ type RuleListProps = {
13
+ isLoading: boolean
14
+ rules: RuleSummary[]
15
+ filteredRules: RuleSummary[]
16
+ onCreate: () => void
17
+ }
18
+
19
+ export function RuleList({
20
+ isLoading,
21
+ rules,
22
+ filteredRules,
23
+ onCreate
24
+ }: RuleListProps) {
25
+ const { t } = useTranslation()
26
+
27
+ if (isLoading) {
28
+ return (
29
+ <div className='knowledge-base-view__rule-list'>
30
+ <LoadingState />
31
+ </div>
32
+ )
33
+ }
34
+
35
+ if (rules.length === 0) {
36
+ return (
37
+ <div className='knowledge-base-view__rule-list'>
38
+ <EmptyState
39
+ description={t('knowledge.rules.empty')}
40
+ actionLabel={t('knowledge.rules.create')}
41
+ onAction={onCreate}
42
+ />
43
+ </div>
44
+ )
45
+ }
46
+
47
+ if (filteredRules.length === 0) {
48
+ return (
49
+ <div className='knowledge-base-view__rule-list'>
50
+ <EmptyState
51
+ description={t('knowledge.filters.noResults')}
52
+ variant='simple'
53
+ />
54
+ </div>
55
+ )
56
+ }
57
+
58
+ return (
59
+ <div className='knowledge-base-view__rule-list'>
60
+ <KnowledgeList
61
+ data={filteredRules}
62
+ renderItem={(rule) => (
63
+ <List.Item className='knowledge-base-view__list-item'>
64
+ <RuleItem rule={rule} />
65
+ </List.Item>
66
+ )}
67
+ />
68
+ </div>
69
+ )
70
+ }
@@ -1,19 +1,33 @@
1
1
  import './RulesTab.scss'
2
2
 
3
- import { Space } from 'antd'
3
+ import { Input, Space } from 'antd'
4
4
  import { useTranslation } from 'react-i18next'
5
5
 
6
+ import type { RuleSummary } from '#~/api.js'
6
7
  import { ActionButton } from './ActionButton'
7
- import { EmptyState } from './EmptyState'
8
+ import { RuleList } from './RuleList'
8
9
  import { SectionHeader } from './SectionHeader'
9
10
  import { TabContent } from './TabContent'
10
11
 
11
12
  type RulesTabProps = {
13
+ rules: RuleSummary[]
14
+ filteredRules: RuleSummary[]
15
+ isLoading: boolean
16
+ query: string
17
+ onQueryChange: (value: string) => void
12
18
  onCreate: () => void
13
19
  onImport: () => void
14
20
  }
15
21
 
16
- export function RulesTab({ onCreate, onImport }: RulesTabProps) {
22
+ export function RulesTab({
23
+ rules,
24
+ filteredRules,
25
+ isLoading,
26
+ query,
27
+ onQueryChange,
28
+ onCreate,
29
+ onImport
30
+ }: RulesTabProps) {
17
31
  const { t } = useTranslation()
18
32
 
19
33
  return (
@@ -39,10 +53,21 @@ export function RulesTab({ onCreate, onImport }: RulesTabProps) {
39
53
  </Space>
40
54
  )}
41
55
  />
42
- <EmptyState
43
- description={t('knowledge.rules.empty')}
44
- actionLabel={t('knowledge.rules.create')}
45
- onAction={onCreate}
56
+ <div className='knowledge-base-view__filters'>
57
+ <Input
58
+ className='knowledge-base-view__filter-input'
59
+ prefix={<span className='material-symbols-rounded knowledge-base-view__filter-icon'>search</span>}
60
+ placeholder={t('knowledge.filters.search')}
61
+ allowClear
62
+ value={query}
63
+ onChange={(e) => onQueryChange(e.target.value)}
64
+ />
65
+ </div>
66
+ <RuleList
67
+ isLoading={isLoading}
68
+ rules={rules}
69
+ filteredRules={filteredRules}
70
+ onCreate={onCreate}
46
71
  />
47
72
  </TabContent>
48
73
  )
@@ -7,7 +7,7 @@ import useSWR from 'swr'
7
7
 
8
8
  import type { SpecDetail, SpecSummary } from '#~/api.js'
9
9
  import { getSpecDetail } from '#~/api.js'
10
- import { MarkdownContent } from '#~/components/chat/MarkdownContent'
10
+ import { MarkdownContent } from '#~/components/MarkdownContent'
11
11
  import { LoadingState } from './LoadingState'
12
12
  import { MetaList } from './MetaList'
13
13
 
@@ -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,24 +1,31 @@
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
5
  import type { ConfigResponse, ModelServiceConfig, RecommendedModelConfig } from '@vibe-forge/core'
6
- import { getConfig } from '../../api'
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
19
  export function useChatModels() {
20
20
  const { t } = useTranslation()
21
- const [selectedModel, setSelectedModel] = useState<string | undefined>(undefined)
21
+ const [selectedModel, setSelectedModel] = useState<string | undefined>(() => {
22
+ try {
23
+ const raw = localStorage.getItem('vf_chat_selected_model')
24
+ return raw == null || raw.trim() === '' ? undefined : raw
25
+ } catch {
26
+ return undefined
27
+ }
28
+ })
22
29
  const { data: configRes } = useSWR<ConfigResponse>('/api/config', getConfig)
23
30
 
24
31
  const mergedModelServices = useMemo(() => {
@@ -39,7 +46,9 @@ export function useChatModels() {
39
46
  const availableModels = useMemo(() => {
40
47
  const list: Array<{ model: string; serviceKey: string; serviceTitle: string }> = []
41
48
  for (const [serviceKey, serviceValue] of modelServiceEntries) {
42
- const service = (serviceValue != null && typeof serviceValue === 'object') ? serviceValue as ModelServiceConfig : undefined
49
+ const service = (serviceValue != null && typeof serviceValue === 'object')
50
+ ? serviceValue as ModelServiceConfig
51
+ : undefined
43
52
  const serviceTitle = service?.title?.trim() !== '' ? service?.title ?? '' : serviceKey
44
53
  const models = Array.isArray(service?.models) ? service?.models.filter(item => typeof item === 'string') : []
45
54
  for (const model of models) {
@@ -53,8 +62,24 @@ export function useChatModels() {
53
62
  const availableModelKey = useMemo(() => availableModelValues.join('|'), [availableModelValues])
54
63
  const availableModelSet = useMemo(() => new Set(availableModelValues), [availableModelKey])
55
64
  const hasAvailableModels = availableModelValues.length > 0
65
+ const modelToService = useMemo(() => {
66
+ const map = new Map<string, { key: string; title: string }>()
67
+ for (const entry of availableModels) {
68
+ if (!map.has(entry.model)) {
69
+ map.set(entry.model, { key: entry.serviceKey, title: entry.serviceTitle })
70
+ }
71
+ }
72
+ return map
73
+ }, [availableModels])
56
74
  const defaultModelService = configRes?.sources?.merged?.general?.defaultModelService
57
75
  const defaultModel = configRes?.sources?.merged?.general?.defaultModel
76
+ const formatModelWithService = useCallback((model: string | undefined) => {
77
+ const normalizedModel = typeof model === 'string' ? model.trim() : ''
78
+ if (normalizedModel === '') return undefined
79
+ if (normalizedModel.includes(',')) return normalizedModel
80
+ const resolvedService = modelToService.get(normalizedModel)?.key ?? defaultModelService
81
+ return resolvedService ? `${resolvedService},${normalizedModel}` : normalizedModel
82
+ }, [defaultModelService, modelToService])
58
83
  const resolvedDefaultModel = useMemo(() => {
59
84
  if (!hasAvailableModels) return undefined
60
85
  if (defaultModel && availableModelSet.has(defaultModel)) return defaultModel
@@ -64,7 +89,17 @@ export function useChatModels() {
64
89
  if (models.length > 0) return models[0]
65
90
  }
66
91
  return availableModelValues[0]
67
- }, [availableModelSet, availableModelValues, defaultModel, defaultModelService, hasAvailableModels, mergedModelServices])
92
+ }, [
93
+ availableModelSet,
94
+ availableModelValues,
95
+ defaultModel,
96
+ defaultModelService,
97
+ hasAvailableModels,
98
+ mergedModelServices
99
+ ])
100
+ const selectedModelWithService = useMemo(() => (
101
+ formatModelWithService(selectedModel)
102
+ ), [formatModelWithService, selectedModel])
68
103
 
69
104
  useEffect(() => {
70
105
  if (!hasAvailableModels) {
@@ -77,6 +112,17 @@ export function useChatModels() {
77
112
  })
78
113
  }, [availableModelSet, hasAvailableModels, resolvedDefaultModel])
79
114
 
115
+ useEffect(() => {
116
+ try {
117
+ if (selectedModel == null || selectedModel.trim() === '') {
118
+ localStorage.removeItem('vf_chat_selected_model')
119
+ } else {
120
+ localStorage.setItem('vf_chat_selected_model', selectedModel)
121
+ }
122
+ } catch {
123
+ }
124
+ }, [selectedModel])
125
+
80
126
  const modelOptions = useMemo<ModelSelectGroup[]>(() => {
81
127
  const buildOption = (params: {
82
128
  value: string
@@ -108,13 +154,6 @@ export function useChatModels() {
108
154
  }
109
155
  }
110
156
 
111
- const modelToService = new Map<string, { key: string; title: string }>()
112
- for (const entry of availableModels) {
113
- if (!modelToService.has(entry.model)) {
114
- modelToService.set(entry.model, { key: entry.serviceKey, title: entry.serviceTitle })
115
- }
116
- }
117
-
118
157
  const resolveFirstAlias = (modelsAlias: Record<string, string[]> | undefined, model: string) => {
119
158
  if (!modelsAlias) return undefined
120
159
  for (const [alias, aliasModels] of Object.entries(modelsAlias)) {
@@ -126,7 +165,9 @@ export function useChatModels() {
126
165
 
127
166
  const serviceGroups = modelServiceEntries
128
167
  .map(([serviceKey, serviceValue]) => {
129
- const service = (serviceValue != null && typeof serviceValue === 'object') ? serviceValue as ModelServiceConfig : undefined
168
+ const service = (serviceValue != null && typeof serviceValue === 'object')
169
+ ? serviceValue as ModelServiceConfig
170
+ : undefined
130
171
  const serviceTitle = service?.title?.trim() !== '' ? service?.title ?? '' : serviceKey
131
172
  const groupTitle = serviceTitle?.trim() !== '' ? serviceTitle : serviceKey
132
173
  const serviceDescription = service?.description
@@ -195,10 +236,18 @@ export function useChatModels() {
195
236
  })
196
237
  }
197
238
  return [...groups, ...serviceGroups]
198
- }, [availableModelSet, availableModels, mergedModelServices, modelServiceEntries, recommendedModels, t])
239
+ }, [
240
+ availableModelSet,
241
+ modelToService,
242
+ mergedModelServices,
243
+ modelServiceEntries,
244
+ recommendedModels,
245
+ t
246
+ ])
199
247
 
200
248
  return {
201
249
  selectedModel,
250
+ selectedModelWithService,
202
251
  setSelectedModel,
203
252
  modelOptions,
204
253
  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,147 @@
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 type { ChatMessageContent, Session } from '@vibe-forge/core'
8
+ import { createSession } from '#~/api.js'
9
+ import { connectionManager } from '#~/connectionManager.js'
10
+ import type { PermissionMode } from './use-chat-permission-mode'
11
+
12
+ export function useChatSessionActions({
13
+ session,
14
+ modelForQuery,
15
+ hasAvailableModels,
16
+ permissionMode,
17
+ onClearMessages
18
+ }: {
19
+ session?: Session
20
+ modelForQuery?: string
21
+ hasAvailableModels: boolean
22
+ permissionMode: PermissionMode
23
+ onClearMessages: () => void
24
+ }) {
25
+ const { message } = App.useApp()
26
+ const { t } = useTranslation()
27
+ const navigate = useNavigate()
28
+ const { mutate } = useSWRConfig()
29
+ const [isCreating, setIsCreating] = useState(false)
30
+ const isThinking = isCreating || session?.status === 'running'
31
+
32
+ const send = useCallback(async (text: string) => {
33
+ if (text.trim() === '' || isThinking) return
34
+ if (!hasAvailableModels) {
35
+ void message.warning(t('chat.modelConfigRequired'))
36
+ return
37
+ }
38
+
39
+ if (!session?.id) {
40
+ setIsCreating(true)
41
+ try {
42
+ const { session: newSession } = await createSession(undefined, text.trim(), undefined, modelForQuery, {
43
+ permissionMode
44
+ })
45
+
46
+ await mutate('/api/sessions', (prev: { sessions: Session[] } | undefined) => {
47
+ if (!prev?.sessions) return { sessions: [newSession] }
48
+ return {
49
+ ...prev,
50
+ sessions: [newSession, ...prev.sessions]
51
+ }
52
+ }, false)
53
+
54
+ void navigate(`/session/${newSession.id}`)
55
+ } catch (err) {
56
+ console.error(err)
57
+ setIsCreating(false)
58
+ void message.error('Failed to create session')
59
+ }
60
+ return
61
+ }
62
+
63
+ connectionManager.send(session.id, {
64
+ type: 'user_message',
65
+ text: text.trim()
66
+ })
67
+ }, [
68
+ hasAvailableModels,
69
+ isThinking,
70
+ message,
71
+ mutate,
72
+ navigate,
73
+ permissionMode,
74
+ modelForQuery,
75
+ session?.id,
76
+ t
77
+ ])
78
+
79
+ const sendContent = useCallback(async (content: ChatMessageContent[]) => {
80
+ if (content.length === 0 || isThinking) return
81
+ if (!hasAvailableModels) {
82
+ void message.warning(t('chat.modelConfigRequired'))
83
+ return
84
+ }
85
+
86
+ if (!session?.id) {
87
+ setIsCreating(true)
88
+ try {
89
+ const { session: newSession } = await createSession(undefined, undefined, content, modelForQuery, {
90
+ permissionMode
91
+ })
92
+
93
+ await mutate('/api/sessions', (prev: { sessions: Session[] } | undefined) => {
94
+ if (!prev?.sessions) return { sessions: [newSession] }
95
+ return {
96
+ ...prev,
97
+ sessions: [newSession, ...prev.sessions]
98
+ }
99
+ }, false)
100
+
101
+ void navigate(`/session/${newSession.id}`)
102
+ setIsCreating(false)
103
+ } catch (err) {
104
+ console.error(err)
105
+ setIsCreating(false)
106
+ void message.error('Failed to create session')
107
+ }
108
+ return
109
+ }
110
+
111
+ connectionManager.send(session.id, {
112
+ type: 'user_message',
113
+ content
114
+ })
115
+ }, [
116
+ hasAvailableModels,
117
+ isThinking,
118
+ message,
119
+ mutate,
120
+ navigate,
121
+ permissionMode,
122
+ modelForQuery,
123
+ session?.id,
124
+ t
125
+ ])
126
+
127
+ const interrupt = useCallback(() => {
128
+ if (!session?.id || isThinking === false) return
129
+ connectionManager.send(session.id, {
130
+ type: 'interrupt'
131
+ })
132
+ }, [isThinking, session?.id])
133
+
134
+ const clearMessages = useCallback(() => {
135
+ onClearMessages()
136
+ void message.success('Messages cleared')
137
+ }, [message, onClearMessages])
138
+
139
+ return {
140
+ isCreating,
141
+ isThinking,
142
+ send,
143
+ sendContent,
144
+ interrupt,
145
+ clearMessages
146
+ }
147
+ }