@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
@@ -3,15 +3,11 @@ import type {
3
3
  MarketplaceInstalledRecord,
4
4
  MarketplaceItemSummary,
5
5
  MarketplaceManageAction,
6
- MarketplacePluginContentView,
7
6
  MarketplaceSkillContentView,
8
7
  MarketplaceSort,
9
8
  MarketplaceItemType,
10
9
  } from "@/shared/lib/api";
11
- import {
12
- fetchMarketplacePluginContent,
13
- fetchMarketplaceSkillContent,
14
- } from "@/shared/lib/api";
10
+ import { fetchMarketplaceSkillContent } from "@/shared/lib/api";
15
11
  import { useDocBrowser } from "@/shared/components/doc-browser";
16
12
  import { useI18n } from "@/app/components/i18n-provider";
17
13
  import { useConfirmDialog } from "@/shared/hooks/use-confirm-dialog";
@@ -59,28 +55,11 @@ const SKELETON_CARD_COUNT = 36;
59
55
 
60
56
  type ScopeType = "all" | "installed";
61
57
 
62
- type MarketplaceRouteType = "plugins" | "skills";
63
58
  type MarketplacePageProps = {
64
- forcedType?: MarketplaceRouteType;
59
+ forcedType?: "skills";
65
60
  };
66
61
 
67
- function getMarketplaceCopyKeys(typeFilter: MarketplaceItemType) {
68
- if (typeFilter === "plugin") {
69
- return {
70
- pageTitle: "marketplacePluginsPageTitle",
71
- pageDescription: "marketplacePluginsPageDescription",
72
- tabMarketplace: "marketplaceTabMarketplacePlugins",
73
- tabInstalled: "marketplaceTabInstalledPlugins",
74
- searchPlaceholder: "marketplaceSearchPlaceholderPlugins",
75
- sectionCatalog: "marketplaceSectionPlugins",
76
- sectionInstalled: "marketplaceSectionInstalledPlugins",
77
- errorLoadData: "marketplaceErrorLoadingPluginsData",
78
- errorLoadInstalled: "marketplaceErrorLoadingInstalledPlugins",
79
- emptyData: "marketplaceNoPlugins",
80
- emptyInstalled: "marketplaceNoInstalledPlugins",
81
- installedCountSuffix: "marketplaceInstalledPluginsCountSuffix",
82
- };
83
- }
62
+ function getMarketplaceCopyKeys() {
84
63
  return {
85
64
  pageTitle: "marketplaceSkillsPageTitle",
86
65
  pageDescription: "marketplaceSkillsPageDescription",
@@ -100,37 +79,17 @@ function getMarketplaceCopyKeys(typeFilter: MarketplaceItemType) {
100
79
  export function MarketplacePage(props: MarketplacePageProps = {}) {
101
80
  const { forcedType } = props;
102
81
  const navigate = useNavigate();
103
- const params = useParams<{ type?: string; scene?: string }>();
82
+ const params = useParams<{ scene?: string }>();
104
83
  const { language } = useI18n();
105
84
  const docBrowser = useDocBrowser();
106
85
 
107
- const routeType: MarketplaceRouteType | null = useMemo(() => {
108
- if (forcedType === "plugins" || forcedType === "skills") {
109
- return forcedType;
110
- }
111
- if (params.type === "plugins" || params.type === "skills") {
112
- return params.type;
113
- }
114
- return null;
115
- }, [forcedType, params.type]);
116
-
117
- useEffect(() => {
118
- if (forcedType) {
119
- return;
120
- }
121
- if (!routeType) {
122
- navigate("/marketplace/plugins", { replace: true });
123
- }
124
- }, [forcedType, routeType, navigate]);
125
-
126
- const typeFilter: MarketplaceItemType =
127
- routeType === "skills" ? "skill" : "plugin";
86
+ const typeFilter: MarketplaceItemType = "skill";
128
87
  const localeFallbacks = useMemo(
129
88
  () => buildLocaleFallbacks(language),
130
89
  [language],
131
90
  );
132
91
 
133
- const copyKeys = getMarketplaceCopyKeys(typeFilter);
92
+ const copyKeys = getMarketplaceCopyKeys();
134
93
 
135
94
  const [searchText, setSearchText] = useState("");
136
95
  const [query, setQuery] = useState("");
@@ -302,12 +261,8 @@ export function MarketplacePage(props: MarketplacePageProps = {}) {
302
261
  type: item.type,
303
262
  spec: installSpec,
304
263
  kind: item.install.kind,
305
- ...(item.type === "skill"
306
- ? {
307
- skill: item.slug,
308
- installPath: `skills/${item.slug}`,
309
- }
310
- : {}),
264
+ skill: item.slug,
265
+ installPath: `skills/${item.slug}`,
311
266
  });
312
267
  } catch {
313
268
  // handled in mutation onError
@@ -391,10 +346,7 @@ export function MarketplacePage(props: MarketplacePageProps = {}) {
391
346
  if (!item) {
392
347
  const url = buildGenericDetailDataUrl({
393
348
  title,
394
- typeLabel:
395
- record?.type === "plugin"
396
- ? t("marketplaceTypePlugin")
397
- : t("marketplaceTypeSkill"),
349
+ typeLabel: t("marketplaceTypeSkill"),
398
350
  spec: record?.spec ?? "-",
399
351
  summary: t("marketplaceInstalledLocalSummary"),
400
352
  metadataRaw: JSON.stringify(record ?? {}, null, 2),
@@ -413,19 +365,12 @@ export function MarketplacePage(props: MarketplacePageProps = {}) {
413
365
  localeFallbacks,
414
366
  );
415
367
  const detailConfig =
416
- item.type === "skill"
417
- ? {
418
- typeLabel: t("marketplaceTypeSkill"),
419
- loadContent: () => fetchMarketplaceSkillContent(item.slug),
420
- fallbackContent: t("marketplaceOperationFailed"),
421
- defaultContent: undefined,
422
- }
423
- : {
424
- typeLabel: t("marketplaceTypePlugin"),
425
- loadContent: () => fetchMarketplacePluginContent(item.slug),
426
- fallbackContent: "-",
427
- defaultContent: item.summary,
428
- };
368
+ {
369
+ typeLabel: t("marketplaceTypeSkill"),
370
+ loadContent: () => fetchMarketplaceSkillContent(item.slug),
371
+ fallbackContent: t("marketplaceOperationFailed"),
372
+ defaultContent: undefined,
373
+ };
429
374
  docBrowser.open(
430
375
  buildGenericDetailDataUrl({
431
376
  title,
@@ -436,8 +381,7 @@ export function MarketplacePage(props: MarketplacePageProps = {}) {
436
381
  openOptions,
437
382
  );
438
383
  try {
439
- const content: MarketplaceSkillContentView | MarketplacePluginContentView =
440
- await detailConfig.loadContent();
384
+ const content: MarketplaceSkillContentView = await detailConfig.loadContent();
441
385
  if (detailRequestRef.current.byKey.get(dedupeKey) !== requestId) {
442
386
  return;
443
387
  }
@@ -14,7 +14,7 @@ type MarketplaceCuratedSceneRouteParams = {
14
14
  items: MarketplaceItemSummary[];
15
15
  installedRecordLookup: Map<string, MarketplaceInstalledRecord>;
16
16
  scene?: string;
17
- forcedType?: "plugins" | "skills";
17
+ forcedType?: "skills";
18
18
  typeFilter: MarketplaceItemType;
19
19
  scope: "all" | "installed";
20
20
  searchText: string;
@@ -148,13 +148,7 @@ export function useInstallMarketplaceItem() {
148
148
  queryKey: ['marketplace-installed', result.type],
149
149
  refetchType: 'inactive'
150
150
  });
151
- if (result.type === 'plugin') {
152
- queryClient.invalidateQueries({ queryKey: ['ncp-session-types'] });
153
- }
154
- const fallback = result.type === 'plugin'
155
- ? t('marketplaceInstallSuccessPlugin')
156
- : t('marketplaceInstallSuccessSkill');
157
- toast.success(result.message || fallback);
151
+ toast.success(result.message || t('marketplaceInstallSuccessSkill'));
158
152
  },
159
153
  onError: (error: Error) => {
160
154
  toast.error(error.message || t('marketplaceInstallFailed'));
@@ -180,9 +174,6 @@ export function useManageMarketplaceItem() {
180
174
  queryKey: ['marketplace-installed', result.type],
181
175
  refetchType: 'inactive'
182
176
  });
183
- if (result.type === 'plugin') {
184
- queryClient.invalidateQueries({ queryKey: ['ncp-session-types'] });
185
- }
186
177
  const fallback = result.action === 'enable'
187
178
  ? t('marketplaceEnableSuccess')
188
179
  : result.action === 'disable'
@@ -11,57 +11,59 @@ import {
11
11
  } from './marketplace-installed-cache.utils';
12
12
 
13
13
  describe('marketplace-installed-cache', () => {
14
- it('adds a plugin record immediately after install success', () => {
14
+ it('adds an MCP record immediately after install success', () => {
15
15
  const request: MarketplaceInstallRequest = {
16
- type: 'plugin',
17
- spec: '@nextclaw/channel-plugin-slack',
18
- kind: 'npm'
16
+ type: 'mcp',
17
+ spec: 'filesystem',
18
+ kind: 'template',
19
+ name: 'filesystem'
19
20
  };
20
21
  const result: MarketplaceInstallResult = {
21
- type: 'plugin',
22
- spec: '@nextclaw/channel-plugin-slack',
22
+ type: 'mcp',
23
+ spec: 'filesystem',
24
+ name: 'filesystem',
23
25
  message: 'installed'
24
26
  };
25
27
 
26
28
  const next = applyInstallResultToInstalledView({ request, result });
27
29
 
28
30
  expect(next.total).toBe(1);
29
- expect(next.specs).toEqual(['@nextclaw/channel-plugin-slack']);
31
+ expect(next.specs).toEqual(['filesystem']);
30
32
  expect(next.records[0]).toMatchObject({
31
- type: 'plugin',
32
- spec: '@nextclaw/channel-plugin-slack',
33
+ type: 'mcp',
34
+ spec: 'filesystem',
33
35
  enabled: true,
34
36
  origin: 'marketplace',
35
37
  runtimeStatus: 'ready'
36
38
  });
37
39
  });
38
40
 
39
- it('marks a plugin record as disabled immediately after disable success', () => {
41
+ it('marks an MCP record as disabled immediately after disable success', () => {
40
42
  const view: MarketplaceInstalledView = {
41
- type: 'plugin',
43
+ type: 'mcp',
42
44
  total: 1,
43
- specs: ['@nextclaw/channel-plugin-slack'],
45
+ specs: ['filesystem'],
44
46
  records: [
45
47
  {
46
- type: 'plugin',
47
- id: '@nextclaw/channel-plugin-slack',
48
- spec: '@nextclaw/channel-plugin-slack',
49
- label: 'Slack Channel',
48
+ type: 'mcp',
49
+ id: 'filesystem',
50
+ spec: 'filesystem',
51
+ label: 'Filesystem',
50
52
  enabled: true,
51
53
  origin: 'marketplace'
52
54
  }
53
55
  ]
54
56
  };
55
57
  const request: MarketplaceManageRequest = {
56
- type: 'plugin',
58
+ type: 'mcp',
57
59
  action: 'disable',
58
- id: '@nextclaw/channel-plugin-slack',
59
- spec: '@nextclaw/channel-plugin-slack'
60
+ id: 'filesystem',
61
+ spec: 'filesystem'
60
62
  };
61
63
  const result: MarketplaceManageResult = {
62
- type: 'plugin',
64
+ type: 'mcp',
63
65
  action: 'disable',
64
- id: '@nextclaw/channel-plugin-slack',
66
+ id: 'filesystem',
65
67
  message: 'disabled'
66
68
  };
67
69
 
@@ -27,10 +27,10 @@ describe('getRuntimeBootstrapPollInterval', () => {
27
27
  ncpAgent: {
28
28
  state: 'running',
29
29
  },
30
- pluginHydration: {
30
+ extensionLoading: {
31
31
  state: 'pending',
32
- loadedPluginCount: 0,
33
- totalPluginCount: 0,
32
+ loadedExtensionCount: 0,
33
+ totalExtensionCount: 0,
34
34
  },
35
35
  channels: {
36
36
  state: 'pending',
@@ -51,10 +51,10 @@ describe('getRuntimeBootstrapPollInterval', () => {
51
51
  state: 'error',
52
52
  error: 'startup failed',
53
53
  },
54
- pluginHydration: {
54
+ extensionLoading: {
55
55
  state: 'pending',
56
- loadedPluginCount: 0,
57
- totalPluginCount: 0,
56
+ loadedExtensionCount: 0,
57
+ totalExtensionCount: 0,
58
58
  },
59
59
  channels: {
60
60
  state: 'pending',
@@ -75,10 +75,10 @@ describe('getRuntimeBootstrapPollInterval', () => {
75
75
  ncpAgent: {
76
76
  state: 'ready',
77
77
  },
78
- pluginHydration: {
78
+ extensionLoading: {
79
79
  state: 'ready',
80
- loadedPluginCount: 1,
81
- totalPluginCount: 1,
80
+ loadedExtensionCount: 1,
81
+ totalExtensionCount: 1,
82
82
  },
83
83
  channels: {
84
84
  state: 'ready',
@@ -108,10 +108,10 @@ describe('getRuntimeBootstrapPollInterval', () => {
108
108
  ncpAgent: {
109
109
  state: 'ready',
110
110
  },
111
- pluginHydration: {
111
+ extensionLoading: {
112
112
  state: 'ready',
113
- loadedPluginCount: 1,
114
- totalPluginCount: 1,
113
+ loadedExtensionCount: 1,
114
+ totalExtensionCount: 1,
115
115
  },
116
116
  channels: {
117
117
  state: 'ready',
@@ -10,10 +10,10 @@ const readyBootstrapStatus: BootstrapStatusView = {
10
10
  ncpAgent: {
11
11
  state: 'ready',
12
12
  },
13
- pluginHydration: {
13
+ extensionLoading: {
14
14
  state: 'ready',
15
- loadedPluginCount: 1,
16
- totalPluginCount: 1,
15
+ loadedExtensionCount: 1,
16
+ totalExtensionCount: 1,
17
17
  },
18
18
  channels: {
19
19
  state: 'ready',
@@ -53,10 +53,10 @@ describe('systemStatusManager', () => {
53
53
  state: 'error',
54
54
  error: 'startup failed',
55
55
  },
56
- pluginHydration: {
56
+ extensionLoading: {
57
57
  state: 'pending',
58
- loadedPluginCount: 0,
59
- totalPluginCount: 0,
58
+ loadedExtensionCount: 0,
59
+ totalExtensionCount: 0,
60
60
  },
61
61
  channels: {
62
62
  state: 'pending',
package/src/index.css CHANGED
@@ -44,7 +44,7 @@
44
44
  user-select: none;
45
45
  }
46
46
 
47
- .desktop-window-no-drag {
47
+ .desktop-window-no-drag, .desktop-window-drag :is(button, a, input, select, textarea, [role='button'], [tabindex]) {
48
48
  -webkit-app-region: no-drag;
49
49
  app-region: no-drag;
50
50
  }
@@ -1,5 +1,6 @@
1
1
  import { act, render, screen } from "@testing-library/react";
2
2
  import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
3
+ import { MemoryRouter } from "react-router-dom";
3
4
  import { afterEach, describe, expect, it, vi } from "vitest";
4
5
  import { DesktopAppShell } from "@/platforms/desktop/components/desktop-app-shell";
5
6
 
@@ -7,6 +8,10 @@ vi.mock("@/app/components/layout/sidebar", () => ({
7
8
  Sidebar: () => <aside data-testid="settings-sidebar">Settings Sidebar</aside>,
8
9
  }));
9
10
 
11
+ vi.mock("@/platforms/mobile", () => ({
12
+ MobileBottomNav: () => <nav data-testid="mobile-bottom-nav" />,
13
+ }));
14
+
10
15
  type WindowStateListener = (snapshot: { isMaximized: boolean }) => void;
11
16
 
12
17
  let windowStateListener: WindowStateListener | null = null;
@@ -27,7 +32,7 @@ function setDesktopPlatform(platform: string | null, isMaximized = false): void
27
32
  : undefined;
28
33
  }
29
34
 
30
- function renderDesktopShell(platform: string | null) {
35
+ function renderDesktopShell(platform: string | null, isMobileLayout = false) {
31
36
  setDesktopPlatform(platform);
32
37
  const queryClient = new QueryClient({
33
38
  defaultOptions: {
@@ -39,13 +44,16 @@ function renderDesktopShell(platform: string | null) {
39
44
 
40
45
  return render(
41
46
  <QueryClientProvider client={queryClient}>
42
- <DesktopAppShell
43
- pathname="/chat"
44
- isDocBrowserOpen={false}
45
- docBrowserMode="floating"
46
- >
47
- <div data-testid="app-content">App Content</div>
48
- </DesktopAppShell>
47
+ <MemoryRouter initialEntries={["/chat"]}>
48
+ <DesktopAppShell
49
+ pathname="/chat"
50
+ isMobileLayout={isMobileLayout}
51
+ isDocBrowserOpen={false}
52
+ docBrowserMode="floating"
53
+ >
54
+ <div data-testid="app-content">App Content</div>
55
+ </DesktopAppShell>
56
+ </MemoryRouter>
49
57
  </QueryClientProvider>,
50
58
  );
51
59
  }
@@ -60,19 +68,19 @@ describe("DesktopAppShell", () => {
60
68
 
61
69
  const chrome = screen.getByTestId("desktop-window-chrome");
62
70
  const sidebarChrome = screen.getByTestId("desktop-window-chrome-sidebar");
63
- const mainDragRegion = screen.getByTestId("desktop-window-chrome-main-drag-region");
64
71
  const resizeStrip = screen.getByTestId("desktop-window-chrome-resize-strip");
65
72
  const controls = screen.getByTestId("desktop-window-controls");
66
73
 
67
74
  expect(chrome).toBeTruthy();
68
75
  expect(chrome.parentElement?.style.getPropertyValue("--desktop-titlebar-height")).toBe("40px");
69
76
  expect(chrome.className).toContain("bg-secondary");
70
- expect(chrome.className).toContain("border-b");
77
+ expect(chrome.className).not.toContain(" shrink-0 border-b ");
78
+ expect(chrome.className).toContain("after:left-[var(--desktop-sidebar-width)]");
79
+ expect(chrome.className).toContain("after:border-b");
71
80
  expect(chrome.className).toContain("desktop-window-drag");
72
81
  expect(sidebarChrome.className).toContain("w-[var(--desktop-sidebar-width)]");
73
- expect(sidebarChrome.className).toContain("desktop-window-no-drag");
74
- expect(mainDragRegion.className).toContain("flex-1");
75
- expect(mainDragRegion.className).toContain("desktop-window-drag");
82
+ expect(sidebarChrome.className).not.toContain("border-b");
83
+ expect(sidebarChrome.className).toContain("desktop-window-drag");
76
84
  expect(resizeStrip.className).toContain("desktop-window-no-drag");
77
85
  expect(resizeStrip.className).toContain("top-0");
78
86
  expect(resizeStrip.className).toContain("h-1");
@@ -102,4 +110,12 @@ describe("DesktopAppShell", () => {
102
110
  expect(screen.queryByTestId("desktop-window-chrome")).toBeNull();
103
111
  expect(screen.getByTestId("app-content")).toBeTruthy();
104
112
  });
113
+
114
+ it("keeps mobile bottom navigation visible inside Windows desktop hosts at narrow widths", () => {
115
+ renderDesktopShell("win32", true);
116
+
117
+ expect(screen.getByTestId("desktop-window-chrome")).toBeTruthy();
118
+ expect(screen.getByTestId("mobile-bottom-nav")).toBeTruthy();
119
+ expect(screen.getByTestId("app-content")).toBeTruthy();
120
+ });
105
121
  });
@@ -1,9 +1,13 @@
1
1
  import { lazy, Suspense } from "react";
2
2
  import type { CSSProperties } from "react";
3
- import { isMainWorkspaceRoute } from "@/app/configs/app-navigation.config";
3
+ import {
4
+ isChatSessionDetailRoute,
5
+ isMainWorkspaceRoute,
6
+ } from "@/app/configs/app-navigation.config";
4
7
  import { Sidebar } from "@/app/components/layout/sidebar";
5
8
  import { DesktopWindowChrome } from "@/platforms/desktop/components/desktop-window-chrome";
6
9
  import { isWindowsDesktopHost } from "@/platforms/desktop/utils/desktop-host.utils";
10
+ import { MobileBottomNav } from "@/platforms/mobile";
7
11
  import { cn } from "@/shared/lib/utils";
8
12
 
9
13
  const DocBrowser = lazy(async () => ({
@@ -12,32 +16,21 @@ const DocBrowser = lazy(async () => ({
12
16
 
13
17
  type DesktopAppShellProps = {
14
18
  pathname: string;
19
+ isMobileLayout?: boolean;
15
20
  isDocBrowserOpen: boolean;
16
21
  docBrowserMode: "floating" | "docked";
17
22
  children: React.ReactNode;
18
23
  };
19
24
 
20
- type DesktopShellStyle = CSSProperties & {
21
- "--desktop-titlebar-height"?: string;
22
- "--desktop-caption-safe-right"?: string;
23
- "--desktop-sidebar-width"?: string;
24
- };
25
-
26
- function createWindowsDesktopShellStyle(isMainRoute: boolean): DesktopShellStyle {
27
- return {
28
- "--desktop-titlebar-height": "40px",
29
- "--desktop-caption-safe-right": "140px",
30
- "--desktop-sidebar-width": isMainRoute ? "280px" : "240px",
31
- };
32
- }
33
-
34
25
  export function DesktopAppShell({
35
26
  pathname,
27
+ isMobileLayout = false,
36
28
  isDocBrowserOpen,
37
29
  docBrowserMode,
38
30
  children,
39
31
  }: DesktopAppShellProps) {
40
32
  const isMainRoute = isMainWorkspaceRoute(pathname);
33
+ const showMobileBottomNav = isMobileLayout && !isChatSessionDetailRoute(pathname);
41
34
  const shouldUseWindowsChrome = isWindowsDesktopHost();
42
35
 
43
36
  return (
@@ -46,7 +39,11 @@ export function DesktopAppShell({
46
39
  "h-screen flex flex-col overflow-hidden bg-background font-sans text-foreground",
47
40
  shouldUseWindowsChrome ? "rounded-[10px]" : null,
48
41
  )}
49
- style={shouldUseWindowsChrome ? createWindowsDesktopShellStyle(isMainRoute) : undefined}
42
+ style={shouldUseWindowsChrome ? ({
43
+ "--desktop-titlebar-height": "40px",
44
+ "--desktop-caption-safe-right": "140px",
45
+ "--desktop-sidebar-width": isMainRoute ? "280px" : "240px",
46
+ } as CSSProperties) : undefined}
50
47
  >
51
48
  {shouldUseWindowsChrome ? <DesktopWindowChrome /> : null}
52
49
  <div className="flex min-h-0 flex-1 overflow-hidden">
@@ -70,6 +67,7 @@ export function DesktopAppShell({
70
67
  ) : null}
71
68
  </div>
72
69
  </div>
70
+ {showMobileBottomNav ? <MobileBottomNav /> : null}
73
71
  {isDocBrowserOpen && docBrowserMode === "floating" ? (
74
72
  <Suspense fallback={null}>
75
73
  <DocBrowser />
@@ -25,7 +25,7 @@ export function DesktopWindowChrome() {
25
25
 
26
26
  return (
27
27
  <header
28
- className="desktop-window-drag relative flex h-[var(--desktop-titlebar-height)] shrink-0 border-b border-[#ebe7dc]/80 bg-secondary"
28
+ className="desktop-window-drag relative flex h-[var(--desktop-titlebar-height)] shrink-0 bg-secondary after:absolute after:bottom-0 after:left-[var(--desktop-sidebar-width)] after:right-0 after:border-b after:border-[#ebe7dc]/80"
29
29
  data-testid="desktop-window-chrome"
30
30
  >
31
31
  <div
@@ -33,7 +33,7 @@ export function DesktopWindowChrome() {
33
33
  data-testid="desktop-window-chrome-resize-strip"
34
34
  />
35
35
  <div
36
- className="desktop-window-no-drag relative z-10 flex h-full w-[var(--desktop-sidebar-width)] shrink-0 items-center bg-secondary pl-4 pr-3 text-secondary-foreground"
36
+ className="desktop-window-drag relative z-10 flex h-full w-[var(--desktop-sidebar-width)] shrink-0 items-center bg-secondary pl-4 pr-3 text-secondary-foreground"
37
37
  data-testid="desktop-window-chrome-sidebar"
38
38
  >
39
39
  <BrandHeader
@@ -41,7 +41,6 @@ export function DesktopWindowChrome() {
41
41
  density="chrome"
42
42
  />
43
43
  </div>
44
- <div aria-hidden="true" className="desktop-window-drag h-full min-w-0 flex-1" data-testid="desktop-window-chrome-main-drag-region" />
45
44
  <div
46
45
  className="desktop-window-no-drag absolute right-0 top-0 z-20 flex h-full w-[var(--desktop-caption-safe-right)] items-start justify-end"
47
46
  data-testid="desktop-window-controls"
@@ -13,12 +13,14 @@ const DocBrowser = lazy(async () => ({
13
13
  type MobileAppShellProps = {
14
14
  pathname: string;
15
15
  isDocBrowserOpen: boolean;
16
+ topbarLeadingInset?: string;
16
17
  children: React.ReactNode;
17
18
  };
18
19
 
19
20
  export function MobileAppShell({
20
21
  pathname,
21
22
  isDocBrowserOpen,
23
+ topbarLeadingInset,
22
24
  children,
23
25
  }: MobileAppShellProps) {
24
26
  const isMainRoute = isMainWorkspaceRoute(pathname);
@@ -27,7 +29,7 @@ export function MobileAppShell({
27
29
 
28
30
  return (
29
31
  <div className="flex h-[100svh] flex-col bg-background font-sans text-foreground supports-[height:100dvh]:h-[100dvh]">
30
- {showTopbar ? <MobileTopbar /> : null}
32
+ {showTopbar ? <MobileTopbar leadingInset={topbarLeadingInset} /> : null}
31
33
  <div className="relative flex-1 min-h-0 overflow-hidden">
32
34
  {isMainRoute ? (
33
35
  <div className="h-full min-h-0 overflow-hidden">{children}</div>
@@ -3,7 +3,7 @@ import { useNavigate, useLocation } from "react-router-dom";
3
3
  import { resolveMobileRouteMeta } from "@/app/configs/app-navigation.config";
4
4
  import { t } from "@/shared/lib/i18n";
5
5
 
6
- export function MobileTopbar() {
6
+ export function MobileTopbar({ leadingInset }: { leadingInset?: string }) {
7
7
  const navigate = useNavigate();
8
8
  const { pathname } = useLocation();
9
9
  const routeMeta = resolveMobileRouteMeta(pathname, t);
@@ -16,7 +16,10 @@ export function MobileTopbar() {
16
16
  paddingTop: "calc(env(safe-area-inset-top, 0px) + 0.25rem)",
17
17
  }}
18
18
  >
19
- <div className="flex min-h-[2.75rem] items-center gap-2 px-3 py-1.5">
19
+ <div
20
+ className="flex min-h-[2.75rem] items-center gap-2 py-1.5 pl-3 pr-3"
21
+ style={{ paddingLeft: leadingInset ? `calc(${leadingInset} + 0.75rem)` : undefined }}
22
+ >
20
23
  {routeMeta.backTarget ? (
21
24
  <button
22
25
  type="button"
@@ -26,9 +29,7 @@ export function MobileTopbar() {
26
29
  >
27
30
  <ArrowLeft className="h-4 w-4" />
28
31
  </button>
29
- ) : (
30
- null
31
- )}
32
+ ) : null}
32
33
  <div className="min-w-0 flex-1">
33
34
  <h1 className="truncate text-center text-[15px] font-semibold text-gray-900">
34
35
  {routeMeta.title}
@@ -1,3 +1,4 @@
1
1
  export { MobileAppShell } from "./components/mobile-app-shell";
2
+ export { MobileBottomNav } from "./components/mobile-bottom-nav";
2
3
  export { MobileSettingsShell } from "./components/mobile-settings-shell";
3
4
  export { ChatMobileShell } from "./components/chat-mobile-shell";