@vibe-forge/client 0.5.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 (134) hide show
  1. package/AGENTS.md +37 -0
  2. package/dist/assets/{arc-C4ymrcSQ.js → arc-CMAHd5G3.js} +1 -1
  3. package/dist/assets/{blockDiagram-c4efeb88-CeB7-kgP.js → blockDiagram-c4efeb88-DKww-VCP.js} +1 -1
  4. package/dist/assets/{c4Diagram-c83219d4-C935Im8S.js → c4Diagram-c83219d4-DKrjVHyY.js} +1 -1
  5. package/dist/assets/channel-Bi4g8rj9.js +1 -0
  6. package/dist/assets/{classDiagram-beda092f-B9IV13KI.js → classDiagram-beda092f-BXx5rdo3.js} +1 -1
  7. package/dist/assets/{classDiagram-v2-2358418a-CXF_K4fE.js → classDiagram-v2-2358418a-CnR3WLsr.js} +1 -1
  8. package/dist/assets/clone-DPrpP2ky.js +1 -0
  9. package/dist/assets/{createText-1719965b-DwX8iC5F.js → createText-1719965b-CmOsl1W7.js} +1 -1
  10. package/dist/assets/{edges-96097737-9P1uH1RE.js → edges-96097737-CQeQgpjD.js} +1 -1
  11. package/dist/assets/{erDiagram-0228fc6a-ixeGTFvg.js → erDiagram-0228fc6a-ZUNB-ucF.js} +1 -1
  12. package/dist/assets/{flowDb-c6c81e3f-G1gSTTBI.js → flowDb-c6c81e3f-DuuKeSLX.js} +1 -1
  13. package/dist/assets/{flowDiagram-50d868cf-CzrG99nD.js → flowDiagram-50d868cf-Bc6n85yR.js} +1 -1
  14. package/dist/assets/flowDiagram-v2-4f6560a1-BZqaeqoh.js +1 -0
  15. package/dist/assets/{flowchart-elk-definition-6af322e1-sFCoysWa.js → flowchart-elk-definition-6af322e1-cAG5afW9.js} +1 -1
  16. package/dist/assets/{ganttDiagram-a2739b55-Ccsk_Lru.js → ganttDiagram-a2739b55-Dp6xhY5I.js} +1 -1
  17. package/dist/assets/{gitGraphDiagram-82fe8481-CwathJ6H.js → gitGraphDiagram-82fe8481-MlIIRBdG.js} +1 -1
  18. package/dist/assets/{graph-DRCU-8Rz.js → graph-D7Es8jZ-.js} +1 -1
  19. package/dist/assets/{index-5325376f-Bq-fg2i_.js → index-5325376f-DC18ottv.js} +1 -1
  20. package/dist/assets/index-D37AbgPQ.js +545 -0
  21. package/dist/assets/{index-CHMuZ5-1.css → index-fcJ9v94I.css} +1 -1
  22. package/dist/assets/{infoDiagram-8eee0895-JBcUkJ6T.js → infoDiagram-8eee0895-CXk21kFp.js} +1 -1
  23. package/dist/assets/{journeyDiagram-c64418c1-DsdQU-R8.js → journeyDiagram-c64418c1-899BKBHL.js} +1 -1
  24. package/dist/assets/{layout-s0slG1OL.js → layout-DLaxdy48.js} +1 -1
  25. package/dist/assets/{line-CymFqgW6.js → line-_lw5YbRM.js} +1 -1
  26. package/dist/assets/{linear-lDQVZ6aQ.js → linear-D5iu84ui.js} +1 -1
  27. package/dist/assets/{mermaid.core-Cmlqga_E.js → mermaid.core-C6sW3GFM.js} +4 -4
  28. package/dist/assets/{mindmap-definition-8da855dc-CqqTDJn_.js → mindmap-definition-8da855dc-BS9Xy9KN.js} +1 -1
  29. package/dist/assets/{pieDiagram-a8764435-BL2Ajx7Z.js → pieDiagram-a8764435-DZt9cEgs.js} +1 -1
  30. package/dist/assets/{quadrantDiagram-1e28029f-ClL_3ASt.js → quadrantDiagram-1e28029f-BTIeHOgn.js} +1 -1
  31. package/dist/assets/{requirementDiagram-08caed73-CB1RgE3K.js → requirementDiagram-08caed73-BHJAKD2g.js} +1 -1
  32. package/dist/assets/{sankeyDiagram-a04cb91d-tgleEYiD.js → sankeyDiagram-a04cb91d-DnAkVOK8.js} +1 -1
  33. package/dist/assets/{sequenceDiagram-c5b8d532-DlatQT5R.js → sequenceDiagram-c5b8d532-92tE3oFv.js} +1 -1
  34. package/dist/assets/{stateDiagram-1ecb1508-B--MLqRs.js → stateDiagram-1ecb1508-DG0ObiMg.js} +1 -1
  35. package/dist/assets/{stateDiagram-v2-c2b004d7-CRMZ6Dpx.js → stateDiagram-v2-c2b004d7-BKoJx2ci.js} +1 -1
  36. package/dist/assets/{styles-b4e223ce-CPiYHfUz.js → styles-b4e223ce-Ba6G4ri9.js} +1 -1
  37. package/dist/assets/{styles-ca3715f6-B9UKPAzX.js → styles-ca3715f6-Bn6RIIVW.js} +1 -1
  38. package/dist/assets/{styles-d45a18b0-BC1Ak1So.js → styles-d45a18b0-_dELBUI6.js} +1 -1
  39. package/dist/assets/{svgDrawCommon-b86b1483-DV8R0g-n.js → svgDrawCommon-b86b1483-CRK-ZoIs.js} +1 -1
  40. package/dist/assets/{timeline-definition-faaaa080-CiqGS5DC.js → timeline-definition-faaaa080-DvQ_RA_i.js} +1 -1
  41. package/dist/assets/{xychartDiagram-f5964ef8-h6VSD3GE.js → xychartDiagram-f5964ef8-CJxeDLfg.js} +1 -1
  42. package/dist/index.html +2 -2
  43. package/package.json +10 -8
  44. package/src/App.tsx +20 -168
  45. package/src/api/base.ts +116 -7
  46. package/src/api.ts +3 -1
  47. package/src/components/ArchiveView.tsx +5 -5
  48. package/src/components/ConfigView.tsx +3 -3
  49. package/src/components/{AutomationView → automation-view}/index.tsx +10 -9
  50. package/src/components/{BenchmarkView → benchmark-view}/index.tsx +5 -4
  51. package/src/components/chat/ChatHeader.tsx +6 -6
  52. package/src/components/chat/ChatHistoryView.tsx +64 -16
  53. package/src/components/chat/ChatTimelineView.tsx +3 -3
  54. package/src/components/chat/CurrentTodoList.scss +56 -27
  55. package/src/components/chat/{Sender → sender}/Sender.scss +201 -71
  56. package/src/components/chat/{Sender → sender}/Sender.tsx +104 -42
  57. package/src/components/chat/tools/core/ToolGroup.tsx +1 -1
  58. package/src/components/config/ConfigSectionForm.tsx +1 -1
  59. package/src/components/layout/AppShell.scss +19 -0
  60. package/src/components/layout/AppShell.tsx +45 -0
  61. package/src/hooks/chat/model-selector.ts +150 -0
  62. package/src/hooks/chat/use-chat-adapter.ts +20 -8
  63. package/src/hooks/chat/use-chat-models.tsx +79 -74
  64. package/src/hooks/chat/use-chat-permission-mode.ts +14 -10
  65. package/src/hooks/chat/use-chat-session-actions.ts +13 -10
  66. package/src/hooks/chat/use-chat-session-messages.ts +46 -6
  67. package/src/hooks/chat/use-chat-session.ts +42 -1
  68. package/src/hooks/use-app-preferences.ts +41 -0
  69. package/src/hooks/use-session-subscription.ts +101 -0
  70. package/src/hooks/use-sidebar-navigation.ts +35 -0
  71. package/src/resources/locales/en.json +6 -0
  72. package/src/resources/locales/zh.json +6 -0
  73. package/src/routes/AppRoutes.tsx +22 -0
  74. package/src/routes/ArchiveRoute.tsx +5 -0
  75. package/src/routes/AutomationRoute.tsx +5 -0
  76. package/src/routes/BenchmarkRoute.tsx +5 -0
  77. package/src/{components/Chat.scss → routes/ChatRoute.scss} +35 -0
  78. package/src/{components/Chat.tsx → routes/ChatRoute.tsx} +54 -28
  79. package/src/routes/ConfigRoute.tsx +5 -0
  80. package/src/routes/KnowledgeRoute.tsx +5 -0
  81. package/dist/assets/channel-84s1ACzD.js +0 -1
  82. package/dist/assets/clone-B2E8tddE.js +0 -1
  83. package/dist/assets/flowDiagram-v2-4f6560a1-CJfJYbME.js +0 -1
  84. package/dist/assets/index-cGZvDhhU.js +0 -542
  85. /package/src/components/{AutomationView → automation-view}/RuleFormPanel.scss +0 -0
  86. /package/src/components/{AutomationView → automation-view}/RuleFormPanel.tsx +0 -0
  87. /package/src/components/{AutomationView → automation-view}/RuleSidebar.scss +0 -0
  88. /package/src/components/{AutomationView → automation-view}/RuleSidebar.tsx +0 -0
  89. /package/src/components/{AutomationView → automation-view}/RunHistoryPanel.scss +0 -0
  90. /package/src/components/{AutomationView → automation-view}/RunHistoryPanel.tsx +0 -0
  91. /package/src/components/{AutomationView → automation-view}/TaskList.scss +0 -0
  92. /package/src/components/{AutomationView → automation-view}/TaskList.tsx +0 -0
  93. /package/src/components/{AutomationView → automation-view}/TriggerList.scss +0 -0
  94. /package/src/components/{AutomationView → automation-view}/TriggerList.tsx +0 -0
  95. /package/src/components/{AutomationView/AutomationView.scss → automation-view/index.scss} +0 -0
  96. /package/src/components/{AutomationView → automation-view}/types.ts +0 -0
  97. /package/src/components/{BenchmarkView → benchmark-view}/BenchmarkCasePanel.scss +0 -0
  98. /package/src/components/{BenchmarkView → benchmark-view}/BenchmarkCasePanel.tsx +0 -0
  99. /package/src/components/{BenchmarkView → benchmark-view}/BenchmarkSidebar.scss +0 -0
  100. /package/src/components/{BenchmarkView → benchmark-view}/BenchmarkSidebar.tsx +0 -0
  101. /package/src/components/{BenchmarkView → benchmark-view}/BenchmarkView.scss +0 -0
  102. /package/src/components/{BenchmarkView → benchmark-view}/types.ts +0 -0
  103. /package/src/components/{BenchmarkView → benchmark-view}/utils.ts +0 -0
  104. /package/src/components/chat/{Messages → messages}/MessageFooter.tsx +0 -0
  105. /package/src/components/chat/{Messages → messages}/MessageItem.scss +0 -0
  106. /package/src/components/chat/{Messages → messages}/MessageItem.tsx +0 -0
  107. /package/src/components/chat/{Messages → messages}/message-utils.ts +0 -0
  108. /package/src/components/chat/{Sender → sender}/CompletionMenu.scss +0 -0
  109. /package/src/components/chat/{Sender → sender}/CompletionMenu.tsx +0 -0
  110. /package/src/components/chat/{Sender → sender}/ThinkingStatus.scss +0 -0
  111. /package/src/components/chat/{Sender → sender}/ThinkingStatus.tsx +0 -0
  112. /package/src/components/chat/{SessionTimelinePanel → session-timeline-panel}/EventList.scss +0 -0
  113. /package/src/components/chat/{SessionTimelinePanel → session-timeline-panel}/EventList.tsx +0 -0
  114. /package/src/components/chat/{SessionTimelinePanel → session-timeline-panel}/gantt.ts +0 -0
  115. /package/src/components/chat/{SessionTimelinePanel → session-timeline-panel}/git-graph.ts +0 -0
  116. /package/src/components/chat/{SessionTimelinePanel → session-timeline-panel}/index.scss +0 -0
  117. /package/src/components/chat/{SessionTimelinePanel → session-timeline-panel}/index.tsx +0 -0
  118. /package/src/components/chat/{SessionTimelinePanel → session-timeline-panel}/mermaid.ts +0 -0
  119. /package/src/components/chat/{SessionTimelinePanel → session-timeline-panel}/types.ts +0 -0
  120. /package/src/components/chat/{SessionTimelinePanel → session-timeline-panel}/utils.ts +0 -0
  121. /package/src/components/config/{recordEditors → record-editors}/BooleanRecordEditor.scss +0 -0
  122. /package/src/components/config/{recordEditors → record-editors}/BooleanRecordEditor.tsx +0 -0
  123. /package/src/components/config/{recordEditors → record-editors}/ChannelRecordEditor.scss +0 -0
  124. /package/src/components/config/{recordEditors → record-editors}/ChannelRecordEditor.tsx +0 -0
  125. /package/src/components/config/{recordEditors → record-editors}/KeyValueEditor.scss +0 -0
  126. /package/src/components/config/{recordEditors → record-editors}/KeyValueEditor.tsx +0 -0
  127. /package/src/components/config/{recordEditors → record-editors}/McpServersRecordEditor.scss +0 -0
  128. /package/src/components/config/{recordEditors → record-editors}/McpServersRecordEditor.tsx +0 -0
  129. /package/src/components/config/{recordEditors → record-editors}/ModelServicesRecordEditor.scss +0 -0
  130. /package/src/components/config/{recordEditors → record-editors}/ModelServicesRecordEditor.tsx +0 -0
  131. /package/src/components/config/{recordEditors → record-editors}/RecordEditors.scss +0 -0
  132. /package/src/components/config/{recordEditors → record-editors}/RecordJsonEditor.scss +0 -0
  133. /package/src/components/config/{recordEditors → record-editors}/RecordJsonEditor.tsx +0 -0
  134. /package/src/components/config/{recordEditors → record-editors}/index.tsx +0 -0
@@ -1,17 +1,17 @@
1
1
  import './Sender.scss'
2
2
 
3
- import { App, Button, Input, Select, Tooltip } from 'antd'
3
+ import { App, Button, Cascader, Input, Select, Tooltip } from 'antd'
4
4
  import type { TextAreaRef } from 'antd/es/input/TextArea'
5
- import React, { useEffect, useRef, useState } from 'react'
5
+ import React, { useRef, useState } from 'react'
6
6
  import { useTranslation } from 'react-i18next'
7
7
  import useSWR from 'swr'
8
8
 
9
+ import type { PermissionMode } from '#~/hooks/chat/use-chat-permission-mode'
9
10
  import type { AskUserQuestionParams, ChatMessageContent, SessionInfo, SessionStatus } from '@vibe-forge/core'
11
+ import { isShortcutMatch } from '../../../utils/shortcutUtils'
10
12
  import type { CompletionItem } from './CompletionMenu'
11
- import type { PermissionMode } from '#~/hooks/chat/use-chat-permission-mode'
12
13
  import { CompletionMenu } from './CompletionMenu'
13
14
  import { ThinkingStatus } from './ThinkingStatus'
14
- import { isShortcutMatch } from '../../../utils/shortcutUtils'
15
15
 
16
16
  const { TextArea } = Input
17
17
 
@@ -19,6 +19,7 @@ interface ModelSelectOption {
19
19
  value: string
20
20
  label: React.ReactNode
21
21
  searchText: string
22
+ displayLabel: string
22
23
  }
23
24
 
24
25
  interface ModelSelectGroup {
@@ -34,13 +35,37 @@ interface PendingImage {
34
35
  mimeType?: string
35
36
  }
36
37
 
38
+ interface SenderToolGroup {
39
+ key: 'chrome-devtools' | 'system'
40
+ label: string
41
+ tools: string[]
42
+ }
43
+
44
+ interface SenderToolOption {
45
+ value: string
46
+ label: React.ReactNode
47
+ children?: SenderToolOption[]
48
+ }
49
+
50
+ const formatToolLabel = (tool: string) => {
51
+ const parts = tool.split('__')
52
+ return parts[parts.length - 1] || tool
53
+ }
54
+
55
+ const getToolGroupIcon = (groupKey: SenderToolGroup['key']) => {
56
+ return groupKey === 'chrome-devtools' ? 'web_traffic' : 'memory'
57
+ }
58
+
37
59
  export function Sender({
38
60
  onSend,
39
61
  onSendContent,
62
+ adapterLocked = false,
40
63
  sessionStatus,
41
64
  onInterrupt,
42
65
  onClear,
43
66
  sessionInfo,
67
+ connectionError,
68
+ onRetryConnection,
44
69
  interactionRequest,
45
70
  onInteractionResponse,
46
71
  placeholder,
@@ -57,10 +82,13 @@ export function Sender({
57
82
  }: {
58
83
  onSend: (text: string) => void
59
84
  onSendContent: (content: ChatMessageContent[]) => void
85
+ adapterLocked?: boolean
60
86
  sessionStatus?: SessionStatus
61
87
  onInterrupt: () => void
62
88
  onClear?: () => void
63
89
  sessionInfo?: SessionInfo | null
90
+ connectionError?: string | null
91
+ onRetryConnection?: () => void
64
92
  interactionRequest?: { id: string; payload: AskUserQuestionParams } | null
65
93
  onInteractionResponse?: (id: string, data: string | string[]) => void
66
94
  placeholder?: string
@@ -85,7 +113,6 @@ export function Sender({
85
113
 
86
114
  const [showToolsList, setShowToolsList] = useState(false)
87
115
  const textareaRef = useRef<TextAreaRef>(null)
88
- const toolsRef = useRef<HTMLDivElement>(null)
89
116
  const fileInputRef = useRef<HTMLInputElement>(null)
90
117
  const isMac = navigator.platform.includes('Mac')
91
118
  const [pendingImages, setPendingImages] = useState<PendingImage[]>([])
@@ -107,16 +134,39 @@ export function Sender({
107
134
  : 'mod+enter'
108
135
 
109
136
  const isThinking = sessionStatus === 'running'
110
-
111
- useEffect(() => {
112
- const handleClickOutside = (event: MouseEvent) => {
113
- if (toolsRef.current && !toolsRef.current.contains(event.target as Node)) {
114
- setShowToolsList(false)
115
- }
116
- }
117
- document.addEventListener('mousedown', handleClickOutside)
118
- return () => document.removeEventListener('mousedown', handleClickOutside)
119
- }, [])
137
+ const groupedTools: SenderToolGroup[] = sessionInfo != null && sessionInfo.type === 'init'
138
+ ? [
139
+ {
140
+ key: 'chrome-devtools',
141
+ label: t('chat.toolGroupChromeDevtools'),
142
+ tools: sessionInfo.tools.filter(tool => tool.startsWith('mcp__ChromeDevtools__'))
143
+ },
144
+ {
145
+ key: 'system',
146
+ label: t('chat.toolGroupSystem'),
147
+ tools: sessionInfo.tools.filter(tool => !tool.startsWith('mcp__ChromeDevtools__'))
148
+ }
149
+ ].filter(group => group.tools.length > 0)
150
+ : []
151
+ const toolCascaderOptions: SenderToolOption[] = groupedTools.map(group => ({
152
+ value: group.key,
153
+ label: (
154
+ <span className='sender-tool-group-option'>
155
+ <span className='sender-tool-group-option__icon material-symbols-rounded'>{getToolGroupIcon(group.key)}</span>
156
+ <span className='sender-tool-group-option__text'>{group.label}</span>
157
+ <span className='sender-tool-group-option__count'>{group.tools.length}</span>
158
+ </span>
159
+ ),
160
+ children: group.tools.map(tool => ({
161
+ value: tool,
162
+ label: (
163
+ <span className='sender-tool-option'>
164
+ <span className='sender-tool-option__dot' />
165
+ <span className='sender-tool-option__text'>{formatToolLabel(tool)}</span>
166
+ </span>
167
+ )
168
+ }))
169
+ }))
120
170
 
121
171
  const [historyIndex, setHistoryIndex] = useState(-1)
122
172
  const [draft, setDraft] = useState('')
@@ -354,7 +404,9 @@ export function Sender({
354
404
  handleSend()
355
405
  return
356
406
  }
357
- if (clearInputShortcut != null && clearInputShortcut.trim() !== '' && isShortcutMatch(e, clearInputShortcut, isMac)) {
407
+ if (
408
+ clearInputShortcut != null && clearInputShortcut.trim() !== '' && isShortcutMatch(e, clearInputShortcut, isMac)
409
+ ) {
358
410
  e.preventDefault()
359
411
  clearInputValue()
360
412
  return
@@ -544,6 +596,20 @@ export function Sender({
544
596
  </div>
545
597
  )}
546
598
  <div className='chat-input-container'>
599
+ {connectionError && connectionError.trim() !== '' && (
600
+ <div className='connection-error-banner'>
601
+ <div className='connection-error-content'>
602
+ <span className='material-symbols-rounded'>error</span>
603
+ <div className='connection-error-copy'>
604
+ <div className='connection-error-title'>{t('chat.connectionErrorTitle')}</div>
605
+ <div className='connection-error-message'>{connectionError}</div>
606
+ </div>
607
+ </div>
608
+ <Button size='small' onClick={onRetryConnection}>
609
+ {t('chat.retryConnection')}
610
+ </Button>
611
+ </div>
612
+ )}
547
613
  {modelUnavailable && (
548
614
  <div className='model-unavailable'>
549
615
  {t('chat.modelConfigRequired')}
@@ -622,31 +688,25 @@ export function Sender({
622
688
  </Tooltip>
623
689
 
624
690
  {sessionInfo != null && sessionInfo.type === 'init' && (
625
- <div className='session-info-toolbar' ref={toolsRef}>
626
- <div
627
- className={`info-item ${showToolsList ? 'active' : ''}`}
628
- onClick={() => setShowToolsList(!showToolsList)}
691
+ <div className='session-info-toolbar'>
692
+ <Cascader
693
+ open={showToolsList}
694
+ options={toolCascaderOptions}
695
+ expandTrigger='hover'
696
+ placement='topLeft'
697
+ allowClear={false}
698
+ popupClassName='sender-tools-cascader-popup'
699
+ onOpenChange={setShowToolsList}
700
+ onChange={() => setShowToolsList(false)}
629
701
  >
630
- <span className='material-symbols-rounded'>build</span>
631
- <span className='info-text'>{t('chat.toolsCount', { count: sessionInfo.tools.length })}</span>
632
- <span className='material-symbols-rounded arrow-icon'>keyboard_arrow_up</span>
633
- </div>
634
-
635
- {showToolsList && (
636
- <div className='tools-list-popup'>
637
- <div className='popup-header'>{t('chat.availableTools')}</div>
638
- <div className='popup-content'>
639
- <div className='tools-list'>
640
- {sessionInfo.tools.map(tool => (
641
- <div key={tool} className='tool-item'>
642
- <span className='material-symbols-rounded'>check_circle</span>
643
- <span className='tool-name'>{tool}</span>
644
- </div>
645
- ))}
646
- </div>
647
- </div>
702
+ <div className={`info-item ${showToolsList ? 'active' : ''}`}>
703
+ <span className='info-item-leading'>
704
+ <span className='material-symbols-rounded'>build</span>
705
+ </span>
706
+ <span className='info-text'>{t('chat.toolsCount', { count: sessionInfo.tools.length })}</span>
707
+ <span className='material-symbols-rounded arrow-icon'>keyboard_arrow_up</span>
648
708
  </div>
649
- )}
709
+ </Cascader>
650
710
  </div>
651
711
  )}
652
712
  </div>
@@ -660,7 +720,7 @@ export function Sender({
660
720
  options={adapterOptions}
661
721
  showSearch={false}
662
722
  allowClear={false}
663
- disabled={modelUnavailable || isThinking}
723
+ disabled={adapterLocked || modelUnavailable || isThinking}
664
724
  onChange={(value) => onAdapterChange?.(value)}
665
725
  placeholder={t('chat.adapterSelectPlaceholder', { defaultValue: 'Adapter' })}
666
726
  optionLabelProp='label'
@@ -678,7 +738,7 @@ export function Sender({
678
738
  disabled={modelUnavailable || isThinking}
679
739
  onChange={(value) => onModelChange?.(value)}
680
740
  placeholder={modelUnavailable ? t('chat.modelUnavailable') : t('chat.modelSelectPlaceholder')}
681
- optionLabelProp='value'
741
+ optionLabelProp='displayLabel'
682
742
  filterOption={(input, option) => {
683
743
  const searchText = String((option as ModelSelectOption | undefined)?.searchText ?? '')
684
744
  return searchText.toLowerCase().includes(input.toLowerCase())
@@ -701,7 +761,9 @@ export function Sender({
701
761
  />
702
762
 
703
763
  <div
704
- className={`chat-send-btn ${input.trim() !== '' && !modelUnavailable ? 'active' : ''} ${isThinking ? 'thinking' : ''} ${modelUnavailable ? 'disabled' : ''}`}
764
+ className={`chat-send-btn ${input.trim() !== '' && !modelUnavailable ? 'active' : ''} ${
765
+ isThinking ? 'thinking' : ''
766
+ } ${modelUnavailable ? 'disabled' : ''}`}
705
767
  onClick={modelUnavailable ? undefined : (isThinking ? onInterrupt : handleSend)}
706
768
  >
707
769
  <span className='material-symbols-rounded'>
@@ -3,7 +3,7 @@ import './ToolGroup.scss'
3
3
  import type { ChatMessage, ChatMessageContent } from '@vibe-forge/core'
4
4
  import React, { useState } from 'react'
5
5
  import { useTranslation } from 'react-i18next'
6
- import { MessageFooter } from '../../Messages/MessageFooter'
6
+ import { MessageFooter } from '../../messages/MessageFooter'
7
7
  import { ToolRenderer } from './ToolRenderer'
8
8
 
9
9
  type ToolGroupProps = {
@@ -25,7 +25,7 @@ import {
25
25
  McpServersRecordEditor,
26
26
  ModelServicesRecordEditor,
27
27
  RecordJsonEditor
28
- } from './recordEditors/index'
28
+ } from './record-editors/index'
29
29
 
30
30
  export const SectionForm = ({
31
31
  sectionKey,
@@ -0,0 +1,19 @@
1
+ .app-shell {
2
+ height: 100vh;
3
+ display: flex;
4
+ flex-direction: row;
5
+ overflow: hidden;
6
+ }
7
+
8
+ .app-shell__content {
9
+ flex: 1;
10
+ position: relative;
11
+ overflow: hidden;
12
+ background-color: #fff;
13
+ }
14
+
15
+ .app-shell--dark {
16
+ .app-shell__content {
17
+ background-color: #141414;
18
+ }
19
+ }
@@ -0,0 +1,45 @@
1
+ import './AppShell.scss'
2
+
3
+ import { Layout } from 'antd'
4
+ import type { PropsWithChildren } from 'react'
5
+
6
+ import type { Session } from '@vibe-forge/core'
7
+
8
+ import { NavRail } from '#~/components/NavRail'
9
+ import { Sidebar } from '#~/components/Sidebar'
10
+
11
+ type AppShellProps = PropsWithChildren<{
12
+ activeSessionId?: string
13
+ isDarkMode: boolean
14
+ onDeletedSession: (deletedId: string, nextId?: string) => void
15
+ onSelectSession: (session: Session, isNew?: boolean) => void
16
+ showSidebar: boolean
17
+ sidebarWidth: number
18
+ }>
19
+
20
+ export function AppShell({
21
+ activeSessionId,
22
+ children,
23
+ isDarkMode,
24
+ onDeletedSession,
25
+ onSelectSession,
26
+ showSidebar,
27
+ sidebarWidth
28
+ }: AppShellProps) {
29
+ return (
30
+ <Layout className={`app-shell ${isDarkMode ? 'app-shell--dark' : ''}`}>
31
+ <NavRail />
32
+ {showSidebar && (
33
+ <Sidebar
34
+ width={sidebarWidth}
35
+ activeId={activeSessionId}
36
+ onSelectSession={onSelectSession}
37
+ onDeletedSession={onDeletedSession}
38
+ />
39
+ )}
40
+ <Layout.Content className='app-shell__content'>
41
+ {children}
42
+ </Layout.Content>
43
+ </Layout>
44
+ )
45
+ }
@@ -0,0 +1,150 @@
1
+ import type { ModelServiceConfig } from '@vibe-forge/core'
2
+
3
+ export interface ServiceModelEntry {
4
+ serviceKey: string
5
+ model: string
6
+ selectorValue: string
7
+ }
8
+
9
+ const normalizeNonEmptyString = (value: string | undefined) => {
10
+ const normalized = typeof value === 'string' ? value.trim() : ''
11
+ return normalized === '' ? undefined : normalized
12
+ }
13
+
14
+ export const buildServiceModelSelector = (serviceKey: string, modelName: string) => `${serviceKey},${modelName}`
15
+
16
+ export const parseServiceModelSelector = (value: string | undefined) => {
17
+ const normalizedValue = normalizeNonEmptyString(value)
18
+ if (!normalizedValue || !normalizedValue.includes(',')) return undefined
19
+
20
+ const [serviceKey, modelName] = normalizedValue.split(/,(.+)/)
21
+ const normalizedServiceKey = normalizeNonEmptyString(serviceKey)
22
+ const normalizedModelName = normalizeNonEmptyString(modelName)
23
+ if (!normalizedServiceKey || !normalizedModelName) return undefined
24
+
25
+ return {
26
+ serviceKey: normalizedServiceKey,
27
+ modelName: normalizedModelName,
28
+ selectorValue: buildServiceModelSelector(normalizedServiceKey, normalizedModelName)
29
+ }
30
+ }
31
+
32
+ export const listServiceModels = (modelServices: Record<string, ModelServiceConfig>) => {
33
+ const list: ServiceModelEntry[] = []
34
+
35
+ for (const [serviceKey, serviceValue] of Object.entries(modelServices)) {
36
+ const normalizedServiceKey = normalizeNonEmptyString(serviceKey)
37
+ if (!normalizedServiceKey) continue
38
+
39
+ const service = (serviceValue != null && typeof serviceValue === 'object')
40
+ ? serviceValue
41
+ : undefined
42
+ const models = Array.isArray(service?.models) ? service.models : []
43
+
44
+ for (const model of models) {
45
+ const normalizedModel = normalizeNonEmptyString(model)
46
+ if (!normalizedModel) continue
47
+
48
+ list.push({
49
+ serviceKey: normalizedServiceKey,
50
+ model: normalizedModel,
51
+ selectorValue: buildServiceModelSelector(normalizedServiceKey, normalizedModel)
52
+ })
53
+ }
54
+ }
55
+
56
+ return list
57
+ }
58
+
59
+ const findExactServiceModel = (serviceModels: ServiceModelEntry[], serviceKey: string, modelName: string) => (
60
+ serviceModels.find(entry => entry.serviceKey === serviceKey && entry.model === modelName)
61
+ )
62
+
63
+ export const resolveServiceModelSelector = (params: {
64
+ value?: string
65
+ serviceModels: ServiceModelEntry[]
66
+ preferredServiceKey?: string
67
+ }) => {
68
+ const normalizedValue = normalizeNonEmptyString(params.value)
69
+ if (!normalizedValue) return undefined
70
+
71
+ const parsed = parseServiceModelSelector(normalizedValue)
72
+ if (parsed) {
73
+ const exactMatch = findExactServiceModel(params.serviceModels, parsed.serviceKey, parsed.modelName)
74
+ if (exactMatch) return exactMatch.selectorValue
75
+ }
76
+
77
+ const modelName = parsed?.modelName ?? normalizedValue
78
+ const candidates = params.serviceModels.filter(entry => entry.model === modelName)
79
+ if (candidates.length === 0) return undefined
80
+
81
+ const preferredKeys = [parsed?.serviceKey, normalizeNonEmptyString(params.preferredServiceKey)]
82
+ .filter((value, index, array): value is string => Boolean(value) && array.indexOf(value) === index)
83
+
84
+ for (const preferredKey of preferredKeys) {
85
+ const candidate = candidates.find(entry => entry.serviceKey === preferredKey)
86
+ if (candidate) return candidate.selectorValue
87
+ }
88
+
89
+ return candidates[0]?.selectorValue
90
+ }
91
+
92
+ export const resolveChatModelSelection = (params: {
93
+ value?: string
94
+ builtinModels?: Iterable<string>
95
+ serviceModels: ServiceModelEntry[]
96
+ defaultModelService?: string
97
+ }) => {
98
+ const normalizedValue = normalizeNonEmptyString(params.value)
99
+ if (!normalizedValue) return undefined
100
+
101
+ const builtinModelSet = new Set(
102
+ Array.from(params.builtinModels ?? [])
103
+ .map(item => normalizeNonEmptyString(item))
104
+ .filter((item): item is string => Boolean(item))
105
+ )
106
+
107
+ if (builtinModelSet.has(normalizedValue)) return normalizedValue
108
+
109
+ return resolveServiceModelSelector({
110
+ value: normalizedValue,
111
+ serviceModels: params.serviceModels,
112
+ preferredServiceKey: params.defaultModelService
113
+ })
114
+ }
115
+
116
+ export const resolveDefaultChatModelSelection = (params: {
117
+ defaultModel?: string
118
+ defaultModelService?: string
119
+ builtinModels?: Iterable<string>
120
+ serviceModels: ServiceModelEntry[]
121
+ }) => {
122
+ const builtinModels = Array.from(params.builtinModels ?? [])
123
+ .map(item => normalizeNonEmptyString(item))
124
+ .filter((item): item is string => Boolean(item))
125
+ const builtinModelSet = new Set(builtinModels)
126
+ const normalizedDefaultModel = normalizeNonEmptyString(params.defaultModel)
127
+
128
+ if (normalizedDefaultModel) {
129
+ const parsed = parseServiceModelSelector(normalizedDefaultModel)
130
+ const resolvedServiceModel = resolveServiceModelSelector({
131
+ value: normalizedDefaultModel,
132
+ serviceModels: params.serviceModels,
133
+ preferredServiceKey: parsed?.serviceKey ?? params.defaultModelService
134
+ })
135
+ if (resolvedServiceModel) return resolvedServiceModel
136
+
137
+ if (builtinModelSet.has(normalizedDefaultModel)) return normalizedDefaultModel
138
+ if (parsed?.modelName && builtinModelSet.has(parsed.modelName)) return parsed.modelName
139
+ }
140
+
141
+ const normalizedDefaultModelService = normalizeNonEmptyString(params.defaultModelService)
142
+ if (normalizedDefaultModelService) {
143
+ const defaultServiceModel = params.serviceModels.find(entry => entry.serviceKey === normalizedDefaultModelService)
144
+ if (defaultServiceModel) return defaultServiceModel.selectorValue
145
+ }
146
+
147
+ if (builtinModels.length > 0) return builtinModels[0]
148
+
149
+ return params.serviceModels[0]?.selectorValue
150
+ }
@@ -1,10 +1,10 @@
1
- import { createElement, type ReactNode } from 'react'
1
+ import { type ReactNode, createElement } from 'react'
2
2
  import { useEffect, useMemo, useState } from 'react'
3
3
  import useSWR from 'swr'
4
4
 
5
5
  import { getConfig } from '#~/api.js'
6
- import type { ConfigResponse } from '@vibe-forge/core'
7
6
  import { getAdapterDisplay } from '#~/resources/adapters.js'
7
+ import type { ConfigResponse } from '@vibe-forge/core'
8
8
 
9
9
  const ADAPTER_STORAGE_KEY = 'vf_chat_adapter'
10
10
 
@@ -26,6 +26,22 @@ export function useChatAdapter() {
26
26
 
27
27
  const defaultAdapter = configRes?.sources?.merged?.general?.defaultAdapter
28
28
 
29
+ const resolveAdapter = (value?: string) => {
30
+ const normalizedValue = typeof value === 'string' ? value.trim() : ''
31
+ const keys = Object.keys(mergedAdapters)
32
+ if (keys.length === 0) return undefined
33
+ if (normalizedValue !== '' && keys.includes(normalizedValue)) return normalizedValue
34
+ if (defaultAdapter && keys.includes(defaultAdapter as string)) return defaultAdapter as string
35
+ return keys[0]
36
+ }
37
+
38
+ const updateSelectedAdapter = (value?: string) => {
39
+ setSelectedAdapter((prev) => {
40
+ const nextValue = resolveAdapter(value)
41
+ return nextValue === prev ? prev : nextValue
42
+ })
43
+ }
44
+
29
45
  const adapterOptions = useMemo<Array<{ value: string; label: ReactNode }>>(() => {
30
46
  const keys = Object.keys(mergedAdapters)
31
47
  return keys.map((key) => {
@@ -55,11 +71,7 @@ export function useChatAdapter() {
55
71
  setSelectedAdapter(undefined)
56
72
  return
57
73
  }
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
- })
74
+ setSelectedAdapter((prev) => resolveAdapter(prev))
63
75
  }, [defaultAdapter, mergedAdapters])
64
76
 
65
77
  // Persist to localStorage
@@ -75,7 +87,7 @@ export function useChatAdapter() {
75
87
 
76
88
  return {
77
89
  selectedAdapter,
78
- setSelectedAdapter,
90
+ setSelectedAdapter: updateSelectedAdapter,
79
91
  adapterOptions
80
92
  }
81
93
  }