@nextclaw/ui 0.14.1-beta.1 → 0.14.2

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 (179) hide show
  1. package/CHANGELOG.md +61 -0
  2. package/dist/assets/{api-dQ6uQSZG.js → api-a14tR_z1.js} +7 -7
  3. package/dist/assets/channels-list-page-CFGkm4DT.js +8 -0
  4. package/dist/assets/chat-page-CEdavJXc.js +105 -0
  5. package/dist/assets/{config-split-page-CeFfbaqc.js → config-split-page-B5b9B0uE.js} +1 -1
  6. package/dist/assets/{confirm-dialog-C95Y4QPC.js → confirm-dialog-ClH_Ec7n.js} +1 -1
  7. package/dist/assets/{desktop-update-config-CcK6jiEb.js → desktop-update-config-DPNDnVpk.js} +1 -1
  8. package/dist/assets/{dist-DTGzeil6.js → dist-BtCS2Ucq.js} +1 -1
  9. package/dist/assets/{dist-fGogBmPA.js → dist-CLGGLm8b.js} +1 -1
  10. package/dist/assets/doc-browser-5nMWYKBD.js +1 -0
  11. package/dist/assets/doc-browser-BJRLhztv.js +1 -0
  12. package/dist/assets/doc-browser-context-D4JwMMxK.js +1 -0
  13. package/dist/assets/{ellipsis-D2F4KMiU.js → ellipsis-CtJXjIol.js} +1 -1
  14. package/dist/assets/{external-link-CcgtBK26.js → external-link-CdJiG13p.js} +1 -1
  15. package/dist/assets/index-BDN1zzMQ.js +105 -0
  16. package/dist/assets/index-g3uSZXoc.css +1 -0
  17. package/dist/assets/mcp-marketplace-page-BU0kIjhR.js +40 -0
  18. package/dist/assets/mcp-marketplace-page-sMNFSq_T.js +1 -0
  19. package/dist/assets/middleware-CkFT3R4v.js +1 -0
  20. package/dist/assets/model-config-page-D6hYXROd.js +1 -0
  21. package/dist/assets/plus-DPjb0piO.js +1 -0
  22. package/dist/assets/provider-scoped-model-input-DAX2HgmR.js +1 -0
  23. package/dist/assets/providers-config-page-BzAeaACA.js +1 -0
  24. package/dist/assets/{react-CTqdhFMk.js → react-C6cZgP44.js} +1 -1
  25. package/dist/assets/remote-B0G80uPr.js +1 -0
  26. package/dist/assets/rotate-cw-DnEB1Rju.js +1 -0
  27. package/dist/assets/runtime-config-page-C5xKaV3i.js +1 -0
  28. package/dist/assets/{save-C9fKWfVB.js → save-DKA0x4zk.js} +1 -1
  29. package/dist/assets/{search-D5FQ5q0X.js → search-BNlcoj5c.js} +1 -1
  30. package/dist/assets/{search-config-page-AMEJJNIS.js → search-config-page-Cq9uJ-N8.js} +1 -1
  31. package/dist/assets/{secrets-config-page-CcgJnvBO.js → secrets-config-page-CxSbZLBo.js} +2 -2
  32. package/dist/assets/{select-Cu5n0E-3.js → select-CvTlDZTQ.js} +1 -1
  33. package/dist/assets/{tag-chip-C5aOtjxu.js → tag-chip-8ffF2y2C.js} +1 -1
  34. package/dist/assets/use-config-DmT0o1gv.js +1 -0
  35. package/dist/assets/x-BiSSpgqB.js +1 -0
  36. package/dist/index.html +15 -14
  37. package/package.json +9 -9
  38. package/src/app/components/app-presenter-provider.tsx +11 -2
  39. package/src/app/components/layout/__tests__/app-layout.test.tsx +11 -2
  40. package/src/app/components/layout/__tests__/sidebar.layout.test.tsx +110 -79
  41. package/src/app/components/layout/app-layout.tsx +18 -4
  42. package/src/app/components/layout/sidebar-items.tsx +194 -71
  43. package/src/app/components/layout/sidebar-rail.styles.ts +15 -0
  44. package/src/app/components/layout/sidebar.tsx +340 -83
  45. package/src/app/managers/viewport-layout.manager.ts +10 -0
  46. package/src/app/stores/viewport-layout.store.ts +39 -3
  47. package/src/features/agents/components/__tests__/agents-page.test.tsx +62 -39
  48. package/src/features/agents/components/agents-page.tsx +10 -6
  49. package/src/features/chat/components/conversation/__tests__/chat-conversation-header-section.test.tsx +145 -0
  50. package/src/features/chat/components/conversation/__tests__/chat-conversation-panel.test.tsx +69 -762
  51. package/src/features/chat/components/conversation/__tests__/chat-conversation-workspace-section.test.tsx +132 -0
  52. package/src/features/chat/components/conversation/chat-conversation-content.tsx +22 -15
  53. package/src/features/chat/components/conversation/chat-conversation-header-section.tsx +14 -15
  54. package/src/features/chat/components/conversation/chat-conversation-panel.tsx +18 -18
  55. package/src/features/chat/components/conversation/chat-conversation-workspace-section.tsx +10 -4
  56. package/src/features/chat/components/layout/__tests__/chat-sidebar-read-state.test.tsx +52 -11
  57. package/src/features/chat/components/layout/__tests__/chat-sidebar.test.tsx +77 -12
  58. package/src/features/chat/components/layout/chat-sidebar-desktop-layout.tsx +247 -0
  59. package/src/features/chat/components/layout/chat-sidebar-toolbar.tsx +182 -21
  60. package/src/features/chat/components/layout/chat-sidebar-utility-menu.tsx +82 -25
  61. package/src/features/chat/components/layout/chat-sidebar.tsx +79 -93
  62. package/src/features/chat/components/providers/chat-presenter.provider.tsx +0 -6
  63. package/src/features/chat/features/conversation/components/session-conversation-area.tsx +289 -0
  64. package/src/features/chat/features/conversation/components/session-conversation-input.tsx +427 -0
  65. package/src/features/chat/features/conversation/hooks/use-session-conversation-controller.ts +204 -0
  66. package/src/features/chat/features/conversation/hooks/use-session-conversation-input-attachments.ts +66 -0
  67. package/src/features/chat/features/{input/hooks/use-chat-input-bar-query-state.ts → conversation/hooks/use-session-conversation-input-query.ts} +32 -17
  68. package/src/features/chat/features/conversation/hooks/use-session-conversation-input-state.ts +281 -0
  69. package/src/features/chat/features/conversation/utils/session-conversation-input-toolbar.utils.ts +158 -0
  70. package/src/features/chat/features/input/hooks/__tests__/use-chat-input-surface-state.test.tsx +56 -0
  71. package/src/features/chat/features/input/hooks/use-chat-input-surface-state.ts +117 -0
  72. package/src/features/chat/features/input/hooks/use-chat-model-favorites.ts +2 -1
  73. package/src/features/chat/features/input/input-surface-plugins/__tests__/panel-app-reference-plugin.test.ts +77 -0
  74. package/src/features/chat/features/input/input-surface-plugins/__tests__/slash-command-plugin.test.ts +108 -0
  75. package/src/features/chat/features/input/input-surface-plugins/chat-input-product-plugin-adapters.types.ts +10 -0
  76. package/src/features/chat/features/input/input-surface-plugins/input-surface-search.utils.ts +108 -0
  77. package/src/features/chat/features/input/input-surface-plugins/panel-app-reference-plugin.utils.ts +108 -0
  78. package/src/features/chat/features/input/input-surface-plugins/slash-command-plugin.utils.ts +161 -0
  79. package/src/features/chat/features/input/utils/__tests__/chat-composer-state.utils.test.ts +22 -1
  80. package/src/features/chat/features/input/utils/__tests__/chat-inline-token.utils.test.ts +26 -13
  81. package/src/features/chat/features/input/utils/__tests__/ncp-chat-input-availability.utils.test.ts +7 -28
  82. package/src/features/chat/features/input/utils/chat-composer-state.utils.ts +6 -0
  83. package/src/features/chat/features/input/utils/chat-inline-token.utils.ts +26 -51
  84. package/src/features/chat/features/input/utils/chat-input-bar.utils.ts +2 -0
  85. package/src/features/chat/features/input/utils/ncp-chat-input-availability.utils.ts +5 -6
  86. package/src/features/chat/features/message/components/__tests__/chat-message-list.container.test.tsx +47 -12
  87. package/src/features/chat/features/message/components/chat-message-list.container.tsx +93 -12
  88. package/src/features/chat/features/message/utils/__tests__/chat-message.utils.test.ts +31 -12
  89. package/src/features/chat/features/message/utils/chat-message-markdown-part.utils.ts +38 -0
  90. package/src/features/chat/features/message/utils/chat-message-part.utils.ts +1 -1
  91. package/src/features/chat/features/session/components/chat-sidebar-project-groups.tsx +5 -4
  92. package/src/features/chat/features/session/components/chat-sidebar-session-list.tsx +2 -2
  93. package/src/features/chat/features/session/hooks/__tests__/use-chat-session-project.test.tsx +3 -30
  94. package/src/features/chat/features/session/hooks/use-chat-session-project.ts +0 -5
  95. package/src/features/chat/features/session/utils/chat-run-metadata.utils.ts +8 -0
  96. package/src/features/chat/features/session-type/components/__tests__/{chat-sidebar-create-menu.test.tsx → chat-session-type-menu.test.tsx} +5 -5
  97. package/src/features/chat/features/session-type/components/__tests__/chat-session-type-option-item.test.tsx +29 -1
  98. package/src/features/chat/features/session-type/components/{chat-sidebar-create-menu.tsx → chat-session-type-menu.tsx} +13 -9
  99. package/src/features/chat/features/session-type/components/chat-session-type-option-item.tsx +17 -5
  100. package/src/features/chat/features/session-type/hooks/__tests__/use-chat-new-session-type-preference.test.tsx +152 -0
  101. package/src/features/chat/features/session-type/hooks/use-chat-new-session-type-preference.ts +159 -0
  102. package/src/features/chat/features/welcome/components/__tests__/chat-conversation-welcome.test.tsx +49 -31
  103. package/src/features/chat/features/welcome/components/__tests__/chat-welcome.test.tsx +2 -2
  104. package/src/features/chat/features/welcome/components/chat-conversation-welcome.tsx +21 -10
  105. package/src/features/chat/features/welcome/components/chat-welcome-session-type-picker.tsx +2 -2
  106. package/src/features/chat/features/welcome/components/chat-welcome.tsx +2 -2
  107. package/src/features/chat/features/workspace/components/__tests__/chat-session-workspace-panel-content.test.tsx +54 -0
  108. package/src/features/chat/features/workspace/components/chat-session-workspace-panel-content.tsx +44 -76
  109. package/src/features/chat/features/workspace/components/chat-session-workspace-panel-nav.tsx +5 -1
  110. package/src/features/chat/features/workspace/components/chat-session-workspace-panel.tsx +7 -1
  111. package/src/features/chat/features/workspace/hooks/use-chat-conversation-workspace-state.ts +40 -31
  112. package/src/features/chat/features/workspace/utils/__tests__/chat-workspace-panel-view-model.utils.test.ts +59 -1
  113. package/src/features/chat/features/workspace/utils/chat-thread-workspace-session.utils.ts +109 -0
  114. package/src/features/chat/features/workspace/utils/chat-workspace-panel-view-model.utils.ts +44 -4
  115. package/src/features/chat/index.ts +0 -1
  116. package/src/features/chat/managers/__tests__/chat-session-list.manager.test.ts +51 -99
  117. package/src/features/chat/managers/__tests__/chat-thread.manager.test.ts +135 -21
  118. package/src/features/chat/managers/chat-session-list.manager.ts +12 -43
  119. package/src/features/chat/managers/chat-thread.manager.ts +108 -34
  120. package/src/features/chat/pages/__tests__/ncp-chat-page.test.ts +48 -1
  121. package/src/features/chat/pages/__tests__/ncp-chat-page.test.tsx +72 -0
  122. package/src/features/chat/pages/ncp-chat-page.tsx +0 -122
  123. package/src/features/chat/presenters/chat.presenter.ts +2 -19
  124. package/src/features/chat/stores/__tests__/chat-thread.store.test.ts +31 -0
  125. package/src/features/chat/stores/chat-thread.store.ts +42 -26
  126. package/src/features/marketplace/components/__tests__/marketplace-page.test.tsx +24 -0
  127. package/src/features/marketplace/components/curated-shelves/marketplace-shelf-card.tsx +8 -3
  128. package/src/features/marketplace/components/marketplace-item-list-view.tsx +2 -2
  129. package/src/features/marketplace/components/marketplace-list-card.tsx +124 -98
  130. package/src/features/marketplace/components/marketplace-page-parts.tsx +11 -6
  131. package/src/features/panel-apps/hooks/use-panel-apps.ts +2 -1
  132. package/src/platforms/desktop/components/__tests__/desktop-app-shell.test.tsx +50 -10
  133. package/src/platforms/desktop/components/desktop-app-shell.tsx +34 -10
  134. package/src/platforms/desktop/components/desktop-window-chrome.tsx +41 -16
  135. package/src/shared/components/common/__tests__/agent-avatar.test.tsx +3 -1
  136. package/src/shared/components/common/agent-avatar.tsx +5 -2
  137. package/src/shared/components/ui/tab-strip/__tests__/compact-tab-strip.test.tsx +24 -1
  138. package/src/shared/components/ui/tab-strip/compact-tab-strip.tsx +8 -4
  139. package/src/shared/lib/api/index.ts +1 -0
  140. package/src/shared/lib/api/preferences/preference-keys.config.ts +9 -0
  141. package/src/shared/lib/i18n/locales/en-US/chat.json +20 -2
  142. package/src/shared/lib/i18n/locales/en-US/core.json +2 -0
  143. package/src/shared/lib/i18n/locales/en-US/marketplace.json +2 -0
  144. package/src/shared/lib/i18n/locales/zh-CN/chat.json +20 -2
  145. package/src/shared/lib/i18n/locales/zh-CN/core.json +2 -0
  146. package/src/shared/lib/i18n/locales/zh-CN/marketplace.json +2 -0
  147. package/src/shared/lib/ui-document-title/__tests__/ui-document-title.test.ts +49 -0
  148. package/src/shared/lib/ui-document-title/index.ts +50 -21
  149. package/dist/assets/channels-list-page-DfpjB8XB.js +0 -8
  150. package/dist/assets/chat-page-D3QaEqjs.js +0 -105
  151. package/dist/assets/doc-browser-CuBTcBEZ.js +0 -1
  152. package/dist/assets/doc-browser-DVHhEb96.js +0 -1
  153. package/dist/assets/doc-browser-context-BfMXwnrz.js +0 -1
  154. package/dist/assets/index-ByJCbQqr.css +0 -1
  155. package/dist/assets/index-DjXqiloY.js +0 -103
  156. package/dist/assets/mcp-marketplace-page-98-V__3u.js +0 -1
  157. package/dist/assets/mcp-marketplace-page-BNq_PNmO.js +0 -40
  158. package/dist/assets/model-config-page-aAUkZkSX.js +0 -1
  159. package/dist/assets/plus-1Rxl5FlV.js +0 -1
  160. package/dist/assets/provider-scoped-model-input-we4_Z4gP.js +0 -1
  161. package/dist/assets/providers-config-page-CtMkPSyu.js +0 -1
  162. package/dist/assets/remote-fQ7N_r8u.js +0 -1
  163. package/dist/assets/rotate-cw-CzfIeAJj.js +0 -1
  164. package/dist/assets/runtime-config-page-TD1UrTbG.js +0 -1
  165. package/dist/assets/use-config-D8TESeo6.js +0 -1
  166. package/dist/assets/x-1qETdQ70.js +0 -1
  167. package/src/features/chat/components/conversation/chat-conversation-alerts.tsx +0 -37
  168. package/src/features/chat/features/input/components/__tests__/chat-attachment-upload-limit.test.ts +0 -38
  169. package/src/features/chat/features/input/components/chat-input-bar.container.tsx +0 -496
  170. package/src/features/chat/features/message/utils/chat-message-inline-content.utils.ts +0 -95
  171. package/src/features/chat/managers/__tests__/chat-input.manager.test.ts +0 -448
  172. package/src/features/chat/managers/__tests__/chat-run-snapshot.manager.test.ts +0 -129
  173. package/src/features/chat/managers/__tests__/chat-run.manager.test.ts +0 -236
  174. package/src/features/chat/managers/__tests__/chat-session-preference-sync.manager.test.ts +0 -219
  175. package/src/features/chat/managers/chat-input.manager.ts +0 -430
  176. package/src/features/chat/managers/chat-run.manager.ts +0 -110
  177. package/src/features/chat/managers/chat-session-preference-sync.manager.ts +0 -142
  178. package/src/features/chat/stores/chat-input.store.ts +0 -104
  179. /package/dist/assets/{config-hints-CTqBnCDp.js → config-hints-BkYBc7z0.js} +0 -0
@@ -37,7 +37,6 @@ type MarketplaceListCardActionProps = {
37
37
  record?: MarketplaceInstalledRecord;
38
38
  state: {
39
39
  isInstalling: boolean;
40
- isDisabled: boolean;
41
40
  canUpdate: boolean;
42
41
  canUninstall: boolean;
43
42
  busyAction?: MarketplaceManageAction;
@@ -52,17 +51,15 @@ type MarketplaceListCardActionProps = {
52
51
  function MarketplaceListCardMeta({
53
52
  title,
54
53
  spec,
55
- summary,
56
54
  }: {
57
55
  title: string;
58
56
  spec: string;
59
- summary: string;
60
57
  }) {
61
58
  return (
62
59
  <TooltipProvider delayDuration={400}>
63
60
  <Tooltip>
64
61
  <TooltipTrigger asChild>
65
- <div className="truncate text-[14px] font-semibold leading-tight text-gray-900">
62
+ <div className="truncate text-[14px] font-semibold leading-tight text-gray-950">
66
63
  {title}
67
64
  </div>
68
65
  </TooltipTrigger>
@@ -71,11 +68,11 @@ function MarketplaceListCardMeta({
71
68
  </TooltipContent>
72
69
  </Tooltip>
73
70
 
74
- <div className="mb-1.5 mt-0.5 flex min-w-0 items-center gap-1.5">
71
+ <div className="mb-2 mt-1 flex min-w-0 items-center gap-1.5">
75
72
  {spec ? (
76
73
  <Tooltip>
77
74
  <TooltipTrigger asChild>
78
- <span className="max-w-full truncate text-[11px] font-mono text-gray-400">
75
+ <span className="max-w-full truncate text-[11px] font-mono leading-tight text-gray-400">
79
76
  {spec}
80
77
  </span>
81
78
  </TooltipTrigger>
@@ -85,15 +82,11 @@ function MarketplaceListCardMeta({
85
82
  </Tooltip>
86
83
  ) : null}
87
84
  </div>
88
-
89
- <p className="line-clamp-2 text-left text-[12px] leading-relaxed text-gray-500/90">
90
- {summary}
91
- </p>
92
85
  </TooltipProvider>
93
86
  );
94
87
  }
95
88
 
96
- function MarketplaceListCardActions(props: MarketplaceListCardActionProps) {
89
+ function MarketplaceListCardActionButtons(props: MarketplaceListCardActionProps) {
97
90
  const {
98
91
  item,
99
92
  record,
@@ -103,96 +96,94 @@ function MarketplaceListCardActions(props: MarketplaceListCardActionProps) {
103
96
  } = props;
104
97
  const {
105
98
  isInstalling,
106
- isDisabled,
107
99
  canUpdate,
108
100
  canUninstall,
109
101
  busyAction,
110
102
  } = state;
111
103
  const busyForRecord = Boolean(busyAction);
112
104
  const hasActions = Boolean((item && !record) || (record && (canUpdate || canUninstall)));
105
+ if (!hasActions) {
106
+ return null;
107
+ }
113
108
 
114
109
  return (
115
110
  <div
116
111
  className={cn(
117
- "relative flex h-8 shrink-0 items-center justify-end",
118
- record ? "md:w-5" : "md:w-0",
112
+ "flex shrink-0 items-center justify-end gap-1.5 opacity-100 transition-opacity duration-150",
113
+ "md:pointer-events-none md:opacity-0",
114
+ "md:group-hover:pointer-events-auto md:group-hover:opacity-100",
115
+ "md:group-focus-within:pointer-events-auto md:group-focus-within:opacity-100",
119
116
  )}
120
117
  >
121
- <div
122
- className={cn(
123
- "hidden items-center justify-end transition-opacity duration-150 md:flex",
124
- hasActions && "group-hover:opacity-0 group-focus-within:opacity-0",
125
- )}
126
- >
127
- {record ? (
128
- <MarketplaceInstalledStatusIcon disabled={isDisabled} />
129
- ) : null}
130
- </div>
118
+ {item && !record && (
119
+ <button
120
+ onClick={(event) => {
121
+ event.stopPropagation();
122
+ onInstall(item);
123
+ }}
124
+ disabled={isInstalling}
125
+ className="inline-flex h-6 items-center gap-1 whitespace-nowrap rounded-md bg-primary px-2 text-[11px] font-medium text-white transition-colors hover:bg-primary-600 disabled:opacity-50"
126
+ >
127
+ <Download className="h-3 w-3" />
128
+ {isInstalling ? t("marketplaceInstalling") : t("marketplaceInstall")}
129
+ </button>
130
+ )}
131
131
 
132
- <div
133
- className={cn(
134
- "flex w-max items-center justify-end gap-2 transition-opacity duration-150",
135
- "opacity-100 md:pointer-events-none md:absolute md:right-0 md:opacity-0",
136
- "md:group-hover:pointer-events-auto md:group-hover:opacity-100",
137
- "md:group-focus-within:pointer-events-auto md:group-focus-within:opacity-100",
138
- )}
139
- >
140
- {item && !record && (
141
- <button
142
- onClick={(event) => {
143
- event.stopPropagation();
144
- onInstall(item);
145
- }}
146
- disabled={isInstalling}
147
- className="inline-flex h-8 items-center gap-1.5 whitespace-nowrap rounded-xl bg-primary px-3 text-xs font-medium text-white transition-colors hover:bg-primary-600 disabled:opacity-50"
148
- >
149
- <Download className="h-3.5 w-3.5" />
150
- {isInstalling ? t("marketplaceInstalling") : t("marketplaceInstall")}
151
- </button>
152
- )}
132
+ {record && canUpdate && (
133
+ <button
134
+ disabled={busyForRecord}
135
+ onClick={(event) => {
136
+ event.stopPropagation();
137
+ onManage("update", record);
138
+ }}
139
+ className="inline-flex h-6 items-center gap-1 whitespace-nowrap rounded-md border border-blue-200/80 bg-white px-2 text-[11px] font-medium text-blue-600 transition-colors hover:border-blue-300 hover:bg-blue-50 disabled:opacity-50"
140
+ >
141
+ <RefreshCw className="h-3 w-3" />
142
+ {busyAction === "update"
143
+ ? t("marketplaceUpdating")
144
+ : t("marketplaceUpdate")}
145
+ </button>
146
+ )}
153
147
 
154
- {record && canUpdate && (
155
- <button
156
- disabled={busyForRecord}
157
- onClick={(event) => {
158
- event.stopPropagation();
159
- onManage("update", record);
160
- }}
161
- className="inline-flex h-8 items-center gap-1.5 whitespace-nowrap rounded-xl border border-blue-200/80 bg-white px-3 text-xs font-medium text-blue-600 transition-colors hover:border-blue-300 hover:bg-blue-50 disabled:opacity-50"
162
- >
163
- <RefreshCw className="h-3.5 w-3.5" />
164
- {busyAction === "update"
165
- ? t("marketplaceUpdating")
166
- : t("marketplaceUpdate")}
167
- </button>
168
- )}
148
+ {record && canUninstall && (
149
+ <button
150
+ disabled={busyForRecord}
151
+ onClick={(event) => {
152
+ event.stopPropagation();
153
+ onManage("uninstall", record);
154
+ }}
155
+ className="inline-flex h-6 items-center gap-1 whitespace-nowrap rounded-md border border-gray-200/80 bg-white px-2 text-[11px] font-medium text-gray-500 transition-colors hover:border-rose-200 hover:bg-rose-50 hover:text-rose-600 disabled:opacity-50"
156
+ >
157
+ <Trash2 className="h-3 w-3" />
158
+ {busyAction === "uninstall"
159
+ ? t("marketplaceRemoving")
160
+ : t("marketplaceUninstall")}
161
+ </button>
162
+ )}
163
+ </div>
164
+ );
165
+ }
169
166
 
170
- {record && canUninstall && (
171
- <button
172
- disabled={busyForRecord}
173
- onClick={(event) => {
174
- event.stopPropagation();
175
- onManage("uninstall", record);
176
- }}
177
- 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-500 transition-colors hover:border-rose-200 hover:bg-rose-50 hover:text-rose-600 disabled:opacity-50"
178
- >
179
- <Trash2 className="h-3.5 w-3.5" />
180
- {busyAction === "uninstall"
181
- ? t("marketplaceRemoving")
182
- : t("marketplaceUninstall")}
183
- </button>
184
- )}
185
- </div>
167
+ function MarketplaceListCardStatus(props: {
168
+ record?: MarketplaceInstalledRecord;
169
+ disabled: boolean;
170
+ }) {
171
+ const { record, disabled } = props;
172
+ if (!record) {
173
+ return null;
174
+ }
175
+ return (
176
+ <div className="flex h-5 w-5 shrink-0 items-center justify-end">
177
+ <MarketplaceInstalledStatusIcon disabled={disabled} />
186
178
  </div>
187
179
  );
188
180
  }
189
181
 
190
182
  function MarketplaceInstalledStatusIcon(props: { disabled: boolean }) {
191
183
  const { disabled } = props;
192
- const { language } = useI18n();
193
184
  const label = disabled
194
- ? readLocalized({ zh: "已禁用", en: "Disabled" }, language)
195
- : readLocalized({ zh: "已安装", en: "Installed" }, language);
185
+ ? t("marketplaceDisabledStatus")
186
+ : t("marketplaceInstalledStatus");
196
187
 
197
188
  return (
198
189
  <TooltipProvider delayDuration={300}>
@@ -251,6 +242,7 @@ export function MarketplaceListCard(props: {
251
242
  pickLocalizedText(item?.summaryI18n, item?.summary, localeFallbacks) ||
252
243
  (record ? t("marketplaceInstalledLocalSummary") : "");
253
244
  const spec = item?.install.spec ?? record?.spec ?? "";
245
+ const tags = item?.tags ?? [];
254
246
  const targetId = record?.id || record?.spec;
255
247
  const busyAction = targetId
256
248
  ? manageState.actionsByTarget.get(targetId)
@@ -268,35 +260,69 @@ export function MarketplaceListCard(props: {
268
260
  return (
269
261
  <article
270
262
  onClick={onOpen}
271
- className="group flex cursor-pointer items-start justify-between gap-3.5 rounded-2xl border border-gray-200/40 bg-white px-5 py-4 shadow-sm transition-all hover:border-blue-300/80 hover:shadow-md"
263
+ className="group flex h-full min-h-[156px] cursor-pointer flex-col rounded-xl border border-gray-200/60 bg-white p-3.5 shadow-sm transition-colors hover:border-gray-300 hover:bg-gray-50/60"
272
264
  >
273
- <div className="flex min-w-0 flex-1 gap-3">
265
+ <div className="flex min-w-0 items-start gap-2.5">
274
266
  <MarketplaceItemIcon
275
267
  name={title}
276
268
  fallback={spec || t("marketplaceTypeSkill")}
269
+ className="h-9 w-9 rounded-lg text-xs"
277
270
  />
278
- <div className="flex min-w-0 flex-1 flex-col justify-center">
279
- <MarketplaceListCardMeta title={title} spec={spec} summary={summary} />
271
+ <div className="flex min-w-0 flex-1 flex-col">
272
+ <MarketplaceListCardMeta title={title} spec={spec} />
280
273
  </div>
274
+
275
+ <MarketplaceListCardStatus record={record} disabled={isDisabled} />
281
276
  </div>
282
277
 
283
- <MarketplaceListCardActions
284
- item={item}
285
- record={record}
286
- state={{
287
- isInstalling,
288
- isDisabled,
289
- canUpdate,
290
- canUninstall,
291
- busyAction,
292
- }}
293
- onInstall={onInstall}
294
- onManage={onManage}
295
- />
278
+ <div className="mt-2 flex min-w-0 flex-1 flex-col">
279
+ <p className="line-clamp-2 text-left text-[12px] leading-relaxed text-gray-500">
280
+ {summary}
281
+ </p>
282
+
283
+ <div className="mt-auto flex min-w-0 items-end justify-between gap-2 pt-2">
284
+ <div className="min-w-0 flex-1">
285
+ <MarketplaceListCardTags tags={tags} />
286
+ </div>
287
+ <MarketplaceListCardActionButtons
288
+ item={item}
289
+ record={record}
290
+ state={{
291
+ isInstalling,
292
+ canUpdate,
293
+ canUninstall,
294
+ busyAction,
295
+ }}
296
+ onInstall={onInstall}
297
+ onManage={onManage}
298
+ />
299
+ </div>
300
+ </div>
296
301
  </article>
297
302
  );
298
303
  }
299
304
 
300
- function readLocalized(text: { zh: string; en: string }, language: string) {
301
- return language.startsWith("zh") ? text.zh : text.en;
305
+ function MarketplaceListCardTags({ tags }: { tags: string[] }) {
306
+ const visibleTags = tags.filter(isVisibleMarketplaceTag).slice(0, 3);
307
+ if (visibleTags.length === 0) {
308
+ return null;
309
+ }
310
+
311
+ return (
312
+ <div className="flex min-w-0 flex-wrap gap-1.5 overflow-hidden">
313
+ {visibleTags.map((tag) => (
314
+ <span
315
+ key={tag}
316
+ className="max-w-[104px] truncate rounded-md bg-gray-100 px-2 py-0.5 text-[11px] font-medium leading-5 text-gray-500"
317
+ >
318
+ {tag}
319
+ </span>
320
+ ))}
321
+ </div>
322
+ );
323
+ }
324
+
325
+ function isVisibleMarketplaceTag(tag: string) {
326
+ const normalized = tag.trim().toLowerCase();
327
+ return normalized.length > 0 && normalized !== "skill";
302
328
  }
@@ -78,21 +78,26 @@ export function MarketplaceListSkeleton({ count }: {
78
78
  {Array.from({ length: count }, (_, index) => (
79
79
  <article
80
80
  key={`marketplace-skeleton-${index}`}
81
- className="h-full rounded-2xl border border-gray-200/40 bg-white px-5 py-4 shadow-sm"
81
+ className="h-full rounded-xl border border-gray-200/60 bg-white p-3.5 shadow-sm"
82
82
  >
83
- <div className="flex items-start justify-between gap-3.5">
84
- <div className="flex min-w-0 flex-1 gap-3">
85
- <Skeleton className="h-10 w-10 shrink-0 rounded-xl" />
86
- <div className="min-w-0 flex-1 space-y-2 pt-0.5">
83
+ <div className="flex items-start justify-between gap-2.5">
84
+ <div className="flex min-w-0 flex-1 gap-2.5">
85
+ <Skeleton className="h-9 w-9 shrink-0 rounded-lg" />
86
+ <div className="min-w-0 flex-1 space-y-2">
87
87
  <Skeleton className="h-4 w-32 max-w-[70%]" />
88
88
  <div className="flex items-center gap-2">
89
89
  <Skeleton className="h-3 w-12" />
90
90
  <Skeleton className="h-3 w-24" />
91
91
  </div>
92
92
  <Skeleton className="h-3 w-full" />
93
+ <div className="flex gap-1.5 pt-0.5">
94
+ <Skeleton className="h-5 w-14 rounded-md" />
95
+ <Skeleton className="h-5 w-16 rounded-md" />
96
+ <Skeleton className="h-5 w-12 rounded-md" />
97
+ </div>
93
98
  </div>
94
99
  </div>
95
- <Skeleton className="h-8 w-20 shrink-0 rounded-xl" />
100
+ <Skeleton className="h-7 w-16 shrink-0 rounded-md" />
96
101
  </div>
97
102
  </article>
98
103
  ))}
@@ -4,10 +4,11 @@ import type { PanelAppPreferencesUpdateView } from '@/shared/lib/api';
4
4
 
5
5
  const PANEL_APPS_QUERY_KEY = ['panel-apps'] as const;
6
6
 
7
- export function usePanelApps() {
7
+ export function usePanelApps(options: { enabled?: boolean } = {}) {
8
8
  return useQuery({
9
9
  queryKey: PANEL_APPS_QUERY_KEY,
10
10
  queryFn: () => nextclawClient.panelApps.listPanelApps(),
11
+ enabled: options.enabled ?? true,
11
12
  staleTime: 0,
12
13
  });
13
14
  }
@@ -1,8 +1,9 @@
1
1
  import { act, render, screen } from "@testing-library/react";
2
2
  import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
3
3
  import { MemoryRouter } from "react-router-dom";
4
- import { afterEach, describe, expect, it, vi } from "vitest";
4
+ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
5
5
  import { DesktopAppShell } from "@/platforms/desktop/components/desktop-app-shell";
6
+ import { viewportLayoutManager } from "@/app/managers/viewport-layout.manager";
6
7
 
7
8
  vi.mock("@/app/components/layout/sidebar", () => ({
8
9
  Sidebar: () => <aside data-testid="settings-sidebar">Settings Sidebar</aside>,
@@ -16,7 +17,10 @@ type WindowStateListener = (snapshot: { isMaximized: boolean }) => void;
16
17
 
17
18
  let windowStateListener: WindowStateListener | null = null;
18
19
 
19
- function setDesktopPlatform(platform: string | null, isMaximized = false): void {
20
+ function setDesktopPlatform(
21
+ platform: string | null,
22
+ isMaximized = false,
23
+ ): void {
20
24
  windowStateListener = null;
21
25
  window.nextclawDesktop = platform
22
26
  ? ({
@@ -59,6 +63,11 @@ function renderDesktopShell(platform: string | null, isMobileLayout = false) {
59
63
  }
60
64
 
61
65
  describe("DesktopAppShell", () => {
66
+ beforeEach(() => {
67
+ window.localStorage.clear();
68
+ viewportLayoutManager.resetForTests();
69
+ });
70
+
62
71
  afterEach(() => {
63
72
  setDesktopPlatform(null);
64
73
  });
@@ -68,29 +77,58 @@ describe("DesktopAppShell", () => {
68
77
 
69
78
  const chrome = screen.getByTestId("desktop-window-chrome");
70
79
  const sidebarChrome = screen.getByTestId("desktop-window-chrome-sidebar");
71
- const resizeStrip = screen.getByTestId("desktop-window-chrome-resize-strip");
80
+ const resizeStrip = screen.getByTestId(
81
+ "desktop-window-chrome-resize-strip",
82
+ );
72
83
  const controls = screen.getByTestId("desktop-window-controls");
73
84
 
74
85
  expect(chrome).toBeTruthy();
75
- expect(chrome.parentElement?.style.getPropertyValue("--desktop-titlebar-height")).toBe("40px");
86
+ expect(
87
+ chrome.parentElement?.style.getPropertyValue("--desktop-titlebar-height"),
88
+ ).toBe("40px");
76
89
  expect(chrome.className).toContain("bg-secondary");
77
90
  expect(chrome.className).not.toContain(" shrink-0 border-b ");
78
- expect(chrome.className).toContain("after:left-[var(--desktop-sidebar-width)]");
91
+ expect(chrome.className).toContain(
92
+ "after:left-[var(--desktop-sidebar-width)]",
93
+ );
79
94
  expect(chrome.className).toContain("after:border-b");
80
95
  expect(chrome.className).toContain("desktop-window-drag");
81
- expect(sidebarChrome.className).toContain("w-[var(--desktop-sidebar-width)]");
96
+ expect(sidebarChrome.className).toContain(
97
+ "w-[var(--desktop-sidebar-width)]",
98
+ );
82
99
  expect(sidebarChrome.className).not.toContain("border-b");
83
100
  expect(sidebarChrome.className).toContain("desktop-window-drag");
84
101
  expect(resizeStrip.className).toContain("desktop-window-no-drag");
85
102
  expect(resizeStrip.className).toContain("top-0");
86
103
  expect(resizeStrip.className).toContain("h-1");
87
104
  expect(controls.className).toContain("desktop-window-no-drag");
88
- expect(screen.getByLabelText("Minimize").className).toContain("desktop-window-no-drag");
89
- expect(screen.getByLabelText("Maximize").className).toContain("desktop-window-no-drag");
90
- expect(screen.getByLabelText("Close").className).toContain("desktop-window-no-drag");
105
+ expect(screen.getByLabelText("Minimize").className).toContain(
106
+ "desktop-window-no-drag",
107
+ );
108
+ expect(screen.getByLabelText("Maximize").className).toContain(
109
+ "desktop-window-no-drag",
110
+ );
111
+ expect(screen.getByLabelText("Close").className).toContain(
112
+ "desktop-window-no-drag",
113
+ );
91
114
  expect(screen.getByTestId("app-content")).toBeTruthy();
92
115
  });
93
116
 
117
+ it("uses the collapsed sidebar width in Windows chrome from the shared layout store", () => {
118
+ viewportLayoutManager.setSidebarCollapsed(true);
119
+
120
+ renderDesktopShell("win32");
121
+
122
+ const chromeRoot = screen.getByTestId(
123
+ "desktop-window-chrome",
124
+ ).parentElement;
125
+
126
+ expect(chromeRoot?.style.getPropertyValue("--desktop-sidebar-width")).toBe(
127
+ "56px",
128
+ );
129
+ expect(screen.getByAltText("NextClaw")).toBeTruthy();
130
+ });
131
+
94
132
  it("switches the Windows maximize button to restore while maximized", async () => {
95
133
  renderDesktopShell("win32");
96
134
 
@@ -100,7 +138,9 @@ describe("DesktopAppShell", () => {
100
138
  windowStateListener?.({ isMaximized: true });
101
139
  });
102
140
 
103
- expect(screen.getByLabelText("Restore").className).toContain("desktop-window-no-drag");
141
+ expect(screen.getByLabelText("Restore").className).toContain(
142
+ "desktop-window-no-drag",
143
+ );
104
144
  expect(screen.queryByLabelText("Maximize")).toBeNull();
105
145
  });
106
146
 
@@ -11,9 +11,12 @@ import { MobileBottomNav } from "@/platforms/mobile";
11
11
  import type { DocBrowserCustomTabRenderers } from "@/shared/components/doc-browser/doc-browser-renderer.types";
12
12
  import type { DocBrowserDockControls } from "@/shared/components/doc-browser/doc-browser-context";
13
13
  import { cn } from "@/shared/lib/utils";
14
+ import { useViewportLayoutStore } from "@/app/stores/viewport-layout.store";
15
+ import { SIDEBAR_RAIL_WIDTH_PX } from "@/app/components/layout/sidebar-rail.styles";
14
16
 
15
17
  const DocBrowser = lazy(async () => ({
16
- default: (await import("@/shared/components/doc-browser/doc-browser")).DocBrowser,
18
+ default: (await import("@/shared/components/doc-browser/doc-browser"))
19
+ .DocBrowser,
17
20
  }));
18
21
 
19
22
  type DesktopAppShellProps = {
@@ -38,8 +41,17 @@ export function DesktopAppShell({
38
41
  children,
39
42
  }: DesktopAppShellProps) {
40
43
  const isMainRoute = isMainWorkspaceRoute(pathname);
41
- const showMobileBottomNav = isMobileLayout && !isChatSessionDetailRoute(pathname);
44
+ const isSidebarCollapsed = useViewportLayoutStore(
45
+ (state) => state.isSidebarCollapsed,
46
+ );
47
+ const showMobileBottomNav =
48
+ isMobileLayout && !isChatSessionDetailRoute(pathname);
42
49
  const shouldUseWindowsChrome = isWindowsDesktopHost();
50
+ const desktopSidebarWidth = isSidebarCollapsed
51
+ ? `${SIDEBAR_RAIL_WIDTH_PX}px`
52
+ : isMainRoute
53
+ ? "280px"
54
+ : "240px";
43
55
 
44
56
  return (
45
57
  <div
@@ -47,13 +59,19 @@ export function DesktopAppShell({
47
59
  "h-screen flex flex-col overflow-hidden bg-background font-sans text-foreground",
48
60
  shouldUseWindowsChrome ? "rounded-[10px]" : null,
49
61
  )}
50
- style={shouldUseWindowsChrome ? ({
51
- "--desktop-titlebar-height": "40px",
52
- "--desktop-caption-safe-right": "140px",
53
- "--desktop-sidebar-width": isMainRoute ? "280px" : "240px",
54
- } as CSSProperties) : undefined}
62
+ style={
63
+ shouldUseWindowsChrome
64
+ ? ({
65
+ "--desktop-titlebar-height": "40px",
66
+ "--desktop-caption-safe-right": "140px",
67
+ "--desktop-sidebar-width": desktopSidebarWidth,
68
+ } as CSSProperties)
69
+ : undefined
70
+ }
55
71
  >
56
- {shouldUseWindowsChrome ? <DesktopWindowChrome /> : null}
72
+ {shouldUseWindowsChrome ? (
73
+ <DesktopWindowChrome sidebarCollapsed={isSidebarCollapsed} />
74
+ ) : null}
57
75
  <div className="flex min-h-0 flex-1 overflow-hidden">
58
76
  {!isMainRoute && <Sidebar mode="settings" />}
59
77
  <div className="flex-1 flex min-w-0 overflow-hidden relative">
@@ -70,7 +88,10 @@ export function DesktopAppShell({
70
88
  </div>
71
89
  {isDocBrowserOpen && docBrowserMode === "docked" ? (
72
90
  <Suspense fallback={null}>
73
- <DocBrowser customTabRenderers={docBrowserRenderers} dockControls={docBrowserDockControls} />
91
+ <DocBrowser
92
+ customTabRenderers={docBrowserRenderers}
93
+ dockControls={docBrowserDockControls}
94
+ />
74
95
  </Suspense>
75
96
  ) : null}
76
97
  {sideDock}
@@ -79,7 +100,10 @@ export function DesktopAppShell({
79
100
  {showMobileBottomNav ? <MobileBottomNav /> : null}
80
101
  {isDocBrowserOpen && docBrowserMode === "floating" ? (
81
102
  <Suspense fallback={null}>
82
- <DocBrowser customTabRenderers={docBrowserRenderers} dockControls={docBrowserDockControls} />
103
+ <DocBrowser
104
+ customTabRenderers={docBrowserRenderers}
105
+ dockControls={docBrowserDockControls}
106
+ />
83
107
  </Suspense>
84
108
  ) : null}
85
109
  </div>
@@ -1,6 +1,7 @@
1
1
  import { useEffect, useState } from "react";
2
2
  import { Copy, Minus, Square, X } from "lucide-react";
3
3
  import { BrandHeader } from "@/shared/components/common/brand-header";
4
+ import { cn } from "@/shared/lib/utils";
4
5
  import type { DesktopWindowStateSnapshot } from "@/platforms/desktop/types/desktop-update.types";
5
6
 
6
7
  type DesktopWindowControlAction = "minimize" | "toggle-maximize" | "close";
@@ -17,7 +18,11 @@ const windowControls: WindowControlDefinition[] = [
17
18
  { action: "close", label: "Close", icon: X, variant: "danger" },
18
19
  ];
19
20
 
20
- export function DesktopWindowChrome() {
21
+ export function DesktopWindowChrome({
22
+ sidebarCollapsed = false,
23
+ }: {
24
+ sidebarCollapsed?: boolean;
25
+ }) {
21
26
  const isMaximized = useDesktopWindowMaximizedState();
22
27
  const maximizeControl: WindowControlDefinition = isMaximized
23
28
  ? { action: "toggle-maximize", label: "Restore", icon: Copy }
@@ -33,21 +38,37 @@ export function DesktopWindowChrome() {
33
38
  data-testid="desktop-window-chrome-resize-strip"
34
39
  />
35
40
  <div
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"
41
+ className={cn(
42
+ "desktop-window-drag relative z-10 flex h-full w-[var(--desktop-sidebar-width)] shrink-0 items-center bg-secondary text-secondary-foreground",
43
+ sidebarCollapsed ? "justify-center px-0" : "pl-4 pr-3",
44
+ )}
37
45
  data-testid="desktop-window-chrome-sidebar"
38
46
  >
39
- <BrandHeader
40
- className="flex min-w-0 shrink-0 items-center gap-2.5"
41
- density="chrome"
42
- />
47
+ {sidebarCollapsed ? (
48
+ <img
49
+ src="/logo.svg"
50
+ alt="NextClaw"
51
+ className="h-6 w-6 shrink-0 object-contain"
52
+ />
53
+ ) : (
54
+ <BrandHeader
55
+ className="flex min-w-0 shrink-0 items-center gap-2.5"
56
+ density="chrome"
57
+ />
58
+ )}
43
59
  </div>
44
60
  <div
45
61
  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"
46
62
  data-testid="desktop-window-controls"
47
63
  >
48
- {[windowControls[0], maximizeControl, windowControls[1]].map((control) => (
49
- <DesktopWindowControlButton key={control.action} control={control} />
50
- ))}
64
+ {[windowControls[0], maximizeControl, windowControls[1]].map(
65
+ (control) => (
66
+ <DesktopWindowControlButton
67
+ key={control.action}
68
+ control={control}
69
+ />
70
+ ),
71
+ )}
51
72
  </div>
52
73
  </header>
53
74
  );
@@ -86,14 +107,18 @@ function useDesktopWindowMaximizedState(): boolean {
86
107
  let isSubscribed = true;
87
108
  const desktopApi = window.nextclawDesktop;
88
109
 
89
- void desktopApi?.getWindowState?.().then((snapshot: DesktopWindowStateSnapshot) => {
90
- if (isSubscribed) {
110
+ void desktopApi
111
+ ?.getWindowState?.()
112
+ .then((snapshot: DesktopWindowStateSnapshot) => {
113
+ if (isSubscribed) {
114
+ setIsMaximized(snapshot.isMaximized);
115
+ }
116
+ });
117
+ const unsubscribe = desktopApi?.onWindowStateChanged?.(
118
+ (snapshot: DesktopWindowStateSnapshot) => {
91
119
  setIsMaximized(snapshot.isMaximized);
92
- }
93
- });
94
- const unsubscribe = desktopApi?.onWindowStateChanged?.((snapshot: DesktopWindowStateSnapshot) => {
95
- setIsMaximized(snapshot.isMaximized);
96
- });
120
+ },
121
+ );
97
122
 
98
123
  return () => {
99
124
  isSubscribed = false;