@nextclaw/ui 0.12.8 → 0.12.10

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 (227) hide show
  1. package/CHANGELOG.md +96 -0
  2. package/dist/assets/ChannelsList-M9FTK1Ak.js +8 -0
  3. package/dist/assets/DocBrowser-CH7-GxlL.js +1 -0
  4. package/dist/assets/{DocBrowser-BMxf9CIK.js → DocBrowser-DMfr0Oow.js} +1 -1
  5. package/dist/assets/{DocBrowserContext-Ce28gRXt.js → DocBrowserContext-BXydqby-.js} +1 -1
  6. package/dist/assets/{LogoBadge-o92MOA2L.js → LogoBadge-hO7tY7hE.js} +1 -1
  7. package/dist/assets/ModelConfig-CNIgLf0e.js +1 -0
  8. package/dist/assets/{ProviderScopedModelInput-CmTIzgI7.js → ProviderScopedModelInput-B3HWP4oz.js} +1 -1
  9. package/dist/assets/ProvidersList-CHjMnRhX.js +1 -0
  10. package/dist/assets/RuntimeConfig-psp8nMSG.js +1 -0
  11. package/dist/assets/SearchConfig-CSoKip1f.js +1 -0
  12. package/dist/assets/{SecretsConfig-Ba1RPJaG.js → SecretsConfig-MEt6MjuD.js} +2 -2
  13. package/dist/assets/SessionsConfig-DifCiXwR.js +2 -0
  14. package/dist/assets/{app-query-client-DniXoIN5.js → app-query-client-9jNewezV.js} +1 -1
  15. package/dist/assets/{book-open-DocgeQtR.js → book-open-DzdUViDm.js} +1 -1
  16. package/dist/assets/chat-page-CLp0UV0Y.js +58 -0
  17. package/dist/assets/chat-session-display-DsYHx0RZ.js +1 -0
  18. package/dist/assets/{chunk-JZWAC4HX-BvKvh1R8.js → chunk-JZWAC4HX-C5dEc8hV.js} +1 -1
  19. package/dist/assets/{client-CVqPF5ie.js → client-C-8fH7-c.js} +1 -1
  20. package/dist/assets/{config-Bop2oB18.js → config-CBScxsdV.js} +1 -1
  21. package/dist/assets/config-split-page-BUout_Ak.js +1 -0
  22. package/dist/assets/{createLucideIcon-DVv8taGY.js → createLucideIcon-dy5ie7Ox.js} +1 -1
  23. package/dist/assets/desktop-update-config-2BS6BMkW.js +1 -0
  24. package/dist/assets/{dist-DmAlInRu.js → dist-BruyLa92.js} +1 -1
  25. package/dist/assets/{dist-Da5Gm_pO.js → dist-Cy7_j6hA.js} +1 -1
  26. package/dist/assets/download-BD0ETkB-.js +1 -0
  27. package/dist/assets/{external-link-DFjw3x1B.js → external-link-kZSAO8nT.js} +1 -1
  28. package/dist/assets/{hash-DJtaCejM.js → hash-BHJC2Ovu.js} +1 -1
  29. package/dist/assets/i18n-CpTZLchQ.js +1 -0
  30. package/dist/assets/index-mW8W2FUu.css +1 -0
  31. package/dist/assets/index-zDZfXoI4.js +6 -0
  32. package/dist/assets/{infiniteQueryBehavior-DHSEQ3OH.js → infiniteQueryBehavior-CyER9hv0.js} +1 -1
  33. package/dist/assets/loader-circle-Bc2gCU33.js +1 -0
  34. package/dist/assets/{logos-DEFUIR12.js → logos-B7gRObP8.js} +1 -1
  35. package/dist/assets/marketplace-page-3qVMnF3d.js +1 -0
  36. package/dist/assets/marketplace-page-BhFIeQzI.js +49 -0
  37. package/dist/assets/mcp-marketplace-page-DYfteJ1D.js +40 -0
  38. package/dist/assets/{page-layout-Da3i3r6G.js → page-layout-0UcO9H9Z.js} +1 -1
  39. package/dist/assets/play-CKDjSQFL.js +1 -0
  40. package/dist/assets/plus-CG0QrVY_.js +1 -0
  41. package/dist/assets/{refresh-ccw-D6HkNtfz.js → refresh-ccw-COVhNHtN.js} +1 -1
  42. package/dist/assets/{refresh-cw-DRcvRrnc.js → refresh-cw-Bcv40SXy.js} +1 -1
  43. package/dist/assets/remote-access-page-CWHG-sug.js +1 -0
  44. package/dist/assets/{rotate-cw-BmDKfXtH.js → rotate-cw-oHMKJMC8.js} +1 -1
  45. package/dist/assets/{save-DHGmi2e9.js → save-EqJPOF0G.js} +1 -1
  46. package/dist/assets/search-BCAlB8nz.js +1 -0
  47. package/dist/assets/security-config-Slh0Mayz.js +1 -0
  48. package/dist/assets/select-CVz0t7MF.js +41 -0
  49. package/dist/assets/setting-row-CbVHAuQt.js +1 -0
  50. package/dist/assets/skeleton-D5rdKvzy.js +1 -0
  51. package/dist/assets/{status-dot-DurKKSwA.js → status-dot-DpPtVzQT.js} +1 -1
  52. package/dist/assets/{switch-0rmPBRKI.js → switch-CM29eCAR.js} +1 -1
  53. package/dist/assets/{tabs-custom-5JLVL6v8.js → tabs-custom-YcZUWn3o.js} +1 -1
  54. package/dist/assets/tag-chip-DMXdnLcj.js +1 -0
  55. package/dist/assets/{trash-2-C6caKPoz.js → trash-2-mJT6oWa2.js} +1 -1
  56. package/dist/assets/{use-infinite-scroll-loader-dwnaa_qi.js → use-infinite-scroll-loader-DJ1L81Dz.js} +1 -1
  57. package/dist/assets/{useConfirmDialog-mMeWD_yo.js → useConfirmDialog-BsVuqu1x.js} +1 -1
  58. package/dist/assets/{useMutation-BmxxvCNf.js → useMutation-CNcz2fgt.js} +1 -1
  59. package/dist/assets/x-Czwxm82I.js +1 -0
  60. package/dist/index.html +95 -21
  61. package/dist/manifest.webmanifest +30 -0
  62. package/dist/offline.html +102 -0
  63. package/dist/pwa-192.png +0 -0
  64. package/dist/pwa-512.png +0 -0
  65. package/dist/runtime-icons/claude.ico +0 -0
  66. package/dist/runtime-icons/codex-openai.svg +6 -0
  67. package/dist/runtime-icons/hermes-agent.png +0 -0
  68. package/dist/sw.js +80 -0
  69. package/index.html +73 -1
  70. package/package.json +5 -5
  71. package/public/manifest.webmanifest +30 -0
  72. package/public/offline.html +102 -0
  73. package/public/pwa-192.png +0 -0
  74. package/public/pwa-512.png +0 -0
  75. package/public/runtime-icons/claude.ico +0 -0
  76. package/public/runtime-icons/codex-openai.svg +6 -0
  77. package/public/runtime-icons/hermes-agent.png +0 -0
  78. package/public/sw.js +80 -0
  79. package/src/account/components/account-panel.tsx +217 -97
  80. package/src/account/managers/account.manager.ts +3 -2
  81. package/src/api/chat-session-type.types.ts +7 -0
  82. package/src/api/runtime-control.types.ts +8 -0
  83. package/src/api/server-path.ts +27 -4
  84. package/src/api/types.ts +25 -10
  85. package/src/app.tsx +227 -54
  86. package/src/components/agents/agent-dialogs.tsx +499 -0
  87. package/src/components/agents/agents-page.test.tsx +238 -0
  88. package/src/components/agents/agents-page.tsx +435 -0
  89. package/src/components/chat/ChatSidebar.test.tsx +43 -1
  90. package/src/components/chat/ChatSidebar.tsx +35 -35
  91. package/src/components/chat/adapters/chat-message.summary-truncation.test.ts +66 -0
  92. package/src/components/chat/adapters/file-operation/card.ts +9 -0
  93. package/src/components/chat/adapters/file-operation/diff.ts +14 -0
  94. package/src/components/chat/{ChatConversationPanel.test.tsx → chat-conversation-panel.test.tsx} +127 -206
  95. package/src/components/chat/chat-conversation-panel.tsx +482 -0
  96. package/src/components/chat/chat-page-shell.tsx +19 -13
  97. package/src/components/chat/chat-session-type-option-item.test.tsx +46 -0
  98. package/src/components/chat/chat-session-type-option-item.tsx +68 -0
  99. package/src/components/chat/chat-session-workspace-file-preview.test.tsx +178 -0
  100. package/src/components/chat/chat-session-workspace-file-preview.tsx +278 -0
  101. package/src/components/chat/chat-session-workspace-panel-nav.tsx +203 -0
  102. package/src/components/chat/chat-session-workspace-panel.tsx +318 -0
  103. package/src/components/chat/chat-sidebar-project-groups.tsx +11 -36
  104. package/src/components/chat/chat-sidebar-session-item.tsx +32 -2
  105. package/src/components/chat/containers/chat-message-list.container.test.tsx +49 -0
  106. package/src/components/chat/containers/chat-message-list.container.tsx +4 -0
  107. package/src/components/chat/managers/chat-session-list.manager.test.ts +12 -0
  108. package/src/components/chat/managers/chat-session-list.manager.ts +7 -0
  109. package/src/components/chat/ncp/__tests__/ncp-session-adapter.cancelled-tool.test.ts +77 -0
  110. package/src/components/chat/ncp/ncp-chat-page.tsx +9 -7
  111. package/src/components/chat/ncp/ncp-chat-thread.manager.ts +179 -41
  112. package/src/components/chat/ncp/ncp-session-adapter.test.ts +36 -1
  113. package/src/components/chat/ncp/ncp-session-adapter.ts +20 -0
  114. package/src/components/chat/ncp/page/ncp-chat-derived-state.ts +62 -13
  115. package/src/components/chat/ncp/tests/ncp-chat-thread.manager.test.ts +189 -0
  116. package/src/components/chat/presenter/chat-presenter-context.tsx +13 -2
  117. package/src/components/chat/session-header/chat-session-header-actions.test.tsx +26 -0
  118. package/src/components/chat/session-header/chat-session-header-actions.tsx +19 -1
  119. package/src/components/chat/stores/chat-input.store.ts +2 -1
  120. package/src/components/chat/stores/chat-thread.store.ts +27 -1
  121. package/src/components/chat/useChatSessionTypeState.ts +10 -1
  122. package/src/components/chat/workspace/chat-session-workspace-file-breadcrumbs.tsx +86 -0
  123. package/src/components/common/BrandHeader.tsx +3 -1
  124. package/src/components/common/session-context-icon.tsx +15 -2
  125. package/src/components/common/{TagInput.tsx → tag-input.tsx} +25 -17
  126. package/src/components/config/ChannelForm.test.tsx +89 -3
  127. package/src/components/config/ChannelForm.tsx +157 -188
  128. package/src/components/config/ChannelsList.test.tsx +163 -119
  129. package/src/components/config/ChannelsList.tsx +90 -101
  130. package/src/components/config/ProviderForm.tsx +108 -146
  131. package/src/components/config/ProvidersList.tsx +100 -123
  132. package/src/components/config/RuntimeConfig.tsx +141 -2
  133. package/src/components/config/SearchConfig.tsx +423 -393
  134. package/src/components/config/channel-form-fields-section.tsx +70 -37
  135. package/src/components/config/config-split-page.tsx +109 -0
  136. package/src/components/config/provider-enabled-field.tsx +17 -10
  137. package/src/components/config/runtime-control-card.test.tsx +56 -0
  138. package/src/components/config/runtime-control-card.tsx +25 -0
  139. package/src/components/config/runtime-presence-card.tsx +93 -79
  140. package/src/components/layout/AppLayout.tsx +25 -37
  141. package/src/components/layout/app-layout.test.tsx +46 -14
  142. package/src/components/layout/runtime-status-entry.test.tsx +157 -0
  143. package/src/components/layout/runtime-status-entry.tsx +143 -0
  144. package/src/components/marketplace/marketplace-detail-doc.ts +93 -0
  145. package/src/components/marketplace/marketplace-list-card.tsx +288 -0
  146. package/src/components/marketplace/marketplace-page-data.ts +129 -0
  147. package/src/components/marketplace/marketplace-page.test.tsx +339 -0
  148. package/src/components/marketplace/marketplace-page.tsx +596 -0
  149. package/src/components/marketplace/mcp/mcp-marketplace-card.tsx +128 -0
  150. package/src/components/marketplace/mcp/mcp-marketplace-dialogs.tsx +191 -0
  151. package/src/components/marketplace/mcp/mcp-marketplace-doc.ts +152 -0
  152. package/src/components/marketplace/mcp/mcp-marketplace-page.test.tsx +223 -0
  153. package/src/components/marketplace/mcp/mcp-marketplace-page.tsx +414 -0
  154. package/src/components/providers/ThemeProvider.tsx +5 -0
  155. package/src/components/remote/remote-access-page.test.tsx +105 -0
  156. package/src/components/remote/remote-access-page.tsx +248 -0
  157. package/src/components/ui/notice-card.tsx +129 -0
  158. package/src/components/ui/setting-row.tsx +51 -0
  159. package/src/components/ui/tag-chip.tsx +39 -0
  160. package/src/components/ui/textarea.tsx +19 -0
  161. package/src/hooks/server-path/use-server-path-read.ts +20 -0
  162. package/src/hooks/useConfig.ts +2 -1
  163. package/src/index.css +24 -0
  164. package/src/lib/app-resource-uri.test.ts +20 -0
  165. package/src/lib/app-resource-uri.ts +29 -0
  166. package/src/lib/chat-message.ts +14 -3
  167. package/src/lib/i18n.chat.ts +12 -1
  168. package/src/lib/i18n.pwa.ts +62 -0
  169. package/src/lib/i18n.remote.ts +1 -1
  170. package/src/lib/i18n.runtime-control.ts +31 -0
  171. package/src/lib/i18n.ts +7 -10
  172. package/src/lib/session-context.utils.test.ts +71 -0
  173. package/src/lib/session-context.utils.ts +28 -3
  174. package/src/lib/session-project/workspace-file-breadcrumb.test.ts +83 -0
  175. package/src/lib/session-project/workspace-file-breadcrumb.ts +188 -0
  176. package/src/pwa/components/pwa-install-entry.test.tsx +110 -0
  177. package/src/pwa/components/pwa-install-entry.tsx +205 -0
  178. package/src/pwa/managers/pwa-install.manager.test.ts +160 -0
  179. package/src/pwa/managers/pwa-install.manager.ts +232 -0
  180. package/src/pwa/managers/pwa-runtime.manager.ts +196 -0
  181. package/src/pwa/managers/pwa-shell-theme.manager.test.ts +30 -0
  182. package/src/pwa/managers/pwa-shell-theme.manager.ts +46 -0
  183. package/src/pwa/pwa-install-banner.storage.ts +55 -0
  184. package/src/pwa/pwa.types.ts +22 -0
  185. package/src/pwa/register-pwa.ts +14 -0
  186. package/src/pwa/stores/pwa.store.ts +17 -0
  187. package/src/vite-env.d.ts +9 -0
  188. package/dist/assets/ChannelsList-KIQIxluX.js +0 -8
  189. package/dist/assets/DocBrowser-CyDgAtO9.js +0 -1
  190. package/dist/assets/MarketplacePage-BySqkYDh.js +0 -49
  191. package/dist/assets/MarketplacePage-C0olZaek.js +0 -1
  192. package/dist/assets/McpMarketplacePage-DqKaiXO9.js +0 -40
  193. package/dist/assets/ModelConfig-IrmzoslW.js +0 -1
  194. package/dist/assets/ProvidersList-8_Kalfwl.js +0 -1
  195. package/dist/assets/RemoteAccessPage-CyQlSjPf.js +0 -1
  196. package/dist/assets/RuntimeConfig-Bk0uYBhf.js +0 -1
  197. package/dist/assets/SearchConfig-DNBR-UbE.js +0 -1
  198. package/dist/assets/SessionsConfig-Doqp5ghH.js +0 -2
  199. package/dist/assets/chat-page-Bph8M5zo.js +0 -58
  200. package/dist/assets/chat-session-display-CoN3Wmn-.js +0 -1
  201. package/dist/assets/config-layout-DmlGaay2.js +0 -1
  202. package/dist/assets/desktop-update-config-1KBrqLBC.js +0 -1
  203. package/dist/assets/i18n-CwHZ-9vt.js +0 -1
  204. package/dist/assets/index-DafCdM4F.css +0 -1
  205. package/dist/assets/index-DdksE6U3.js +0 -6
  206. package/dist/assets/loader-circle-PsSP0H9n.js +0 -1
  207. package/dist/assets/play-DBQbBxTA.js +0 -1
  208. package/dist/assets/plus-DUOVbsyQ.js +0 -1
  209. package/dist/assets/popover-C_mWOFzI.js +0 -1
  210. package/dist/assets/search-MChQRYR1.js +0 -1
  211. package/dist/assets/security-config-CbXfPZzr.js +0 -1
  212. package/dist/assets/select-Caud8QvU.js +0 -41
  213. package/dist/assets/skeleton-B-4vRq_Z.js +0 -1
  214. package/dist/assets/x-DuMhMATD.js +0 -1
  215. package/src/components/agents/AgentDialogs.tsx +0 -400
  216. package/src/components/agents/AgentsPage.test.tsx +0 -217
  217. package/src/components/agents/AgentsPage.tsx +0 -352
  218. package/src/components/chat/ChatConversationPanel.tsx +0 -256
  219. package/src/components/chat/chat-child-session-panel.tsx +0 -270
  220. package/src/components/config/config-layout.ts +0 -10
  221. package/src/components/marketplace/MarketplacePage.test.tsx +0 -322
  222. package/src/components/marketplace/MarketplacePage.tsx +0 -827
  223. package/src/components/marketplace/mcp/McpMarketplacePage.test.tsx +0 -208
  224. package/src/components/marketplace/mcp/McpMarketplacePage.tsx +0 -580
  225. package/src/components/remote/RemoteAccessPage.test.tsx +0 -103
  226. package/src/components/remote/RemoteAccessPage.tsx +0 -144
  227. /package/dist/assets/{config-hints-BZoDjXye.js → config-hints-BhTmc9P1.js} +0 -0
@@ -1,41 +1,44 @@
1
- import { useEffect, useMemo, useState } from 'react';
2
- import { useConfig, useConfigMeta, useConfigSchema, useCreateProvider } from '@/hooks/useConfig';
3
- import { Search, KeyRound, Plus } from 'lucide-react';
4
- import { ProviderForm } from './ProviderForm';
5
- import { cn } from '@/lib/utils';
6
- import { Tabs } from '@/components/ui/tabs-custom';
7
- import { LogoBadge } from '@/components/common/LogoBadge';
8
- import { hintForPath } from '@/lib/config-hints';
9
- import { StatusDot } from '@/components/ui/status-dot';
10
- import { t } from '@/lib/i18n';
11
- import { PageLayout, PageHeader } from '@/components/layout/page-layout';
12
- import { Input } from '@/components/ui/input';
13
- import { Button } from '@/components/ui/button';
14
- import { CONFIG_SIDEBAR_CARD_CLASS, CONFIG_SPLIT_GRID_CLASS } from './config-layout';
15
-
16
- function formatBasePreview(base?: string | null): string | null {
1
+ import { useEffect, useMemo, useState } from "react";
2
+ import { KeyRound, Plus, Search } from "lucide-react";
3
+ import { useConfig, useConfigMeta, useConfigSchema, useCreateProvider } from "@/hooks/useConfig";
4
+ import { LogoBadge } from "@/components/common/LogoBadge";
5
+ import { PageHeader, PageLayout } from "@/components/layout/page-layout";
6
+ import { Button } from "@/components/ui/button";
7
+ import { Input } from "@/components/ui/input";
8
+ import { Tabs } from "@/components/ui/tabs-custom";
9
+ import { StatusDot } from "@/components/ui/status-dot";
10
+ import { hintForPath } from "@/lib/config-hints";
11
+ import { t } from "@/lib/i18n";
12
+ import { cn } from "@/lib/utils";
13
+ import { ProviderForm } from "./ProviderForm";
14
+ import {
15
+ ConfigSelectionCard,
16
+ ConfigSplitEmptyState,
17
+ ConfigSplitPage,
18
+ ConfigSplitPaneBody,
19
+ ConfigSplitPaneHeader,
20
+ ConfigSplitSidebar,
21
+ } from "./config-split-page";
22
+
23
+ function formatBasePreview(base?: string | null) {
17
24
  if (!base) {
18
25
  return null;
19
26
  }
20
27
  try {
21
28
  const parsed = new URL(base);
22
- const path = parsed.pathname && parsed.pathname !== '/' ? parsed.pathname : '';
23
- return `${parsed.host}${path}`;
29
+ return `${parsed.host}${parsed.pathname && parsed.pathname !== "/" ? parsed.pathname : ""}`;
24
30
  } catch {
25
- return base.replace(/^https?:\/\//, '');
31
+ return base.replace(/^https?:\/\//, "");
26
32
  }
27
33
  }
28
34
 
29
- function sortProvidersForDisplay<T extends { name: string }>(providers: T[]): T[] {
35
+ function sortProvidersForDisplay<T extends { name: string }>(providers: T[]) {
30
36
  return providers
31
37
  .map((provider, index) => ({ provider, index }))
32
38
  .sort((left, right) => {
33
- const leftPriority = left.provider.name === 'nextclaw' ? 1 : 0;
34
- const rightPriority = right.provider.name === 'nextclaw' ? 1 : 0;
35
- if (leftPriority !== rightPriority) {
36
- return leftPriority - rightPriority;
37
- }
38
- return left.index - right.index;
39
+ const leftPriority = left.provider.name === "nextclaw" ? 1 : 0;
40
+ const rightPriority = right.provider.name === "nextclaw" ? 1 : 0;
41
+ return leftPriority !== rightPriority ? leftPriority - rightPriority : left.index - right.index;
39
42
  })
40
43
  .map(({ provider }) => provider);
41
44
  }
@@ -45,130 +48,109 @@ export function ProvidersList() {
45
48
  const { data: meta } = useConfigMeta();
46
49
  const { data: schema } = useConfigSchema();
47
50
  const createProvider = useCreateProvider();
48
-
49
- const [activeTab, setActiveTab] = useState('installed');
50
- const [selectedProvider, setSelectedProvider] = useState<string | undefined>();
51
- const [query, setQuery] = useState('');
52
-
53
- const uiHints = schema?.uiHints;
51
+ const [activeTab, setActiveTab] = useState("installed");
52
+ const [selectedProvider, setSelectedProvider] = useState<string>();
53
+ const [query, setQuery] = useState("");
54
54
  const providers = useMemo(() => sortProvidersForDisplay(meta?.providers ?? []), [meta?.providers]);
55
55
  const providersConfig = config?.providers ?? {};
56
- const configuredCount = providers.filter((provider) => {
57
- const current = providersConfig[provider.name];
58
- return current?.enabled !== false && current?.apiKeySet;
59
- }).length;
60
-
61
- const tabs = [
62
- { id: 'installed', label: t('providersTabConfigured'), count: configuredCount },
63
- { id: 'all', label: t('providersTabAll'), count: providers.length }
64
- ];
65
56
 
66
57
  const filteredProviders = useMemo(() => {
67
- const baseProviders = providers;
68
- const baseConfig = config?.providers ?? {};
69
58
  const keyword = query.trim().toLowerCase();
70
- return baseProviders
71
- .filter((provider) => {
72
- if (activeTab === 'installed') {
73
- const current = baseConfig[provider.name];
74
- return Boolean(current?.enabled !== false && current?.apiKeySet);
75
- }
76
- return true;
77
- })
59
+ return providers
60
+ .filter((provider) =>
61
+ activeTab !== "installed" || Boolean(providersConfig[provider.name]?.enabled !== false && providersConfig[provider.name]?.apiKeySet),
62
+ )
78
63
  .filter((provider) => {
79
64
  if (!keyword) {
80
65
  return true;
81
66
  }
82
- const configDisplayName = baseConfig[provider.name]?.displayName?.trim();
83
- const display = (configDisplayName || provider.displayName || provider.name).toLowerCase();
84
- return display.includes(keyword) || provider.name.toLowerCase().includes(keyword);
67
+ const display =
68
+ providersConfig[provider.name]?.displayName?.trim() || provider.displayName || provider.name;
69
+ return display.toLowerCase().includes(keyword) || provider.name.toLowerCase().includes(keyword);
85
70
  });
86
- }, [providers, config, activeTab, query]);
71
+ }, [activeTab, providers, providersConfig, query]);
87
72
 
88
73
  useEffect(() => {
89
- if (filteredProviders.length === 0) {
90
- setSelectedProvider(undefined);
91
- return;
92
- }
93
-
94
- const exists = filteredProviders.some((provider) => provider.name === selectedProvider);
95
- if (!exists) {
96
- setSelectedProvider(filteredProviders[0].name);
97
- }
74
+ setSelectedProvider(
75
+ filteredProviders.some((provider) => provider.name === selectedProvider)
76
+ ? selectedProvider
77
+ : filteredProviders[0]?.name,
78
+ );
98
79
  }, [filteredProviders, selectedProvider]);
99
80
 
100
- const selectedName = selectedProvider;
101
-
102
- const handleCreateCustomProvider = async () => {
103
- try {
104
- const result = await createProvider.mutateAsync({ data: {} });
105
- setActiveTab('all');
106
- setQuery('');
107
- setSelectedProvider(result.name);
108
- } catch {
109
- // toast handled in hook
110
- }
111
- };
112
-
113
81
  if (!config || !meta) {
114
- return <div className="p-8">{t('providersLoading')}</div>;
82
+ return <div className="p-8">{t("providersLoading")}</div>;
115
83
  }
116
84
 
117
85
  return (
118
- <PageLayout>
119
- <PageHeader title={t('providersPageTitle')} description={t('providersPageDescription')} />
120
-
121
- <div className={CONFIG_SPLIT_GRID_CLASS}>
122
- <section className={CONFIG_SIDEBAR_CARD_CLASS}>
123
- <div className="border-b border-gray-100 px-4 pt-4 pb-3 space-y-3">
124
- <Tabs tabs={tabs} activeTab={activeTab} onChange={setActiveTab} className="mb-0" />
86
+ <PageLayout className="pb-0 xl:flex xl:h-full xl:min-h-0 xl:flex-col">
87
+ <PageHeader title={t("providersPageTitle")} description={t("providersPageDescription")} />
88
+ <ConfigSplitPage className="xl:min-h-0">
89
+ <ConfigSplitSidebar>
90
+ <ConfigSplitPaneHeader className="space-y-3 px-4 pb-3 pt-4">
91
+ <Tabs
92
+ tabs={[
93
+ {
94
+ id: "installed",
95
+ label: t("providersTabConfigured"),
96
+ count: providers.filter((provider) => providersConfig[provider.name]?.enabled !== false && providersConfig[provider.name]?.apiKeySet).length,
97
+ },
98
+ { id: "all", label: t("providersTabAll"), count: providers.length },
99
+ ]}
100
+ activeTab={activeTab}
101
+ onChange={setActiveTab}
102
+ className="mb-0"
103
+ />
125
104
  <Button
126
105
  type="button"
127
106
  variant="outline"
128
107
  className="w-full justify-center"
129
- onClick={handleCreateCustomProvider}
108
+ onClick={async () => {
109
+ try {
110
+ const result = await createProvider.mutateAsync({ data: {} });
111
+ setActiveTab("all");
112
+ setQuery("");
113
+ setSelectedProvider(result.name);
114
+ } catch {
115
+ // toast handled in hook
116
+ }
117
+ }}
130
118
  disabled={createProvider.isPending}
131
119
  >
132
120
  <Plus className="mr-2 h-4 w-4" />
133
- {createProvider.isPending ? t('saving') : t('providerAddCustom')}
121
+ {createProvider.isPending ? t("saving") : t("providerAddCustom")}
134
122
  </Button>
135
- </div>
123
+ </ConfigSplitPaneHeader>
136
124
 
137
125
  <div className="border-b border-gray-100 px-4 py-3">
138
126
  <div className="relative">
139
127
  <Search className="pointer-events-none absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-gray-400" />
140
128
  <Input
141
129
  value={query}
142
- onChange={(e) => setQuery(e.target.value)}
143
- placeholder={t('providersFilterPlaceholder')}
130
+ onChange={(event) => setQuery(event.target.value)}
131
+ placeholder={t("providersFilterPlaceholder")}
144
132
  className="h-10 rounded-xl pl-9"
145
133
  />
146
134
  </div>
147
135
  </div>
148
136
 
149
- <div className="min-h-0 flex-1 space-y-2 overflow-y-auto p-3">
137
+ <ConfigSplitPaneBody className="space-y-2 p-3">
150
138
  {filteredProviders.map((provider) => {
151
- const providerConfig = config.providers[provider.name];
139
+ const providerConfig = providersConfig[provider.name];
152
140
  const isEnabled = providerConfig?.enabled !== false;
153
141
  const isReady = Boolean(isEnabled && providerConfig?.apiKeySet);
154
- const isActive = selectedName === provider.name;
155
- const providerLabel = providerConfig?.displayName?.trim() || provider.displayName || provider.name;
156
- const providerHint = hintForPath(`providers.${provider.name}`, uiHints);
157
- const resolvedBase = providerConfig?.apiBase || provider.defaultApiBase || '';
158
- const basePreview = formatBasePreview(resolvedBase);
159
- const description = basePreview || providerHint?.help || t('providersDefaultDescription');
142
+ const providerLabel =
143
+ providerConfig?.displayName?.trim() || provider.displayName || provider.name;
144
+ const description =
145
+ formatBasePreview(providerConfig?.apiBase || provider.defaultApiBase || "") ||
146
+ hintForPath(`providers.${provider.name}`, schema?.uiHints)?.help ||
147
+ t("providersDefaultDescription");
160
148
 
161
149
  return (
162
- <button
150
+ <ConfigSelectionCard
163
151
  key={provider.name}
164
- type="button"
165
152
  onClick={() => setSelectedProvider(provider.name)}
166
- className={cn(
167
- 'w-full rounded-xl border p-2.5 text-left transition-all',
168
- isActive
169
- ? 'border-primary/30 bg-primary-50/40 shadow-sm'
170
- : 'border-gray-200/70 bg-white hover:border-gray-300 hover:bg-gray-50/70'
171
- )}
153
+ active={selectedProvider === provider.name}
172
154
  >
173
155
  <div className="flex items-start justify-between gap-3">
174
156
  <div className="flex min-w-0 items-center gap-3">
@@ -176,8 +158,8 @@ export function ProvidersList() {
176
158
  name={provider.name}
177
159
  src={provider.logo ? `/logos/${provider.logo}` : null}
178
160
  className={cn(
179
- 'h-10 w-10 rounded-lg border',
180
- isReady ? 'border-primary/30 bg-white' : 'border-gray-200/70 bg-white'
161
+ "h-10 w-10 rounded-lg border",
162
+ isReady ? "border-primary/30 bg-white" : "border-gray-200/70 bg-white",
181
163
  )}
182
164
  imgClassName="h-5 w-5 object-contain"
183
165
  fallback={<span className="text-sm font-semibold uppercase text-gray-500">{provider.name[0]}</span>}
@@ -188,35 +170,30 @@ export function ProvidersList() {
188
170
  </div>
189
171
  </div>
190
172
  <StatusDot
191
- status={isEnabled ? (isReady ? 'ready' : 'setup') : 'inactive'}
192
- label={isEnabled ? (isReady ? t('statusReady') : t('statusSetup')) : t('disabled')}
173
+ status={isEnabled ? (isReady ? "ready" : "setup") : "inactive"}
174
+ label={isEnabled ? (isReady ? t("statusReady") : t("statusSetup")) : t("disabled")}
193
175
  className="min-w-[56px] justify-center"
194
176
  />
195
177
  </div>
196
- </button>
178
+ </ConfigSelectionCard>
197
179
  );
198
180
  })}
199
181
 
200
- {filteredProviders.length === 0 && (
201
- <div className="flex h-full min-h-[220px] flex-col items-center justify-center rounded-xl border border-dashed border-gray-200 bg-gray-50/70 py-10 text-center">
202
- <div className="mb-3 flex h-10 w-10 items-center justify-center rounded-lg bg-white">
203
- <KeyRound className="h-5 w-5 text-gray-300" />
204
- </div>
205
- <p className="text-sm font-medium text-gray-700">{t('providersNoMatch')}</p>
206
- </div>
207
- )}
208
- </div>
209
- </section>
182
+ {filteredProviders.length === 0 ? (
183
+ <ConfigSplitEmptyState icon={KeyRound} title={t("providersNoMatch")} />
184
+ ) : null}
185
+ </ConfigSplitPaneBody>
186
+ </ConfigSplitSidebar>
210
187
 
211
188
  <ProviderForm
212
- providerName={selectedName}
189
+ providerName={selectedProvider}
213
190
  onProviderDeleted={(deletedProvider) => {
214
191
  if (deletedProvider === selectedProvider) {
215
192
  setSelectedProvider(undefined);
216
193
  }
217
194
  }}
218
195
  />
219
- </div>
196
+ </ConfigSplitPage>
220
197
  </PageLayout>
221
198
  );
222
199
  }
@@ -1,6 +1,6 @@
1
1
  import { useEffect, useMemo, useState } from 'react';
2
2
  import { useConfig, useConfigSchema, useUpdateRuntime } from '@/hooks/useConfig';
3
- import type { AgentBindingView, AgentProfileView } from '@/api/types';
3
+ import type { AgentBindingView, AgentProfileView, RuntimeEntryView } from '@/api/types';
4
4
  import { Button } from '@/components/ui/button';
5
5
  import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
6
6
  import { RuntimeControlCard } from '@/components/config/runtime-control-card';
@@ -19,11 +19,16 @@ import {
19
19
  import { hintForPath } from '@/lib/config-hints';
20
20
  import { t } from '@/lib/i18n';
21
21
  import { PageLayout, PageHeader } from '@/components/layout/page-layout';
22
+ import { PwaInstallCard } from '@/pwa/components/pwa-install-entry';
22
23
  import { Plus, Save, Trash2 } from 'lucide-react';
23
24
  import { toast } from 'sonner';
24
25
 
25
26
  type DmScope = 'main' | 'per-peer' | 'per-channel-peer' | 'per-account-channel-peer';
26
27
  type PeerKind = '' | 'direct' | 'group' | 'channel';
28
+ type RuntimeEntryDraft = RuntimeEntryView & {
29
+ id: string;
30
+ configText: string;
31
+ };
27
32
 
28
33
  const DM_SCOPE_OPTIONS: Array<{ value: DmScope; label: string }> = [
29
34
  { value: 'main', label: 'main' },
@@ -32,12 +37,25 @@ const DM_SCOPE_OPTIONS: Array<{ value: DmScope; label: string }> = [
32
37
  { value: 'per-account-channel-peer', label: 'per-account-channel-peer' }
33
38
  ];
34
39
 
40
+ const DEFAULT_NARP_STDIO_ENTRY_CONFIG = {
41
+ wireDialect: 'acp',
42
+ processScope: 'per-session',
43
+ command: '',
44
+ args: ['acp'],
45
+ env: {},
46
+ cwd: '',
47
+ startupTimeoutMs: 8000,
48
+ probeTimeoutMs: 3000,
49
+ requestTimeoutMs: 120000
50
+ };
51
+
35
52
  function RuntimeConfigOverview() {
36
53
  return (
37
54
  <>
38
55
  <PageHeader title={t('runtimePageTitle')} description={t('runtimePageDescription')} />
39
56
  <RuntimeControlCard />
40
57
  <RuntimePresenceCard />
58
+ <PwaInstallCard />
41
59
  </>
42
60
  );
43
61
  }
@@ -49,6 +67,7 @@ export function RuntimeConfig() {
49
67
 
50
68
  const [agents, setAgents] = useState<AgentProfileView[]>([]);
51
69
  const [bindings, setBindings] = useState<AgentBindingView[]>([]);
70
+ const [runtimeEntries, setRuntimeEntries] = useState<RuntimeEntryDraft[]>([]);
52
71
  const [dmScope, setDmScope] = useState<DmScope>('per-channel-peer');
53
72
  const [defaultContextTokens, setDefaultContextTokens] = useState(200000);
54
73
  const [defaultEngine, setDefaultEngine] = useState('native');
@@ -59,6 +78,16 @@ export function RuntimeConfig() {
59
78
  }
60
79
  setAgents((config.agents.list ?? []).map(hydrateRuntimeAgent));
61
80
  setBindings((config.bindings ?? []).map(hydrateRuntimeBinding));
81
+ setRuntimeEntries(
82
+ Object.entries(config.agents.runtimes?.entries ?? {}).map(([id, entry]) => ({
83
+ id,
84
+ enabled: entry.enabled !== false,
85
+ label: entry.label ?? '',
86
+ type: entry.type,
87
+ config: entry.config ?? {},
88
+ configText: JSON.stringify(entry.config ?? {}, null, 2)
89
+ }))
90
+ );
62
91
  setDmScope((config.session?.dmScope as DmScope) ?? 'per-channel-peer');
63
92
  setDefaultContextTokens(config.agents.defaults.contextTokens ?? 200000);
64
93
  setDefaultEngine(config.agents.defaults.engine ?? 'native');
@@ -72,6 +101,7 @@ export function RuntimeConfig() {
72
101
  const agentEngineHint = hintForPath('agents.list.*.engine', uiHints);
73
102
  const agentsHint = hintForPath('agents.list', uiHints);
74
103
  const bindingsHint = hintForPath('bindings', uiHints);
104
+ const runtimeEntriesHint = hintForPath('agents.runtimes.entries', uiHints);
75
105
 
76
106
  const knownAgentIds = useMemo(() => {
77
107
  const ids = new Set<string>(['main']);
@@ -91,6 +121,29 @@ export function RuntimeConfig() {
91
121
  const updateBinding = (index: number, next: AgentBindingView) => {
92
122
  setBindings((prev) => prev.map((binding, cursor) => (cursor === index ? next : binding)));
93
123
  };
124
+
125
+ const updateRuntimeEntry = (index: number, patch: Partial<RuntimeEntryDraft>) => {
126
+ setRuntimeEntries((prev) => prev.map((entry, cursor) => (cursor === index ? { ...entry, ...patch } : entry)));
127
+ };
128
+
129
+ const removeRuntimeEntry = (index: number) => {
130
+ setRuntimeEntries((prev) => prev.filter((_, cursor) => cursor !== index));
131
+ };
132
+
133
+ const addRuntimeEntry = () => {
134
+ setRuntimeEntries((prev) => [
135
+ ...prev,
136
+ {
137
+ id: '',
138
+ enabled: true,
139
+ label: '',
140
+ type: 'narp-stdio',
141
+ config: DEFAULT_NARP_STDIO_ENTRY_CONFIG,
142
+ configText: JSON.stringify(DEFAULT_NARP_STDIO_ENTRY_CONFIG, null, 2)
143
+ }
144
+ ]);
145
+ };
146
+
94
147
  const handleSave = () => {
95
148
  try {
96
149
  const normalizedAgents = agents.map((agent, index) => {
@@ -150,6 +203,33 @@ export function RuntimeConfig() {
150
203
  return normalized;
151
204
  });
152
205
 
206
+ const normalizedRuntimeEntries = runtimeEntries.reduce<Record<string, RuntimeEntryView>>((entries, entry, index) => {
207
+ const id = entry.id.trim();
208
+ const type = entry.type.trim();
209
+ if (!id) {
210
+ throw new Error(`Runtime entry id is required at index ${index}.`);
211
+ }
212
+ if (!type) {
213
+ throw new Error(`Runtime entry type is required for "${id}".`);
214
+ }
215
+ if (entries[id]) {
216
+ throw new Error(`Duplicate runtime entry id: ${id}`);
217
+ }
218
+
219
+ const configValue = entry.configText.trim() ? JSON.parse(entry.configText) : {};
220
+ if (configValue && (typeof configValue !== 'object' || Array.isArray(configValue))) {
221
+ throw new Error(`Runtime entry config for "${id}" must be a JSON object.`);
222
+ }
223
+
224
+ entries[id] = {
225
+ enabled: entry.enabled !== false,
226
+ ...(entry.label?.trim() ? { label: entry.label.trim() } : {}),
227
+ type,
228
+ config: (configValue as Record<string, unknown>) ?? {}
229
+ };
230
+ return entries;
231
+ }, {});
232
+
153
233
  updateRuntime.mutate({
154
234
  data: {
155
235
  agents: {
@@ -157,7 +237,10 @@ export function RuntimeConfig() {
157
237
  contextTokens: Math.max(1000, defaultContextTokens),
158
238
  engine: defaultEngine.trim() || 'native'
159
239
  },
160
- list: normalizedAgents
240
+ list: normalizedAgents,
241
+ runtimes: {
242
+ entries: normalizedRuntimeEntries
243
+ }
161
244
  },
162
245
  bindings: normalizedBindings,
163
246
  session: {
@@ -228,6 +311,62 @@ export function RuntimeConfig() {
228
311
  </CardContent>
229
312
  </Card>
230
313
 
314
+ <Card>
315
+ <CardHeader>
316
+ <CardTitle>{runtimeEntriesHint?.label ?? 'Runtime Entries'}</CardTitle>
317
+ <CardDescription>{runtimeEntriesHint?.help ?? '统一管理可见的 runtime entry 与其配置。'}</CardDescription>
318
+ </CardHeader>
319
+ <CardContent className="space-y-3">
320
+ {runtimeEntries.map((entry, index) => (
321
+ <div key={`${index}-${entry.id || 'runtime-entry'}`} className="rounded-xl border border-gray-200 p-3 space-y-3">
322
+ <div className="grid grid-cols-1 md:grid-cols-2 gap-3">
323
+ <Input
324
+ value={entry.id}
325
+ onChange={(event) => updateRuntimeEntry(index, { id: event.target.value })}
326
+ placeholder="entry id,例如 hermes"
327
+ />
328
+ <Input
329
+ value={entry.label ?? ''}
330
+ onChange={(event) => updateRuntimeEntry(index, { label: event.target.value })}
331
+ placeholder="展示名称,例如 Hermes"
332
+ />
333
+ <Input
334
+ value={entry.type}
335
+ onChange={(event) => updateRuntimeEntry(index, { type: event.target.value })}
336
+ placeholder="runtime type,例如 narp-stdio"
337
+ />
338
+ <div className="flex items-center justify-between rounded-lg border border-gray-200 px-3 py-2">
339
+ <span className="text-sm text-gray-700">Enabled</span>
340
+ <Switch
341
+ checked={entry.enabled !== false}
342
+ onCheckedChange={(checked) => updateRuntimeEntry(index, { enabled: checked })}
343
+ />
344
+ </div>
345
+ </div>
346
+ <div className="space-y-2">
347
+ <label className="text-sm font-medium text-gray-800">Config JSON</label>
348
+ <textarea
349
+ className="min-h-32 w-full rounded-md border border-gray-200 px-3 py-2 text-sm font-mono"
350
+ value={entry.configText}
351
+ onChange={(event) => updateRuntimeEntry(index, { configText: event.target.value })}
352
+ spellCheck={false}
353
+ />
354
+ </div>
355
+ <div className="flex justify-end">
356
+ <Button type="button" variant="outline" onClick={() => removeRuntimeEntry(index)}>
357
+ <Trash2 className="mr-2 h-4 w-4" />
358
+ {t('deleteButton')}
359
+ </Button>
360
+ </div>
361
+ </div>
362
+ ))}
363
+ <Button type="button" variant="outline" onClick={addRuntimeEntry}>
364
+ <Plus className="mr-2 h-4 w-4" />
365
+ Add Runtime Entry
366
+ </Button>
367
+ </CardContent>
368
+ </Card>
369
+
231
370
  <Card>
232
371
  <CardHeader>
233
372
  <CardTitle>{agentsHint?.label ?? t('agentList')}</CardTitle>