@nextclaw/ui 0.12.34 → 0.12.35-beta.0

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 (133) hide show
  1. package/CHANGELOG.md +63 -0
  2. package/dist/assets/{api-rASx2-N4.js → api-De1PAch0.js} +3 -3
  3. package/dist/assets/{book-open-BcnAiKde.js → book-open-825Pcd0z.js} +1 -1
  4. package/dist/assets/channels-list-page-CGjXDSD-.js +8 -0
  5. package/dist/assets/chat-page-RsUTK_bY.js +105 -0
  6. package/dist/assets/{config-split-page-CcrEUtwu.js → config-split-page-BvfxFQSb.js} +1 -1
  7. package/dist/assets/{createLucideIcon-DzY6wN61.js → createLucideIcon-CeNHY6BW.js} +1 -1
  8. package/dist/assets/desktop-update-config-B_YIv5ZJ.js +1 -0
  9. package/dist/assets/{dist-NBjcwlsw.js → dist-uH7tiW6E.js} +1 -1
  10. package/dist/assets/{doc-browser-D2pq4IHR.js → doc-browser-CDe9yz9n.js} +1 -1
  11. package/dist/assets/doc-browser-DwT64GuF.js +1 -0
  12. package/dist/assets/{doc-browser-context-BPeeiNJ_.js → doc-browser-context-CQAx_92e.js} +1 -1
  13. package/dist/assets/{es2015-CVAuMqlM.js → es2015-c9XYpli5.js} +1 -1
  14. package/dist/assets/{external-link-CpEvG65F.js → external-link-BE1deZ_c.js} +1 -1
  15. package/dist/assets/i18n-DnubSHoK.js +1 -0
  16. package/dist/assets/index-6WZ6JjaU.css +1 -0
  17. package/dist/assets/index-BjH_p3R6.js +103 -0
  18. package/dist/assets/{key-round-DUq47t0P.js → key-round-CHo3gqpO.js} +1 -1
  19. package/dist/assets/mcp-marketplace-page-0C214hGi.js +40 -0
  20. package/dist/assets/mcp-marketplace-page-amWcMncu.js +1 -0
  21. package/dist/assets/model-config-B8zNrkag.js +1 -0
  22. package/dist/assets/notice-card-DO33_LzD.js +5 -0
  23. package/dist/assets/play-CqlWbVNo.js +1 -0
  24. package/dist/assets/plus-p61tQh2-.js +1 -0
  25. package/dist/assets/provider-scoped-model-input-B0_r_4EY.js +1 -0
  26. package/dist/assets/providers-list-B6v3foi7.js +1 -0
  27. package/dist/assets/remote-k671rLmY.js +1 -0
  28. package/dist/assets/{rotate-cw-BN9yjccP.js → rotate-cw-CYUByuqo.js} +1 -1
  29. package/dist/assets/runtime-config-page-DgH-XJwK.js +1 -0
  30. package/dist/assets/{save-CO_4qf6b.js → save-B6Ga8JIq.js} +1 -1
  31. package/dist/assets/{search-CRtQwr-h.js → search-5kKpgNoP.js} +1 -1
  32. package/dist/assets/search-config-DPZ6d9y6.js +1 -0
  33. package/dist/assets/{secrets-config-DFbHCb9n.js → secrets-config-CrXA11bo.js} +2 -2
  34. package/dist/assets/{select-HI5kiwX_.js → select-C0hGOCOK.js} +1 -1
  35. package/dist/assets/{setting-row-B4QnghjV.js → setting-row-xsM_0nTd.js} +1 -1
  36. package/dist/assets/{tabs-custom-D48zdZoc.js → tabs-custom-Bnqfhe4p.js} +1 -1
  37. package/dist/assets/{tag-chip-BPgMfp9l.js → tag-chip-cktJJMzh.js} +1 -1
  38. package/dist/assets/x-DDQw-C2x.js +1 -0
  39. package/dist/index.html +17 -29
  40. package/package.json +9 -9
  41. package/src/app/components/layout/app-layout.test.tsx +49 -4
  42. package/src/app/components/layout/app-layout.tsx +4 -7
  43. package/src/app/components/layout/settings-entry-page.test.tsx +9 -0
  44. package/src/app/components/layout/sidebar.layout.test.tsx +2 -2
  45. package/src/app/components/layout/sidebar.tsx +3 -3
  46. package/src/app/configs/app-navigation.config.ts +0 -12
  47. package/src/app/hooks/use-app-event-consumers.ts +1 -6
  48. package/src/app/hooks/use-viewport-layout.ts +0 -1
  49. package/src/app/index.tsx +2 -10
  50. package/src/features/channels/components/config/channel-form.tsx +85 -47
  51. package/src/features/channels/components/config/weixin-channel-auth-section.test.tsx +45 -1
  52. package/src/features/channels/components/config/weixin-channel-auth-section.tsx +119 -1
  53. package/src/features/channels/hooks/use-channel-auth.ts +8 -1
  54. package/src/features/channels/index.ts +1 -1
  55. package/src/features/chat/components/conversation/chat-conversation-panel.test.tsx +2 -2
  56. package/src/features/chat/components/conversation/chat-conversation-panel.tsx +2 -7
  57. package/src/features/chat/components/layout/chat-sidebar.test.tsx +27 -3
  58. package/src/features/chat/components/layout/chat-sidebar.tsx +2 -3
  59. package/src/features/chat/managers/chat-session-list.manager.test.ts +12 -11
  60. package/src/features/chat/managers/chat-session-list.manager.ts +2 -1
  61. package/src/features/chat/managers/chat-ui.manager.ts +2 -2
  62. package/src/features/chat/managers/ncp-chat-input.manager.test.ts +3 -3
  63. package/src/features/chat/pages/ncp-chat-page.test.ts +3 -0
  64. package/src/features/chat/pages/ncp-chat-page.tsx +19 -19
  65. package/src/features/chat/utils/chat-session-route.utils.test.ts +14 -0
  66. package/src/features/chat/utils/chat-session-route.utils.ts +3 -1
  67. package/src/features/chat/utils/ncp-chat-runtime-availability.utils.test.ts +6 -6
  68. package/src/features/marketplace/components/marketplace-list-card.tsx +4 -36
  69. package/src/features/marketplace/components/marketplace-page-detail.test.tsx +0 -1
  70. package/src/features/marketplace/components/marketplace-page.test.tsx +9 -128
  71. package/src/features/marketplace/components/marketplace-page.tsx +16 -72
  72. package/src/features/marketplace/hooks/use-marketplace-curated-scene-route.ts +1 -1
  73. package/src/features/marketplace/hooks/use-marketplace.ts +1 -10
  74. package/src/features/marketplace/utils/marketplace-installed-cache.utils.test.ts +23 -21
  75. package/src/features/system-status/managers/system-status.manager.bootstrap-polling.test.ts +12 -12
  76. package/src/features/system-status/managers/system-status.manager.test.ts +6 -6
  77. package/src/index.css +1 -1
  78. package/src/platforms/desktop/components/desktop-app-shell.test.tsx +29 -13
  79. package/src/platforms/desktop/components/desktop-app-shell.tsx +14 -16
  80. package/src/platforms/desktop/components/desktop-window-chrome.tsx +2 -3
  81. package/src/platforms/mobile/components/mobile-app-shell.tsx +3 -1
  82. package/src/platforms/mobile/components/mobile-topbar.tsx +6 -5
  83. package/src/platforms/mobile/index.ts +1 -0
  84. package/src/shared/components/config/provider-form-support.ts +51 -33
  85. package/src/shared/components/config/provider-form.tsx +68 -35
  86. package/src/shared/components/config/provider-models-section.tsx +19 -4
  87. package/src/shared/components/model-config.test.tsx +2 -0
  88. package/src/shared/components/ui/button.tsx +1 -1
  89. package/src/shared/lib/api/channel-auth.types.ts +8 -0
  90. package/src/shared/lib/api/types.ts +17 -20
  91. package/src/shared/lib/api/utils/channel-auth.utils.ts +10 -1
  92. package/src/shared/lib/api/utils/marketplace.utils.ts +0 -5
  93. package/src/shared/lib/i18n/channel-auth.constants.ts +24 -10
  94. package/src/shared/lib/i18n/chat-labels.utils.ts +2 -2
  95. package/src/shared/lib/i18n/index.ts +2 -0
  96. package/src/shared/lib/i18n/marketplace.ts +0 -16
  97. package/src/shared/lib/provider-models/index.ts +41 -13
  98. package/src/shared/lib/ui-document-title/index.ts +0 -1
  99. package/dist/assets/app-manager-provider-CUkcbdWk.js +0 -1
  100. package/dist/assets/channels-list-page-ZWYqfcCe.js +0 -8
  101. package/dist/assets/chat-page-ACm58eUO.js +0 -1
  102. package/dist/assets/cpu-DKc3bQic.js +0 -3
  103. package/dist/assets/desktop-C-cDRCBY.js +0 -3
  104. package/dist/assets/desktop-update-config-DAD9JN9Q.js +0 -1
  105. package/dist/assets/dialog-CR80q3Rm.js +0 -5
  106. package/dist/assets/doc-browser-DhIkcy0-.js +0 -1
  107. package/dist/assets/doc-browser-fLs0TcdS.js +0 -1
  108. package/dist/assets/download-CMM8po31.js +0 -1
  109. package/dist/assets/i18n-D1144VAA.js +0 -1
  110. package/dist/assets/index-DV57tBmg.css +0 -1
  111. package/dist/assets/index-Db9Cufsy.js +0 -100
  112. package/dist/assets/marketplace-page-CAp_gpNe.js +0 -105
  113. package/dist/assets/marketplace-page-PPY5uMHK.js +0 -1
  114. package/dist/assets/mcp-marketplace-page-87__92b1.js +0 -1
  115. package/dist/assets/mcp-marketplace-page-C6HENB6V.js +0 -40
  116. package/dist/assets/model-config-CK3a8DWH.js +0 -1
  117. package/dist/assets/notice-card-CF4AJPM3.js +0 -1
  118. package/dist/assets/play-AqrNslHI.js +0 -1
  119. package/dist/assets/plus-B-YHtTNC.js +0 -1
  120. package/dist/assets/popover-DeAliE_4.js +0 -1
  121. package/dist/assets/provider-scoped-model-input-B8LJ6RCt.js +0 -1
  122. package/dist/assets/providers-list-BTPjqNdK.js +0 -1
  123. package/dist/assets/refresh-cw-CrbD8EkT.js +0 -1
  124. package/dist/assets/remote-DiPWGRZc.js +0 -1
  125. package/dist/assets/runtime-config-page-Bb9he7t8.js +0 -1
  126. package/dist/assets/search-config-BkbSXa9d.js +0 -1
  127. package/dist/assets/settings-MWL2SMyk.js +0 -1
  128. package/dist/assets/sparkles-BmgOD4nY.js +0 -1
  129. package/dist/assets/status-dot-l3kPFdq_.js +0 -1
  130. package/dist/assets/use-config-BFqzVF8Y.js +0 -1
  131. package/dist/assets/use-infinite-scroll-loader-DXhw9W4l.js +0 -1
  132. package/dist/assets/x-BeyYA_h6.js +0 -1
  133. /package/dist/assets/{config-hints-BNfpOL4J.js → config-hints-DGfwbumO.js} +0 -0
@@ -52,6 +52,7 @@ describe('ChatSessionListManager', () => {
52
52
  it('applies the requested session type when creating a session', () => {
53
53
  const uiManager = {
54
54
  goToChatRoot: vi.fn(),
55
+ navigateTo: vi.fn(),
55
56
  goToSession: vi.fn(),
56
57
  isAtChatRoot: vi.fn(() => true),
57
58
  } as unknown as ConstructorParameters<typeof ChatSessionListManager>[0];
@@ -63,7 +64,7 @@ describe('ChatSessionListManager', () => {
63
64
  manager.createSession('codex');
64
65
 
65
66
  expect(streamActionsManager.resetStreamState).toHaveBeenCalledTimes(1);
66
- expect(uiManager.goToChatRoot).toHaveBeenCalledTimes(1);
67
+ expect(uiManager.navigateTo).toHaveBeenCalledWith('/chat/draft');
67
68
  expect(useChatSessionListStore.getState().snapshot.selectedSessionKey).toBeNull();
68
69
  expect(useChatThreadStore.getState().snapshot.sessionKey).toBeNull();
69
70
  expect(useChatThreadStore.getState().snapshot.hasSubmittedDraftMessage).toBe(false);
@@ -75,6 +76,7 @@ describe('ChatSessionListManager', () => {
75
76
  it('starts an agent draft chat through one owner state transition', () => {
76
77
  const uiManager = {
77
78
  goToChatRoot: vi.fn(),
79
+ navigateTo: vi.fn(),
78
80
  goToSession: vi.fn(),
79
81
  isAtChatRoot: vi.fn(() => true),
80
82
  } as unknown as ConstructorParameters<typeof ChatSessionListManager>[0];
@@ -86,7 +88,7 @@ describe('ChatSessionListManager', () => {
86
88
  manager.startAgentDraftChat('researcher', 'codex');
87
89
 
88
90
  expect(streamActionsManager.resetStreamState).toHaveBeenCalledTimes(1);
89
- expect(uiManager.goToChatRoot).toHaveBeenCalledTimes(1);
91
+ expect(uiManager.navigateTo).toHaveBeenCalledWith('/chat/draft');
90
92
  expect(useChatSessionListStore.getState().snapshot.selectedAgentId).toBe('researcher');
91
93
  expect(useChatSessionListStore.getState().snapshot.selectedSessionKey).toBeNull();
92
94
  expect(useChatThreadStore.getState().snapshot.sessionKey).toBeNull();
@@ -99,6 +101,7 @@ describe('ChatSessionListManager', () => {
99
101
  it('hydrates the draft project root when creating a session inside a project group', () => {
100
102
  const uiManager = {
101
103
  goToChatRoot: vi.fn(),
104
+ navigateTo: vi.fn(),
102
105
  goToSession: vi.fn(),
103
106
  isAtChatRoot: vi.fn(() => true),
104
107
  } as unknown as ConstructorParameters<typeof ChatSessionListManager>[0];
@@ -122,6 +125,7 @@ describe('ChatSessionListManager', () => {
122
125
  });
123
126
  const uiManager = {
124
127
  goToChatRoot: vi.fn(),
128
+ navigateTo: vi.fn(),
125
129
  goToSession: vi.fn(),
126
130
  isAtChatRoot: vi.fn(() => true),
127
131
  } as unknown as ConstructorParameters<typeof ChatSessionListManager>[0];
@@ -134,6 +138,7 @@ describe('ChatSessionListManager', () => {
134
138
 
135
139
  expect(sessionKey).toBeNull();
136
140
  expect(uiManager.goToChatRoot).not.toHaveBeenCalled();
141
+ expect(uiManager.navigateTo).not.toHaveBeenCalled();
137
142
  expect(uiManager.goToSession).not.toHaveBeenCalled();
138
143
  expect(useChatSessionListStore.getState().snapshot.selectedSessionKey).toBeNull();
139
144
  expect(useChatThreadStore.getState().snapshot.hasSubmittedDraftMessage).toBe(true);
@@ -142,6 +147,7 @@ describe('ChatSessionListManager', () => {
142
147
  it('does not eagerly replace the old selected session before the route finishes switching', () => {
143
148
  const uiManager = {
144
149
  goToChatRoot: vi.fn(),
150
+ navigateTo: vi.fn(),
145
151
  goToSession: vi.fn(),
146
152
  isAtChatRoot: vi.fn(() => true),
147
153
  } as unknown as ConstructorParameters<typeof ChatSessionListManager>[0];
@@ -159,6 +165,7 @@ describe('ChatSessionListManager', () => {
159
165
  it('delegates existing-session selection to routing while preserving workspace panel state', () => {
160
166
  const uiManager = {
161
167
  goToChatRoot: vi.fn(),
168
+ navigateTo: vi.fn(),
162
169
  goToSession: vi.fn(),
163
170
  isAtChatRoot: vi.fn(() => true),
164
171
  } as unknown as ConstructorParameters<typeof ChatSessionListManager>[0];
@@ -178,9 +185,7 @@ describe('ChatSessionListManager', () => {
178
185
  });
179
186
 
180
187
  it('updates the sidebar list mode without touching other session list state', () => {
181
- const uiManager = {
182
- isAtChatRoot: vi.fn(() => true),
183
- } as unknown as ConstructorParameters<typeof ChatSessionListManager>[0];
188
+ const uiManager = {} as ConstructorParameters<typeof ChatSessionListManager>[0];
184
189
  const streamActionsManager = {} as ConstructorParameters<typeof ChatSessionListManager>[1];
185
190
 
186
191
  const manager = new ChatSessionListManager(uiManager, streamActionsManager);
@@ -192,9 +197,7 @@ describe('ChatSessionListManager', () => {
192
197
 
193
198
  it('marks a session as read through the session list owner boundary', () => {
194
199
  const manager = new ChatSessionListManager(
195
- {
196
- isAtChatRoot: vi.fn(() => true),
197
- } as unknown as ConstructorParameters<typeof ChatSessionListManager>[0],
200
+ {} as ConstructorParameters<typeof ChatSessionListManager>[0],
198
201
  {} as ConstructorParameters<typeof ChatSessionListManager>[1]
199
202
  );
200
203
 
@@ -210,9 +213,7 @@ describe('ChatSessionListManager', () => {
210
213
 
211
214
  it('skips persisting read state when the backend already has the same watermark', () => {
212
215
  const manager = new ChatSessionListManager(
213
- {
214
- isAtChatRoot: vi.fn(() => true),
215
- } as unknown as ConstructorParameters<typeof ChatSessionListManager>[0],
216
+ {} as ConstructorParameters<typeof ChatSessionListManager>[0],
216
217
  {} as ConstructorParameters<typeof ChatSessionListManager>[1]
217
218
  );
218
219
 
@@ -6,6 +6,7 @@ import type { SetStateAction } from 'react';
6
6
  import type { ChatStreamActionsManager } from '@/features/chat/managers/chat-stream-actions.manager';
7
7
  import { normalizeSessionProjectRootValue } from '@/shared/lib/session-project';
8
8
  import { updateNcpSession } from '@/shared/lib/api';
9
+ import { CHAT_DRAFT_SESSION_PATH } from '@/features/chat/utils/chat-session-route.utils';
9
10
  export class ChatSessionListManager {
10
11
  constructor(
11
12
  private uiManager: ChatUiManager,
@@ -117,7 +118,7 @@ export class ChatSessionListManager {
117
118
  pendingProjectRoot: normalizedProjectRoot,
118
119
  pendingProjectRootSessionKey: null
119
120
  });
120
- this.uiManager.goToChatRoot();
121
+ this.uiManager.navigateTo(CHAT_DRAFT_SESSION_PATH);
121
122
  };
122
123
 
123
124
  startAgentDraftChat = (agentId: string, sessionType: string): void => {
@@ -1,5 +1,5 @@
1
1
  import type { NavigateFunction, NavigateOptions } from 'react-router-dom';
2
- import { buildSessionPath } from '@/features/chat/utils/chat-session-route.utils';
2
+ import { CHAT_DRAFT_SESSION_PATH, buildSessionPath } from '@/features/chat/utils/chat-session-route.utils';
3
3
 
4
4
  type ChatUiState = {
5
5
  pathname: string;
@@ -67,7 +67,7 @@ export class ChatUiManager {
67
67
  this.navigateTo('/chat', options);
68
68
  };
69
69
 
70
- isAtChatRoot = () => this.state.pathname === '/chat';
70
+ isAtChatRoot = () => this.state.pathname === '/chat' || this.state.pathname === CHAT_DRAFT_SESSION_PATH;
71
71
 
72
72
  goToSession = (sessionKey: string, options?: NavigateOptions) => {
73
73
  this.navigateTo(buildSessionPath(sessionKey), options);
@@ -39,10 +39,10 @@ describe('NcpChatInputManager', () => {
39
39
  ncpAgent: {
40
40
  state: 'ready',
41
41
  },
42
- pluginHydration: {
42
+ extensionLoading: {
43
43
  state: 'ready',
44
- loadedPluginCount: 1,
45
- totalPluginCount: 1,
44
+ loadedExtensionCount: 1,
45
+ totalExtensionCount: 1,
46
46
  },
47
47
  channels: {
48
48
  state: 'ready',
@@ -76,8 +76,11 @@ describe('buildNcpSendMetadata', () => {
76
76
  projectRoot: ' /tmp/project-alpha ',
77
77
  }),
78
78
  ).toMatchObject({
79
+ agentId: 'engineer',
79
80
  agent_id: 'engineer',
81
+ agentRuntimeId: 'codex',
80
82
  session_type: 'codex',
83
+ projectRoot: '/tmp/project-alpha',
81
84
  project_root: '/tmp/project-alpha',
82
85
  });
83
86
  });
@@ -39,6 +39,10 @@ import {
39
39
  normalizeSessionProjectRootValue,
40
40
  } from "@/shared/lib/session-project";
41
41
 
42
+ function createMetadataFields(value: string | undefined, fields: readonly string[]): Record<string, string> {
43
+ return value ? Object.fromEntries(fields.map((field) => [field, value])) : {};
44
+ }
45
+
42
46
  export function buildNcpSendMetadata(payload: {
43
47
  agentId?: string;
44
48
  model?: string;
@@ -48,26 +52,22 @@ export function buildNcpSendMetadata(payload: {
48
52
  requestedSkills?: string[];
49
53
  composerNodes?: Parameters<typeof buildInlineSkillTokensFromComposer>[0];
50
54
  }): Record<string, unknown> {
51
- const metadata: Record<string, unknown> = {};
52
- if (payload.model?.trim()) {
53
- metadata.model = payload.model.trim();
54
- metadata.preferred_model = payload.model.trim();
55
- }
56
- if (payload.thinkingLevel?.trim()) {
57
- metadata.thinking = payload.thinkingLevel.trim();
58
- metadata.preferred_thinking = payload.thinkingLevel.trim();
59
- }
60
- if (payload.sessionType?.trim()) {
61
- metadata.session_type = payload.sessionType.trim();
62
- metadata.runtime = payload.sessionType.trim();
63
- }
64
- if (payload.agentId?.trim()) {
65
- metadata.agent_id = payload.agentId.trim();
66
- }
67
55
  const projectRoot = normalizeSessionProjectRootValue(payload.projectRoot);
68
- if (projectRoot) {
69
- metadata.project_root = projectRoot;
70
- }
56
+ const metadata: Record<string, unknown> = {
57
+ ...createMetadataFields(payload.model?.trim(), ["model", "preferred_model"]),
58
+ ...createMetadataFields(payload.thinkingLevel?.trim(), [
59
+ "thinkingEffort",
60
+ "thinking",
61
+ "preferred_thinking",
62
+ ]),
63
+ ...createMetadataFields(payload.sessionType?.trim(), [
64
+ "agentRuntimeId",
65
+ "session_type",
66
+ "runtime",
67
+ ]),
68
+ ...createMetadataFields(payload.agentId?.trim(), ["agentId", "agent_id"]),
69
+ ...createMetadataFields(projectRoot ?? undefined, ["projectRoot", "project_root"]),
70
+ };
71
71
  const requestedSkills = normalizeRequestedSkills(payload.requestedSkills);
72
72
  if (requestedSkills.length > 0) {
73
73
  metadata.requested_skill_refs = requestedSkills;
@@ -0,0 +1,14 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import {
3
+ CHAT_DRAFT_SESSION_PATH,
4
+ buildSessionPath,
5
+ parseSessionKeyFromRoute,
6
+ } from '@/features/chat/utils/chat-session-route.utils';
7
+
8
+ describe('chat session route utils', () => {
9
+ it('keeps the draft route separate from real session keys', () => {
10
+ expect(CHAT_DRAFT_SESSION_PATH).toBe('/chat/draft');
11
+ expect(parseSessionKeyFromRoute('draft')).toBeNull();
12
+ expect(parseSessionKeyFromRoute(buildSessionPath('draft').slice('/chat/'.length))).toBe('draft');
13
+ });
14
+ });
@@ -1,4 +1,6 @@
1
1
  const SESSION_ROUTE_PREFIX = 'sid_';
2
+ export const CHAT_DRAFT_SESSION_PATH = '/chat/draft';
3
+ const DRAFT_SESSION_ROUTE_ID = CHAT_DRAFT_SESSION_PATH.slice('/chat/'.length);
2
4
 
3
5
  export function encodeSessionRouteId(sessionKey: string): string {
4
6
  const bytes = new TextEncoder().encode(sessionKey);
@@ -26,7 +28,7 @@ export function decodeSessionRouteId(routeValue: string): string | null {
26
28
  }
27
29
 
28
30
  export function parseSessionKeyFromRoute(routeValue?: string): string | null {
29
- if (!routeValue) {
31
+ if (!routeValue || routeValue === DRAFT_SESSION_ROUTE_ID) {
30
32
  return null;
31
33
  }
32
34
  const decodedToken = decodeSessionRouteId(routeValue);
@@ -20,10 +20,10 @@ function createStatus(overrides: Partial<SystemStatusView> = {}): SystemStatusVi
20
20
  ncpAgent: {
21
21
  state: 'ready',
22
22
  },
23
- pluginHydration: {
23
+ extensionLoading: {
24
24
  state: 'ready',
25
- loadedPluginCount: 1,
26
- totalPluginCount: 1,
25
+ loadedExtensionCount: 1,
26
+ totalExtensionCount: 1,
27
27
  },
28
28
  channels: {
29
29
  state: 'ready',
@@ -97,10 +97,10 @@ describe('ncp-chat-runtime-availability.utils', () => {
97
97
  state: 'error',
98
98
  error: 'boom',
99
99
  },
100
- pluginHydration: {
100
+ extensionLoading: {
101
101
  state: 'pending',
102
- loadedPluginCount: 0,
103
- totalPluginCount: 0,
102
+ loadedExtensionCount: 0,
103
+ totalExtensionCount: 0,
104
104
  },
105
105
  channels: {
106
106
  state: 'pending',
@@ -19,8 +19,6 @@ import { cn } from "@/shared/lib/utils";
19
19
  import {
20
20
  CheckCircle2,
21
21
  Download,
22
- Power,
23
- PowerOff,
24
22
  Trash2,
25
23
  } from "lucide-react";
26
24
 
@@ -35,7 +33,6 @@ export type ManageState = {
35
33
  type MarketplaceListCardActionProps = {
36
34
  item?: MarketplaceItemSummary;
37
35
  record?: MarketplaceInstalledRecord;
38
- pluginRecord?: MarketplaceInstalledRecord;
39
36
  isInstalling: boolean;
40
37
  isDisabled: boolean;
41
38
  canUninstall: boolean;
@@ -97,7 +94,6 @@ function MarketplaceListCardActions(props: MarketplaceListCardActionProps) {
97
94
  const {
98
95
  item,
99
96
  record,
100
- pluginRecord,
101
97
  isInstalling,
102
98
  isDisabled,
103
99
  canUninstall,
@@ -107,7 +103,7 @@ function MarketplaceListCardActions(props: MarketplaceListCardActionProps) {
107
103
  onInstall,
108
104
  onManage,
109
105
  } = props;
110
- const hasActions = Boolean((item && !record) || pluginRecord || (record && canUninstall));
106
+ const hasActions = Boolean((item && !record) || (record && canUninstall));
111
107
 
112
108
  return (
113
109
  <div
@@ -152,30 +148,6 @@ function MarketplaceListCardActions(props: MarketplaceListCardActionProps) {
152
148
  </button>
153
149
  )}
154
150
 
155
- {pluginRecord && (
156
- <button
157
- disabled={busyForRecord}
158
- onClick={(event) => {
159
- event.stopPropagation();
160
- onManage(isDisabled ? "enable" : "disable", pluginRecord);
161
- }}
162
- className="inline-flex h-8 items-center gap-1.5 whitespace-nowrap rounded-xl border border-gray-200/80 bg-white px-3 text-xs font-medium text-gray-600 transition-colors hover:border-gray-300 hover:bg-gray-50 disabled:opacity-50"
163
- >
164
- {isDisabled ? (
165
- <Power className="h-3.5 w-3.5" />
166
- ) : (
167
- <PowerOff className="h-3.5 w-3.5" />
168
- )}
169
- {busyAction && busyAction !== "uninstall"
170
- ? busyAction === "enable"
171
- ? t("marketplaceEnabling")
172
- : t("marketplaceDisabling")
173
- : isDisabled
174
- ? t("marketplaceEnable")
175
- : t("marketplaceDisable")}
176
- </button>
177
- )}
178
-
179
151
  {record && canUninstall && (
180
152
  <button
181
153
  disabled={busyForRecord}
@@ -217,7 +189,7 @@ function MarketplaceInstalledStatusIcon(props: {
217
189
  )}
218
190
  >
219
191
  {disabled ? (
220
- <PowerOff className="h-4 w-4" />
192
+ <CheckCircle2 className="h-4 w-4 opacity-40" />
221
193
  ) : (
222
194
  <CheckCircle2 className="h-4 w-4" />
223
195
  )}
@@ -253,7 +225,6 @@ export function MarketplaceListCard(props: {
253
225
  onManage,
254
226
  } = props;
255
227
  const localeFallbacks = buildLocaleFallbacks(language);
256
- const pluginRecord = record?.type === "plugin" ? record : undefined;
257
228
  const title =
258
229
  item?.name ??
259
230
  record?.label ??
@@ -269,9 +240,7 @@ export function MarketplaceListCard(props: {
269
240
  ? manageState.actionsByTarget.get(targetId)
270
241
  : undefined;
271
242
  const busyForRecord = Boolean(busyAction);
272
- const canUninstall =
273
- (record?.type === "plugin" && record.origin !== "bundled") ||
274
- (record?.type === "skill" && record.source === "workspace");
243
+ const canUninstall = record?.type === "skill" && record.source === "workspace";
275
244
  const isDisabled = record
276
245
  ? record.enabled === false || record.runtimeStatus === "disabled"
277
246
  : false;
@@ -288,7 +257,7 @@ export function MarketplaceListCard(props: {
288
257
  <div className="flex min-w-0 flex-1 gap-3">
289
258
  <MarketplaceItemIcon
290
259
  name={title}
291
- fallback={spec || t("marketplaceTypeExtension")}
260
+ fallback={spec || t("marketplaceTypeSkill")}
292
261
  />
293
262
  <div className="flex min-w-0 flex-1 flex-col justify-center">
294
263
  <MarketplaceListCardMeta title={title} spec={spec} summary={summary} />
@@ -298,7 +267,6 @@ export function MarketplaceListCard(props: {
298
267
  <MarketplaceListCardActions
299
268
  item={item}
300
269
  record={record}
301
- pluginRecord={pluginRecord}
302
270
  isInstalling={isInstalling}
303
271
  isDisabled={isDisabled}
304
272
  canUninstall={canUninstall}
@@ -44,7 +44,6 @@ vi.mock("@/shared/components/doc-browser", () => ({
44
44
 
45
45
  vi.mock("@/shared/lib/api", () => ({
46
46
  fetchMarketplaceSkillContent: mocks.fetchMarketplaceSkillContent,
47
- fetchMarketplacePluginContent: vi.fn(),
48
47
  }));
49
48
 
50
49
  vi.mock("@/app/components/i18n-provider", () => ({
@@ -1,8 +1,6 @@
1
1
  import { render, screen } from "@testing-library/react";
2
- import userEvent from "@testing-library/user-event";
3
2
  import { MarketplacePage } from "@/features/marketplace";
4
3
  import type {
5
- MarketplaceInstalledRecord,
6
4
  MarketplaceInstalledView,
7
5
  MarketplaceItemSummary,
8
6
  MarketplaceListView,
@@ -28,7 +26,7 @@ const mocks = vi.hoisted(() => ({
28
26
  navigate: vi.fn(),
29
27
  docOpen: vi.fn(),
30
28
  confirm: vi.fn(),
31
- routeParams: {} as { type?: string; scene?: string },
29
+ routeParams: {} as { scene?: string },
32
30
  itemsQuery: null as unknown as ItemsQueryState,
33
31
  installedQuery: null as unknown as InstalledQueryState,
34
32
  installMutation: {
@@ -118,41 +116,6 @@ function createMarketplaceItem(
118
116
  };
119
117
  }
120
118
 
121
- function createPluginMarketplaceItem(
122
- overrides: Partial<MarketplaceItemSummary> = {},
123
- ): MarketplaceItemSummary {
124
- return createMarketplaceItem({
125
- id: "plugin-channel-slack",
126
- slug: "channel-slack",
127
- type: "plugin",
128
- name: "Slack Channel",
129
- summary: "Optional Slack channel for NextClaw",
130
- summaryI18n: { en: "Optional Slack channel for NextClaw" },
131
- install: {
132
- kind: "npm",
133
- spec: "@nextclaw/channel-plugin-slack",
134
- command: "npm install @nextclaw/channel-plugin-slack",
135
- },
136
- ...overrides,
137
- });
138
- }
139
-
140
- function createInstalledRecord(
141
- overrides: Partial<MarketplaceInstalledRecord> = {},
142
- ): MarketplaceInstalledRecord {
143
- return {
144
- type: "plugin",
145
- id: "@nextclaw/channel-plugin-slack",
146
- spec: "@nextclaw/channel-plugin-slack",
147
- label: "Slack Channel",
148
- enabled: true,
149
- origin: "marketplace",
150
- source: "marketplace",
151
- installedAt: "2026-03-19T00:00:00.000Z",
152
- ...overrides,
153
- };
154
- }
155
-
156
119
  function createItemsQuery(overrides: Partial<Record<string, unknown>> = {}) {
157
120
  return {
158
121
  data: undefined as MarketplaceListView | undefined,
@@ -238,7 +201,7 @@ describe("MarketplacePage", () => {
238
201
  expect(screen.getByText("Web Search")).toBeTruthy();
239
202
  });
240
203
 
241
- it("does not render the redundant plugin type label in plugin cards", () => {
204
+ it("does not render a redundant type label in skill cards", () => {
242
205
  mocks.itemsQuery = createItemsQuery({
243
206
  data: {
244
207
  total: 1,
@@ -247,31 +210,16 @@ describe("MarketplacePage", () => {
247
210
  totalPages: 1,
248
211
  sort: "relevance",
249
212
  items: [
250
- createMarketplaceItem({
251
- id: "plugin-channel-slack",
252
- slug: "channel-slack",
253
- type: "plugin",
254
- name: "Slack Channel",
255
- summary: "Optional Slack channel for NextClaw",
256
- summaryI18n: { en: "Optional Slack channel for NextClaw" },
257
- install: {
258
- kind: "npm",
259
- spec: "@nextclaw/channel-plugin-slack",
260
- command:
261
- "npm install @nextclaw/channel-plugin-slack",
262
- },
263
- }),
213
+ createMarketplaceItem(),
264
214
  ],
265
215
  } satisfies MarketplaceListView,
266
216
  });
267
217
 
268
- const { container } = render(<MarketplacePage forcedType="plugins" />);
218
+ const { container } = render(<MarketplacePage forcedType="skills" />);
269
219
  const card = container.querySelector("article");
270
220
 
271
- expect(card?.textContent).toContain(
272
- "@nextclaw/channel-plugin-slack",
273
- );
274
- expect(card?.textContent).not.toContain("Plugin");
221
+ expect(card?.textContent).toContain("@nextclaw/web-search");
222
+ expect(card?.textContent).not.toContain("Skill");
275
223
  });
276
224
 
277
225
  it("does not dim the loaded list during background refresh", () => {
@@ -282,81 +230,14 @@ describe("MarketplacePage", () => {
282
230
  pageSize: 20,
283
231
  totalPages: 1,
284
232
  sort: "relevance",
285
- items: [createPluginMarketplaceItem()],
233
+ items: [createMarketplaceItem()],
286
234
  } satisfies MarketplaceListView,
287
235
  isFetching: true,
288
236
  });
289
237
 
290
- const { container } = render(<MarketplacePage forcedType="plugins" />);
238
+ const { container } = render(<MarketplacePage forcedType="skills" />);
291
239
 
292
- expect(screen.getByText("Slack Channel")).toBeTruthy();
240
+ expect(screen.getByText("Web Search")).toBeTruthy();
293
241
  expect(container.querySelector(".opacity-70")).toBeNull();
294
242
  });
295
-
296
- it("only disables the targeted plugin action while a manage request is pending", async () => {
297
- const user = userEvent.setup();
298
- let resolveMutation: (() => void) | undefined;
299
- mocks.itemsQuery = createItemsQuery({
300
- data: {
301
- total: 2,
302
- page: 1,
303
- pageSize: 20,
304
- totalPages: 1,
305
- sort: "relevance",
306
- items: [
307
- createPluginMarketplaceItem(),
308
- createPluginMarketplaceItem({
309
- id: "plugin-channel-discord",
310
- slug: "channel-discord",
311
- name: "Discord Channel",
312
- install: {
313
- kind: "npm",
314
- spec: "@nextclaw/channel-plugin-discord",
315
- command:
316
- "npm install @nextclaw/channel-plugin-discord",
317
- },
318
- }),
319
- ],
320
- } satisfies MarketplaceListView,
321
- });
322
- mocks.installedQuery = createInstalledQuery({
323
- data: {
324
- type: "plugin",
325
- total: 2,
326
- specs: [
327
- "@nextclaw/channel-plugin-slack",
328
- "@nextclaw/channel-plugin-discord",
329
- ],
330
- records: [
331
- createInstalledRecord(),
332
- createInstalledRecord({
333
- id: "@nextclaw/channel-plugin-discord",
334
- spec: "@nextclaw/channel-plugin-discord",
335
- label: "Discord Channel",
336
- }),
337
- ],
338
- } satisfies MarketplaceInstalledView,
339
- });
340
- mocks.manageMutation.mutateAsync.mockImplementation(
341
- () =>
342
- new Promise<void>((resolve) => {
343
- resolveMutation = resolve;
344
- }),
345
- );
346
-
347
- render(<MarketplacePage forcedType="plugins" />);
348
-
349
- const disableButtons = screen.getAllByRole("button", { name: "Disable" });
350
- const firstDisableButton = disableButtons[0];
351
- const secondDisableButton = disableButtons[1];
352
-
353
- await user.click(firstDisableButton);
354
-
355
- expect(mocks.manageMutation.mutateAsync).toHaveBeenCalledTimes(1);
356
- expect(firstDisableButton.hasAttribute("disabled")).toBe(true);
357
- expect(firstDisableButton.textContent).toContain("Disabling");
358
- expect(secondDisableButton.hasAttribute("disabled")).toBe(false);
359
-
360
- resolveMutation?.();
361
- });
362
243
  });