@nextclaw/ui 0.14.4 → 0.15.1

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 (222) hide show
  1. package/CHANGELOG.md +73 -0
  2. package/dist/assets/api-b1R9VdeH.js +13 -0
  3. package/dist/assets/appearance-settings-page-D2O9pch9.js +1 -0
  4. package/dist/assets/book-open-B77lQ2Tg.js +1 -0
  5. package/dist/assets/channels-list-page-DcsIQc6A.js +8 -0
  6. package/dist/assets/chat-page-CZ7WxPir.js +1 -0
  7. package/dist/assets/{config-split-page-CRhlZ_pH.js → config-split-page-BWQ6_Raq.js} +1 -1
  8. package/dist/assets/confirm-dialog-C-NXSioX.js +5 -0
  9. package/dist/assets/desktop-update-config-usHpXugP.js +1 -0
  10. package/dist/assets/{dist-CikkhOlq.js → dist-YtoqU-AT.js} +1 -1
  11. package/dist/assets/{dist-PVA_xL1D.js → dist-jdGmbb6w.js} +1 -1
  12. package/dist/assets/doc-browser-B_Tf9n70.js +1 -0
  13. package/dist/assets/doc-browser-BdZPFOVa.js +1 -0
  14. package/dist/assets/{doc-browser-context-MTw4EvKd.js → doc-browser-context-DwokemUn.js} +1 -1
  15. package/dist/assets/es2015-bNpL2LKy.js +41 -0
  16. package/dist/assets/{external-link-DVJIQf_G.js → external-link-BM6fk8q-.js} +1 -1
  17. package/dist/assets/host-capabilities-9bB_998Z.js +1 -0
  18. package/dist/assets/i18n-provider-CntWolRA.js +1 -0
  19. package/dist/assets/index-BIcEw90u.js +104 -0
  20. package/dist/assets/index-BiNSTKTU.css +1 -0
  21. package/dist/assets/loader-circle-MBgNSVIH.js +1 -0
  22. package/dist/assets/mcp-marketplace-page-CLmUstLN.js +9 -0
  23. package/dist/assets/mcp-marketplace-page-rZkmWPMy.js +1 -0
  24. package/dist/assets/model-config-page-Di0ySJD2.js +1 -0
  25. package/dist/assets/plus-vDqW5Kxp.js +1 -0
  26. package/dist/assets/provider-scoped-model-input-Cm_a4GHf.js +1 -0
  27. package/dist/assets/providers-config-page-BAf2W0MV.js +1 -0
  28. package/dist/assets/{react-VNqT5Zp7.js → react-BN8uzPnp.js} +1 -1
  29. package/dist/assets/refresh-cw-BpVcFPmJ.js +1 -0
  30. package/dist/assets/remote-CLQ0IBM8.js +1 -0
  31. package/dist/assets/rotate-cw-CvNBv7_w.js +1 -0
  32. package/dist/assets/runtime-config-page-CflvvoDc.js +1 -0
  33. package/dist/assets/{save-BOJ477H-.js → save-BpFW5fpk.js} +1 -1
  34. package/dist/assets/{search-BEti5beA.js → search-D0uO7enf.js} +1 -1
  35. package/dist/assets/search-config-page-D0klFUXd.js +1 -0
  36. package/dist/assets/{secrets-config-page--IzrIOid.js → secrets-config-page-dVZ631Wp.js} +2 -2
  37. package/dist/assets/security-config-C5gnlkuj.js +1 -0
  38. package/dist/assets/select-Bh-CrA4p.js +1 -0
  39. package/dist/assets/setting-row-BJfCHRcb.js +1 -0
  40. package/dist/assets/settings-2-Blzi1vr6.js +1 -0
  41. package/dist/assets/side-dock-ZdaSrQVB.js +3 -0
  42. package/dist/assets/skeleton-BLlEzebZ.js +1 -0
  43. package/dist/assets/switch-CFLJFNSd.js +1 -0
  44. package/dist/assets/tag-chip-CzGihiQx.js +1 -0
  45. package/dist/assets/tooltip-CdcrBjrC.js +1 -0
  46. package/dist/assets/use-config-CVPgsBmU.js +1 -0
  47. package/dist/assets/use-confirm-dialog-CSeBk9SF.js +1 -0
  48. package/dist/assets/x-BF2KQ--V.js +1 -0
  49. package/dist/index.html +53 -17
  50. package/index.html +25 -2
  51. package/package.json +9 -9
  52. package/src/app/components/layout/__tests__/app-layout.test.tsx +27 -0
  53. package/src/app/components/layout/__tests__/settings-entry-page.test.tsx +9 -1
  54. package/src/app/components/layout/__tests__/sidebar.layout.test.tsx +6 -3
  55. package/src/app/components/layout/app-layout.tsx +10 -3
  56. package/src/app/components/layout/sidebar-items.tsx +8 -8
  57. package/src/app/components/layout/sidebar-rail.styles.ts +4 -2
  58. package/src/app/components/layout/sidebar.tsx +21 -28
  59. package/src/app/configs/app-navigation.config.ts +11 -5
  60. package/src/app/index.tsx +8 -0
  61. package/src/app/managers/__tests__/viewport-layout.manager.test.ts +59 -0
  62. package/src/app/managers/viewport-layout.manager.ts +39 -0
  63. package/src/app/presenters/app.presenter.ts +25 -2
  64. package/src/app/styles/design-system.css +91 -4
  65. package/src/features/agents/components/__tests__/agents-page.test.tsx +135 -2
  66. package/src/features/agents/components/agent-advanced-config-fields.tsx +70 -0
  67. package/src/features/agents/components/agent-details-dialog.tsx +398 -0
  68. package/src/features/agents/components/agent-dialogs.tsx +20 -47
  69. package/src/features/agents/components/agents-page.tsx +76 -10
  70. package/src/features/agents/types/agent-form.types.ts +21 -0
  71. package/src/features/agents/utils/agent-form.utils.ts +39 -0
  72. package/src/features/chat/components/conversation/__tests__/chat-conversation-content.test.tsx +86 -0
  73. package/src/features/chat/components/conversation/__tests__/chat-conversation-header-section.test.tsx +132 -1
  74. package/src/features/chat/components/conversation/chat-conversation-content.tsx +47 -17
  75. package/src/features/chat/components/conversation/chat-conversation-header-section.tsx +8 -0
  76. package/src/features/chat/components/conversation/chat-conversation-header.tsx +7 -3
  77. package/src/features/chat/components/layout/__tests__/chat-sidebar-read-state.test.tsx +7 -0
  78. package/src/features/chat/components/layout/chat-sidebar-desktop-layout.tsx +9 -5
  79. package/src/features/chat/components/layout/chat-sidebar-utility-menu.tsx +5 -5
  80. package/src/features/chat/features/conversation/components/__tests__/session-conversation-area.test.tsx +66 -2
  81. package/src/features/chat/features/conversation/components/__tests__/session-conversation-input.streaming.test.tsx +170 -1
  82. package/src/features/chat/features/conversation/components/session-conversation-area.tsx +23 -1
  83. package/src/features/chat/features/conversation/components/session-conversation-input.tsx +24 -7
  84. package/src/features/chat/features/conversation/components/session-queued-input-rows.tsx +96 -0
  85. package/src/features/chat/features/conversation/hooks/__tests__/use-session-conversation-controller.test.tsx +181 -0
  86. package/src/features/chat/features/conversation/hooks/use-session-conversation-controller.ts +351 -89
  87. package/src/features/chat/features/input/hooks/__tests__/use-chat-input-surface-state.test.tsx +29 -4
  88. package/src/features/chat/features/input/hooks/use-chat-input-surface-state.ts +28 -3
  89. package/src/features/chat/features/input/input-surface-plugins/__tests__/slash-command-plugin.test.ts +108 -8
  90. package/src/features/chat/features/input/input-surface-plugins/panel-app-reference-plugin.utils.ts +50 -14
  91. package/src/features/chat/features/input/input-surface-plugins/slash-command-plugin.utils.ts +77 -6
  92. package/src/features/chat/features/message/components/__tests__/chat-inline-panel-app-card.test.tsx +21 -12
  93. package/src/features/chat/features/message/components/__tests__/chat-message-list.container.test.tsx +167 -1
  94. package/src/features/chat/features/message/components/chat-inline-panel-app-card.tsx +19 -11
  95. package/src/features/chat/features/message/components/chat-message-list.container.tsx +38 -64
  96. package/src/features/chat/features/message/utils/__tests__/chat-message-process-summary.utils.test.ts +59 -0
  97. package/src/features/chat/features/message/utils/__tests__/chat-message-show-content-tool-card.utils.test.ts +56 -1
  98. package/src/features/chat/features/message/utils/chat-inline-panel-app-card.utils.ts +0 -2
  99. package/src/features/chat/features/message/utils/chat-message-process-summary.utils.ts +80 -0
  100. package/src/features/chat/features/message/utils/chat-message-show-content-tool-card.utils.ts +12 -3
  101. package/src/features/chat/features/message/utils/chat-message-texts.utils.ts +60 -0
  102. package/src/features/chat/features/message/utils/chat-message.utils.ts +3 -0
  103. package/src/features/chat/features/ncp/hooks/__tests__/use-ncp-agent-runtime.test.tsx +62 -1
  104. package/src/features/chat/features/ncp/hooks/__tests__/use-ncp-session-list-view.test.tsx +74 -0
  105. package/src/features/chat/features/ncp/hooks/use-ncp-session-list-view.ts +3 -2
  106. package/src/features/chat/features/session/components/chat-sidebar-project-groups.tsx +2 -2
  107. package/src/features/chat/features/session/components/chat-sidebar-session-item.tsx +1 -1
  108. package/src/features/chat/features/session/components/session-header/__tests__/chat-session-header-actions.test.tsx +3 -1
  109. package/src/features/chat/features/session/components/session-header/chat-session-metadata-dialog.tsx +1 -1
  110. package/src/features/chat/features/session/components/session-header/chat-session-title-switcher.tsx +294 -0
  111. package/src/features/chat/features/session/utils/__tests__/chat-session-display.utils.test.ts +15 -1
  112. package/src/features/chat/features/session/utils/__tests__/ncp-session-adapter.utils.test.ts +24 -4
  113. package/src/features/chat/features/session/utils/chat-session-display.utils.ts +3 -0
  114. package/src/features/chat/features/session/utils/ncp-session-adapter.utils.ts +1 -0
  115. package/src/features/chat/features/workspace/components/__tests__/chat-session-workspace-file-preview.test.tsx +304 -115
  116. package/src/features/chat/features/workspace/components/chat-session-workspace-file-breadcrumb-browser.tsx +121 -0
  117. package/src/features/chat/features/workspace/components/chat-session-workspace-file-breadcrumbs.tsx +75 -20
  118. package/src/features/chat/features/workspace/components/chat-session-workspace-file-preview.tsx +64 -5
  119. package/src/features/chat/features/workspace/utils/__tests__/chat-workspace-panel-view-model.utils.test.ts +46 -0
  120. package/src/features/chat/features/workspace/utils/chat-workspace-file-tab-persistence.utils.ts +83 -0
  121. package/src/features/chat/managers/__tests__/chat-thread.manager.test.ts +126 -1
  122. package/src/features/chat/managers/__tests__/chat-ui.manager.test.ts +2 -0
  123. package/src/features/chat/managers/chat-thread.manager.ts +15 -3
  124. package/src/features/chat/managers/chat-ui.manager.ts +5 -1
  125. package/src/features/chat/presenters/chat.presenter.ts +2 -1
  126. package/src/features/chat/stores/__tests__/chat-thread.store.test.ts +4 -0
  127. package/src/features/chat/stores/chat-thread.store.ts +6 -64
  128. package/src/features/chat/types/chat-message.types.ts +4 -0
  129. package/src/features/marketplace/components/__tests__/marketplace-page-detail.test.tsx +33 -26
  130. package/src/features/marketplace/components/__tests__/marketplace-page.test.tsx +1 -0
  131. package/src/features/marketplace/components/curated-shelves/__tests__/marketplace-curated-scene-route.test.tsx +1 -0
  132. package/src/features/marketplace/components/detail-doc/__tests__/marketplace-detail-doc.test.tsx +115 -0
  133. package/src/features/marketplace/components/marketplace-detail-doc.tsx +441 -0
  134. package/src/features/marketplace/components/mcp/__tests__/mcp-marketplace-page.test.tsx +3 -3
  135. package/src/features/marketplace/components/mcp/mcp-marketplace-doc.ts +0 -60
  136. package/src/features/marketplace/components/mcp/mcp-marketplace-page.tsx +64 -29
  137. package/src/features/marketplace/hooks/use-marketplace-item-detail.ts +42 -31
  138. package/src/features/marketplace/index.ts +3 -0
  139. package/src/features/marketplace/stores/marketplace-detail-doc.store.ts +38 -0
  140. package/src/features/panel-apps/components/__tests__/panel-apps-list.test.tsx +32 -2
  141. package/src/features/pwa/managers/__tests__/pwa-shell-theme.manager.test.ts +11 -0
  142. package/src/features/pwa/managers/pwa-shell-theme.manager.ts +8 -3
  143. package/src/features/settings/pages/__tests__/appearance-settings-page.test.tsx +25 -0
  144. package/src/features/settings/pages/appearance-settings-page.tsx +40 -0
  145. package/src/features/side-dock/components/__tests__/side-dock.test.tsx +44 -2
  146. package/src/features/side-dock/components/side-dock.tsx +100 -13
  147. package/src/features/side-dock/configs/side-dock-built-in-items.config.ts +10 -0
  148. package/src/features/side-dock/managers/__tests__/side-dock.manager.test.ts +32 -2
  149. package/src/features/side-dock/managers/side-dock.manager.ts +17 -7
  150. package/src/features/side-dock/stores/side-dock.store.ts +9 -0
  151. package/src/features/side-dock/types/side-dock.types.ts +10 -3
  152. package/src/features/side-dock/utils/side-dock-item.utils.ts +15 -2
  153. package/src/features/system-status/components/__tests__/desktop-update-config.test.tsx +97 -5
  154. package/src/features/system-status/components/desktop-update-config.tsx +91 -2
  155. package/src/features/system-status/utils/update-release-notes.utils.ts +94 -0
  156. package/src/index.css +501 -27
  157. package/src/platforms/desktop/components/desktop-window-chrome.tsx +1 -1
  158. package/src/shared/components/doc-browser/__tests__/doc-browser-context.test.tsx +30 -1
  159. package/src/shared/components/doc-browser/__tests__/doc-browser.test.tsx +206 -38
  160. package/src/shared/components/doc-browser/doc-browser-panel-parts.tsx +34 -11
  161. package/src/shared/components/doc-browser/doc-browser-renderer.types.ts +1 -1
  162. package/src/shared/components/doc-browser/doc-browser-tab-strip.tsx +23 -5
  163. package/src/shared/components/doc-browser/doc-browser.tsx +128 -75
  164. package/src/shared/components/doc-browser/managers/doc-browser.manager.ts +9 -1
  165. package/src/shared/components/doc-browser/utils/doc-browser-floating-panel.utils.ts +83 -0
  166. package/src/shared/components/ui/__tests__/dialog.test.tsx +27 -0
  167. package/src/shared/components/ui/actions/icon-action-button.tsx +14 -2
  168. package/src/shared/components/ui/dialog.tsx +2 -2
  169. package/src/shared/components/ui/popover.tsx +1 -1
  170. package/src/shared/components/ui/select.tsx +1 -1
  171. package/src/shared/components/ui/switch.tsx +4 -3
  172. package/src/shared/components/ui/tab-strip/__tests__/compact-tab-strip.test.tsx +54 -0
  173. package/src/shared/components/ui/tab-strip/compact-tab-strip.tsx +4 -5
  174. package/src/shared/components/ui/tooltip.tsx +1 -1
  175. package/src/shared/lib/api/ncp-session.types.ts +1 -1
  176. package/src/shared/lib/api/types.ts +12 -3
  177. package/src/shared/lib/api/utils/server-path.utils.ts +27 -1
  178. package/src/shared/lib/i18n/locales/en-US/agents.json +41 -0
  179. package/src/shared/lib/i18n/locales/en-US/chat.json +18 -6
  180. package/src/shared/lib/i18n/locales/en-US/core.json +2 -0
  181. package/src/shared/lib/i18n/locales/en-US/desktop-update.json +8 -0
  182. package/src/shared/lib/i18n/locales/en-US/doc-browser.json +11 -0
  183. package/src/shared/lib/i18n/locales/en-US/marketplace.json +5 -0
  184. package/src/shared/lib/i18n/locales/zh-CN/agents.json +41 -0
  185. package/src/shared/lib/i18n/locales/zh-CN/chat.json +18 -6
  186. package/src/shared/lib/i18n/locales/zh-CN/core.json +2 -0
  187. package/src/shared/lib/i18n/locales/zh-CN/desktop-update.json +8 -0
  188. package/src/shared/lib/i18n/locales/zh-CN/doc-browser.json +11 -0
  189. package/src/shared/lib/i18n/locales/zh-CN/marketplace.json +5 -0
  190. package/src/shared/lib/session-project/__tests__/workspace-file-breadcrumb.test.ts +14 -0
  191. package/src/shared/lib/session-project/index.ts +1 -1
  192. package/src/shared/lib/session-project/{workspace-file-breadcrumb.ts → workspace-file-breadcrumb.utils.ts} +62 -8
  193. package/src/shared/lib/theme/index.ts +51 -13
  194. package/dist/assets/api-BP_8IaxB.js +0 -13
  195. package/dist/assets/channels-list-page-DEh5Jjqx.js +0 -8
  196. package/dist/assets/chat-page-BRB0N5WW.js +0 -105
  197. package/dist/assets/confirm-dialog-DsivFJ4r.js +0 -5
  198. package/dist/assets/desktop-update-config-B0w9p2ep.js +0 -1
  199. package/dist/assets/doc-browser-BoNJ7-tE.js +0 -1
  200. package/dist/assets/doc-browser-CdJ_d4ik.js +0 -1
  201. package/dist/assets/ellipsis-y6vDtq4M.js +0 -1
  202. package/dist/assets/index-BiE-K8-q.css +0 -1
  203. package/dist/assets/index-D9BAE_L_.js +0 -105
  204. package/dist/assets/mcp-marketplace-page-B1SZNY5m.js +0 -40
  205. package/dist/assets/mcp-marketplace-page-Cq_Tw42T.js +0 -1
  206. package/dist/assets/model-config-page-DotaTUHb.js +0 -1
  207. package/dist/assets/plus-BbZqquJe.js +0 -1
  208. package/dist/assets/provider-scoped-model-input-DsFRuWJH.js +0 -1
  209. package/dist/assets/providers-config-page-DW4tQSLN.js +0 -1
  210. package/dist/assets/remote-B8TiUfwW.js +0 -1
  211. package/dist/assets/rotate-cw-DjHDTkCv.js +0 -1
  212. package/dist/assets/runtime-config-page-S6ErRHR8.js +0 -1
  213. package/dist/assets/search-config-page-DGj5am03.js +0 -1
  214. package/dist/assets/select-hoj8dK4K.js +0 -41
  215. package/dist/assets/tag-chip-T8-h-die.js +0 -1
  216. package/dist/assets/use-config-CjRV2hwL.js +0 -1
  217. package/dist/assets/x-D9PXs_xP.js +0 -1
  218. package/src/features/marketplace/components/detail-doc/__tests__/marketplace-detail-doc.test.ts +0 -40
  219. package/src/features/marketplace/components/detail-doc/marketplace-detail-doc-renderer.ts +0 -201
  220. package/src/features/marketplace/components/marketplace-detail-doc.ts +0 -142
  221. /package/dist/assets/{config-hints-BkYBc7z0.js → config-hints-fGnUjDe9.js} +0 -0
  222. /package/dist/assets/{middleware-CkFT3R4v.js → middleware-BILWbJE4.js} +0 -0
@@ -1,9 +1,24 @@
1
1
  import { beforeEach, describe, expect, it, vi } from 'vitest';
2
+ import {
3
+ getSideDockBuiltInItems,
4
+ SIDE_DOCK_GITHUB_PROJECT_ITEM_ID,
5
+ SIDE_DOCK_GITHUB_PROJECT_URL,
6
+ } from '@/features/side-dock/configs/side-dock-built-in-items.config';
2
7
  import { SideDockManager } from '@/features/side-dock/managers/side-dock.manager';
3
8
  import { useSideDockStore } from '@/features/side-dock/stores/side-dock.store';
4
9
  import type { SideDockItem } from '@/features/side-dock/types/side-dock.types';
5
10
  import type { DocBrowserManager } from '@/shared/components/doc-browser/managers/doc-browser.manager';
6
11
 
12
+ const mocks = vi.hoisted(() => ({
13
+ openExternalUrl: vi.fn(),
14
+ }));
15
+
16
+ vi.mock('@/shared/lib/host-capabilities', () => ({
17
+ hostCapabilityManager: {
18
+ openExternalUrl: mocks.openExternalUrl,
19
+ },
20
+ }));
21
+
7
22
  const customItem: SideDockItem = {
8
23
  builtIn: false,
9
24
  icon: { type: 'builtin', name: 'docs' },
@@ -16,14 +31,17 @@ const customItem: SideDockItem = {
16
31
  describe('SideDockManager', () => {
17
32
  beforeEach(() => {
18
33
  window.localStorage.clear();
34
+ mocks.openExternalUrl.mockReset();
35
+ mocks.openExternalUrl.mockResolvedValue({ opened: true });
36
+ useSideDockStore.getState().setVisible(true);
19
37
  useSideDockStore.getState().setPinnedItems([]);
20
38
  });
21
39
 
22
- it('opens right panel resource items through DocBrowserManager', () => {
40
+ it('opens right panel resource items through DocBrowserManager', async () => {
23
41
  const open = vi.fn();
24
42
  const manager = new SideDockManager({ open } as unknown as DocBrowserManager);
25
43
 
26
- manager.openItem(customItem);
44
+ await manager.openItem(customItem);
27
45
 
28
46
  expect(open).toHaveBeenCalledWith('nextclaw://docs/custom', {
29
47
  dockIcon: { type: 'builtin', name: 'docs' },
@@ -31,6 +49,18 @@ describe('SideDockManager', () => {
31
49
  });
32
50
  });
33
51
 
52
+ it('opens the GitHub project shortcut through host capabilities', async () => {
53
+ const open = vi.fn();
54
+ const manager = new SideDockManager({ open } as unknown as DocBrowserManager);
55
+ const githubItem = getSideDockBuiltInItems().find((item) => item.id === SIDE_DOCK_GITHUB_PROJECT_ITEM_ID);
56
+
57
+ expect(githubItem).toBeTruthy();
58
+ await manager.openItem(githubItem as SideDockItem);
59
+
60
+ expect(open).not.toHaveBeenCalled();
61
+ expect(mocks.openExternalUrl).toHaveBeenCalledWith(SIDE_DOCK_GITHUB_PROJECT_URL);
62
+ });
63
+
34
64
  it('pins removable items and keeps built-in items immutable', () => {
35
65
  const manager = new SideDockManager({ open: vi.fn() } as unknown as DocBrowserManager);
36
66
 
@@ -10,6 +10,7 @@ import {
10
10
  createPinnedSideDockItem,
11
11
  mergeSideDockItems,
12
12
  } from '@/features/side-dock/utils/side-dock-item.utils';
13
+ import { hostCapabilityManager } from '@/shared/lib/host-capabilities';
13
14
  import type { DocBrowserManager } from '@/shared/components/doc-browser/managers/doc-browser.manager';
14
15
  import type {
15
16
  DocBrowserDockIcon,
@@ -20,6 +21,7 @@ const SIDE_DOCK_PINNED_ITEM_ID_PREFIX = 'pinned';
20
21
  const SIDE_DOCK_ICON_NAMES = new Set<SideDockIconName>([
21
22
  'apps',
22
23
  'docs',
24
+ 'github',
23
25
  'new-tab',
24
26
  'panel-app',
25
27
  'service-apps',
@@ -78,12 +80,16 @@ export class SideDockManager {
78
80
  useSideDockStore.getState().pinnedItems,
79
81
  );
80
82
 
81
- openItem = (item: Pick<SideDockItem, 'icon' | 'label' | 'target'>): void => {
83
+ openItem = async (item: Pick<SideDockItem, 'icon' | 'label' | 'target'>): Promise<void> => {
82
84
  if (item.target.type === 'right-panel-resource') {
83
85
  this.docBrowserManager.open(item.target.uri, {
84
86
  dockIcon: item.icon,
85
87
  title: item.label,
86
88
  });
89
+ return;
90
+ }
91
+ if (item.target.type === 'external-url') {
92
+ await hostCapabilityManager.openExternalUrl(item.target.url);
87
93
  }
88
94
  };
89
95
 
@@ -130,18 +136,19 @@ export class SideDockManager {
130
136
  };
131
137
 
132
138
  pinItem = (item: SideDockItem, createdAt: string = new Date().toISOString()): void => {
133
- if (!item.removable) {
139
+ if (!item.removable || item.target.type !== 'right-panel-resource') {
134
140
  return;
135
141
  }
136
142
  const uri = normalizeSideDockResourceUri(item.target.uri);
143
+ const resourceItem = {
144
+ ...item,
145
+ target: { type: 'right-panel-resource' as const, uri },
146
+ };
137
147
  this.setPinnedItems((items) => {
138
148
  if (items.some((pinnedItem) => pinnedItem.id === item.id || normalizeSideDockResourceUri(pinnedItem.target.uri) === uri)) {
139
149
  return items;
140
150
  }
141
- return [...items, createPinnedSideDockItem({
142
- ...item,
143
- target: { ...item.target, uri },
144
- }, createdAt)];
151
+ return [...items, createPinnedSideDockItem(resourceItem, createdAt)];
145
152
  });
146
153
  };
147
154
 
@@ -176,6 +183,9 @@ export class SideDockManager {
176
183
 
177
184
  private findItemByUri = (uri: string): SideDockItem | undefined => {
178
185
  const normalized = normalizeSideDockResourceUri(uri);
179
- return this.getItems().find((item) => normalizeSideDockResourceUri(item.target.uri) === normalized);
186
+ return this.getItems().find((item) => (
187
+ item.target.type === 'right-panel-resource'
188
+ && normalizeSideDockResourceUri(item.target.uri) === normalized
189
+ ));
180
190
  };
181
191
  }
@@ -7,7 +7,9 @@ const SIDE_DOCK_STORAGE_KEY = 'nextclaw.side-dock.state';
7
7
  const SIDE_DOCK_STORAGE_VERSION = 1;
8
8
 
9
9
  type SideDockStore = {
10
+ isVisible: boolean;
10
11
  pinnedItems: SideDockPinnedItem[];
12
+ setVisible: (isVisible: boolean) => void;
11
13
  setPinnedItems: (items: SideDockPinnedItem[]) => void;
12
14
  };
13
15
 
@@ -18,7 +20,9 @@ function isRecord(value: unknown): value is Record<string, unknown> {
18
20
  export const useSideDockStore = create<SideDockStore>()(
19
21
  persist(
20
22
  (set) => ({
23
+ isVisible: true,
21
24
  pinnedItems: [],
25
+ setVisible: (isVisible) => set({ isVisible }),
22
26
  setPinnedItems: (items) => set({ pinnedItems: normalizeSideDockPinnedItems(items) }),
23
27
  }),
24
28
  {
@@ -26,14 +30,19 @@ export const useSideDockStore = create<SideDockStore>()(
26
30
  version: SIDE_DOCK_STORAGE_VERSION,
27
31
  storage: createJSONStorage(() => window.localStorage),
28
32
  partialize: (state) => ({
33
+ isVisible: state.isVisible,
29
34
  pinnedItems: state.pinnedItems,
30
35
  }),
31
36
  merge: (persistedState, currentState) => {
32
37
  const pinnedItems = isRecord(persistedState)
33
38
  ? normalizeSideDockPinnedItems(persistedState.pinnedItems)
34
39
  : [];
40
+ const isVisible = isRecord(persistedState) && typeof persistedState.isVisible === 'boolean'
41
+ ? persistedState.isVisible
42
+ : true;
35
43
  return {
36
44
  ...currentState,
45
+ isVisible,
37
46
  pinnedItems,
38
47
  };
39
48
  },
@@ -1,11 +1,18 @@
1
1
  export type SideDockItemId = string;
2
2
 
3
- export type SideDockItemTarget = {
3
+ export type SideDockResourceItemTarget = {
4
4
  type: 'right-panel-resource';
5
5
  uri: string;
6
6
  };
7
7
 
8
- export type SideDockIconName = 'apps' | 'docs' | 'new-tab' | 'panel-app' | 'service-apps';
8
+ export type SideDockExternalUrlTarget = {
9
+ type: 'external-url';
10
+ url: string;
11
+ };
12
+
13
+ export type SideDockItemTarget = SideDockResourceItemTarget | SideDockExternalUrlTarget;
14
+
15
+ export type SideDockIconName = 'apps' | 'docs' | 'github' | 'new-tab' | 'panel-app' | 'service-apps';
9
16
 
10
17
  export type SideDockItemIcon =
11
18
  | { type: 'builtin'; name: SideDockIconName }
@@ -26,7 +33,7 @@ export type SideDockPinnedItem = {
26
33
  icon: SideDockItemIcon;
27
34
  id: SideDockItemId;
28
35
  label: string;
29
- target: SideDockItemTarget;
36
+ target: SideDockResourceItemTarget;
30
37
  };
31
38
 
32
39
  export type SideDockResourceDockState = {
@@ -3,11 +3,13 @@ import type {
3
3
  SideDockItem,
4
4
  SideDockItemIcon,
5
5
  SideDockPinnedItem,
6
+ SideDockResourceItemTarget,
6
7
  } from '@/features/side-dock/types/side-dock.types';
7
8
 
8
9
  const SIDE_DOCK_BUILTIN_ICON_NAMES: SideDockIconName[] = [
9
10
  'apps',
10
11
  'docs',
12
+ 'github',
11
13
  'new-tab',
12
14
  'panel-app',
13
15
  'service-apps',
@@ -99,7 +101,10 @@ export function normalizeSideDockPinnedItems(value: unknown): SideDockPinnedItem
99
101
  return Array.from(itemsById.values());
100
102
  }
101
103
 
102
- export function createPinnedSideDockItem(item: SideDockItem, createdAt: string): SideDockPinnedItem {
104
+ export function createPinnedSideDockItem(
105
+ item: SideDockItem & { target: SideDockResourceItemTarget },
106
+ createdAt: string,
107
+ ): SideDockPinnedItem {
103
108
  return {
104
109
  createdAt,
105
110
  icon: item.icon,
@@ -109,6 +114,10 @@ export function createPinnedSideDockItem(item: SideDockItem, createdAt: string):
109
114
  };
110
115
  }
111
116
 
117
+ function getSideDockResourceUri(item: SideDockItem | SideDockPinnedItem): string | null {
118
+ return item.target.type === 'right-panel-resource' ? item.target.uri : null;
119
+ }
120
+
112
121
  export function createSideDockItemFromPinnedItem(item: SideDockPinnedItem): SideDockItem {
113
122
  return {
114
123
  builtIn: false,
@@ -125,7 +134,11 @@ export function mergeSideDockItems(
125
134
  pinnedItems: SideDockPinnedItem[],
126
135
  ): SideDockItem[] {
127
136
  const seenIds = new Set(builtInItems.map((item) => item.id));
128
- const seenUris = new Set(builtInItems.map((item) => item.target.uri));
137
+ const seenUris = new Set(
138
+ builtInItems
139
+ .map(getSideDockResourceUri)
140
+ .filter((uri): uri is string => uri !== null),
141
+ );
129
142
  const customItems = pinnedItems
130
143
  .filter((item) => !seenIds.has(item.id) && !seenUris.has(item.target.uri))
131
144
  .map(createSideDockItemFromPinnedItem);
@@ -1,6 +1,7 @@
1
1
  import { render, screen } from '@testing-library/react';
2
2
  import userEvent from '@testing-library/user-event';
3
- import { beforeEach, describe, expect, it, vi } from 'vitest';
3
+ import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
4
+ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
4
5
  import { useRuntimeUpdateStore } from '@/features/system-status';
5
6
  import { DesktopUpdateConfig } from '@/features/system-status/components/desktop-update-config';
6
7
  import { setLanguage } from '@/shared/lib/i18n';
@@ -26,6 +27,22 @@ vi.mock('@/features/system-status', async () => {
26
27
  };
27
28
  });
28
29
 
30
+ function renderDesktopUpdateConfig() {
31
+ const queryClient = new QueryClient({
32
+ defaultOptions: {
33
+ queries: {
34
+ gcTime: 0,
35
+ retry: false
36
+ }
37
+ }
38
+ });
39
+ return render(
40
+ <QueryClientProvider client={queryClient}>
41
+ <DesktopUpdateConfig />
42
+ </QueryClientProvider>
43
+ );
44
+ }
45
+
29
46
  describe('DesktopUpdateConfig', () => {
30
47
  beforeEach(() => {
31
48
  setLanguage('zh');
@@ -60,7 +77,7 @@ describe('DesktopUpdateConfig', () => {
60
77
  availableVersion: '0.18.2-beta.1',
61
78
  downloadedVersion: null,
62
79
  minimumHostVersion: null,
63
- releaseNotesUrl: 'https://example.com/release-notes',
80
+ releaseNotesUrl: null,
64
81
  lastCheckedAt: '2026-04-13T12:00:00.000Z',
65
82
  progress: null,
66
83
  canAutoDownload: false,
@@ -77,8 +94,12 @@ describe('DesktopUpdateConfig', () => {
77
94
  });
78
95
  });
79
96
 
97
+ afterEach(() => {
98
+ vi.unstubAllGlobals();
99
+ });
100
+
80
101
  it('renders current channel information and beta guidance', () => {
81
- render(<DesktopUpdateConfig />);
102
+ renderDesktopUpdateConfig();
82
103
 
83
104
  expect(mocks.start).not.toHaveBeenCalled();
84
105
  expect(mocks.stop).not.toHaveBeenCalled();
@@ -93,7 +114,7 @@ describe('DesktopUpdateConfig', () => {
93
114
  it('sends the newly selected release channel to the desktop update manager', async () => {
94
115
  const user = userEvent.setup();
95
116
 
96
- render(<DesktopUpdateConfig />);
117
+ renderDesktopUpdateConfig();
97
118
 
98
119
  await user.click(screen.getByRole('combobox'));
99
120
  await user.click(screen.getByRole('option', { name: 'Stable' }));
@@ -118,9 +139,80 @@ describe('DesktopUpdateConfig', () => {
118
139
  : null
119
140
  }));
120
141
 
121
- render(<DesktopUpdateConfig />);
142
+ renderDesktopUpdateConfig();
122
143
 
123
144
  expect(screen.getByText('正在下载 50%')).toBeTruthy();
124
145
  expect(screen.getByText('50 B / 100 B')).toBeTruthy();
125
146
  });
147
+
148
+ it('loads structured release notes from the docs JSON endpoint', async () => {
149
+ const fetchMock = vi.fn(async () => new Response(JSON.stringify({
150
+ schemaVersion: 1,
151
+ product: 'NextClaw',
152
+ version: '0.22.0',
153
+ sections: [
154
+ {
155
+ kind: 'feature',
156
+ title: {
157
+ 'zh-CN': '功能',
158
+ 'en-US': 'Features'
159
+ },
160
+ items: [
161
+ {
162
+ title: {
163
+ 'zh-CN': '更新前查看内容',
164
+ 'en-US': 'Review before updating'
165
+ },
166
+ body: {
167
+ 'zh-CN': '更新提示可以直接展示本版本的结构化说明。',
168
+ 'en-US': 'The update prompt can show structured notes for this version.'
169
+ }
170
+ }
171
+ ]
172
+ },
173
+ {
174
+ kind: 'fix',
175
+ title: {
176
+ 'zh-CN': '修复',
177
+ 'en-US': 'Fixes'
178
+ },
179
+ items: [
180
+ {
181
+ title: {
182
+ 'zh-CN': '附件发送修复',
183
+ 'en-US': 'Attachment delivery fix'
184
+ }
185
+ }
186
+ ]
187
+ }
188
+ ]
189
+ }), {
190
+ headers: { 'content-type': 'application/json' },
191
+ status: 200
192
+ }));
193
+ vi.stubGlobal('fetch', fetchMock);
194
+ useRuntimeUpdateStore.setState((state) => ({
195
+ ...state,
196
+ snapshot: state.snapshot
197
+ ? {
198
+ ...state.snapshot,
199
+ status: 'update-available',
200
+ availableVersion: '0.22.0',
201
+ releaseNotesUrl: 'https://docs.nextclaw.io/zh/notes/2026-07-05-nextclaw-v0-22-0'
202
+ }
203
+ : null
204
+ }));
205
+
206
+ renderDesktopUpdateConfig();
207
+
208
+ expect(await screen.findByText('更新前查看内容')).toBeTruthy();
209
+ expect(screen.getByText('修复')).toBeTruthy();
210
+ expect(fetchMock).toHaveBeenCalledWith(
211
+ 'https://docs.nextclaw.io/release-notes/nextclaw-v0.22.0.json',
212
+ expect.objectContaining({
213
+ cache: 'no-store',
214
+ headers: { accept: 'application/json' }
215
+ })
216
+ );
217
+ });
126
218
  });
@@ -1,4 +1,5 @@
1
1
  import type { UpdateSnapshot } from '@nextclaw/shared';
2
+ import { useQuery } from '@tanstack/react-query';
2
3
  import { runtimeUpdateManager, useRuntimeUpdateStore } from '@/features/system-status';
3
4
  import { Button } from '@/shared/components/ui/button';
4
5
  import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/shared/components/ui/card';
@@ -6,9 +7,16 @@ import { Label } from '@/shared/components/ui/label';
6
7
  import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/shared/components/ui/select';
7
8
  import { Switch } from '@/shared/components/ui/switch';
8
9
  import { PageHeader, PageLayout } from '@/app/components/layout/page-layout';
9
- import { formatDateTime, t } from '@/shared/lib/i18n';
10
+ import { formatDateTime, getLanguage, t } from '@/shared/lib/i18n';
10
11
  import { cn } from '@/shared/lib/utils';
11
12
  import { hostCapabilityManager } from '@/shared/lib/host-capabilities';
13
+ import {
14
+ fetchReleaseNotesData,
15
+ readReleaseNotesText,
16
+ resolveReleaseNotesDataUrl,
17
+ type ReleaseNotesLocale,
18
+ type ReleaseNotesSection
19
+ } from '@/features/system-status/utils/update-release-notes.utils';
12
20
  import { Download, ExternalLink, RefreshCw, RotateCw } from 'lucide-react';
13
21
 
14
22
  const STATUS_LABEL_KEYS: Record<string, string> = {
@@ -21,6 +29,13 @@ const STATUS_LABEL_KEYS: Record<string, string> = {
21
29
  failed: 'desktopUpdatesStatusFailed',
22
30
  };
23
31
 
32
+ const RELEASE_NOTES_KIND_LABEL_KEYS: Record<string, string> = {
33
+ feature: 'desktopUpdatesReleaseNotesFeature',
34
+ enhancement: 'desktopUpdatesReleaseNotesEnhancement',
35
+ fix: 'desktopUpdatesReleaseNotesFix',
36
+ compatibility: 'desktopUpdatesReleaseNotesCompatibility'
37
+ };
38
+
24
39
  function StatusBadge({ status }: { status: string }) {
25
40
  return <span className={cn('inline-flex rounded-full px-3 py-1 text-xs font-medium ring-1', getStatusTone(status))}>{getStatusLabel(status)}</span>;
26
41
  }
@@ -117,6 +132,80 @@ function getStatusTone(status: string): string {
117
132
  return 'bg-gray-100 text-gray-700 ring-gray-200';
118
133
  }
119
134
 
135
+ function getReleaseNotesLocale(): ReleaseNotesLocale {
136
+ return getLanguage() === 'zh' ? 'zh-CN' : 'en-US';
137
+ }
138
+
139
+ function getReleaseNotesSectionTitle(section: ReleaseNotesSection, locale: ReleaseNotesLocale): string {
140
+ const title = readReleaseNotesText(section.title, locale);
141
+ return title ?? t(RELEASE_NOTES_KIND_LABEL_KEYS[section.kind] ?? 'desktopUpdatesReleaseNotesEnhancement');
142
+ }
143
+
144
+ function ReleaseNotesPreview({ snapshot }: { snapshot: UpdateSnapshot }) {
145
+ const dataUrl = resolveReleaseNotesDataUrl(snapshot);
146
+ const locale = getReleaseNotesLocale();
147
+ const releaseNotesQuery = useQuery({
148
+ queryKey: ['runtime-release-notes', dataUrl],
149
+ queryFn: async () => await fetchReleaseNotesData(dataUrl ?? ''),
150
+ enabled: Boolean(dataUrl),
151
+ retry: false,
152
+ staleTime: 5 * 60 * 1000
153
+ });
154
+ const payload = releaseNotesQuery.data;
155
+ const title = readReleaseNotesText(payload?.title, locale) ?? t('desktopUpdatesReleaseNotesPreviewTitle');
156
+ const summary = readReleaseNotesText(payload?.summary, locale) ?? t('desktopUpdatesReleaseNotesPreviewDescription');
157
+
158
+ if (!snapshot.releaseNotesUrl) {
159
+ return null;
160
+ }
161
+
162
+ return (
163
+ <Card>
164
+ <CardHeader>
165
+ <div className="flex flex-wrap items-start justify-between gap-3">
166
+ <div className="space-y-1">
167
+ <CardTitle>{title}</CardTitle>
168
+ <CardDescription>{summary}</CardDescription>
169
+ </div>
170
+ <Button variant="ghost" onClick={() => void hostCapabilityManager.openExternalUrl(snapshot.releaseNotesUrl ?? '')}>
171
+ <ExternalLink className="mr-2 h-4 w-4" />
172
+ {t('desktopUpdatesReleaseNotes')}
173
+ </Button>
174
+ </div>
175
+ </CardHeader>
176
+ <CardContent className="space-y-4">
177
+ {releaseNotesQuery.isLoading ? (
178
+ <p className="text-sm text-gray-500">{t('desktopUpdatesReleaseNotesPreviewLoading')}</p>
179
+ ) : null}
180
+ {!dataUrl || releaseNotesQuery.isError ? (
181
+ <p className="text-sm text-gray-500">{t('desktopUpdatesReleaseNotesPreviewUnavailable')}</p>
182
+ ) : null}
183
+ {payload ? (
184
+ <div className="grid gap-4 md:grid-cols-2">
185
+ {payload.sections.map((section) => (
186
+ <div key={section.kind} className="rounded-xl border border-gray-200 bg-gray-50/60 p-4">
187
+ <p className="text-sm font-semibold text-gray-900">{getReleaseNotesSectionTitle(section, locale)}</p>
188
+ <ul className="mt-3 space-y-3">
189
+ {section.items.map((item, index) => {
190
+ const itemTitle = readReleaseNotesText(item.title, locale);
191
+ const itemBody = readReleaseNotesText(item.body, locale);
192
+ return (
193
+ <li key={`${itemTitle ?? section.kind}-${index}`} className="space-y-1">
194
+ {itemTitle ? <p className="text-sm font-medium text-gray-800">{itemTitle}</p> : null}
195
+ {itemBody ? <p className="text-sm leading-5 text-gray-600">{itemBody}</p> : null}
196
+ </li>
197
+ );
198
+ })}
199
+ </ul>
200
+ </div>
201
+ ))}
202
+ </div>
203
+ ) : null}
204
+ </CardContent>
205
+ </Card>
206
+ );
207
+ }
208
+
120
209
  function RuntimeUpdateUnavailableState() {
121
210
  return (
122
211
  <PageLayout className="space-y-6">
@@ -192,6 +281,7 @@ export function DesktopUpdateConfig() {
192
281
  {snapshot.errorMessage && snapshot.status !== 'blocked' ? <div className="rounded-2xl border border-red-200 bg-red-50/70 p-4 text-sm text-red-700">{snapshot.errorMessage}</div> : null}
193
282
  </CardContent>
194
283
  </Card>
284
+ <ReleaseNotesPreview snapshot={snapshot} />
195
285
  <Card>
196
286
  <CardHeader>
197
287
  <CardTitle>{t('desktopUpdatesPreferencesTitle')}</CardTitle>
@@ -250,7 +340,6 @@ export function DesktopUpdateConfig() {
250
340
  <RotateCw className={cn('mr-2 h-4 w-4', isApplying && 'animate-spin')} />
251
341
  {t('runtimeUpdatesApplyNow')}
252
342
  </Button>
253
- {snapshot.releaseNotesUrl ? <Button variant="ghost" onClick={() => void hostCapabilityManager.openExternalUrl(snapshot.releaseNotesUrl ?? '')}><ExternalLink className="mr-2 h-4 w-4" />{t('desktopUpdatesReleaseNotes')}</Button> : null}
254
343
  </CardContent>
255
344
  </Card>
256
345
  </PageLayout>
@@ -0,0 +1,94 @@
1
+ import type { UpdateSnapshot } from '@nextclaw/shared';
2
+
3
+ export type ReleaseNotesLocale = 'zh-CN' | 'en-US';
4
+
5
+ export type ReleaseNotesText = Partial<Record<ReleaseNotesLocale, string>>;
6
+
7
+ export type ReleaseNotesItem = {
8
+ title: ReleaseNotesText;
9
+ body?: ReleaseNotesText;
10
+ };
11
+
12
+ export type ReleaseNotesSection = {
13
+ kind: string;
14
+ title?: ReleaseNotesText;
15
+ items: ReleaseNotesItem[];
16
+ };
17
+
18
+ export type ReleaseNotesPayload = {
19
+ title?: ReleaseNotesText;
20
+ summary?: ReleaseNotesText;
21
+ sections: ReleaseNotesSection[];
22
+ };
23
+
24
+ export function readReleaseNotesText(
25
+ value: ReleaseNotesText | undefined,
26
+ locale: ReleaseNotesLocale
27
+ ): string | null {
28
+ const fallbackLocale = locale === 'zh-CN' ? 'en-US' : 'zh-CN';
29
+ const text = value?.[locale]?.trim() || value?.[fallbackLocale]?.trim();
30
+ return text || null;
31
+ }
32
+
33
+ export function resolveReleaseNotesDataUrl(snapshot: UpdateSnapshot): string | null {
34
+ const releaseNotesUrl = snapshot.releaseNotesUrl?.trim();
35
+ const version = (snapshot.availableVersion ?? snapshot.downloadedVersion)?.trim();
36
+ if (!releaseNotesUrl || !version || !/^[0-9A-Za-z.-]+$/.test(version)) {
37
+ return null;
38
+ }
39
+
40
+ try {
41
+ return new URL(`/release-notes/nextclaw-v${version}.json`, releaseNotesUrl).toString();
42
+ } catch {
43
+ return null;
44
+ }
45
+ }
46
+
47
+ function isRecord(value: unknown): value is Record<string, unknown> {
48
+ return Boolean(value) && typeof value === 'object' && !Array.isArray(value);
49
+ }
50
+
51
+ function isReleaseNotesText(value: unknown): value is ReleaseNotesText {
52
+ if (!isRecord(value)) {
53
+ return false;
54
+ }
55
+ return typeof value['zh-CN'] === 'string' || typeof value['en-US'] === 'string';
56
+ }
57
+
58
+ function isReleaseNotesItem(value: unknown): value is ReleaseNotesItem {
59
+ if (!isRecord(value) || !isReleaseNotesText(value.title)) {
60
+ return false;
61
+ }
62
+ return value.body === undefined || isReleaseNotesText(value.body);
63
+ }
64
+
65
+ function isReleaseNotesSection(value: unknown): value is ReleaseNotesSection {
66
+ return isRecord(value)
67
+ && typeof value.kind === 'string'
68
+ && Array.isArray(value.items)
69
+ && value.items.every(isReleaseNotesItem)
70
+ && (value.title === undefined || isReleaseNotesText(value.title));
71
+ }
72
+
73
+ function isReleaseNotesPayload(value: unknown): value is ReleaseNotesPayload {
74
+ return isRecord(value)
75
+ && Array.isArray(value.sections)
76
+ && value.sections.every(isReleaseNotesSection)
77
+ && (value.title === undefined || isReleaseNotesText(value.title))
78
+ && (value.summary === undefined || isReleaseNotesText(value.summary));
79
+ }
80
+
81
+ export async function fetchReleaseNotesData(url: string): Promise<ReleaseNotesPayload> {
82
+ const response = await fetch(url, {
83
+ cache: 'no-store',
84
+ headers: { accept: 'application/json' }
85
+ });
86
+ if (!response.ok) {
87
+ throw new Error(`Release notes request failed with HTTP ${response.status}.`);
88
+ }
89
+ const payload: unknown = await response.json();
90
+ if (!isReleaseNotesPayload(payload)) {
91
+ throw new Error('Release notes payload has an unsupported shape.');
92
+ }
93
+ return payload;
94
+ }