@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.
Files changed (147) hide show
  1. package/CHANGELOG.md +24 -0
  2. package/dist/assets/api-Dyb5rgh0.js +13 -0
  3. package/dist/assets/app-presenter-provider-7jV0nPiL.js +106 -0
  4. package/dist/assets/{appearance-settings-page-80fLa4-9.js → appearance-settings-page-DFm3BbEb.js} +1 -1
  5. package/dist/assets/{book-open-CN7Shpij.js → book-open-L6LCuqOr.js} +1 -1
  6. package/dist/assets/{channels-list-page-R7M6pzp6.js → channels-list-page-DOaPKNJA.js} +2 -2
  7. package/dist/assets/{chat-page-152YuwfU.js → chat-page-Cw9uDhD_.js} +1 -1
  8. package/dist/assets/{config-split-page-BlfvfmTm.js → config-split-page-CqlqjnCK.js} +1 -1
  9. package/dist/assets/{confirm-dialog-Dt8Kdbgp.js → confirm-dialog-bAp8NU93.js} +1 -1
  10. package/dist/assets/desktop-update-config-2SjMfCMl.js +1 -0
  11. package/dist/assets/{dist-D5ycbwCo.js → dist-CrtbGLlh.js} +1 -1
  12. package/dist/assets/doc-browser-A5IRyDXQ2.js +1 -0
  13. package/dist/assets/{doc-browser-C9u7N9vH.js → doc-browser-BmnQa30-.js} +1 -1
  14. package/dist/assets/{doc-browser-context-CCdW9TJf.js → doc-browser-context-BlpUUFrR.js} +1 -1
  15. package/dist/assets/{download-CyPgglKr.js → download-BhZZNXD8.js} +1 -1
  16. package/dist/assets/{external-link-BahW1sl3.js → external-link-B9Uoq5J7.js} +1 -1
  17. package/dist/assets/{eye-D8gGXMkD.js → eye-K5knauEM.js} +1 -1
  18. package/dist/assets/{folder-DGWqFDvE.js → folder-BIYZZWJv.js} +1 -1
  19. package/dist/assets/{form-actions-D2imZLnU.js → form-actions-CKprWFeM.js} +1 -1
  20. package/dist/assets/index-BVn702m4.css +1 -0
  21. package/dist/assets/{index-CT0iYlxV.js → index-CFxJ1Fkg.js} +2 -2
  22. package/dist/assets/loader-circle-Bj2RMrCR.js +1 -0
  23. package/dist/assets/{logo-badge-CuPetu-v.js → logo-badge-BQ-XFZod.js} +1 -1
  24. package/dist/assets/mcp-marketplace-page-BfA7A3n4.js +9 -0
  25. package/dist/assets/mcp-marketplace-page-CXHYO95j.js +1 -0
  26. package/dist/assets/{model-config-page-DiJcvBML.js → model-config-page-BbWbW_qM.js} +1 -1
  27. package/dist/assets/plus-TmFdwO3P.js +1 -0
  28. package/dist/assets/{popover-Btp7KVc3.js → popover-CX9zFsuc.js} +1 -1
  29. package/dist/assets/{provider-scoped-model-input-DZMtoAJG.js → provider-scoped-model-input-B-9_DtIP.js} +1 -1
  30. package/dist/assets/providers-config-page-xwIjGur2.js +1 -0
  31. package/dist/assets/{react-BY-U3h3t.js → react-DsgudwpT.js} +1 -1
  32. package/dist/assets/{refresh-cw-Cme5qFej.js → refresh-cw-kXdvbhFf.js} +1 -1
  33. package/dist/assets/remote-B9F2uCk9.js +1 -0
  34. package/dist/assets/rotate-cw-DEc6aihr.js +1 -0
  35. package/dist/assets/runtime-config-page-DTsi7czJ.js +1 -0
  36. package/dist/assets/{save-DNg2yJTS.js → save-C9qpz2_2.js} +1 -1
  37. package/dist/assets/{search-3ykfrAFt.js → search-_kEi29qo.js} +1 -1
  38. package/dist/assets/{search-config-page-DrVKa_Ls.js → search-config-page-H7ug8f4D.js} +1 -1
  39. package/dist/assets/{secrets-config-page-B39uQ5kF.js → secrets-config-page-C1_6rINu.js} +2 -2
  40. package/dist/assets/security-config-Cr2ERKLu.js +1 -0
  41. package/dist/assets/{select-B5EecXJt.js → select-MJKhKAWR.js} +1 -1
  42. package/dist/assets/{setting-row-Qv1Ps79v.js → setting-row-DA3tJuew.js} +1 -1
  43. package/dist/assets/{settings-2-D-3um9Az.js → settings-2-D_y7zXJz.js} +1 -1
  44. package/dist/assets/{skeleton-BgkzQd4U.js → skeleton-0uTLUhm9.js} +1 -1
  45. package/dist/assets/{tabs-BVD8gp3y.js → tabs-h44XPocd.js} +1 -1
  46. package/dist/assets/{tag-chip-0qn-f3Bo.js → tag-chip-bRZ04q3C.js} +1 -1
  47. package/dist/assets/{trash-2-cnsMppRC.js → trash-2-BRpSE-Wt.js} +1 -1
  48. package/dist/assets/use-config-CwDEAl6f.js +1 -0
  49. package/dist/assets/{use-confirm-dialog-C_o0eQeH.js → use-confirm-dialog-BbnWw-8c.js} +1 -1
  50. package/dist/assets/{use-viewport-layout-BWav4KCL.js → use-viewport-layout-CqgIMDQL.js} +1 -1
  51. package/dist/assets/x-DdEDL6ju.js +1 -0
  52. package/dist/index.html +29 -29
  53. package/package.json +8 -8
  54. package/src/features/chat/components/conversation/__tests__/chat-conversation-header-section.test.tsx +84 -1
  55. package/src/features/chat/components/conversation/chat-conversation-header-section.tsx +13 -2
  56. package/src/features/chat/components/layout/__tests__/chat-sidebar-test.utils.ts +22 -0
  57. package/src/features/chat/components/layout/__tests__/chat-sidebar.test.tsx +13 -24
  58. package/src/features/chat/components/layout/chat-sidebar-desktop-layout.tsx +16 -4
  59. package/src/features/chat/components/layout/chat-sidebar.tsx +38 -2
  60. package/src/features/chat/features/conversation/components/__tests__/session-conversation-area.test.tsx +1 -0
  61. package/src/features/chat/features/conversation/components/__tests__/session-conversation-input.streaming.test.tsx +52 -2
  62. package/src/features/chat/features/conversation/components/session-conversation-area.tsx +22 -0
  63. package/src/features/chat/features/conversation/components/session-conversation-input.tsx +8 -9
  64. package/src/features/chat/features/conversation/hooks/__tests__/use-session-conversation-input-state.test.tsx +55 -0
  65. package/src/features/chat/features/conversation/hooks/use-session-conversation-input-state.ts +12 -11
  66. package/src/features/chat/features/conversation/hooks/use-session-conversation-preference-actions.ts +94 -0
  67. package/src/features/chat/features/input/hooks/__tests__/use-chat-input-surface-state.test.tsx +37 -0
  68. package/src/features/chat/features/input/hooks/use-chat-input-surface-state.ts +140 -63
  69. package/src/features/chat/features/input/input-surface-plugins/__tests__/context-reference-plugin.test.ts +175 -0
  70. package/src/features/chat/features/input/input-surface-plugins/__tests__/slash-command-plugin.test.ts +23 -0
  71. package/src/features/chat/features/input/input-surface-plugins/chat-input-product-plugin-adapters.types.ts +8 -1
  72. package/src/features/chat/features/input/input-surface-plugins/context-reference-plugin.utils.ts +274 -0
  73. package/src/features/chat/features/input/input-surface-plugins/slash-command-plugin.utils.ts +4 -1
  74. package/src/features/chat/features/input/utils/__tests__/chat-composer-state.utils.test.ts +26 -0
  75. package/src/features/chat/features/input/utils/__tests__/chat-inline-token.utils.test.ts +50 -4
  76. package/src/features/chat/features/input/utils/chat-composer-state.utils.ts +4 -10
  77. package/src/features/chat/features/input/utils/chat-composer-token-protocol.utils.ts +29 -0
  78. package/src/features/chat/features/input/utils/chat-inline-token.utils.ts +66 -8
  79. package/src/features/chat/features/input/utils/chat-input-bar.utils.ts +1 -0
  80. package/src/features/chat/features/ncp/hooks/__tests__/use-hydrated-ncp-agent.test.tsx +41 -3
  81. package/src/features/chat/features/project/components/__tests__/chat-project-create-dialog.test.tsx +67 -0
  82. package/src/features/chat/features/project/components/chat-project-create-dialog.tsx +203 -0
  83. package/src/features/chat/features/session/components/session-header/__tests__/chat-session-header-actions.test.tsx +7 -7
  84. package/src/features/chat/features/session/components/session-header/__tests__/chat-session-project-dialog.test.tsx +47 -0
  85. package/src/features/chat/features/session/components/session-header/chat-session-header-actions.tsx +6 -3
  86. package/src/features/chat/features/session/components/session-header/chat-session-project-badge.tsx +3 -0
  87. package/src/features/chat/features/session/components/session-header/chat-session-project-dialog.tsx +3 -0
  88. package/src/features/chat/features/session/components/session-header/chat-session-switcher-groups.tsx +245 -0
  89. package/src/features/chat/features/session/components/session-header/chat-session-title-switcher.tsx +81 -127
  90. package/src/features/chat/features/session/utils/__tests__/chat-sidebar-session-groups.utils.test.ts +22 -0
  91. package/src/features/chat/features/session/utils/chat-run-metadata.utils.ts +6 -6
  92. package/src/features/chat/features/session/utils/chat-sidebar-session-groups.utils.ts +12 -1
  93. package/src/features/chat/features/welcome/components/chat-conversation-welcome.tsx +3 -0
  94. package/src/features/chat/features/welcome/components/chat-welcome.tsx +1 -0
  95. package/src/features/chat/features/welcome/utils/__tests__/chat-welcome-project-options.utils.test.ts +20 -0
  96. package/src/features/chat/features/welcome/utils/chat-welcome-project-options.utils.ts +15 -2
  97. package/src/features/chat/pages/__tests__/ncp-chat-page.test.ts +23 -0
  98. package/src/features/settings/components/config/provider-form.tsx +0 -4
  99. package/src/features/settings/utils/__tests__/provider-form-support.utils.test.ts +7 -9
  100. package/src/features/settings/utils/provider-form-model.utils.ts +0 -4
  101. package/src/features/system-status/hooks/use-current-version-release-notes-link.ts +33 -0
  102. package/src/features/system-status/index.ts +9 -0
  103. package/src/features/system-status/utils/update-release-notes.utils.ts +44 -3
  104. package/src/index.css +10 -16
  105. package/src/shared/components/common/__tests__/brand-header.test.tsx +110 -3
  106. package/src/shared/components/common/brand-header.tsx +121 -35
  107. package/src/shared/components/path-picker/__tests__/server-path-picker-dialog.test.tsx +288 -38
  108. package/src/shared/components/path-picker/server-path-picker-dialog.tsx +290 -199
  109. package/src/shared/components/path-picker/server-path-picker-directory-list.tsx +103 -0
  110. package/src/shared/components/path-picker/server-path-picker-footer.tsx +58 -0
  111. package/src/shared/components/path-picker/server-path-picker-locations.tsx +148 -0
  112. package/src/shared/components/path-picker/server-path-picker-navigation-bar.tsx +198 -0
  113. package/src/shared/components/path-picker/server-path-picker-new-folder.tsx +59 -0
  114. package/src/shared/components/path-picker/server-path-picker-toolbar.tsx +25 -0
  115. package/src/shared/hooks/use-projects.ts +28 -0
  116. package/src/shared/hooks/use-server-path-create-directory.ts +6 -0
  117. package/src/shared/hooks/use-server-path-search.ts +40 -0
  118. package/src/shared/lib/api/index.ts +1 -0
  119. package/src/shared/lib/api/projects/project.utils.ts +14 -0
  120. package/src/shared/lib/api/types.ts +27 -0
  121. package/src/shared/lib/api/utils/server-path.utils.ts +21 -1
  122. package/src/shared/lib/i18n/locales/en-US/chat.json +35 -2
  123. package/src/shared/lib/i18n/locales/en-US/core.json +2 -3
  124. package/src/shared/lib/i18n/locales/en-US/desktop-update.json +4 -0
  125. package/src/shared/lib/i18n/locales/en-US/path-picker.json +25 -0
  126. package/src/shared/lib/i18n/locales/zh-CN/chat.json +35 -2
  127. package/src/shared/lib/i18n/locales/zh-CN/core.json +2 -3
  128. package/src/shared/lib/i18n/locales/zh-CN/desktop-update.json +4 -0
  129. package/src/shared/lib/i18n/locales/zh-CN/path-picker.json +25 -0
  130. package/dist/assets/api-IlX_CbwU.js +0 -13
  131. package/dist/assets/app-presenter-provider-Bbl_MVbg.js +0 -106
  132. package/dist/assets/desktop-update-config-DreMJu9z.js +0 -1
  133. package/dist/assets/doc-browser-DJQ5XPsi2.js +0 -1
  134. package/dist/assets/index-0av6WO9-.css +0 -1
  135. package/dist/assets/loader-circle-DtKek-qG.js +0 -1
  136. package/dist/assets/mcp-marketplace-page-4qMwFDpz.js +0 -1
  137. package/dist/assets/mcp-marketplace-page-BbxN5aZF.js +0 -9
  138. package/dist/assets/plus-BwgE-Zwf.js +0 -1
  139. package/dist/assets/providers-config-page-DfiWveEF.js +0 -1
  140. package/dist/assets/remote-BaVECEZw.js +0 -1
  141. package/dist/assets/rotate-cw-CMRZxIkH.js +0 -1
  142. package/dist/assets/runtime-config-page-wygwvMB4.js +0 -1
  143. package/dist/assets/security-config-BPzet76z.js +0 -1
  144. package/dist/assets/use-config-D6yx0lb0.js +0 -1
  145. package/dist/assets/x-B3o1dWR5.js +0 -1
  146. package/src/features/chat/features/input/input-surface-plugins/__tests__/panel-app-reference-plugin.test.ts +0 -77
  147. 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(serializeModelsForSave(['gpt-5.5', 'openai-work/gpt-5.4'], 'openai-work')).toEqual([
26
- 'openai-work/gpt-5.5',
27
- 'openai-work/gpt-5.4'
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 drafts without accepting provider prefixes', () => {
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([], ' other/gpt-5.5 ', ['openai-work'])).toEqual({
66
- models: [],
67
- draft: ' other/gpt-5.5 ',
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 || !version || !/^[0-9A-Za-z.-]+$/.test(version)) {
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${version}.json`, releaseNotesUrl).toString();
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-top: 0.72rem;
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
- .chat-markdown h2,
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-top: 0;
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
- function renderBrandHeader() {
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({