@nextclaw/ui 0.12.23 → 0.12.25

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 (215) hide show
  1. package/CHANGELOG.md +136 -0
  2. package/dist/assets/api-DGD9_Bg4.js +15 -0
  3. package/dist/assets/app-manager-provider-oYdeYPSv.js +1 -0
  4. package/dist/assets/{book-open-DDlN5MvX.js → book-open-BcnAiKde.js} +1 -1
  5. package/dist/assets/channels-list-page-FJDuPwU6.js +8 -0
  6. package/dist/assets/chat-page-D1fMNBrT.js +1 -0
  7. package/dist/assets/config-split-page-CcrEUtwu.js +1 -0
  8. package/dist/assets/cpu-DPPwMzoC.js +3 -0
  9. package/dist/assets/{createLucideIcon-BLMK3QUd.js → createLucideIcon-DzY6wN61.js} +1 -1
  10. package/dist/assets/desktop-kk7qvZ-v.js +3 -0
  11. package/dist/assets/desktop-update-config-CP8dFYXK.js +1 -0
  12. package/dist/assets/{dialog-dxsKz7jJ.js → dialog-BKo0RItd.js} +1 -1
  13. package/dist/assets/{dist-DsYTOyq7.js → dist-CFiwgaLs.js} +1 -1
  14. package/dist/assets/doc-browser-CAhfnm0D.js +1 -0
  15. package/dist/assets/{doc-browser-context-BJuMaI3o.js → doc-browser-context-FukQHvyo.js} +1 -1
  16. package/dist/assets/doc-browser-p9DDNPWB.js +1 -0
  17. package/dist/assets/doc-browser-rZIQIjuw.js +1 -0
  18. package/dist/assets/download-CMM8po31.js +1 -0
  19. package/dist/assets/{es2015-V75WQJ2s.js → es2015-BhznEEyJ.js} +1 -1
  20. package/dist/assets/{external-link-DwfSfTLB.js → external-link-CpEvG65F.js} +1 -1
  21. package/dist/assets/i18n-D1144VAA.js +1 -0
  22. package/dist/assets/index-D-AAMKCt.js +103 -0
  23. package/dist/assets/index-DnBeV2Xm.css +1 -0
  24. package/dist/assets/{key-round-CJ5gDAAG.js → key-round-DUq47t0P.js} +1 -1
  25. package/dist/assets/marketplace-page-BrCLRIc4.js +105 -0
  26. package/dist/assets/marketplace-page-odDpPYEs.js +1 -0
  27. package/dist/assets/mcp-marketplace-page-CfbOBgKK.js +1 -0
  28. package/dist/assets/mcp-marketplace-page-DIq_SpMe.js +40 -0
  29. package/dist/assets/model-config-Bc6VVnxy.js +1 -0
  30. package/dist/assets/{notice-card-D1RNsTn_.js → notice-card-Dr6xCwva.js} +1 -1
  31. package/dist/assets/play-AqrNslHI.js +1 -0
  32. package/dist/assets/plus-B-YHtTNC.js +1 -0
  33. package/dist/assets/{popover-BMyiifTA.js → popover-BDFNiLlg.js} +1 -1
  34. package/dist/assets/provider-scoped-model-input-BMTp4BEH.js +1 -0
  35. package/dist/assets/providers-list-DN0tvISH.js +1 -0
  36. package/dist/assets/refresh-cw-CrbD8EkT.js +1 -0
  37. package/dist/assets/remote-Dr3jcfWP.js +1 -0
  38. package/dist/assets/{rotate-cw-BZ2JObNs.js → rotate-cw-BN9yjccP.js} +1 -1
  39. package/dist/assets/runtime-config-page-CRWOwBbl.js +1 -0
  40. package/dist/assets/{save-euRxl8pI.js → save-CO_4qf6b.js} +1 -1
  41. package/dist/assets/{search-CLd7m0M7.js → search-CRtQwr-h.js} +1 -1
  42. package/dist/assets/search-config-C4c1yZSP.js +1 -0
  43. package/dist/assets/secrets-config-zAF30YfO.js +3 -0
  44. package/dist/assets/{select-DTdzR8j8.js → select-BUTwE_lC.js} +1 -1
  45. package/dist/assets/{setting-row-CvKngoNI.js → setting-row-BavcnXw1.js} +1 -1
  46. package/dist/assets/settings-MWL2SMyk.js +1 -0
  47. package/dist/assets/{sparkles-DVfeSVJQ.js → sparkles-BmgOD4nY.js} +1 -1
  48. package/dist/assets/{status-dot-ChvPCib9.js → status-dot-l3kPFdq_.js} +1 -1
  49. package/dist/assets/{tabs-custom-Hia_ong0.js → tabs-custom-D48zdZoc.js} +1 -1
  50. package/dist/assets/{tag-chip-BywQeHJj.js → tag-chip-Dm2Lqnpu.js} +1 -1
  51. package/dist/assets/use-config-Cyv5IuSt.js +1 -0
  52. package/dist/assets/use-infinite-scroll-loader-Cvz8ZteY.js +1 -0
  53. package/dist/assets/x-BeyYA_h6.js +1 -0
  54. package/dist/index.html +29 -40
  55. package/package.json +9 -9
  56. package/src/app/components/layout/sidebar.layout.test.tsx +2 -4
  57. package/src/app/components/theme-provider.tsx +1 -0
  58. package/src/app/configs/app-navigation.config.ts +0 -6
  59. package/src/app/index.tsx +4 -7
  60. package/src/features/agents/components/agents-page.test.tsx +25 -15
  61. package/src/features/agents/components/agents-page.tsx +133 -172
  62. package/src/features/channels/components/config/channel-form.test.tsx +1 -0
  63. package/src/features/channels/components/config/channel-form.tsx +4 -3
  64. package/src/features/channels/components/config/weixin-channel-auth-section.test.tsx +38 -1
  65. package/src/features/channels/components/config/weixin-channel-auth-section.tsx +137 -40
  66. package/src/features/channels/index.ts +1 -1
  67. package/src/features/channels/utils/channel-form-fields.utils.test.ts +26 -0
  68. package/src/features/channels/utils/channel-form-fields.utils.ts +32 -18
  69. package/src/features/chat/components/chat-session-workspace-panel-nav.tsx +23 -4
  70. package/src/features/chat/components/chat-session-workspace-panel.tsx +34 -2
  71. package/src/features/chat/components/chat-sidebar-session-item.tsx +9 -3
  72. package/src/features/chat/components/conversation/chat-conversation-header.test.tsx +71 -0
  73. package/src/features/chat/components/conversation/chat-conversation-header.tsx +6 -0
  74. package/src/features/chat/components/conversation/chat-conversation-panel.test.tsx +181 -61
  75. package/src/features/chat/components/conversation/chat-conversation-panel.tsx +56 -25
  76. package/src/features/chat/components/conversation/session-header/chat-session-header-actions.test.tsx +24 -0
  77. package/src/features/chat/components/conversation/session-header/chat-session-header-actions.tsx +26 -5
  78. package/src/features/chat/components/layout/chat-sidebar-utility-menu.tsx +174 -0
  79. package/src/features/chat/components/layout/chat-sidebar.test.tsx +119 -8
  80. package/src/features/chat/components/layout/chat-sidebar.tsx +57 -75
  81. package/src/features/chat/components/providers/chat-presenter.provider.tsx +2 -0
  82. package/src/features/chat/components/workspace/session-cron-job-content.tsx +103 -0
  83. package/src/features/chat/hooks/use-hydrated-ncp-agent.test.tsx +6 -0
  84. package/src/features/chat/hooks/use-ncp-agent-runtime.test.tsx +172 -69
  85. package/src/features/chat/hooks/use-ncp-chat-derived-state.ts +2 -2
  86. package/src/features/chat/hooks/use-ncp-chat-page-data.test.tsx +70 -0
  87. package/src/features/chat/hooks/use-ncp-chat-page-data.ts +7 -7
  88. package/src/features/chat/hooks/use-ncp-child-session-tabs-view.ts +2 -8
  89. package/src/features/chat/hooks/use-ncp-session-conversation.test.tsx +10 -0
  90. package/src/features/chat/hooks/use-ncp-session-conversation.ts +2 -1
  91. package/src/features/chat/hooks/use-ncp-session-list-view.ts +1 -2
  92. package/src/features/chat/hooks/use-selected-session-context-window-indicator.ts +2 -4
  93. package/src/features/chat/managers/chat-session-list.manager.test.ts +21 -20
  94. package/src/features/chat/managers/chat-session-list.manager.ts +15 -24
  95. package/src/features/chat/managers/ncp-chat-input.manager.test.ts +22 -13
  96. package/src/features/chat/managers/ncp-chat-input.manager.ts +4 -2
  97. package/src/features/chat/managers/ncp-chat-presenter.manager.ts +6 -0
  98. package/src/features/chat/managers/ncp-chat-thread.manager.test.ts +52 -1
  99. package/src/features/chat/managers/ncp-chat-thread.manager.ts +21 -0
  100. package/src/features/chat/pages/ncp-chat-page.tsx +28 -17
  101. package/src/features/chat/stores/chat-session-list.store.ts +0 -3
  102. package/src/features/chat/stores/chat-thread.store.ts +4 -0
  103. package/src/features/chat/types/chat-stream.types.ts +1 -1
  104. package/src/features/chat/utils/chat-session-display.utils.test.ts +83 -1
  105. package/src/features/chat/utils/chat-session-display.utils.ts +73 -0
  106. package/src/features/chat/utils/ncp-session-adapter.utils.test.ts +22 -0
  107. package/src/features/chat/utils/ncp-session-adapter.utils.ts +33 -1
  108. package/src/features/marketplace/components/curated-shelves/marketplace-curated-scene-route.test.tsx +235 -0
  109. package/src/features/marketplace/components/curated-shelves/marketplace-curated-shelves.config.ts +162 -0
  110. package/src/features/marketplace/components/curated-shelves/marketplace-curated-shelves.tsx +355 -0
  111. package/src/features/marketplace/components/curated-shelves/marketplace-shelf-card.tsx +118 -0
  112. package/src/features/marketplace/components/detail-doc/marketplace-detail-doc-renderer.ts +201 -0
  113. package/src/features/marketplace/components/detail-doc/marketplace-detail-doc.test.ts +40 -0
  114. package/src/features/marketplace/components/marketplace-catalog-grid.tsx +114 -0
  115. package/src/features/marketplace/components/marketplace-detail-doc.ts +73 -24
  116. package/src/features/marketplace/components/marketplace-item-icon.tsx +45 -0
  117. package/src/features/marketplace/components/marketplace-list-card.tsx +177 -93
  118. package/src/features/marketplace/components/marketplace-page-detail.test.tsx +9 -2
  119. package/src/features/marketplace/components/marketplace-page-parts.tsx +1 -1
  120. package/src/features/marketplace/components/marketplace-page.test.tsx +25 -6
  121. package/src/features/marketplace/components/marketplace-page.tsx +154 -132
  122. package/src/features/marketplace/hooks/use-marketplace-curated-scene-route.ts +97 -0
  123. package/src/features/marketplace/hooks/use-marketplace.ts +59 -3
  124. package/src/features/system-status/components/config/runtime-agent-list-card.tsx +4 -8
  125. package/src/features/system-status/components/config/runtime-binding-list-card.tsx +5 -7
  126. package/src/features/system-status/components/config/runtime-config-editor.tsx +1 -19
  127. package/src/features/system-status/components/config/runtime-entry-list-card.tsx +10 -11
  128. package/src/features/system-status/components/config/runtime-settings-card.tsx +15 -23
  129. package/src/features/system-status/components/runtime-control-card.test.tsx +8 -6
  130. package/src/features/system-status/components/runtime-control-card.tsx +7 -6
  131. package/src/features/system-status/pages/runtime-config-page.test.tsx +19 -9
  132. package/src/features/system-status/pages/runtime-config-page.tsx +2 -3
  133. package/src/features/system-status/utils/runtime-config-agent.utils.ts +4 -4
  134. package/src/features/system-status/utils/system-status.utils.ts +31 -6
  135. package/src/index.css +8 -0
  136. package/src/platforms/desktop/components/desktop-app-shell.test.tsx +67 -0
  137. package/src/platforms/desktop/components/desktop-app-shell.tsx +46 -18
  138. package/src/platforms/desktop/components/desktop-window-chrome.tsx +30 -0
  139. package/src/platforms/desktop/index.ts +6 -0
  140. package/src/platforms/desktop/types/desktop-update.types.ts +3 -0
  141. package/src/platforms/desktop/utils/desktop-host.utils.ts +56 -0
  142. package/src/shared/components/common/brand-header.tsx +36 -16
  143. package/src/shared/components/config/provider-form-support.ts +2 -22
  144. package/src/shared/components/cron-config.tsx +12 -58
  145. package/src/shared/components/doc-browser/doc-browser.tsx +4 -4
  146. package/src/shared/components/ui/select.tsx +19 -7
  147. package/src/shared/lib/api/channel-auth.types.ts +1 -0
  148. package/src/shared/lib/api/ncp-session-query-cache.test.ts +26 -1
  149. package/src/shared/lib/api/ncp-session-query-cache.ts +5 -1
  150. package/src/shared/lib/api/ncp-session.types.ts +9 -0
  151. package/src/shared/lib/api/types.ts +12 -1
  152. package/src/shared/lib/api/utils/marketplace.utils.ts +7 -1
  153. package/src/shared/lib/cron/cron-job-view.utils.ts +59 -0
  154. package/src/shared/lib/cron/index.ts +1 -0
  155. package/src/shared/lib/i18n/{channel-auth.ts → channel-auth.constants.ts} +31 -0
  156. package/src/shared/lib/i18n/chat-labels.utils.ts +3 -2
  157. package/src/shared/lib/i18n/index.ts +20 -59
  158. package/src/shared/lib/i18n/{runtime-control.ts → runtime-control-labels.utils.ts} +30 -1
  159. package/src/shared/lib/provider-models/index.test.ts +39 -0
  160. package/src/shared/lib/provider-models/index.ts +1 -3
  161. package/src/shared/lib/ui-document-title/index.ts +0 -1
  162. package/tsconfig.json +1 -0
  163. package/vite.config.ts +1 -1
  164. package/vitest.config.ts +1 -1
  165. package/dist/assets/api-BGd3rgv_.js +0 -15
  166. package/dist/assets/app-manager-provider-BuJ_U9eC.js +0 -1
  167. package/dist/assets/app-navigation.config-BTdUuqXS.js +0 -1
  168. package/dist/assets/channels-list-page-BrwymXPe.js +0 -8
  169. package/dist/assets/chat-DGM6K3Qs.js +0 -61
  170. package/dist/assets/chat-page-DpmXMWNS.js +0 -1
  171. package/dist/assets/chunk-JZWAC4HX-Kydj4yEz.js +0 -3
  172. package/dist/assets/config-split-page-DIOCjj2Q.js +0 -1
  173. package/dist/assets/desktop-update-config-BGKiqc6q.js +0 -1
  174. package/dist/assets/doc-browser-C8FM5fC0.js +0 -1
  175. package/dist/assets/doc-browser-RJUOL_GO.js +0 -1
  176. package/dist/assets/doc-browser-p82AdNO-.js +0 -1
  177. package/dist/assets/folder-CeJKPx5P.js +0 -1
  178. package/dist/assets/hash-BqxRTZW5.js +0 -1
  179. package/dist/assets/i18n-DnTGDIRw.js +0 -1
  180. package/dist/assets/index-BrEdR78s.js +0 -2
  181. package/dist/assets/index-D8MKmXtO.css +0 -1
  182. package/dist/assets/loader-circle-fd-vQKtW.js +0 -1
  183. package/dist/assets/logo-badge-KAe-7d8c.js +0 -1
  184. package/dist/assets/logos-C4sYP1Vl.js +0 -1
  185. package/dist/assets/marketplace-page-B2Pm2RDJ.js +0 -1
  186. package/dist/assets/marketplace-page-CPHxlYL8.js +0 -49
  187. package/dist/assets/mcp-marketplace-page-BcjVmw36.js +0 -1
  188. package/dist/assets/mcp-marketplace-page-CswPXSjf.js +0 -40
  189. package/dist/assets/message-square-z_osm9c0.js +0 -1
  190. package/dist/assets/model-config-Cmruiqdx.js +0 -1
  191. package/dist/assets/play-Dv6Nr1Ew.js +0 -1
  192. package/dist/assets/plus-D8eKFY7h.js +0 -1
  193. package/dist/assets/provider-scoped-model-input-D7ACiMAO.js +0 -1
  194. package/dist/assets/providers-list-gg7LrfuB.js +0 -1
  195. package/dist/assets/refresh-ccw-ByVwmnN_.js +0 -1
  196. package/dist/assets/refresh-cw-PcqoYB3K.js +0 -1
  197. package/dist/assets/remote-Db2M39Cv.js +0 -1
  198. package/dist/assets/runtime-config-page-BT_VV41p.js +0 -1
  199. package/dist/assets/search-config-0VTPpz-w.js +0 -1
  200. package/dist/assets/secrets-config-DwQbLLEy.js +0 -3
  201. package/dist/assets/sessions-config-page-CAG7Zevv.js +0 -2
  202. package/dist/assets/settings-drbWqzA4.js +0 -1
  203. package/dist/assets/skeleton-BK1SOSRA.js +0 -1
  204. package/dist/assets/theme-provider-COAwWFv8.js +0 -2
  205. package/dist/assets/tooltip-BOYp8Ue7.js +0 -1
  206. package/dist/assets/trash-2-CBsHCfqq.js +0 -1
  207. package/dist/assets/use-config-DTwhNDQE.js +0 -1
  208. package/dist/assets/use-confirm-dialog-oeSqhmrx.js +0 -1
  209. package/dist/assets/use-infinite-scroll-loader-X3KGuME8.js +0 -1
  210. package/dist/assets/use-viewport-layout-C0NJAVXs.js +0 -1
  211. package/dist/assets/x-CM-XDMpk.js +0 -1
  212. package/src/features/chat/components/config/sessions-config-detail-pane.tsx +0 -244
  213. package/src/features/chat/pages/sessions-config-page.test.tsx +0 -152
  214. package/src/features/chat/pages/sessions-config-page.tsx +0 -192
  215. /package/dist/assets/{config-hints-MogHYQ8G.js → config-hints-BNfpOL4J.js} +0 -0
@@ -1,4 +1,4 @@
1
- import { useInfiniteQuery, useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
1
+ import { useInfiniteQuery, useMutation, useQueries, useQuery, useQueryClient } from '@tanstack/react-query';
2
2
  import { toast } from 'sonner';
3
3
  import { t } from '@/shared/lib/i18n';
4
4
  import {
@@ -10,6 +10,7 @@ import {
10
10
  fetchMarketplaceInstalled,
11
11
  fetchMarketplaceItems,
12
12
  fetchMarketplaceRecommendations,
13
+ fetchMarketplaceSkillScenes,
13
14
  installMarketplaceItem,
14
15
  manageMarketplaceItem,
15
16
  type MarketplaceListParams
@@ -18,7 +19,8 @@ import type {
18
19
  MarketplaceInstallRequest,
19
20
  MarketplaceInstalledView,
20
21
  MarketplaceItemType,
21
- MarketplaceManageRequest
22
+ MarketplaceManageRequest,
23
+ MarketplaceSceneView
22
24
  } from '@/shared/lib/api';
23
25
  import { collapseMarketplaceListPages } from '@/features/marketplace/utils/marketplace-list-pages.utils';
24
26
  import { useMemo } from 'react';
@@ -36,7 +38,12 @@ export function useMarketplaceItems(params: MarketplaceListParams) {
36
38
  }
37
39
 
38
40
  const previousType = 'type' in previousParams ? previousParams.type : undefined;
39
- return previousType === params.type ? previousData : undefined;
41
+ return previousType === params.type && hasSameMarketplaceListIdentity(
42
+ previousParams as MarketplaceListParams,
43
+ params
44
+ )
45
+ ? previousData
46
+ : undefined;
40
47
  },
41
48
  staleTime: 15_000
42
49
  });
@@ -49,6 +56,17 @@ export function useMarketplaceItems(params: MarketplaceListParams) {
49
56
  };
50
57
  }
51
58
 
59
+ function hasSameMarketplaceListIdentity(
60
+ previousParams: MarketplaceListParams,
61
+ nextParams: MarketplaceListParams
62
+ ): boolean {
63
+ return previousParams.q === nextParams.q
64
+ && previousParams.tag === nextParams.tag
65
+ && previousParams.scene === nextParams.scene
66
+ && previousParams.sort === nextParams.sort
67
+ && previousParams.pageSize === nextParams.pageSize;
68
+ }
69
+
52
70
  export function useMarketplaceRecommendations(type: MarketplaceItemType, params: { scene?: string; limit?: number }) {
53
71
  return useQuery({
54
72
  queryKey: ['marketplace-recommendations', type, params],
@@ -57,6 +75,44 @@ export function useMarketplaceRecommendations(type: MarketplaceItemType, params:
57
75
  });
58
76
  }
59
77
 
78
+ export function useMarketplaceSkillScenes(enabled = true) {
79
+ return useQuery({
80
+ queryKey: ['marketplace-skill-scenes'],
81
+ queryFn: fetchMarketplaceSkillScenes,
82
+ enabled,
83
+ staleTime: 60_000
84
+ });
85
+ }
86
+
87
+ export function useMarketplaceSkillSceneCounts(
88
+ scenes: MarketplaceSceneView[],
89
+ enabled = true
90
+ ) {
91
+ const queries = useQueries({
92
+ queries: scenes.map((scene) => ({
93
+ queryKey: ['marketplace-skill-scene-count', scene.scene],
94
+ queryFn: () => fetchMarketplaceItems({
95
+ type: 'skill',
96
+ scene: scene.scene,
97
+ pageSize: 1
98
+ }),
99
+ enabled: enabled && typeof scene.count !== 'number',
100
+ staleTime: 60_000
101
+ }))
102
+ });
103
+
104
+ return useMemo(() => {
105
+ const counts = new Map<string, number>();
106
+ scenes.forEach((scene, index) => {
107
+ const knownCount = typeof scene.count === 'number' ? scene.count : queries[index]?.data?.total;
108
+ if (typeof knownCount === 'number') {
109
+ counts.set(scene.scene, knownCount);
110
+ }
111
+ });
112
+ return counts;
113
+ }, [queries, scenes]);
114
+ }
115
+
60
116
  export function useMarketplaceItem(slug: string | null, type?: MarketplaceItemType) {
61
117
  return useQuery({
62
118
  queryKey: ['marketplace-item', slug, type],
@@ -13,16 +13,12 @@ export function RuntimeAgentListCard(props: {
13
13
  onRemoveAgent: (index: number) => void;
14
14
  onAddAgent: () => void;
15
15
  onSetDefaultAgent: (index: number, checked: boolean) => void;
16
- label?: string;
17
- help?: string;
18
- contextTokensLabel?: string;
19
- engineLabel?: string;
20
16
  }) {
21
17
  return (
22
18
  <Card>
23
19
  <CardHeader>
24
- <CardTitle>{props.label ?? t('agentList')}</CardTitle>
25
- <CardDescription>{props.help ?? t('agentListHelp')}</CardDescription>
20
+ <CardTitle>{t('agentList')}</CardTitle>
21
+ <CardDescription>{t('agentListHelp')}</CardDescription>
26
22
  </CardHeader>
27
23
  <CardContent className="space-y-3">
28
24
  {props.agents.map((agent, index) => (
@@ -31,7 +27,7 @@ export function RuntimeAgentListCard(props: {
31
27
  <Input value={agent.id} onChange={(event) => props.onUpdateAgent(index, { id: event.target.value })} placeholder={t('agentIdPlaceholder')} />
32
28
  <Input value={agent.workspace ?? ''} onChange={(event) => props.onUpdateAgent(index, { workspace: event.target.value })} placeholder={t('workspaceOverridePlaceholder')} />
33
29
  <Input value={agent.model ?? ''} onChange={(event) => props.onUpdateAgent(index, { model: event.target.value })} placeholder={t('modelOverridePlaceholder')} />
34
- <Input value={agent.runtime ?? agent.engine ?? ''} onChange={(event) => props.onUpdateAgent(index, { runtime: event.target.value })} placeholder={props.engineLabel ?? t('engineOverridePlaceholder')} />
30
+ <Input value={agent.runtime ?? agent.engine ?? ''} onChange={(event) => props.onUpdateAgent(index, { runtime: event.target.value })} placeholder={t('engineOverridePlaceholder')} />
35
31
  <div className="grid grid-cols-1 md:grid-cols-2 gap-2">
36
32
  <Input
37
33
  type="number"
@@ -39,7 +35,7 @@ export function RuntimeAgentListCard(props: {
39
35
  step={1000}
40
36
  value={agent.contextTokens ?? ''}
41
37
  onChange={(event) => props.onUpdateAgent(index, { contextTokens: parseOptionalInt(event.target.value) })}
42
- placeholder={props.contextTokensLabel ?? t('contextTokensPlaceholder')}
38
+ placeholder={t('contextTokensPlaceholder')}
43
39
  />
44
40
  <Input
45
41
  type="number"
@@ -12,14 +12,12 @@ export function RuntimeBindingListCard(props: {
12
12
  onUpdateBinding: (index: number, next: AgentBindingView) => void;
13
13
  onRemoveBinding: (index: number) => void;
14
14
  onAddBinding: () => void;
15
- label?: string;
16
- help?: string;
17
15
  }) {
18
16
  return (
19
17
  <Card>
20
18
  <CardHeader>
21
- <CardTitle>{props.label ?? t('bindings')}</CardTitle>
22
- <CardDescription>{props.help ?? t('bindingsHelp')}</CardDescription>
19
+ <CardTitle>{t('bindings')}</CardTitle>
20
+ <CardDescription>{t('bindingsHelp')}</CardDescription>
23
21
  </CardHeader>
24
22
  <CardContent className="space-y-3">
25
23
  {props.bindings.map((binding, index) => {
@@ -47,9 +45,9 @@ export function RuntimeBindingListCard(props: {
47
45
  </SelectTrigger>
48
46
  <SelectContent>
49
47
  <SelectItem value="__none__">{t('peerKindOptional')}</SelectItem>
50
- <SelectItem value="direct">direct</SelectItem>
51
- <SelectItem value="group">group</SelectItem>
52
- <SelectItem value="channel">channel</SelectItem>
48
+ <SelectItem value="direct">{t('peerKindDirect')}</SelectItem>
49
+ <SelectItem value="group">{t('peerKindGroup')}</SelectItem>
50
+ <SelectItem value="channel">{t('peerKindChannel')}</SelectItem>
53
51
  </SelectContent>
54
52
  </Select>
55
53
  <Input
@@ -1,9 +1,8 @@
1
1
  import { useMemo, useState } from 'react';
2
2
  import { Save } from 'lucide-react';
3
- import type { ConfigUiHints, ConfigView, RuntimeConfigUpdate } from '@/shared/lib/api';
3
+ import type { ConfigView, RuntimeConfigUpdate } from '@/shared/lib/api';
4
4
  import { Button } from '@/shared/components/ui/button';
5
5
  import { PageLayout } from '@/app/components/layout/page-layout';
6
- import { hintForPath } from '@/shared/lib/config-hints';
7
6
  import { t } from '@/shared/lib/i18n';
8
7
  import { RuntimeConfigOverview } from '@/features/system-status/components/config/runtime-config-overview';
9
8
  import { RuntimeAgentListCard } from '@/features/system-status/components/config/runtime-agent-list-card';
@@ -27,7 +26,6 @@ type UpdateRuntimeMutation = {
27
26
 
28
27
  export function RuntimeConfigEditor(props: {
29
28
  config: ConfigView;
30
- uiHints?: ConfigUiHints;
31
29
  updateRuntime: UpdateRuntimeMutation;
32
30
  }) {
33
31
  const initialState = useMemo(() => createRuntimeConfigEditorState(props.config), [props.config]);
@@ -88,22 +86,12 @@ export function RuntimeConfigEditor(props: {
88
86
  onDmScopeChange={setDmScope}
89
87
  onDefaultContextTokensChange={setDefaultContextTokens}
90
88
  onDefaultEngineChange={setDefaultEngine}
91
- companionEnabledLabel={hintForPath('companion.enabled', props.uiHints)?.label}
92
- companionEnabledHelp={hintForPath('companion.enabled', props.uiHints)?.help}
93
- dmScopeLabel={hintForPath('session.dmScope', props.uiHints)?.label}
94
- dmScopeHelp={hintForPath('session.dmScope', props.uiHints)?.help}
95
- defaultContextTokensLabel={hintForPath('agents.defaults.contextTokens', props.uiHints)?.label}
96
- defaultContextTokensHelp={hintForPath('agents.defaults.contextTokens', props.uiHints)?.help}
97
- defaultEngineLabel={hintForPath('agents.defaults.engine', props.uiHints)?.label}
98
- defaultEngineHelp={hintForPath('agents.defaults.engine', props.uiHints)?.help}
99
89
  />
100
90
  <RuntimeEntryListCard
101
91
  entries={runtimeEntries}
102
92
  onUpdateEntry={updateRuntimeEntry}
103
93
  onRemoveEntry={(index) => setRuntimeEntries((previous) => previous.filter((_, cursor) => cursor !== index))}
104
94
  onAddEntry={() => setRuntimeEntries((previous) => [...previous, createEmptyRuntimeEntry()])}
105
- label={hintForPath('agents.runtimes.entries', props.uiHints)?.label}
106
- help={hintForPath('agents.runtimes.entries', props.uiHints)?.help}
107
95
  />
108
96
  <RuntimeAgentListCard
109
97
  agents={agents}
@@ -113,18 +101,12 @@ export function RuntimeConfigEditor(props: {
113
101
  onSetDefaultAgent={(index, checked) =>
114
102
  setAgents((previous) => checked ? previous.map((entry, cursor) => ({ ...entry, default: cursor === index })) : previous.map((entry, cursor) => (cursor === index ? { ...entry, default: false } : entry)))
115
103
  }
116
- label={hintForPath('agents.list', props.uiHints)?.label}
117
- help={hintForPath('agents.list', props.uiHints)?.help}
118
- contextTokensLabel={hintForPath('agents.list.*.contextTokens', props.uiHints)?.label}
119
- engineLabel={hintForPath('agents.list.*.engine', props.uiHints)?.label}
120
104
  />
121
105
  <RuntimeBindingListCard
122
106
  bindings={bindings}
123
107
  onUpdateBinding={updateBinding}
124
108
  onRemoveBinding={(index) => setBindings((previous) => previous.filter((_, cursor) => cursor !== index))}
125
109
  onAddBinding={() => setBindings((previous) => [...previous, createEmptyRuntimeBinding()])}
126
- label={hintForPath('bindings', props.uiHints)?.label}
127
- help={hintForPath('bindings', props.uiHints)?.help}
128
110
  />
129
111
  <div className="flex justify-end">
130
112
  <Button type="button" onClick={handleSave} disabled={props.updateRuntime.isPending}>
@@ -3,6 +3,7 @@ import { Button } from '@/shared/components/ui/button';
3
3
  import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/shared/components/ui/card';
4
4
  import { Input } from '@/shared/components/ui/input';
5
5
  import { Switch } from '@/shared/components/ui/switch';
6
+ import { t } from '@/shared/lib/i18n';
6
7
  import type { RuntimeEntryDraft } from '@/features/system-status/utils/runtime-config-agent.utils';
7
8
 
8
9
  export function RuntimeEntryListCard(props: {
@@ -10,29 +11,27 @@ export function RuntimeEntryListCard(props: {
10
11
  onUpdateEntry: (index: number, patch: Partial<RuntimeEntryDraft>) => void;
11
12
  onRemoveEntry: (index: number) => void;
12
13
  onAddEntry: () => void;
13
- label?: string;
14
- help?: string;
15
14
  }) {
16
15
  return (
17
16
  <Card>
18
17
  <CardHeader>
19
- <CardTitle>{props.label ?? 'Runtime Entries'}</CardTitle>
20
- <CardDescription>{props.help ?? '统一管理可见的 runtime entry 与其配置。'}</CardDescription>
18
+ <CardTitle>{t('runtimeEntries')}</CardTitle>
19
+ <CardDescription>{t('runtimeEntriesHelp')}</CardDescription>
21
20
  </CardHeader>
22
21
  <CardContent className="space-y-3">
23
22
  {props.entries.map((entry, index) => (
24
23
  <div key={`${index}-${entry.id || 'runtime-entry'}`} className="rounded-xl border border-gray-200 p-3 space-y-3">
25
24
  <div className="grid grid-cols-1 md:grid-cols-2 gap-3">
26
- <Input value={entry.id} onChange={(event) => props.onUpdateEntry(index, { id: event.target.value })} placeholder="entry id,例如 hermes" />
27
- <Input value={entry.label ?? ''} onChange={(event) => props.onUpdateEntry(index, { label: event.target.value })} placeholder="展示名称,例如 Hermes" />
28
- <Input value={entry.type} onChange={(event) => props.onUpdateEntry(index, { type: event.target.value })} placeholder="runtime type,例如 narp-stdio" />
25
+ <Input value={entry.id} onChange={(event) => props.onUpdateEntry(index, { id: event.target.value })} placeholder={t('runtimeEntryIdPlaceholder')} />
26
+ <Input value={entry.label ?? ''} onChange={(event) => props.onUpdateEntry(index, { label: event.target.value })} placeholder={t('runtimeEntryLabelPlaceholder')} />
27
+ <Input value={entry.type} onChange={(event) => props.onUpdateEntry(index, { type: event.target.value })} placeholder={t('runtimeEntryTypePlaceholder')} />
29
28
  <div className="flex items-center justify-between rounded-lg border border-gray-200 px-3 py-2">
30
- <span className="text-sm text-gray-700">Enabled</span>
29
+ <span className="text-sm text-gray-700">{t('enabled')}</span>
31
30
  <Switch checked={entry.enabled !== false} onCheckedChange={(checked) => props.onUpdateEntry(index, { enabled: checked })} />
32
31
  </div>
33
32
  </div>
34
33
  <div className="space-y-2">
35
- <label className="text-sm font-medium text-gray-800">Config JSON</label>
34
+ <label className="text-sm font-medium text-gray-800">{t('runtimeEntryConfigJson')}</label>
36
35
  <textarea
37
36
  className="min-h-32 w-full rounded-md border border-gray-200 px-3 py-2 text-sm font-mono"
38
37
  value={entry.configText}
@@ -43,14 +42,14 @@ export function RuntimeEntryListCard(props: {
43
42
  <div className="flex justify-end">
44
43
  <Button type="button" variant="outline" onClick={() => props.onRemoveEntry(index)}>
45
44
  <Trash2 className="mr-2 h-4 w-4" />
46
- Delete
45
+ {t('delete')}
47
46
  </Button>
48
47
  </div>
49
48
  </div>
50
49
  ))}
51
50
  <Button type="button" variant="outline" onClick={props.onAddEntry}>
52
51
  <Plus className="mr-2 h-4 w-4" />
53
- Add Runtime Entry
52
+ {t('addRuntimeEntry')}
54
53
  </Button>
55
54
  </CardContent>
56
55
  </Card>
@@ -5,11 +5,11 @@ import { Switch } from '@/shared/components/ui/switch';
5
5
  import { t } from '@/shared/lib/i18n';
6
6
  import type { DmScope } from '@/features/system-status/utils/runtime-config-agent.utils';
7
7
 
8
- const DM_SCOPE_OPTIONS: Array<{ value: DmScope; label: string }> = [
9
- { value: 'main', label: 'main' },
10
- { value: 'per-peer', label: 'per-peer' },
11
- { value: 'per-channel-peer', label: 'per-channel-peer' },
12
- { value: 'per-account-channel-peer', label: 'per-account-channel-peer' }
8
+ const DM_SCOPE_OPTIONS: Array<{ value: DmScope; labelKey: string }> = [
9
+ { value: 'main', labelKey: 'dmScopeMain' },
10
+ { value: 'per-peer', labelKey: 'dmScopePerPeer' },
11
+ { value: 'per-channel-peer', labelKey: 'dmScopePerChannelPeer' },
12
+ { value: 'per-account-channel-peer', labelKey: 'dmScopePerAccountChannelPeer' }
13
13
  ];
14
14
 
15
15
  export function RuntimeSettingsCard(props: {
@@ -21,31 +21,23 @@ export function RuntimeSettingsCard(props: {
21
21
  onDmScopeChange: (value: DmScope) => void;
22
22
  onDefaultContextTokensChange: (value: number) => void;
23
23
  onDefaultEngineChange: (value: string) => void;
24
- companionEnabledLabel?: string;
25
- companionEnabledHelp?: string;
26
- dmScopeLabel?: string;
27
- dmScopeHelp?: string;
28
- defaultContextTokensLabel?: string;
29
- defaultContextTokensHelp?: string;
30
- defaultEngineLabel?: string;
31
- defaultEngineHelp?: string;
32
24
  }) {
33
25
  return (
34
26
  <Card>
35
27
  <CardHeader>
36
- <CardTitle>{props.dmScopeLabel ?? t('dmScope')}</CardTitle>
37
- <CardDescription>{props.dmScopeHelp ?? t('dmScopeHelp')}</CardDescription>
28
+ <CardTitle>{t('dmScope')}</CardTitle>
29
+ <CardDescription>{t('dmScopeHelp')}</CardDescription>
38
30
  </CardHeader>
39
31
  <CardContent className="space-y-4">
40
32
  <div className="flex items-start justify-between gap-4 rounded-md border border-gray-200 px-4 py-3">
41
33
  <div className="space-y-1">
42
- <div className="text-sm font-medium text-gray-800">{props.companionEnabledLabel ?? t('runtimeCompanionEnabled')}</div>
43
- <p className="text-xs text-gray-500">{props.companionEnabledHelp ?? t('runtimeCompanionEnabledHelp')}</p>
34
+ <div className="text-sm font-medium text-gray-800">{t('runtimeCompanionEnabled')}</div>
35
+ <p className="text-xs text-gray-500">{t('runtimeCompanionEnabledHelp')}</p>
44
36
  </div>
45
37
  <Switch checked={props.companionEnabled} onCheckedChange={props.onCompanionEnabledChange} />
46
38
  </div>
47
39
  <div className="space-y-2">
48
- <label className="text-sm font-medium text-gray-800">{props.defaultContextTokensLabel ?? t('defaultContextTokens')}</label>
40
+ <label className="text-sm font-medium text-gray-800">{t('defaultContextTokens')}</label>
49
41
  <Input
50
42
  type="number"
51
43
  min={1000}
@@ -53,19 +45,19 @@ export function RuntimeSettingsCard(props: {
53
45
  value={props.defaultContextTokens}
54
46
  onChange={(event) => props.onDefaultContextTokensChange(Math.max(1000, Number.parseInt(event.target.value, 10) || 1000))}
55
47
  />
56
- <p className="text-xs text-gray-500">{props.defaultContextTokensHelp ?? t('defaultContextTokensHelp')}</p>
48
+ <p className="text-xs text-gray-500">{t('defaultContextTokensHelp')}</p>
57
49
  </div>
58
50
  <div className="space-y-2">
59
- <label className="text-sm font-medium text-gray-800">{props.defaultEngineLabel ?? t('defaultEngine')}</label>
51
+ <label className="text-sm font-medium text-gray-800">{t('defaultEngine')}</label>
60
52
  <Input
61
53
  value={props.defaultEngine}
62
54
  onChange={(event) => props.onDefaultEngineChange(event.target.value)}
63
55
  placeholder={t('defaultEnginePlaceholder')}
64
56
  />
65
- <p className="text-xs text-gray-500">{props.defaultEngineHelp ?? t('defaultEngineHelp')}</p>
57
+ <p className="text-xs text-gray-500">{t('defaultEngineHelp')}</p>
66
58
  </div>
67
59
  <div className="space-y-2">
68
- <label className="text-sm font-medium text-gray-800">{props.dmScopeLabel ?? t('dmScope')}</label>
60
+ <label className="text-sm font-medium text-gray-800">{t('dmScope')}</label>
69
61
  <Select value={props.dmScope} onValueChange={(value) => props.onDmScopeChange(value as DmScope)}>
70
62
  <SelectTrigger>
71
63
  <SelectValue />
@@ -73,7 +65,7 @@ export function RuntimeSettingsCard(props: {
73
65
  <SelectContent>
74
66
  {DM_SCOPE_OPTIONS.map((option) => (
75
67
  <SelectItem key={option.value} value={option.value}>
76
- {option.label}
68
+ {t(option.labelKey)}
77
69
  </SelectItem>
78
70
  ))}
79
71
  </SelectContent>
@@ -15,7 +15,7 @@ const baseControlView = {
15
15
  available: false,
16
16
  requiresConfirmation: false,
17
17
  impact: 'brief-ui-disconnect' as const,
18
- reasonIfUnavailable: '当前页面已经由运行中的本地服务托管。'
18
+ reasonIfUnavailable: 'This page is already hosted by the running local service.'
19
19
  },
20
20
  canRestartService: {
21
21
  available: true,
@@ -31,9 +31,9 @@ const baseControlView = {
31
31
  available: false,
32
32
  requiresConfirmation: true,
33
33
  impact: 'full-app-relaunch' as const,
34
- reasonIfUnavailable: 'desktop only'
34
+ reasonIfUnavailable: 'App restart is only available in the desktop shell.'
35
35
  },
36
- managementHint: 'This page is served by the running local service.'
36
+ managementHint: 'This page is served by the running local service. Closing the browser does not stop it.'
37
37
  };
38
38
 
39
39
  const mocks = vi.hoisted(() => ({
@@ -87,7 +87,9 @@ describe('RuntimeControlCard', () => {
87
87
  expect(screen.getByRole('button', { name: '停止服务' })).toBeTruthy();
88
88
  expect(startButton.disabled).toBe(true);
89
89
  expect(restartAppButton.disabled).toBe(true);
90
- expect(screen.getByText('desktop only')).toBeTruthy();
90
+ expect(screen.getByText('运行时正常')).toBeTruthy();
91
+ expect(screen.getByText('此页面由正在运行的本地服务提供。关闭浏览器不会停止它。')).toBeTruthy();
92
+ expect(screen.getByText('只有桌面壳环境支持重启整个应用。')).toBeTruthy();
91
93
  });
92
94
 
93
95
  it('runs the restart service flow through the system status manager', async () => {
@@ -106,7 +108,7 @@ describe('RuntimeControlCard', () => {
106
108
  await waitFor(() => {
107
109
  expect(mocks.runRuntimeControlAction).toHaveBeenCalledWith('restart-service');
108
110
  });
109
- expect(toast.success).toHaveBeenCalledWith('Restart scheduled. This page may disconnect for a few seconds.');
111
+ expect(toast.success).toHaveBeenCalledWith('重启已安排。此页面可能会短暂断开。');
110
112
  });
111
113
 
112
114
  it('runs the stop service flow after confirmation', async () => {
@@ -127,7 +129,7 @@ describe('RuntimeControlCard', () => {
127
129
  expect(confirmSpy).toHaveBeenCalledTimes(1);
128
130
  expect(mocks.runRuntimeControlAction).toHaveBeenCalledWith('stop-service');
129
131
  });
130
- expect(toast.success).toHaveBeenCalledWith('Stop scheduled. This page will disconnect shortly.');
132
+ expect(toast.success).toHaveBeenCalledWith('停止已安排。此页面即将断开。');
131
133
  });
132
134
 
133
135
  it('runs the desktop restart app flow after confirmation', async () => {
@@ -8,6 +8,7 @@ import type {
8
8
  import { Button } from '@/shared/components/ui/button';
9
9
  import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/shared/components/ui/card';
10
10
  import { useRuntimeControlPanelView, systemStatusManager } from '@/features/system-status';
11
+ import { localizeRuntimeControlMessage } from '@/features/system-status/utils/system-status.utils';
11
12
  import { t } from '@/shared/lib/i18n';
12
13
  import { Loader2, Play, RotateCw, Square } from 'lucide-react';
13
14
  import { toast } from 'sonner';
@@ -140,7 +141,7 @@ export function RuntimeControlCard() {
140
141
  const capability = resolveActionCapability(controlView, action);
141
142
 
142
143
  if (!capability?.available) {
143
- toast.error(capability?.reasonIfUnavailable ?? t('runtimeControlLoadFailed'));
144
+ toast.error(localizeRuntimeControlMessage(capability?.reasonIfUnavailable) ?? t('runtimeControlLoadFailed'));
144
145
  return;
145
146
  }
146
147
  if (
@@ -153,7 +154,7 @@ export function RuntimeControlCard() {
153
154
 
154
155
  try {
155
156
  const result = await systemStatusManager.runRuntimeControlAction(action);
156
- toast.success(result.message);
157
+ toast.success(localizeRuntimeControlMessage(result.message) ?? result.message);
157
158
  } catch (error) {
158
159
  const message = error instanceof Error ? error.message : t('runtimeControlActionFailed');
159
160
  toast.error(`${t('runtimeControlActionFailed')}: ${message}`);
@@ -162,7 +163,7 @@ export function RuntimeControlCard() {
162
163
 
163
164
  const handleRestartApp = async () => {
164
165
  if (!controlView?.canRestartApp.available) {
165
- toast.error(controlView?.canRestartApp.reasonIfUnavailable ?? t('runtimeRestartAppUnavailable'));
166
+ toast.error(localizeRuntimeControlMessage(controlView?.canRestartApp.reasonIfUnavailable) ?? t('runtimeRestartAppUnavailable'));
166
167
  return;
167
168
  }
168
169
  if (!window.confirm(t('runtimeControlRestartAppConfirm'))) {
@@ -171,7 +172,7 @@ export function RuntimeControlCard() {
171
172
 
172
173
  try {
173
174
  const result = await systemStatusManager.runRuntimeControlAction('restart-app');
174
- toast.success(result.message);
175
+ toast.success(localizeRuntimeControlMessage(result.message) ?? result.message);
175
176
  } catch (error) {
176
177
  const message = error instanceof Error ? error.message : t('runtimeControlActionFailed');
177
178
  toast.error(`${t('runtimeControlActionFailed')}: ${message}`);
@@ -194,7 +195,7 @@ export function RuntimeControlCard() {
194
195
  </div>
195
196
  <p className="text-sm text-gray-600">{displayedMessage}</p>
196
197
  <div className="text-xs text-gray-500">{resolveLifecycleLabel(displayedLifecycle)}</div>
197
- {controlView?.managementHint ? <p className="text-xs text-gray-500">{controlView.managementHint}</p> : null}
198
+ {controlView?.managementHint ? <p className="text-xs text-gray-500">{localizeRuntimeControlMessage(controlView.managementHint)}</p> : null}
198
199
  {errorMessage && !busy ? <p className="text-sm text-amber-700">{errorMessage}</p> : null}
199
200
  </div>
200
201
 
@@ -251,7 +252,7 @@ export function RuntimeControlCard() {
251
252
  .filter((item) => !item.capability.available && item.capability.reasonIfUnavailable)
252
253
  .map((item) => (
253
254
  <p key={`${item.action}-reason`} className="text-xs text-gray-500">
254
- {item.capability.reasonIfUnavailable}
255
+ {localizeRuntimeControlMessage(item.capability.reasonIfUnavailable)}
255
256
  </p>
256
257
  ))}
257
258
  </CardContent>
@@ -9,15 +9,11 @@ const mocks = vi.hoisted(() => ({
9
9
  useConfigResult: null as unknown as {
10
10
  data: Record<string, unknown> | null;
11
11
  isLoading: boolean;
12
- },
13
- useConfigSchemaResult: null as unknown as {
14
- data: Record<string, unknown> | null;
15
12
  }
16
13
  }));
17
14
 
18
15
  vi.mock('@/shared/hooks/use-config', () => ({
19
16
  useConfig: () => mocks.useConfigResult,
20
- useConfigSchema: () => mocks.useConfigSchemaResult,
21
17
  useUpdateRuntime: () => ({
22
18
  mutate: mocks.mutate,
23
19
  isPending: false
@@ -68,16 +64,14 @@ describe('RuntimeConfig', () => {
68
64
  }
69
65
  }
70
66
  ],
67
+ companion: {
68
+ enabled: false
69
+ },
71
70
  session: {
72
71
  dmScope: 'per-peer'
73
72
  }
74
73
  }
75
74
  };
76
- mocks.useConfigSchemaResult = {
77
- data: {
78
- uiHints: {}
79
- }
80
- };
81
75
  });
82
76
 
83
77
  it('saves the migrated runtime page through the system-status feature root', async () => {
@@ -131,10 +125,26 @@ describe('RuntimeConfig', () => {
131
125
  }
132
126
  }
133
127
  ],
128
+ companion: {
129
+ enabled: false
130
+ },
134
131
  session: {
135
132
  dmScope: 'per-peer'
136
133
  }
137
134
  }
138
135
  });
139
136
  });
137
+
138
+ it('renders runtime settings labels in Chinese when the UI language is Chinese', () => {
139
+ setLanguage('zh');
140
+
141
+ render(<RuntimeConfig />);
142
+
143
+ expect(screen.getByText('保存运行时设置')).toBeTruthy();
144
+ expect(screen.getByText('运行时入口')).toBeTruthy();
145
+ expect(screen.getByText('启用')).toBeTruthy();
146
+ expect(screen.getByText('配置 JSON')).toBeTruthy();
147
+ expect(screen.getByPlaceholderText('默认运行时(如 native 或 codex)')).toBeTruthy();
148
+ expect(screen.getByPlaceholderText('入口 ID,例如 hermes')).toBeTruthy();
149
+ });
140
150
  });
@@ -1,15 +1,14 @@
1
- import { useConfig, useConfigSchema, useUpdateRuntime } from '@/shared/hooks/use-config';
1
+ import { useConfig, useUpdateRuntime } from '@/shared/hooks/use-config';
2
2
  import { RuntimeConfigEditor } from '@/features/system-status/components/config/runtime-config-editor';
3
3
  import { t } from '@/shared/lib/i18n';
4
4
 
5
5
  export function RuntimeConfig() {
6
6
  const { data: config, isLoading } = useConfig();
7
- const { data: schema } = useConfigSchema();
8
7
  const updateRuntime = useUpdateRuntime();
9
8
 
10
9
  if (isLoading || !config) {
11
10
  return <div className="p-8 text-gray-400">{t('runtimeLoading')}</div>;
12
11
  }
13
12
 
14
- return <RuntimeConfigEditor config={config} uiHints={schema?.uiHints} updateRuntime={updateRuntime} />;
13
+ return <RuntimeConfigEditor config={config} updateRuntime={updateRuntime} />;
15
14
  }
@@ -122,11 +122,11 @@ export function createRuntimeConfigUpdatePayload(input: {
122
122
  const normalizedRuntimeEntries = input.runtimeEntries.reduce<Record<string, RuntimeEntryView>>((entries, entry, index) => {
123
123
  const id = entry.id.trim();
124
124
  const type = entry.type.trim();
125
- if (!id) throw new Error(`Runtime entry id is required at index ${index}.`);
126
- if (!type) throw new Error(`Runtime entry type is required for "${id}".`);
127
- if (entries[id]) throw new Error(`Duplicate runtime entry id: ${id}`);
125
+ if (!id) throw new Error(t('runtimeEntryIdRequired').replace('{index}', String(index)));
126
+ if (!type) throw new Error(t('runtimeEntryTypeRequired').replace('{id}', id));
127
+ if (entries[id]) throw new Error(`${t('runtimeEntryDuplicate')}: ${id}`);
128
128
  const configValue = entry.configText.trim() ? JSON.parse(entry.configText) : {};
129
- if (configValue && (typeof configValue !== 'object' || Array.isArray(configValue))) throw new Error(`Runtime entry config for "${id}" must be a JSON object.`);
129
+ if (configValue && (typeof configValue !== 'object' || Array.isArray(configValue))) throw new Error(t('runtimeEntryConfigObjectRequired').replace('{id}', id));
130
130
  entries[id] = {
131
131
  enabled: entry.enabled !== false,
132
132
  ...(entry.label?.trim() ? { label: entry.label.trim() } : {}),