@nextclaw/ui 0.12.9 → 0.12.11

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 (245) hide show
  1. package/CHANGELOG.md +102 -0
  2. package/dist/assets/ChannelsList-SQ7Oxotv.js +8 -0
  3. package/dist/assets/DocBrowser-BCO2k6XD.js +1 -0
  4. package/dist/assets/{DocBrowser-6ReNjvzF.js → DocBrowser-rDOjI3ga.js} +1 -1
  5. package/dist/assets/{DocBrowserContext-B6SpA7Qs.js → DocBrowserContext-BUq3Wo8O.js} +1 -1
  6. package/dist/assets/{LogoBadge-ByNLYg65.js → LogoBadge-DP8Ye7wJ.js} +1 -1
  7. package/dist/assets/ModelConfig-C77Ae9ru.js +1 -0
  8. package/dist/assets/ProviderScopedModelInput-CEnK61uo.js +1 -0
  9. package/dist/assets/ProvidersList-BCupBayq.js +1 -0
  10. package/dist/assets/RuntimeConfig-Ad-CAcmy.js +1 -0
  11. package/dist/assets/SearchConfig-BfCz4wJ4.js +1 -0
  12. package/dist/assets/SecretsConfig-DjmBIhyy.js +3 -0
  13. package/dist/assets/{SessionsConfig-ChHQ7M5c.js → SessionsConfig-CvjxU40H.js} +2 -2
  14. package/dist/assets/{book-open-BdcxxoQu.js → book-open-BE8M56IM.js} +1 -1
  15. package/dist/assets/chat-page-JKC6ln-y.js +58 -0
  16. package/dist/assets/chat-session-display-YcRMrAMa.js +1 -0
  17. package/dist/assets/{chunk-JZWAC4HX-DK5HPmIK.js → chunk-JZWAC4HX-erTUn3b8.js} +1 -1
  18. package/dist/assets/client-CszWMVKi.js +7 -0
  19. package/dist/assets/config-split-page-BAGSzUR3.js +1 -0
  20. package/dist/assets/{createLucideIcon-BSeTgkZW.js → createLucideIcon-CCiTGX8L.js} +1 -1
  21. package/dist/assets/desktop-DfkLlkG2.js +1 -0
  22. package/dist/assets/desktop-update-config-BXeGlqHD.js +1 -0
  23. package/dist/assets/dialog-BghZFPch.js +5 -0
  24. package/dist/assets/{dist-6TrrnPCR.js → dist-Dd9cr-kz.js} +1 -1
  25. package/dist/assets/dist-ZwoAXs46.js +9 -0
  26. package/dist/assets/{download-BhDxnyvU.js → download-D7LOizcW.js} +1 -1
  27. package/dist/assets/es2015-CEAreese.js +41 -0
  28. package/dist/assets/{external-link-BgErLCNT.js → external-link-qsnCMhw1.js} +1 -1
  29. package/dist/assets/{hash-Bl7dr_UG.js → hash-0zjWsNl-.js} +1 -1
  30. package/dist/assets/{i18n-eDHeDY0n.js → i18n-DvzXOGQX.js} +1 -1
  31. package/dist/assets/index-DvVTC9FF.css +1 -0
  32. package/dist/assets/index-lr6rQUSd.js +2 -0
  33. package/dist/assets/key-round-BLe9D8ND.js +1 -0
  34. package/dist/assets/loader-circle-wj7kARHv.js +1 -0
  35. package/dist/assets/{logos-x89HbrZ4.js → logos-_v5b2SdG.js} +1 -1
  36. package/dist/assets/marketplace-page-CAAk1Khc.js +1 -0
  37. package/dist/assets/marketplace-page-CfCiq90S.js +49 -0
  38. package/dist/assets/mcp-marketplace-page-D0Pp9Hs-.js +40 -0
  39. package/dist/assets/play-o6NmwGTi.js +1 -0
  40. package/dist/assets/plus-I9pBS4Fl.js +1 -0
  41. package/dist/assets/{refresh-cw-C47QSEwg.js → refresh-cw-MNqgR3LZ.js} +1 -1
  42. package/dist/assets/remote-C9fXm4V5.js +1 -0
  43. package/dist/assets/{save-3S6-H3Xw.js → save-D4bObrmH.js} +1 -1
  44. package/dist/assets/search-DxmL3IWE.js +1 -0
  45. package/dist/assets/security-config-BUm6FFfl.js +1 -0
  46. package/dist/assets/select-BILPf7zs.js +1 -0
  47. package/dist/assets/setting-row-BATDgg4r.js +1 -0
  48. package/dist/assets/skeleton-COKMAnJy.js +1 -0
  49. package/dist/assets/{switch-BsLtHOH-.js → switch-CBOzecWS.js} +1 -1
  50. package/dist/assets/{tabs-custom-D3HYMt6k.js → tabs-custom-Bx3cNhD-.js} +1 -1
  51. package/dist/assets/tag-chip-zUaDE2-H.js +1 -0
  52. package/dist/assets/{trash-2-G48scll7.js → trash-2-CQUgYyRn.js} +1 -1
  53. package/dist/assets/use-infinite-scroll-loader-B5V2Klve.js +1 -0
  54. package/dist/assets/useConfirmDialog-patAnl1g.js +1 -0
  55. package/dist/assets/{useMutation-CBWjE2uj.js → useMutation-__AYv-Pz.js} +1 -1
  56. package/dist/assets/x-BHUGQIUv.js +1 -0
  57. package/dist/index.html +22 -22
  58. package/dist/runtime-icons/claude.ico +0 -0
  59. package/dist/runtime-icons/codex-openai.svg +6 -0
  60. package/dist/runtime-icons/hermes-agent.png +0 -0
  61. package/module-structure.config.json +7 -0
  62. package/package.json +6 -6
  63. package/public/runtime-icons/claude.ico +0 -0
  64. package/public/runtime-icons/codex-openai.svg +6 -0
  65. package/public/runtime-icons/hermes-agent.png +0 -0
  66. package/src/api/chat-session-type.types.ts +7 -0
  67. package/src/api/config.ts +10 -0
  68. package/src/api/raw-client.test.ts +1 -1
  69. package/src/api/{raw-client.ts → raw-client.utils.ts} +2 -0
  70. package/src/api/runtime-control.types.ts +8 -0
  71. package/src/api/types.ts +48 -0
  72. package/src/app/components/app-manager-provider.tsx +20 -0
  73. package/src/app/managers/app.manager.ts +12 -0
  74. package/src/app.tsx +223 -59
  75. package/src/components/agents/agent-dialogs.tsx +499 -0
  76. package/src/components/agents/agents-page.test.tsx +238 -0
  77. package/src/components/agents/agents-page.tsx +435 -0
  78. package/src/components/chat/chat-conversation-panel.test.tsx +30 -0
  79. package/src/components/chat/chat-conversation-panel.tsx +83 -13
  80. package/src/components/chat/chat-input/ncp-chat-input-availability.utils.test.ts +92 -0
  81. package/src/components/chat/chat-input/ncp-chat-input-availability.utils.ts +45 -0
  82. package/src/components/chat/chat-page-shell.tsx +19 -13
  83. package/src/components/chat/chat-session-type-option-item.test.tsx +46 -0
  84. package/src/components/chat/chat-session-type-option-item.tsx +68 -0
  85. package/src/components/chat/chat-session-workspace-file-preview.test.tsx +87 -0
  86. package/src/components/chat/chat-session-workspace-file-preview.tsx +14 -43
  87. package/src/components/chat/chat-session-workspace-panel-nav.tsx +8 -2
  88. package/src/components/chat/chat-sidebar-project-groups.tsx +11 -36
  89. package/src/components/chat/containers/chat-input-bar.container.tsx +24 -12
  90. package/src/components/chat/{ChatSidebar.test.tsx → containers/chat-sidebar.test.tsx} +5 -4
  91. package/src/components/chat/{ChatSidebar.tsx → containers/chat-sidebar.tsx} +24 -72
  92. package/src/components/chat/hooks/use-chat-sidebar-session-label-editor.ts +49 -0
  93. package/src/components/chat/ncp/__tests__/ncp-session-adapter.cancelled-tool.test.ts +77 -0
  94. package/src/components/chat/ncp/ncp-app-client-fetch.ts +3 -0
  95. package/src/components/chat/ncp/ncp-chat-input.manager.ts +13 -5
  96. package/src/components/chat/ncp/ncp-chat-page.tsx +23 -2
  97. package/src/components/chat/ncp/ncp-session-adapter.test.ts +1 -0
  98. package/src/components/chat/ncp/ncp-session-adapter.ts +3 -0
  99. package/src/components/chat/ncp/page/ncp-chat-derived-state.ts +10 -4
  100. package/src/components/chat/ncp/session-conversation/use-ncp-session-conversation.test.tsx +48 -4
  101. package/src/components/chat/ncp/session-conversation/use-ncp-session-conversation.ts +43 -5
  102. package/src/components/chat/ncp/tests/ncp-chat-input.manager.test.ts +51 -1
  103. package/src/components/chat/stores/chat-input.store.ts +2 -1
  104. package/src/components/chat/stores/chat-thread.store.ts +3 -1
  105. package/src/components/chat/useChatSessionTypeState.ts +10 -1
  106. package/src/components/chat/workspace/chat-session-workspace-file-breadcrumbs.tsx +86 -0
  107. package/src/components/common/BrandHeader.tsx +3 -1
  108. package/src/components/common/session-context-icon.tsx +15 -2
  109. package/src/components/common/{TagInput.tsx → tag-input.tsx} +25 -17
  110. package/src/components/config/ChannelForm.test.tsx +89 -3
  111. package/src/components/config/ChannelForm.tsx +157 -188
  112. package/src/components/config/ChannelsList.test.tsx +163 -119
  113. package/src/components/config/ChannelsList.tsx +90 -101
  114. package/src/components/config/ProviderForm.tsx +108 -146
  115. package/src/components/config/ProvidersList.tsx +100 -123
  116. package/src/components/config/SearchConfig.tsx +423 -393
  117. package/src/components/config/channel-form-fields-section.tsx +70 -37
  118. package/src/components/config/config-split-page.tsx +109 -0
  119. package/src/components/config/desktop-update-config.test.tsx +10 -4
  120. package/src/components/config/desktop-update-config.tsx +5 -3
  121. package/src/components/config/provider-enabled-field.tsx +17 -10
  122. package/src/components/config/runtime-control-card.test.tsx +136 -158
  123. package/src/components/config/runtime-control-card.tsx +43 -68
  124. package/src/components/config/runtime-presence-card.test.tsx +10 -14
  125. package/src/components/config/runtime-presence-card.tsx +97 -81
  126. package/src/components/layout/AppLayout.tsx +25 -37
  127. package/src/components/layout/Sidebar.tsx +4 -4
  128. package/src/components/layout/app-layout.test.tsx +46 -14
  129. package/src/components/layout/runtime-status-entry.test.tsx +101 -0
  130. package/src/components/layout/runtime-status-entry.tsx +95 -0
  131. package/src/components/layout/sidebar.layout.test.tsx +11 -5
  132. package/src/components/marketplace/marketplace-detail-doc.ts +93 -0
  133. package/src/components/marketplace/marketplace-list-card.tsx +288 -0
  134. package/src/components/marketplace/marketplace-page-data.ts +129 -0
  135. package/src/components/marketplace/marketplace-page.test.tsx +339 -0
  136. package/src/components/marketplace/marketplace-page.tsx +596 -0
  137. package/src/components/marketplace/mcp/mcp-marketplace-card.tsx +128 -0
  138. package/src/components/marketplace/mcp/mcp-marketplace-dialogs.tsx +191 -0
  139. package/src/components/marketplace/mcp/mcp-marketplace-doc.ts +152 -0
  140. package/src/components/marketplace/mcp/mcp-marketplace-page.test.tsx +223 -0
  141. package/src/components/marketplace/mcp/mcp-marketplace-page.tsx +414 -0
  142. package/src/components/ui/notice-card.tsx +129 -0
  143. package/src/components/ui/setting-row.tsx +51 -0
  144. package/src/components/ui/tag-chip.tsx +39 -0
  145. package/src/components/ui/textarea.tsx +19 -0
  146. package/src/features/account/components/account-panel.tsx +255 -0
  147. package/src/features/account/index.ts +6 -0
  148. package/src/{account → features/account}/managers/account.manager.ts +6 -5
  149. package/src/features/remote/components/remote-access-page.test.tsx +104 -0
  150. package/src/features/remote/components/remote-access-page.tsx +250 -0
  151. package/src/{hooks/useRemoteAccess.ts → features/remote/hooks/use-remote-access.ts} +1 -1
  152. package/src/features/remote/index.ts +27 -0
  153. package/src/{remote → features/remote}/managers/remote-access.manager.ts +3 -4
  154. package/src/{remote → features/remote/services}/remote-access-feedback.service.test.ts +1 -1
  155. package/src/features/system-status/hooks/use-system-status.ts +104 -0
  156. package/src/features/system-status/index.ts +12 -0
  157. package/src/features/system-status/managers/system-status.manager.bootstrap-polling.test.ts +126 -0
  158. package/src/features/system-status/managers/system-status.manager.test.ts +142 -0
  159. package/src/features/system-status/managers/system-status.manager.ts +511 -0
  160. package/src/features/system-status/stores/system-status.store.ts +32 -0
  161. package/src/features/system-status/types/system-status.types.ts +73 -0
  162. package/src/features/system-status/utils/system-status.utils.test.ts +132 -0
  163. package/src/features/system-status/utils/system-status.utils.ts +202 -0
  164. package/src/hooks/use-realtime-query-bridge.ts +34 -18
  165. package/src/hooks/useConfig.ts +2 -1
  166. package/src/index.css +24 -0
  167. package/src/lib/app-resource-uri.test.ts +20 -0
  168. package/src/lib/app-resource-uri.ts +29 -0
  169. package/src/lib/i18n.chat.ts +8 -0
  170. package/src/lib/i18n.remote.ts +1 -1
  171. package/src/lib/i18n.runtime-control.ts +31 -0
  172. package/src/lib/i18n.ts +5 -8
  173. package/src/lib/session-context.utils.test.ts +71 -0
  174. package/src/lib/session-context.utils.ts +28 -3
  175. package/src/lib/session-project/workspace-file-breadcrumb.test.ts +83 -0
  176. package/src/lib/session-project/workspace-file-breadcrumb.ts +188 -0
  177. package/src/platforms/desktop/index.ts +20 -0
  178. package/src/{desktop → platforms/desktop}/managers/desktop-presence.manager.ts +2 -2
  179. package/src/{desktop → platforms/desktop}/managers/desktop-update.manager.ts +2 -2
  180. package/src/{desktop → platforms/desktop}/stores/desktop-presence.store.ts +1 -1
  181. package/src/{desktop → platforms/desktop}/stores/desktop-update.store.ts +1 -1
  182. package/src/stores/ui.store.ts +0 -9
  183. package/src/transport/{app-client.ts → app-client.service.ts} +9 -9
  184. package/src/transport/app-client.test.ts +9 -5
  185. package/src/transport/index.ts +1 -1
  186. package/src/transport/{local.transport.ts → local-transport.service.ts} +14 -12
  187. package/dist/assets/ChannelsList-Ita2Zm1_.js +0 -8
  188. package/dist/assets/DocBrowser-BNwbPHf4.js +0 -1
  189. package/dist/assets/MarketplacePage-CjX2MWww.js +0 -1
  190. package/dist/assets/MarketplacePage-D0sDlYX4.js +0 -49
  191. package/dist/assets/McpMarketplacePage-BGKJm1sJ.js +0 -40
  192. package/dist/assets/ModelConfig-BzZenCH-.js +0 -1
  193. package/dist/assets/ProviderScopedModelInput-Da7khnBA.js +0 -1
  194. package/dist/assets/ProvidersList-BbVzRxjY.js +0 -1
  195. package/dist/assets/RemoteAccessPage-BaDH_X1Q.js +0 -1
  196. package/dist/assets/RuntimeConfig-F_XKGgLm.js +0 -1
  197. package/dist/assets/SearchConfig-BGkzXQP-.js +0 -1
  198. package/dist/assets/SecretsConfig-D281Rotl.js +0 -3
  199. package/dist/assets/app-query-client-VnFElj4E.js +0 -1
  200. package/dist/assets/chat-page-Doe0yTtB.js +0 -58
  201. package/dist/assets/chat-session-display-cw78aiI_.js +0 -1
  202. package/dist/assets/client-_i4MU2bB.js +0 -7
  203. package/dist/assets/config-DtIQwrHF.js +0 -1
  204. package/dist/assets/config-layout-CHs0mAaR.js +0 -1
  205. package/dist/assets/desktop-update-config-Dpcf4BKG.js +0 -1
  206. package/dist/assets/dist-ccBFUi-o.js +0 -9
  207. package/dist/assets/index-CF9xve0E.js +0 -6
  208. package/dist/assets/index-FgA52VBt.css +0 -1
  209. package/dist/assets/infiniteQueryBehavior-ZDS92Qpp.js +0 -1
  210. package/dist/assets/loader-circle-ACM1s51e.js +0 -1
  211. package/dist/assets/page-layout-vZnghcFy.js +0 -1
  212. package/dist/assets/play-CFUwCA2E.js +0 -1
  213. package/dist/assets/plus-rYsv72JG.js +0 -1
  214. package/dist/assets/popover-Bg1VoTZ6.js +0 -1
  215. package/dist/assets/refresh-ccw-DT98i__E.js +0 -1
  216. package/dist/assets/rotate-cw-JtFzpNn6.js +0 -1
  217. package/dist/assets/search-3kFR_zh9.js +0 -1
  218. package/dist/assets/security-config-BWaiARNk.js +0 -1
  219. package/dist/assets/select-DJ2MUjBB.js +0 -41
  220. package/dist/assets/skeleton-ByQepn0M.js +0 -1
  221. package/dist/assets/status-dot-vbanNPFU.js +0 -1
  222. package/dist/assets/use-infinite-scroll-loader-DkNhD-42.js +0 -1
  223. package/dist/assets/useConfirmDialog-BkvTN-vd.js +0 -1
  224. package/dist/assets/x-ByDbItbq.js +0 -1
  225. package/src/account/components/account-panel.tsx +0 -135
  226. package/src/components/agents/AgentDialogs.tsx +0 -400
  227. package/src/components/agents/AgentsPage.test.tsx +0 -217
  228. package/src/components/agents/AgentsPage.tsx +0 -352
  229. package/src/components/config/config-layout.ts +0 -10
  230. package/src/components/marketplace/MarketplacePage.test.tsx +0 -322
  231. package/src/components/marketplace/MarketplacePage.tsx +0 -827
  232. package/src/components/marketplace/mcp/McpMarketplacePage.test.tsx +0 -208
  233. package/src/components/marketplace/mcp/McpMarketplacePage.tsx +0 -580
  234. package/src/components/remote/RemoteAccessPage.test.tsx +0 -103
  235. package/src/components/remote/RemoteAccessPage.tsx +0 -144
  236. package/src/hooks/use-runtime-control.ts +0 -24
  237. package/src/presenter/app-presenter-context.tsx +0 -20
  238. package/src/presenter/app.presenter.ts +0 -12
  239. package/src/runtime-control/runtime-control.manager.ts +0 -118
  240. /package/dist/assets/{config-hints-BhTmc9P1.js → config-hints-DSQQbeOA.js} +0 -0
  241. /package/src/{account → features/account}/stores/account.store.ts +0 -0
  242. /package/src/{remote → features/remote/services}/remote-access-feedback.service.ts +0 -0
  243. /package/src/{remote/remote-access.query.ts → features/remote/services/remote-access-query.service.ts} +0 -0
  244. /package/src/{remote → features/remote}/stores/remote-access.store.ts +0 -0
  245. /package/src/{desktop → platforms/desktop/types}/desktop-update.types.ts +0 -0
@@ -1,25 +1,30 @@
1
1
  import { useEffect, useMemo, useRef, useState } from 'react';
2
- import { useConfig, useConfigMeta, useConfigSchema, useUpdateChannel, useExecuteConfigAction } from '@/hooks/useConfig';
2
+ import { BookOpen, ChevronDown } from 'lucide-react';
3
+ import { toast } from 'sonner';
4
+ import { useConfig, useConfigMeta, useConfigSchema, useExecuteConfigAction, useUpdateChannel } from '@/hooks/useConfig';
5
+ import { LogoBadge } from '@/components/common/LogoBadge';
3
6
  import { Button } from '@/components/ui/button';
4
7
  import { StatusDot } from '@/components/ui/status-dot';
5
- import { LogoBadge } from '@/components/common/LogoBadge';
6
- import { t } from '@/lib/i18n';
7
- import { hintForPath } from '@/lib/config-hints';
8
- import { cn } from '@/lib/utils';
9
- import { toast } from 'sonner';
10
- import { BookOpen, ChevronDown } from 'lucide-react';
11
8
  import type { ConfigActionManifest } from '@/api/types';
9
+ import { hintForPath } from '@/lib/config-hints';
12
10
  import { resolveChannelTutorialUrl } from '@/lib/channel-tutorials';
11
+ import { t } from '@/lib/i18n';
13
12
  import { getChannelLogo } from '@/lib/logos';
14
- import { CONFIG_DETAIL_CARD_CLASS, CONFIG_EMPTY_DETAIL_CARD_CLASS } from './config-layout';
15
- import { ChannelFormFieldsSection } from './channel-form-fields-section';
13
+ import { cn } from '@/lib/utils';
14
+ import { appClient } from '@/transport';
15
+ import {
16
+ ConfigSplitDetailPane,
17
+ ConfigSplitEmptyPane,
18
+ ConfigSplitPaneBody,
19
+ ConfigSplitPaneFooter,
20
+ ConfigSplitPaneHeader
21
+ } from './config-split-page';
16
22
  import { buildChannelFormDefinitions, type ChannelField, type ChannelFormBlock, type ChannelFormFieldSection } from './channel-form-fields';
23
+ import { ChannelFormFieldsSection } from './channel-form-fields-section';
17
24
  import { WeixinChannelAuthSection } from './weixin-channel-auth-section';
18
25
 
19
- type ChannelFormProps = {
20
- channelName?: string;
21
- };
22
-
26
+ type ChannelFormProps = { channelName?: string };
27
+ type ChannelApplyState = { status: 'applying' | 'applied' | 'failed'; message?: string } | null;
23
28
  const EMPTY_CHANNEL_FIELDS: ChannelField[] = [];
24
29
  const DEFAULT_CHANNEL_LAYOUT_BLOCKS: ChannelFormBlock[] = [{ type: 'fields', section: 'all' }];
25
30
 
@@ -30,62 +35,77 @@ function isRecord(value: unknown): value is Record<string, unknown> {
30
35
  function deepMergeRecords(base: Record<string, unknown>, patch: Record<string, unknown>): Record<string, unknown> {
31
36
  const next: Record<string, unknown> = { ...base };
32
37
  for (const [key, value] of Object.entries(patch)) {
33
- const prev = next[key];
34
- if (isRecord(prev) && isRecord(value)) {
35
- next[key] = deepMergeRecords(prev, value);
36
- continue;
37
- }
38
- next[key] = value;
38
+ next[key] = isRecord(next[key]) && isRecord(value) ? deepMergeRecords(next[key] as Record<string, unknown>, value) : value;
39
39
  }
40
40
  return next;
41
41
  }
42
42
 
43
- function buildScopeDraft(scope: string, value: Record<string, unknown>): Record<string, unknown> {
44
- const segments = scope.split('.');
43
+ function buildScopeDraft(scope: string, value: Record<string, unknown>) {
45
44
  const output: Record<string, unknown> = {};
46
- let cursor: Record<string, unknown> = output;
45
+ let cursor = output;
46
+ const segments = scope.split('.');
47
47
  for (let index = 0; index < segments.length - 1; index += 1) {
48
- const segment = segments[index];
49
- cursor[segment] = {};
50
- cursor = cursor[segment] as Record<string, unknown>;
48
+ cursor[segments[index]] = {};
49
+ cursor = cursor[segments[index]] as Record<string, unknown>;
51
50
  }
52
51
  cursor[segments[segments.length - 1]] = value;
53
52
  return output;
54
53
  }
55
54
 
56
- function resolveFieldsForSection(fields: ChannelField[], section: ChannelFormFieldSection): ChannelField[] {
55
+ function resolveFieldsForSection(fields: ChannelField[], section: ChannelFormFieldSection) {
57
56
  if (section === 'all') {
58
57
  return fields;
59
58
  }
60
- if (section === 'primary') {
61
- return fields.filter((field) => field.section === 'primary');
62
- }
63
- return fields.filter((field) => field.section !== 'primary');
59
+ return fields.filter((field) => (section === 'primary' ? field.section === 'primary' : field.section !== 'primary'));
64
60
  }
65
61
 
66
- function buildJsonDrafts(channelConfig: Record<string, unknown>, fields: ChannelField[]): Record<string, string> {
62
+ function buildJsonDrafts(channelConfig: Record<string, unknown>, fields: ChannelField[]) {
67
63
  const nextDrafts: Record<string, string> = {};
68
- fields
69
- .filter((field) => field.type === 'json')
70
- .forEach((field) => {
71
- nextDrafts[field.name] = JSON.stringify(channelConfig[field.name] ?? {}, null, 2);
72
- });
64
+ fields.filter((field) => field.type === 'json').forEach((field) => {
65
+ nextDrafts[field.name] = JSON.stringify(channelConfig[field.name] ?? {}, null, 2);
66
+ });
73
67
  return nextDrafts;
74
68
  }
75
69
 
76
- function buildChannelFormHydrationKey(
77
- channelName: string | undefined,
78
- channelConfig: Record<string, unknown> | null | undefined,
79
- fields: ChannelField[]
80
- ): string {
81
- if (!channelName || !channelConfig) {
82
- return `empty:${channelName ?? ''}`;
70
+ function useChannelApplyState(channelName: string | undefined) {
71
+ const [channelApplyState, setChannelApplyState] = useState<ChannelApplyState>(null);
72
+
73
+ useEffect(() => {
74
+ if (!channelName) {
75
+ setChannelApplyState(null);
76
+ return;
77
+ }
78
+ return appClient.subscribe((event) => {
79
+ if (event.type !== 'channel.config.apply-status' || event.payload.channel !== channelName) {
80
+ return;
81
+ }
82
+ setChannelApplyState(
83
+ event.payload.status === 'started'
84
+ ? { status: 'applying' }
85
+ : event.payload.status === 'succeeded'
86
+ ? { status: 'applied' }
87
+ : { status: 'failed', message: event.payload.message }
88
+ );
89
+ });
90
+ }, [channelName]);
91
+
92
+ return channelApplyState;
93
+ }
94
+
95
+ function buildChannelApplyStatusView(channelApplyState: ChannelApplyState) {
96
+ if (!channelApplyState) {
97
+ return null;
83
98
  }
84
- return JSON.stringify({
85
- channelName,
86
- channelConfig,
87
- jsonFields: fields.filter((field) => field.type === 'json').map((field) => field.name)
88
- });
99
+ if (channelApplyState.status === 'applying') {
100
+ return { className: 'text-amber-600', label: t('channelConfigApplying') };
101
+ }
102
+ if (channelApplyState.status === 'applied') {
103
+ return { className: 'text-emerald-600', label: t('channelConfigApplied') };
104
+ }
105
+ return {
106
+ className: 'text-red-600',
107
+ label: `${t('channelConfigApplyFailed')}${channelApplyState.message ? `: ${channelApplyState.message}` : ''}`
108
+ };
89
109
  }
90
110
 
91
111
  export function ChannelForm({ channelName }: ChannelFormProps) {
@@ -94,71 +114,63 @@ export function ChannelForm({ channelName }: ChannelFormProps) {
94
114
  const { data: schema } = useConfigSchema();
95
115
  const updateChannel = useUpdateChannel();
96
116
  const executeAction = useExecuteConfigAction();
97
-
98
117
  const [formData, setFormData] = useState<Record<string, unknown>>({});
99
118
  const [jsonDrafts, setJsonDrafts] = useState<Record<string, string>>({});
100
119
  const [runningActionId, setRunningActionId] = useState<string | null>(null);
120
+ const channelApplyState = useChannelApplyState(channelName);
101
121
  const lastHydrationKeyRef = useRef<string | null>(null);
102
122
 
103
123
  const channelConfig = channelName ? config?.channels[channelName] : null;
104
- const channelDefinitions = useMemo(() => buildChannelFormDefinitions(), []);
105
- const channelDefinition = channelName ? channelDefinitions[channelName] : undefined;
124
+ const channelDefinition = useMemo(() => buildChannelFormDefinitions()[channelName || ''], [channelName]);
106
125
  const fields = channelDefinition?.fields ?? EMPTY_CHANNEL_FIELDS;
107
126
  const layoutBlocks = channelDefinition?.layout ?? DEFAULT_CHANNEL_LAYOUT_BLOCKS;
108
127
  const uiHints = schema?.uiHints;
109
128
  const scope = channelName ? `channels.${channelName}` : null;
110
129
  const actions = schema?.actions?.filter((action) => action.scope === scope) ?? [];
111
- const channelLabel = channelName
112
- ? hintForPath(`channels.${channelName}`, uiHints)?.label ?? channelName
113
- : channelName;
114
130
  const channelMeta = meta?.channels.find((item) => item.name === channelName);
131
+ const channelLabel = channelName ? hintForPath(`channels.${channelName}`, uiHints)?.label ?? channelName : channelName;
115
132
  const tutorialUrl = channelMeta ? resolveChannelTutorialUrl(channelMeta) : undefined;
116
- const hydrationKey = buildChannelFormHydrationKey(channelName, channelConfig, fields);
133
+ const hydrationKey = channelName && channelConfig
134
+ ? JSON.stringify({ channelName, channelConfig, jsonFields: fields.filter((field) => field.type === 'json').map((field) => field.name) })
135
+ : `empty:${channelName ?? ''}`;
117
136
 
118
137
  useEffect(() => {
119
138
  if (lastHydrationKeyRef.current === hydrationKey) {
120
139
  return;
121
140
  }
122
141
  lastHydrationKeyRef.current = hydrationKey;
123
-
124
142
  if (channelConfig) {
125
143
  setFormData({ ...channelConfig });
126
144
  setJsonDrafts(buildJsonDrafts(channelConfig, fields));
127
- } else {
128
- setFormData({});
129
- setJsonDrafts({});
145
+ return;
130
146
  }
147
+ setFormData({});
148
+ setJsonDrafts({});
131
149
  }, [channelConfig, fields, hydrationKey]);
132
150
 
133
- const updateField = (name: string, value: unknown) => {
134
- setFormData((prev) => ({ ...prev, [name]: value }));
135
- };
151
+ const updateField = (name: string, value: unknown) => setFormData((prev) => ({ ...prev, [name]: value }));
136
152
 
137
153
  const handleSubmit = (e: React.FormEvent) => {
138
154
  e.preventDefault();
139
-
140
- if (!channelName) return;
155
+ if (!channelName) {
156
+ return;
157
+ }
141
158
 
142
159
  const payload: Record<string, unknown> = { ...formData };
143
160
  for (const field of fields) {
144
- if (field.type !== 'password') {
145
- continue;
146
- }
147
- const value = payload[field.name];
148
- if (typeof value !== 'string' || value.length === 0) {
149
- delete payload[field.name];
150
- }
151
- }
152
- for (const field of fields) {
153
- if (field.type !== 'json') {
154
- continue;
161
+ if (field.type === 'password') {
162
+ const value = payload[field.name];
163
+ if (typeof value !== 'string' || value.length === 0) {
164
+ delete payload[field.name];
165
+ }
155
166
  }
156
- const raw = jsonDrafts[field.name] ?? '';
157
- try {
158
- payload[field.name] = raw.trim() ? JSON.parse(raw) : {};
159
- } catch {
160
- toast.error(`${t('invalidJson')}: ${field.name}`);
161
- return;
167
+ if (field.type === 'json') {
168
+ try {
169
+ payload[field.name] = (jsonDrafts[field.name] ?? '').trim() ? JSON.parse(jsonDrafts[field.name]) : {};
170
+ } catch {
171
+ toast.error(`${t('invalidJson')}: ${field.name}`);
172
+ return;
173
+ }
162
174
  }
163
175
  }
164
176
 
@@ -166,21 +178,14 @@ export function ChannelForm({ channelName }: ChannelFormProps) {
166
178
  };
167
179
 
168
180
  const applyActionPatchToForm = (patch?: Record<string, unknown>) => {
169
- if (!patch || !channelName) {
170
- return;
171
- }
172
- const channelsNode = patch.channels;
173
- if (!isRecord(channelsNode)) {
174
- return;
175
- }
176
- const channelPatch = channelsNode[channelName];
177
- if (!isRecord(channelPatch)) {
181
+ if (!patch || !channelName || !isRecord(patch.channels) || !isRecord(patch.channels[channelName])) {
178
182
  return;
179
183
  }
184
+ const channelPatch = patch.channels[channelName] as Record<string, unknown>;
180
185
  setFormData((prev) => deepMergeRecords(prev, channelPatch));
181
186
  setJsonDrafts((prev) => {
182
- let changed = false;
183
187
  const nextDrafts = { ...prev };
188
+ let changed = false;
184
189
  for (const field of fields) {
185
190
  if (field.type !== 'json' || !Object.prototype.hasOwnProperty.call(channelPatch, field.name)) {
186
191
  continue;
@@ -200,34 +205,19 @@ export function ChannelForm({ channelName }: ChannelFormProps) {
200
205
  setRunningActionId(action.id);
201
206
  try {
202
207
  let nextData = { ...formData };
203
-
204
208
  if (action.saveBeforeRun) {
205
- nextData = {
206
- ...nextData,
207
- ...(action.savePatch ?? {})
208
- };
209
+ nextData = { ...nextData, ...(action.savePatch ?? {}) };
209
210
  setFormData(nextData);
210
211
  await updateChannel.mutateAsync({ channel: channelName, data: nextData });
211
212
  }
212
-
213
213
  const result = await executeAction.mutateAsync({
214
214
  actionId: action.id,
215
- data: {
216
- scope,
217
- draftConfig: buildScopeDraft(scope, nextData)
218
- }
215
+ data: { scope, draftConfig: buildScopeDraft(scope, nextData) }
219
216
  });
220
-
221
217
  applyActionPatchToForm(result.patch);
222
-
223
- if (result.ok) {
224
- toast.success(result.message || t('success'));
225
- } else {
226
- toast.error(result.message || t('error'));
227
- }
218
+ result.ok ? toast.success(result.message || t('success')) : toast.error(result.message || t('error'));
228
219
  } catch (error) {
229
- const message = error instanceof Error ? error.message : String(error);
230
- toast.error(`${t('error')}: ${message}`);
220
+ toast.error(`${t('error')}: ${error instanceof Error ? error.message : String(error)}`);
231
221
  } finally {
232
222
  setRunningActionId(null);
233
223
  }
@@ -235,135 +225,114 @@ export function ChannelForm({ channelName }: ChannelFormProps) {
235
225
 
236
226
  if (!channelName || !channelMeta || !channelConfig) {
237
227
  return (
238
- <div className={CONFIG_EMPTY_DETAIL_CARD_CLASS}>
228
+ <ConfigSplitEmptyPane>
239
229
  <div>
240
- <h3 className="text-base font-semibold text-gray-900">{t('channelsSelectTitle')}</h3>
241
- <p className="mt-2 text-sm text-gray-500">{t('channelsSelectDescription')}</p>
230
+ <h3 className='text-base font-semibold text-gray-900'>{t('channelsSelectTitle')}</h3>
231
+ <p className='mt-2 text-sm text-gray-500'>{t('channelsSelectDescription')}</p>
242
232
  </div>
243
- </div>
233
+ </ConfigSplitEmptyPane>
244
234
  );
245
235
  }
246
236
 
247
237
  const enabled = typeof formData.enabled === 'boolean' ? formData.enabled : Boolean(channelConfig.enabled);
238
+ const channelApplyStatus = buildChannelApplyStatusView(channelApplyState);
248
239
 
249
240
  return (
250
- <div className={CONFIG_DETAIL_CARD_CLASS}>
251
- <div className="border-b border-gray-100 px-6 py-5">
252
- <div className="flex flex-wrap items-center justify-between gap-3">
253
- <div className="min-w-0">
254
- <div className="flex items-center gap-3">
241
+ <ConfigSplitDetailPane>
242
+ <ConfigSplitPaneHeader className='px-6 py-5'>
243
+ <div className='flex flex-wrap items-center justify-between gap-3'>
244
+ <div className='min-w-0'>
245
+ <div className='flex items-center gap-3'>
255
246
  <LogoBadge
256
247
  name={channelName}
257
248
  src={getChannelLogo(channelName)}
258
- className={cn(
259
- 'h-9 w-9 rounded-lg border',
260
- enabled ? 'border-primary/30 bg-white' : 'border-gray-200/70 bg-white'
261
- )}
262
- imgClassName="h-5 w-5 object-contain"
263
- fallback={<span className="text-sm font-semibold uppercase text-gray-500">{channelName[0]}</span>}
249
+ className={cn('h-9 w-9 rounded-lg border', enabled ? 'border-primary/30 bg-white' : 'border-gray-200/70 bg-white')}
250
+ imgClassName='h-5 w-5 object-contain'
251
+ fallback={<span className='text-sm font-semibold uppercase text-gray-500'>{channelName[0]}</span>}
264
252
  />
265
- <h3 className="truncate text-lg font-semibold text-gray-900 capitalize">{channelLabel}</h3>
253
+ <h3 className='truncate text-lg font-semibold text-gray-900 capitalize'>{channelLabel}</h3>
266
254
  </div>
267
- <p className="mt-2 text-sm text-gray-500">{t('channelsFormDescription')}</p>
268
- {tutorialUrl && (
269
- <a
270
- href={tutorialUrl}
271
- className="mt-2 inline-flex items-center gap-1.5 text-xs text-primary transition-colors hover:text-primary-hover"
272
- >
273
- <BookOpen className="h-3.5 w-3.5" />
255
+ <p className='mt-2 text-sm text-gray-500'>{t('channelsFormDescription')}</p>
256
+ {channelApplyStatus ? <p className={cn('mt-2 text-xs font-medium', channelApplyStatus.className)}>{channelApplyStatus.label}</p> : null}
257
+ {tutorialUrl ? (
258
+ <a href={tutorialUrl} className='mt-2 inline-flex items-center gap-1.5 text-xs text-primary transition-colors hover:text-primary-hover'>
259
+ <BookOpen className='h-3.5 w-3.5' />
274
260
  {t('channelsGuideTitle')}
275
261
  </a>
276
- )}
262
+ ) : null}
277
263
  </div>
278
264
  <StatusDot status={enabled ? 'active' : 'inactive'} label={enabled ? t('statusActive') : t('statusInactive')} />
279
265
  </div>
280
- </div>
266
+ </ConfigSplitPaneHeader>
281
267
 
282
- <form onSubmit={handleSubmit} className="flex min-h-0 flex-1 flex-col">
283
- <div className="min-h-0 flex-1 space-y-6 overflow-y-auto overscroll-contain px-6 py-5">
268
+ <form onSubmit={handleSubmit} className='flex min-h-0 flex-1 flex-col'>
269
+ <ConfigSplitPaneBody className='space-y-6 px-6 py-5'>
284
270
  {layoutBlocks.map((block, index) => {
285
271
  if (block.type === 'fields') {
286
272
  const blockFields = resolveFieldsForSection(fields, block.section);
287
273
  if (blockFields.length === 0) {
288
274
  return null;
289
275
  }
276
+ const content = (
277
+ <ChannelFormFieldsSection
278
+ channelName={channelName}
279
+ fields={blockFields}
280
+ formData={formData}
281
+ jsonDrafts={jsonDrafts}
282
+ setJsonDrafts={setJsonDrafts}
283
+ updateField={updateField}
284
+ uiHints={uiHints}
285
+ />
286
+ );
290
287
  if (!block.collapsible) {
291
- return (
292
- <ChannelFormFieldsSection
293
- key={`${block.type}-${block.section}-${index}`}
294
- channelName={channelName}
295
- fields={blockFields}
296
- formData={formData}
297
- jsonDrafts={jsonDrafts}
298
- setJsonDrafts={setJsonDrafts}
299
- updateField={updateField}
300
- uiHints={uiHints}
301
- />
302
- );
288
+ return <div key={`${block.type}-${block.section}-${index}`}>{content}</div>;
303
289
  }
304
290
  return (
305
- <details key={`${block.type}-${block.section}-${index}`} className="group rounded-2xl border border-gray-200/80 bg-white">
306
- <summary className="flex cursor-pointer list-none items-center justify-between gap-3 px-5 py-4 text-sm font-medium text-gray-900">
291
+ <details key={`${block.type}-${block.section}-${index}`} className='group rounded-2xl border border-gray-200/80 bg-white'>
292
+ <summary className='flex cursor-pointer list-none items-center justify-between gap-3 px-5 py-4 text-sm font-medium text-gray-900'>
307
293
  <div>
308
294
  <p>{block.collapsible.title}</p>
309
- {block.collapsible.description ? (
310
- <p className="mt-1 text-xs font-normal text-gray-500">{block.collapsible.description}</p>
311
- ) : null}
295
+ {block.collapsible.description ? <p className='mt-1 text-xs font-normal text-gray-500'>{block.collapsible.description}</p> : null}
312
296
  </div>
313
- <ChevronDown className="h-4 w-4 text-gray-400 transition-transform group-open:rotate-180" />
297
+ <ChevronDown className='h-4 w-4 text-gray-400 transition-transform group-open:rotate-180' />
314
298
  </summary>
315
- <div className="space-y-6 border-t border-gray-100 px-5 py-5">
316
- <ChannelFormFieldsSection
317
- channelName={channelName}
318
- fields={blockFields}
319
- formData={formData}
320
- jsonDrafts={jsonDrafts}
321
- setJsonDrafts={setJsonDrafts}
322
- updateField={updateField}
323
- uiHints={uiHints}
324
- />
325
- </div>
299
+ <div className='space-y-6 border-t border-gray-100 px-5 py-5'>{content}</div>
326
300
  </details>
327
301
  );
328
302
  }
329
-
330
- if (block.sectionId === 'weixin-auth') {
331
- return (
332
- <WeixinChannelAuthSection
333
- key={`${block.type}-${block.sectionId}-${index}`}
334
- channelConfig={channelConfig}
335
- formData={formData}
336
- channelEnabled={enabled}
337
- disabled={updateChannel.isPending || Boolean(runningActionId)}
338
- />
339
- );
340
- }
341
-
342
- return null;
303
+ return block.sectionId === 'weixin-auth' ? (
304
+ <WeixinChannelAuthSection
305
+ key={`${block.type}-${block.sectionId}-${index}`}
306
+ channelConfig={channelConfig}
307
+ formData={formData}
308
+ channelEnabled={enabled}
309
+ disabled={updateChannel.isPending || Boolean(runningActionId)}
310
+ />
311
+ ) : null;
343
312
  })}
344
- </div>
313
+ </ConfigSplitPaneBody>
345
314
 
346
- <div className="flex flex-wrap items-center justify-between gap-3 border-t border-gray-100 px-6 py-4">
347
- <div className="flex flex-wrap items-center gap-2">
315
+ <ConfigSplitPaneFooter className='flex flex-wrap items-center justify-between gap-3 px-6 py-4'>
316
+ <div className='flex flex-wrap items-center gap-2'>
348
317
  {actions
349
318
  .filter((action) => action.trigger === 'manual')
350
319
  .map((action) => (
351
320
  <Button
352
321
  key={action.id}
353
- type="button"
322
+ type='button'
354
323
  onClick={() => handleManualAction(action)}
355
324
  disabled={updateChannel.isPending || Boolean(runningActionId)}
356
- variant="secondary"
325
+ variant='secondary'
357
326
  >
358
327
  {runningActionId === action.id ? t('connecting') : action.title}
359
328
  </Button>
360
329
  ))}
361
330
  </div>
362
- <Button type="submit" disabled={updateChannel.isPending || Boolean(runningActionId)}>
331
+ <Button type='submit' disabled={updateChannel.isPending || Boolean(runningActionId)}>
363
332
  {updateChannel.isPending ? t('saving') : t('save')}
364
333
  </Button>
365
- </div>
334
+ </ConfigSplitPaneFooter>
366
335
  </form>
367
- </div>
336
+ </ConfigSplitDetailPane>
368
337
  );
369
338
  }