@vibe-forge/client 2.0.1 → 3.0.0-alpha.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 (85) hide show
  1. package/dist/assets/{arc-CqviK3HX.js → arc-1JbypnRY.js} +1 -1
  2. package/dist/assets/{blockDiagram-c4efeb88-BEp50UHp.js → blockDiagram-c4efeb88-jT5b9nId.js} +1 -1
  3. package/dist/assets/{c4Diagram-c83219d4-C5w55JzM.js → c4Diagram-c83219d4-KNPh-1ta.js} +1 -1
  4. package/dist/assets/channel-pzvjJnfd.js +1 -0
  5. package/dist/assets/{classDiagram-beda092f-CQJVtHEy.js → classDiagram-beda092f-vvXuOT3F.js} +1 -1
  6. package/dist/assets/{classDiagram-v2-2358418a-B37Xl9jB.js → classDiagram-v2-2358418a-vH4j7skr.js} +1 -1
  7. package/dist/assets/clone-CtWwIeUb.js +1 -0
  8. package/dist/assets/{createText-1719965b-9YwvWMdV.js → createText-1719965b-COeSYnf3.js} +1 -1
  9. package/dist/assets/{cssMode-BX88r5f4.js → cssMode-Doc7QRKn.js} +1 -1
  10. package/dist/assets/{edges-96097737-CNHoXVrD.js → edges-96097737-D09v6R_r.js} +1 -1
  11. package/dist/assets/{erDiagram-0228fc6a-BoYldy0g.js → erDiagram-0228fc6a-CbZ6rfxl.js} +1 -1
  12. package/dist/assets/{flowDb-c6c81e3f-CoPw_R-Q.js → flowDb-c6c81e3f-CnZOzhdk.js} +1 -1
  13. package/dist/assets/{flowDiagram-50d868cf-nCqbSXd-.js → flowDiagram-50d868cf-jv-xWM2p.js} +1 -1
  14. package/dist/assets/flowDiagram-v2-4f6560a1-BiyrkIKs.js +1 -0
  15. package/dist/assets/{flowchart-elk-definition-6af322e1-BwMuPTrV.js → flowchart-elk-definition-6af322e1-D8pf677G.js} +1 -1
  16. package/dist/assets/{freemarker2-DUFDSvgj.js → freemarker2-CL0o23Gj.js} +1 -1
  17. package/dist/assets/{ganttDiagram-a2739b55-CLNH3S_C.js → ganttDiagram-a2739b55-BAPQzC4u.js} +1 -1
  18. package/dist/assets/{gitGraphDiagram-82fe8481-uDu1ectX.js → gitGraphDiagram-82fe8481-BI2x71m0.js} +1 -1
  19. package/dist/assets/{graph-DuC4kt4I.js → graph-CZK4Bjpq.js} +1 -1
  20. package/dist/assets/{handlebars-BSd4a6l9.js → handlebars-CCG38Pg6.js} +1 -1
  21. package/dist/assets/{html-H48gEjQd.js → html-BddshTWG.js} +1 -1
  22. package/dist/assets/{htmlMode-Nqw7-Nqh.js → htmlMode-xKXiYcDz.js} +1 -1
  23. package/dist/assets/{index-5325376f-rnz0GXAT.js → index-5325376f-BWMTD8RU.js} +1 -1
  24. package/dist/assets/{index-DeQLT67a.js → index-CBe7kDkV.js} +322 -322
  25. package/dist/assets/{index-DiOCtPLP.css → index-MWOwVzqE.css} +1 -1
  26. package/dist/assets/{infoDiagram-8eee0895-BsGB550b.js → infoDiagram-8eee0895-CR78btIF.js} +1 -1
  27. package/dist/assets/{javascript-0g2herYV.js → javascript-BPlRHPjg.js} +1 -1
  28. package/dist/assets/{journeyDiagram-c64418c1-DLldlz0H.js → journeyDiagram-c64418c1-DZRv6FKz.js} +1 -1
  29. package/dist/assets/{jsonMode-CN5ZURMh.js → jsonMode-BYLVfdkf.js} +1 -1
  30. package/dist/assets/{layout-QKUiDNJK.js → layout-BtudyPU2.js} +1 -1
  31. package/dist/assets/{line-CeP3XWjD.js → line-DgHXrIhS.js} +1 -1
  32. package/dist/assets/{linear-74cQVgWT.js → linear-DtBoKICx.js} +1 -1
  33. package/dist/assets/{liquid-B6cRrfrb.js → liquid-DxBlJk0W.js} +1 -1
  34. package/dist/assets/{lspLanguageFeatures-C5ogOh5E.js → lspLanguageFeatures-DmKVpmeH.js} +1 -1
  35. package/dist/assets/{mdx-BBIy-KRj.js → mdx-DrScsd-w.js} +1 -1
  36. package/dist/assets/{mermaid.core-BhdbV0mr.js → mermaid.core-Cj_NJ_lZ.js} +4 -4
  37. package/dist/assets/{mindmap-definition-8da855dc-B67VKJuD.js → mindmap-definition-8da855dc-CMXbwtsc.js} +1 -1
  38. package/dist/assets/{pieDiagram-a8764435-Cxv9WY_E.js → pieDiagram-a8764435-Bd6rfpJv.js} +1 -1
  39. package/dist/assets/{python-CBdGo8__.js → python-Crbi7B7n.js} +1 -1
  40. package/dist/assets/{quadrantDiagram-1e28029f-BTkj65P_.js → quadrantDiagram-1e28029f-H-FdBQn6.js} +1 -1
  41. package/dist/assets/{razor-azKH0Dwj.js → razor-DHyrzIfE.js} +1 -1
  42. package/dist/assets/{requirementDiagram-08caed73-D4jVXpOT.js → requirementDiagram-08caed73-BSyCVDSG.js} +1 -1
  43. package/dist/assets/{sankeyDiagram-a04cb91d-CXhutIA1.js → sankeyDiagram-a04cb91d-S6E6BReg.js} +1 -1
  44. package/dist/assets/{sequenceDiagram-c5b8d532-B56TTZlx.js → sequenceDiagram-c5b8d532-BMHY4KMj.js} +1 -1
  45. package/dist/assets/{stateDiagram-1ecb1508-Cs0plMcS.js → stateDiagram-1ecb1508-BMrMw6XR.js} +1 -1
  46. package/dist/assets/{stateDiagram-v2-c2b004d7-LSJaXPJN.js → stateDiagram-v2-c2b004d7-B5SIFUb_.js} +1 -1
  47. package/dist/assets/{styles-b4e223ce-UdXfHMuu.js → styles-b4e223ce-DqiXwnx3.js} +1 -1
  48. package/dist/assets/{styles-ca3715f6-EuRy_hTu.js → styles-ca3715f6-jNxW2Db8.js} +1 -1
  49. package/dist/assets/{styles-d45a18b0-B24zVoK3.js → styles-d45a18b0-C4nlfLcZ.js} +1 -1
  50. package/dist/assets/{svgDrawCommon-b86b1483-B2S0NW3K.js → svgDrawCommon-b86b1483-FIWFuZfb.js} +1 -1
  51. package/dist/assets/{timeline-definition-faaaa080-DFWKh9mU.js → timeline-definition-faaaa080-CEQDoqdf.js} +1 -1
  52. package/dist/assets/{tsMode-FZsHWiOn.js → tsMode-DnNy3rE9.js} +1 -1
  53. package/dist/assets/{typescript-CYdJ3s3D.js → typescript-YBZ4vw5B.js} +1 -1
  54. package/dist/assets/{xml-C16X_hpZ.js → xml-BJGGYD70.js} +1 -1
  55. package/dist/assets/{xychartDiagram-f5964ef8-DyBiBYci.js → xychartDiagram-f5964ef8-DUNOFOk0.js} +1 -1
  56. package/dist/assets/{yaml-CRjA4-Rj.js → yaml-BMY4mu5s.js} +1 -1
  57. package/dist/index.html +2 -2
  58. package/package.json +13 -13
  59. package/src/components/ConfigView.scss +7 -6
  60. package/src/components/ConfigView.tsx +2 -1
  61. package/src/components/Sidebar.tsx +1 -0
  62. package/src/components/automation-view/@components/AutomationTaskComposer.tsx +10 -8
  63. package/src/components/automation-view/AutomationEmptyLanding.tsx +12 -0
  64. package/src/components/automation-view/TaskList.scss +5 -1
  65. package/src/components/chat/ChatHistoryView.tsx +5 -3
  66. package/src/components/chat/NewSessionGuide.tsx +16 -10
  67. package/src/components/chat/NewSessionGuideStarterList.tsx +4 -1
  68. package/src/components/chat/NewSessionGuideStarterSection.tsx +2 -1
  69. package/src/components/chat/messages/MessageItem.tsx +19 -5
  70. package/src/components/chat/messages/message-action-utils.ts +11 -0
  71. package/src/components/chat/sender/Sender.scss +12 -0
  72. package/src/components/config/AppSettingsPanel.tsx +84 -30
  73. package/src/components/knowledge-base/components/@hooks/use-skills-cli-modal-controller.ts +157 -0
  74. package/src/components/knowledge-base/components/@hooks/use-skills-tab-actions.ts +63 -0
  75. package/src/components/knowledge-base/components/SkillsTab.tsx +40 -193
  76. package/src/components/sidebar/SidebarHeader.tsx +44 -26
  77. package/src/components/sidebar/sidebar-search-visibility.ts +18 -0
  78. package/src/hooks/useQueryParams.ts +91 -23
  79. package/src/resources/locales/en.json +12 -0
  80. package/src/resources/locales/zh.json +12 -0
  81. package/src/routes/ChatRoute.scss +50 -45
  82. package/src/store/index.ts +111 -3
  83. package/dist/assets/channel-C6LTxxLg.js +0 -1
  84. package/dist/assets/clone-CG6ZcokX.js +0 -1
  85. package/dist/assets/flowDiagram-v2-4f6560a1-CSZTI7GQ.js +0 -1
@@ -1,6 +1,6 @@
1
1
  import './SkillsTab.scss'
2
2
 
3
- import { App, Form } from 'antd'
3
+ import { App } from 'antd'
4
4
  import React from 'react'
5
5
  import type { ReactNode } from 'react'
6
6
  import { useTranslation } from 'react-i18next'
@@ -9,22 +9,15 @@ import useSWR from 'swr'
9
9
 
10
10
  import type { ConfigResponse } from '@vibe-forge/types'
11
11
 
12
- import type { SkillHubItem, SkillSummary } from '#~/api.js'
13
- import {
14
- getApiErrorMessage,
15
- getConfig,
16
- importSkillArchive,
17
- installSkillHubItem,
18
- installSkillsCliItem,
19
- listSkills,
20
- searchSkillsCli
21
- } from '#~/api.js'
12
+ import type { SkillSummary } from '#~/api.js'
13
+ import { getConfig, listSkills } from '#~/api.js'
14
+ import { useSkillsCliModalController } from './@hooks/use-skills-cli-modal-controller'
15
+ import { useSkillsTabActions } from './@hooks/use-skills-tab-actions'
22
16
  import { ProjectSkillsList } from './ProjectSkillsList'
23
17
  import { SkillArchiveInput } from './SkillArchiveInput'
24
18
  import { SkillMarketView } from './SkillMarketView'
25
19
  import { SkillRegistryModal } from './SkillRegistryModal'
26
20
  import { SkillsCliModal } from './SkillsCliModal'
27
- import type { SkillsCliFormValues } from './SkillsCliModal'
28
21
  import { SkillsTabActions } from './SkillsTabActions'
29
22
  import { TabContent } from './TabContent'
30
23
  import { ALL_REGISTRIES, filterProjectSkills } from './skill-hub-utils'
@@ -33,15 +26,6 @@ import { useSkillMarketFilters } from './use-skill-market-filters'
33
26
  import { useSkillMarketSearch } from './use-skill-market-search'
34
27
  import { useSkillRegistryModal } from './use-skill-registry-modal'
35
28
 
36
- const SKILLS_CLI_INITIAL_LIMIT = 100
37
- const SKILLS_CLI_LIMIT_STEP = 100
38
- const SKILLS_CLI_MAX_LIMIT = 500
39
-
40
- const trimOptionalString = (value: string | undefined) => {
41
- const normalizedValue = value?.trim()
42
- return normalizedValue == null || normalizedValue === '' ? undefined : normalizedValue
43
- }
44
-
45
29
  interface SkillsTabProps {
46
30
  leading?: ReactNode
47
31
  installFilter: SkillHubInstallFilter
@@ -82,20 +66,6 @@ export function SkillsTab({
82
66
  const { t } = useTranslation()
83
67
  const { message } = App.useApp()
84
68
  const navigate = useNavigate()
85
- const importInputRef = React.useRef<HTMLInputElement | null>(null)
86
- const [installingId, setInstallingId] = React.useState<string | null>(null)
87
- const [importing, setImporting] = React.useState(false)
88
- const [skillsCliOpen, setSkillsCliOpen] = React.useState(false)
89
- const [skillsCliSearching, setSkillsCliSearching] = React.useState(false)
90
- const [skillsCliLoadingMore, setSkillsCliLoadingMore] = React.useState(false)
91
- const [skillsCliInstallingId, setSkillsCliInstallingId] = React.useState<string | null>(null)
92
- const [skillsCliItems, setSkillsCliItems] = React.useState<SkillHubItem[]>([])
93
- const [skillsCliError, setSkillsCliError] = React.useState<string | null>(null)
94
- const [skillsCliHasMore, setSkillsCliHasMore] = React.useState(false)
95
- const [skillsCliHasSearched, setSkillsCliHasSearched] = React.useState(false)
96
- const [skillsCliLimit, setSkillsCliLimit] = React.useState(SKILLS_CLI_INITIAL_LIMIT)
97
- const [skillsCliResetKey, setSkillsCliResetKey] = React.useState('')
98
- const [skillsCliForm] = Form.useForm<SkillsCliFormValues>()
99
69
  const {
100
70
  data: skillsRes,
101
71
  isLoading: isSkillsLoading,
@@ -109,6 +79,19 @@ export function SkillsTab({
109
79
  mutateHub: marketSearch.mutate,
110
80
  setRegistry: onRegistryChange
111
81
  })
82
+ const actions = useSkillsTabActions({
83
+ marketMutate: marketSearch.mutate,
84
+ message,
85
+ mutateConfig,
86
+ mutateSkills,
87
+ onRefresh,
88
+ t
89
+ })
90
+ const skillsCliModal = useSkillsCliModalController({
91
+ message,
92
+ mutateSkills,
93
+ t
94
+ })
112
95
 
113
96
  const skills = skillsRes?.skills ?? []
114
97
  const registries = marketSearch.data?.registries ?? []
@@ -124,156 +107,20 @@ export function SkillsTab({
124
107
  ...registries.map(item => ({ label: item.id, value: item.id }))
125
108
  ], [registries, t])
126
109
 
127
- const handleRefresh = async () => {
128
- await Promise.all([mutateSkills(), marketSearch.mutate(), mutateConfig(), onRefresh()])
129
- }
130
-
131
- const handleInstall = async (item: SkillHubItem) => {
132
- setInstallingId(item.id)
133
- try {
134
- await installSkillHubItem({
135
- registry: item.registry,
136
- plugin: item.installRef ?? item.name,
137
- force: item.installed
138
- })
139
- await Promise.all([marketSearch.mutate(), mutateSkills()])
140
- void message.success(t('knowledge.skills.installSuccess'))
141
- } catch (error) {
142
- void message.error(getApiErrorMessage(error, t('knowledge.skills.installFailed')))
143
- } finally {
144
- setInstallingId(null)
145
- }
146
- }
147
-
148
- const handleImportArchive = async (file: File) => {
149
- setImporting(true)
150
- try {
151
- const result = await importSkillArchive(file)
152
- await Promise.all([mutateSkills(), onRefresh()])
153
- void message.success(t('knowledge.skills.importSuccess', { count: result.fileCount }))
154
- } catch (error) {
155
- void message.error(getApiErrorMessage(error, t('knowledge.skills.importFailed')))
156
- } finally {
157
- setImporting(false)
158
- }
159
- }
160
-
161
- const resolveSkillsCliRequest = React.useCallback(async () => {
162
- try {
163
- await skillsCliForm.validateFields(['source'])
164
- } catch {
165
- return undefined
166
- }
167
-
168
- const values = skillsCliForm.getFieldsValue()
169
- const source = values.source.trim()
170
- return {
171
- source,
172
- query: trimOptionalString(values.query),
173
- registry: trimOptionalString(values.registry)
174
- }
175
- }, [skillsCliForm])
176
-
177
- const runSkillsCliSearch = React.useCallback(async (nextLimit: number) => {
178
- const request = await resolveSkillsCliRequest()
179
- if (request == null) return false
180
-
181
- const result = await searchSkillsCli({
182
- limit: nextLimit,
183
- source: request.source,
184
- ...(request.query != null ? { query: request.query } : {}),
185
- ...(request.registry != null ? { registry: request.registry } : {})
186
- })
187
-
188
- setSkillsCliHasSearched(true)
189
- setSkillsCliLimit(nextLimit)
190
- setSkillsCliItems(result.items)
191
- setSkillsCliError(result.error ?? null)
192
- setSkillsCliHasMore(result.hasMore === true && nextLimit < SKILLS_CLI_MAX_LIMIT)
193
- setSkillsCliResetKey([request.source, request.query ?? '', request.registry ?? '', String(nextLimit)].join('\0'))
194
- return true
195
- }, [resolveSkillsCliRequest])
196
-
197
- const handleSearchSkillsCli = async () => {
198
- setSkillsCliSearching(true)
199
- setSkillsCliLoadingMore(false)
200
- try {
201
- await runSkillsCliSearch(SKILLS_CLI_INITIAL_LIMIT)
202
- } catch (error) {
203
- void message.error(getApiErrorMessage(error, t('knowledge.skills.skillsCliSearchFailed')))
204
- } finally {
205
- setSkillsCliSearching(false)
206
- }
207
- }
208
-
209
- const handleLoadMoreSkillsCli = async () => {
210
- const nextLimit = Math.min(skillsCliLimit + SKILLS_CLI_LIMIT_STEP, SKILLS_CLI_MAX_LIMIT)
211
- if (nextLimit === skillsCliLimit) return
212
-
213
- setSkillsCliLoadingMore(true)
214
- try {
215
- await runSkillsCliSearch(nextLimit)
216
- } catch (error) {
217
- void message.error(getApiErrorMessage(error, t('knowledge.skills.skillsCliSearchFailed')))
218
- } finally {
219
- setSkillsCliLoadingMore(false)
220
- }
221
- }
222
-
223
- const handleInstallSkillsCli = async (item: SkillHubItem) => {
224
- const request = await resolveSkillsCliRequest()
225
- if (request == null) return
226
-
227
- setSkillsCliInstallingId(item.id)
228
- try {
229
- await installSkillsCliItem({
230
- source: request.source,
231
- skill: item.installRef ?? item.name,
232
- force: item.installed,
233
- ...(request.registry != null ? { registry: request.registry } : {})
234
- })
235
- await mutateSkills()
236
- try {
237
- await runSkillsCliSearch(skillsCliLimit)
238
- } catch {
239
- // Keep the install success state even if refreshing the source list fails afterwards.
240
- }
241
- void message.success(t('knowledge.skills.installSuccess'))
242
- } catch (error) {
243
- void message.error(getApiErrorMessage(error, t('knowledge.skills.installFailed')))
244
- } finally {
245
- setSkillsCliInstallingId(null)
246
- }
247
- }
248
-
249
- const handleCloseSkillsCli = React.useCallback(() => {
250
- setSkillsCliOpen(false)
251
- setSkillsCliSearching(false)
252
- setSkillsCliLoadingMore(false)
253
- setSkillsCliInstallingId(null)
254
- setSkillsCliItems([])
255
- setSkillsCliError(null)
256
- setSkillsCliHasMore(false)
257
- setSkillsCliHasSearched(false)
258
- setSkillsCliLimit(SKILLS_CLI_INITIAL_LIMIT)
259
- setSkillsCliResetKey('')
260
- skillsCliForm.resetFields()
261
- }, [skillsCliForm])
262
-
263
110
  return (
264
111
  <TabContent className='knowledge-base-view__skills-tab'>
265
112
  <SkillsTabActions
266
- importing={importing}
113
+ importing={actions.importing}
267
114
  leading={leading}
268
115
  viewMode={viewMode}
269
- onRefresh={() => void handleRefresh()}
270
- onImport={() => importInputRef.current?.click()}
116
+ onRefresh={() => void actions.handleRefresh()}
117
+ onImport={() => actions.importInputRef.current?.click()}
271
118
  onOpenConfig={() => navigate('/config?tab=plugins')}
272
119
  onViewModeChange={onViewModeChange}
273
120
  />
274
121
  <SkillArchiveInput
275
- inputRef={importInputRef}
276
- onSelect={(file) => void handleImportArchive(file)}
122
+ inputRef={actions.importInputRef}
123
+ onSelect={(file) => void actions.handleImportArchive(file)}
277
124
  />
278
125
  {viewMode === 'project' && (
279
126
  <ProjectSkillsList
@@ -286,7 +133,7 @@ export function SkillsTab({
286
133
  {viewMode === 'market' && (
287
134
  <SkillMarketView
288
135
  hubItems={marketFilters.filteredHubItems}
289
- installingId={installingId}
136
+ installingId={actions.installingId}
290
137
  installFilter={installFilter}
291
138
  isLoading={marketSearch.isLoading && hubItems.length === 0}
292
139
  query={marketQuery}
@@ -299,8 +146,8 @@ export function SkillsTab({
299
146
  canLoadMore={marketSearch.canLoadMore}
300
147
  loadingMore={marketSearch.isValidating && hubItems.length > 0}
301
148
  onAddRegistry={() => registryModal.setOpen(true)}
302
- onOpenSkillsCli={() => setSkillsCliOpen(true)}
303
- onInstall={handleInstall}
149
+ onOpenSkillsCli={() => skillsCliModal.setOpen(true)}
150
+ onInstall={actions.handleInstall}
304
151
  onInstallFilterChange={onInstallFilterChange}
305
152
  onLoadMore={marketSearch.loadMore}
306
153
  onQueryChange={onMarketQueryChange}
@@ -318,20 +165,20 @@ export function SkillsTab({
318
165
  onClose={() => registryModal.setOpen(false)}
319
166
  />
320
167
  <SkillsCliModal
321
- canLoadMore={skillsCliHasMore}
322
- form={skillsCliForm}
323
- hasSearched={skillsCliHasSearched}
324
- installingId={skillsCliInstallingId}
325
- items={skillsCliItems}
326
- loadingMore={skillsCliLoadingMore}
327
- open={skillsCliOpen}
328
- resetKey={skillsCliResetKey}
329
- searchError={skillsCliError}
330
- searching={skillsCliSearching}
331
- onClose={handleCloseSkillsCli}
332
- onInstall={handleInstallSkillsCli}
333
- onLoadMore={() => void handleLoadMoreSkillsCli()}
334
- onSearch={() => void handleSearchSkillsCli()}
168
+ canLoadMore={skillsCliModal.hasMore}
169
+ form={skillsCliModal.form}
170
+ hasSearched={skillsCliModal.hasSearched}
171
+ installingId={skillsCliModal.installingId}
172
+ items={skillsCliModal.items}
173
+ loadingMore={skillsCliModal.loadingMore}
174
+ open={skillsCliModal.open}
175
+ resetKey={skillsCliModal.resetKey}
176
+ searchError={skillsCliModal.searchError}
177
+ searching={skillsCliModal.searching}
178
+ onClose={skillsCliModal.handleClose}
179
+ onInstall={skillsCliModal.handleInstall}
180
+ onLoadMore={() => void skillsCliModal.handleLoadMore()}
181
+ onSearch={() => void skillsCliModal.handleSearch()}
335
182
  />
336
183
  </TabContent>
337
184
  )
@@ -1,13 +1,17 @@
1
1
  import './SidebarHeader.scss'
2
2
 
3
3
  import { Button, Tooltip } from 'antd'
4
+ import { useAtomValue } from 'jotai'
4
5
  import React, { useState } from 'react'
5
6
  import { useTranslation } from 'react-i18next'
6
7
 
7
8
  import { SidebarListHeader } from '#~/components/sidebar-list/SidebarListHeader'
8
9
  import { useResponsiveLayout } from '#~/hooks/use-responsive-layout'
9
10
  import type { SidebarSessionSortOrder } from '#~/hooks/use-sidebar-query-state'
11
+ import { sessionListSearchThresholdAtom } from '#~/store/index'
12
+
10
13
  import { SidebarHeaderSearchActions } from './SidebarHeaderSearchActions'
14
+ import { shouldShowSidebarSearchRow } from './sidebar-search-visibility'
11
15
 
12
16
  interface SidebarHeaderProps {
13
17
  adapterFilters: string[]
@@ -21,6 +25,7 @@ interface SidebarHeaderProps {
21
25
  isSidebarCollapsed: boolean
22
26
  searchQuery: string
23
27
  selectedCount: number
28
+ sessionCount: number
24
29
  sortOrder: SidebarSessionSortOrder
25
30
  sortSelection?: SidebarSessionSortOrder
26
31
  shortcutLabel: string
@@ -52,6 +57,7 @@ export function SidebarHeader({
52
57
  isSidebarCollapsed,
53
58
  searchQuery,
54
59
  selectedCount,
60
+ sessionCount,
55
61
  sortOrder,
56
62
  sortSelection,
57
63
  shortcutLabel,
@@ -72,8 +78,16 @@ export function SidebarHeader({
72
78
  }: SidebarHeaderProps) {
73
79
  const { t } = useTranslation()
74
80
  const { isTouchInteraction } = useResponsiveLayout()
81
+ const sessionListSearchThreshold = useAtomValue(sessionListSearchThresholdAtom)
75
82
  const [isSearchActionsOpen, setIsSearchActionsOpen] = useState(false)
76
83
  const shouldShowSearchActions = !isSidebarCollapsed && (isSearchActionsOpen || isBatchMode)
84
+ const shouldShowSearchRow = !isSidebarCollapsed && shouldShowSidebarSearchRow({
85
+ hasActiveSearchControls,
86
+ isBatchMode,
87
+ isSearchActionsOpen,
88
+ sessionCount,
89
+ threshold: sessionListSearchThreshold
90
+ })
77
91
 
78
92
  const primaryAction = !isSidebarCollapsed
79
93
  ? (
@@ -141,6 +155,35 @@ export function SidebarHeader({
141
155
  </Tooltip>
142
156
  )
143
157
 
158
+ const headerContent = shouldShowSearchRow
159
+ ? (
160
+ <SidebarHeaderSearchActions
161
+ adapterFilters={adapterFilters}
162
+ availableAdapters={availableAdapters}
163
+ availableTags={availableTags}
164
+ hasActiveSearchControls={hasActiveSearchControls}
165
+ isBatchMode={isBatchMode}
166
+ searchQuery={searchQuery}
167
+ selectedCount={selectedCount}
168
+ shouldShowSearchActions={shouldShowSearchActions}
169
+ sortOrder={sortOrder}
170
+ sortSelection={sortSelection}
171
+ tagFilters={tagFilters}
172
+ totalCount={totalCount}
173
+ onBatchArchive={onBatchArchive}
174
+ onBatchDelete={onBatchDelete}
175
+ onBatchStar={onBatchStar}
176
+ onAdapterFilterChange={onAdapterFilterChange}
177
+ onSearchChange={onSearchChange}
178
+ onSortOrderChange={onSortOrderChange}
179
+ onSelectAll={onSelectAll}
180
+ onTagFilterChange={onTagFilterChange}
181
+ onToggleBatchMode={onToggleBatchMode}
182
+ onToggleSearchActions={() => setIsSearchActionsOpen((prev) => !prev)}
183
+ />
184
+ )
185
+ : null
186
+
144
187
  return (
145
188
  <SidebarListHeader
146
189
  className='sidebar-header'
@@ -149,32 +192,7 @@ export function SidebarHeader({
149
192
  primaryAction={primaryAction}
150
193
  sideAction={sideAction}
151
194
  >
152
- {!isSidebarCollapsed && (
153
- <SidebarHeaderSearchActions
154
- adapterFilters={adapterFilters}
155
- availableAdapters={availableAdapters}
156
- availableTags={availableTags}
157
- hasActiveSearchControls={hasActiveSearchControls}
158
- isBatchMode={isBatchMode}
159
- searchQuery={searchQuery}
160
- selectedCount={selectedCount}
161
- shouldShowSearchActions={shouldShowSearchActions}
162
- sortOrder={sortOrder}
163
- sortSelection={sortSelection}
164
- tagFilters={tagFilters}
165
- totalCount={totalCount}
166
- onBatchArchive={onBatchArchive}
167
- onBatchDelete={onBatchDelete}
168
- onBatchStar={onBatchStar}
169
- onAdapterFilterChange={onAdapterFilterChange}
170
- onSearchChange={onSearchChange}
171
- onSortOrderChange={onSortOrderChange}
172
- onSelectAll={onSelectAll}
173
- onTagFilterChange={onTagFilterChange}
174
- onToggleBatchMode={onToggleBatchMode}
175
- onToggleSearchActions={() => setIsSearchActionsOpen((prev) => !prev)}
176
- />
177
- )}
195
+ {headerContent}
178
196
  </SidebarListHeader>
179
197
  )
180
198
  }
@@ -0,0 +1,18 @@
1
+ export const shouldShowSidebarSearchRow = ({
2
+ hasActiveSearchControls,
3
+ isBatchMode,
4
+ isSearchActionsOpen,
5
+ sessionCount,
6
+ threshold
7
+ }: {
8
+ hasActiveSearchControls: boolean
9
+ isBatchMode: boolean
10
+ isSearchActionsOpen: boolean
11
+ sessionCount: number
12
+ threshold: number
13
+ }) => {
14
+ if (threshold <= 0) return true
15
+ if (hasActiveSearchControls || isBatchMode || isSearchActionsOpen) return true
16
+
17
+ return sessionCount > threshold
18
+ }
@@ -1,5 +1,7 @@
1
1
  import { useCallback, useMemo } from 'react'
2
- import { useSearchParams } from 'react-router-dom'
2
+ import { useNavigate, useSearchParams } from 'react-router-dom'
3
+
4
+ import { getClientBase } from '#~/runtime-config.js'
3
5
 
4
6
  type QueryParamKey<T> = Extract<keyof T, string>
5
7
 
@@ -9,12 +11,79 @@ interface QueryParamConfig<T extends Record<string, string>> {
9
11
  omit?: Partial<Record<QueryParamKey<T>, (value: string) => boolean>>
10
12
  }
11
13
 
14
+ export const resolveQueryParamPathname = (rawPathname: string, clientBase: string) => {
15
+ if (!rawPathname.startsWith(clientBase)) {
16
+ return rawPathname === '' ? '/' : rawPathname
17
+ }
18
+
19
+ const relativePath = rawPathname.slice(clientBase.length)
20
+ if (relativePath === '') {
21
+ return '/'
22
+ }
23
+
24
+ return relativePath.startsWith('/') ? relativePath : `/${relativePath}`
25
+ }
26
+
27
+ export const buildQueryParamNavigationTarget = <T extends Record<string, string>>({
28
+ clientBase,
29
+ currentHash,
30
+ currentPathname,
31
+ currentSearch,
32
+ defaults,
33
+ keySet,
34
+ keys,
35
+ omit,
36
+ patch
37
+ }: {
38
+ clientBase: string
39
+ currentHash: string
40
+ currentPathname: string
41
+ currentSearch: string
42
+ defaults?: Partial<T>
43
+ keySet: Set<string>
44
+ keys: QueryParamKey<T>[]
45
+ omit?: Partial<Record<QueryParamKey<T>, (value: string) => boolean>>
46
+ patch: Partial<T>
47
+ }) => {
48
+ const currentSearchParams = new URLSearchParams(currentSearch)
49
+ const nextParams = new URLSearchParams()
50
+ const merged = keys.reduce((acc, key) => {
51
+ const raw = currentSearchParams.get(key)
52
+ const fallback = defaults?.[key] ?? ''
53
+ acc[key] = (raw ?? fallback) as T[QueryParamKey<T>]
54
+ return acc
55
+ }, {} as T)
56
+
57
+ Object.assign(merged, patch)
58
+
59
+ keys.forEach((key) => {
60
+ const value = merged[key] ?? ''
61
+ const shouldOmit = value === '' || (omit?.[key] ? omit[key]!(value) : false)
62
+ if (!shouldOmit) nextParams.set(key, value)
63
+ })
64
+
65
+ for (const [key, value] of currentSearchParams.entries()) {
66
+ if (!keySet.has(key)) nextParams.append(key, value)
67
+ }
68
+
69
+ if (nextParams.toString() === currentSearchParams.toString()) {
70
+ return null
71
+ }
72
+
73
+ return {
74
+ pathname: resolveQueryParamPathname(currentPathname, clientBase),
75
+ search: nextParams.toString() === '' ? '' : `?${nextParams.toString()}`,
76
+ hash: currentHash
77
+ }
78
+ }
79
+
12
80
  export const useQueryParams = <T extends Record<string, string>>({
13
81
  keys,
14
82
  defaults,
15
83
  omit
16
84
  }: QueryParamConfig<T>) => {
17
- const [searchParams, setSearchParams] = useSearchParams()
85
+ const navigate = useNavigate()
86
+ const [searchParams] = useSearchParams()
18
87
  const keySet = useMemo(() => new Set<string>(keys), [keys])
19
88
 
20
89
  const values = useMemo(() => {
@@ -27,30 +96,29 @@ export const useQueryParams = <T extends Record<string, string>>({
27
96
  }, [defaults, keys, searchParams])
28
97
 
29
98
  const update = useCallback((patch: Partial<T>) => {
30
- const nextParams = new URLSearchParams()
31
- const merged = keys.reduce((acc, key) => {
32
- const raw = searchParams.get(key)
33
- const fallback = defaults?.[key] ?? ''
34
- acc[key] = (raw ?? fallback) as T[QueryParamKey<T>]
35
- return acc
36
- }, {} as T)
37
-
38
- Object.assign(merged, patch)
39
-
40
- keys.forEach((key) => {
41
- const value = merged[key] ?? ''
42
- const shouldOmit = value === '' || (omit?.[key] ? omit[key]!(value) : false)
43
- if (!shouldOmit) nextParams.set(key, value)
99
+ const currentLocation = typeof window === 'undefined'
100
+ ? {
101
+ hash: '',
102
+ pathname: '/',
103
+ search: searchParams.toString() === '' ? '' : `?${searchParams.toString()}`
104
+ }
105
+ : window.location
106
+ const target = buildQueryParamNavigationTarget<T>({
107
+ clientBase: getClientBase(),
108
+ currentHash: currentLocation.hash,
109
+ currentPathname: currentLocation.pathname,
110
+ currentSearch: currentLocation.search,
111
+ defaults,
112
+ keySet,
113
+ keys,
114
+ omit,
115
+ patch
44
116
  })
45
117
 
46
- for (const [key, value] of searchParams.entries()) {
47
- if (!keySet.has(key)) nextParams.append(key, value)
48
- }
49
-
50
- if (nextParams.toString() !== searchParams.toString()) {
51
- setSearchParams(nextParams, { replace: true })
118
+ if (target != null) {
119
+ void navigate(target, { replace: true })
52
120
  }
53
- }, [defaults, keySet, keys, omit, searchParams, setSearchParams])
121
+ }, [defaults, keySet, keys, navigate, omit, searchParams])
54
122
 
55
123
  return { values, update, searchParams }
56
124
  }
@@ -1945,15 +1945,27 @@
1945
1945
  "about": "About"
1946
1946
  },
1947
1947
  "appSettings": {
1948
+ "themeMode": {
1949
+ "label": "Theme mode",
1950
+ "desc": "Switch between light, dark, or system theme for the interface"
1951
+ },
1948
1952
  "senderHeaderDisplay": {
1949
1953
  "label": "Composer header default",
1950
1954
  "desc": "Default expand or collapse behavior for the composer header when the URL does not override it",
1951
1955
  "expanded": "Expanded by default",
1952
1956
  "collapsed": "Collapsed by default"
1953
1957
  },
1958
+ "sessionListSearchThreshold": {
1959
+ "label": "Session list search threshold",
1960
+ "desc": "Only show the search and filter row when the session count exceeds this value; use 0 to always show it"
1961
+ },
1954
1962
  "announcements": {
1955
1963
  "label": "Show announcements",
1956
1964
  "desc": "Show announcements when creating a new session"
1965
+ },
1966
+ "recommendedActions": {
1967
+ "label": "Show recommended actions",
1968
+ "desc": "Show the recommended action list when creating a new session"
1957
1969
  }
1958
1970
  }
1959
1971
  }
@@ -1946,15 +1946,27 @@
1946
1946
  "about": "关于"
1947
1947
  },
1948
1948
  "appSettings": {
1949
+ "themeMode": {
1950
+ "label": "主题模式",
1951
+ "desc": "切换浅色、深色或跟随系统的界面主题"
1952
+ },
1949
1953
  "senderHeaderDisplay": {
1950
1954
  "label": "输入区顶栏默认展示",
1951
1955
  "desc": "链接未显式指定时,默认展开或折叠输入区顶栏",
1952
1956
  "expanded": "默认展开",
1953
1957
  "collapsed": "默认折叠"
1954
1958
  },
1959
+ "sessionListSearchThreshold": {
1960
+ "label": "会话列表搜索展示阈值",
1961
+ "desc": "只有会话数量超过该值时才展示搜索和筛选区域,0 表示始终展示"
1962
+ },
1955
1963
  "announcements": {
1956
1964
  "label": "显示公告",
1957
1965
  "desc": "新建会话时展示公告信息"
1966
+ },
1967
+ "recommendedActions": {
1968
+ "label": "显示推荐操作",
1969
+ "desc": "新建会话时展示推荐操作列表"
1958
1970
  }
1959
1971
  }
1960
1972
  }