@nextclaw/ui 0.15.5 → 0.15.6
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 +24 -0
- package/dist/assets/api-Dyb5rgh0.js +13 -0
- package/dist/assets/app-presenter-provider-7jV0nPiL.js +106 -0
- package/dist/assets/{appearance-settings-page-80fLa4-9.js → appearance-settings-page-DFm3BbEb.js} +1 -1
- package/dist/assets/{book-open-CN7Shpij.js → book-open-L6LCuqOr.js} +1 -1
- package/dist/assets/{channels-list-page-R7M6pzp6.js → channels-list-page-DOaPKNJA.js} +2 -2
- package/dist/assets/{chat-page-152YuwfU.js → chat-page-Cw9uDhD_.js} +1 -1
- package/dist/assets/{config-split-page-BlfvfmTm.js → config-split-page-CqlqjnCK.js} +1 -1
- package/dist/assets/{confirm-dialog-Dt8Kdbgp.js → confirm-dialog-bAp8NU93.js} +1 -1
- package/dist/assets/desktop-update-config-2SjMfCMl.js +1 -0
- package/dist/assets/{dist-D5ycbwCo.js → dist-CrtbGLlh.js} +1 -1
- package/dist/assets/doc-browser-A5IRyDXQ2.js +1 -0
- package/dist/assets/{doc-browser-C9u7N9vH.js → doc-browser-BmnQa30-.js} +1 -1
- package/dist/assets/{doc-browser-context-CCdW9TJf.js → doc-browser-context-BlpUUFrR.js} +1 -1
- package/dist/assets/{download-CyPgglKr.js → download-BhZZNXD8.js} +1 -1
- package/dist/assets/{external-link-BahW1sl3.js → external-link-B9Uoq5J7.js} +1 -1
- package/dist/assets/{eye-D8gGXMkD.js → eye-K5knauEM.js} +1 -1
- package/dist/assets/{folder-DGWqFDvE.js → folder-BIYZZWJv.js} +1 -1
- package/dist/assets/{form-actions-D2imZLnU.js → form-actions-CKprWFeM.js} +1 -1
- package/dist/assets/index-BVn702m4.css +1 -0
- package/dist/assets/{index-CT0iYlxV.js → index-CFxJ1Fkg.js} +2 -2
- package/dist/assets/loader-circle-Bj2RMrCR.js +1 -0
- package/dist/assets/{logo-badge-CuPetu-v.js → logo-badge-BQ-XFZod.js} +1 -1
- package/dist/assets/mcp-marketplace-page-BfA7A3n4.js +9 -0
- package/dist/assets/mcp-marketplace-page-CXHYO95j.js +1 -0
- package/dist/assets/{model-config-page-DiJcvBML.js → model-config-page-BbWbW_qM.js} +1 -1
- package/dist/assets/plus-TmFdwO3P.js +1 -0
- package/dist/assets/{popover-Btp7KVc3.js → popover-CX9zFsuc.js} +1 -1
- package/dist/assets/{provider-scoped-model-input-DZMtoAJG.js → provider-scoped-model-input-B-9_DtIP.js} +1 -1
- package/dist/assets/providers-config-page-xwIjGur2.js +1 -0
- package/dist/assets/{react-BY-U3h3t.js → react-DsgudwpT.js} +1 -1
- package/dist/assets/{refresh-cw-Cme5qFej.js → refresh-cw-kXdvbhFf.js} +1 -1
- package/dist/assets/remote-B9F2uCk9.js +1 -0
- package/dist/assets/rotate-cw-DEc6aihr.js +1 -0
- package/dist/assets/runtime-config-page-DTsi7czJ.js +1 -0
- package/dist/assets/{save-DNg2yJTS.js → save-C9qpz2_2.js} +1 -1
- package/dist/assets/{search-3ykfrAFt.js → search-_kEi29qo.js} +1 -1
- package/dist/assets/{search-config-page-DrVKa_Ls.js → search-config-page-H7ug8f4D.js} +1 -1
- package/dist/assets/{secrets-config-page-B39uQ5kF.js → secrets-config-page-C1_6rINu.js} +2 -2
- package/dist/assets/security-config-Cr2ERKLu.js +1 -0
- package/dist/assets/{select-B5EecXJt.js → select-MJKhKAWR.js} +1 -1
- package/dist/assets/{setting-row-Qv1Ps79v.js → setting-row-DA3tJuew.js} +1 -1
- package/dist/assets/{settings-2-D-3um9Az.js → settings-2-D_y7zXJz.js} +1 -1
- package/dist/assets/{skeleton-BgkzQd4U.js → skeleton-0uTLUhm9.js} +1 -1
- package/dist/assets/{tabs-BVD8gp3y.js → tabs-h44XPocd.js} +1 -1
- package/dist/assets/{tag-chip-0qn-f3Bo.js → tag-chip-bRZ04q3C.js} +1 -1
- package/dist/assets/{trash-2-cnsMppRC.js → trash-2-BRpSE-Wt.js} +1 -1
- package/dist/assets/use-config-CwDEAl6f.js +1 -0
- package/dist/assets/{use-confirm-dialog-C_o0eQeH.js → use-confirm-dialog-BbnWw-8c.js} +1 -1
- package/dist/assets/{use-viewport-layout-BWav4KCL.js → use-viewport-layout-CqgIMDQL.js} +1 -1
- package/dist/assets/x-DdEDL6ju.js +1 -0
- package/dist/index.html +29 -29
- package/package.json +8 -8
- package/src/features/chat/components/conversation/__tests__/chat-conversation-header-section.test.tsx +84 -1
- package/src/features/chat/components/conversation/chat-conversation-header-section.tsx +13 -2
- package/src/features/chat/components/layout/__tests__/chat-sidebar-test.utils.ts +22 -0
- package/src/features/chat/components/layout/__tests__/chat-sidebar.test.tsx +13 -24
- package/src/features/chat/components/layout/chat-sidebar-desktop-layout.tsx +16 -4
- package/src/features/chat/components/layout/chat-sidebar.tsx +38 -2
- package/src/features/chat/features/conversation/components/__tests__/session-conversation-area.test.tsx +1 -0
- package/src/features/chat/features/conversation/components/__tests__/session-conversation-input.streaming.test.tsx +52 -2
- package/src/features/chat/features/conversation/components/session-conversation-area.tsx +22 -0
- package/src/features/chat/features/conversation/components/session-conversation-input.tsx +8 -9
- package/src/features/chat/features/conversation/hooks/__tests__/use-session-conversation-input-state.test.tsx +55 -0
- package/src/features/chat/features/conversation/hooks/use-session-conversation-input-state.ts +12 -11
- package/src/features/chat/features/conversation/hooks/use-session-conversation-preference-actions.ts +94 -0
- package/src/features/chat/features/input/hooks/__tests__/use-chat-input-surface-state.test.tsx +37 -0
- package/src/features/chat/features/input/hooks/use-chat-input-surface-state.ts +140 -63
- package/src/features/chat/features/input/input-surface-plugins/__tests__/context-reference-plugin.test.ts +175 -0
- package/src/features/chat/features/input/input-surface-plugins/__tests__/slash-command-plugin.test.ts +23 -0
- package/src/features/chat/features/input/input-surface-plugins/chat-input-product-plugin-adapters.types.ts +8 -1
- package/src/features/chat/features/input/input-surface-plugins/context-reference-plugin.utils.ts +274 -0
- package/src/features/chat/features/input/input-surface-plugins/slash-command-plugin.utils.ts +4 -1
- package/src/features/chat/features/input/utils/__tests__/chat-composer-state.utils.test.ts +26 -0
- package/src/features/chat/features/input/utils/__tests__/chat-inline-token.utils.test.ts +50 -4
- package/src/features/chat/features/input/utils/chat-composer-state.utils.ts +4 -10
- package/src/features/chat/features/input/utils/chat-composer-token-protocol.utils.ts +29 -0
- package/src/features/chat/features/input/utils/chat-inline-token.utils.ts +66 -8
- package/src/features/chat/features/input/utils/chat-input-bar.utils.ts +1 -0
- package/src/features/chat/features/ncp/hooks/__tests__/use-hydrated-ncp-agent.test.tsx +41 -3
- package/src/features/chat/features/project/components/__tests__/chat-project-create-dialog.test.tsx +67 -0
- package/src/features/chat/features/project/components/chat-project-create-dialog.tsx +203 -0
- package/src/features/chat/features/session/components/session-header/__tests__/chat-session-header-actions.test.tsx +7 -7
- package/src/features/chat/features/session/components/session-header/__tests__/chat-session-project-dialog.test.tsx +47 -0
- package/src/features/chat/features/session/components/session-header/chat-session-header-actions.tsx +6 -3
- package/src/features/chat/features/session/components/session-header/chat-session-project-badge.tsx +3 -0
- package/src/features/chat/features/session/components/session-header/chat-session-project-dialog.tsx +3 -0
- package/src/features/chat/features/session/components/session-header/chat-session-switcher-groups.tsx +245 -0
- package/src/features/chat/features/session/components/session-header/chat-session-title-switcher.tsx +81 -127
- package/src/features/chat/features/session/utils/__tests__/chat-sidebar-session-groups.utils.test.ts +22 -0
- package/src/features/chat/features/session/utils/chat-run-metadata.utils.ts +6 -6
- package/src/features/chat/features/session/utils/chat-sidebar-session-groups.utils.ts +12 -1
- package/src/features/chat/features/welcome/components/chat-conversation-welcome.tsx +3 -0
- package/src/features/chat/features/welcome/components/chat-welcome.tsx +1 -0
- package/src/features/chat/features/welcome/utils/__tests__/chat-welcome-project-options.utils.test.ts +20 -0
- package/src/features/chat/features/welcome/utils/chat-welcome-project-options.utils.ts +15 -2
- package/src/features/chat/pages/__tests__/ncp-chat-page.test.ts +23 -0
- package/src/features/settings/components/config/provider-form.tsx +0 -4
- package/src/features/settings/utils/__tests__/provider-form-support.utils.test.ts +7 -9
- package/src/features/settings/utils/provider-form-model.utils.ts +0 -4
- package/src/features/system-status/hooks/use-current-version-release-notes-link.ts +33 -0
- package/src/features/system-status/index.ts +9 -0
- package/src/features/system-status/utils/update-release-notes.utils.ts +44 -3
- package/src/index.css +10 -16
- package/src/shared/components/common/__tests__/brand-header.test.tsx +110 -3
- package/src/shared/components/common/brand-header.tsx +121 -35
- package/src/shared/components/path-picker/__tests__/server-path-picker-dialog.test.tsx +288 -38
- package/src/shared/components/path-picker/server-path-picker-dialog.tsx +290 -199
- package/src/shared/components/path-picker/server-path-picker-directory-list.tsx +103 -0
- package/src/shared/components/path-picker/server-path-picker-footer.tsx +58 -0
- package/src/shared/components/path-picker/server-path-picker-locations.tsx +148 -0
- package/src/shared/components/path-picker/server-path-picker-navigation-bar.tsx +198 -0
- package/src/shared/components/path-picker/server-path-picker-new-folder.tsx +59 -0
- package/src/shared/components/path-picker/server-path-picker-toolbar.tsx +25 -0
- package/src/shared/hooks/use-projects.ts +28 -0
- package/src/shared/hooks/use-server-path-create-directory.ts +6 -0
- package/src/shared/hooks/use-server-path-search.ts +40 -0
- package/src/shared/lib/api/index.ts +1 -0
- package/src/shared/lib/api/projects/project.utils.ts +14 -0
- package/src/shared/lib/api/types.ts +27 -0
- package/src/shared/lib/api/utils/server-path.utils.ts +21 -1
- package/src/shared/lib/i18n/locales/en-US/chat.json +35 -2
- package/src/shared/lib/i18n/locales/en-US/core.json +2 -3
- package/src/shared/lib/i18n/locales/en-US/desktop-update.json +4 -0
- package/src/shared/lib/i18n/locales/en-US/path-picker.json +25 -0
- package/src/shared/lib/i18n/locales/zh-CN/chat.json +35 -2
- package/src/shared/lib/i18n/locales/zh-CN/core.json +2 -3
- package/src/shared/lib/i18n/locales/zh-CN/desktop-update.json +4 -0
- package/src/shared/lib/i18n/locales/zh-CN/path-picker.json +25 -0
- package/dist/assets/api-IlX_CbwU.js +0 -13
- package/dist/assets/app-presenter-provider-Bbl_MVbg.js +0 -106
- package/dist/assets/desktop-update-config-DreMJu9z.js +0 -1
- package/dist/assets/doc-browser-DJQ5XPsi2.js +0 -1
- package/dist/assets/index-0av6WO9-.css +0 -1
- package/dist/assets/loader-circle-DtKek-qG.js +0 -1
- package/dist/assets/mcp-marketplace-page-4qMwFDpz.js +0 -1
- package/dist/assets/mcp-marketplace-page-BbxN5aZF.js +0 -9
- package/dist/assets/plus-BwgE-Zwf.js +0 -1
- package/dist/assets/providers-config-page-DfiWveEF.js +0 -1
- package/dist/assets/remote-BaVECEZw.js +0 -1
- package/dist/assets/rotate-cw-CMRZxIkH.js +0 -1
- package/dist/assets/runtime-config-page-wygwvMB4.js +0 -1
- package/dist/assets/security-config-BPzet76z.js +0 -1
- package/dist/assets/use-config-D6yx0lb0.js +0 -1
- package/dist/assets/x-B3o1dWR5.js +0 -1
- package/src/features/chat/features/input/input-surface-plugins/__tests__/panel-app-reference-plugin.test.ts +0 -77
- package/src/features/chat/features/input/input-surface-plugins/panel-app-reference-plugin.utils.ts +0 -144
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { NcpSessionSummaryView } from "@/shared/lib/api";
|
|
1
|
+
import type { NcpSessionSummaryView, ProjectView } from "@/shared/lib/api";
|
|
2
2
|
import { adaptNcpSessionSummaries } from "@/features/chat/features/session/utils/ncp-session-adapter.utils";
|
|
3
3
|
import { getSessionProjectName } from "@/shared/lib/session-project";
|
|
4
4
|
|
|
@@ -17,14 +17,27 @@ function readSessionActivityAt(session: {
|
|
|
17
17
|
|
|
18
18
|
export function buildChatWelcomeProjectOptions(params: {
|
|
19
19
|
defaultProjectRoot: string | null;
|
|
20
|
+
projects?: readonly ProjectView[];
|
|
20
21
|
sessionSummaries: readonly NcpSessionSummaryView[];
|
|
21
22
|
}): ChatWelcomeProjectOption[] {
|
|
22
|
-
const { defaultProjectRoot, sessionSummaries } = params;
|
|
23
|
+
const { defaultProjectRoot, projects = [], sessionSummaries } = params;
|
|
23
24
|
const groups = new Map<
|
|
24
25
|
string,
|
|
25
26
|
ChatWelcomeProjectOption & { latestUpdatedAt: number }
|
|
26
27
|
>();
|
|
27
28
|
|
|
29
|
+
for (const project of projects) {
|
|
30
|
+
if (project.rootPath === defaultProjectRoot) {
|
|
31
|
+
continue;
|
|
32
|
+
}
|
|
33
|
+
groups.set(project.rootPath, {
|
|
34
|
+
projectRoot: project.rootPath,
|
|
35
|
+
projectName: project.name,
|
|
36
|
+
sessionCount: 0,
|
|
37
|
+
latestUpdatedAt: new Date(project.updatedAt).getTime(),
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
|
|
28
41
|
for (const session of adaptNcpSessionSummaries([...sessionSummaries])) {
|
|
29
42
|
const projectRoot = session.projectRoot?.trim();
|
|
30
43
|
if (!projectRoot || projectRoot === defaultProjectRoot) {
|
|
@@ -215,6 +215,29 @@ describe('buildChatRunMetadata', () => {
|
|
|
215
215
|
).not.toHaveProperty('ui_inline_tokens');
|
|
216
216
|
});
|
|
217
217
|
|
|
218
|
+
it('adds structured workspace references to run metadata', () => {
|
|
219
|
+
expect(
|
|
220
|
+
buildChatRunMetadata({
|
|
221
|
+
composerNodes: [
|
|
222
|
+
createChatComposerTokenNode({
|
|
223
|
+
tokenKind: 'workspace_file',
|
|
224
|
+
tokenKey: 'src/file name.ts',
|
|
225
|
+
label: 'file name.ts',
|
|
226
|
+
}),
|
|
227
|
+
],
|
|
228
|
+
}),
|
|
229
|
+
).toMatchObject({
|
|
230
|
+
ui_inline_tokens: [
|
|
231
|
+
{
|
|
232
|
+
kind: 'workspace_file',
|
|
233
|
+
key: 'src/file name.ts',
|
|
234
|
+
label: 'file name.ts',
|
|
235
|
+
rawText: '@file:src%2Ffile%20name.ts',
|
|
236
|
+
},
|
|
237
|
+
],
|
|
238
|
+
});
|
|
239
|
+
});
|
|
240
|
+
|
|
218
241
|
it('serializes child session materialization metadata for side chat drafts', () => {
|
|
219
242
|
expect(
|
|
220
243
|
buildChatRunMetadata({
|
|
@@ -230,10 +230,6 @@ export function ProviderForm({ providerName, onProviderDeleted }: ProviderFormPr
|
|
|
230
230
|
|
|
231
231
|
const handleAddModel = () => {
|
|
232
232
|
const result = addProviderLocalModel(models, modelDraft, providerModelAliases);
|
|
233
|
-
if (result.errorKey) {
|
|
234
|
-
toast.error(t('providerModelInvalidProviderPrefix'));
|
|
235
|
-
return;
|
|
236
|
-
}
|
|
237
233
|
setModels(result.models);
|
|
238
234
|
setModelDraft(result.draft);
|
|
239
235
|
};
|
|
@@ -22,10 +22,9 @@ describe('provider form model defaults', () => {
|
|
|
22
22
|
});
|
|
23
23
|
|
|
24
24
|
it('serializes local model ids with the providerId prefix', () => {
|
|
25
|
-
expect(
|
|
26
|
-
'openai-work/gpt-5.5',
|
|
27
|
-
|
|
28
|
-
]);
|
|
25
|
+
expect(
|
|
26
|
+
serializeModelsForSave(['gpt-5.5', 'openai-work/gpt-5.4', 'bedrock/claude-fable-5'], 'openai-work')
|
|
27
|
+
).toEqual(['openai-work/gpt-5.5', 'openai-work/gpt-5.4', 'openai-work/bedrock/claude-fable-5']);
|
|
29
28
|
});
|
|
30
29
|
|
|
31
30
|
it('builds a minimal save payload from changed provider fields', () => {
|
|
@@ -57,15 +56,14 @@ describe('provider form model defaults', () => {
|
|
|
57
56
|
});
|
|
58
57
|
});
|
|
59
58
|
|
|
60
|
-
it('adds local model
|
|
59
|
+
it('adds provider-local model ids with nested namespace segments', () => {
|
|
61
60
|
expect(addProviderLocalModel([], ' gpt-5.5 ', ['openai-work'])).toEqual({
|
|
62
61
|
models: ['gpt-5.5'],
|
|
63
62
|
draft: ''
|
|
64
63
|
});
|
|
65
|
-
expect(addProviderLocalModel([], '
|
|
66
|
-
models: [],
|
|
67
|
-
draft: '
|
|
68
|
-
errorKey: 'providerModelInvalidProviderPrefix'
|
|
64
|
+
expect(addProviderLocalModel([], ' bedrock/claude-fable-5 ', ['openrouter'])).toEqual({
|
|
65
|
+
models: ['bedrock/claude-fable-5'],
|
|
66
|
+
draft: ''
|
|
69
67
|
});
|
|
70
68
|
});
|
|
71
69
|
|
|
@@ -8,7 +8,6 @@ import {
|
|
|
8
8
|
type AddProviderModelResult = {
|
|
9
9
|
models: string[];
|
|
10
10
|
draft: string;
|
|
11
|
-
errorKey?: 'providerModelInvalidProviderPrefix';
|
|
12
11
|
};
|
|
13
12
|
|
|
14
13
|
export function addProviderLocalModel(models: string[], draft: string, aliases: string[]): AddProviderModelResult {
|
|
@@ -16,9 +15,6 @@ export function addProviderLocalModel(models: string[], draft: string, aliases:
|
|
|
16
15
|
if (!next) {
|
|
17
16
|
return { models, draft };
|
|
18
17
|
}
|
|
19
|
-
if (next.includes('/')) {
|
|
20
|
-
return { models, draft, errorKey: 'providerModelInvalidProviderPrefix' };
|
|
21
|
-
}
|
|
22
18
|
if (models.includes(next)) {
|
|
23
19
|
return { models, draft: '' };
|
|
24
20
|
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { useQuery } from '@tanstack/react-query';
|
|
2
|
+
import { getLanguage } from '@/shared/lib/i18n';
|
|
3
|
+
import {
|
|
4
|
+
fetchReleaseNotesData,
|
|
5
|
+
resolveReleaseNotesHtmlUrl,
|
|
6
|
+
resolveVersionReleaseNotesDataUrl,
|
|
7
|
+
type ReleaseNotesLink,
|
|
8
|
+
type ReleaseNotesLocale
|
|
9
|
+
} from '@/features/system-status/utils/update-release-notes.utils';
|
|
10
|
+
|
|
11
|
+
function getReleaseNotesLocale(): ReleaseNotesLocale {
|
|
12
|
+
return getLanguage() === 'zh' ? 'zh-CN' : 'en-US';
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function useCurrentVersionReleaseNotesLink(productVersion: string | undefined): ReleaseNotesLink | null {
|
|
16
|
+
const dataUrl = resolveVersionReleaseNotesDataUrl(productVersion);
|
|
17
|
+
const locale = getReleaseNotesLocale();
|
|
18
|
+
const releaseNotesQuery = useQuery({
|
|
19
|
+
queryKey: ['current-release-notes', dataUrl],
|
|
20
|
+
queryFn: async () => await fetchReleaseNotesData(dataUrl ?? ''),
|
|
21
|
+
enabled: Boolean(dataUrl),
|
|
22
|
+
retry: false,
|
|
23
|
+
staleTime: 5 * 60 * 1000
|
|
24
|
+
});
|
|
25
|
+
const url = resolveReleaseNotesHtmlUrl(releaseNotesQuery.data, locale);
|
|
26
|
+
const version = productVersion?.trim();
|
|
27
|
+
return url && version
|
|
28
|
+
? {
|
|
29
|
+
url,
|
|
30
|
+
versionLabel: `v${version}`
|
|
31
|
+
}
|
|
32
|
+
: null;
|
|
33
|
+
}
|
|
@@ -2,7 +2,16 @@ export { useRuntimeControlPanelView, useRuntimeStatusBadgeView, useSystemStatus,
|
|
|
2
2
|
export { systemStatusManager } from './managers/system-status.manager';
|
|
3
3
|
export { isTransientRuntimeConnectionErrorMessage } from '@/shared/lib/transport';
|
|
4
4
|
export { runtimeUpdateManager } from './managers/runtime-update.manager';
|
|
5
|
+
export { useCurrentVersionReleaseNotesLink } from './hooks/use-current-version-release-notes-link';
|
|
6
|
+
export {
|
|
7
|
+
fetchReleaseNotesData,
|
|
8
|
+
resolveReleaseNotesHtmlUrl,
|
|
9
|
+
resolveUpdateReleaseNotesLink,
|
|
10
|
+
resolveVersionReleaseNotesDataUrl,
|
|
11
|
+
} from './utils/update-release-notes.utils';
|
|
5
12
|
export type { SystemStatusState, SystemStatusView } from './types/system-status.types';
|
|
6
13
|
export { useSystemStatusStore } from './stores/system-status.store';
|
|
7
14
|
export { useRuntimeUpdateStore } from './stores/runtime-update.store';
|
|
15
|
+
export type { RuntimeUpdateBusyAction } from './stores/runtime-update.store';
|
|
16
|
+
export type { ReleaseNotesLink, ReleaseNotesLocale } from './utils/update-release-notes.utils';
|
|
8
17
|
export { SecurityConfig } from './components/security-config';
|
|
@@ -4,11 +4,20 @@ export type ReleaseNotesLocale = 'zh-CN' | 'en-US';
|
|
|
4
4
|
|
|
5
5
|
export type ReleaseNotesText = Partial<Record<ReleaseNotesLocale, string>>;
|
|
6
6
|
|
|
7
|
+
export type ReleaseNotesLink = {
|
|
8
|
+
url: string;
|
|
9
|
+
versionLabel: string;
|
|
10
|
+
};
|
|
11
|
+
|
|
7
12
|
export type ReleaseNotesItem = {
|
|
8
13
|
title: ReleaseNotesText;
|
|
9
14
|
body?: ReleaseNotesText;
|
|
10
15
|
};
|
|
11
16
|
|
|
17
|
+
export type ReleaseNotesLinks = {
|
|
18
|
+
html?: ReleaseNotesText;
|
|
19
|
+
};
|
|
20
|
+
|
|
12
21
|
export type ReleaseNotesSection = {
|
|
13
22
|
kind: string;
|
|
14
23
|
title?: ReleaseNotesText;
|
|
@@ -18,9 +27,12 @@ export type ReleaseNotesSection = {
|
|
|
18
27
|
export type ReleaseNotesPayload = {
|
|
19
28
|
title?: ReleaseNotesText;
|
|
20
29
|
summary?: ReleaseNotesText;
|
|
30
|
+
links?: ReleaseNotesLinks;
|
|
21
31
|
sections: ReleaseNotesSection[];
|
|
22
32
|
};
|
|
23
33
|
|
|
34
|
+
const DEFAULT_RELEASE_NOTES_BASE_URL = 'https://docs.nextclaw.io/';
|
|
35
|
+
|
|
24
36
|
export function readReleaseNotesText(
|
|
25
37
|
value: ReleaseNotesText | undefined,
|
|
26
38
|
locale: ReleaseNotesLocale
|
|
@@ -33,17 +45,40 @@ export function readReleaseNotesText(
|
|
|
33
45
|
export function resolveReleaseNotesDataUrl(snapshot: UpdateSnapshot): string | null {
|
|
34
46
|
const releaseNotesUrl = snapshot.releaseNotesUrl?.trim();
|
|
35
47
|
const version = (snapshot.availableVersion ?? snapshot.downloadedVersion)?.trim();
|
|
36
|
-
if (!releaseNotesUrl
|
|
48
|
+
if (!releaseNotesUrl) {
|
|
37
49
|
return null;
|
|
38
50
|
}
|
|
51
|
+
return resolveVersionReleaseNotesDataUrl(version, releaseNotesUrl);
|
|
52
|
+
}
|
|
39
53
|
|
|
54
|
+
export function resolveVersionReleaseNotesDataUrl(version: string | null | undefined, baseUrl = DEFAULT_RELEASE_NOTES_BASE_URL): string | null {
|
|
55
|
+
const normalizedVersion = version?.trim();
|
|
56
|
+
if (!normalizedVersion || !/^[0-9A-Za-z.-]+$/.test(normalizedVersion)) {
|
|
57
|
+
return null;
|
|
58
|
+
}
|
|
40
59
|
try {
|
|
41
|
-
return new URL(`/release-notes/nextclaw-v${
|
|
60
|
+
return new URL(`/release-notes/nextclaw-v${normalizedVersion}.json`, baseUrl).toString();
|
|
42
61
|
} catch {
|
|
43
62
|
return null;
|
|
44
63
|
}
|
|
45
64
|
}
|
|
46
65
|
|
|
66
|
+
export function resolveReleaseNotesHtmlUrl(payload: ReleaseNotesPayload | undefined, locale: ReleaseNotesLocale): string | null {
|
|
67
|
+
return readReleaseNotesText(payload?.links?.html, locale);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export function resolveUpdateReleaseNotesLink(snapshot: UpdateSnapshot): ReleaseNotesLink | null {
|
|
71
|
+
const url = snapshot.releaseNotesUrl?.trim();
|
|
72
|
+
const version = (snapshot.downloadedVersion ?? snapshot.availableVersion)?.trim();
|
|
73
|
+
if (!url || !version) {
|
|
74
|
+
return null;
|
|
75
|
+
}
|
|
76
|
+
return {
|
|
77
|
+
url,
|
|
78
|
+
versionLabel: `v${version}`
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
|
|
47
82
|
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
48
83
|
return Boolean(value) && typeof value === 'object' && !Array.isArray(value);
|
|
49
84
|
}
|
|
@@ -70,12 +105,18 @@ function isReleaseNotesSection(value: unknown): value is ReleaseNotesSection {
|
|
|
70
105
|
&& (value.title === undefined || isReleaseNotesText(value.title));
|
|
71
106
|
}
|
|
72
107
|
|
|
108
|
+
function isReleaseNotesLinks(value: unknown): value is ReleaseNotesLinks {
|
|
109
|
+
return isRecord(value)
|
|
110
|
+
&& (value.html === undefined || isReleaseNotesText(value.html));
|
|
111
|
+
}
|
|
112
|
+
|
|
73
113
|
function isReleaseNotesPayload(value: unknown): value is ReleaseNotesPayload {
|
|
74
114
|
return isRecord(value)
|
|
75
115
|
&& Array.isArray(value.sections)
|
|
76
116
|
&& value.sections.every(isReleaseNotesSection)
|
|
77
117
|
&& (value.title === undefined || isReleaseNotesText(value.title))
|
|
78
|
-
&& (value.summary === undefined || isReleaseNotesText(value.summary))
|
|
118
|
+
&& (value.summary === undefined || isReleaseNotesText(value.summary))
|
|
119
|
+
&& (value.links === undefined || isReleaseNotesLinks(value.links));
|
|
79
120
|
}
|
|
80
121
|
|
|
81
122
|
export async function fetchReleaseNotesData(url: string): Promise<ReleaseNotesPayload> {
|
package/src/index.css
CHANGED
|
@@ -719,8 +719,9 @@
|
|
|
719
719
|
--md-table-head-bg: hsl(var(--muted) / 0.84);
|
|
720
720
|
}
|
|
721
721
|
|
|
722
|
+
/* The root flow owns external spacing; individual blocks only own their surface. */
|
|
722
723
|
.chat-markdown > * + * {
|
|
723
|
-
margin-
|
|
724
|
+
margin-block-start: 0.75rem;
|
|
724
725
|
}
|
|
725
726
|
|
|
726
727
|
.chat-markdown p,
|
|
@@ -729,21 +730,19 @@
|
|
|
729
730
|
color: var(--md-text);
|
|
730
731
|
}
|
|
731
732
|
|
|
732
|
-
.chat-markdown h1,
|
|
733
|
-
|
|
734
|
-
.chat-markdown h3,
|
|
735
|
-
.chat-markdown h4,
|
|
736
|
-
.chat-markdown h5,
|
|
737
|
-
.chat-markdown h6 {
|
|
738
|
-
margin: 0.2rem 0 0;
|
|
739
|
-
padding-bottom: 0.5em;
|
|
733
|
+
.chat-markdown :is(h1, h2, h3, h4, h5, h6) {
|
|
734
|
+
margin: 0;
|
|
740
735
|
font-weight: 700;
|
|
741
736
|
line-height: 1.35;
|
|
742
737
|
letter-spacing: -0.01em;
|
|
743
738
|
}
|
|
744
739
|
|
|
745
|
-
.chat-markdown :is(h1, h2, h3, h4, h5, h6)
|
|
746
|
-
margin-
|
|
740
|
+
.chat-markdown > :is(h1, h2, h3, h4, h5, h6):not(:first-child) {
|
|
741
|
+
margin-block-start: 1.25rem;
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
.chat-markdown > :is(h1, h2, h3, h4, h5, h6) + * {
|
|
745
|
+
margin-block-start: 0.5rem;
|
|
747
746
|
}
|
|
748
747
|
|
|
749
748
|
.chat-markdown h1 {
|
|
@@ -766,7 +765,6 @@
|
|
|
766
765
|
|
|
767
766
|
.chat-markdown ul,
|
|
768
767
|
.chat-markdown ol {
|
|
769
|
-
margin: 0.35rem 0;
|
|
770
768
|
padding-left: 1.5rem;
|
|
771
769
|
list-style-position: outside;
|
|
772
770
|
}
|
|
@@ -839,7 +837,6 @@
|
|
|
839
837
|
.chat-markdown hr {
|
|
840
838
|
border: 0;
|
|
841
839
|
border-top: 1px solid var(--md-border);
|
|
842
|
-
margin: 0.92rem 0;
|
|
843
840
|
}
|
|
844
841
|
|
|
845
842
|
.chat-markdown a {
|
|
@@ -874,7 +871,6 @@
|
|
|
874
871
|
}
|
|
875
872
|
|
|
876
873
|
.chat-codeblock {
|
|
877
|
-
margin: 0.2rem 0 0.1rem;
|
|
878
874
|
border: 1px solid var(--md-code-toolbar-border);
|
|
879
875
|
border-radius: 0.5rem;
|
|
880
876
|
overflow: hidden;
|
|
@@ -1299,7 +1295,6 @@
|
|
|
1299
1295
|
}
|
|
1300
1296
|
|
|
1301
1297
|
.chat-table-wrap {
|
|
1302
|
-
margin: 0.15rem 0;
|
|
1303
1298
|
border: 1px solid var(--md-border);
|
|
1304
1299
|
border-radius: 0.72rem;
|
|
1305
1300
|
overflow-x: auto;
|
|
@@ -1378,7 +1373,6 @@
|
|
|
1378
1373
|
}
|
|
1379
1374
|
|
|
1380
1375
|
.chat-markdown blockquote {
|
|
1381
|
-
margin: 0.2rem 0;
|
|
1382
1376
|
border-left: 3px solid var(--md-blockquote-border);
|
|
1383
1377
|
border-radius: 0 0.55rem 0.55rem 0;
|
|
1384
1378
|
padding: 0.45rem 0.72rem;
|
|
@@ -10,7 +10,9 @@ import { setLanguage } from '@/shared/lib/i18n';
|
|
|
10
10
|
|
|
11
11
|
const mocks = vi.hoisted(() => ({
|
|
12
12
|
applyDownloadedUpdate: vi.fn(),
|
|
13
|
-
downloadUpdate: vi.fn()
|
|
13
|
+
downloadUpdate: vi.fn(),
|
|
14
|
+
fetchReleaseNotes: vi.fn(),
|
|
15
|
+
openExternalUrl: vi.fn()
|
|
14
16
|
}));
|
|
15
17
|
|
|
16
18
|
vi.mock('@/features/system-status', async () => {
|
|
@@ -25,7 +27,30 @@ vi.mock('@/features/system-status', async () => {
|
|
|
25
27
|
};
|
|
26
28
|
});
|
|
27
29
|
|
|
28
|
-
|
|
30
|
+
vi.mock('@/shared/lib/host-capabilities', () => ({
|
|
31
|
+
hostCapabilityManager: {
|
|
32
|
+
openExternalUrl: mocks.openExternalUrl,
|
|
33
|
+
},
|
|
34
|
+
}));
|
|
35
|
+
|
|
36
|
+
function createReleaseNotesResponse(urls?: { zh?: string; en?: string }): Response {
|
|
37
|
+
return new Response(JSON.stringify({
|
|
38
|
+
links: urls
|
|
39
|
+
? {
|
|
40
|
+
html: {
|
|
41
|
+
...(urls.zh ? { 'zh-CN': urls.zh } : {}),
|
|
42
|
+
...(urls.en ? { 'en-US': urls.en } : {})
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
: undefined,
|
|
46
|
+
sections: []
|
|
47
|
+
}), {
|
|
48
|
+
headers: { 'content-type': 'application/json' },
|
|
49
|
+
status: 200
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function renderBrandHeader(options: { productVersion?: string } = {}) {
|
|
29
54
|
const queryClient = new QueryClient({
|
|
30
55
|
defaultOptions: {
|
|
31
56
|
queries: {
|
|
@@ -35,7 +60,7 @@ function renderBrandHeader() {
|
|
|
35
60
|
});
|
|
36
61
|
queryClient.setQueryData(['app-meta'], {
|
|
37
62
|
name: 'NextClaw',
|
|
38
|
-
productVersion: '0.18.11'
|
|
63
|
+
productVersion: options.productVersion ?? '0.18.11'
|
|
39
64
|
});
|
|
40
65
|
return render(
|
|
41
66
|
<QueryClientProvider client={queryClient}>
|
|
@@ -57,6 +82,10 @@ describe('BrandHeader', () => {
|
|
|
57
82
|
});
|
|
58
83
|
mocks.applyDownloadedUpdate.mockReset();
|
|
59
84
|
mocks.downloadUpdate.mockReset();
|
|
85
|
+
mocks.fetchReleaseNotes.mockReset();
|
|
86
|
+
mocks.fetchReleaseNotes.mockResolvedValue(createReleaseNotesResponse());
|
|
87
|
+
mocks.openExternalUrl.mockReset();
|
|
88
|
+
vi.stubGlobal('fetch', mocks.fetchReleaseNotes);
|
|
60
89
|
});
|
|
61
90
|
|
|
62
91
|
it('shows update progress next to the product version', async () => {
|
|
@@ -105,6 +134,84 @@ describe('BrandHeader', () => {
|
|
|
105
134
|
expect(screen.queryByRole('button', { name: '更新' })).toBeNull();
|
|
106
135
|
});
|
|
107
136
|
|
|
137
|
+
it('opens release notes from the current version tooltip when that version has docs notes', async () => {
|
|
138
|
+
const user = userEvent.setup();
|
|
139
|
+
mocks.fetchReleaseNotes.mockResolvedValueOnce(createReleaseNotesResponse({
|
|
140
|
+
zh: 'https://docs.nextclaw.io/zh/notes/2026-07-15-nextclaw-v0-23-0',
|
|
141
|
+
en: 'https://docs.nextclaw.io/en/notes/2026-07-15-nextclaw-v0-23-0'
|
|
142
|
+
}));
|
|
143
|
+
|
|
144
|
+
renderBrandHeader({ productVersion: '0.23.0' });
|
|
145
|
+
|
|
146
|
+
const version = await screen.findByRole('button', {
|
|
147
|
+
name: '当前版本 v0.23.0,查看 v0.23.0 更新说明'
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
await user.hover(version);
|
|
151
|
+
expect((await screen.findByRole('tooltip')).textContent).toContain('点击查看 v0.23.0 更新说明');
|
|
152
|
+
|
|
153
|
+
await user.click(version);
|
|
154
|
+
|
|
155
|
+
expect(mocks.fetchReleaseNotes).toHaveBeenCalledWith(
|
|
156
|
+
'https://docs.nextclaw.io/release-notes/nextclaw-v0.23.0.json',
|
|
157
|
+
expect.objectContaining({ cache: 'no-store' })
|
|
158
|
+
);
|
|
159
|
+
expect(mocks.openExternalUrl).toHaveBeenCalledWith('https://docs.nextclaw.io/zh/notes/2026-07-15-nextclaw-v0-23-0');
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
it('keeps update target release notes available without replacing the current version link', async () => {
|
|
163
|
+
const user = userEvent.setup();
|
|
164
|
+
mocks.fetchReleaseNotes.mockResolvedValueOnce(createReleaseNotesResponse({
|
|
165
|
+
zh: 'https://docs.nextclaw.io/zh/notes/2026-07-14-nextclaw-v0-18-11'
|
|
166
|
+
}));
|
|
167
|
+
useRuntimeUpdateStore.setState({
|
|
168
|
+
supported: true,
|
|
169
|
+
initialized: true,
|
|
170
|
+
busyAction: null,
|
|
171
|
+
snapshot: {
|
|
172
|
+
status: 'update-available',
|
|
173
|
+
installationKind: 'npm-runtime-bundle',
|
|
174
|
+
channel: 'stable',
|
|
175
|
+
hostVersion: '0.18.11',
|
|
176
|
+
currentVersion: '0.18.11',
|
|
177
|
+
availableVersion: '0.18.12',
|
|
178
|
+
downloadedVersion: null,
|
|
179
|
+
minimumHostVersion: null,
|
|
180
|
+
releaseNotesUrl: 'https://docs.nextclaw.io/zh/notes/2026-07-15-nextclaw-v0-18-12',
|
|
181
|
+
lastCheckedAt: null,
|
|
182
|
+
progress: null,
|
|
183
|
+
canAutoDownload: true,
|
|
184
|
+
canApplyInApp: false,
|
|
185
|
+
requiresRestart: false,
|
|
186
|
+
blockReason: null,
|
|
187
|
+
recoveryCommand: null,
|
|
188
|
+
errorMessage: null,
|
|
189
|
+
preferences: {
|
|
190
|
+
automaticChecks: true,
|
|
191
|
+
autoDownload: true
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
renderBrandHeader();
|
|
197
|
+
|
|
198
|
+
const currentVersion = await screen.findByRole('button', {
|
|
199
|
+
name: '当前版本 v0.18.11,查看 v0.18.11 更新说明'
|
|
200
|
+
});
|
|
201
|
+
await user.click(currentVersion);
|
|
202
|
+
expect(mocks.openExternalUrl).toHaveBeenCalledWith('https://docs.nextclaw.io/zh/notes/2026-07-14-nextclaw-v0-18-11');
|
|
203
|
+
|
|
204
|
+
await user.click(screen.getByRole('button', { name: '下载' }));
|
|
205
|
+
expect(mocks.downloadUpdate).toHaveBeenCalledTimes(1);
|
|
206
|
+
|
|
207
|
+
const updateReleaseNotes = screen.getByRole('button', { name: '查看 v0.18.12 更新说明' });
|
|
208
|
+
await user.hover(updateReleaseNotes);
|
|
209
|
+
expect((await screen.findByRole('tooltip')).textContent).toBe('查看 v0.18.12 更新说明');
|
|
210
|
+
|
|
211
|
+
await user.click(updateReleaseNotes);
|
|
212
|
+
expect(mocks.openExternalUrl).toHaveBeenCalledWith('https://docs.nextclaw.io/zh/notes/2026-07-15-nextclaw-v0-18-12');
|
|
213
|
+
});
|
|
214
|
+
|
|
108
215
|
it('applies the downloaded update from the version-adjacent update button', async () => {
|
|
109
216
|
const user = userEvent.setup();
|
|
110
217
|
useRuntimeUpdateStore.setState({
|