@vibe-forge/client 0.4.0 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (142) hide show
  1. package/AGENTS.md +37 -0
  2. package/cli.cjs +1 -0
  3. package/dist/assets/{arc-DgIxeTMg.js → arc-CMAHd5G3.js} +1 -1
  4. package/dist/assets/{blockDiagram-c4efeb88-CEAob3X9.js → blockDiagram-c4efeb88-DKww-VCP.js} +1 -1
  5. package/dist/assets/{c4Diagram-c83219d4-DwIxpDKd.js → c4Diagram-c83219d4-DKrjVHyY.js} +1 -1
  6. package/dist/assets/channel-Bi4g8rj9.js +1 -0
  7. package/dist/assets/{classDiagram-beda092f-Cz1q8u_0.js → classDiagram-beda092f-BXx5rdo3.js} +1 -1
  8. package/dist/assets/{classDiagram-v2-2358418a-CImgTuwd.js → classDiagram-v2-2358418a-CnR3WLsr.js} +1 -1
  9. package/dist/assets/clone-DPrpP2ky.js +1 -0
  10. package/dist/assets/{createText-1719965b-C1_HJcCc.js → createText-1719965b-CmOsl1W7.js} +1 -1
  11. package/dist/assets/{edges-96097737-BU8qStzd.js → edges-96097737-CQeQgpjD.js} +1 -1
  12. package/dist/assets/{erDiagram-0228fc6a-DNA1Fz2L.js → erDiagram-0228fc6a-ZUNB-ucF.js} +1 -1
  13. package/dist/assets/{flowDb-c6c81e3f-DjiCStMN.js → flowDb-c6c81e3f-DuuKeSLX.js} +1 -1
  14. package/dist/assets/{flowDiagram-50d868cf-CSDi0-RD.js → flowDiagram-50d868cf-Bc6n85yR.js} +1 -1
  15. package/dist/assets/flowDiagram-v2-4f6560a1-BZqaeqoh.js +1 -0
  16. package/dist/assets/{flowchart-elk-definition-6af322e1-DrhIMas7.js → flowchart-elk-definition-6af322e1-cAG5afW9.js} +1 -1
  17. package/dist/assets/{ganttDiagram-a2739b55-CTZnUP5z.js → ganttDiagram-a2739b55-Dp6xhY5I.js} +1 -1
  18. package/dist/assets/{gitGraphDiagram-82fe8481-COOW7jTi.js → gitGraphDiagram-82fe8481-MlIIRBdG.js} +1 -1
  19. package/dist/assets/{graph-CIkpD4Kx.js → graph-D7Es8jZ-.js} +1 -1
  20. package/dist/assets/{index-5325376f-aVVRRTIu.js → index-5325376f-DC18ottv.js} +1 -1
  21. package/dist/assets/index-D37AbgPQ.js +545 -0
  22. package/dist/assets/{index-D1giUI7r.css → index-fcJ9v94I.css} +1 -1
  23. package/dist/assets/{infoDiagram-8eee0895-DQpZ1LVD.js → infoDiagram-8eee0895-CXk21kFp.js} +1 -1
  24. package/dist/assets/{journeyDiagram-c64418c1-DoKguIuk.js → journeyDiagram-c64418c1-899BKBHL.js} +1 -1
  25. package/dist/assets/{layout-Tnmha8Nh.js → layout-DLaxdy48.js} +1 -1
  26. package/dist/assets/{line-BQR2SOyl.js → line-_lw5YbRM.js} +1 -1
  27. package/dist/assets/{linear-DlG0eemV.js → linear-D5iu84ui.js} +1 -1
  28. package/dist/assets/{mermaid.core-BnwYO0He.js → mermaid.core-C6sW3GFM.js} +4 -4
  29. package/dist/assets/{mindmap-definition-8da855dc-BllYwDID.js → mindmap-definition-8da855dc-BS9Xy9KN.js} +1 -1
  30. package/dist/assets/{pieDiagram-a8764435-DwCkhPVc.js → pieDiagram-a8764435-DZt9cEgs.js} +1 -1
  31. package/dist/assets/{quadrantDiagram-1e28029f-c40GKTU0.js → quadrantDiagram-1e28029f-BTIeHOgn.js} +1 -1
  32. package/dist/assets/{requirementDiagram-08caed73-DnQp2Tk6.js → requirementDiagram-08caed73-BHJAKD2g.js} +1 -1
  33. package/dist/assets/{sankeyDiagram-a04cb91d-CnJrs13b.js → sankeyDiagram-a04cb91d-DnAkVOK8.js} +1 -1
  34. package/dist/assets/{sequenceDiagram-c5b8d532-1YBwnpKu.js → sequenceDiagram-c5b8d532-92tE3oFv.js} +1 -1
  35. package/dist/assets/{stateDiagram-1ecb1508-BFBxQ6Fh.js → stateDiagram-1ecb1508-DG0ObiMg.js} +1 -1
  36. package/dist/assets/{stateDiagram-v2-c2b004d7-Dmechvv2.js → stateDiagram-v2-c2b004d7-BKoJx2ci.js} +1 -1
  37. package/dist/assets/{styles-b4e223ce-DWWfWX8O.js → styles-b4e223ce-Ba6G4ri9.js} +1 -1
  38. package/dist/assets/{styles-ca3715f6-CKKvZxaU.js → styles-ca3715f6-Bn6RIIVW.js} +1 -1
  39. package/dist/assets/{styles-d45a18b0-dKMOUh9p.js → styles-d45a18b0-_dELBUI6.js} +1 -1
  40. package/dist/assets/{svgDrawCommon-b86b1483-CBgjChPM.js → svgDrawCommon-b86b1483-CRK-ZoIs.js} +1 -1
  41. package/dist/assets/{timeline-definition-faaaa080-NCt-HHmb.js → timeline-definition-faaaa080-DvQ_RA_i.js} +1 -1
  42. package/dist/assets/{xychartDiagram-f5964ef8-BJhXS4dG.js → xychartDiagram-f5964ef8-CJxeDLfg.js} +1 -1
  43. package/dist/index.html +2 -2
  44. package/package.json +10 -7
  45. package/src/App.tsx +20 -168
  46. package/src/api/base.ts +116 -7
  47. package/src/api/sessions.ts +3 -1
  48. package/src/api.ts +3 -1
  49. package/src/components/ArchiveView.tsx +5 -5
  50. package/src/components/ConfigView.tsx +28 -28
  51. package/src/components/{AutomationView → automation-view}/index.tsx +10 -9
  52. package/src/components/{BenchmarkView → benchmark-view}/index.tsx +5 -4
  53. package/src/components/chat/ChatHeader.tsx +6 -6
  54. package/src/components/chat/ChatHistoryView.tsx +74 -16
  55. package/src/components/chat/ChatTimelineView.tsx +3 -3
  56. package/src/components/chat/CurrentTodoList.scss +56 -27
  57. package/src/components/chat/{Sender → sender}/Sender.scss +278 -85
  58. package/src/components/chat/{Sender → sender}/Sender.tsx +125 -41
  59. package/src/components/chat/tools/core/ToolGroup.tsx +1 -1
  60. package/src/components/config/ConfigSectionForm.tsx +1 -1
  61. package/src/components/config/ConfigSourceSwitch.tsx +12 -34
  62. package/src/components/config/channelDefinitions.ts +2 -2
  63. package/src/components/layout/AppShell.scss +19 -0
  64. package/src/components/layout/AppShell.tsx +45 -0
  65. package/src/components/sidebar/SessionItem.scss +17 -0
  66. package/src/components/sidebar/SessionItem.tsx +21 -13
  67. package/src/hooks/chat/model-selector.ts +150 -0
  68. package/src/hooks/chat/use-chat-adapter.ts +93 -0
  69. package/src/hooks/chat/use-chat-models.tsx +126 -57
  70. package/src/hooks/chat/use-chat-permission-mode.ts +14 -10
  71. package/src/hooks/chat/use-chat-session-actions.ts +22 -13
  72. package/src/hooks/chat/use-chat-session-messages.ts +62 -10
  73. package/src/hooks/chat/use-chat-session.ts +49 -2
  74. package/src/hooks/use-app-preferences.ts +41 -0
  75. package/src/hooks/use-session-subscription.ts +101 -0
  76. package/src/hooks/use-sidebar-navigation.ts +35 -0
  77. package/src/resources/adapters.ts +20 -0
  78. package/src/resources/locales/en.json +6 -0
  79. package/src/resources/locales/zh.json +6 -0
  80. package/src/routes/AppRoutes.tsx +22 -0
  81. package/src/routes/ArchiveRoute.tsx +5 -0
  82. package/src/routes/AutomationRoute.tsx +5 -0
  83. package/src/routes/BenchmarkRoute.tsx +5 -0
  84. package/src/{components/Chat.scss → routes/ChatRoute.scss} +35 -0
  85. package/src/routes/ChatRoute.tsx +132 -0
  86. package/src/routes/ConfigRoute.tsx +5 -0
  87. package/src/routes/KnowledgeRoute.tsx +5 -0
  88. package/dist/assets/channel-DhtnrNJ6.js +0 -1
  89. package/dist/assets/clone-7bHB6YkC.js +0 -1
  90. package/dist/assets/flowDiagram-v2-4f6560a1-_13Sz5Wh.js +0 -1
  91. package/dist/assets/index-DRSI_ZIL.js +0 -514
  92. package/src/components/Chat.tsx +0 -100
  93. package/src/components/{AutomationView → automation-view}/RuleFormPanel.scss +0 -0
  94. package/src/components/{AutomationView → automation-view}/RuleFormPanel.tsx +0 -0
  95. package/src/components/{AutomationView → automation-view}/RuleSidebar.scss +0 -0
  96. package/src/components/{AutomationView → automation-view}/RuleSidebar.tsx +0 -0
  97. package/src/components/{AutomationView → automation-view}/RunHistoryPanel.scss +0 -0
  98. package/src/components/{AutomationView → automation-view}/RunHistoryPanel.tsx +0 -0
  99. package/src/components/{AutomationView → automation-view}/TaskList.scss +0 -0
  100. package/src/components/{AutomationView → automation-view}/TaskList.tsx +0 -0
  101. package/src/components/{AutomationView → automation-view}/TriggerList.scss +0 -0
  102. package/src/components/{AutomationView → automation-view}/TriggerList.tsx +0 -0
  103. package/src/components/{AutomationView/AutomationView.scss → automation-view/index.scss} +0 -0
  104. package/src/components/{AutomationView → automation-view}/types.ts +0 -0
  105. package/src/components/{BenchmarkView → benchmark-view}/BenchmarkCasePanel.scss +0 -0
  106. package/src/components/{BenchmarkView → benchmark-view}/BenchmarkCasePanel.tsx +0 -0
  107. package/src/components/{BenchmarkView → benchmark-view}/BenchmarkSidebar.scss +0 -0
  108. package/src/components/{BenchmarkView → benchmark-view}/BenchmarkSidebar.tsx +0 -0
  109. package/src/components/{BenchmarkView → benchmark-view}/BenchmarkView.scss +0 -0
  110. package/src/components/{BenchmarkView → benchmark-view}/types.ts +0 -0
  111. package/src/components/{BenchmarkView → benchmark-view}/utils.ts +0 -0
  112. package/src/components/chat/{Messages → messages}/MessageFooter.tsx +0 -0
  113. package/src/components/chat/{Messages → messages}/MessageItem.scss +0 -0
  114. package/src/components/chat/{Messages → messages}/MessageItem.tsx +0 -0
  115. package/src/components/chat/{Messages → messages}/message-utils.ts +0 -0
  116. package/src/components/chat/{Sender → sender}/CompletionMenu.scss +0 -0
  117. package/src/components/chat/{Sender → sender}/CompletionMenu.tsx +0 -0
  118. package/src/components/chat/{Sender → sender}/ThinkingStatus.scss +0 -0
  119. package/src/components/chat/{Sender → sender}/ThinkingStatus.tsx +0 -0
  120. package/src/components/chat/{SessionTimelinePanel → session-timeline-panel}/EventList.scss +0 -0
  121. package/src/components/chat/{SessionTimelinePanel → session-timeline-panel}/EventList.tsx +0 -0
  122. package/src/components/chat/{SessionTimelinePanel → session-timeline-panel}/gantt.ts +0 -0
  123. package/src/components/chat/{SessionTimelinePanel → session-timeline-panel}/git-graph.ts +0 -0
  124. package/src/components/chat/{SessionTimelinePanel → session-timeline-panel}/index.scss +0 -0
  125. package/src/components/chat/{SessionTimelinePanel → session-timeline-panel}/index.tsx +0 -0
  126. package/src/components/chat/{SessionTimelinePanel → session-timeline-panel}/mermaid.ts +0 -0
  127. package/src/components/chat/{SessionTimelinePanel → session-timeline-panel}/types.ts +0 -0
  128. package/src/components/chat/{SessionTimelinePanel → session-timeline-panel}/utils.ts +0 -0
  129. package/src/components/config/{recordEditors → record-editors}/BooleanRecordEditor.scss +0 -0
  130. package/src/components/config/{recordEditors → record-editors}/BooleanRecordEditor.tsx +0 -0
  131. package/src/components/config/{recordEditors → record-editors}/ChannelRecordEditor.scss +0 -0
  132. package/src/components/config/{recordEditors → record-editors}/ChannelRecordEditor.tsx +33 -33
  133. /package/src/components/config/{recordEditors → record-editors}/KeyValueEditor.scss +0 -0
  134. /package/src/components/config/{recordEditors → record-editors}/KeyValueEditor.tsx +0 -0
  135. /package/src/components/config/{recordEditors → record-editors}/McpServersRecordEditor.scss +0 -0
  136. /package/src/components/config/{recordEditors → record-editors}/McpServersRecordEditor.tsx +0 -0
  137. /package/src/components/config/{recordEditors → record-editors}/ModelServicesRecordEditor.scss +0 -0
  138. /package/src/components/config/{recordEditors → record-editors}/ModelServicesRecordEditor.tsx +0 -0
  139. /package/src/components/config/{recordEditors → record-editors}/RecordEditors.scss +0 -0
  140. /package/src/components/config/{recordEditors → record-editors}/RecordJsonEditor.scss +0 -0
  141. /package/src/components/config/{recordEditors → record-editors}/RecordJsonEditor.tsx +0 -0
  142. /package/src/components/config/{recordEditors → record-editors}/index.tsx +0 -0
@@ -1,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,
@@ -50,14 +75,20 @@ export function Sender({
50
75
  permissionMode,
51
76
  permissionModeOptions,
52
77
  onPermissionModeChange,
78
+ selectedAdapter,
79
+ adapterOptions,
80
+ onAdapterChange,
53
81
  modelUnavailable
54
82
  }: {
55
83
  onSend: (text: string) => void
56
84
  onSendContent: (content: ChatMessageContent[]) => void
85
+ adapterLocked?: boolean
57
86
  sessionStatus?: SessionStatus
58
87
  onInterrupt: () => void
59
88
  onClear?: () => void
60
89
  sessionInfo?: SessionInfo | null
90
+ connectionError?: string | null
91
+ onRetryConnection?: () => void
61
92
  interactionRequest?: { id: string; payload: AskUserQuestionParams } | null
62
93
  onInteractionResponse?: (id: string, data: string | string[]) => void
63
94
  placeholder?: string
@@ -67,6 +98,9 @@ export function Sender({
67
98
  permissionMode: PermissionMode
68
99
  permissionModeOptions: Array<{ value: PermissionMode; label: React.ReactNode }>
69
100
  onPermissionModeChange: (mode: PermissionMode) => void
101
+ selectedAdapter?: string
102
+ adapterOptions?: Array<{ value: string; label: React.ReactNode }>
103
+ onAdapterChange?: (adapter: string) => void
70
104
  modelUnavailable?: boolean
71
105
  }) {
72
106
  const { t } = useTranslation()
@@ -79,7 +113,6 @@ export function Sender({
79
113
 
80
114
  const [showToolsList, setShowToolsList] = useState(false)
81
115
  const textareaRef = useRef<TextAreaRef>(null)
82
- const toolsRef = useRef<HTMLDivElement>(null)
83
116
  const fileInputRef = useRef<HTMLInputElement>(null)
84
117
  const isMac = navigator.platform.includes('Mac')
85
118
  const [pendingImages, setPendingImages] = useState<PendingImage[]>([])
@@ -101,16 +134,39 @@ export function Sender({
101
134
  : 'mod+enter'
102
135
 
103
136
  const isThinking = sessionStatus === 'running'
104
-
105
- useEffect(() => {
106
- const handleClickOutside = (event: MouseEvent) => {
107
- if (toolsRef.current && !toolsRef.current.contains(event.target as Node)) {
108
- setShowToolsList(false)
109
- }
110
- }
111
- document.addEventListener('mousedown', handleClickOutside)
112
- return () => document.removeEventListener('mousedown', handleClickOutside)
113
- }, [])
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
+ }))
114
170
 
115
171
  const [historyIndex, setHistoryIndex] = useState(-1)
116
172
  const [draft, setDraft] = useState('')
@@ -348,7 +404,9 @@ export function Sender({
348
404
  handleSend()
349
405
  return
350
406
  }
351
- if (clearInputShortcut != null && clearInputShortcut.trim() !== '' && isShortcutMatch(e, clearInputShortcut, isMac)) {
407
+ if (
408
+ clearInputShortcut != null && clearInputShortcut.trim() !== '' && isShortcutMatch(e, clearInputShortcut, isMac)
409
+ ) {
352
410
  e.preventDefault()
353
411
  clearInputValue()
354
412
  return
@@ -538,6 +596,20 @@ export function Sender({
538
596
  </div>
539
597
  )}
540
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
+ )}
541
613
  {modelUnavailable && (
542
614
  <div className='model-unavailable'>
543
615
  {t('chat.modelConfigRequired')}
@@ -616,36 +688,46 @@ export function Sender({
616
688
  </Tooltip>
617
689
 
618
690
  {sessionInfo != null && sessionInfo.type === 'init' && (
619
- <div className='session-info-toolbar' ref={toolsRef}>
620
- <div
621
- className={`info-item ${showToolsList ? 'active' : ''}`}
622
- 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)}
623
701
  >
624
- <span className='material-symbols-rounded'>build</span>
625
- <span className='info-text'>{t('chat.toolsCount', { count: sessionInfo.tools.length })}</span>
626
- <span className='material-symbols-rounded arrow-icon'>keyboard_arrow_up</span>
627
- </div>
628
-
629
- {showToolsList && (
630
- <div className='tools-list-popup'>
631
- <div className='popup-header'>{t('chat.availableTools')}</div>
632
- <div className='popup-content'>
633
- <div className='tools-list'>
634
- {sessionInfo.tools.map(tool => (
635
- <div key={tool} className='tool-item'>
636
- <span className='material-symbols-rounded'>check_circle</span>
637
- <span className='tool-name'>{tool}</span>
638
- </div>
639
- ))}
640
- </div>
641
- </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>
642
708
  </div>
643
- )}
709
+ </Cascader>
644
710
  </div>
645
711
  )}
646
712
  </div>
647
713
 
648
714
  <div className='toolbar-right'>
715
+ {adapterOptions && adapterOptions.length > 1 && (
716
+ <Select
717
+ className='adapter-select'
718
+ classNames={{ popup: { root: 'adapter-select-popup' } }}
719
+ value={selectedAdapter}
720
+ options={adapterOptions}
721
+ showSearch={false}
722
+ allowClear={false}
723
+ disabled={adapterLocked || modelUnavailable || isThinking}
724
+ onChange={(value) => onAdapterChange?.(value)}
725
+ placeholder={t('chat.adapterSelectPlaceholder', { defaultValue: 'Adapter' })}
726
+ optionLabelProp='label'
727
+ popupMatchSelectWidth={false}
728
+ />
729
+ )}
730
+
649
731
  <Select
650
732
  className='model-select'
651
733
  classNames={{ popup: { root: 'model-select-popup' } }}
@@ -656,7 +738,7 @@ export function Sender({
656
738
  disabled={modelUnavailable || isThinking}
657
739
  onChange={(value) => onModelChange?.(value)}
658
740
  placeholder={modelUnavailable ? t('chat.modelUnavailable') : t('chat.modelSelectPlaceholder')}
659
- optionLabelProp='value'
741
+ optionLabelProp='displayLabel'
660
742
  filterOption={(input, option) => {
661
743
  const searchText = String((option as ModelSelectOption | undefined)?.searchText ?? '')
662
744
  return searchText.toLowerCase().includes(input.toLowerCase())
@@ -679,7 +761,9 @@ export function Sender({
679
761
  />
680
762
 
681
763
  <div
682
- 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' : ''}`}
683
767
  onClick={modelUnavailable ? undefined : (isThinking ? onInterrupt : handleSend)}
684
768
  >
685
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,
@@ -1,19 +1,16 @@
1
1
  import { Radio } from 'antd'
2
+ import type { ReactNode } from 'react'
2
3
 
3
4
  import type { ConfigSource } from '@vibe-forge/core'
4
5
 
5
- import type { TranslationFn } from './configUtils'
6
-
7
6
  export function ConfigSourceSwitch({
8
7
  value,
9
8
  onChange,
10
- configPresent,
11
- t
9
+ options,
12
10
  }: {
13
11
  value: ConfigSource
14
12
  onChange: (value: ConfigSource) => void
15
- configPresent?: { project?: boolean; user?: boolean }
16
- t: TranslationFn
13
+ options: Array<{ value: ConfigSource; icon: string; label: ReactNode }>
17
14
  }) {
18
15
  return (
19
16
  <Radio.Group
@@ -24,34 +21,15 @@ export function ConfigSourceSwitch({
24
21
  onChange={(event) => {
25
22
  onChange(event.target.value as ConfigSource)
26
23
  }}
27
- options={[
28
- {
29
- label: (
30
- <span className='config-view__source-option'>
31
- <span className='material-symbols-rounded'>folder</span>
32
- <span>
33
- {configPresent?.project === true
34
- ? t('config.sources.project')
35
- : t('config.sources.projectMissing')}
36
- </span>
37
- </span>
38
- ),
39
- value: 'project'
40
- },
41
- {
42
- label: (
43
- <span className='config-view__source-option'>
44
- <span className='material-symbols-rounded'>person</span>
45
- <span>
46
- {configPresent?.user === true
47
- ? t('config.sources.user')
48
- : t('config.sources.userMissing')}
49
- </span>
50
- </span>
51
- ),
52
- value: 'user'
53
- }
54
- ]}
24
+ options={options.map(opt => ({
25
+ value: opt.value,
26
+ label: (
27
+ <span className='config-view__source-option'>
28
+ <span className='material-symbols-rounded'>{opt.icon}</span>
29
+ <span>{opt.label}</span>
30
+ </span>
31
+ )
32
+ }))}
55
33
  />
56
34
  )
57
35
  }
@@ -1,6 +1,6 @@
1
- import { larkChannelDefinition } from '@vibe-forge/channel-lark'
1
+ import { channelDefinition } from '@vibe-forge/channel-lark'
2
2
  import type { ChannelDescriptor } from '@vibe-forge/core/channel'
3
3
 
4
4
  export const channelDefinitions: ChannelDescriptor[] = [
5
- larkChannelDefinition
5
+ channelDefinition
6
6
  ]
@@ -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
+ }
@@ -163,11 +163,15 @@
163
163
  align-items: center;
164
164
 
165
165
  .session-tag {
166
+ max-width: 120px;
166
167
  font-size: 10px;
167
168
  margin: 0;
168
169
  padding: 0 4px;
169
170
  line-height: 14px;
170
171
  border-radius: 2px;
172
+ display: inline-flex;
173
+ align-items: center;
174
+ white-space: nowrap;
171
175
 
172
176
  &--automation {
173
177
  padding: 0 6px;
@@ -176,6 +180,19 @@
176
180
  &__link {
177
181
  color: var(--primary-color);
178
182
  text-decoration: none;
183
+ display: inline-block;
184
+ max-width: 100%;
185
+ overflow: hidden;
186
+ text-overflow: ellipsis;
187
+ white-space: nowrap;
188
+ }
189
+
190
+ &__text {
191
+ display: inline-block;
192
+ max-width: 100%;
193
+ overflow: hidden;
194
+ text-overflow: ellipsis;
195
+ white-space: nowrap;
179
196
  }
180
197
  }
181
198
  }
@@ -226,28 +226,36 @@ export function SessionItem({
226
226
  if (automationTag) {
227
227
  const href = `/automation?rule=${encodeURIComponent(automationTag.ruleId)}`
228
228
  return (
229
- <Tag
229
+ <Tooltip
230
230
  key={tag}
231
- className='session-tag session-tag--automation'
232
- onClick={(event) => event.stopPropagation()}
231
+ title={automationTag.ruleTitle}
233
232
  >
234
- <a
235
- className='session-tag__link'
236
- href={href}
233
+ <Tag
234
+ className='session-tag session-tag--automation'
237
235
  onClick={(event) => event.stopPropagation()}
238
236
  >
239
- {automationTag.ruleTitle}
240
- </a>
241
- </Tag>
237
+ <a
238
+ className='session-tag__link'
239
+ href={href}
240
+ onClick={(event) => event.stopPropagation()}
241
+ >
242
+ {automationTag.ruleTitle}
243
+ </a>
244
+ </Tag>
245
+ </Tooltip>
242
246
  )
243
247
  }
244
248
  return (
245
- <Tag
249
+ <Tooltip
246
250
  key={tag}
247
- className='session-tag'
251
+ title={tag}
248
252
  >
249
- {tag}
250
- </Tag>
253
+ <Tag className='session-tag'>
254
+ <span className='session-tag__text'>
255
+ {tag}
256
+ </span>
257
+ </Tag>
258
+ </Tooltip>
251
259
  )
252
260
  })}
253
261
  </div>
@@ -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
+ }