@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.
- package/CHANGELOG.md +73 -0
- package/dist/assets/api-b1R9VdeH.js +13 -0
- package/dist/assets/appearance-settings-page-D2O9pch9.js +1 -0
- package/dist/assets/book-open-B77lQ2Tg.js +1 -0
- package/dist/assets/channels-list-page-DcsIQc6A.js +8 -0
- package/dist/assets/chat-page-CZ7WxPir.js +1 -0
- package/dist/assets/{config-split-page-CRhlZ_pH.js → config-split-page-BWQ6_Raq.js} +1 -1
- package/dist/assets/confirm-dialog-C-NXSioX.js +5 -0
- package/dist/assets/desktop-update-config-usHpXugP.js +1 -0
- package/dist/assets/{dist-CikkhOlq.js → dist-YtoqU-AT.js} +1 -1
- package/dist/assets/{dist-PVA_xL1D.js → dist-jdGmbb6w.js} +1 -1
- package/dist/assets/doc-browser-B_Tf9n70.js +1 -0
- package/dist/assets/doc-browser-BdZPFOVa.js +1 -0
- package/dist/assets/{doc-browser-context-MTw4EvKd.js → doc-browser-context-DwokemUn.js} +1 -1
- package/dist/assets/es2015-bNpL2LKy.js +41 -0
- package/dist/assets/{external-link-DVJIQf_G.js → external-link-BM6fk8q-.js} +1 -1
- package/dist/assets/host-capabilities-9bB_998Z.js +1 -0
- package/dist/assets/i18n-provider-CntWolRA.js +1 -0
- package/dist/assets/index-BIcEw90u.js +104 -0
- package/dist/assets/index-BiNSTKTU.css +1 -0
- package/dist/assets/loader-circle-MBgNSVIH.js +1 -0
- package/dist/assets/mcp-marketplace-page-CLmUstLN.js +9 -0
- package/dist/assets/mcp-marketplace-page-rZkmWPMy.js +1 -0
- package/dist/assets/model-config-page-Di0ySJD2.js +1 -0
- package/dist/assets/plus-vDqW5Kxp.js +1 -0
- package/dist/assets/provider-scoped-model-input-Cm_a4GHf.js +1 -0
- package/dist/assets/providers-config-page-BAf2W0MV.js +1 -0
- package/dist/assets/{react-VNqT5Zp7.js → react-BN8uzPnp.js} +1 -1
- package/dist/assets/refresh-cw-BpVcFPmJ.js +1 -0
- package/dist/assets/remote-CLQ0IBM8.js +1 -0
- package/dist/assets/rotate-cw-CvNBv7_w.js +1 -0
- package/dist/assets/runtime-config-page-CflvvoDc.js +1 -0
- package/dist/assets/{save-BOJ477H-.js → save-BpFW5fpk.js} +1 -1
- package/dist/assets/{search-BEti5beA.js → search-D0uO7enf.js} +1 -1
- package/dist/assets/search-config-page-D0klFUXd.js +1 -0
- package/dist/assets/{secrets-config-page--IzrIOid.js → secrets-config-page-dVZ631Wp.js} +2 -2
- package/dist/assets/security-config-C5gnlkuj.js +1 -0
- package/dist/assets/select-Bh-CrA4p.js +1 -0
- package/dist/assets/setting-row-BJfCHRcb.js +1 -0
- package/dist/assets/settings-2-Blzi1vr6.js +1 -0
- package/dist/assets/side-dock-ZdaSrQVB.js +3 -0
- package/dist/assets/skeleton-BLlEzebZ.js +1 -0
- package/dist/assets/switch-CFLJFNSd.js +1 -0
- package/dist/assets/tag-chip-CzGihiQx.js +1 -0
- package/dist/assets/tooltip-CdcrBjrC.js +1 -0
- package/dist/assets/use-config-CVPgsBmU.js +1 -0
- package/dist/assets/use-confirm-dialog-CSeBk9SF.js +1 -0
- package/dist/assets/x-BF2KQ--V.js +1 -0
- package/dist/index.html +53 -17
- package/index.html +25 -2
- package/package.json +9 -9
- package/src/app/components/layout/__tests__/app-layout.test.tsx +27 -0
- package/src/app/components/layout/__tests__/settings-entry-page.test.tsx +9 -1
- package/src/app/components/layout/__tests__/sidebar.layout.test.tsx +6 -3
- package/src/app/components/layout/app-layout.tsx +10 -3
- package/src/app/components/layout/sidebar-items.tsx +8 -8
- package/src/app/components/layout/sidebar-rail.styles.ts +4 -2
- package/src/app/components/layout/sidebar.tsx +21 -28
- package/src/app/configs/app-navigation.config.ts +11 -5
- package/src/app/index.tsx +8 -0
- package/src/app/managers/__tests__/viewport-layout.manager.test.ts +59 -0
- package/src/app/managers/viewport-layout.manager.ts +39 -0
- package/src/app/presenters/app.presenter.ts +25 -2
- package/src/app/styles/design-system.css +91 -4
- package/src/features/agents/components/__tests__/agents-page.test.tsx +135 -2
- package/src/features/agents/components/agent-advanced-config-fields.tsx +70 -0
- package/src/features/agents/components/agent-details-dialog.tsx +398 -0
- package/src/features/agents/components/agent-dialogs.tsx +20 -47
- package/src/features/agents/components/agents-page.tsx +76 -10
- package/src/features/agents/types/agent-form.types.ts +21 -0
- package/src/features/agents/utils/agent-form.utils.ts +39 -0
- package/src/features/chat/components/conversation/__tests__/chat-conversation-content.test.tsx +86 -0
- package/src/features/chat/components/conversation/__tests__/chat-conversation-header-section.test.tsx +132 -1
- package/src/features/chat/components/conversation/chat-conversation-content.tsx +47 -17
- package/src/features/chat/components/conversation/chat-conversation-header-section.tsx +8 -0
- package/src/features/chat/components/conversation/chat-conversation-header.tsx +7 -3
- package/src/features/chat/components/layout/__tests__/chat-sidebar-read-state.test.tsx +7 -0
- package/src/features/chat/components/layout/chat-sidebar-desktop-layout.tsx +9 -5
- package/src/features/chat/components/layout/chat-sidebar-utility-menu.tsx +5 -5
- package/src/features/chat/features/conversation/components/__tests__/session-conversation-area.test.tsx +66 -2
- package/src/features/chat/features/conversation/components/__tests__/session-conversation-input.streaming.test.tsx +170 -1
- package/src/features/chat/features/conversation/components/session-conversation-area.tsx +23 -1
- package/src/features/chat/features/conversation/components/session-conversation-input.tsx +24 -7
- package/src/features/chat/features/conversation/components/session-queued-input-rows.tsx +96 -0
- package/src/features/chat/features/conversation/hooks/__tests__/use-session-conversation-controller.test.tsx +181 -0
- package/src/features/chat/features/conversation/hooks/use-session-conversation-controller.ts +351 -89
- package/src/features/chat/features/input/hooks/__tests__/use-chat-input-surface-state.test.tsx +29 -4
- package/src/features/chat/features/input/hooks/use-chat-input-surface-state.ts +28 -3
- package/src/features/chat/features/input/input-surface-plugins/__tests__/slash-command-plugin.test.ts +108 -8
- package/src/features/chat/features/input/input-surface-plugins/panel-app-reference-plugin.utils.ts +50 -14
- package/src/features/chat/features/input/input-surface-plugins/slash-command-plugin.utils.ts +77 -6
- package/src/features/chat/features/message/components/__tests__/chat-inline-panel-app-card.test.tsx +21 -12
- package/src/features/chat/features/message/components/__tests__/chat-message-list.container.test.tsx +167 -1
- package/src/features/chat/features/message/components/chat-inline-panel-app-card.tsx +19 -11
- package/src/features/chat/features/message/components/chat-message-list.container.tsx +38 -64
- package/src/features/chat/features/message/utils/__tests__/chat-message-process-summary.utils.test.ts +59 -0
- package/src/features/chat/features/message/utils/__tests__/chat-message-show-content-tool-card.utils.test.ts +56 -1
- package/src/features/chat/features/message/utils/chat-inline-panel-app-card.utils.ts +0 -2
- package/src/features/chat/features/message/utils/chat-message-process-summary.utils.ts +80 -0
- package/src/features/chat/features/message/utils/chat-message-show-content-tool-card.utils.ts +12 -3
- package/src/features/chat/features/message/utils/chat-message-texts.utils.ts +60 -0
- package/src/features/chat/features/message/utils/chat-message.utils.ts +3 -0
- package/src/features/chat/features/ncp/hooks/__tests__/use-ncp-agent-runtime.test.tsx +62 -1
- package/src/features/chat/features/ncp/hooks/__tests__/use-ncp-session-list-view.test.tsx +74 -0
- package/src/features/chat/features/ncp/hooks/use-ncp-session-list-view.ts +3 -2
- package/src/features/chat/features/session/components/chat-sidebar-project-groups.tsx +2 -2
- package/src/features/chat/features/session/components/chat-sidebar-session-item.tsx +1 -1
- package/src/features/chat/features/session/components/session-header/__tests__/chat-session-header-actions.test.tsx +3 -1
- package/src/features/chat/features/session/components/session-header/chat-session-metadata-dialog.tsx +1 -1
- package/src/features/chat/features/session/components/session-header/chat-session-title-switcher.tsx +294 -0
- package/src/features/chat/features/session/utils/__tests__/chat-session-display.utils.test.ts +15 -1
- package/src/features/chat/features/session/utils/__tests__/ncp-session-adapter.utils.test.ts +24 -4
- package/src/features/chat/features/session/utils/chat-session-display.utils.ts +3 -0
- package/src/features/chat/features/session/utils/ncp-session-adapter.utils.ts +1 -0
- package/src/features/chat/features/workspace/components/__tests__/chat-session-workspace-file-preview.test.tsx +304 -115
- package/src/features/chat/features/workspace/components/chat-session-workspace-file-breadcrumb-browser.tsx +121 -0
- package/src/features/chat/features/workspace/components/chat-session-workspace-file-breadcrumbs.tsx +75 -20
- package/src/features/chat/features/workspace/components/chat-session-workspace-file-preview.tsx +64 -5
- package/src/features/chat/features/workspace/utils/__tests__/chat-workspace-panel-view-model.utils.test.ts +46 -0
- package/src/features/chat/features/workspace/utils/chat-workspace-file-tab-persistence.utils.ts +83 -0
- package/src/features/chat/managers/__tests__/chat-thread.manager.test.ts +126 -1
- package/src/features/chat/managers/__tests__/chat-ui.manager.test.ts +2 -0
- package/src/features/chat/managers/chat-thread.manager.ts +15 -3
- package/src/features/chat/managers/chat-ui.manager.ts +5 -1
- package/src/features/chat/presenters/chat.presenter.ts +2 -1
- package/src/features/chat/stores/__tests__/chat-thread.store.test.ts +4 -0
- package/src/features/chat/stores/chat-thread.store.ts +6 -64
- package/src/features/chat/types/chat-message.types.ts +4 -0
- package/src/features/marketplace/components/__tests__/marketplace-page-detail.test.tsx +33 -26
- package/src/features/marketplace/components/__tests__/marketplace-page.test.tsx +1 -0
- package/src/features/marketplace/components/curated-shelves/__tests__/marketplace-curated-scene-route.test.tsx +1 -0
- package/src/features/marketplace/components/detail-doc/__tests__/marketplace-detail-doc.test.tsx +115 -0
- package/src/features/marketplace/components/marketplace-detail-doc.tsx +441 -0
- package/src/features/marketplace/components/mcp/__tests__/mcp-marketplace-page.test.tsx +3 -3
- package/src/features/marketplace/components/mcp/mcp-marketplace-doc.ts +0 -60
- package/src/features/marketplace/components/mcp/mcp-marketplace-page.tsx +64 -29
- package/src/features/marketplace/hooks/use-marketplace-item-detail.ts +42 -31
- package/src/features/marketplace/index.ts +3 -0
- package/src/features/marketplace/stores/marketplace-detail-doc.store.ts +38 -0
- package/src/features/panel-apps/components/__tests__/panel-apps-list.test.tsx +32 -2
- package/src/features/pwa/managers/__tests__/pwa-shell-theme.manager.test.ts +11 -0
- package/src/features/pwa/managers/pwa-shell-theme.manager.ts +8 -3
- package/src/features/settings/pages/__tests__/appearance-settings-page.test.tsx +25 -0
- package/src/features/settings/pages/appearance-settings-page.tsx +40 -0
- package/src/features/side-dock/components/__tests__/side-dock.test.tsx +44 -2
- package/src/features/side-dock/components/side-dock.tsx +100 -13
- package/src/features/side-dock/configs/side-dock-built-in-items.config.ts +10 -0
- package/src/features/side-dock/managers/__tests__/side-dock.manager.test.ts +32 -2
- package/src/features/side-dock/managers/side-dock.manager.ts +17 -7
- package/src/features/side-dock/stores/side-dock.store.ts +9 -0
- package/src/features/side-dock/types/side-dock.types.ts +10 -3
- package/src/features/side-dock/utils/side-dock-item.utils.ts +15 -2
- package/src/features/system-status/components/__tests__/desktop-update-config.test.tsx +97 -5
- package/src/features/system-status/components/desktop-update-config.tsx +91 -2
- package/src/features/system-status/utils/update-release-notes.utils.ts +94 -0
- package/src/index.css +501 -27
- package/src/platforms/desktop/components/desktop-window-chrome.tsx +1 -1
- package/src/shared/components/doc-browser/__tests__/doc-browser-context.test.tsx +30 -1
- package/src/shared/components/doc-browser/__tests__/doc-browser.test.tsx +206 -38
- package/src/shared/components/doc-browser/doc-browser-panel-parts.tsx +34 -11
- package/src/shared/components/doc-browser/doc-browser-renderer.types.ts +1 -1
- package/src/shared/components/doc-browser/doc-browser-tab-strip.tsx +23 -5
- package/src/shared/components/doc-browser/doc-browser.tsx +128 -75
- package/src/shared/components/doc-browser/managers/doc-browser.manager.ts +9 -1
- package/src/shared/components/doc-browser/utils/doc-browser-floating-panel.utils.ts +83 -0
- package/src/shared/components/ui/__tests__/dialog.test.tsx +27 -0
- package/src/shared/components/ui/actions/icon-action-button.tsx +14 -2
- package/src/shared/components/ui/dialog.tsx +2 -2
- package/src/shared/components/ui/popover.tsx +1 -1
- package/src/shared/components/ui/select.tsx +1 -1
- package/src/shared/components/ui/switch.tsx +4 -3
- package/src/shared/components/ui/tab-strip/__tests__/compact-tab-strip.test.tsx +54 -0
- package/src/shared/components/ui/tab-strip/compact-tab-strip.tsx +4 -5
- package/src/shared/components/ui/tooltip.tsx +1 -1
- package/src/shared/lib/api/ncp-session.types.ts +1 -1
- package/src/shared/lib/api/types.ts +12 -3
- package/src/shared/lib/api/utils/server-path.utils.ts +27 -1
- package/src/shared/lib/i18n/locales/en-US/agents.json +41 -0
- package/src/shared/lib/i18n/locales/en-US/chat.json +18 -6
- package/src/shared/lib/i18n/locales/en-US/core.json +2 -0
- package/src/shared/lib/i18n/locales/en-US/desktop-update.json +8 -0
- package/src/shared/lib/i18n/locales/en-US/doc-browser.json +11 -0
- package/src/shared/lib/i18n/locales/en-US/marketplace.json +5 -0
- package/src/shared/lib/i18n/locales/zh-CN/agents.json +41 -0
- package/src/shared/lib/i18n/locales/zh-CN/chat.json +18 -6
- package/src/shared/lib/i18n/locales/zh-CN/core.json +2 -0
- package/src/shared/lib/i18n/locales/zh-CN/desktop-update.json +8 -0
- package/src/shared/lib/i18n/locales/zh-CN/doc-browser.json +11 -0
- package/src/shared/lib/i18n/locales/zh-CN/marketplace.json +5 -0
- package/src/shared/lib/session-project/__tests__/workspace-file-breadcrumb.test.ts +14 -0
- package/src/shared/lib/session-project/index.ts +1 -1
- package/src/shared/lib/session-project/{workspace-file-breadcrumb.ts → workspace-file-breadcrumb.utils.ts} +62 -8
- package/src/shared/lib/theme/index.ts +51 -13
- package/dist/assets/api-BP_8IaxB.js +0 -13
- package/dist/assets/channels-list-page-DEh5Jjqx.js +0 -8
- package/dist/assets/chat-page-BRB0N5WW.js +0 -105
- package/dist/assets/confirm-dialog-DsivFJ4r.js +0 -5
- package/dist/assets/desktop-update-config-B0w9p2ep.js +0 -1
- package/dist/assets/doc-browser-BoNJ7-tE.js +0 -1
- package/dist/assets/doc-browser-CdJ_d4ik.js +0 -1
- package/dist/assets/ellipsis-y6vDtq4M.js +0 -1
- package/dist/assets/index-BiE-K8-q.css +0 -1
- package/dist/assets/index-D9BAE_L_.js +0 -105
- package/dist/assets/mcp-marketplace-page-B1SZNY5m.js +0 -40
- package/dist/assets/mcp-marketplace-page-Cq_Tw42T.js +0 -1
- package/dist/assets/model-config-page-DotaTUHb.js +0 -1
- package/dist/assets/plus-BbZqquJe.js +0 -1
- package/dist/assets/provider-scoped-model-input-DsFRuWJH.js +0 -1
- package/dist/assets/providers-config-page-DW4tQSLN.js +0 -1
- package/dist/assets/remote-B8TiUfwW.js +0 -1
- package/dist/assets/rotate-cw-DjHDTkCv.js +0 -1
- package/dist/assets/runtime-config-page-S6ErRHR8.js +0 -1
- package/dist/assets/search-config-page-DGj5am03.js +0 -1
- package/dist/assets/select-hoj8dK4K.js +0 -41
- package/dist/assets/tag-chip-T8-h-die.js +0 -1
- package/dist/assets/use-config-CjRV2hwL.js +0 -1
- package/dist/assets/x-D9PXs_xP.js +0 -1
- package/src/features/marketplace/components/detail-doc/__tests__/marketplace-detail-doc.test.ts +0 -40
- package/src/features/marketplace/components/detail-doc/marketplace-detail-doc-renderer.ts +0 -201
- package/src/features/marketplace/components/marketplace-detail-doc.ts +0 -142
- /package/dist/assets/{config-hints-BkYBc7z0.js → config-hints-fGnUjDe9.js} +0 -0
- /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) =>
|
|
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
|
|
3
|
+
export type SideDockResourceItemTarget = {
|
|
4
4
|
type: 'right-panel-resource';
|
|
5
5
|
uri: string;
|
|
6
6
|
};
|
|
7
7
|
|
|
8
|
-
export type
|
|
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:
|
|
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(
|
|
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(
|
|
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 {
|
|
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:
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
+
}
|