@nextclaw/ui 0.15.1 → 0.15.3

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 (192) hide show
  1. package/CHANGELOG.md +51 -0
  2. package/dist/assets/{api-b1R9VdeH.js → api-iNBpppzj.js} +3 -3
  3. package/dist/assets/{appearance-settings-page-D2O9pch9.js → appearance-settings-page-DMffQFR3.js} +1 -1
  4. package/dist/assets/{book-open-B77lQ2Tg.js → book-open-C3mJGwiy.js} +1 -1
  5. package/dist/assets/channels-list-page-CCtp0BRS.js +8 -0
  6. package/dist/assets/chat-page-H5a0dWvO.js +1 -0
  7. package/dist/assets/config-split-page-CrOJgB-Y.js +1 -0
  8. package/dist/assets/{confirm-dialog-C-NXSioX.js → confirm-dialog-Chv50USy.js} +2 -2
  9. package/dist/assets/{desktop-update-config-usHpXugP.js → desktop-update-config-KzTuXRf_.js} +1 -1
  10. package/dist/assets/{dist-jdGmbb6w.js → dist-BunzmC4B.js} +1 -1
  11. package/dist/assets/{dist-YtoqU-AT.js → dist-ChmGwvtK.js} +1 -1
  12. package/dist/assets/doc-browser-DNRL4Zvd.js +1 -0
  13. package/dist/assets/doc-browser-Dg0pmkOg.js +1 -0
  14. package/dist/assets/doc-browser-context-BHY-0FSD.js +1 -0
  15. package/dist/assets/{es2015-bNpL2LKy.js → es2015-DQZ5JWwK.js} +1 -1
  16. package/dist/assets/{external-link-BM6fk8q-.js → external-link-CU0kzzIP.js} +1 -1
  17. package/dist/assets/form-actions-BvCp_0lS.js +1 -0
  18. package/dist/assets/{i18n-provider-CntWolRA.js → i18n-provider-Cu8XyZb_.js} +1 -1
  19. package/dist/assets/index-CzfBdvK1.css +1 -0
  20. package/dist/assets/index-DGqHne1Q.js +104 -0
  21. package/dist/assets/loader-circle-CDSFuzmk.js +1 -0
  22. package/dist/assets/mcp-marketplace-page-BeEQK0DL.js +9 -0
  23. package/dist/assets/mcp-marketplace-page-D6idOTLp.js +1 -0
  24. package/dist/assets/model-config-page-bNNo-XyG.js +1 -0
  25. package/dist/assets/plus-D4DsdLN-.js +1 -0
  26. package/dist/assets/{provider-scoped-model-input-Cm_a4GHf.js → provider-scoped-model-input-IlgQCsoT.js} +1 -1
  27. package/dist/assets/providers-config-page-wLo6CAJw.js +1 -0
  28. package/dist/assets/{react-BN8uzPnp.js → react-BPIsQIlO.js} +1 -1
  29. package/dist/assets/{refresh-cw-BpVcFPmJ.js → refresh-cw-CILd1vAP.js} +1 -1
  30. package/dist/assets/remote-uKEYp7AR.js +1 -0
  31. package/dist/assets/rotate-cw-wPFPg1kH.js +1 -0
  32. package/dist/assets/runtime-config-page-DDn1sP4G.js +1 -0
  33. package/dist/assets/{save-BpFW5fpk.js → save-ByJqcPTP.js} +1 -1
  34. package/dist/assets/{search-D0uO7enf.js → search-DVIV6pfD.js} +1 -1
  35. package/dist/assets/search-config-page-B-Cf4iiB.js +1 -0
  36. package/dist/assets/secrets-config-page-DPKsY8Iw.js +3 -0
  37. package/dist/assets/security-config-e9SSBlKr.js +1 -0
  38. package/dist/assets/{select-Bh-CrA4p.js → select-BdgczSNw.js} +1 -1
  39. package/dist/assets/{setting-row-BJfCHRcb.js → setting-row-BkTVUTKx.js} +1 -1
  40. package/dist/assets/{settings-2-Blzi1vr6.js → settings-2-CH5shGg_.js} +1 -1
  41. package/dist/assets/{side-dock-ZdaSrQVB.js → side-dock-BsBvcJ9i.js} +2 -2
  42. package/dist/assets/{skeleton-BLlEzebZ.js → skeleton-DeX1FKw7.js} +1 -1
  43. package/dist/assets/switch-Cv8Ekb3x.js +1 -0
  44. package/dist/assets/{tag-chip-CzGihiQx.js → tag-chip-RB9uKcoC.js} +1 -1
  45. package/dist/assets/{tooltip-CdcrBjrC.js → tooltip-oQlEN-l6.js} +1 -1
  46. package/dist/assets/{use-config-CVPgsBmU.js → use-config-l3W7y8Qz.js} +1 -1
  47. package/dist/assets/{use-confirm-dialog-CSeBk9SF.js → use-confirm-dialog-BAxaVOJI.js} +1 -1
  48. package/dist/assets/x-BBMffEN0.js +1 -0
  49. package/dist/index.html +38 -30
  50. package/index.html +12 -4
  51. package/package.json +9 -9
  52. package/src/app/components/layout/__tests__/sidebar.layout.test.tsx +29 -1
  53. package/src/app/components/layout/sidebar-items.tsx +46 -7
  54. package/src/app/components/layout/sidebar-rail.styles.ts +2 -2
  55. package/src/app/components/layout/sidebar.tsx +84 -62
  56. package/src/app/configs/app-navigation.config.ts +49 -0
  57. package/src/app/styles/design-system.css +99 -4
  58. package/src/features/channels/components/config/channel-form.tsx +14 -11
  59. package/src/features/chat/components/conversation/__tests__/chat-conversation-header-section.test.tsx +40 -0
  60. package/src/features/chat/components/conversation/chat-conversation-header-section.tsx +3 -1
  61. package/src/features/chat/components/conversation/chat-conversation-panel.tsx +1 -1
  62. package/src/features/chat/components/layout/__tests__/chat-sidebar-toolbar.test.tsx +48 -0
  63. package/src/features/chat/components/layout/__tests__/chat-sidebar-utility-menu.test.tsx +55 -0
  64. package/src/features/chat/components/layout/chat-sidebar-desktop-layout.tsx +0 -3
  65. package/src/features/chat/components/layout/chat-sidebar-toolbar.tsx +3 -4
  66. package/src/features/chat/components/layout/chat-sidebar-utility-menu.tsx +30 -31
  67. package/src/features/chat/components/layout/chat-sidebar.tsx +21 -7
  68. package/src/features/chat/features/conversation/components/session-conversation-input.tsx +2 -1
  69. package/src/features/chat/features/conversation/components/session-queued-input-rows.tsx +1 -1
  70. package/src/features/chat/features/message/components/__tests__/chat-inline-panel-app-card.test.tsx +22 -1
  71. package/src/features/chat/features/message/components/__tests__/chat-message-list.container.test.tsx +62 -1
  72. package/src/features/chat/features/message/components/chat-inline-panel-app-card.tsx +10 -5
  73. package/src/features/chat/features/message/components/chat-message-list.container.tsx +47 -1
  74. package/src/features/chat/features/message/utils/__tests__/chat-message-process-summary.utils.test.ts +11 -1
  75. package/src/features/chat/features/message/utils/__tests__/chat-message-texts.utils.test.ts +23 -0
  76. package/src/features/chat/features/message/utils/chat-inline-panel-app-card.utils.ts +0 -11
  77. package/src/features/chat/features/message/utils/chat-message-process-summary.utils.ts +5 -0
  78. package/src/features/chat/features/message/utils/chat-message-texts.utils.ts +138 -0
  79. package/src/features/chat/features/session/components/__tests__/chat-sidebar-project-groups.test.tsx +116 -0
  80. package/src/features/chat/features/session/components/chat-sidebar-project-groups.tsx +108 -46
  81. package/src/features/chat/features/session/components/chat-sidebar-session-entry.tsx +6 -0
  82. package/src/features/chat/features/session/components/chat-sidebar-session-item.tsx +48 -24
  83. package/src/features/chat/features/session/components/chat-sidebar-session-list.tsx +0 -3
  84. package/src/features/chat/features/session/components/session-header/chat-session-header-actions.tsx +0 -4
  85. package/src/features/chat/features/session/components/session-header/chat-session-title-switcher.tsx +2 -2
  86. package/src/features/chat/features/session/utils/chat-sidebar-session-groups.utils.ts +25 -3
  87. package/src/features/chat/features/welcome/components/chat-welcome-capability-grid.tsx +9 -4
  88. package/src/features/chat/features/welcome/components/chat-welcome-project-picker.tsx +1 -1
  89. package/src/features/chat/features/welcome/components/chat-welcome-session-type-picker.tsx +1 -1
  90. package/src/features/chat/features/welcome/components/chat-welcome.tsx +0 -5
  91. package/src/features/chat/features/workspace/components/__tests__/chat-session-workspace-file-preview.test.tsx +226 -56
  92. package/src/features/chat/features/workspace/components/__tests__/chat-session-workspace-panel-content.test.tsx +1 -0
  93. package/src/features/chat/features/workspace/components/__tests__/chat-session-workspace-panel.test.tsx +144 -0
  94. package/src/features/chat/features/workspace/components/chat-session-workspace-directory-browser.tsx +99 -0
  95. package/src/features/chat/features/workspace/components/chat-session-workspace-file-breadcrumb-browser.tsx +2 -2
  96. package/src/features/chat/features/workspace/components/chat-session-workspace-file-breadcrumbs.tsx +4 -2
  97. package/src/features/chat/features/workspace/components/chat-session-workspace-file-preview.tsx +232 -21
  98. package/src/features/chat/features/workspace/components/chat-session-workspace-panel-content.tsx +3 -0
  99. package/src/features/chat/features/workspace/components/chat-session-workspace-panel-nav.tsx +44 -1
  100. package/src/features/chat/features/workspace/components/chat-session-workspace-panel.tsx +45 -3
  101. package/src/features/chat/features/workspace/utils/chat-workspace-file-tab-persistence.utils.ts +4 -0
  102. package/src/features/chat/managers/__tests__/chat-session-list.manager.test.ts +33 -2
  103. package/src/features/chat/managers/chat-session-list.manager.ts +32 -0
  104. package/src/features/chat/managers/chat-thread.manager.ts +2 -0
  105. package/src/features/chat/stores/chat-session-list.store.ts +28 -5
  106. package/src/features/chat/stores/chat-thread.store.ts +3 -0
  107. package/src/features/cron/components/cron-config.tsx +63 -80
  108. package/src/features/marketplace/components/detail-doc/__tests__/marketplace-detail-doc.test.tsx +44 -6
  109. package/src/features/marketplace/components/marketplace-detail-doc.tsx +8 -4
  110. package/src/features/marketplace/components/marketplace-page-parts.tsx +1 -1
  111. package/src/features/marketplace/components/mcp/mcp-marketplace-page.tsx +5 -12
  112. package/src/features/marketplace/hooks/use-marketplace-detail-doc-entry.ts +84 -0
  113. package/src/features/panel-apps/components/panel-app-list-item.tsx +6 -6
  114. package/src/features/panel-apps/components/panel-apps-list.tsx +2 -2
  115. package/src/features/panel-apps/index.ts +1 -0
  116. package/src/features/panel-apps/utils/panel-app-doc-browser.utils.tsx +2 -10
  117. package/src/features/panel-apps/utils/panel-app-iframe.utils.ts +6 -0
  118. package/src/features/pwa/managers/__tests__/pwa-shell-theme.manager.test.ts +11 -3
  119. package/src/features/pwa/managers/pwa-shell-theme.manager.ts +3 -1
  120. package/src/features/right-panel-resources/components/right-panel-resource-home-page.tsx +1 -1
  121. package/src/features/service-apps/components/service-apps-panel.tsx +145 -35
  122. package/src/features/settings/components/config/provider-auth-section.tsx +1 -1
  123. package/src/features/settings/components/config/provider-form.tsx +11 -8
  124. package/src/features/settings/components/config/provider-models-section.tsx +7 -7
  125. package/src/features/settings/components/config/provider-pill-selector.tsx +1 -1
  126. package/src/features/settings/components/config/secrets-config-form.tsx +5 -4
  127. package/src/features/settings/pages/model-config-page.tsx +26 -25
  128. package/src/features/settings/pages/providers-config-page.tsx +10 -13
  129. package/src/features/settings/pages/search-config-page.tsx +8 -5
  130. package/src/features/side-dock/components/side-dock.tsx +3 -3
  131. package/src/features/system-status/components/config/runtime-config-editor.tsx +5 -4
  132. package/src/index.css +257 -13
  133. package/src/shared/components/actions/action-link.tsx +1 -1
  134. package/src/shared/components/config-split-page.tsx +6 -6
  135. package/src/shared/components/doc-browser/__tests__/doc-browser-url.utils.test.ts +73 -0
  136. package/src/shared/components/doc-browser/__tests__/doc-browser.test.tsx +48 -0
  137. package/src/shared/components/doc-browser/doc-browser-context.tsx +4 -0
  138. package/src/shared/components/doc-browser/doc-browser-panel-parts.tsx +6 -2
  139. package/src/shared/components/doc-browser/doc-browser-renderer.types.ts +2 -1
  140. package/src/shared/components/doc-browser/doc-browser-tab-strip.tsx +10 -10
  141. package/src/shared/components/doc-browser/doc-browser.tsx +6 -4
  142. package/src/shared/components/doc-browser/utils/doc-browser-url.utils.ts +62 -4
  143. package/src/shared/components/resizable-right-panel/__tests__/resizable-right-panel.test.tsx +18 -0
  144. package/src/shared/components/resizable-right-panel/resizable-right-panel.tsx +28 -3
  145. package/src/shared/components/ui/actions/__tests__/icon-action-button.test.tsx +36 -0
  146. package/src/shared/components/ui/actions/form-actions.tsx +25 -0
  147. package/src/shared/components/ui/actions/icon-action-button.tsx +25 -2
  148. package/src/shared/components/ui/button.tsx +1 -1
  149. package/src/shared/components/ui/card.tsx +12 -4
  150. package/src/shared/components/ui/dialog.tsx +1 -1
  151. package/src/shared/components/ui/input.tsx +1 -1
  152. package/src/shared/components/ui/select.tsx +1 -1
  153. package/src/shared/components/ui/switch.tsx +1 -1
  154. package/src/shared/components/ui/tab-strip/compact-tab-strip.tsx +2 -1
  155. package/src/shared/components/ui/tabs.tsx +1 -1
  156. package/src/shared/components/ui/textarea.tsx +1 -1
  157. package/src/shared/hooks/use-server-path-browse.ts +26 -4
  158. package/src/shared/hooks/use-server-path-read.ts +11 -1
  159. package/src/shared/lib/api/utils/server-path.utils.ts +1 -0
  160. package/src/shared/lib/i18n/locales/en-US/chat.json +99 -0
  161. package/src/shared/lib/i18n/locales/en-US/core.json +3 -0
  162. package/src/shared/lib/i18n/locales/en-US/doc-browser.json +5 -0
  163. package/src/shared/lib/i18n/locales/zh-CN/chat.json +99 -0
  164. package/src/shared/lib/i18n/locales/zh-CN/core.json +3 -0
  165. package/src/shared/lib/i18n/locales/zh-CN/doc-browser.json +5 -0
  166. package/src/shared/lib/session-project/workspace-file-breadcrumb.utils.ts +9 -5
  167. package/src/shared/lib/theme/index.test.ts +21 -0
  168. package/src/shared/lib/theme/index.ts +6 -4
  169. package/src/vite-env.d.ts +2 -0
  170. package/dist/assets/channels-list-page-DcsIQc6A.js +0 -8
  171. package/dist/assets/chat-page-CZ7WxPir.js +0 -1
  172. package/dist/assets/config-split-page-BWQ6_Raq.js +0 -1
  173. package/dist/assets/doc-browser-B_Tf9n70.js +0 -1
  174. package/dist/assets/doc-browser-BdZPFOVa.js +0 -1
  175. package/dist/assets/doc-browser-context-DwokemUn.js +0 -1
  176. package/dist/assets/index-BIcEw90u.js +0 -104
  177. package/dist/assets/index-BiNSTKTU.css +0 -1
  178. package/dist/assets/loader-circle-MBgNSVIH.js +0 -1
  179. package/dist/assets/mcp-marketplace-page-CLmUstLN.js +0 -9
  180. package/dist/assets/mcp-marketplace-page-rZkmWPMy.js +0 -1
  181. package/dist/assets/model-config-page-Di0ySJD2.js +0 -1
  182. package/dist/assets/plus-vDqW5Kxp.js +0 -1
  183. package/dist/assets/providers-config-page-BAf2W0MV.js +0 -1
  184. package/dist/assets/remote-CLQ0IBM8.js +0 -1
  185. package/dist/assets/rotate-cw-CvNBv7_w.js +0 -1
  186. package/dist/assets/runtime-config-page-CflvvoDc.js +0 -1
  187. package/dist/assets/search-config-page-D0klFUXd.js +0 -1
  188. package/dist/assets/secrets-config-page-dVZ631Wp.js +0 -3
  189. package/dist/assets/security-config-C5gnlkuj.js +0 -1
  190. package/dist/assets/switch-CFLJFNSd.js +0 -1
  191. package/dist/assets/x-BF2KQ--V.js +0 -1
  192. /package/dist/assets/{config-hints-fGnUjDe9.js → config-hints-C_iP1zl_.js} +0 -0
@@ -16,7 +16,6 @@ import {
16
16
  SIDEBAR_RAIL_CONTROL_CLASS,
17
17
  SIDEBAR_RAIL_ICON_CLASS,
18
18
  SIDEBAR_RAIL_ITEM_GAP_CLASS,
19
- SIDEBAR_RAIL_PRIMARY_SURFACE_CLASS,
20
19
  SIDEBAR_RAIL_SURFACE_CLASS,
21
20
  } from "@/app/components/layout/sidebar-rail.styles";
22
21
 
@@ -150,7 +149,7 @@ export function ChatSidebarDesktopToolbar(props: ChatSidebarToolbarProps) {
150
149
  label={t("chatSidebarNewTask")}
151
150
  className={cn(
152
151
  SIDEBAR_RAIL_CONTROL_CLASS,
153
- SIDEBAR_RAIL_PRIMARY_SURFACE_CLASS,
152
+ SIDEBAR_RAIL_SURFACE_CLASS,
154
153
  )}
155
154
  onClick={() => {
156
155
  onCreateMenuOpenChange(false);
@@ -259,7 +258,7 @@ export function ChatSidebarDesktopToolbar(props: ChatSidebarToolbarProps) {
259
258
 
260
259
  <div className="px-4 pb-3">
261
260
  <div className="relative">
262
- <Search className="absolute left-3 top-2.5 h-3.5 w-3.5 text-muted-foreground/70" />
261
+ <Search className="pointer-events-none absolute left-3 top-2.5 h-3.5 w-3.5 text-muted-foreground/70" />
263
262
  <Input
264
263
  value={query}
265
264
  onChange={(event) => onQueryChange(event.target.value)}
@@ -294,7 +293,7 @@ export function ChatSidebarMobileToolbar(props: ChatSidebarToolbarProps) {
294
293
  <div className="px-4 pb-2 pt-1">
295
294
  <div className="flex items-center gap-2">
296
295
  <div className="relative min-w-0 flex-1">
297
- <Search className="absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-muted-foreground/70" />
296
+ <Search className="pointer-events-none absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-muted-foreground/70" />
298
297
  <Input
299
298
  value={query}
300
299
  onChange={(event) => onQueryChange(event.target.value)}
@@ -34,10 +34,7 @@ import {
34
34
  type LucideIcon,
35
35
  } from "lucide-react";
36
36
 
37
- type ChatSidebarUtilityOption<Value extends string> = {
38
- value: Value;
39
- label: string;
40
- };
37
+ type ChatSidebarUtilityOption<Value extends string> = { value: Value; label: string };
41
38
 
42
39
  type ChatSidebarUtilityMenuProps = {
43
40
  isOpen: boolean;
@@ -55,6 +52,12 @@ type ChatSidebarUtilityMenuProps = {
55
52
  collapsed?: boolean;
56
53
  };
57
54
 
55
+ function isChatComposerFocusTarget(target: EventTarget | null): boolean {
56
+ return target instanceof HTMLElement && Boolean(
57
+ target.closest('.nextclaw-chat-input-bar-shell [role="textbox"][contenteditable="true"]'),
58
+ );
59
+ }
60
+
58
61
  export function ChatSidebarUtilityMenu({
59
62
  isOpen,
60
63
  onOpenChange,
@@ -70,16 +73,6 @@ export function ChatSidebarUtilityMenu({
70
73
  onOpenApps,
71
74
  collapsed = false,
72
75
  }: ChatSidebarUtilityMenuProps) {
73
- const handleOpenDocs = () => {
74
- onOpenDocs();
75
- onOpenChange(false);
76
- };
77
-
78
- const handleOpenApps = () => {
79
- onOpenApps();
80
- onOpenChange(false);
81
- };
82
-
83
76
  return (
84
77
  <Popover open={isOpen} onOpenChange={onOpenChange}>
85
78
  {collapsed ? (
@@ -115,7 +108,7 @@ export function ChatSidebarUtilityMenu({
115
108
  <button
116
109
  type="button"
117
110
  aria-label={t("settingsMenu")}
118
- className="flex w-full items-center gap-2.5 rounded-xl px-3 py-2 text-[13px] font-medium text-muted-foreground transition-colors duration-base hover:bg-gray-200/60 hover:text-gray-900 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary/35"
111
+ className="flex w-full items-center gap-2.5 rounded-xl px-3 py-2 text-[13px] font-medium text-muted-foreground transition-colors duration-base hover:bg-gray-200/60 hover:text-gray-900 focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-border"
119
112
  >
120
113
  <Settings className="h-4 w-4 shrink-0 text-muted-foreground/70" />
121
114
  <span className="min-w-0 flex-1 text-left">{t("settings")}</span>
@@ -129,6 +122,11 @@ export function ChatSidebarUtilityMenu({
129
122
  side={collapsed ? "right" : "top"}
130
123
  align={collapsed ? "end" : "start"}
131
124
  className="w-64 p-2"
125
+ onFocusOutside={(event) => {
126
+ if (isChatComposerFocusTarget(event.detail.originalEvent.target)) {
127
+ event.preventDefault();
128
+ }
129
+ }}
132
130
  >
133
131
  <div className="space-y-1">
134
132
  <NavLink
@@ -139,22 +137,23 @@ export function ChatSidebarUtilityMenu({
139
137
  <Settings className="h-4 w-4 text-muted-foreground/70" />
140
138
  <span className="flex-1 text-left">{t("settings")}</span>
141
139
  </NavLink>
142
- <button
143
- type="button"
144
- onClick={handleOpenDocs}
145
- className="flex w-full items-center gap-2.5 rounded-lg px-3 py-2 text-[13px] font-medium text-foreground transition-colors hover:bg-gray-200/60 hover:text-gray-900"
146
- >
147
- <BookOpen className="h-4 w-4 text-muted-foreground/70" />
148
- <span className="flex-1 text-left">{t("docBrowserHelp")}</span>
149
- </button>
150
- <button
151
- type="button"
152
- onClick={handleOpenApps}
153
- className="flex w-full items-center gap-2.5 rounded-lg px-3 py-2 text-[13px] font-medium text-foreground transition-colors hover:bg-gray-200/60 hover:text-gray-900"
154
- >
155
- <Boxes className="h-4 w-4 text-muted-foreground/70" />
156
- <span className="flex-1 text-left">{t("appsTitle")}</span>
157
- </button>
140
+ {[
141
+ { key: "docs", icon: BookOpen, label: t("docBrowserHelp"), action: onOpenDocs },
142
+ { key: "apps", icon: Boxes, label: t("appsTitle"), action: onOpenApps },
143
+ ].map(({ key, icon: Icon, label, action }) => (
144
+ <button
145
+ key={key}
146
+ type="button"
147
+ onClick={() => {
148
+ action();
149
+ onOpenChange(false);
150
+ }}
151
+ className="flex w-full items-center gap-2.5 rounded-lg px-3 py-2 text-[13px] font-medium text-foreground transition-colors hover:bg-gray-200/60 hover:text-gray-900"
152
+ >
153
+ <Icon className="h-4 w-4 text-muted-foreground/70" />
154
+ <span className="flex-1 text-left">{label}</span>
155
+ </button>
156
+ ))}
158
157
  </div>
159
158
 
160
159
  <div className="my-2 h-px bg-border" />
@@ -105,7 +105,7 @@ export function ChatSidebar({
105
105
  const { language, setLanguage } = useI18n();
106
106
  const { theme, setTheme } = useTheme();
107
107
  const currentThemeLabel = t(
108
- THEME_OPTIONS.find((o) => o.value === theme)?.labelKey ?? "themeNatural",
108
+ THEME_OPTIONS.find((o) => o.value === theme)?.labelKey ?? "themeWork",
109
109
  );
110
110
  const currentLanguageLabel =
111
111
  LANGUAGE_OPTIONS.find((o) => o.value === language)?.label ?? language;
@@ -132,6 +132,14 @@ export function ChatSidebar({
132
132
  ),
133
133
  [agentsQuery.data?.agents],
134
134
  );
135
+ const pinnedSessionKeys = useMemo(
136
+ () => new Set(listSnapshot.pinnedSessionKeys),
137
+ [listSnapshot.pinnedSessionKeys],
138
+ );
139
+ const pinnedProjectRoots = useMemo(
140
+ () => new Set(listSnapshot.pinnedProjectRoots),
141
+ [listSnapshot.pinnedProjectRoots],
142
+ );
135
143
  const sortedItems = useMemo(
136
144
  () => sortSessionItemsByActivityAtDesc(items),
137
145
  [items],
@@ -140,10 +148,13 @@ export function ChatSidebar({
140
148
  () => groupChildSessionsByParentKey(items),
141
149
  [items],
142
150
  );
143
- const groups = useMemo(() => groupSessionsByDate(sortedItems), [sortedItems]);
151
+ const groups = useMemo(
152
+ () => groupSessionsByDate(sortedItems, pinnedSessionKeys),
153
+ [pinnedSessionKeys, sortedItems],
154
+ );
144
155
  const projectGroups = useMemo(
145
- () => groupSessionsByProject(sortedItems),
146
- [sortedItems],
156
+ () => groupSessionsByProject(sortedItems, pinnedSessionKeys, pinnedProjectRoots),
157
+ [pinnedProjectRoots, pinnedSessionKeys, sortedItems],
147
158
  );
148
159
  const sessionTypeOptions = useMemo(
149
160
  () => buildSessionTypeOptions(sessionTypesData?.options ?? []),
@@ -200,6 +211,7 @@ export function ChatSidebar({
200
211
  draftLabel={draftLabel}
201
212
  savingSessionKey={savingSessionKey}
202
213
  sessionTypeOptions={sessionTypeOptions}
214
+ isPinned={pinnedSessionKeys.has(item.session.key)}
203
215
  sessionTitle={getSessionTitle}
204
216
  onSelectSession={presenter.chatSessionListManager.selectSession}
205
217
  onOpenChildSessions={(parentSessionKey, activeChildSessionKey) =>
@@ -212,6 +224,9 @@ export function ChatSidebar({
212
224
  onDraftLabelChange={setDraftLabel}
213
225
  onSaveSessionLabel={saveSessionLabel}
214
226
  onCancelEditingSessionLabel={cancelEditingSessionLabel}
227
+ onTogglePinned={() =>
228
+ presenter.chatSessionListManager.toggleSessionPinned(item.session.key)
229
+ }
215
230
  />
216
231
  );
217
232
  const createSessionAndOpenIfNeeded = (
@@ -233,9 +248,9 @@ export function ChatSidebar({
233
248
  : shouldCollapse
234
249
  ? cn(
235
250
  SIDEBAR_RAIL_WIDTH_CLASS,
236
- "shrink-0 border-r border-border/70",
251
+ "shrink-0",
237
252
  )
238
- : "w-[280px] shrink-0 border-r border-border/70",
253
+ : "w-[280px] shrink-0",
239
254
  )}
240
255
  data-sidebar-collapsed={shouldCollapse ? "true" : "false"}
241
256
  >
@@ -295,7 +310,6 @@ export function ChatSidebar({
295
310
  isCollapsed={shouldCollapse}
296
311
  isLoading={isLoading}
297
312
  isProjectFirstView={isProjectFirstView}
298
- onCreateSession={createSessionAndOpenIfNeeded}
299
313
  onSelectMode={presenter.chatSessionListManager.setListMode}
300
314
  projectGroups={projectGroups}
301
315
  renderSessionItem={renderSessionItem}
@@ -417,7 +417,8 @@ export const SessionConversationInput = memo(function SessionConversationInput(p
417
417
  },
418
418
  })}
419
419
  toolbar={{
420
- selects: toolbarSelects,
420
+ selects: [],
421
+ trailingSelects: toolbarSelects,
421
422
  accessories: [
422
423
  {
423
424
  key: 'attach',
@@ -44,7 +44,7 @@ function QueuedInputIconButton({
44
44
  aria-label={label}
45
45
  disabled={disabled}
46
46
  onClick={onClick}
47
- className="inline-flex h-7 w-7 shrink-0 items-center justify-center rounded-sm text-muted-foreground transition-colors hover:bg-muted hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring disabled:cursor-not-allowed disabled:text-muted-foreground/50"
47
+ className="inline-flex h-7 w-7 shrink-0 items-center justify-center rounded-sm text-muted-foreground transition-colors hover:bg-muted hover:text-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-border disabled:cursor-not-allowed disabled:text-muted-foreground/50"
48
48
  >
49
49
  {children}
50
50
  </button>
@@ -32,8 +32,13 @@ vi.mock('@/app/presenters/app.presenter', () => ({
32
32
  }));
33
33
 
34
34
  vi.mock('@/features/panel-apps', () => ({
35
+ PANEL_APP_IFRAME_SANDBOX: 'allow-scripts allow-forms allow-modals allow-popups allow-popups-to-escape-sandbox allow-downloads allow-pointer-lock allow-presentation',
35
36
  findPanelAppEntryByDisplayId: (entries: Array<{ appId: string; id: string; title: string }>, value: string) =>
36
37
  entries.find((entry) => [entry.appId, entry.id, entry.title].includes(value)) ?? null,
38
+ focusPanelAppIframe: (iframe: HTMLIFrameElement | null) => {
39
+ iframe?.focus({ preventScroll: true });
40
+ iframe?.contentWindow?.focus();
41
+ },
37
42
  usePanelApps: () => mocks.panelApps,
38
43
  }));
39
44
 
@@ -68,7 +73,6 @@ it('renders inline panel apps as bounded card-mode iframes with an expand action
68
73
  expect(iframe.getAttribute('src')).toBe(
69
74
  '/api/panel-apps/weather-card/content?nextclawDisplayMode=card&nextclawPlacement=inline',
70
75
  );
71
- expect(iframe.getAttribute('scrolling')).toBe('auto');
72
76
 
73
77
  fireEvent.click(screen.getByLabelText('chatPanelCardExpand'));
74
78
 
@@ -80,6 +84,23 @@ it('renders inline panel apps as bounded card-mode iframes with an expand action
80
84
  }));
81
85
  });
82
86
 
87
+ it('focuses the inline panel app iframe when the pointer enters it', () => {
88
+ render(<ChatInlinePanelAppCard panelApp={{
89
+ appId: 'weather-card',
90
+ title: 'Weather',
91
+ }} />);
92
+
93
+ const iframe = screen.getByTitle('Weather') as HTMLIFrameElement;
94
+ const iframeFocus = vi.spyOn(iframe, 'focus');
95
+ const contentWindowFocus = vi.spyOn(iframe.contentWindow!, 'focus').mockImplementation(() => undefined);
96
+
97
+ fireEvent.pointerOver(iframe);
98
+
99
+ expect(iframe.getAttribute('tabindex')).toBe('0');
100
+ expect(iframeFocus).toHaveBeenCalledWith({ preventScroll: true });
101
+ expect(contentWindowFocus).toHaveBeenCalled();
102
+ });
103
+
83
104
  it('can render inline panel apps without a side-panel expand action', () => {
84
105
  render(
85
106
  <ChatInlinePanelAppCard
@@ -9,6 +9,8 @@ const captures = vi.hoisted(() => ({
9
9
  messages: unknown[];
10
10
  onToolAction?: (action: unknown) => void;
11
11
  onFileOpen?: (action: unknown) => void;
12
+ onAttachmentOpen?: (file: unknown) => void;
13
+ onInlineTokenClick?: (token: unknown) => void;
12
14
  renderInlineDisplay?: (display: unknown) => unknown;
13
15
  renderPanelAppCard?: (panelApp: unknown) => unknown;
14
16
  texts?: Record<string, unknown>;
@@ -16,6 +18,7 @@ const captures = vi.hoisted(() => ({
16
18
  language: "en",
17
19
  openFilePreview: vi.fn(),
18
20
  handleToolAction: vi.fn(),
21
+ showContent: vi.fn(),
19
22
  }));
20
23
 
21
24
  vi.mock("@nextclaw/agent-chat-ui", async (importOriginal) => {
@@ -26,6 +29,7 @@ vi.mock("@nextclaw/agent-chat-ui", async (importOriginal) => {
26
29
  messages: unknown[];
27
30
  onToolAction?: (action: unknown) => void;
28
31
  onFileOpen?: (action: unknown) => void;
32
+ onAttachmentOpen?: (file: unknown) => void;
29
33
  renderInlineDisplay?: (display: unknown) => unknown;
30
34
  renderPanelAppCard?: (panelApp: unknown) => unknown;
31
35
  texts?: Record<string, unknown>;
@@ -42,6 +46,9 @@ vi.mock("@/features/chat/components/providers/chat-presenter.provider", () => ({
42
46
  openFilePreview: captures.openFilePreview,
43
47
  handleToolAction: captures.handleToolAction,
44
48
  },
49
+ chatUiManager: {
50
+ showContent: captures.showContent,
51
+ },
45
52
  }),
46
53
  }));
47
54
 
@@ -59,6 +66,7 @@ beforeEach(() => {
59
66
  captures.language = "en";
60
67
  captures.openFilePreview.mockReset();
61
68
  captures.handleToolAction.mockReset();
69
+ captures.showContent.mockReset();
62
70
  });
63
71
 
64
72
  it("reuses adapted message references when the source message object is unchanged", () => {
@@ -300,6 +308,41 @@ it("wires markdown file link actions to the workspace file preview manager", ()
300
308
  expect(captures.openFilePreview).toHaveBeenCalledWith(action);
301
309
  });
302
310
 
311
+ it("opens message attachments in the workspace file preview", () => {
312
+ const message = {
313
+ id: "assistant-attachment",
314
+ sessionId: "session-1",
315
+ role: "assistant",
316
+ status: "final",
317
+ timestamp: "2026-03-31T10:05:00.000Z",
318
+ parts: [
319
+ {
320
+ type: "file",
321
+ name: "spec.pdf",
322
+ mimeType: "application/pdf",
323
+ contentBase64: "cGRm",
324
+ },
325
+ ],
326
+ } satisfies NcpMessage;
327
+
328
+ render(<ChatMessageListContainer messages={[message]} isSending={false} />);
329
+ captures.renders[captures.renders.length - 1]?.onAttachmentOpen?.({
330
+ label: "spec.pdf",
331
+ mimeType: "application/pdf",
332
+ dataUrl: "data:application/pdf;base64,cGRm",
333
+ isImage: false,
334
+ });
335
+
336
+ expect(captures.openFilePreview).toHaveBeenCalledWith({
337
+ path: "spec.pdf",
338
+ label: "spec.pdf",
339
+ viewMode: "preview",
340
+ contentUrl: "data:application/pdf;base64,cGRm",
341
+ mimeType: "application/pdf",
342
+ });
343
+ expect(captures.showContent).not.toHaveBeenCalled();
344
+ });
345
+
303
346
  it("renders context inheritance as a divider without repeating inherited messages", () => {
304
347
  const inheritedMessage = {
305
348
  id: "child-session:inherited:1",
@@ -387,7 +430,7 @@ it("keeps Hermes tool invocation parts as tool cards instead of flattening them
387
430
  });
388
431
  });
389
432
 
390
- it("passes localized attachment card texts to the shared chat UI", () => {
433
+ it("passes localized message presentation texts to the shared chat UI", () => {
391
434
  captures.language = "zh";
392
435
 
393
436
  render(<ChatMessageListContainer messages={[]} isSending={false} />);
@@ -400,6 +443,24 @@ it("passes localized attachment card texts to the shared chat UI", () => {
400
443
  pdf: "chatAttachmentCategoryPdf",
401
444
  generic: "chatAttachmentCategoryGeneric",
402
445
  },
446
+ reasoningCharacterCountTemplates: {
447
+ inProgress: "chatReasoningInProgressCharacterCount",
448
+ completed: "chatReasoningCompletedCharacterCount",
449
+ },
450
+ toolStatusLabels: {
451
+ terminal: {
452
+ running: "chatToolTerminalRunning",
453
+ success: "chatToolTerminalSuccess",
454
+ error: "chatToolTerminalError",
455
+ cancelled: "chatToolTerminalCancelled",
456
+ },
457
+ fileEdit: {
458
+ running: "chatToolFileEditRunning",
459
+ success: "chatToolFileEditSuccess",
460
+ error: "chatToolFileEditError",
461
+ cancelled: "chatToolFileEditCancelled",
462
+ },
463
+ },
403
464
  });
404
465
  });
405
466
 
@@ -2,13 +2,17 @@ import { useEffect, useId, useMemo, useRef } from 'react';
2
2
  import type { ChatPanelAppCardViewModel } from '@nextclaw/agent-chat-ui';
3
3
  import { AppWindow, Maximize2 } from 'lucide-react';
4
4
  import { getPresenter } from '@/app/presenters/app.presenter';
5
- import { findPanelAppEntryByDisplayId, usePanelApps } from '@/features/panel-apps';
5
+ import {
6
+ PANEL_APP_IFRAME_SANDBOX,
7
+ findPanelAppEntryByDisplayId,
8
+ focusPanelAppIframe,
9
+ usePanelApps,
10
+ } from '@/features/panel-apps';
6
11
  import { createPanelAppRightPanelResourceTarget } from '@/features/right-panel-resources';
7
12
  import {
8
13
  createInlinePanelAppCardUrl,
9
14
  createFallbackPanelAppContentPath,
10
15
  createInlinePanelAppTab,
11
- PANEL_APP_INLINE_IFRAME_SANDBOX,
12
16
  } from '@/features/chat/features/message/utils/chat-inline-panel-app-card.utils';
13
17
  import { useDocBrowser } from '@/shared/components/doc-browser';
14
18
  import { IconActionButton } from '@/shared/components/ui/actions/icon-action-button';
@@ -98,7 +102,7 @@ export function ChatInlinePanelAppCard({
98
102
  </div>
99
103
  {showExpandAction ? (
100
104
  <IconActionButton
101
- className="h-7 w-7 rounded-md text-muted-foreground hover:bg-accent hover:text-accent-foreground"
105
+ size="md"
102
106
  icon={<Maximize2 className="h-3.5 w-3.5" />}
103
107
  label={t('chatPanelCardExpand')}
104
108
  onClick={openExpanded}
@@ -113,8 +117,9 @@ export function ChatInlinePanelAppCard({
113
117
  key={iframeInstanceId}
114
118
  src={cardUrl}
115
119
  title={title}
116
- sandbox={PANEL_APP_INLINE_IFRAME_SANDBOX}
117
- scrolling="auto"
120
+ sandbox={PANEL_APP_IFRAME_SANDBOX}
121
+ tabIndex={0}
122
+ onPointerOver={() => focusPanelAppIframe(iframeRef.current)}
118
123
  className="h-full w-full border-0 bg-card"
119
124
  />
120
125
  </div>
@@ -1,7 +1,8 @@
1
- import { useMemo } from "react";
1
+ import { useCallback, useMemo } from "react";
2
2
  import type { NcpMessage } from "@nextclaw/ncp";
3
3
  import {
4
4
  type ChatInlineDisplayViewModel,
5
+ type ChatInlineTokenViewModel,
5
6
  type ChatMessageViewModel,
6
7
  ChatMessageList,
7
8
  } from "@nextclaw/agent-chat-ui";
@@ -23,6 +24,7 @@ import {
23
24
  } from "@/features/chat/features/session/utils/ncp-session-context-metadata.utils";
24
25
  import { AgentIdentityAvatar } from "@/shared/components/common/agent-identity";
25
26
  import { ChatInlinePanelAppCard } from "@/features/chat/features/message/components/chat-inline-panel-app-card";
27
+ import { useChatQueryStore } from "@/features/chat/stores/ncp-chat-query.store";
26
28
  import { useI18n } from "@/app/components/i18n-provider";
27
29
  import { formatDateTime, t } from "@/shared/lib/i18n";
28
30
 
@@ -342,6 +344,48 @@ export function ChatMessageListContainer({
342
344
  () => buildTimelineItems({ rawMessages, messages }),
343
345
  [messages, rawMessages],
344
346
  );
347
+ const sessionSkillsQuery = useChatQueryStore((state) => state.snapshot.sessionSkillsQuery);
348
+ const handleInlineTokenClick = useCallback(
349
+ (token: ChatInlineTokenViewModel) => {
350
+ if (token.kind !== "skill") {
351
+ return;
352
+ }
353
+ const skillKey = token.key.trim();
354
+ if (!skillKey) {
355
+ return;
356
+ }
357
+ const records = sessionSkillsQuery?.data?.records ?? [];
358
+ const matched = records.find((record) => record.ref === skillKey || record.name === skillKey);
359
+ const skillPath = matched?.path?.trim();
360
+ if (!skillPath) {
361
+ return;
362
+ }
363
+ presenter.chatThreadManager.openFilePreview({
364
+ path: skillPath,
365
+ label: token.label || matched?.name || skillKey,
366
+ viewMode: "preview",
367
+ previewViewer: "rendered",
368
+ });
369
+ },
370
+ [presenter.chatThreadManager, sessionSkillsQuery],
371
+ );
372
+ const handleAttachmentOpen = useCallback(
373
+ (file: { label: string; mimeType: string; dataUrl?: string; sizeBytes?: number; isImage: boolean }) => {
374
+ const contentUrl = file.dataUrl?.trim();
375
+ if (!contentUrl) {
376
+ return;
377
+ }
378
+ const label = file.label.trim() || "attachment";
379
+ presenter.chatThreadManager.openFilePreview({
380
+ path: label,
381
+ label,
382
+ viewMode: "preview",
383
+ contentUrl,
384
+ mimeType: file.mimeType,
385
+ });
386
+ },
387
+ [presenter.chatThreadManager],
388
+ );
345
389
  return (
346
390
  <div className={className}>
347
391
  {timelineItems.map((item, index) =>
@@ -358,6 +402,8 @@ export function ChatMessageListContainer({
358
402
  texts={messageTexts}
359
403
  onToolAction={presenter.chatThreadManager.handleToolAction}
360
404
  onFileOpen={presenter.chatThreadManager.openFilePreview}
405
+ onAttachmentOpen={handleAttachmentOpen}
406
+ onInlineTokenClick={handleInlineTokenClick}
361
407
  renderInlineDisplay={renderChatInlineDisplay}
362
408
  renderToolAgent={(agentId) => (
363
409
  <AgentIdentityAvatar
@@ -18,7 +18,7 @@ const baseAssistantMessage = {
18
18
  toolCallId: "tool-1",
19
19
  toolName: "exec_command",
20
20
  state: "result",
21
- args: "{\"cmd\":\"git status\"}",
21
+ args: '{"cmd":"git status"}',
22
22
  result: "clean",
23
23
  },
24
24
  {
@@ -56,4 +56,14 @@ describe("buildChatMessageProcessSummary", () => {
56
56
  label: "Processed 3m 51s",
57
57
  });
58
58
  });
59
+
60
+ it("stays free of tool-activity semantics", () => {
61
+ const summary = buildChatMessageProcessSummary({
62
+ message: baseAssistantMessage,
63
+ processedLabel: "Processed",
64
+ });
65
+ expect(summary?.label).toBe("Processed");
66
+ expect(summary?.label.toLowerCase()).not.toContain("bash");
67
+ expect(summary?.label.toLowerCase()).not.toContain("read");
68
+ });
59
69
  });
@@ -0,0 +1,23 @@
1
+ import { expect, it, vi } from "vitest";
2
+ import { buildChatMessageTexts } from "@/features/chat/features/message/utils/chat-message-texts.utils";
3
+
4
+ vi.mock("@/shared/lib/i18n", () => ({
5
+ t: (key: string) => key,
6
+ }));
7
+
8
+ it("provides localized status keys for built-in tool categories", () => {
9
+ expect(buildChatMessageTexts("zh").toolStatusLabels?.builtIn).toMatchObject({
10
+ directory: {
11
+ running: "chatToolDirectoryRunning",
12
+ success: "chatToolDirectorySuccess",
13
+ error: "chatToolDirectoryError",
14
+ cancelled: "chatToolDirectoryCancelled",
15
+ },
16
+ memory: {
17
+ running: "chatToolMemoryRunning",
18
+ success: "chatToolMemorySuccess",
19
+ error: "chatToolMemoryError",
20
+ cancelled: "chatToolMemoryCancelled",
21
+ },
22
+ });
23
+ });
@@ -4,17 +4,6 @@ import {
4
4
  } from '@/features/right-panel-resources';
5
5
  import type { DocBrowserTab } from '@/shared/components/doc-browser/doc-browser-context';
6
6
 
7
- export const PANEL_APP_INLINE_IFRAME_SANDBOX = [
8
- 'allow-scripts',
9
- 'allow-forms',
10
- 'allow-modals',
11
- 'allow-popups',
12
- 'allow-popups-to-escape-sandbox',
13
- 'allow-downloads',
14
- 'allow-pointer-lock',
15
- 'allow-presentation',
16
- ].join(' ');
17
-
18
7
  const PANEL_APP_INLINE_CARD_SEARCH_PARAMS = {
19
8
  nextclawDisplayMode: 'card',
20
9
  nextclawPlacement: 'inline',
@@ -68,6 +68,11 @@ function formatLifecycleDuration(message: NcpMessage): string | null {
68
68
  return `${seconds}s`;
69
69
  }
70
70
 
71
+ /**
72
+ * Outer process collapse only.
73
+ * Tool activity grouping is a separate layer over consecutive tool-cards
74
+ * and must not be mixed into this summary.
75
+ */
71
76
  export function buildChatMessageProcessSummary({
72
77
  message,
73
78
  processedLabel,