@nextclaw/ui 0.12.24 → 0.12.26

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 (210) hide show
  1. package/CHANGELOG.md +136 -29
  2. package/dist/assets/api-DGD9_Bg4.js +15 -0
  3. package/dist/assets/app-manager-provider-oYdeYPSv.js +1 -0
  4. package/dist/assets/{book-open-DDlN5MvX.js → book-open-BcnAiKde.js} +1 -1
  5. package/dist/assets/channels-list-page-HgLgrEg4.js +8 -0
  6. package/dist/assets/chat-page-DAKMFDrS.js +1 -0
  7. package/dist/assets/config-split-page-CcrEUtwu.js +1 -0
  8. package/dist/assets/cpu-DPPwMzoC.js +3 -0
  9. package/dist/assets/{createLucideIcon-BLMK3QUd.js → createLucideIcon-DzY6wN61.js} +1 -1
  10. package/dist/assets/desktop-DVUbOWbR.js +3 -0
  11. package/dist/assets/desktop-update-config-CP8dFYXK.js +1 -0
  12. package/dist/assets/{dialog-C3D7Be0p.js → dialog-BKo0RItd.js} +1 -1
  13. package/dist/assets/{dist-CPlbUgwU.js → dist-CFiwgaLs.js} +1 -1
  14. package/dist/assets/doc-browser-CAhfnm0D.js +1 -0
  15. package/dist/assets/{doc-browser-context-BJuMaI3o.js → doc-browser-context-FukQHvyo.js} +1 -1
  16. package/dist/assets/doc-browser-p9DDNPWB.js +1 -0
  17. package/dist/assets/doc-browser-rZIQIjuw.js +1 -0
  18. package/dist/assets/download-CMM8po31.js +1 -0
  19. package/dist/assets/{es2015-xqN1slyW.js → es2015-BhznEEyJ.js} +1 -1
  20. package/dist/assets/{external-link-DwfSfTLB.js → external-link-CpEvG65F.js} +1 -1
  21. package/dist/assets/i18n-D1144VAA.js +1 -0
  22. package/dist/assets/index-Cuwst6cc.js +100 -0
  23. package/dist/assets/index-dlcqieQ0.css +1 -0
  24. package/dist/assets/{key-round-CJ5gDAAG.js → key-round-DUq47t0P.js} +1 -1
  25. package/dist/assets/marketplace-page-BeFbwxR-.js +105 -0
  26. package/dist/assets/marketplace-page-CR4xq-TM.js +1 -0
  27. package/dist/assets/mcp-marketplace-page-DlRrSCj3.js +1 -0
  28. package/dist/assets/mcp-marketplace-page-DwnaLNTx.js +40 -0
  29. package/dist/assets/model-config-L2l6YAlQ.js +1 -0
  30. package/dist/assets/{notice-card-BFDbKQDA.js → notice-card-Dr6xCwva.js} +1 -1
  31. package/dist/assets/play-AqrNslHI.js +1 -0
  32. package/dist/assets/plus-B-YHtTNC.js +1 -0
  33. package/dist/assets/{popover-B86Dbfhf.js → popover-BDFNiLlg.js} +1 -1
  34. package/dist/assets/provider-scoped-model-input-BMTp4BEH.js +1 -0
  35. package/dist/assets/providers-list-DYAEunOp.js +1 -0
  36. package/dist/assets/refresh-cw-CrbD8EkT.js +1 -0
  37. package/dist/assets/remote-Dr3jcfWP.js +1 -0
  38. package/dist/assets/{rotate-cw-BZ2JObNs.js → rotate-cw-BN9yjccP.js} +1 -1
  39. package/dist/assets/runtime-config-page-BdeU8PEK.js +1 -0
  40. package/dist/assets/{save-euRxl8pI.js → save-CO_4qf6b.js} +1 -1
  41. package/dist/assets/{search-CLd7m0M7.js → search-CRtQwr-h.js} +1 -1
  42. package/dist/assets/search-config-CQUhd5RU.js +1 -0
  43. package/dist/assets/secrets-config-D-NWlW9q.js +3 -0
  44. package/dist/assets/{select-CJ0wbo3D.js → select-BUTwE_lC.js} +1 -1
  45. package/dist/assets/{setting-row-D1Yygqp7.js → setting-row-BavcnXw1.js} +1 -1
  46. package/dist/assets/settings-MWL2SMyk.js +1 -0
  47. package/dist/assets/{sparkles-DVfeSVJQ.js → sparkles-BmgOD4nY.js} +1 -1
  48. package/dist/assets/{status-dot-ChvPCib9.js → status-dot-l3kPFdq_.js} +1 -1
  49. package/dist/assets/{tabs-custom-Hia_ong0.js → tabs-custom-D48zdZoc.js} +1 -1
  50. package/dist/assets/{tag-chip-FrkmkT8r.js → tag-chip-Dm2Lqnpu.js} +1 -1
  51. package/dist/assets/use-config-Cyv5IuSt.js +1 -0
  52. package/dist/assets/use-infinite-scroll-loader-CFVdPpNv.js +1 -0
  53. package/dist/assets/x-BeyYA_h6.js +1 -0
  54. package/dist/index.html +29 -40
  55. package/package.json +9 -9
  56. package/src/app/components/layout/sidebar.layout.test.tsx +2 -4
  57. package/src/app/components/theme-provider.tsx +1 -0
  58. package/src/app/configs/app-navigation.config.ts +0 -6
  59. package/src/app/index.tsx +4 -7
  60. package/src/features/agents/components/agents-page.test.tsx +25 -15
  61. package/src/features/agents/components/agents-page.tsx +133 -172
  62. package/src/features/channels/components/config/channel-form.test.tsx +1 -0
  63. package/src/features/channels/components/config/channel-form.tsx +4 -3
  64. package/src/features/channels/components/config/weixin-channel-auth-section.test.tsx +38 -1
  65. package/src/features/channels/components/config/weixin-channel-auth-section.tsx +137 -40
  66. package/src/features/channels/index.ts +1 -1
  67. package/src/features/channels/utils/channel-form-fields.utils.test.ts +26 -0
  68. package/src/features/channels/utils/channel-form-fields.utils.ts +32 -18
  69. package/src/features/chat/components/chat-session-workspace-panel-nav.tsx +23 -4
  70. package/src/features/chat/components/chat-session-workspace-panel.tsx +53 -35
  71. package/src/features/chat/components/chat-sidebar-session-item.tsx +16 -12
  72. package/src/features/chat/components/conversation/chat-conversation-header.test.tsx +74 -0
  73. package/src/features/chat/components/conversation/chat-conversation-header.tsx +8 -2
  74. package/src/features/chat/components/conversation/chat-conversation-panel.test.tsx +262 -114
  75. package/src/features/chat/components/conversation/chat-conversation-panel.tsx +210 -174
  76. package/src/features/chat/components/conversation/chat-input-bar.container.tsx +11 -1
  77. package/src/features/chat/components/conversation/session-header/chat-session-header-actions.test.tsx +24 -0
  78. package/src/features/chat/components/conversation/session-header/chat-session-header-actions.tsx +27 -6
  79. package/src/features/chat/components/layout/chat-sidebar-utility-menu.tsx +174 -0
  80. package/src/features/chat/components/layout/chat-sidebar.test.tsx +45 -8
  81. package/src/features/chat/components/layout/chat-sidebar.tsx +29 -46
  82. package/src/features/chat/components/providers/chat-presenter.provider.tsx +4 -0
  83. package/src/features/chat/components/workspace/session-cron-job-content.tsx +103 -0
  84. package/src/features/chat/hooks/use-ncp-agent-runtime.test.tsx +153 -80
  85. package/src/features/chat/hooks/use-ncp-chat-page-data.test.tsx +70 -0
  86. package/src/features/chat/hooks/use-ncp-chat-page-data.ts +1 -1
  87. package/src/features/chat/hooks/use-ncp-child-session-tabs-view.ts +2 -8
  88. package/src/features/chat/hooks/use-ncp-session-list-view.ts +1 -2
  89. package/src/features/chat/managers/chat-session-list.manager.test.ts +7 -9
  90. package/src/features/chat/managers/chat-session-list.manager.ts +5 -10
  91. package/src/features/chat/managers/ncp-chat-input.manager.test.ts +20 -2
  92. package/src/features/chat/managers/ncp-chat-input.manager.ts +18 -0
  93. package/src/features/chat/managers/ncp-chat-presenter.manager.ts +7 -0
  94. package/src/features/chat/managers/ncp-chat-thread.manager.test.ts +52 -1
  95. package/src/features/chat/managers/ncp-chat-thread.manager.ts +21 -0
  96. package/src/features/chat/pages/ncp-chat-page.tsx +9 -5
  97. package/src/features/chat/stores/chat-input.store.ts +3 -1
  98. package/src/features/chat/stores/chat-session-list.store.ts +0 -2
  99. package/src/features/chat/stores/chat-thread.store.ts +4 -0
  100. package/src/features/chat/utils/chat-session-display.utils.test.ts +83 -1
  101. package/src/features/chat/utils/chat-session-display.utils.ts +73 -0
  102. package/src/features/chat/utils/ncp-chat-input-availability.utils.test.ts +1 -0
  103. package/src/features/chat/utils/ncp-session-adapter.utils.test.ts +22 -0
  104. package/src/features/chat/utils/ncp-session-adapter.utils.ts +32 -0
  105. package/src/features/marketplace/components/curated-shelves/marketplace-curated-scene-route.test.tsx +235 -0
  106. package/src/features/marketplace/components/curated-shelves/marketplace-curated-shelves.config.ts +162 -0
  107. package/src/features/marketplace/components/curated-shelves/marketplace-curated-shelves.tsx +355 -0
  108. package/src/features/marketplace/components/curated-shelves/marketplace-shelf-card.tsx +118 -0
  109. package/src/features/marketplace/components/detail-doc/marketplace-detail-doc-renderer.ts +201 -0
  110. package/src/features/marketplace/components/detail-doc/marketplace-detail-doc.test.ts +40 -0
  111. package/src/features/marketplace/components/marketplace-catalog-grid.tsx +114 -0
  112. package/src/features/marketplace/components/marketplace-detail-doc.ts +73 -24
  113. package/src/features/marketplace/components/marketplace-item-icon.tsx +45 -0
  114. package/src/features/marketplace/components/marketplace-list-card.tsx +177 -93
  115. package/src/features/marketplace/components/marketplace-page-detail.test.tsx +9 -2
  116. package/src/features/marketplace/components/marketplace-page-parts.tsx +1 -1
  117. package/src/features/marketplace/components/marketplace-page.test.tsx +25 -6
  118. package/src/features/marketplace/components/marketplace-page.tsx +154 -132
  119. package/src/features/marketplace/hooks/use-marketplace-curated-scene-route.ts +97 -0
  120. package/src/features/marketplace/hooks/use-marketplace.ts +59 -3
  121. package/src/features/system-status/components/config/runtime-agent-list-card.tsx +4 -8
  122. package/src/features/system-status/components/config/runtime-binding-list-card.tsx +5 -7
  123. package/src/features/system-status/components/config/runtime-config-editor.tsx +1 -19
  124. package/src/features/system-status/components/config/runtime-entry-list-card.tsx +10 -11
  125. package/src/features/system-status/components/config/runtime-settings-card.tsx +15 -23
  126. package/src/features/system-status/components/runtime-control-card.test.tsx +8 -6
  127. package/src/features/system-status/components/runtime-control-card.tsx +7 -6
  128. package/src/features/system-status/pages/runtime-config-page.test.tsx +19 -9
  129. package/src/features/system-status/pages/runtime-config-page.tsx +2 -3
  130. package/src/features/system-status/utils/runtime-config-agent.utils.ts +4 -4
  131. package/src/features/system-status/utils/system-status.utils.ts +31 -6
  132. package/src/index.css +8 -0
  133. package/src/platforms/desktop/components/desktop-app-shell.test.tsx +68 -0
  134. package/src/platforms/desktop/components/desktop-app-shell.tsx +46 -18
  135. package/src/platforms/desktop/components/desktop-window-chrome.tsx +30 -0
  136. package/src/platforms/desktop/index.ts +6 -0
  137. package/src/platforms/desktop/types/desktop-update.types.ts +3 -0
  138. package/src/platforms/desktop/utils/desktop-host.utils.ts +56 -0
  139. package/src/shared/components/common/brand-header.tsx +36 -16
  140. package/src/shared/components/config/provider-form-support.ts +2 -22
  141. package/src/shared/components/cron-config.tsx +12 -58
  142. package/src/shared/components/doc-browser/doc-browser.tsx +4 -4
  143. package/src/shared/components/ui/select.tsx +19 -7
  144. package/src/shared/lib/api/channel-auth.types.ts +1 -0
  145. package/src/shared/lib/api/ncp-session.types.ts +9 -0
  146. package/src/shared/lib/api/types.ts +12 -1
  147. package/src/shared/lib/api/utils/marketplace.utils.ts +7 -1
  148. package/src/shared/lib/cron/cron-job-view.utils.ts +59 -0
  149. package/src/shared/lib/cron/index.ts +1 -0
  150. package/src/shared/lib/i18n/{channel-auth.ts → channel-auth.constants.ts} +31 -0
  151. package/src/shared/lib/i18n/chat-labels.utils.ts +3 -2
  152. package/src/shared/lib/i18n/index.ts +20 -59
  153. package/src/shared/lib/i18n/{runtime-control.ts → runtime-control-labels.utils.ts} +30 -1
  154. package/src/shared/lib/provider-models/index.test.ts +39 -0
  155. package/src/shared/lib/provider-models/index.ts +1 -3
  156. package/src/shared/lib/ui-document-title/index.ts +0 -1
  157. package/tsconfig.json +1 -0
  158. package/vite.config.ts +1 -1
  159. package/vitest.config.ts +1 -1
  160. package/dist/assets/api-D2xRKmZd.js +0 -15
  161. package/dist/assets/app-manager-provider-CNaZboG4.js +0 -1
  162. package/dist/assets/app-navigation.config-Ihhrrt--.js +0 -1
  163. package/dist/assets/channels-list-page-p26lgxLk.js +0 -8
  164. package/dist/assets/chat-Dkh2qtuz.js +0 -61
  165. package/dist/assets/chat-page-DoTmE2wx.js +0 -1
  166. package/dist/assets/chunk-JZWAC4HX-Kydj4yEz.js +0 -3
  167. package/dist/assets/config-split-page-DIOCjj2Q.js +0 -1
  168. package/dist/assets/desktop-update-config-DlpzDfKM.js +0 -1
  169. package/dist/assets/doc-browser-C8FM5fC0.js +0 -1
  170. package/dist/assets/doc-browser-RJUOL_GO.js +0 -1
  171. package/dist/assets/doc-browser-p82AdNO-.js +0 -1
  172. package/dist/assets/folder-CeJKPx5P.js +0 -1
  173. package/dist/assets/hash-BqxRTZW5.js +0 -1
  174. package/dist/assets/i18n-DnTGDIRw.js +0 -1
  175. package/dist/assets/index-D8MKmXtO.css +0 -1
  176. package/dist/assets/index-pBvbJ5Mt.js +0 -2
  177. package/dist/assets/loader-circle-fd-vQKtW.js +0 -1
  178. package/dist/assets/logo-badge-KAe-7d8c.js +0 -1
  179. package/dist/assets/logos-C4sYP1Vl.js +0 -1
  180. package/dist/assets/marketplace-page-Cql0kDi-.js +0 -1
  181. package/dist/assets/marketplace-page-m4P5g_Ht.js +0 -49
  182. package/dist/assets/mcp-marketplace-page-9WVKl1m1.js +0 -1
  183. package/dist/assets/mcp-marketplace-page-ByzBQZcx.js +0 -40
  184. package/dist/assets/message-square-z_osm9c0.js +0 -1
  185. package/dist/assets/model-config-Dbr_0APb.js +0 -1
  186. package/dist/assets/play-Dv6Nr1Ew.js +0 -1
  187. package/dist/assets/plus-D8eKFY7h.js +0 -1
  188. package/dist/assets/provider-scoped-model-input-DFm6N2f7.js +0 -1
  189. package/dist/assets/providers-list-BJcLOjun.js +0 -1
  190. package/dist/assets/refresh-ccw-ByVwmnN_.js +0 -1
  191. package/dist/assets/refresh-cw-PcqoYB3K.js +0 -1
  192. package/dist/assets/remote-BOxo9iwd.js +0 -1
  193. package/dist/assets/runtime-config-page-CjLhnbSl.js +0 -1
  194. package/dist/assets/search-config-J4Htco-P.js +0 -1
  195. package/dist/assets/secrets-config-CUdERjco.js +0 -3
  196. package/dist/assets/sessions-config-page-DpK991fs.js +0 -2
  197. package/dist/assets/settings-drbWqzA4.js +0 -1
  198. package/dist/assets/skeleton-BK1SOSRA.js +0 -1
  199. package/dist/assets/theme-provider-0hxjiPc_.js +0 -2
  200. package/dist/assets/tooltip-Cj4yA0gH.js +0 -1
  201. package/dist/assets/trash-2-CBsHCfqq.js +0 -1
  202. package/dist/assets/use-config-38Ur-89i.js +0 -1
  203. package/dist/assets/use-confirm-dialog-DPQThaeU.js +0 -1
  204. package/dist/assets/use-infinite-scroll-loader-5Gf1xQi7.js +0 -1
  205. package/dist/assets/use-viewport-layout-D1XzKeip.js +0 -1
  206. package/dist/assets/x-CM-XDMpk.js +0 -1
  207. package/src/features/chat/components/config/sessions-config-detail-pane.tsx +0 -244
  208. package/src/features/chat/pages/sessions-config-page.test.tsx +0 -152
  209. package/src/features/chat/pages/sessions-config-page.tsx +0 -192
  210. /package/dist/assets/{config-hints-MogHYQ8G.js → config-hints-BNfpOL4J.js} +0 -0
@@ -13,6 +13,21 @@ import type {
13
13
  SystemStatusView,
14
14
  } from '@/features/system-status/types/system-status.types';
15
15
 
16
+ const RUNTIME_CONTROL_MESSAGE_LABELS: Record<string, string> = {
17
+ 'Use this page to manage the local NextClaw service. Closing the browser does not stop the service.': 'runtimeControlManagedLocalMessage',
18
+ 'This page is served by the running local service. Closing the browser does not stop it.': 'runtimeControlManagedLocalHint',
19
+ 'This page is served by the running local service.': 'runtimeControlManagedLocalHintShort',
20
+ 'This page is already hosted by the running local service.': 'runtimeControlStartUnavailableHosted',
21
+ 'App restart is only available in the desktop shell.': 'runtimeControlRestartAppDesktopOnly',
22
+ 'The local service is not running.': 'runtimeControlServiceNotRunning',
23
+ 'The local service is already stopped.': 'runtimeControlServiceAlreadyStopped',
24
+ 'Managed service started.': 'runtimeControlManagedServiceStarted',
25
+ 'Managed service start scheduled.': 'runtimeControlManagedServiceStartScheduled',
26
+ 'Restart scheduled. This page may disconnect for a few seconds.': 'runtimeControlRestartScheduled',
27
+ 'Stop scheduled. This page will disconnect shortly.': 'runtimeControlStopScheduled',
28
+ 'runtime healthy': 'runtimeControlHealthy'
29
+ };
30
+
16
31
  function resolveSystemStatusPhase(state: SystemStatusState): SystemStatusPhase {
17
32
  return state.activeSystemAction ? 'service-transitioning' : state.lifecyclePhase;
18
33
  }
@@ -70,6 +85,14 @@ function resolveActionServiceState(
70
85
  return null;
71
86
  }
72
87
 
88
+ export function localizeRuntimeControlMessage(message: string | null | undefined): string | null {
89
+ if (!message) {
90
+ return null;
91
+ }
92
+ const labelKey = RUNTIME_CONTROL_MESSAGE_LABELS[message.trim()];
93
+ return labelKey ? t(labelKey) : message;
94
+ }
95
+
73
96
  export function buildActiveSystemActionState(params: {
74
97
  action: RuntimeControlAction;
75
98
  message: string | null;
@@ -113,8 +136,8 @@ export function toRuntimeStatusBadgeView(
113
136
  tone: 'attention',
114
137
  title: t('runtimeControlTitle'),
115
138
  description:
116
- state.activeSystemAction.message ||
117
- state.runtimeControlView.message ||
139
+ localizeRuntimeControlMessage(state.activeSystemAction.message) ||
140
+ localizeRuntimeControlMessage(state.runtimeControlView.message) ||
118
141
  t('runtimeControlDescription'),
119
142
  reasonLines: [],
120
143
  actionLabel: null,
@@ -161,9 +184,9 @@ export function toRuntimeControlPanelView(
161
184
  const visibleServiceState =
162
185
  action?.serviceState ?? controlView?.serviceState ?? 'unknown';
163
186
  const visibleMessage =
164
- action?.message ||
165
- state.lastSystemActionError ||
166
- controlView?.message ||
187
+ localizeRuntimeControlMessage(action?.message) ||
188
+ localizeRuntimeControlMessage(state.lastSystemActionError) ||
189
+ localizeRuntimeControlMessage(controlView?.message) ||
167
190
  t('runtimeControlDescription');
168
191
 
169
192
  return {
@@ -175,6 +198,8 @@ export function toRuntimeControlPanelView(
175
198
  busy: Boolean(action),
176
199
  pendingRestart: controlView?.pendingRestart ?? null,
177
200
  errorMessage:
178
- state.lastSystemActionError || state.runtimeControlError || null,
201
+ localizeRuntimeControlMessage(state.lastSystemActionError) ||
202
+ localizeRuntimeControlMessage(state.runtimeControlError) ||
203
+ null,
179
204
  };
180
205
  }
package/src/index.css CHANGED
@@ -38,6 +38,14 @@
38
38
 
39
39
  @layer utilities {
40
40
 
41
+ .desktop-window-drag {
42
+ -webkit-app-region: drag;
43
+ }
44
+
45
+ .desktop-window-no-drag {
46
+ -webkit-app-region: no-drag;
47
+ }
48
+
41
49
  /* ========================================
42
50
  SCROLLBAR
43
51
  ======================================== */
@@ -0,0 +1,68 @@
1
+ import { render, screen } from "@testing-library/react";
2
+ import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
3
+ import { afterEach, describe, expect, it, vi } from "vitest";
4
+ import { DesktopAppShell } from "@/platforms/desktop/components/desktop-app-shell";
5
+
6
+ vi.mock("@/app/components/layout/sidebar", () => ({
7
+ Sidebar: () => <aside data-testid="settings-sidebar">Settings Sidebar</aside>,
8
+ }));
9
+
10
+ function setDesktopPlatform(platform: string | null): void {
11
+ window.nextclawDesktop = platform
12
+ ? ({
13
+ platform,
14
+ } as typeof window.nextclawDesktop)
15
+ : undefined;
16
+ }
17
+
18
+ function renderDesktopShell(platform: string | null) {
19
+ setDesktopPlatform(platform);
20
+ const queryClient = new QueryClient({
21
+ defaultOptions: {
22
+ queries: {
23
+ retry: false,
24
+ },
25
+ },
26
+ });
27
+
28
+ return render(
29
+ <QueryClientProvider client={queryClient}>
30
+ <DesktopAppShell
31
+ pathname="/chat"
32
+ isDocBrowserOpen={false}
33
+ docBrowserMode="floating"
34
+ >
35
+ <div data-testid="app-content">App Content</div>
36
+ </DesktopAppShell>
37
+ </QueryClientProvider>,
38
+ );
39
+ }
40
+
41
+ describe("DesktopAppShell", () => {
42
+ afterEach(() => {
43
+ setDesktopPlatform(null);
44
+ });
45
+
46
+ it("renders the reserved Windows chrome row above app content", () => {
47
+ renderDesktopShell("win32");
48
+
49
+ const chrome = screen.getByTestId("desktop-window-chrome");
50
+ const sidebarChrome = screen.getByTestId("desktop-window-chrome-sidebar");
51
+ const mainChrome = screen.getByTestId("desktop-window-chrome-main");
52
+
53
+ expect(chrome).toBeTruthy();
54
+ expect(chrome.parentElement?.style.getPropertyValue("--desktop-titlebar-height")).toBe("40px");
55
+ expect(sidebarChrome.className).toContain("w-[var(--desktop-sidebar-width)]");
56
+ expect(sidebarChrome.className).not.toContain("border-b");
57
+ expect(mainChrome.className).toContain("border-b");
58
+ expect(mainChrome.className).toContain("pr-[var(--desktop-caption-safe-right)]");
59
+ expect(screen.getByTestId("app-content")).toBeTruthy();
60
+ });
61
+
62
+ it("keeps non-Windows desktop hosts on the existing shell shape", () => {
63
+ renderDesktopShell("darwin");
64
+
65
+ expect(screen.queryByTestId("desktop-window-chrome")).toBeNull();
66
+ expect(screen.getByTestId("app-content")).toBeTruthy();
67
+ });
68
+ });
@@ -1,6 +1,10 @@
1
1
  import { lazy, Suspense } from "react";
2
+ import type { CSSProperties } from "react";
2
3
  import { isMainWorkspaceRoute } from "@/app/configs/app-navigation.config";
3
4
  import { Sidebar } from "@/app/components/layout/sidebar";
5
+ import { DesktopWindowChrome } from "@/platforms/desktop/components/desktop-window-chrome";
6
+ import { isWindowsDesktopHost } from "@/platforms/desktop/utils/desktop-host.utils";
7
+ import { cn } from "@/shared/lib/utils";
4
8
 
5
9
  const DocBrowser = lazy(async () => ({
6
10
  default: (await import("@/shared/components/doc-browser/doc-browser")).DocBrowser,
@@ -13,6 +17,20 @@ type DesktopAppShellProps = {
13
17
  children: React.ReactNode;
14
18
  };
15
19
 
20
+ type DesktopShellStyle = CSSProperties & {
21
+ "--desktop-titlebar-height"?: string;
22
+ "--desktop-caption-safe-right"?: string;
23
+ "--desktop-sidebar-width"?: string;
24
+ };
25
+
26
+ function createWindowsDesktopShellStyle(isMainRoute: boolean): DesktopShellStyle {
27
+ return {
28
+ "--desktop-titlebar-height": "40px",
29
+ "--desktop-caption-safe-right": "140px",
30
+ "--desktop-sidebar-width": isMainRoute ? "280px" : "240px",
31
+ };
32
+ }
33
+
16
34
  export function DesktopAppShell({
17
35
  pathname,
18
36
  isDocBrowserOpen,
@@ -20,27 +38,37 @@ export function DesktopAppShell({
20
38
  children,
21
39
  }: DesktopAppShellProps) {
22
40
  const isMainRoute = isMainWorkspaceRoute(pathname);
41
+ const shouldUseWindowsChrome = isWindowsDesktopHost();
23
42
 
24
43
  return (
25
- <div className="h-screen flex bg-background font-sans text-foreground">
26
- {!isMainRoute && <Sidebar mode="settings" />}
27
- <div className="flex-1 flex min-w-0 overflow-hidden relative">
28
- <div className="flex-1 flex flex-col min-w-0 overflow-hidden">
29
- {isMainRoute ? (
30
- <div className="flex-1 h-full overflow-hidden">{children}</div>
31
- ) : (
32
- <main className="flex-1 overflow-auto p-8 pb-16 custom-scrollbar">
33
- <div className="mx-auto h-full max-w-6xl animate-fade-in">
34
- {children}
35
- </div>
36
- </main>
37
- )}
44
+ <div
45
+ className={cn(
46
+ "h-screen flex flex-col overflow-hidden bg-background font-sans text-foreground",
47
+ shouldUseWindowsChrome ? "rounded-[10px]" : null,
48
+ )}
49
+ style={shouldUseWindowsChrome ? createWindowsDesktopShellStyle(isMainRoute) : undefined}
50
+ >
51
+ {shouldUseWindowsChrome ? <DesktopWindowChrome /> : null}
52
+ <div className="flex min-h-0 flex-1 overflow-hidden">
53
+ {!isMainRoute && <Sidebar mode="settings" />}
54
+ <div className="flex-1 flex min-w-0 overflow-hidden relative">
55
+ <div className="flex-1 flex flex-col min-w-0 overflow-hidden">
56
+ {isMainRoute ? (
57
+ <div className="flex-1 h-full overflow-hidden">{children}</div>
58
+ ) : (
59
+ <main className="flex-1 overflow-auto p-8 pb-16 custom-scrollbar">
60
+ <div className="mx-auto h-full max-w-6xl animate-fade-in">
61
+ {children}
62
+ </div>
63
+ </main>
64
+ )}
65
+ </div>
66
+ {isDocBrowserOpen && docBrowserMode === "docked" ? (
67
+ <Suspense fallback={null}>
68
+ <DocBrowser />
69
+ </Suspense>
70
+ ) : null}
38
71
  </div>
39
- {isDocBrowserOpen && docBrowserMode === "docked" ? (
40
- <Suspense fallback={null}>
41
- <DocBrowser />
42
- </Suspense>
43
- ) : null}
44
72
  </div>
45
73
  {isDocBrowserOpen && docBrowserMode === "floating" ? (
46
74
  <Suspense fallback={null}>
@@ -0,0 +1,30 @@
1
+ import { BrandHeader } from "@/shared/components/common/brand-header";
2
+
3
+ export function DesktopWindowChrome() {
4
+ return (
5
+ <header
6
+ className="flex h-[var(--desktop-titlebar-height)] shrink-0 bg-background"
7
+ data-testid="desktop-window-chrome"
8
+ >
9
+ <div
10
+ className="desktop-window-drag flex h-full w-[var(--desktop-sidebar-width)] shrink-0 items-center bg-secondary pl-4 pr-3 text-secondary-foreground"
11
+ data-testid="desktop-window-chrome-sidebar"
12
+ >
13
+ <div className="desktop-window-no-drag flex min-w-0 shrink-0 items-center">
14
+ <BrandHeader
15
+ className="flex min-w-0 items-center gap-2.5"
16
+ density="chrome"
17
+ />
18
+ </div>
19
+ <div className="min-w-0 flex-1 self-stretch" aria-hidden="true" />
20
+ </div>
21
+ <div
22
+ className="desktop-window-drag flex min-w-0 flex-1 items-center border-b border-[#ebe7dc]/80 bg-secondary pr-[var(--desktop-caption-safe-right)]"
23
+ aria-hidden="true"
24
+ data-testid="desktop-window-chrome-main"
25
+ >
26
+ <div className="min-w-0 flex-1 self-stretch" />
27
+ </div>
28
+ </header>
29
+ );
30
+ }
@@ -7,8 +7,14 @@ export {
7
7
  DesktopUpdateManager,
8
8
  } from './managers/desktop-update.manager';
9
9
  export { DesktopAppShell } from './components/desktop-app-shell';
10
+ export { DesktopWindowChrome } from './components/desktop-window-chrome';
10
11
  export { useDesktopPresenceStore } from './stores/desktop-presence.store';
11
12
  export { useDesktopUpdateStore } from './stores/desktop-update.store';
13
+ export {
14
+ getDesktopHostPlatform,
15
+ isMacDesktopHost,
16
+ isWindowsDesktopHost,
17
+ } from './utils/desktop-host.utils';
12
18
  export type {
13
19
  DesktopPresencePreferences,
14
20
  DesktopPresenceSnapshot,
@@ -48,6 +48,8 @@ export type DesktopPresenceSnapshot = DesktopPresencePreferences & {
48
48
 
49
49
  export type DesktopUiLanguagePreference = 'en' | 'zh';
50
50
 
51
+ export type DesktopShellTheme = 'warm' | 'cool';
52
+
51
53
  export type NextClawDesktopBridge = {
52
54
  platform: string;
53
55
  version: string;
@@ -63,5 +65,6 @@ export type NextClawDesktopBridge = {
63
65
  getPresenceState: () => Promise<DesktopPresenceSnapshot>;
64
66
  updatePresencePreferences: (preferences: Partial<DesktopPresencePreferences>) => Promise<DesktopPresenceSnapshot>;
65
67
  setLocalePreference?: (language: DesktopUiLanguagePreference | null) => Promise<DesktopUiLanguagePreference | null>;
68
+ setShellTheme?: (theme: DesktopShellTheme) => Promise<void>;
66
69
  onUpdateStateChanged: (listener: (snapshot: DesktopUpdateSnapshot) => void) => () => void;
67
70
  };
@@ -0,0 +1,56 @@
1
+ export type DesktopHostPlatform = string | null;
2
+
3
+ const devPlatformOverrideParam = "nextclawDesktopPlatform";
4
+ const devPlatformOverrideStorageKey = "nextclaw.desktopPlatformOverride";
5
+
6
+ export function getDesktopHostPlatform(): DesktopHostPlatform {
7
+ if (typeof window === "undefined") {
8
+ return null;
9
+ }
10
+ return getDevDesktopHostPlatformOverride() ?? window.nextclawDesktop?.platform ?? null;
11
+ }
12
+
13
+ export function isMacDesktopHost(): boolean {
14
+ return getDesktopHostPlatform() === "darwin";
15
+ }
16
+
17
+ export function isWindowsDesktopHost(): boolean {
18
+ return getDesktopHostPlatform() === "win32";
19
+ }
20
+
21
+ function getDevDesktopHostPlatformOverride(): DesktopHostPlatform {
22
+ if (!import.meta.env.DEV) {
23
+ return null;
24
+ }
25
+ const platform = new URLSearchParams(window.location.search).get(devPlatformOverrideParam);
26
+ if (platform === "win32" || platform === "darwin") {
27
+ writeDevDesktopHostPlatformOverride(platform);
28
+ return platform;
29
+ }
30
+ if (platform === "clear") {
31
+ writeDevDesktopHostPlatformOverride(null);
32
+ return null;
33
+ }
34
+ return readDevDesktopHostPlatformOverride();
35
+ }
36
+
37
+ function readDevDesktopHostPlatformOverride(): DesktopHostPlatform {
38
+ try {
39
+ const platform = window.sessionStorage.getItem(devPlatformOverrideStorageKey);
40
+ return platform === "win32" || platform === "darwin" ? platform : null;
41
+ } catch {
42
+ return null;
43
+ }
44
+ }
45
+
46
+ function writeDevDesktopHostPlatformOverride(platform: DesktopHostPlatform): void {
47
+ try {
48
+ if (platform) {
49
+ window.sessionStorage.setItem(devPlatformOverrideStorageKey, platform);
50
+ return;
51
+ }
52
+ window.sessionStorage.removeItem(devPlatformOverrideStorageKey);
53
+ } catch {
54
+ // Storage can be unavailable in restricted browser contexts; URL-only override still works.
55
+ }
56
+ }
@@ -4,28 +4,44 @@ import { useAppMeta } from '@/shared/hooks/use-config';
4
4
  import { type ReactNode, useState } from 'react';
5
5
  import { RuntimeStatusEntry } from '@/app/components/layout/runtime-status-entry';
6
6
  import { t } from '@/shared/lib/i18n';
7
+ import { cn } from '@/shared/lib/utils';
7
8
 
8
9
  type BrandHeaderProps = {
9
10
  className?: string;
11
+ density?: 'sidebar' | 'chrome';
10
12
  suffix?: ReactNode;
11
13
  };
12
14
 
13
- export function BrandHeader({ className, suffix }: BrandHeaderProps) {
15
+ export function BrandHeader({ className, density = 'sidebar', suffix }: BrandHeaderProps) {
14
16
  const { data } = useAppMeta();
15
17
  const productName = data?.name ?? 'NextClaw';
16
18
  const productVersion = data?.productVersion?.trim();
17
19
  const versionLabel = productVersion ? `v${productVersion}` : null;
18
20
  const resolvedSuffix = suffix ?? <RuntimeStatusEntry />;
21
+ const shouldReserveMacWindowControls = typeof window !== 'undefined' && window.nextclawDesktop?.platform === 'darwin';
22
+ const isChromeDensity = density === 'chrome';
19
23
 
20
24
  return (
21
- <div className={className ?? 'flex items-center gap-2.5'}>
22
- <div className="h-7 w-7 rounded-lg overflow-hidden flex items-center justify-center">
25
+ <div className={cn(className ?? 'flex min-w-0 items-center gap-2', shouldReserveMacWindowControls && 'pl-[58px]')}>
26
+ <div
27
+ className={cn(
28
+ 'flex shrink-0 items-center justify-center overflow-hidden',
29
+ isChromeDensity ? 'h-6 w-6 rounded-md' : 'h-6 w-6 rounded-md',
30
+ )}
31
+ >
23
32
  <img src="/logo.svg" alt={productName} className="h-full w-full object-contain" />
24
33
  </div>
25
- <div className="flex min-w-0 items-baseline gap-2">
26
- <div className="flex min-w-0 flex-1 items-baseline gap-2">
27
- <span className="shrink-0 text-[15px] font-semibold tracking-[-0.01em] text-gray-800">{productName}</span>
28
- {versionLabel ? <BrandVersionLabel versionLabel={versionLabel} /> : null}
34
+ <div className="flex min-w-0 items-center gap-2">
35
+ <div className="flex min-w-0 flex-1 items-baseline gap-1.5">
36
+ <span
37
+ className={cn(
38
+ 'shrink-0 font-semibold text-gray-800',
39
+ isChromeDensity ? 'text-[15px]' : 'text-[14px]',
40
+ )}
41
+ >
42
+ {productName}
43
+ </span>
44
+ {versionLabel ? <BrandVersionLabel versionLabel={versionLabel} density={density} /> : null}
29
45
  </div>
30
46
  <RuntimeUpdateInlineStatus />
31
47
  {resolvedSuffix ? <span className="inline-flex items-center shrink-0">{resolvedSuffix}</span> : null}
@@ -34,8 +50,15 @@ export function BrandHeader({ className, suffix }: BrandHeaderProps) {
34
50
  );
35
51
  }
36
52
 
37
- function BrandVersionLabel({ versionLabel }: { versionLabel: string }) {
53
+ function BrandVersionLabel({
54
+ versionLabel,
55
+ density,
56
+ }: {
57
+ versionLabel: string;
58
+ density: BrandHeaderProps['density'];
59
+ }) {
38
60
  const [isTooltipOpen, setIsTooltipOpen] = useState(false);
61
+ const isChromeDensity = density === 'chrome';
39
62
 
40
63
  return (
41
64
  <span
@@ -48,7 +71,10 @@ function BrandVersionLabel({ versionLabel }: { versionLabel: string }) {
48
71
  <span
49
72
  tabIndex={0}
50
73
  aria-label={versionLabel}
51
- className="block min-w-0 truncate text-[13px] font-medium text-gray-500 outline-none"
74
+ className={cn(
75
+ 'block min-w-0 truncate font-medium text-gray-500 outline-none',
76
+ isChromeDensity ? 'text-[12px]' : 'text-[12px]',
77
+ )}
52
78
  >
53
79
  {versionLabel}
54
80
  </span>
@@ -100,13 +126,7 @@ function RuntimeUpdateInlineBadge({ snapshot }: { snapshot: UpdateSnapshot }) {
100
126
  if (snapshot.status === 'blocked' || snapshot.status === 'failed') {
101
127
  return <RuntimeUpdateIssueIcon snapshot={snapshot} />;
102
128
  }
103
- const label = snapshot.status === 'downloading'
104
- ? resolveInlineDownloadLabel(snapshot)
105
- : snapshot.status === 'downloaded'
106
- ? t('desktopUpdatesInlineReady')
107
- : snapshot.status === 'update-available'
108
- ? t('desktopUpdatesInlineDownload')
109
- : null;
129
+ const label = snapshot.status === 'downloading' ? resolveInlineDownloadLabel(snapshot) : null;
110
130
  if (!label) {
111
131
  return null;
112
132
  }
@@ -104,31 +104,11 @@ function modelListsEqual(left: string[], right: string[]): boolean {
104
104
  return left.every((item, index) => item === right[index]);
105
105
  }
106
106
 
107
- function mergeModelLists(base: string[], extra: string[]): string[] {
108
- const merged = [...base];
109
- for (const item of extra) {
110
- if (!merged.includes(item)) {
111
- merged.push(item);
112
- }
113
- }
114
- return merged;
115
- }
116
-
117
- function resolveEditableModels(defaultModels: string[], savedModels: string[]): string[] {
118
- if (savedModels.length === 0) {
119
- return defaultModels;
120
- }
121
- const looksLikeLegacyCustomList = savedModels.every((model) => !defaultModels.includes(model));
122
- if (looksLikeLegacyCustomList) {
123
- return mergeModelLists(defaultModels, savedModels);
124
- }
107
+ function resolveEditableModels(_defaultModels: string[], savedModels: string[]): string[] {
125
108
  return savedModels;
126
109
  }
127
110
 
128
- function serializeModelsForSave(models: string[], defaultModels: string[]): string[] {
129
- if (modelListsEqual(models, defaultModels)) {
130
- return [];
131
- }
111
+ function serializeModelsForSave(models: string[], _defaultModels: string[]): string[] {
132
112
  return models;
133
113
  }
134
114
 
@@ -6,65 +6,19 @@ import { Button } from '@/shared/components/ui/button';
6
6
  import { Input } from '@/shared/components/ui/input';
7
7
  import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/shared/components/ui/select';
8
8
  import { Card, CardContent } from '@/shared/components/ui/card';
9
+ import {
10
+ describeCronDelivery,
11
+ describeCronSchedule,
12
+ describeCronSession,
13
+ formatCronDate,
14
+ } from '@/shared/lib/cron';
9
15
  import { cn } from '@/shared/lib/utils';
10
- import { formatDateTime, t } from '@/shared/lib/i18n';
16
+ import { t } from '@/shared/lib/i18n';
11
17
  import { PageLayout, PageHeader } from '@/app/components/layout/page-layout';
12
18
  import { AlarmClock, RefreshCw, Trash2, Play, Power } from 'lucide-react';
13
19
 
14
20
  type StatusFilter = 'all' | 'enabled' | 'disabled';
15
21
 
16
- function formatDate(value?: string | null): string {
17
- return formatDateTime(value ?? undefined);
18
- }
19
-
20
- function formatDateFromMs(value?: number | null): string {
21
- if (typeof value !== 'number' || !Number.isFinite(value)) {
22
- return '-';
23
- }
24
- return formatDateTime(new Date(value));
25
- }
26
-
27
- function formatEveryDuration(ms?: number | null): string {
28
- if (typeof ms !== 'number' || !Number.isFinite(ms)) {
29
- return '-';
30
- }
31
- const seconds = Math.round(ms / 1000);
32
- if (seconds < 60) return `${seconds}s`;
33
- const minutes = Math.round(seconds / 60);
34
- if (minutes < 60) return `${minutes}m`;
35
- const hours = Math.round(minutes / 60);
36
- if (hours < 24) return `${hours}h`;
37
- const days = Math.round(hours / 24);
38
- return `${days}d`;
39
- }
40
-
41
- function describeSchedule(job: CronJobView): string {
42
- const { schedule } = job;
43
- if (schedule.kind === 'cron') {
44
- return schedule.expr ? `cron ${schedule.expr}` : 'cron';
45
- }
46
- if (schedule.kind === 'every') {
47
- return `every ${formatEveryDuration(schedule.everyMs)}`;
48
- }
49
- if (schedule.kind === 'at') {
50
- return `at ${formatDateFromMs(schedule.atMs)}`;
51
- }
52
- return '-';
53
- }
54
-
55
- function describeDelivery(job: CronJobView): string {
56
- if (!job.payload.deliver) {
57
- return '-';
58
- }
59
- const channel = job.payload.channel ?? '-';
60
- const target = job.payload.to ?? '-';
61
- return `${channel}:${target}`;
62
- }
63
-
64
- function describeSession(job: CronJobView): string {
65
- return job.payload.sessionId?.trim() || `cron:${job.id}`;
66
- }
67
-
68
22
  function matchQuery(job: CronJobView, query: string): boolean {
69
23
  const q = query.trim().toLowerCase();
70
24
  if (!q) return true;
@@ -107,14 +61,14 @@ function CronJobCard(props: {
107
61
  <span className="rounded-full bg-amber-50 px-2 py-0.5 text-[10px] font-semibold text-amber-700">{t('cronOneShot')}</span>
108
62
  ) : null}
109
63
  </div>
110
- <div className="mt-2 text-xs text-gray-500">{t('cronScheduleLabel')}: {describeSchedule(job)}</div>
64
+ <div className="mt-2 text-xs text-gray-500">{t('cronScheduleLabel')}: {describeCronSchedule(job)}</div>
111
65
  <div className="mt-2 whitespace-pre-wrap break-words text-sm text-gray-700">{job.payload.message}</div>
112
- <div className="mt-2 text-xs text-gray-500">{t('cronSessionLabel')}: {describeSession(job)}</div>
113
- <div className="mt-2 text-xs text-gray-500">{t('cronDeliverTo')}: {describeDelivery(job)}</div>
66
+ <div className="mt-2 text-xs text-gray-500">{t('cronSessionLabel')}: {describeCronSession(job)}</div>
67
+ <div className="mt-2 text-xs text-gray-500">{t('cronDeliverTo')}: {describeCronDelivery(job)}</div>
114
68
  </div>
115
69
  <div className="min-w-[220px] space-y-2 text-xs text-gray-500">
116
- <div><span className="font-medium text-gray-700">{t('cronNextRun')}:</span> {formatDate(job.state.nextRunAt)}</div>
117
- <div><span className="font-medium text-gray-700">{t('cronLastRun')}:</span> {formatDate(job.state.lastRunAt)}</div>
70
+ <div><span className="font-medium text-gray-700">{t('cronNextRun')}:</span> {formatCronDate(job.state.nextRunAt)}</div>
71
+ <div><span className="font-medium text-gray-700">{t('cronLastRun')}:</span> {formatCronDate(job.state.lastRunAt)}</div>
118
72
  <div><span className="font-medium text-gray-700">{t('cronLastStatus')}:</span> {job.state.lastStatus ?? '-'}</div>
119
73
  {job.state.lastError ? <div className="break-words text-[11px] text-red-500">{job.state.lastError}</div> : null}
120
74
  </div>
@@ -320,7 +320,7 @@ export function DocBrowser({ displayMode = 'desktop' }: DocBrowserProps) {
320
320
  </div>
321
321
  </div>
322
322
 
323
- <div className="flex items-center gap-1.5 px-2.5 py-2 bg-white border-b border-gray-100 overflow-x-auto custom-scrollbar">
323
+ <div className="flex items-center gap-1.5 px-2.5 py-2 bg-background border-b border-[#f1e7d4] overflow-x-auto custom-scrollbar">
324
324
  {tabs.map((tab) => {
325
325
  const isActive = tab.id === activeTabId;
326
326
  return (
@@ -329,8 +329,8 @@ export function DocBrowser({ displayMode = 'desktop' }: DocBrowserProps) {
329
329
  className={cn(
330
330
  'inline-flex items-center gap-1 h-7 px-1.5 rounded-lg text-xs border max-w-[220px] shrink-0 transition-colors',
331
331
  isActive
332
- ? 'bg-blue-50 border-blue-300 text-blue-700'
333
- : 'bg-gray-50 border-gray-200 text-gray-600 hover:bg-gray-100'
332
+ ? 'bg-amber-50/80 border-amber-200 text-amber-900 shadow-[0_1px_2px_rgba(30,20,10,0.04)]'
333
+ : 'bg-[#f9f8f5] border-[#eee3d1] text-[#78644d] hover:bg-[#fff7ea] hover:text-[#2f2212]'
334
334
  )}
335
335
  >
336
336
  <button
@@ -357,7 +357,7 @@ export function DocBrowser({ displayMode = 'desktop' }: DocBrowserProps) {
357
357
  })}
358
358
  <button
359
359
  onClick={() => openNewTab(undefined, { kind: 'docs', title: 'Docs' })}
360
- className="inline-flex items-center justify-center w-7 h-7 rounded-lg border border-gray-200 bg-white text-gray-600 hover:bg-gray-100 shrink-0"
360
+ className="inline-flex items-center justify-center w-7 h-7 rounded-lg border border-[#eee3d1] bg-white text-[#78644d] hover:bg-[#fff7ea] hover:text-[#2f2212] shrink-0"
361
361
  title={t('docBrowserNewTab')}
362
362
  >
363
363
  <Plus className="w-3.5 h-3.5" />