@vibe-forge/client 2.0.1 → 3.0.0-alpha.1

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-CLPB686k.js} +1 -1
  2. package/dist/assets/{blockDiagram-c4efeb88-BEp50UHp.js → blockDiagram-c4efeb88-DMStYteY.js} +1 -1
  3. package/dist/assets/{c4Diagram-c83219d4-C5w55JzM.js → c4Diagram-c83219d4-rf9gMSjH.js} +1 -1
  4. package/dist/assets/channel-DVzVHik1.js +1 -0
  5. package/dist/assets/{classDiagram-beda092f-CQJVtHEy.js → classDiagram-beda092f-C4vQa5QZ.js} +1 -1
  6. package/dist/assets/{classDiagram-v2-2358418a-B37Xl9jB.js → classDiagram-v2-2358418a-f9zo4MCR.js} +1 -1
  7. package/dist/assets/clone-Bqdku2Xw.js +1 -0
  8. package/dist/assets/{createText-1719965b-9YwvWMdV.js → createText-1719965b-D_7VWOAv.js} +1 -1
  9. package/dist/assets/{cssMode-BX88r5f4.js → cssMode-TBU0lKLl.js} +1 -1
  10. package/dist/assets/{edges-96097737-CNHoXVrD.js → edges-96097737-C4aqXnw4.js} +1 -1
  11. package/dist/assets/{erDiagram-0228fc6a-BoYldy0g.js → erDiagram-0228fc6a-CUOFDP88.js} +1 -1
  12. package/dist/assets/{flowDb-c6c81e3f-CoPw_R-Q.js → flowDb-c6c81e3f-CtPmxsKO.js} +1 -1
  13. package/dist/assets/{flowDiagram-50d868cf-nCqbSXd-.js → flowDiagram-50d868cf-BxJQf6xZ.js} +1 -1
  14. package/dist/assets/flowDiagram-v2-4f6560a1-UwlDcmES.js +1 -0
  15. package/dist/assets/{flowchart-elk-definition-6af322e1-BwMuPTrV.js → flowchart-elk-definition-6af322e1-Dit-sZjo.js} +1 -1
  16. package/dist/assets/{freemarker2-DUFDSvgj.js → freemarker2-CTXFNqpX.js} +1 -1
  17. package/dist/assets/{ganttDiagram-a2739b55-CLNH3S_C.js → ganttDiagram-a2739b55-CULsxs9m.js} +1 -1
  18. package/dist/assets/{gitGraphDiagram-82fe8481-uDu1ectX.js → gitGraphDiagram-82fe8481-C6B6ctTM.js} +1 -1
  19. package/dist/assets/{graph-DuC4kt4I.js → graph-E5s7kY4o.js} +1 -1
  20. package/dist/assets/{handlebars-BSd4a6l9.js → handlebars-VV2sVWpA.js} +1 -1
  21. package/dist/assets/{html-H48gEjQd.js → html-BaQZhNyu.js} +1 -1
  22. package/dist/assets/{htmlMode-Nqw7-Nqh.js → htmlMode-DC-T7U3Z.js} +1 -1
  23. package/dist/assets/{index-5325376f-rnz0GXAT.js → index-5325376f-BcHntDDH.js} +1 -1
  24. package/dist/assets/{index-DeQLT67a.js → index-Dc8IL8wB.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-DBhMGcJZ.js} +1 -1
  27. package/dist/assets/{javascript-0g2herYV.js → javascript-D1mh-XtA.js} +1 -1
  28. package/dist/assets/{journeyDiagram-c64418c1-DLldlz0H.js → journeyDiagram-c64418c1-C6ccHNG_.js} +1 -1
  29. package/dist/assets/{jsonMode-CN5ZURMh.js → jsonMode-BcVpgG1G.js} +1 -1
  30. package/dist/assets/{layout-QKUiDNJK.js → layout-DQUeOk09.js} +1 -1
  31. package/dist/assets/{line-CeP3XWjD.js → line-BNSZ8woB.js} +1 -1
  32. package/dist/assets/{linear-74cQVgWT.js → linear-FDIGQp-F.js} +1 -1
  33. package/dist/assets/{liquid-B6cRrfrb.js → liquid-Yp7v-HuI.js} +1 -1
  34. package/dist/assets/{lspLanguageFeatures-C5ogOh5E.js → lspLanguageFeatures-y-HB9mm1.js} +1 -1
  35. package/dist/assets/{mdx-BBIy-KRj.js → mdx-BzbOmj2n.js} +1 -1
  36. package/dist/assets/{mermaid.core-BhdbV0mr.js → mermaid.core-BZWBFlxL.js} +4 -4
  37. package/dist/assets/{mindmap-definition-8da855dc-B67VKJuD.js → mindmap-definition-8da855dc-Bx0W62wP.js} +1 -1
  38. package/dist/assets/{pieDiagram-a8764435-Cxv9WY_E.js → pieDiagram-a8764435-ICiizDcw.js} +1 -1
  39. package/dist/assets/{python-CBdGo8__.js → python--itpZziZ.js} +1 -1
  40. package/dist/assets/{quadrantDiagram-1e28029f-BTkj65P_.js → quadrantDiagram-1e28029f-DPLXwibA.js} +1 -1
  41. package/dist/assets/{razor-azKH0Dwj.js → razor-BY3vF3Z2.js} +1 -1
  42. package/dist/assets/{requirementDiagram-08caed73-D4jVXpOT.js → requirementDiagram-08caed73-Czz2Ayg6.js} +1 -1
  43. package/dist/assets/{sankeyDiagram-a04cb91d-CXhutIA1.js → sankeyDiagram-a04cb91d-D82XBO4T.js} +1 -1
  44. package/dist/assets/{sequenceDiagram-c5b8d532-B56TTZlx.js → sequenceDiagram-c5b8d532-BiyoxjF7.js} +1 -1
  45. package/dist/assets/{stateDiagram-1ecb1508-Cs0plMcS.js → stateDiagram-1ecb1508-BtohcrjI.js} +1 -1
  46. package/dist/assets/{stateDiagram-v2-c2b004d7-LSJaXPJN.js → stateDiagram-v2-c2b004d7-Bte1ATNh.js} +1 -1
  47. package/dist/assets/{styles-b4e223ce-UdXfHMuu.js → styles-b4e223ce-BB-1jLao.js} +1 -1
  48. package/dist/assets/{styles-ca3715f6-EuRy_hTu.js → styles-ca3715f6-Dv3R8M8l.js} +1 -1
  49. package/dist/assets/{styles-d45a18b0-B24zVoK3.js → styles-d45a18b0-CslmN8E7.js} +1 -1
  50. package/dist/assets/{svgDrawCommon-b86b1483-B2S0NW3K.js → svgDrawCommon-b86b1483-C9wC1rgS.js} +1 -1
  51. package/dist/assets/{timeline-definition-faaaa080-DFWKh9mU.js → timeline-definition-faaaa080-DUFt2nhX.js} +1 -1
  52. package/dist/assets/{tsMode-FZsHWiOn.js → tsMode-xH26JMT6.js} +1 -1
  53. package/dist/assets/{typescript-CYdJ3s3D.js → typescript-DBlHiZkC.js} +1 -1
  54. package/dist/assets/{xml-C16X_hpZ.js → xml-DvpPym76.js} +1 -1
  55. package/dist/assets/{xychartDiagram-f5964ef8-DyBiBYci.js → xychartDiagram-f5964ef8-Brmas6JU.js} +1 -1
  56. package/dist/assets/{yaml-CRjA4-Rj.js → yaml-BGpvKN7u.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,11 +1,11 @@
1
1
  import './NewSessionGuide.scss'
2
2
 
3
- import { useAtom } from 'jotai'
3
+ import { useAtom, useAtomValue } from 'jotai'
4
4
 
5
5
  import type { ConversationStarterConfig } from '@vibe-forge/types'
6
6
 
7
7
  import { MarkdownContent } from '#~/components/MarkdownContent'
8
- import { showAnnouncementsAtom } from '#~/store/index.js'
8
+ import { showAnnouncementsAtom, showNewSessionStarterListAtom } from '#~/store/index.js'
9
9
 
10
10
  import { NewSessionGuideStarterList } from './NewSessionGuideStarterList'
11
11
 
@@ -21,8 +21,12 @@ export function NewSessionGuide({
21
21
  onApplyStarter: (starter: ConversationStarterConfig) => void
22
22
  }) {
23
23
  const [showAnnouncements, setShowAnnouncements] = useAtom(showAnnouncementsAtom)
24
+ const showNewSessionStarterList = useAtomValue(showNewSessionStarterListAtom)
24
25
  const visibleAnnouncements = showAnnouncements ? announcements : []
25
- const hasGuideContent = visibleAnnouncements.length > 0 || startupPresets.length > 0 || builtinActions.length > 0
26
+ const visibleStartupPresets = showNewSessionStarterList ? startupPresets : []
27
+ const visibleBuiltinActions = showNewSessionStarterList ? builtinActions : []
28
+ const hasStarterList = visibleStartupPresets.length > 0 || visibleBuiltinActions.length > 0
29
+ const hasGuideContent = visibleAnnouncements.length > 0 || hasStarterList
26
30
 
27
31
  if (!hasGuideContent) {
28
32
  return null
@@ -51,13 +55,15 @@ export function NewSessionGuide({
51
55
  </button>
52
56
  </div>
53
57
  )}
54
- <div className='new-session-guide__main'>
55
- <NewSessionGuideStarterList
56
- startupPresets={startupPresets}
57
- builtinActions={builtinActions}
58
- onApplyStarter={onApplyStarter}
59
- />
60
- </div>
58
+ {hasStarterList && (
59
+ <div className='new-session-guide__main'>
60
+ <NewSessionGuideStarterList
61
+ startupPresets={visibleStartupPresets}
62
+ builtinActions={visibleBuiltinActions}
63
+ onApplyStarter={onApplyStarter}
64
+ />
65
+ </div>
66
+ )}
61
67
  </div>
62
68
  )
63
69
  }
@@ -1,8 +1,11 @@
1
- import type { ConversationStarterConfig } from '@vibe-forge/types'
2
1
  import { useEffect, useMemo, useRef, useState } from 'react'
3
2
  import { useTranslation } from 'react-i18next'
3
+
4
+ import type { ConversationStarterConfig } from '@vibe-forge/types'
5
+
4
6
  import { NewSessionGuideStarterSection } from './NewSessionGuideStarterSection'
5
7
  import { buildConversationStarterListItems, partitionConversationStarterListItems } from './new-session-guide-config'
8
+
6
9
  const FAVORITE_STARTER_STORAGE_KEY = 'vf_new_session_guide_favorites'
7
10
  const RECENT_STARTER_STORAGE_KEY = 'vf_new_session_guide_recent'
8
11
  const DEFAULT_VISIBLE_STARTER_COUNT = 8
@@ -1,7 +1,8 @@
1
- import type { ConversationStarterConfig } from '@vibe-forge/types'
2
1
  import { Tooltip } from 'antd'
3
2
  import { useTranslation } from 'react-i18next'
4
3
 
4
+ import type { ConversationStarterConfig } from '@vibe-forge/types'
5
+
5
6
  import type { ConversationStarterListItem } from './new-session-guide-config'
6
7
  import { normalizeConversationStarterMode } from './new-session-guide-config'
7
8
 
@@ -29,6 +29,7 @@ interface MessageItemProps {
29
29
  sessionId?: string
30
30
  sessionInfo?: SessionInfo | null
31
31
  isSessionBusy: boolean
32
+ hideActionButtons: boolean
32
33
  isEditing: boolean
33
34
  isCompactLayout: boolean
34
35
  isTouchInteraction: boolean
@@ -54,6 +55,7 @@ function MessageItemComponent({
54
55
  sessionId,
55
56
  sessionInfo,
56
57
  isSessionBusy,
58
+ hideActionButtons,
57
59
  isEditing,
58
60
  isCompactLayout,
59
61
  isTouchInteraction,
@@ -83,9 +85,11 @@ function MessageItemComponent({
83
85
  const canRecall = isPersistedMessage && !isSessionBusy && isUser
84
86
  const canFork = isPersistedMessage && !isSessionBusy && isUser
85
87
  const canCopy = copyableText != null
86
- const shouldShowAssistantActions = !isUser && showAssistantActions
88
+ const shouldShowAssistantActions = !hideActionButtons && !isUser && showAssistantActions
87
89
  const showCompactActionMenu = isCompactLayout || isTouchInteraction
88
- const shouldShowCompactActionMenu = showCompactActionMenu && (isUser || shouldShowAssistantActions)
90
+ const shouldShowCompactActionMenu = !hideActionButtons &&
91
+ showCompactActionMenu &&
92
+ (isUser || shouldShowAssistantActions)
89
93
 
90
94
  useEffect(() => {
91
95
  setIsSubmitting(false)
@@ -115,6 +119,15 @@ function MessageItemComponent({
115
119
  }
116
120
  }, [isActionsVisible])
117
121
 
122
+ useEffect(() => {
123
+ if (!hideActionButtons) {
124
+ return
125
+ }
126
+
127
+ setIsActionsVisible(false)
128
+ setPendingConfirmAction(null)
129
+ }, [hideActionButtons])
130
+
118
131
  useEffect(() => {
119
132
  if (pendingConfirmAction == null) {
120
133
  return
@@ -277,7 +290,7 @@ function MessageItemComponent({
277
290
  }
278
291
 
279
292
  const handleActionsPointerEnter = () => {
280
- if (!isUser || isEditing) {
293
+ if (!isUser || isEditing || hideActionButtons) {
281
294
  return
282
295
  }
283
296
 
@@ -297,7 +310,7 @@ function MessageItemComponent({
297
310
  }
298
311
 
299
312
  const handleActionsPointerLeave = () => {
300
- if (!isUser) {
313
+ if (!isUser || hideActionButtons) {
301
314
  return
302
315
  }
303
316
 
@@ -448,7 +461,7 @@ function MessageItemComponent({
448
461
  onPointerLeave={handleActionsPointerLeave}
449
462
  >
450
463
  <div className={`message-body-container ${isEditing ? 'is-editing' : ''}`}>
451
- {isUser && !isEditing && !showCompactActionMenu && (
464
+ {isUser && !isEditing && !showCompactActionMenu && !hideActionButtons && (
452
465
  <div className='message-side-actions'>
453
466
  {actionButtons}
454
467
  </div>
@@ -496,6 +509,7 @@ const areMessageItemPropsEqual = (prev: MessageItemProps, next: MessageItemProps
496
509
  prev.isFirstInGroup === next.isFirstInGroup &&
497
510
  prev.isTargeted === next.isTargeted &&
498
511
  prev.isSessionBusy === next.isSessionBusy &&
512
+ prev.hideActionButtons === next.hideActionButtons &&
499
513
  prev.isEditing === next.isEditing &&
500
514
  prev.isCompactLayout === next.isCompactLayout &&
501
515
  prev.isTouchInteraction === next.isTouchInteraction &&
@@ -1,5 +1,16 @@
1
1
  import type { ChatRenderItem } from './message-utils'
2
2
 
3
+ export function getLastMessageAnchorId(renderItems: ChatRenderItem[]) {
4
+ for (let index = renderItems.length - 1; index >= 0; index -= 1) {
5
+ const item = renderItems[index]
6
+ if (item?.type === 'message') {
7
+ return item.anchorId
8
+ }
9
+ }
10
+
11
+ return null
12
+ }
13
+
3
14
  export function getLastAssistantActionAnchorId(renderItems: ChatRenderItem[]) {
4
15
  const lastItem = renderItems[renderItems.length - 1]
5
16
  if (lastItem?.type === 'tool-group') {
@@ -1,3 +1,15 @@
1
+ .sender-container {
2
+ flex: 0;
3
+ display: flex;
4
+ flex-direction: column;
5
+ gap: 8px;
6
+ min-width: 0;
7
+ justify-content: flex-end;
8
+ transition:
9
+ flex .4s cubic-bezier(.4, 0, .2, 1),
10
+ padding-bottom .4s cubic-bezier(.4, 0, .2, 1);
11
+ }
12
+
1
13
  .chat-input-wrapper {
2
14
  width: 100%;
3
15
  margin: 0;
@@ -1,17 +1,26 @@
1
1
  import '../ConfigView.scss'
2
2
 
3
- import { Select, Switch } from 'antd'
3
+ import { InputNumber, Select, Switch } from 'antd'
4
4
  import { useAtom } from 'jotai'
5
5
 
6
- import { senderHeaderDisplayAtom, showAnnouncementsAtom } from '#~/store/index.js'
7
- import type { SenderHeaderDisplayMode } from '#~/store/index.js'
6
+ import {
7
+ senderHeaderDisplayAtom,
8
+ sessionListSearchThresholdAtom,
9
+ showAnnouncementsAtom,
10
+ showNewSessionStarterListAtom,
11
+ themeAtom
12
+ } from '#~/store/index.js'
13
+ import type { SenderHeaderDisplayMode, ThemeMode } from '#~/store/index.js'
8
14
 
9
15
  import { FieldRow } from './ConfigFieldRow'
10
16
  import type { TranslationFn } from './configUtils'
11
17
 
12
18
  export function AppSettingsPanel({ t }: { t: TranslationFn }) {
19
+ const [themeMode, setThemeMode] = useAtom(themeAtom)
13
20
  const [showAnnouncements, setShowAnnouncements] = useAtom(showAnnouncementsAtom)
21
+ const [showNewSessionStarterList, setShowNewSessionStarterList] = useAtom(showNewSessionStarterListAtom)
14
22
  const [senderHeaderDisplay, setSenderHeaderDisplay] = useAtom(senderHeaderDisplayAtom)
23
+ const [sessionListSearchThreshold, setSessionListSearchThreshold] = useAtom(sessionListSearchThresholdAtom)
15
24
 
16
25
  return (
17
26
  <div className='config-view__editor-wrap'>
@@ -22,33 +31,78 @@ export function AppSettingsPanel({ t }: { t: TranslationFn }) {
22
31
  </div>
23
32
  </div>
24
33
  <div className='config-view__card'>
25
- <FieldRow
26
- title={t('config.appSettings.senderHeaderDisplay.label')}
27
- description={t('config.appSettings.senderHeaderDisplay.desc')}
28
- icon='unfold_more'
29
- >
30
- <Select<SenderHeaderDisplayMode>
31
- value={senderHeaderDisplay}
32
- onChange={setSenderHeaderDisplay}
33
- options={[
34
- {
35
- value: 'expanded',
36
- label: t('config.appSettings.senderHeaderDisplay.expanded')
37
- },
38
- {
39
- value: 'collapsed',
40
- label: t('config.appSettings.senderHeaderDisplay.collapsed')
41
- }
42
- ]}
43
- />
44
- </FieldRow>
45
- <FieldRow
46
- title={t('config.appSettings.announcements.label')}
47
- description={t('config.appSettings.announcements.desc')}
48
- icon='campaign'
49
- >
50
- <Switch checked={showAnnouncements} onChange={setShowAnnouncements} />
51
- </FieldRow>
34
+ <div className='config-view__app-settings-list'>
35
+ <FieldRow
36
+ title={t('config.appSettings.themeMode.label')}
37
+ description={t('config.appSettings.themeMode.desc')}
38
+ icon='dark_mode'
39
+ >
40
+ <Select<ThemeMode>
41
+ value={themeMode}
42
+ onChange={setThemeMode}
43
+ options={[
44
+ {
45
+ value: 'light',
46
+ label: t('common.themeLight')
47
+ },
48
+ {
49
+ value: 'dark',
50
+ label: t('common.themeDark')
51
+ },
52
+ {
53
+ value: 'system',
54
+ label: t('common.themeSystem')
55
+ }
56
+ ]}
57
+ />
58
+ </FieldRow>
59
+ <FieldRow
60
+ title={t('config.appSettings.senderHeaderDisplay.label')}
61
+ description={t('config.appSettings.senderHeaderDisplay.desc')}
62
+ icon='unfold_more'
63
+ >
64
+ <Select<SenderHeaderDisplayMode>
65
+ value={senderHeaderDisplay}
66
+ onChange={setSenderHeaderDisplay}
67
+ options={[
68
+ {
69
+ value: 'expanded',
70
+ label: t('config.appSettings.senderHeaderDisplay.expanded')
71
+ },
72
+ {
73
+ value: 'collapsed',
74
+ label: t('config.appSettings.senderHeaderDisplay.collapsed')
75
+ }
76
+ ]}
77
+ />
78
+ </FieldRow>
79
+ <FieldRow
80
+ title={t('config.appSettings.sessionListSearchThreshold.label')}
81
+ description={t('config.appSettings.sessionListSearchThreshold.desc')}
82
+ icon='search'
83
+ >
84
+ <InputNumber
85
+ min={0}
86
+ precision={0}
87
+ value={sessionListSearchThreshold}
88
+ onChange={(value) => setSessionListSearchThreshold(value ?? 0)}
89
+ />
90
+ </FieldRow>
91
+ <FieldRow
92
+ title={t('config.appSettings.announcements.label')}
93
+ description={t('config.appSettings.announcements.desc')}
94
+ icon='campaign'
95
+ >
96
+ <Switch checked={showAnnouncements} onChange={setShowAnnouncements} />
97
+ </FieldRow>
98
+ <FieldRow
99
+ title={t('config.appSettings.recommendedActions.label')}
100
+ description={t('config.appSettings.recommendedActions.desc')}
101
+ icon='tips_and_updates'
102
+ >
103
+ <Switch checked={showNewSessionStarterList} onChange={setShowNewSessionStarterList} />
104
+ </FieldRow>
105
+ </div>
52
106
  </div>
53
107
  </div>
54
108
  )
@@ -0,0 +1,157 @@
1
+ import React from 'react'
2
+
3
+ import { Form } from 'antd'
4
+ import type { MessageInstance } from 'antd/es/message/interface'
5
+ import type { TFunction } from 'i18next'
6
+
7
+ import type { SkillHubItem } from '#~/api.js'
8
+ import { getApiErrorMessage, installSkillsCliItem, searchSkillsCli } from '#~/api.js'
9
+ import type { SkillsCliFormValues } from '../SkillsCliModal'
10
+
11
+ const SKILLS_CLI_INITIAL_LIMIT = 100
12
+ const SKILLS_CLI_LIMIT_STEP = 100
13
+ const SKILLS_CLI_MAX_LIMIT = 500
14
+
15
+ const trimOptionalString = (value: string | undefined) => {
16
+ const normalizedValue = value?.trim()
17
+ return normalizedValue == null || normalizedValue === '' ? undefined : normalizedValue
18
+ }
19
+
20
+ export const useSkillsCliModalController = (params: {
21
+ message: MessageInstance
22
+ mutateSkills: () => Promise<unknown>
23
+ t: TFunction
24
+ }) => {
25
+ const [open, setOpen] = React.useState(false)
26
+ const [searching, setSearching] = React.useState(false)
27
+ const [loadingMore, setLoadingMore] = React.useState(false)
28
+ const [installingId, setInstallingId] = React.useState<string | null>(null)
29
+ const [items, setItems] = React.useState<SkillHubItem[]>([])
30
+ const [error, setError] = React.useState<string | null>(null)
31
+ const [hasMore, setHasMore] = React.useState(false)
32
+ const [hasSearched, setHasSearched] = React.useState(false)
33
+ const [limit, setLimit] = React.useState(SKILLS_CLI_INITIAL_LIMIT)
34
+ const [resetKey, setResetKey] = React.useState('')
35
+ const [form] = Form.useForm<SkillsCliFormValues>()
36
+
37
+ const resolveRequest = React.useCallback(async () => {
38
+ try {
39
+ await form.validateFields(['source'])
40
+ } catch {
41
+ return undefined
42
+ }
43
+
44
+ const values = form.getFieldsValue()
45
+ const source = values.source.trim()
46
+ return {
47
+ source,
48
+ query: trimOptionalString(values.query),
49
+ registry: trimOptionalString(values.registry)
50
+ }
51
+ }, [form])
52
+
53
+ const runSearch = React.useCallback(async (nextLimit: number) => {
54
+ const request = await resolveRequest()
55
+ if (request == null) return false
56
+
57
+ const result = await searchSkillsCli({
58
+ limit: nextLimit,
59
+ source: request.source,
60
+ ...(request.query != null ? { query: request.query } : {}),
61
+ ...(request.registry != null ? { registry: request.registry } : {})
62
+ })
63
+
64
+ setHasSearched(true)
65
+ setLimit(nextLimit)
66
+ setItems(result.items)
67
+ setError(result.error ?? null)
68
+ setHasMore(result.hasMore === true && nextLimit < SKILLS_CLI_MAX_LIMIT)
69
+ setResetKey([request.source, request.query ?? '', request.registry ?? '', String(nextLimit)].join('\0'))
70
+ return true
71
+ }, [resolveRequest])
72
+
73
+ const handleSearch = React.useCallback(async () => {
74
+ setSearching(true)
75
+ setLoadingMore(false)
76
+ try {
77
+ await runSearch(SKILLS_CLI_INITIAL_LIMIT)
78
+ } catch (error) {
79
+ void params.message.error(getApiErrorMessage(error, params.t('knowledge.skills.skillsCliSearchFailed')))
80
+ } finally {
81
+ setSearching(false)
82
+ }
83
+ }, [params, runSearch])
84
+
85
+ const handleLoadMore = React.useCallback(async () => {
86
+ const nextLimit = Math.min(limit + SKILLS_CLI_LIMIT_STEP, SKILLS_CLI_MAX_LIMIT)
87
+ if (nextLimit === limit) return
88
+
89
+ setLoadingMore(true)
90
+ try {
91
+ await runSearch(nextLimit)
92
+ } catch (error) {
93
+ void params.message.error(getApiErrorMessage(error, params.t('knowledge.skills.skillsCliSearchFailed')))
94
+ } finally {
95
+ setLoadingMore(false)
96
+ }
97
+ }, [limit, params, runSearch])
98
+
99
+ const handleInstall = React.useCallback(async (item: SkillHubItem) => {
100
+ const request = await resolveRequest()
101
+ if (request == null) return
102
+
103
+ setInstallingId(item.id)
104
+ try {
105
+ await installSkillsCliItem({
106
+ source: request.source,
107
+ skill: item.installRef ?? item.name,
108
+ force: item.installed,
109
+ ...(request.registry != null ? { registry: request.registry } : {})
110
+ })
111
+ await params.mutateSkills()
112
+ try {
113
+ await runSearch(limit)
114
+ } catch {
115
+ // Keep the install success state even if refreshing the source list fails afterwards.
116
+ }
117
+ void params.message.success(params.t('knowledge.skills.installSuccess'))
118
+ } catch (error) {
119
+ void params.message.error(getApiErrorMessage(error, params.t('knowledge.skills.installFailed')))
120
+ } finally {
121
+ setInstallingId(null)
122
+ }
123
+ }, [limit, params, resolveRequest, runSearch])
124
+
125
+ const handleClose = React.useCallback(() => {
126
+ setOpen(false)
127
+ setSearching(false)
128
+ setLoadingMore(false)
129
+ setInstallingId(null)
130
+ setItems([])
131
+ setError(null)
132
+ setHasMore(false)
133
+ setHasSearched(false)
134
+ setLimit(SKILLS_CLI_INITIAL_LIMIT)
135
+ setResetKey('')
136
+ form.resetFields()
137
+ }, [form])
138
+
139
+ return {
140
+ form,
141
+ hasMore,
142
+ hasSearched,
143
+ installingId,
144
+ items,
145
+ limit,
146
+ loadingMore,
147
+ open,
148
+ resetKey,
149
+ searching,
150
+ searchError: error,
151
+ setOpen,
152
+ handleClose,
153
+ handleInstall,
154
+ handleLoadMore,
155
+ handleSearch
156
+ }
157
+ }
@@ -0,0 +1,63 @@
1
+ import React from 'react'
2
+
3
+ import type { MessageInstance } from 'antd/es/message/interface'
4
+ import type { TFunction } from 'i18next'
5
+
6
+ import type { SkillHubItem } from '#~/api.js'
7
+ import { getApiErrorMessage, importSkillArchive, installSkillHubItem } from '#~/api.js'
8
+
9
+ export const useSkillsTabActions = (params: {
10
+ marketMutate: () => Promise<unknown>
11
+ message: MessageInstance
12
+ mutateConfig: () => Promise<unknown>
13
+ mutateSkills: () => Promise<unknown>
14
+ onRefresh: () => void | Promise<void>
15
+ t: TFunction
16
+ }) => {
17
+ const importInputRef = React.useRef<HTMLInputElement | null>(null)
18
+ const [installingId, setInstallingId] = React.useState<string | null>(null)
19
+ const [importing, setImporting] = React.useState(false)
20
+
21
+ const handleRefresh = React.useCallback(async () => {
22
+ await Promise.all([params.mutateSkills(), params.marketMutate(), params.mutateConfig(), params.onRefresh()])
23
+ }, [params])
24
+
25
+ const handleInstall = React.useCallback(async (item: SkillHubItem) => {
26
+ setInstallingId(item.id)
27
+ try {
28
+ await installSkillHubItem({
29
+ registry: item.registry,
30
+ plugin: item.installRef ?? item.name,
31
+ force: item.installed
32
+ })
33
+ await Promise.all([params.marketMutate(), params.mutateSkills()])
34
+ void params.message.success(params.t('knowledge.skills.installSuccess'))
35
+ } catch (error) {
36
+ void params.message.error(getApiErrorMessage(error, params.t('knowledge.skills.installFailed')))
37
+ } finally {
38
+ setInstallingId(null)
39
+ }
40
+ }, [params])
41
+
42
+ const handleImportArchive = React.useCallback(async (file: File) => {
43
+ setImporting(true)
44
+ try {
45
+ const result = await importSkillArchive(file)
46
+ await Promise.all([params.mutateSkills(), params.onRefresh()])
47
+ void params.message.success(params.t('knowledge.skills.importSuccess', { count: result.fileCount }))
48
+ } catch (error) {
49
+ void params.message.error(getApiErrorMessage(error, params.t('knowledge.skills.importFailed')))
50
+ } finally {
51
+ setImporting(false)
52
+ }
53
+ }, [params])
54
+
55
+ return {
56
+ importInputRef,
57
+ importing,
58
+ installingId,
59
+ handleImportArchive,
60
+ handleInstall,
61
+ handleRefresh
62
+ }
63
+ }