@nextclaw/ui 0.12.9 → 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 (178) hide show
  1. package/CHANGELOG.md +61 -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-6ReNjvzF.js → DocBrowser-DMfr0Oow.js} +1 -1
  5. package/dist/assets/{DocBrowserContext-B6SpA7Qs.js → DocBrowserContext-BXydqby-.js} +1 -1
  6. package/dist/assets/{LogoBadge-ByNLYg65.js → LogoBadge-hO7tY7hE.js} +1 -1
  7. package/dist/assets/ModelConfig-CNIgLf0e.js +1 -0
  8. package/dist/assets/{ProviderScopedModelInput-Da7khnBA.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-D281Rotl.js → SecretsConfig-MEt6MjuD.js} +2 -2
  13. package/dist/assets/SessionsConfig-DifCiXwR.js +2 -0
  14. package/dist/assets/{app-query-client-VnFElj4E.js → app-query-client-9jNewezV.js} +1 -1
  15. package/dist/assets/{book-open-BdcxxoQu.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-DK5HPmIK.js → chunk-JZWAC4HX-C5dEc8hV.js} +1 -1
  19. package/dist/assets/{client-_i4MU2bB.js → client-C-8fH7-c.js} +1 -1
  20. package/dist/assets/{config-DtIQwrHF.js → config-CBScxsdV.js} +1 -1
  21. package/dist/assets/config-split-page-BUout_Ak.js +1 -0
  22. package/dist/assets/{createLucideIcon-BSeTgkZW.js → createLucideIcon-dy5ie7Ox.js} +1 -1
  23. package/dist/assets/desktop-update-config-2BS6BMkW.js +1 -0
  24. package/dist/assets/{dist-ccBFUi-o.js → dist-BruyLa92.js} +1 -1
  25. package/dist/assets/{dist-6TrrnPCR.js → dist-Cy7_j6hA.js} +1 -1
  26. package/dist/assets/{download-BhDxnyvU.js → download-BD0ETkB-.js} +1 -1
  27. package/dist/assets/{external-link-BgErLCNT.js → external-link-kZSAO8nT.js} +1 -1
  28. package/dist/assets/{hash-Bl7dr_UG.js → hash-BHJC2Ovu.js} +1 -1
  29. package/dist/assets/{i18n-eDHeDY0n.js → i18n-CpTZLchQ.js} +1 -1
  30. package/dist/assets/index-mW8W2FUu.css +1 -0
  31. package/dist/assets/index-zDZfXoI4.js +6 -0
  32. package/dist/assets/{infiniteQueryBehavior-ZDS92Qpp.js → infiniteQueryBehavior-CyER9hv0.js} +1 -1
  33. package/dist/assets/loader-circle-Bc2gCU33.js +1 -0
  34. package/dist/assets/{logos-x89HbrZ4.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-vZnghcFy.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-DT98i__E.js → refresh-ccw-COVhNHtN.js} +1 -1
  42. package/dist/assets/{refresh-cw-C47QSEwg.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-JtFzpNn6.js → rotate-cw-oHMKJMC8.js} +1 -1
  45. package/dist/assets/{save-3S6-H3Xw.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-vbanNPFU.js → status-dot-DpPtVzQT.js} +1 -1
  52. package/dist/assets/{switch-BsLtHOH-.js → switch-CM29eCAR.js} +1 -1
  53. package/dist/assets/{tabs-custom-D3HYMt6k.js → tabs-custom-YcZUWn3o.js} +1 -1
  54. package/dist/assets/tag-chip-DMXdnLcj.js +1 -0
  55. package/dist/assets/{trash-2-G48scll7.js → trash-2-mJT6oWa2.js} +1 -1
  56. package/dist/assets/{use-infinite-scroll-loader-DkNhD-42.js → use-infinite-scroll-loader-DJ1L81Dz.js} +1 -1
  57. package/dist/assets/{useConfirmDialog-BkvTN-vd.js → useConfirmDialog-BsVuqu1x.js} +1 -1
  58. package/dist/assets/{useMutation-CBWjE2uj.js → useMutation-CNcz2fgt.js} +1 -1
  59. package/dist/assets/x-Czwxm82I.js +1 -0
  60. package/dist/index.html +22 -22
  61. package/dist/runtime-icons/claude.ico +0 -0
  62. package/dist/runtime-icons/codex-openai.svg +6 -0
  63. package/dist/runtime-icons/hermes-agent.png +0 -0
  64. package/package.json +6 -6
  65. package/public/runtime-icons/claude.ico +0 -0
  66. package/public/runtime-icons/codex-openai.svg +6 -0
  67. package/public/runtime-icons/hermes-agent.png +0 -0
  68. package/src/account/components/account-panel.tsx +217 -97
  69. package/src/account/managers/account.manager.ts +3 -2
  70. package/src/api/chat-session-type.types.ts +7 -0
  71. package/src/api/runtime-control.types.ts +8 -0
  72. package/src/api/types.ts +8 -0
  73. package/src/app.tsx +221 -57
  74. package/src/components/agents/agent-dialogs.tsx +499 -0
  75. package/src/components/agents/agents-page.test.tsx +238 -0
  76. package/src/components/agents/agents-page.tsx +435 -0
  77. package/src/components/chat/ChatSidebar.tsx +11 -35
  78. package/src/components/chat/chat-conversation-panel.test.tsx +20 -0
  79. package/src/components/chat/chat-conversation-panel.tsx +83 -13
  80. package/src/components/chat/chat-page-shell.tsx +19 -13
  81. package/src/components/chat/chat-session-type-option-item.test.tsx +46 -0
  82. package/src/components/chat/chat-session-type-option-item.tsx +68 -0
  83. package/src/components/chat/chat-session-workspace-file-preview.test.tsx +87 -0
  84. package/src/components/chat/chat-session-workspace-file-preview.tsx +14 -43
  85. package/src/components/chat/chat-session-workspace-panel-nav.tsx +8 -2
  86. package/src/components/chat/chat-sidebar-project-groups.tsx +11 -36
  87. package/src/components/chat/ncp/__tests__/ncp-session-adapter.cancelled-tool.test.ts +77 -0
  88. package/src/components/chat/ncp/ncp-chat-page.tsx +2 -0
  89. package/src/components/chat/ncp/ncp-session-adapter.test.ts +1 -0
  90. package/src/components/chat/ncp/ncp-session-adapter.ts +3 -0
  91. package/src/components/chat/ncp/page/ncp-chat-derived-state.ts +10 -4
  92. package/src/components/chat/stores/chat-input.store.ts +2 -1
  93. package/src/components/chat/stores/chat-thread.store.ts +3 -1
  94. package/src/components/chat/useChatSessionTypeState.ts +10 -1
  95. package/src/components/chat/workspace/chat-session-workspace-file-breadcrumbs.tsx +86 -0
  96. package/src/components/common/BrandHeader.tsx +3 -1
  97. package/src/components/common/session-context-icon.tsx +15 -2
  98. package/src/components/common/{TagInput.tsx → tag-input.tsx} +25 -17
  99. package/src/components/config/ChannelForm.test.tsx +89 -3
  100. package/src/components/config/ChannelForm.tsx +157 -188
  101. package/src/components/config/ChannelsList.test.tsx +163 -119
  102. package/src/components/config/ChannelsList.tsx +90 -101
  103. package/src/components/config/ProviderForm.tsx +108 -146
  104. package/src/components/config/ProvidersList.tsx +100 -123
  105. package/src/components/config/SearchConfig.tsx +423 -393
  106. package/src/components/config/channel-form-fields-section.tsx +70 -37
  107. package/src/components/config/config-split-page.tsx +109 -0
  108. package/src/components/config/provider-enabled-field.tsx +17 -10
  109. package/src/components/config/runtime-control-card.test.tsx +56 -0
  110. package/src/components/config/runtime-control-card.tsx +25 -0
  111. package/src/components/config/runtime-presence-card.tsx +93 -79
  112. package/src/components/layout/AppLayout.tsx +25 -37
  113. package/src/components/layout/app-layout.test.tsx +46 -14
  114. package/src/components/layout/runtime-status-entry.test.tsx +157 -0
  115. package/src/components/layout/runtime-status-entry.tsx +143 -0
  116. package/src/components/marketplace/marketplace-detail-doc.ts +93 -0
  117. package/src/components/marketplace/marketplace-list-card.tsx +288 -0
  118. package/src/components/marketplace/marketplace-page-data.ts +129 -0
  119. package/src/components/marketplace/marketplace-page.test.tsx +339 -0
  120. package/src/components/marketplace/marketplace-page.tsx +596 -0
  121. package/src/components/marketplace/mcp/mcp-marketplace-card.tsx +128 -0
  122. package/src/components/marketplace/mcp/mcp-marketplace-dialogs.tsx +191 -0
  123. package/src/components/marketplace/mcp/mcp-marketplace-doc.ts +152 -0
  124. package/src/components/marketplace/mcp/mcp-marketplace-page.test.tsx +223 -0
  125. package/src/components/marketplace/mcp/mcp-marketplace-page.tsx +414 -0
  126. package/src/components/remote/remote-access-page.test.tsx +105 -0
  127. package/src/components/remote/remote-access-page.tsx +248 -0
  128. package/src/components/ui/notice-card.tsx +129 -0
  129. package/src/components/ui/setting-row.tsx +51 -0
  130. package/src/components/ui/tag-chip.tsx +39 -0
  131. package/src/components/ui/textarea.tsx +19 -0
  132. package/src/hooks/useConfig.ts +2 -1
  133. package/src/index.css +24 -0
  134. package/src/lib/app-resource-uri.test.ts +20 -0
  135. package/src/lib/app-resource-uri.ts +29 -0
  136. package/src/lib/i18n.remote.ts +1 -1
  137. package/src/lib/i18n.runtime-control.ts +31 -0
  138. package/src/lib/i18n.ts +5 -8
  139. package/src/lib/session-context.utils.test.ts +71 -0
  140. package/src/lib/session-context.utils.ts +28 -3
  141. package/src/lib/session-project/workspace-file-breadcrumb.test.ts +83 -0
  142. package/src/lib/session-project/workspace-file-breadcrumb.ts +188 -0
  143. package/dist/assets/ChannelsList-Ita2Zm1_.js +0 -8
  144. package/dist/assets/DocBrowser-BNwbPHf4.js +0 -1
  145. package/dist/assets/MarketplacePage-CjX2MWww.js +0 -1
  146. package/dist/assets/MarketplacePage-D0sDlYX4.js +0 -49
  147. package/dist/assets/McpMarketplacePage-BGKJm1sJ.js +0 -40
  148. package/dist/assets/ModelConfig-BzZenCH-.js +0 -1
  149. package/dist/assets/ProvidersList-BbVzRxjY.js +0 -1
  150. package/dist/assets/RemoteAccessPage-BaDH_X1Q.js +0 -1
  151. package/dist/assets/RuntimeConfig-F_XKGgLm.js +0 -1
  152. package/dist/assets/SearchConfig-BGkzXQP-.js +0 -1
  153. package/dist/assets/SessionsConfig-ChHQ7M5c.js +0 -2
  154. package/dist/assets/chat-page-Doe0yTtB.js +0 -58
  155. package/dist/assets/chat-session-display-cw78aiI_.js +0 -1
  156. package/dist/assets/config-layout-CHs0mAaR.js +0 -1
  157. package/dist/assets/desktop-update-config-Dpcf4BKG.js +0 -1
  158. package/dist/assets/index-CF9xve0E.js +0 -6
  159. package/dist/assets/index-FgA52VBt.css +0 -1
  160. package/dist/assets/loader-circle-ACM1s51e.js +0 -1
  161. package/dist/assets/play-CFUwCA2E.js +0 -1
  162. package/dist/assets/plus-rYsv72JG.js +0 -1
  163. package/dist/assets/popover-Bg1VoTZ6.js +0 -1
  164. package/dist/assets/search-3kFR_zh9.js +0 -1
  165. package/dist/assets/security-config-BWaiARNk.js +0 -1
  166. package/dist/assets/select-DJ2MUjBB.js +0 -41
  167. package/dist/assets/skeleton-ByQepn0M.js +0 -1
  168. package/dist/assets/x-ByDbItbq.js +0 -1
  169. package/src/components/agents/AgentDialogs.tsx +0 -400
  170. package/src/components/agents/AgentsPage.test.tsx +0 -217
  171. package/src/components/agents/AgentsPage.tsx +0 -352
  172. package/src/components/config/config-layout.ts +0 -10
  173. package/src/components/marketplace/MarketplacePage.test.tsx +0 -322
  174. package/src/components/marketplace/MarketplacePage.tsx +0 -827
  175. package/src/components/marketplace/mcp/McpMarketplacePage.test.tsx +0 -208
  176. package/src/components/marketplace/mcp/McpMarketplacePage.tsx +0 -580
  177. package/src/components/remote/RemoteAccessPage.test.tsx +0 -103
  178. package/src/components/remote/RemoteAccessPage.tsx +0 -144
@@ -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
  }