@nextclaw/ui 0.14.4 → 0.15.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 (194) hide show
  1. package/CHANGELOG.md +54 -0
  2. package/dist/assets/api-DLtsiBKm.js +13 -0
  3. package/dist/assets/appearance-settings-page-CtkeYXux.js +1 -0
  4. package/dist/assets/book-open-BQwWBDKb.js +1 -0
  5. package/dist/assets/channels-list-page-Cv8o9Hxg.js +8 -0
  6. package/dist/assets/chat-page-BtA7H1Ea.js +1 -0
  7. package/dist/assets/{config-split-page-CRhlZ_pH.js → config-split-page-EidRBpP2.js} +1 -1
  8. package/dist/assets/confirm-dialog-iIJFFp2T.js +5 -0
  9. package/dist/assets/desktop-update-config-C6n_TB1r.js +1 -0
  10. package/dist/assets/{dist-CikkhOlq.js → dist-CBxWOazz.js} +1 -1
  11. package/dist/assets/{dist-PVA_xL1D.js → dist-D2eZC3OR.js} +1 -1
  12. package/dist/assets/doc-browser-B76XOXmW.js +1 -0
  13. package/dist/assets/doc-browser-DBnj9UMB.js +1 -0
  14. package/dist/assets/{doc-browser-context-MTw4EvKd.js → doc-browser-context-Sv-044Cl.js} +1 -1
  15. package/dist/assets/{ellipsis-y6vDtq4M.js → ellipsis-2uzhypw5.js} +1 -1
  16. package/dist/assets/es2015-DhQo_oS6.js +41 -0
  17. package/dist/assets/{external-link-DVJIQf_G.js → external-link-CM5dnUI6.js} +1 -1
  18. package/dist/assets/host-capabilities-9bB_998Z.js +1 -0
  19. package/dist/assets/i18n-provider-di1waTGX.js +1 -0
  20. package/dist/assets/index-CBRP-7-a.css +1 -0
  21. package/dist/assets/index-Z1fYpbNd.js +104 -0
  22. package/dist/assets/loader-circle-BjmN5DeJ.js +1 -0
  23. package/dist/assets/mcp-marketplace-page-C_MVQUbT.js +9 -0
  24. package/dist/assets/mcp-marketplace-page-CoZIiD6G.js +1 -0
  25. package/dist/assets/model-config-page-Cdr9P6TQ.js +1 -0
  26. package/dist/assets/plus-3ztRZD_l.js +1 -0
  27. package/dist/assets/provider-scoped-model-input-g1WkGiJ8.js +1 -0
  28. package/dist/assets/providers-config-page-CWlO7J4A.js +1 -0
  29. package/dist/assets/{react-VNqT5Zp7.js → react-DjpzoZXB.js} +1 -1
  30. package/dist/assets/refresh-cw-DaTv3zxE.js +1 -0
  31. package/dist/assets/remote-CKWZfVVB.js +1 -0
  32. package/dist/assets/rotate-cw-CHcaRUDz.js +1 -0
  33. package/dist/assets/runtime-config-page-CfTqh1nv.js +1 -0
  34. package/dist/assets/{save-BOJ477H-.js → save-BZof1uie.js} +1 -1
  35. package/dist/assets/{search-BEti5beA.js → search-MxAarS_g.js} +1 -1
  36. package/dist/assets/search-config-page-Dm8daYny.js +1 -0
  37. package/dist/assets/{secrets-config-page--IzrIOid.js → secrets-config-page-D8AbcR05.js} +2 -2
  38. package/dist/assets/security-config-Ds0LsJmo.js +1 -0
  39. package/dist/assets/select-D7GcNP0z.js +1 -0
  40. package/dist/assets/setting-row-CuozlnJd.js +1 -0
  41. package/dist/assets/side-dock-BcLau5S_.js +3 -0
  42. package/dist/assets/skeleton-shfmXuYp.js +1 -0
  43. package/dist/assets/switch-C1BWMU5c.js +1 -0
  44. package/dist/assets/tag-chip-DCUrQUCP.js +1 -0
  45. package/dist/assets/tooltip-DfAHHA76.js +1 -0
  46. package/dist/assets/use-config-DAdbT5QJ.js +1 -0
  47. package/dist/assets/use-confirm-dialog-eWcJ6V-s.js +1 -0
  48. package/dist/assets/x-DqL1mYLC.js +1 -0
  49. package/dist/index.html +53 -17
  50. package/index.html +25 -2
  51. package/package.json +9 -9
  52. package/src/app/components/layout/__tests__/app-layout.test.tsx +27 -0
  53. package/src/app/components/layout/__tests__/settings-entry-page.test.tsx +9 -1
  54. package/src/app/components/layout/__tests__/sidebar.layout.test.tsx +6 -3
  55. package/src/app/components/layout/app-layout.tsx +10 -3
  56. package/src/app/components/layout/sidebar-items.tsx +8 -8
  57. package/src/app/components/layout/sidebar-rail.styles.ts +4 -2
  58. package/src/app/components/layout/sidebar.tsx +21 -28
  59. package/src/app/configs/app-navigation.config.ts +11 -5
  60. package/src/app/index.tsx +8 -0
  61. package/src/app/managers/__tests__/viewport-layout.manager.test.ts +59 -0
  62. package/src/app/managers/viewport-layout.manager.ts +39 -0
  63. package/src/app/presenters/app.presenter.ts +25 -2
  64. package/src/app/styles/design-system.css +91 -4
  65. package/src/features/chat/components/conversation/__tests__/chat-conversation-content.test.tsx +86 -0
  66. package/src/features/chat/components/conversation/chat-conversation-content.tsx +47 -17
  67. package/src/features/chat/components/layout/__tests__/chat-sidebar-read-state.test.tsx +7 -0
  68. package/src/features/chat/components/layout/chat-sidebar-desktop-layout.tsx +9 -5
  69. package/src/features/chat/components/layout/chat-sidebar-utility-menu.tsx +5 -5
  70. package/src/features/chat/features/conversation/components/__tests__/session-conversation-area.test.tsx +51 -2
  71. package/src/features/chat/features/conversation/components/__tests__/session-conversation-input.streaming.test.tsx +170 -1
  72. package/src/features/chat/features/conversation/components/session-conversation-area.tsx +23 -1
  73. package/src/features/chat/features/conversation/components/session-conversation-input.tsx +24 -7
  74. package/src/features/chat/features/conversation/components/session-queued-input-rows.tsx +96 -0
  75. package/src/features/chat/features/conversation/hooks/__tests__/use-session-conversation-controller.test.tsx +181 -0
  76. package/src/features/chat/features/conversation/hooks/use-session-conversation-controller.ts +351 -89
  77. package/src/features/chat/features/input/hooks/__tests__/use-chat-input-surface-state.test.tsx +29 -4
  78. package/src/features/chat/features/input/hooks/use-chat-input-surface-state.ts +28 -3
  79. package/src/features/chat/features/input/input-surface-plugins/__tests__/slash-command-plugin.test.ts +108 -8
  80. package/src/features/chat/features/input/input-surface-plugins/panel-app-reference-plugin.utils.ts +50 -14
  81. package/src/features/chat/features/input/input-surface-plugins/slash-command-plugin.utils.ts +77 -6
  82. package/src/features/chat/features/message/components/__tests__/chat-inline-panel-app-card.test.tsx +21 -12
  83. package/src/features/chat/features/message/components/__tests__/chat-message-list.container.test.tsx +166 -0
  84. package/src/features/chat/features/message/components/chat-inline-panel-app-card.tsx +19 -11
  85. package/src/features/chat/features/message/components/chat-message-list.container.tsx +38 -64
  86. package/src/features/chat/features/message/utils/__tests__/chat-message-process-summary.utils.test.ts +59 -0
  87. package/src/features/chat/features/message/utils/__tests__/chat-message-show-content-tool-card.utils.test.ts +56 -1
  88. package/src/features/chat/features/message/utils/chat-inline-panel-app-card.utils.ts +0 -2
  89. package/src/features/chat/features/message/utils/chat-message-process-summary.utils.ts +80 -0
  90. package/src/features/chat/features/message/utils/chat-message-show-content-tool-card.utils.ts +12 -3
  91. package/src/features/chat/features/message/utils/chat-message-texts.utils.ts +60 -0
  92. package/src/features/chat/features/message/utils/chat-message.utils.ts +3 -0
  93. package/src/features/chat/features/ncp/hooks/__tests__/use-ncp-agent-runtime.test.tsx +39 -0
  94. package/src/features/chat/features/session/components/chat-sidebar-project-groups.tsx +2 -2
  95. package/src/features/chat/features/session/components/chat-sidebar-session-item.tsx +1 -1
  96. package/src/features/chat/features/session/components/session-header/__tests__/chat-session-header-actions.test.tsx +3 -1
  97. package/src/features/chat/features/session/components/session-header/chat-session-metadata-dialog.tsx +1 -1
  98. package/src/features/chat/features/workspace/components/__tests__/chat-session-workspace-file-preview.test.tsx +270 -115
  99. package/src/features/chat/features/workspace/components/chat-session-workspace-file-breadcrumb-browser.tsx +121 -0
  100. package/src/features/chat/features/workspace/components/chat-session-workspace-file-breadcrumbs.tsx +75 -20
  101. package/src/features/chat/features/workspace/components/chat-session-workspace-file-preview.tsx +70 -5
  102. package/src/features/chat/features/workspace/utils/chat-workspace-file-tab-persistence.utils.ts +83 -0
  103. package/src/features/chat/managers/__tests__/chat-thread.manager.test.ts +79 -1
  104. package/src/features/chat/managers/__tests__/chat-ui.manager.test.ts +2 -0
  105. package/src/features/chat/managers/chat-thread.manager.ts +9 -3
  106. package/src/features/chat/managers/chat-ui.manager.ts +5 -1
  107. package/src/features/chat/presenters/chat.presenter.ts +2 -1
  108. package/src/features/chat/stores/__tests__/chat-thread.store.test.ts +4 -0
  109. package/src/features/chat/stores/chat-thread.store.ts +6 -64
  110. package/src/features/chat/types/chat-message.types.ts +4 -0
  111. package/src/features/marketplace/components/__tests__/marketplace-page-detail.test.tsx +33 -26
  112. package/src/features/marketplace/components/__tests__/marketplace-page.test.tsx +1 -0
  113. package/src/features/marketplace/components/curated-shelves/__tests__/marketplace-curated-scene-route.test.tsx +1 -0
  114. package/src/features/marketplace/components/detail-doc/__tests__/marketplace-detail-doc.test.tsx +115 -0
  115. package/src/features/marketplace/components/marketplace-detail-doc.tsx +441 -0
  116. package/src/features/marketplace/components/mcp/__tests__/mcp-marketplace-page.test.tsx +3 -3
  117. package/src/features/marketplace/components/mcp/mcp-marketplace-doc.ts +0 -60
  118. package/src/features/marketplace/components/mcp/mcp-marketplace-page.tsx +64 -29
  119. package/src/features/marketplace/hooks/use-marketplace-item-detail.ts +42 -31
  120. package/src/features/marketplace/index.ts +3 -0
  121. package/src/features/marketplace/stores/marketplace-detail-doc.store.ts +38 -0
  122. package/src/features/panel-apps/components/__tests__/panel-apps-list.test.tsx +32 -2
  123. package/src/features/pwa/managers/__tests__/pwa-shell-theme.manager.test.ts +11 -0
  124. package/src/features/pwa/managers/pwa-shell-theme.manager.ts +8 -3
  125. package/src/features/settings/pages/__tests__/appearance-settings-page.test.tsx +25 -0
  126. package/src/features/settings/pages/appearance-settings-page.tsx +40 -0
  127. package/src/features/side-dock/components/__tests__/side-dock.test.tsx +44 -2
  128. package/src/features/side-dock/components/side-dock.tsx +100 -13
  129. package/src/features/side-dock/configs/side-dock-built-in-items.config.ts +10 -0
  130. package/src/features/side-dock/managers/__tests__/side-dock.manager.test.ts +32 -2
  131. package/src/features/side-dock/managers/side-dock.manager.ts +17 -7
  132. package/src/features/side-dock/stores/side-dock.store.ts +9 -0
  133. package/src/features/side-dock/types/side-dock.types.ts +10 -3
  134. package/src/features/side-dock/utils/side-dock-item.utils.ts +15 -2
  135. package/src/index.css +501 -27
  136. package/src/platforms/desktop/components/desktop-window-chrome.tsx +1 -1
  137. package/src/shared/components/doc-browser/__tests__/doc-browser-context.test.tsx +30 -1
  138. package/src/shared/components/doc-browser/__tests__/doc-browser.test.tsx +206 -38
  139. package/src/shared/components/doc-browser/doc-browser-panel-parts.tsx +34 -11
  140. package/src/shared/components/doc-browser/doc-browser-renderer.types.ts +1 -1
  141. package/src/shared/components/doc-browser/doc-browser-tab-strip.tsx +23 -5
  142. package/src/shared/components/doc-browser/doc-browser.tsx +128 -75
  143. package/src/shared/components/doc-browser/managers/doc-browser.manager.ts +9 -1
  144. package/src/shared/components/doc-browser/utils/doc-browser-floating-panel.utils.ts +83 -0
  145. package/src/shared/components/ui/__tests__/dialog.test.tsx +27 -0
  146. package/src/shared/components/ui/actions/icon-action-button.tsx +14 -2
  147. package/src/shared/components/ui/dialog.tsx +2 -2
  148. package/src/shared/components/ui/popover.tsx +1 -1
  149. package/src/shared/components/ui/select.tsx +1 -1
  150. package/src/shared/components/ui/switch.tsx +4 -3
  151. package/src/shared/components/ui/tab-strip/__tests__/compact-tab-strip.test.tsx +54 -0
  152. package/src/shared/components/ui/tab-strip/compact-tab-strip.tsx +4 -5
  153. package/src/shared/components/ui/tooltip.tsx +1 -1
  154. package/src/shared/lib/api/utils/server-path.utils.ts +27 -1
  155. package/src/shared/lib/i18n/locales/en-US/chat.json +15 -6
  156. package/src/shared/lib/i18n/locales/en-US/core.json +2 -0
  157. package/src/shared/lib/i18n/locales/en-US/doc-browser.json +11 -0
  158. package/src/shared/lib/i18n/locales/en-US/marketplace.json +5 -0
  159. package/src/shared/lib/i18n/locales/zh-CN/chat.json +15 -6
  160. package/src/shared/lib/i18n/locales/zh-CN/core.json +2 -0
  161. package/src/shared/lib/i18n/locales/zh-CN/doc-browser.json +11 -0
  162. package/src/shared/lib/i18n/locales/zh-CN/marketplace.json +5 -0
  163. package/src/shared/lib/session-project/__tests__/workspace-file-breadcrumb.test.ts +14 -0
  164. package/src/shared/lib/session-project/index.ts +1 -1
  165. package/src/shared/lib/session-project/{workspace-file-breadcrumb.ts → workspace-file-breadcrumb.utils.ts} +62 -8
  166. package/src/shared/lib/theme/index.ts +51 -13
  167. package/dist/assets/api-BP_8IaxB.js +0 -13
  168. package/dist/assets/channels-list-page-DEh5Jjqx.js +0 -8
  169. package/dist/assets/chat-page-BRB0N5WW.js +0 -105
  170. package/dist/assets/confirm-dialog-DsivFJ4r.js +0 -5
  171. package/dist/assets/desktop-update-config-B0w9p2ep.js +0 -1
  172. package/dist/assets/doc-browser-BoNJ7-tE.js +0 -1
  173. package/dist/assets/doc-browser-CdJ_d4ik.js +0 -1
  174. package/dist/assets/index-BiE-K8-q.css +0 -1
  175. package/dist/assets/index-D9BAE_L_.js +0 -105
  176. package/dist/assets/mcp-marketplace-page-B1SZNY5m.js +0 -40
  177. package/dist/assets/mcp-marketplace-page-Cq_Tw42T.js +0 -1
  178. package/dist/assets/model-config-page-DotaTUHb.js +0 -1
  179. package/dist/assets/plus-BbZqquJe.js +0 -1
  180. package/dist/assets/provider-scoped-model-input-DsFRuWJH.js +0 -1
  181. package/dist/assets/providers-config-page-DW4tQSLN.js +0 -1
  182. package/dist/assets/remote-B8TiUfwW.js +0 -1
  183. package/dist/assets/rotate-cw-DjHDTkCv.js +0 -1
  184. package/dist/assets/runtime-config-page-S6ErRHR8.js +0 -1
  185. package/dist/assets/search-config-page-DGj5am03.js +0 -1
  186. package/dist/assets/select-hoj8dK4K.js +0 -41
  187. package/dist/assets/tag-chip-T8-h-die.js +0 -1
  188. package/dist/assets/use-config-CjRV2hwL.js +0 -1
  189. package/dist/assets/x-D9PXs_xP.js +0 -1
  190. package/src/features/marketplace/components/detail-doc/__tests__/marketplace-detail-doc.test.ts +0 -40
  191. package/src/features/marketplace/components/detail-doc/marketplace-detail-doc-renderer.ts +0 -201
  192. package/src/features/marketplace/components/marketplace-detail-doc.ts +0 -142
  193. /package/dist/assets/{config-hints-BkYBc7z0.js → config-hints-fGnUjDe9.js} +0 -0
  194. /package/dist/assets/{middleware-CkFT3R4v.js → middleware-BILWbJE4.js} +0 -0
@@ -4,7 +4,10 @@ import type {
4
4
  } from "@/shared/lib/api";
5
5
  import { fetchMarketplaceSkillContent } from "@/shared/lib/api";
6
6
  import { useDocBrowser } from "@/shared/components/doc-browser";
7
- import { buildGenericDetailDataUrl } from "@/features/marketplace/components/marketplace-detail-doc";
7
+ import {
8
+ createMarketplaceDetailDocId,
9
+ openMarketplaceDetailDoc,
10
+ } from "@/features/marketplace/components/marketplace-detail-doc";
8
11
  import { pickLocalizedText } from "@/features/marketplace/components/marketplace-localization";
9
12
  import { t } from "@/shared/lib/i18n";
10
13
  import { useRef } from "react";
@@ -23,25 +26,26 @@ export function useMarketplaceItemDetail(localeFallbacks: string[]) {
23
26
  record?.id ??
24
27
  record?.spec ??
25
28
  t("marketplaceUnknownItem");
29
+ const detailId = createMarketplaceDetailDocId(
30
+ item ? "skill" : "skill-local",
31
+ item?.slug ?? record?.id ?? record?.spec ?? title,
32
+ );
26
33
  const dedupeKey = item
27
34
  ? `marketplace:${item.type}:${item.slug}`
28
35
  : `marketplace:${record?.type ?? "unknown"}:${record?.id ?? record?.spec ?? title}`;
29
- const openOptions = { title, kind: "content" as const, dedupeKey };
30
- const updateOptions = { ...openOptions, activate: false };
31
36
  const typeLabel = t("marketplaceTypeSkill");
32
37
 
33
38
  if (!item) {
34
- docBrowser.open(
35
- buildGenericDetailDataUrl({
36
- title,
37
- typeLabel,
38
- spec: record?.spec ?? "-",
39
- summary: t("marketplaceInstalledLocalSummary"),
40
- metadataRaw: JSON.stringify(record ?? {}, null, 2),
41
- contentRaw: "-",
42
- }),
43
- openOptions,
44
- );
39
+ openMarketplaceDetailDoc(docBrowser, {
40
+ id: detailId,
41
+ title,
42
+ typeLabel,
43
+ spec: record?.spec ?? "-",
44
+ status: "ready",
45
+ summary: t("marketplaceInstalledLocalSummary"),
46
+ metadataRaw: JSON.stringify(record ?? {}, null, 2),
47
+ contentRaw: "-",
48
+ });
45
49
  return;
46
50
  }
47
51
 
@@ -53,26 +57,30 @@ export function useMarketplaceItemDetail(localeFallbacks: string[]) {
53
57
  item.summary,
54
58
  localeFallbacks,
55
59
  );
56
- docBrowser.open(
57
- buildGenericDetailDataUrl({
58
- title,
59
- typeLabel,
60
- spec: item.install.spec,
61
- loading: true,
62
- }),
63
- openOptions,
64
- );
60
+ openMarketplaceDetailDoc(docBrowser, {
61
+ id: detailId,
62
+ title,
63
+ typeLabel,
64
+ spec: item.install.spec,
65
+ status: "loading",
66
+ summary,
67
+ tags: item.tags,
68
+ author: item.author,
69
+ });
65
70
 
66
71
  try {
67
72
  const content = await fetchMarketplaceSkillContent(item.slug);
68
73
  if (detailRequestRef.current.byKey.get(dedupeKey) !== requestId) {
69
74
  return;
70
75
  }
71
- docBrowser.open(
72
- buildGenericDetailDataUrl({
76
+ openMarketplaceDetailDoc(
77
+ docBrowser,
78
+ {
79
+ id: detailId,
73
80
  title,
74
81
  typeLabel,
75
82
  spec: item.install.spec,
83
+ status: "ready",
76
84
  summary,
77
85
  metadataRaw: content.metadataRaw,
78
86
  contentRaw: content.bodyRaw || content.raw,
@@ -80,18 +88,21 @@ export function useMarketplaceItemDetail(localeFallbacks: string[]) {
80
88
  sourceLabel: `Source (${content.source})`,
81
89
  tags: item.tags,
82
90
  author: item.author,
83
- }),
84
- updateOptions,
91
+ },
92
+ { activate: false },
85
93
  );
86
94
  } catch (error) {
87
95
  if (detailRequestRef.current.byKey.get(dedupeKey) !== requestId) {
88
96
  return;
89
97
  }
90
- docBrowser.open(
91
- buildGenericDetailDataUrl({
98
+ openMarketplaceDetailDoc(
99
+ docBrowser,
100
+ {
101
+ id: detailId,
92
102
  title,
93
103
  typeLabel,
94
104
  spec: item.install.spec,
105
+ status: "error",
95
106
  summary,
96
107
  metadataRaw: JSON.stringify(
97
108
  { error: error instanceof Error ? error.message : String(error) },
@@ -99,8 +110,8 @@ export function useMarketplaceItemDetail(localeFallbacks: string[]) {
99
110
  2,
100
111
  ),
101
112
  contentRaw: t("marketplaceOperationFailed"),
102
- }),
103
- updateOptions,
113
+ },
114
+ { activate: false },
104
115
  );
105
116
  }
106
117
  };
@@ -1,5 +1,8 @@
1
1
  export { MarketplacePage } from './components/marketplace-page';
2
2
  export { McpMarketplacePage } from './components/mcp/mcp-marketplace-page';
3
+ export {
4
+ MARKETPLACE_DETAIL_DOC_BROWSER_RENDERERS,
5
+ } from './components/marketplace-detail-doc';
3
6
  export {
4
7
  useInstallMarketplaceItem,
5
8
  useManageMarketplaceItem,
@@ -0,0 +1,38 @@
1
+ import { create } from 'zustand';
2
+
3
+ export type MarketplaceDetailDocStatus = 'loading' | 'ready' | 'error';
4
+
5
+ export type MarketplaceDetailDocEntry = {
6
+ id: string;
7
+ title: string;
8
+ typeLabel: string;
9
+ spec: string;
10
+ status: MarketplaceDetailDocStatus;
11
+ summary?: string;
12
+ description?: string;
13
+ metadataRaw?: string;
14
+ contentRaw?: string;
15
+ sourceUrl?: string;
16
+ sourceLabel?: string;
17
+ tags?: string[];
18
+ author?: string;
19
+ };
20
+
21
+ type MarketplaceDetailDocStore = {
22
+ entries: Record<string, MarketplaceDetailDocEntry>;
23
+ setEntry: (entry: MarketplaceDetailDocEntry) => void;
24
+ };
25
+
26
+ export const useMarketplaceDetailDocStore = create<MarketplaceDetailDocStore>((set) => ({
27
+ entries: {},
28
+ setEntry: (entry) => set((state) => ({
29
+ entries: {
30
+ ...state.entries,
31
+ [entry.id]: entry,
32
+ },
33
+ })),
34
+ }));
35
+
36
+ export function setMarketplaceDetailDocEntry(entry: MarketplaceDetailDocEntry): void {
37
+ useMarketplaceDetailDocStore.getState().setEntry(entry);
38
+ }
@@ -20,8 +20,10 @@ const mocks = vi.hoisted(() => ({
20
20
  title: string;
21
21
  updatedAt: string;
22
22
  }>,
23
+ grantPanelAppClient: vi.fn(),
23
24
  navigate: vi.fn(),
24
25
  refetchPanelApps: vi.fn(),
26
+ requestAuthorization: vi.fn(async () => true),
25
27
  requestDraft: vi.fn(),
26
28
  }));
27
29
 
@@ -31,7 +33,7 @@ vi.mock('@/app/components/app-presenter-provider', () => ({
31
33
  requestDraft: mocks.requestDraft,
32
34
  },
33
35
  serviceActionAuthorizationManager: {
34
- requestAuthorization: vi.fn(async () => true),
36
+ requestAuthorization: mocks.requestAuthorization,
35
37
  },
36
38
  }),
37
39
  }));
@@ -47,7 +49,7 @@ vi.mock('@/features/panel-apps/hooks/use-panel-apps', () => ({
47
49
  mutate: vi.fn(),
48
50
  }),
49
51
  useGrantPanelAppClient: () => ({
50
- mutateAsync: vi.fn(),
52
+ mutateAsync: mocks.grantPanelAppClient,
51
53
  }),
52
54
  usePanelApps: () => ({
53
55
  data: {
@@ -91,8 +93,11 @@ function createPanelAppEntry(overrides: Partial<(typeof mocks.entries)[number]>
91
93
  describe('PanelAppsList', () => {
92
94
  beforeEach(() => {
93
95
  mocks.entries = [];
96
+ mocks.grantPanelAppClient.mockReset();
94
97
  mocks.navigate.mockReset();
95
98
  mocks.refetchPanelApps.mockReset();
99
+ mocks.requestAuthorization.mockReset();
100
+ mocks.requestAuthorization.mockResolvedValue(true);
96
101
  mocks.requestDraft.mockReset();
97
102
  });
98
103
 
@@ -134,4 +139,29 @@ describe('PanelAppsList', () => {
134
139
  expect(screen.getByText('No panel apps in this view')).toBeTruthy();
135
140
  expect(screen.queryByText('Create your first panel app')).toBeNull();
136
141
  });
142
+
143
+ it('continues opening a client panel app after authorization', async () => {
144
+ const user = userEvent.setup();
145
+ const onOpenPanelApp = vi.fn();
146
+ const entry = createPanelAppEntry({
147
+ appId: 'demo-app',
148
+ clientDeclared: true,
149
+ clientGranted: false,
150
+ });
151
+ mocks.entries = [entry];
152
+
153
+ render(<PanelAppsList onOpenPanelApp={onOpenPanelApp} />);
154
+
155
+ await user.click(screen.getByRole('button', { name: /Demo Panel/ }));
156
+
157
+ expect(mocks.requestAuthorization).toHaveBeenCalledWith(expect.objectContaining({
158
+ panelAppId: 'demo-app',
159
+ actions: [expect.objectContaining({
160
+ actionId: 'nextclaw.client',
161
+ risk: 'dangerous',
162
+ })],
163
+ }));
164
+ expect(mocks.grantPanelAppClient).toHaveBeenCalledWith('demo-app');
165
+ expect(onOpenPanelApp).toHaveBeenCalledWith(entry);
166
+ });
137
167
  });
@@ -6,7 +6,9 @@ describe('PwaShellThemeManager', () => {
6
6
  document.head.innerHTML = '<meta name="theme-color" content="#FFFFFF" />';
7
7
  document.body.innerHTML = '<div id="root"></div>';
8
8
  document.documentElement.removeAttribute('data-theme');
9
+ document.documentElement.removeAttribute('data-theme-appearance');
9
10
  document.documentElement.style.backgroundColor = '';
11
+ document.documentElement.style.colorScheme = '';
10
12
  document.body.style.backgroundColor = '';
11
13
  });
12
14
 
@@ -72,6 +74,15 @@ describe('PwaShellThemeManager', () => {
72
74
  expect(document.body.style.backgroundColor).toBe('rgb(248, 248, 247)');
73
75
  });
74
76
 
77
+ it('applies night shell colors and dark color scheme', () => {
78
+ pwaShellThemeManager.syncTheme('night');
79
+
80
+ const meta = document.querySelector('meta[name="theme-color"]');
81
+ expect(meta?.getAttribute('content')).toBe('#101318');
82
+ expect(document.body.style.backgroundColor).toBe('rgb(16, 19, 24)');
83
+ expect(document.documentElement.style.colorScheme).toBe('dark');
84
+ });
85
+
75
86
  it('maps the legacy leaf theme to warm shell colors', () => {
76
87
  document.documentElement.setAttribute('data-theme', 'leaf');
77
88
 
@@ -1,4 +1,8 @@
1
- import { normalizeTheme, type UiTheme } from '@/shared/lib/theme';
1
+ import {
2
+ getThemeAppearance,
3
+ normalizeTheme,
4
+ type UiTheme,
5
+ } from '@/shared/lib/theme';
2
6
 
3
7
  const PWA_SHELL_THEME_COLORS: Record<UiTheme, string> = {
4
8
  natural: '#FAF9F7',
@@ -7,7 +11,8 @@ const PWA_SHELL_THEME_COLORS: Record<UiTheme, string> = {
7
11
  cool: '#F8FAFC',
8
12
  dawn: '#FAF7F4',
9
13
  graphite: '#F8F8F7',
10
- probe: '#FEFCF6'
14
+ night: '#101318',
15
+ probe: '#FEFCF6',
11
16
  };
12
17
 
13
18
  export class PwaShellThemeManager {
@@ -19,7 +24,7 @@ export class PwaShellThemeManager {
19
24
  const themeColor = PWA_SHELL_THEME_COLORS[theme];
20
25
  this.updateThemeMeta(themeColor);
21
26
  this.updateSurfaceBackgrounds(themeColor);
22
- document.documentElement.style.colorScheme = 'light';
27
+ document.documentElement.style.colorScheme = getThemeAppearance(theme);
23
28
  };
24
29
 
25
30
  syncCurrentTheme = () => {
@@ -0,0 +1,25 @@
1
+ import { fireEvent, render, screen } from '@testing-library/react';
2
+ import { beforeEach, describe, expect, it } from 'vitest';
3
+ import { AppearanceSettingsPage } from '@/features/settings/pages/appearance-settings-page';
4
+ import { useSideDockStore } from '@/features/side-dock';
5
+
6
+ describe('AppearanceSettingsPage', () => {
7
+ beforeEach(() => {
8
+ window.localStorage.clear();
9
+ useSideDockStore.getState().setVisible(true);
10
+ useSideDockStore.getState().setPinnedItems([]);
11
+ });
12
+
13
+ it('reopens the SideDock from the appearance settings switch', () => {
14
+ useSideDockStore.getState().setVisible(false);
15
+
16
+ render(<AppearanceSettingsPage />);
17
+
18
+ const visibilitySwitch = screen.getByRole('switch', { name: 'Show SideDock' });
19
+ expect(visibilitySwitch.getAttribute('aria-checked')).toBe('false');
20
+
21
+ fireEvent.click(visibilitySwitch);
22
+
23
+ expect(useSideDockStore.getState().isVisible).toBe(true);
24
+ });
25
+ });
@@ -0,0 +1,40 @@
1
+ import { Palette } from 'lucide-react';
2
+ import { PageHeader, PageLayout } from '@/app/components/layout/page-layout';
3
+ import { SettingRow } from '@/shared/components/settings/setting-row';
4
+ import { Card, CardContent, CardHeader, CardTitle } from '@/shared/components/ui/card';
5
+ import { Switch } from '@/shared/components/ui/switch';
6
+ import { useSideDockStore } from '@/features/side-dock';
7
+ import { t } from '@/shared/lib/i18n';
8
+
9
+ export function AppearanceSettingsPage() {
10
+ const isSideDockVisible = useSideDockStore((state) => state.isVisible);
11
+ const setSideDockVisible = useSideDockStore((state) => state.setVisible);
12
+
13
+ return (
14
+ <PageLayout>
15
+ <PageHeader title={t('appearance')} />
16
+ <Card>
17
+ <CardHeader>
18
+ <CardTitle className="flex items-center gap-2">
19
+ <Palette className="h-4 w-4" />
20
+ {t('appearance')}
21
+ </CardTitle>
22
+ </CardHeader>
23
+ <CardContent>
24
+ <SettingRow
25
+ title={t('sideDockVisibilityTitle')}
26
+ description={t('sideDockVisibilityDescription')}
27
+ control={
28
+ <Switch
29
+ id="appearance-side-dock-visible"
30
+ aria-label={t('sideDockVisibilityTitle')}
31
+ checked={isSideDockVisible}
32
+ onCheckedChange={setSideDockVisible}
33
+ />
34
+ }
35
+ />
36
+ </CardContent>
37
+ </Card>
38
+ </PageLayout>
39
+ );
40
+ }
@@ -1,6 +1,9 @@
1
- import { fireEvent, render, screen } from '@testing-library/react';
1
+ import { fireEvent, render, screen, waitFor } from '@testing-library/react';
2
2
  import { beforeEach, describe, expect, it, vi } from 'vitest';
3
- import { getSideDockBuiltInItems } from '@/features/side-dock/configs/side-dock-built-in-items.config';
3
+ import {
4
+ getSideDockBuiltInItems,
5
+ SIDE_DOCK_GITHUB_PROJECT_ITEM_ID,
6
+ } from '@/features/side-dock/configs/side-dock-built-in-items.config';
4
7
  import { SideDock } from '@/features/side-dock/components/side-dock';
5
8
  import { useSideDockStore } from '@/features/side-dock/stores/side-dock.store';
6
9
  import type { SideDockManager } from '@/features/side-dock/managers/side-dock.manager';
@@ -12,6 +15,7 @@ import { createDefaultDocBrowserState } from '@/shared/components/doc-browser/ut
12
15
  describe('SideDock', () => {
13
16
  beforeEach(() => {
14
17
  window.localStorage.clear();
18
+ useSideDockStore.getState().setVisible(true);
15
19
  useSideDockStore.getState().setPinnedItems([]);
16
20
  useDocBrowserStore.getState().setSnapshot(createDefaultDocBrowserState());
17
21
  });
@@ -40,6 +44,44 @@ describe('SideDock', () => {
40
44
  expect(openItem).toHaveBeenCalledWith(builtInItems[0]);
41
45
  });
42
46
 
47
+ it('places the GitHub project shortcut in the utility section', () => {
48
+ const manager = { openItem: vi.fn() } as unknown as SideDockManager;
49
+ const docBrowserManager = new DocBrowserManager();
50
+
51
+ render(
52
+ <DocBrowserProvider manager={docBrowserManager}>
53
+ <SideDock manager={manager} />
54
+ </DocBrowserProvider>,
55
+ );
56
+
57
+ const utilitySection = screen.getByTestId('side-dock-utility');
58
+ expect(utilitySection.className).toContain('mt-auto');
59
+ expect(utilitySection.querySelector(`[data-side-dock-item-id="${SIDE_DOCK_GITHUB_PROJECT_ITEM_ID}"]`)).toBeTruthy();
60
+ });
61
+
62
+ it('confirms before hiding the dock and persists the visibility preference', async () => {
63
+ const manager = { openItem: vi.fn() } as unknown as SideDockManager;
64
+ const docBrowserManager = new DocBrowserManager();
65
+
66
+ render(
67
+ <DocBrowserProvider manager={docBrowserManager}>
68
+ <SideDock manager={manager} />
69
+ </DocBrowserProvider>,
70
+ );
71
+
72
+ const hideButton = document.querySelector('[data-side-dock-action="hide"]');
73
+ expect(hideButton).toBeTruthy();
74
+
75
+ fireEvent.click(hideButton as Element);
76
+
77
+ expect(await screen.findByText('You can turn it back on from Settings > Appearance.')).toBeTruthy();
78
+ fireEvent.click(screen.getByRole('button', { name: 'Hide' }));
79
+
80
+ await waitFor(() => {
81
+ expect(useSideDockStore.getState().isVisible).toBe(false);
82
+ });
83
+ });
84
+
43
85
  it('does not highlight the default docs tab while DocBrowser is closed', () => {
44
86
  const manager = { openItem: vi.fn() } as unknown as SideDockManager;
45
87
  const docBrowserManager = new DocBrowserManager();
@@ -1,7 +1,10 @@
1
- import { AppWindow, BookOpen, Boxes, Plus, X } from 'lucide-react';
1
+ import { AppWindow, BookOpen, Boxes, Github, PanelRightClose, Plus, X } from 'lucide-react';
2
2
  import type { LucideIcon } from 'lucide-react';
3
3
  import type { SideDockManager } from '@/features/side-dock/managers/side-dock.manager';
4
- import { getSideDockBuiltInItems } from '@/features/side-dock/configs/side-dock-built-in-items.config';
4
+ import {
5
+ getSideDockBuiltInItems,
6
+ SIDE_DOCK_GITHUB_PROJECT_ITEM_ID,
7
+ } from '@/features/side-dock/configs/side-dock-built-in-items.config';
5
8
  import { useSideDockStore } from '@/features/side-dock/stores/side-dock.store';
6
9
  import type {
7
10
  SideDockIconName,
@@ -19,6 +22,7 @@ import {
19
22
  TooltipProvider,
20
23
  TooltipTrigger,
21
24
  } from '@/shared/components/ui/tooltip';
25
+ import { useConfirmDialog } from '@/shared/hooks/use-confirm-dialog';
22
26
  import { t } from '@/shared/lib/i18n';
23
27
  import { cn } from '@/shared/lib/utils';
24
28
 
@@ -29,11 +33,14 @@ type SideDockProps = {
29
33
  const SIDE_DOCK_ICON_COMPONENTS: Record<SideDockIconName, LucideIcon> = {
30
34
  apps: Boxes,
31
35
  docs: BookOpen,
36
+ github: Github,
32
37
  'new-tab': Plus,
33
38
  'panel-app': AppWindow,
34
39
  'service-apps': AppWindow,
35
40
  };
36
41
 
42
+ const SIDE_DOCK_UTILITY_ITEM_IDS = new Set<string>([SIDE_DOCK_GITHUB_PROJECT_ITEM_ID]);
43
+
37
44
  const SIDE_DOCK_EMOJI_ICON_PATTERN = /^\p{Extended_Pictographic}(?:\uFE0F|\uFE0E)?(?:\u200D\p{Extended_Pictographic}(?:\uFE0F|\uFE0E)?)*$/u;
38
45
 
39
46
  function isSideDockEmojiIcon(value: string): boolean {
@@ -117,10 +124,45 @@ function SideDockButton({
117
124
  );
118
125
  }
119
126
 
127
+ function SideDockActionButton({
128
+ icon: Icon,
129
+ label,
130
+ onClick,
131
+ }: {
132
+ icon: LucideIcon;
133
+ label: string;
134
+ onClick: () => void;
135
+ }) {
136
+ return (
137
+ <Tooltip>
138
+ <TooltipTrigger asChild>
139
+ <button
140
+ type="button"
141
+ aria-label={label}
142
+ title={label}
143
+ data-side-dock-action="hide"
144
+ onClick={onClick}
145
+ className={cn(
146
+ 'flex h-10 w-10 shrink-0 items-center justify-center rounded-lg text-muted-foreground transition-colors',
147
+ 'hover:bg-primary/10 hover:text-primary focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring',
148
+ )}
149
+ >
150
+ <Icon className="h-5 w-5" aria-hidden="true" />
151
+ </button>
152
+ </TooltipTrigger>
153
+ <TooltipContent side="left">{label}</TooltipContent>
154
+ </Tooltip>
155
+ );
156
+ }
157
+
120
158
  function normalizeSideDockUri(uri?: string): string {
121
159
  return uri?.trim() ?? '';
122
160
  }
123
161
 
162
+ function getSideDockResourceUri(item: SideDockItem): string | null {
163
+ return item.target.type === 'right-panel-resource' ? item.target.uri : null;
164
+ }
165
+
124
166
  function isSideDockItemActive(
125
167
  item: SideDockItem,
126
168
  isDocBrowserOpen: boolean,
@@ -130,7 +172,10 @@ function isSideDockItemActive(
130
172
  if (!isDocBrowserOpen || !currentTab) {
131
173
  return false;
132
174
  }
133
- const itemUri = normalizeSideDockUri(item.target.uri);
175
+ const itemUri = normalizeSideDockUri(getSideDockResourceUri(item) ?? undefined);
176
+ if (!itemUri) {
177
+ return false;
178
+ }
134
179
  const currentResourceUri = normalizeSideDockUri(currentTab.resourceUri);
135
180
  if (
136
181
  (currentResourceUri && currentResourceUri === itemUri)
@@ -151,16 +196,35 @@ function hasExactActiveSideDockItem(items: SideDockItem[], currentTab?: DocBrows
151
196
  const currentResourceUri = normalizeSideDockUri(currentTab.resourceUri);
152
197
  const currentUrl = normalizeSideDockUri(currentTab.currentUrl);
153
198
  return items.some((item) => {
154
- const itemUri = normalizeSideDockUri(item.target.uri);
199
+ const itemUri = normalizeSideDockUri(getSideDockResourceUri(item) ?? undefined);
155
200
  return itemUri === currentResourceUri || itemUri === currentUrl;
156
201
  });
157
202
  }
158
203
 
204
+ function isSideDockUtilityItem(item: SideDockItem): boolean {
205
+ return SIDE_DOCK_UTILITY_ITEM_IDS.has(item.id);
206
+ }
207
+
159
208
  export function SideDock({ manager }: SideDockProps) {
160
209
  const pinnedItems = useSideDockStore((state) => state.pinnedItems);
210
+ const setSideDockVisible = useSideDockStore((state) => state.setVisible);
161
211
  const { currentTab, isOpen } = useDocBrowser();
212
+ const { confirm, ConfirmDialog } = useConfirmDialog();
162
213
  const items = mergeSideDockItems(getSideDockBuiltInItems(), pinnedItems);
214
+ const mainItems = items.filter((item) => !isSideDockUtilityItem(item));
215
+ const utilityItems = items.filter(isSideDockUtilityItem);
163
216
  const hasExactActiveItem = hasExactActiveSideDockItem(items, currentTab);
217
+ const handleHideSideDock = async (): Promise<void> => {
218
+ const confirmed = await confirm({
219
+ title: t('sideDockHideConfirmTitle'),
220
+ description: t('sideDockHideConfirmDescription'),
221
+ confirmLabel: t('sideDockHideConfirmAction'),
222
+ cancelLabel: t('cancel'),
223
+ });
224
+ if (confirmed) {
225
+ setSideDockVisible(false);
226
+ }
227
+ };
164
228
 
165
229
  return (
166
230
  <TooltipProvider delayDuration={250}>
@@ -168,15 +232,38 @@ export function SideDock({ manager }: SideDockProps) {
168
232
  data-testid="side-dock"
169
233
  className="z-30 flex h-full w-14 shrink-0 flex-col items-center gap-1 border-l border-border/60 bg-background/95 px-2 py-3"
170
234
  >
171
- {items.map((item) => (
172
- <SideDockButton
173
- key={item.id}
174
- active={isSideDockItemActive(item, isOpen, currentTab, hasExactActiveItem)}
175
- item={item}
176
- onOpen={manager.openItem}
177
- onUnpin={(dockItem) => manager.unpinItem(dockItem.id)}
178
- />
179
- ))}
235
+ <div className="flex w-full flex-col items-center gap-1">
236
+ {mainItems.map((item) => (
237
+ <SideDockButton
238
+ key={item.id}
239
+ active={isSideDockItemActive(item, isOpen, currentTab, hasExactActiveItem)}
240
+ item={item}
241
+ onOpen={manager.openItem}
242
+ onUnpin={(dockItem) => manager.unpinItem(dockItem.id)}
243
+ />
244
+ ))}
245
+ </div>
246
+ {utilityItems.length > 0 ? (
247
+ <div data-testid="side-dock-utility" className="mt-auto flex w-full flex-col items-center gap-1">
248
+ {utilityItems.map((item) => (
249
+ <SideDockButton
250
+ key={item.id}
251
+ active={isSideDockItemActive(item, isOpen, currentTab, hasExactActiveItem)}
252
+ item={item}
253
+ onOpen={manager.openItem}
254
+ onUnpin={(dockItem) => manager.unpinItem(dockItem.id)}
255
+ />
256
+ ))}
257
+ <SideDockActionButton
258
+ icon={PanelRightClose}
259
+ label={t('sideDockHide')}
260
+ onClick={() => {
261
+ void handleHideSideDock();
262
+ }}
263
+ />
264
+ </div>
265
+ ) : null}
266
+ <ConfirmDialog />
180
267
  </aside>
181
268
  </TooltipProvider>
182
269
  );
@@ -7,6 +7,8 @@ import type { SideDockItem } from '@/features/side-dock/types/side-dock.types';
7
7
  import { t } from '@/shared/lib/i18n';
8
8
 
9
9
  export const SIDE_DOCK_DOCS_URL = 'nextclaw://docs';
10
+ export const SIDE_DOCK_GITHUB_PROJECT_ITEM_ID = 'github-project';
11
+ export const SIDE_DOCK_GITHUB_PROJECT_URL = 'https://github.com/Peiiii/nextclaw';
10
12
 
11
13
  export function getSideDockBuiltInItems(): SideDockItem[] {
12
14
  return [
@@ -42,5 +44,13 @@ export function getSideDockBuiltInItems(): SideDockItem[] {
42
44
  removable: false,
43
45
  target: { type: 'right-panel-resource', uri: RIGHT_PANEL_HOME_URL },
44
46
  },
47
+ {
48
+ builtIn: true,
49
+ icon: { type: 'builtin', name: 'github' },
50
+ id: SIDE_DOCK_GITHUB_PROJECT_ITEM_ID,
51
+ label: t('sideDockGitHubProject'),
52
+ removable: false,
53
+ target: { type: 'external-url', url: SIDE_DOCK_GITHUB_PROJECT_URL },
54
+ },
45
55
  ];
46
56
  }