@nextclaw/ui 0.15.0 → 0.15.2

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 (191) hide show
  1. package/CHANGELOG.md +50 -0
  2. package/dist/assets/{api-DLtsiBKm.js → api-B7Qigvtm.js} +2 -2
  3. package/dist/assets/{appearance-settings-page-CtkeYXux.js → appearance-settings-page-CuE0Iiqj.js} +1 -1
  4. package/dist/assets/{book-open-BQwWBDKb.js → book-open-Ba2f7YiG.js} +1 -1
  5. package/dist/assets/channels-list-page-CTsM6TcP.js +8 -0
  6. package/dist/assets/chat-page-C_5dcuE3.js +1 -0
  7. package/dist/assets/config-split-page-Fbb4PtTf.js +1 -0
  8. package/dist/assets/{confirm-dialog-iIJFFp2T.js → confirm-dialog-D2EG3nqE.js} +1 -1
  9. package/dist/assets/desktop-update-config-BGR3HUi1.js +1 -0
  10. package/dist/assets/{dist-D2eZC3OR.js → dist-7JTjycxE.js} +1 -1
  11. package/dist/assets/{dist-CBxWOazz.js → dist-wXLhk4hl.js} +1 -1
  12. package/dist/assets/doc-browser-bHyXnxlj.js +1 -0
  13. package/dist/assets/doc-browser-context-CYAGm5d5.js +1 -0
  14. package/dist/assets/doc-browser-gTrQ3jCF.js +1 -0
  15. package/dist/assets/{es2015-DhQo_oS6.js → es2015-j1SRCIX5.js} +1 -1
  16. package/dist/assets/{external-link-CM5dnUI6.js → external-link-DxuPaDoK.js} +1 -1
  17. package/dist/assets/form-actions-BztL7P_T.js +1 -0
  18. package/dist/assets/{i18n-provider-di1waTGX.js → i18n-provider-B4oMxOFx.js} +1 -1
  19. package/dist/assets/index-CvrvblgX.css +1 -0
  20. package/dist/assets/index-Obd4JYU2.js +104 -0
  21. package/dist/assets/loader-circle-CdzgK4eH.js +1 -0
  22. package/dist/assets/{mcp-marketplace-page-C_MVQUbT.js → mcp-marketplace-page-Bt7CuOoq.js} +2 -2
  23. package/dist/assets/mcp-marketplace-page-Ck3t78_n.js +1 -0
  24. package/dist/assets/model-config-page-DeKk9gvE.js +1 -0
  25. package/dist/assets/plus-Bx7wQbhG.js +1 -0
  26. package/dist/assets/{provider-scoped-model-input-g1WkGiJ8.js → provider-scoped-model-input-DsH2LF1q.js} +1 -1
  27. package/dist/assets/providers-config-page-BFnGp68F.js +1 -0
  28. package/dist/assets/{react-DjpzoZXB.js → react-UirzUuST.js} +1 -1
  29. package/dist/assets/{refresh-cw-DaTv3zxE.js → refresh-cw-eT8BF-8B.js} +1 -1
  30. package/dist/assets/remote-CDkHZtA8.js +1 -0
  31. package/dist/assets/rotate-cw-1JqxHTuw.js +1 -0
  32. package/dist/assets/runtime-config-page-BelAqiNG.js +1 -0
  33. package/dist/assets/{save-BZof1uie.js → save-Cb2g0BOi.js} +1 -1
  34. package/dist/assets/{search-MxAarS_g.js → search-BcHjwdsb.js} +1 -1
  35. package/dist/assets/search-config-page-zPm6--Ig.js +1 -0
  36. package/dist/assets/secrets-config-page-Cxyp-KuQ.js +3 -0
  37. package/dist/assets/security-config-B_gCq9fn.js +1 -0
  38. package/dist/assets/{select-D7GcNP0z.js → select-CdSEOwVM.js} +1 -1
  39. package/dist/assets/{setting-row-CuozlnJd.js → setting-row-CTAtGexr.js} +1 -1
  40. package/dist/assets/settings-2-2DWauOjY.js +1 -0
  41. package/dist/assets/{side-dock-BcLau5S_.js → side-dock-DPvakJ0a.js} +2 -2
  42. package/dist/assets/{skeleton-shfmXuYp.js → skeleton-P5oM14E1.js} +1 -1
  43. package/dist/assets/{switch-C1BWMU5c.js → switch-BU_UEULb.js} +1 -1
  44. package/dist/assets/{tag-chip-DCUrQUCP.js → tag-chip-D-WsKoT1.js} +1 -1
  45. package/dist/assets/{tooltip-DfAHHA76.js → tooltip-CjkOaHW7.js} +1 -1
  46. package/dist/assets/{use-config-DAdbT5QJ.js → use-config--hx5oxQn.js} +1 -1
  47. package/dist/assets/{use-confirm-dialog-eWcJ6V-s.js → use-confirm-dialog-CD9a_SK1.js} +1 -1
  48. package/dist/assets/x-DT5x45JU.js +1 -0
  49. package/dist/index.html +38 -30
  50. package/index.html +12 -4
  51. package/package.json +9 -9
  52. package/src/app/components/layout/__tests__/sidebar.layout.test.tsx +29 -1
  53. package/src/app/components/layout/sidebar-items.tsx +43 -4
  54. package/src/app/components/layout/sidebar.tsx +83 -61
  55. package/src/app/configs/app-navigation.config.ts +49 -0
  56. package/src/app/styles/design-system.css +82 -0
  57. package/src/features/agents/components/__tests__/agents-page.test.tsx +135 -2
  58. package/src/features/agents/components/agent-advanced-config-fields.tsx +70 -0
  59. package/src/features/agents/components/agent-details-dialog.tsx +398 -0
  60. package/src/features/agents/components/agent-dialogs.tsx +20 -47
  61. package/src/features/agents/components/agents-page.tsx +76 -10
  62. package/src/features/agents/types/agent-form.types.ts +21 -0
  63. package/src/features/agents/utils/agent-form.utils.ts +39 -0
  64. package/src/features/channels/components/config/channel-form.tsx +14 -11
  65. package/src/features/chat/components/conversation/__tests__/chat-conversation-header-section.test.tsx +172 -1
  66. package/src/features/chat/components/conversation/chat-conversation-header-section.tsx +11 -1
  67. package/src/features/chat/components/conversation/chat-conversation-header.tsx +7 -3
  68. package/src/features/chat/components/conversation/chat-conversation-panel.tsx +1 -1
  69. package/src/features/chat/components/layout/__tests__/chat-sidebar-toolbar.test.tsx +48 -0
  70. package/src/features/chat/components/layout/__tests__/chat-sidebar-utility-menu.test.tsx +55 -0
  71. package/src/features/chat/components/layout/chat-sidebar-desktop-layout.tsx +0 -3
  72. package/src/features/chat/components/layout/chat-sidebar-toolbar.tsx +3 -4
  73. package/src/features/chat/components/layout/chat-sidebar-utility-menu.tsx +29 -30
  74. package/src/features/chat/components/layout/chat-sidebar.tsx +21 -7
  75. package/src/features/chat/features/conversation/components/__tests__/session-conversation-area.test.tsx +18 -3
  76. package/src/features/chat/features/conversation/components/session-conversation-input.tsx +2 -1
  77. package/src/features/chat/features/message/components/__tests__/chat-inline-panel-app-card.test.tsx +22 -1
  78. package/src/features/chat/features/message/components/__tests__/chat-message-list.container.test.tsx +1 -1
  79. package/src/features/chat/features/message/components/chat-inline-panel-app-card.tsx +10 -5
  80. package/src/features/chat/features/message/utils/chat-inline-panel-app-card.utils.ts +0 -11
  81. package/src/features/chat/features/ncp/hooks/__tests__/use-ncp-agent-runtime.test.tsx +23 -1
  82. package/src/features/chat/features/ncp/hooks/__tests__/use-ncp-session-list-view.test.tsx +74 -0
  83. package/src/features/chat/features/ncp/hooks/use-ncp-session-list-view.ts +3 -2
  84. package/src/features/chat/features/session/components/__tests__/chat-sidebar-project-groups.test.tsx +116 -0
  85. package/src/features/chat/features/session/components/chat-sidebar-project-groups.tsx +108 -46
  86. package/src/features/chat/features/session/components/chat-sidebar-session-entry.tsx +6 -0
  87. package/src/features/chat/features/session/components/chat-sidebar-session-item.tsx +48 -24
  88. package/src/features/chat/features/session/components/chat-sidebar-session-list.tsx +0 -3
  89. package/src/features/chat/features/session/components/session-header/chat-session-header-actions.tsx +0 -4
  90. package/src/features/chat/features/session/components/session-header/chat-session-title-switcher.tsx +294 -0
  91. package/src/features/chat/features/session/utils/__tests__/chat-session-display.utils.test.ts +15 -1
  92. package/src/features/chat/features/session/utils/__tests__/ncp-session-adapter.utils.test.ts +24 -4
  93. package/src/features/chat/features/session/utils/chat-session-display.utils.ts +3 -0
  94. package/src/features/chat/features/session/utils/chat-sidebar-session-groups.utils.ts +25 -3
  95. package/src/features/chat/features/session/utils/ncp-session-adapter.utils.ts +1 -0
  96. package/src/features/chat/features/welcome/components/chat-welcome-capability-grid.tsx +9 -4
  97. package/src/features/chat/features/welcome/components/chat-welcome.tsx +0 -5
  98. package/src/features/chat/features/workspace/components/__tests__/chat-session-workspace-file-preview.test.tsx +193 -56
  99. package/src/features/chat/features/workspace/components/__tests__/chat-session-workspace-panel-content.test.tsx +1 -0
  100. package/src/features/chat/features/workspace/components/__tests__/chat-session-workspace-panel.test.tsx +144 -0
  101. package/src/features/chat/features/workspace/components/chat-session-workspace-directory-browser.tsx +99 -0
  102. package/src/features/chat/features/workspace/components/chat-session-workspace-file-breadcrumbs.tsx +3 -1
  103. package/src/features/chat/features/workspace/components/chat-session-workspace-file-preview.tsx +67 -14
  104. package/src/features/chat/features/workspace/components/chat-session-workspace-panel-content.tsx +3 -0
  105. package/src/features/chat/features/workspace/components/chat-session-workspace-panel-nav.tsx +44 -1
  106. package/src/features/chat/features/workspace/components/chat-session-workspace-panel.tsx +45 -3
  107. package/src/features/chat/features/workspace/utils/__tests__/chat-workspace-panel-view-model.utils.test.ts +46 -0
  108. package/src/features/chat/managers/__tests__/chat-session-list.manager.test.ts +33 -2
  109. package/src/features/chat/managers/__tests__/chat-thread.manager.test.ts +47 -0
  110. package/src/features/chat/managers/chat-session-list.manager.ts +32 -0
  111. package/src/features/chat/managers/chat-thread.manager.ts +8 -2
  112. package/src/features/chat/stores/chat-session-list.store.ts +28 -5
  113. package/src/features/marketplace/components/mcp/mcp-marketplace-page.tsx +5 -12
  114. package/src/features/panel-apps/components/panel-app-list-item.tsx +6 -6
  115. package/src/features/panel-apps/index.ts +1 -0
  116. package/src/features/panel-apps/utils/panel-app-doc-browser.utils.tsx +2 -10
  117. package/src/features/panel-apps/utils/panel-app-iframe.utils.ts +6 -0
  118. package/src/features/pwa/managers/__tests__/pwa-shell-theme.manager.test.ts +11 -3
  119. package/src/features/pwa/managers/pwa-shell-theme.manager.ts +3 -1
  120. package/src/features/service-apps/components/service-apps-panel.tsx +145 -35
  121. package/src/features/settings/components/config/provider-auth-section.tsx +1 -1
  122. package/src/features/settings/components/config/provider-form.tsx +11 -8
  123. package/src/features/settings/components/config/provider-models-section.tsx +7 -7
  124. package/src/features/settings/components/config/provider-pill-selector.tsx +1 -1
  125. package/src/features/settings/components/config/secrets-config-form.tsx +5 -4
  126. package/src/features/settings/pages/model-config-page.tsx +26 -25
  127. package/src/features/settings/pages/providers-config-page.tsx +10 -13
  128. package/src/features/settings/pages/search-config-page.tsx +8 -5
  129. package/src/features/system-status/components/__tests__/desktop-update-config.test.tsx +97 -5
  130. package/src/features/system-status/components/config/runtime-config-editor.tsx +5 -4
  131. package/src/features/system-status/components/desktop-update-config.tsx +91 -2
  132. package/src/features/system-status/utils/update-release-notes.utils.ts +94 -0
  133. package/src/index.css +18 -0
  134. package/src/shared/components/config-split-page.tsx +6 -6
  135. package/src/shared/components/doc-browser/__tests__/doc-browser-url.utils.test.ts +73 -0
  136. package/src/shared/components/doc-browser/__tests__/doc-browser.test.tsx +48 -0
  137. package/src/shared/components/doc-browser/doc-browser-context.tsx +4 -0
  138. package/src/shared/components/doc-browser/doc-browser-panel-parts.tsx +5 -1
  139. package/src/shared/components/doc-browser/doc-browser-renderer.types.ts +2 -1
  140. package/src/shared/components/doc-browser/doc-browser-tab-strip.tsx +10 -10
  141. package/src/shared/components/doc-browser/doc-browser.tsx +6 -4
  142. package/src/shared/components/doc-browser/utils/doc-browser-url.utils.ts +62 -4
  143. package/src/shared/components/resizable-right-panel/__tests__/resizable-right-panel.test.tsx +18 -0
  144. package/src/shared/components/resizable-right-panel/resizable-right-panel.tsx +28 -3
  145. package/src/shared/components/ui/actions/__tests__/icon-action-button.test.tsx +36 -0
  146. package/src/shared/components/ui/actions/form-actions.tsx +25 -0
  147. package/src/shared/components/ui/actions/icon-action-button.tsx +25 -2
  148. package/src/shared/components/ui/card.tsx +12 -4
  149. package/src/shared/components/ui/tab-strip/compact-tab-strip.tsx +2 -1
  150. package/src/shared/hooks/use-server-path-browse.ts +26 -4
  151. package/src/shared/hooks/use-server-path-read.ts +11 -1
  152. package/src/shared/lib/api/ncp-session.types.ts +1 -1
  153. package/src/shared/lib/api/types.ts +12 -3
  154. package/src/shared/lib/api/utils/server-path.utils.ts +1 -0
  155. package/src/shared/lib/i18n/locales/en-US/agents.json +41 -0
  156. package/src/shared/lib/i18n/locales/en-US/chat.json +19 -0
  157. package/src/shared/lib/i18n/locales/en-US/core.json +3 -0
  158. package/src/shared/lib/i18n/locales/en-US/desktop-update.json +8 -0
  159. package/src/shared/lib/i18n/locales/en-US/doc-browser.json +5 -0
  160. package/src/shared/lib/i18n/locales/zh-CN/agents.json +41 -0
  161. package/src/shared/lib/i18n/locales/zh-CN/chat.json +19 -0
  162. package/src/shared/lib/i18n/locales/zh-CN/core.json +3 -0
  163. package/src/shared/lib/i18n/locales/zh-CN/desktop-update.json +8 -0
  164. package/src/shared/lib/i18n/locales/zh-CN/doc-browser.json +5 -0
  165. package/src/shared/lib/session-project/workspace-file-breadcrumb.utils.ts +9 -5
  166. package/src/shared/lib/theme/index.test.ts +21 -0
  167. package/src/shared/lib/theme/index.ts +6 -4
  168. package/src/vite-env.d.ts +2 -0
  169. package/dist/assets/channels-list-page-Cv8o9Hxg.js +0 -8
  170. package/dist/assets/chat-page-BtA7H1Ea.js +0 -1
  171. package/dist/assets/config-split-page-EidRBpP2.js +0 -1
  172. package/dist/assets/desktop-update-config-C6n_TB1r.js +0 -1
  173. package/dist/assets/doc-browser-B76XOXmW.js +0 -1
  174. package/dist/assets/doc-browser-DBnj9UMB.js +0 -1
  175. package/dist/assets/doc-browser-context-Sv-044Cl.js +0 -1
  176. package/dist/assets/ellipsis-2uzhypw5.js +0 -1
  177. package/dist/assets/index-CBRP-7-a.css +0 -1
  178. package/dist/assets/index-Z1fYpbNd.js +0 -104
  179. package/dist/assets/loader-circle-BjmN5DeJ.js +0 -1
  180. package/dist/assets/mcp-marketplace-page-CoZIiD6G.js +0 -1
  181. package/dist/assets/model-config-page-Cdr9P6TQ.js +0 -1
  182. package/dist/assets/plus-3ztRZD_l.js +0 -1
  183. package/dist/assets/providers-config-page-CWlO7J4A.js +0 -1
  184. package/dist/assets/remote-CKWZfVVB.js +0 -1
  185. package/dist/assets/rotate-cw-CHcaRUDz.js +0 -1
  186. package/dist/assets/runtime-config-page-CfTqh1nv.js +0 -1
  187. package/dist/assets/search-config-page-Dm8daYny.js +0 -1
  188. package/dist/assets/secrets-config-page-D8AbcR05.js +0 -3
  189. package/dist/assets/security-config-Ds0LsJmo.js +0 -1
  190. package/dist/assets/x-DqL1mYLC.js +0 -1
  191. /package/dist/assets/{config-hints-fGnUjDe9.js → config-hints-C_iP1zl_.js} +0 -0
@@ -7,6 +7,9 @@ export type ChatSessionListSnapshot = {
7
7
  selectedAgentId: string;
8
8
  query: string;
9
9
  listMode: ChatSessionListMode;
10
+ pinnedSessionKeys: string[];
11
+ pinnedProjectRoots: string[];
12
+ collapsedProjectRoots: string[];
10
13
  };
11
14
 
12
15
  const CHAT_SESSION_LIST_MODE_STORAGE_KEY = 'nextclaw.chat.session-list.mode';
@@ -15,6 +18,9 @@ const CHAT_SESSION_LIST_DEFAULT_MODE: ChatSessionListMode = 'time-first';
15
18
  type PersistedChatSessionListStore = {
16
19
  snapshot?: {
17
20
  listMode?: unknown;
21
+ pinnedSessionKeys?: unknown;
22
+ pinnedProjectRoots?: unknown;
23
+ collapsedProjectRoots?: unknown;
18
24
  };
19
25
  };
20
26
 
@@ -22,6 +28,13 @@ function isChatSessionListMode(value: unknown): value is ChatSessionListMode {
22
28
  return value === 'time-first' || value === 'project-first';
23
29
  }
24
30
 
31
+ function normalizePersistedStringList(value: unknown): string[] {
32
+ if (!Array.isArray(value)) {
33
+ return [];
34
+ }
35
+ return [...new Set(value.filter((item): item is string => typeof item === 'string').map((item) => item.trim()).filter(Boolean))];
36
+ }
37
+
25
38
  function resolvePersistedChatSessionListMode(persistedState: unknown): ChatSessionListMode | null {
26
39
  if (!persistedState || typeof persistedState !== 'object') {
27
40
  return null;
@@ -75,7 +88,10 @@ const initialSnapshot: ChatSessionListSnapshot = {
75
88
  selectedSessionKey: null,
76
89
  selectedAgentId: 'main',
77
90
  query: '',
78
- listMode: CHAT_SESSION_LIST_DEFAULT_MODE
91
+ listMode: CHAT_SESSION_LIST_DEFAULT_MODE,
92
+ pinnedSessionKeys: [],
93
+ pinnedProjectRoots: [],
94
+ collapsedProjectRoots: [],
79
95
  };
80
96
 
81
97
  function createSetSnapshotAction(set: ChatSessionListStoreSet) {
@@ -123,19 +139,26 @@ export const useChatSessionListStore = create<ChatSessionListStore>()(
123
139
  storage: createJSONStorage(() => window.localStorage),
124
140
  partialize: (state): PersistedChatSessionListStore => ({
125
141
  snapshot: {
126
- listMode: state.snapshot.listMode
142
+ listMode: state.snapshot.listMode,
143
+ pinnedSessionKeys: state.snapshot.pinnedSessionKeys,
144
+ pinnedProjectRoots: state.snapshot.pinnedProjectRoots,
145
+ collapsedProjectRoots: state.snapshot.collapsedProjectRoots,
127
146
  }
128
147
  }),
129
148
  merge: (persistedState, currentState) => {
130
- const listMode = resolvePersistedChatSessionListMode(persistedState);
131
- if (!listMode) {
149
+ if (!persistedState || typeof persistedState !== 'object') {
132
150
  return currentState;
133
151
  }
152
+ const { snapshot } = persistedState as PersistedChatSessionListStore;
153
+ const listMode = resolvePersistedChatSessionListMode(persistedState);
134
154
  return {
135
155
  ...currentState,
136
156
  snapshot: {
137
157
  ...currentState.snapshot,
138
- listMode
158
+ ...(listMode ? { listMode } : {}),
159
+ pinnedSessionKeys: normalizePersistedStringList(snapshot?.pinnedSessionKeys),
160
+ pinnedProjectRoots: normalizePersistedStringList(snapshot?.pinnedProjectRoots),
161
+ collapsedProjectRoots: normalizePersistedStringList(snapshot?.collapsedProjectRoots),
139
162
  }
140
163
  };
141
164
  }
@@ -266,21 +266,14 @@ export function McpMarketplacePage() {
266
266
  return (
267
267
  <PageLayout className="flex h-full min-h-0 flex-col pb-0 px-0">
268
268
  <div className="flex flex-col gap-6 w-full max-w-[1400px] h-full min-h-0 mx-auto">
269
-
270
- {/* Modern App Store Hero for MCP */}
271
- <div className="relative overflow-hidden rounded-[32px] bg-gradient-to-br from-[#0F172A] via-[#1E1B4B] to-[#312E81] px-10 py-14 text-white shadow-xl isolate">
272
- <div className="absolute top-0 right-0 -m-32 opacity-30 pointer-events-none mix-blend-screen scale-150 transform-gpu">
273
- <div className="w-[500px] h-[500px] rounded-full bg-gradient-to-tl from-fuchsia-500/40 via-purple-500/30 to-indigo-500/20 blur-[80px]"></div>
274
- </div>
275
- <div className="absolute bottom-0 left-0 -m-32 opacity-20 pointer-events-none mix-blend-screen transform-gpu">
276
- <div className="w-[400px] h-[400px] rounded-full bg-gradient-to-tr from-blue-500/40 to-cyan-500/20 blur-[80px]"></div>
277
- </div>
278
-
269
+ <div className="relative isolate overflow-hidden rounded-[28px] border border-border bg-card px-10 py-12 text-card-foreground shadow-card">
270
+ <div className="absolute inset-y-0 left-0 w-1 bg-primary/80" />
271
+
279
272
  <div className="relative z-10 flex flex-col gap-3">
280
- <h1 className="text-[38px] font-extrabold tracking-[-0.02em] leading-tight drop-shadow-sm text-transparent bg-clip-text bg-gradient-to-r from-white via-white to-white/70">
273
+ <h1 className="text-[38px] font-extrabold leading-tight text-foreground">
281
274
  {t("marketplaceMcpPageTitle")}
282
275
  </h1>
283
- <p className="text-[17px] font-medium text-purple-100/70 max-w-2xl leading-relaxed tracking-wide">
276
+ <p className="max-w-2xl text-[17px] font-medium leading-relaxed text-muted-foreground">
284
277
  {t("marketplaceMcpPageDescription")}
285
278
  </p>
286
279
  </div>
@@ -38,11 +38,11 @@ export function PanelAppListItem({
38
38
  };
39
39
 
40
40
  return (
41
- <div className="group w-full min-w-0 rounded-lg border border-border bg-card px-2.5 py-2.5 transition-colors hover:border-primary/35 hover:bg-accent/60">
41
+ <div className="group w-full min-w-0 rounded-lg border border-border/60 bg-card px-2.5 py-2.5 transition-colors hover:bg-muted/40">
42
42
  <div className="flex min-w-0 items-start gap-2">
43
43
  <button type="button" onClick={onOpen} className="min-w-0 flex-1 text-left">
44
44
  <span className="flex min-w-0 items-center gap-2">
45
- <span className="flex h-7 w-7 shrink-0 items-center justify-center rounded-md bg-accent text-base text-accent-foreground">
45
+ <span className="flex h-7 w-7 shrink-0 items-center justify-center rounded-md bg-muted text-base text-muted-foreground">
46
46
  <PanelAppIcon icon={entry.icon} title={entry.title} />
47
47
  </span>
48
48
  <span className="block min-w-0 flex-1 truncate text-sm font-medium text-foreground">{entry.title}</span>
@@ -52,22 +52,22 @@ export function PanelAppListItem({
52
52
  ) : null}
53
53
  <span className="mt-0.5 block truncate text-[11px] leading-4 text-muted-foreground/70">{secondary}</span>
54
54
  </button>
55
- <div className="flex shrink-0 items-center gap-1">
55
+ <div className="flex shrink-0 items-center gap-0.5 opacity-70 transition-opacity group-hover:opacity-100">
56
56
  <button
57
57
  type="button"
58
58
  onClick={handleFavorite}
59
59
  disabled={favoritePending}
60
- className="rounded-md p-1.5 text-muted-foreground/70 transition-colors hover:bg-background hover:text-amber-500 disabled:opacity-50"
60
+ className="rounded-md p-1.5 text-muted-foreground/70 transition-colors hover:bg-muted hover:text-foreground disabled:opacity-50"
61
61
  title={favoriteLabel}
62
62
  aria-label={favoriteLabel}
63
63
  >
64
- <Star className={entry.favorite ? 'h-4 w-4 fill-amber-400 text-amber-500' : 'h-4 w-4'} />
64
+ <Star className={entry.favorite ? 'h-4 w-4 fill-current text-foreground' : 'h-4 w-4'} />
65
65
  </button>
66
66
  <Popover open={isMenuOpen} onOpenChange={setIsMenuOpen}>
67
67
  <PopoverTrigger asChild>
68
68
  <button
69
69
  type="button"
70
- className="rounded-md p-1.5 text-muted-foreground/70 transition-colors hover:bg-background hover:text-foreground"
70
+ className="rounded-md p-1.5 text-muted-foreground/70 transition-colors hover:bg-muted hover:text-foreground"
71
71
  aria-label={t('panelAppsMoreActions')}
72
72
  disabled={deletePending}
73
73
  >
@@ -2,6 +2,7 @@ export { PanelAppsList } from './components/panel-apps-list';
2
2
  export { usePanelApps } from './hooks/use-panel-apps';
3
3
  export { PanelAppBridgeManager } from './managers/panel-app-bridge.manager';
4
4
  export { findPanelAppEntryByDisplayId } from './utils/panel-app-entry-match.utils';
5
+ export { PANEL_APP_IFRAME_SANDBOX, focusPanelAppIframe } from './utils/panel-app-iframe.utils';
5
6
  export {
6
7
  openApps,
7
8
  PANEL_APPS_DOC_BROWSER_RENDERERS,
@@ -14,20 +14,11 @@ import {
14
14
  RightPanelResourceHomePage,
15
15
  } from '@/features/right-panel-resources';
16
16
  import { t } from '@/shared/lib/i18n';
17
+ import { PANEL_APP_IFRAME_SANDBOX, focusPanelAppIframe } from './panel-app-iframe.utils';
17
18
 
18
19
  export const APPS_TAB_KIND = RIGHT_PANEL_APPS_TAB_KIND;
19
20
  export const PANEL_APP_TAB_KIND = RIGHT_PANEL_PANEL_APP_TAB_KIND;
20
21
  const DEFAULT_APPS_PANEL_TAB: AppsPanelTab = 'panel-apps';
21
- const PANEL_APP_IFRAME_SANDBOX = [
22
- 'allow-scripts',
23
- 'allow-forms',
24
- 'allow-modals',
25
- 'allow-popups',
26
- 'allow-popups-to-escape-sandbox',
27
- 'allow-downloads',
28
- 'allow-pointer-lock',
29
- 'allow-presentation',
30
- ].join(' ');
31
22
 
32
23
  function isAppsPanelTab(value: unknown): value is AppsPanelTab {
33
24
  return value === 'panel-apps' || value === 'service-apps';
@@ -79,6 +70,7 @@ export const PANEL_APPS_DOC_BROWSER_RENDERERS: DocBrowserCustomTabRenderers = {
79
70
  getIframeSandbox: () => PANEL_APP_IFRAME_SANDBOX,
80
71
  getTitle: (tab) => tab.title || t('panelAppsTitle'),
81
72
  onIframeMessage: (params) => getPresenter().panelAppBridgeManager.handleIframeMessage(params),
73
+ onIframePointerOver: (event) => focusPanelAppIframe(event.currentTarget),
82
74
  renderIcon: () => <AppWindow className="w-4 h-4 text-primary shrink-0" />,
83
75
  renderToolbar: ({ open, refreshIframe, tab }) => (
84
76
  <PanelAppToolbar
@@ -0,0 +1,6 @@
1
+ export const PANEL_APP_IFRAME_SANDBOX = 'allow-scripts allow-forms allow-modals allow-popups allow-popups-to-escape-sandbox allow-downloads allow-pointer-lock allow-presentation';
2
+
3
+ export function focusPanelAppIframe(iframe: HTMLIFrameElement | null): void {
4
+ iframe?.focus({ preventScroll: true });
5
+ iframe?.contentWindow?.focus();
6
+ }
@@ -12,12 +12,12 @@ describe('PwaShellThemeManager', () => {
12
12
  document.body.style.backgroundColor = '';
13
13
  });
14
14
 
15
- it('applies natural shell colors by default', () => {
15
+ it('applies default work shell colors when no theme is selected', () => {
16
16
  pwaShellThemeManager.syncCurrentTheme();
17
17
 
18
18
  const meta = document.querySelector('meta[name="theme-color"]');
19
- expect(meta?.getAttribute('content')).toBe('#FAF9F7');
20
- expect(document.body.style.backgroundColor).toBe('rgb(250, 249, 247)');
19
+ expect(meta?.getAttribute('content')).toBe('#FFFFFF');
20
+ expect(document.body.style.backgroundColor).toBe('rgb(255, 255, 255)');
21
21
  });
22
22
 
23
23
  it('applies natural shell colors explicitly', () => {
@@ -36,6 +36,14 @@ describe('PwaShellThemeManager', () => {
36
36
  expect(document.body.style.backgroundColor).toBe('rgb(255, 255, 255)');
37
37
  });
38
38
 
39
+ it('applies work shell colors explicitly', () => {
40
+ pwaShellThemeManager.syncTheme('work');
41
+
42
+ const meta = document.querySelector('meta[name="theme-color"]');
43
+ expect(meta?.getAttribute('content')).toBe('#FFFFFF');
44
+ expect(document.body.style.backgroundColor).toBe('rgb(255, 255, 255)');
45
+ });
46
+
39
47
  it('applies warm shell colors', () => {
40
48
  pwaShellThemeManager.syncTheme('warm');
41
49
 
@@ -1,4 +1,5 @@
1
1
  import {
2
+ DEFAULT_UI_THEME,
2
3
  getThemeAppearance,
3
4
  normalizeTheme,
4
5
  type UiTheme,
@@ -6,6 +7,7 @@ import {
6
7
 
7
8
  const PWA_SHELL_THEME_COLORS: Record<UiTheme, string> = {
8
9
  natural: '#FAF9F7',
10
+ work: '#FFFFFF',
9
11
  minimal: '#FFFFFF',
10
12
  warm: '#FAF8F4',
11
13
  cool: '#F8FAFC',
@@ -33,7 +35,7 @@ export class PwaShellThemeManager {
33
35
  }
34
36
 
35
37
  const currentTheme = document.documentElement.getAttribute('data-theme');
36
- this.syncTheme(normalizeTheme(currentTheme) ?? 'natural');
38
+ this.syncTheme(normalizeTheme(currentTheme) ?? DEFAULT_UI_THEME);
37
39
  };
38
40
 
39
41
  private updateThemeMeta = (themeColor: string) => {
@@ -7,6 +7,7 @@ import type {
7
7
  import {
8
8
  AlertTriangle,
9
9
  CheckCircle2,
10
+ ChevronDown,
10
11
  CircleDashed,
11
12
  LoaderCircle,
12
13
  MoreVertical,
@@ -53,6 +54,7 @@ export function ServiceAppsPanel({
53
54
  const [discoveredActionsByApp, setDiscoveredActionsByApp] = useState<
54
55
  Record<string, ServiceActionView[]>
55
56
  >({});
57
+ const [expandedActionsByApp, setExpandedActionsByApp] = useState<Record<string, boolean>>({});
56
58
 
57
59
  const refetch = () => {
58
60
  void serviceApps.refetch();
@@ -86,12 +88,16 @@ export function ServiceAppsPanel({
86
88
  ...current,
87
89
  [appId]: result.actions,
88
90
  }));
91
+ setExpandedActionsByApp((current) => ({
92
+ ...current,
93
+ [appId]: true,
94
+ }));
89
95
  });
90
96
  };
91
97
 
92
98
  return (
93
99
  <div className="flex h-full min-h-0 flex-col bg-card text-card-foreground">
94
- <div className="flex items-center justify-between gap-2 border-b border-border/70 px-4 py-3">
100
+ <div className="flex items-center justify-between gap-2 border-b border-border/60 px-4 py-3">
95
101
  <div className="flex min-w-0 items-center gap-2">
96
102
  {headerContent ?? (
97
103
  <>
@@ -110,20 +116,31 @@ export function ServiceAppsPanel({
110
116
  </div>
111
117
 
112
118
  {apps.length === 0 ? (
113
- <div className="flex flex-1 items-center justify-center px-6 text-center text-sm text-muted-foreground">
114
- {t('serviceAppsEmpty')}
119
+ <div className="flex flex-1 items-center justify-center px-6 py-8 text-center">
120
+ <div className="w-full max-w-xs">
121
+ <div className="mx-auto flex h-10 w-10 items-center justify-center rounded-xl bg-muted text-muted-foreground">
122
+ <Server className="h-5 w-5" />
123
+ </div>
124
+ <h2 className="mt-3 text-sm font-semibold text-foreground">{t('serviceAppsEmptyTitle')}</h2>
125
+ <p className="mt-1 text-xs leading-5 text-muted-foreground">{t('serviceAppsEmptyDescription')}</p>
126
+ </div>
115
127
  </div>
116
128
  ) : (
117
- <div className="custom-scrollbar min-h-0 flex-1 overflow-y-auto p-3">
118
- <div className="space-y-3">
129
+ <div className="custom-scrollbar min-h-0 flex-1 overflow-y-auto bg-muted/25 p-3">
130
+ <div className="space-y-2.5">
119
131
  {apps.map((app) => (
120
132
  <ServiceAppCard
121
133
  key={app.id}
122
134
  app={app}
123
135
  actions={discoveredActionsByApp[app.id] ?? actions.filter((action) => action.appId === app.id)}
136
+ actionsOpen={Boolean(expandedActionsByApp[app.id])}
124
137
  grants={grants}
125
138
  deletePending={deleteServiceApp.isPending}
126
139
  isDiscovering={discoverServiceAppActions.isPending}
140
+ onActionsOpenChange={(open) => setExpandedActionsByApp((current) => ({
141
+ ...current,
142
+ [app.id]: open,
143
+ }))}
127
144
  onDiscover={discover}
128
145
  onDelete={(appId) => void deleteServiceApp.mutate(appId)}
129
146
  onRestart={(appId) => void restartServiceApp.mutate(appId)}
@@ -143,9 +160,11 @@ export function ServiceAppsPanel({
143
160
  function ServiceAppCard({
144
161
  app,
145
162
  actions,
163
+ actionsOpen,
146
164
  grants,
147
165
  deletePending,
148
166
  isDiscovering,
167
+ onActionsOpenChange,
149
168
  onDiscover,
150
169
  onDelete,
151
170
  onRestart,
@@ -153,9 +172,11 @@ function ServiceAppCard({
153
172
  }: {
154
173
  app: ServiceAppRecordView;
155
174
  actions: ServiceActionView[];
175
+ actionsOpen: boolean;
156
176
  grants: ServiceActionGrantView[];
157
177
  deletePending: boolean;
158
178
  isDiscovering: boolean;
179
+ onActionsOpenChange: (open: boolean) => void;
159
180
  onDiscover: (appId: string) => void;
160
181
  onDelete: (appId: string) => void;
161
182
  onRestart: (appId: string) => void;
@@ -172,21 +193,21 @@ function ServiceAppCard({
172
193
 
173
194
  return (
174
195
  <TooltipProvider delayDuration={250}>
175
- <section className="rounded-lg border border-border bg-card p-3">
176
- <div className="flex items-start justify-between gap-3">
177
- <div className="min-w-0">
196
+ <section className="overflow-hidden rounded-xl border border-border/70 bg-card shadow-[0_1px_2px_rgba(30,20,10,0.03)]">
197
+ <div className="flex items-start gap-3 px-3 pt-3">
198
+ <div className="flex h-9 w-9 shrink-0 items-center justify-center rounded-lg bg-primary/10 text-primary">
199
+ <Server className="h-4 w-4" />
200
+ </div>
201
+ <div className="min-w-0 flex-1">
178
202
  <div className="flex items-center gap-2">
179
203
  <div className="truncate text-sm font-semibold text-foreground">{app.title}</div>
180
204
  <ServiceAppStatusBadge status={app.status} />
181
205
  </div>
182
206
  {app.description ? (
183
- <div className="mt-1 line-clamp-2 text-xs text-muted-foreground">{app.description}</div>
184
- ) : null}
185
- {app.lastError ? (
186
- <div className="mt-2 rounded bg-rose-50 px-2 py-1 text-xs text-rose-700">{app.lastError}</div>
207
+ <div className="mt-1 line-clamp-2 text-xs leading-5 text-muted-foreground">{app.description}</div>
187
208
  ) : null}
188
209
  </div>
189
- <div className="flex shrink-0 items-center gap-1">
210
+ <div className="flex shrink-0 items-center gap-0.5">
190
211
  <ServiceAppIconButton
191
212
  disabled={isDiscovering || !canConnectAndDiscover}
192
213
  icon={Radar}
@@ -207,7 +228,7 @@ function ServiceAppCard({
207
228
  <PopoverTrigger asChild>
208
229
  <button
209
230
  type="button"
210
- className="rounded-md p-1.5 text-muted-foreground transition-colors hover:bg-accent hover:text-accent-foreground disabled:opacity-50"
231
+ className="rounded-md p-1.5 text-muted-foreground transition-colors hover:bg-muted hover:text-foreground disabled:opacity-50"
211
232
  aria-label={t('serviceAppsMoreActions')}
212
233
  disabled={deletePending}
213
234
  >
@@ -229,17 +250,85 @@ function ServiceAppCard({
229
250
  </Popover>
230
251
  </div>
231
252
  </div>
253
+
254
+ {app.lastError ? (
255
+ <div className="mx-3 mt-3 rounded-lg border border-rose-200/70 bg-rose-50 px-2.5 py-1.5 text-xs text-rose-700">
256
+ {app.lastError}
257
+ </div>
258
+ ) : null}
259
+
232
260
  <ServiceAppDiagnostics app={app} />
233
- <div className="mt-3 space-y-1">
234
- {actions.map((action) => (
235
- <ServiceActionRow
236
- key={action.id}
237
- action={action}
238
- grants={grants.filter((grant) => grant.actionId === action.id)}
239
- onRevoke={onRevoke}
261
+
262
+ <div className="mt-3 border-t border-border/50 bg-muted/30">
263
+ <button
264
+ type="button"
265
+ onClick={() => onActionsOpenChange(!actionsOpen)}
266
+ className="flex w-full items-start justify-between gap-2 px-3 py-2 text-left transition-colors hover:bg-muted/40"
267
+ aria-expanded={actionsOpen}
268
+ >
269
+ <div className="min-w-0 flex-1">
270
+ <div className="flex items-center gap-2">
271
+ <span className="text-[11px] font-medium uppercase tracking-wide text-muted-foreground/80">
272
+ {t('serviceAppsActionsSection')}
273
+ </span>
274
+ <span className="rounded-full bg-card px-1.5 py-0.5 text-[11px] tabular-nums text-muted-foreground/70">
275
+ {actions.length}
276
+ </span>
277
+ </div>
278
+ {!actionsOpen ? (
279
+ actions.length === 0 ? (
280
+ <div className="mt-1 text-[11px] text-muted-foreground">
281
+ {t('serviceAppsActionsEmpty')}
282
+ </div>
283
+ ) : (
284
+ <div className="mt-1.5 flex flex-wrap gap-1">
285
+ {actions.slice(0, 4).map((action) => (
286
+ <span
287
+ key={action.id}
288
+ className="max-w-[9.5rem] truncate rounded-md border border-border/50 bg-card px-1.5 py-0.5 text-[11px] text-muted-foreground"
289
+ title={action.title ?? action.name}
290
+ >
291
+ {action.title ?? action.name}
292
+ </span>
293
+ ))}
294
+ {actions.length > 4 ? (
295
+ <span className="rounded-md bg-card px-1.5 py-0.5 text-[11px] text-muted-foreground/70">
296
+ +{actions.length - 4}
297
+ </span>
298
+ ) : null}
299
+ </div>
300
+ )
301
+ ) : null}
302
+ </div>
303
+ <ChevronDown
304
+ className={cn(
305
+ 'mt-0.5 h-3.5 w-3.5 shrink-0 text-muted-foreground/70 transition-transform',
306
+ actionsOpen && 'rotate-180',
307
+ )}
240
308
  />
241
- ))}
309
+ </button>
310
+ {actionsOpen ? (
311
+ <div className="px-3 pb-2.5">
312
+ {actions.length === 0 ? (
313
+ <div className="rounded-lg border border-dashed border-border/70 bg-card/70 px-2.5 py-2 text-[11px] text-muted-foreground">
314
+ {t('serviceAppsActionsEmpty')}
315
+ </div>
316
+ ) : (
317
+ <div className="space-y-1.5">
318
+ {actions.map((action) => (
319
+ <ServiceActionRow
320
+ key={action.id}
321
+ action={action}
322
+ grants={grants.filter((grant) => grant.actionId === action.id)}
323
+ onRevoke={onRevoke}
324
+ />
325
+ ))}
326
+ </div>
327
+ )}
328
+ </div>
329
+ ) : null}
242
330
  </div>
331
+
243
332
  <ConfirmDialog
244
333
  open={isDeleteDialogOpen}
245
334
  onOpenChange={setIsDeleteDialogOpen}
@@ -263,7 +352,7 @@ function ServiceAppStatusBadge({ status }: { status: ServiceAppStatus }) {
263
352
  <TooltipTrigger asChild>
264
353
  <span
265
354
  className={cn(
266
- 'inline-flex shrink-0 items-center gap-1 rounded px-1.5 py-0.5 text-[11px] font-medium',
355
+ 'inline-flex shrink-0 items-center gap-1 rounded-full px-1.5 py-0.5 text-[11px] font-medium',
267
356
  view.className,
268
357
  )}
269
358
  aria-label={view.label}
@@ -345,7 +434,7 @@ function ServiceAppIconButton({
345
434
  type="button"
346
435
  onClick={onClick}
347
436
  disabled={disabled}
348
- className="rounded-md p-1.5 text-muted-foreground transition-colors hover:bg-accent hover:text-accent-foreground disabled:cursor-not-allowed disabled:text-muted-foreground/40 disabled:opacity-60 disabled:hover:bg-transparent"
437
+ className="rounded-md p-1.5 text-muted-foreground transition-colors hover:bg-muted hover:text-foreground disabled:cursor-not-allowed disabled:text-muted-foreground/40 disabled:opacity-60 disabled:hover:bg-transparent"
349
438
  aria-label={label}
350
439
  >
351
440
  <Icon className="h-3.5 w-3.5" />
@@ -386,6 +475,7 @@ function ServiceAppMenuItem({
386
475
  }
387
476
 
388
477
  function ServiceAppDiagnostics({ app }: { app: ServiceAppRecordView }) {
478
+ const [open, setOpen] = useState(false);
389
479
  const command = [app.command, ...(app.args ?? [])]
390
480
  .filter((value): value is string => Boolean(value?.trim()))
391
481
  .join(' ');
@@ -403,11 +493,29 @@ function ServiceAppDiagnostics({ app }: { app: ServiceAppRecordView }) {
403
493
  }
404
494
 
405
495
  return (
406
- <dl className="mt-3 grid grid-cols-[4.5rem_minmax(0,1fr)] gap-x-2 gap-y-1 border-t border-border/70 pt-2 text-[11px]">
407
- {rows.map((row) => (
408
- <ServiceAppDiagnosticRow key={row.label} label={row.label} value={row.value} />
409
- ))}
410
- </dl>
496
+ <div className="mx-3 mt-3 overflow-hidden rounded-lg border border-border/50 bg-muted/35">
497
+ <button
498
+ type="button"
499
+ onClick={() => setOpen((value) => !value)}
500
+ className="flex w-full items-center justify-between gap-2 px-2.5 py-1.5 text-left transition-colors hover:bg-muted/50"
501
+ aria-expanded={open}
502
+ >
503
+ <span className="text-[11px] font-medium text-muted-foreground">{t('serviceAppsDetails')}</span>
504
+ <ChevronDown
505
+ className={cn(
506
+ 'h-3.5 w-3.5 shrink-0 text-muted-foreground/70 transition-transform',
507
+ open && 'rotate-180',
508
+ )}
509
+ />
510
+ </button>
511
+ {open ? (
512
+ <dl className="grid grid-cols-[4.75rem_minmax(0,1fr)] gap-x-2 gap-y-1 border-t border-border/50 px-2.5 py-2 text-[11px]">
513
+ {rows.map((row) => (
514
+ <ServiceAppDiagnosticRow key={row.label} label={row.label} value={row.value} />
515
+ ))}
516
+ </dl>
517
+ ) : null}
518
+ </div>
411
519
  );
412
520
  }
413
521
 
@@ -438,10 +546,12 @@ function ServiceActionRow({
438
546
  onRevoke: (grant: ServiceActionGrantView) => void;
439
547
  }) {
440
548
  return (
441
- <div className="rounded bg-muted/60 px-2 py-1.5">
549
+ <div className="rounded-lg border border-border/50 bg-card px-2.5 py-2">
442
550
  <div className="flex items-center justify-between gap-2">
443
551
  <div className="flex min-w-0 items-center gap-2">
444
- <Wrench className="h-3.5 w-3.5 shrink-0 text-muted-foreground/70" />
552
+ <span className="flex h-6 w-6 shrink-0 items-center justify-center rounded-md bg-muted text-muted-foreground">
553
+ <Wrench className="h-3.5 w-3.5" />
554
+ </span>
445
555
  <div className="min-w-0">
446
556
  <div className="truncate text-xs font-medium text-foreground">{action.title ?? action.name}</div>
447
557
  {action.description ? (
@@ -451,15 +561,15 @@ function ServiceActionRow({
451
561
  </div>
452
562
  <div className="flex shrink-0 items-center gap-1">
453
563
  {action.runtimeState ? (
454
- <span className="rounded bg-card px-1.5 py-0.5 text-[11px] text-muted-foreground">
564
+ <span className="rounded-full bg-muted px-1.5 py-0.5 text-[11px] text-muted-foreground">
455
565
  {t(`serviceAppsRuntimeState_${action.runtimeState}`)}
456
566
  </span>
457
567
  ) : null}
458
- <span className="rounded bg-card px-1.5 py-0.5 text-[11px] text-muted-foreground">{action.risk}</span>
568
+ <span className="rounded-full bg-muted px-1.5 py-0.5 text-[11px] text-muted-foreground">{action.risk}</span>
459
569
  </div>
460
570
  </div>
461
571
  {grants.map((grant) => (
462
- <div key={`${grant.caller.surface}:${grant.caller.appId}:${grant.actionId}`} className="mt-1 flex items-center justify-between gap-2 pl-5">
572
+ <div key={`${grant.caller.surface}:${grant.caller.appId}:${grant.actionId}`} className="mt-1.5 flex items-center justify-between gap-2 pl-8">
463
573
  <div className="flex min-w-0 items-center gap-1.5 text-[11px] text-muted-foreground">
464
574
  <ShieldCheck className="h-3 w-3 shrink-0 text-emerald-500" />
465
575
  <span className="truncate">{t('serviceAppsGrantedTo')} {grant.caller.appId}</span>
@@ -467,7 +577,7 @@ function ServiceActionRow({
467
577
  <button
468
578
  type="button"
469
579
  onClick={() => onRevoke(grant)}
470
- className="rounded-md p-1 text-muted-foreground/70 transition-colors hover:bg-background hover:text-rose-600"
580
+ className="rounded-md p-1 text-muted-foreground/70 transition-colors hover:bg-muted hover:text-rose-600"
471
581
  title={t('serviceAppsRevokeGrant')}
472
582
  aria-label={t('serviceAppsRevokeGrant')}
473
583
  >
@@ -28,7 +28,7 @@ export function ProviderAuthSection(props: ProviderAuthSectionProps) {
28
28
  if (providerAuth?.kind !== 'device_code') return null;
29
29
 
30
30
  return (
31
- <div className="space-y-2 rounded-xl border border-primary/20 bg-primary-50/50 p-3">
31
+ <div className="space-y-2 rounded-xl border border-border/60 bg-muted/35 p-3">
32
32
  <Label className="text-sm font-medium text-foreground">
33
33
  {providerAuth.displayName || t('providerAuthSectionTitle')}
34
34
  </Label>
@@ -13,6 +13,7 @@ import { MaskedInput } from '@/shared/components/common/masked-input';
13
13
  import { Button } from '@/shared/components/ui/button';
14
14
  import { Input } from '@/shared/components/ui/input';
15
15
  import { Label } from '@/shared/components/ui/label';
16
+ import { FormActions } from '@/shared/components/ui/actions/form-actions';
16
17
  import { getLanguage, t } from '@/shared/lib/i18n';
17
18
  import type { ThinkingLevel } from '@/shared/lib/api';
18
19
  import {
@@ -548,14 +549,16 @@ function ProviderFormDetailPane(props: ProviderFormDetailPaneProps) {
548
549
  />
549
550
  </ConfigSplitPaneBody>
550
551
 
551
- <ConfigSplitPaneFooter className='flex items-center justify-between px-6 py-4'>
552
- <Button type='button' variant='outline' size='sm' onClick={onTestConnection} disabled={isTestPending}>
553
- <CircleDotDashed className='mr-1.5 h-4 w-4' />
554
- {isTestPending ? t('providerTestingConnection') : t('providerTestConnection')}
555
- </Button>
556
- <Button type='submit' disabled={isUpdatePending || !hasChanges}>
557
- {isUpdatePending ? t('saving') : hasChanges ? t('save') : t('unchanged')}
558
- </Button>
552
+ <ConfigSplitPaneFooter>
553
+ <FormActions align="between">
554
+ <Button type='button' variant='outline' size='sm' onClick={onTestConnection} disabled={isTestPending}>
555
+ <CircleDotDashed className='mr-1.5 h-3.5 w-3.5' />
556
+ {isTestPending ? t('providerTestingConnection') : t('providerTestConnection')}
557
+ </Button>
558
+ <Button type='submit' size='sm' disabled={isUpdatePending || !hasChanges}>
559
+ {isUpdatePending ? t('saving') : hasChanges ? t('save') : t('unchanged')}
560
+ </Button>
561
+ </FormActions>
559
562
  </ConfigSplitPaneFooter>
560
563
  </form>
561
564
  </ConfigSplitDetailPane>