@nextclaw/ui 0.15.2 → 0.15.4

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 (212) hide show
  1. package/CHANGELOG.md +41 -0
  2. package/dist/assets/aiden0z-pptx-renderer.es-Bd99vut2.js +94 -0
  3. package/dist/assets/api-BOXOrpcJ.js +13 -0
  4. package/dist/assets/app-presenter-provider-CkHLbvZQ.js +106 -0
  5. package/dist/assets/app-presenter-provider-D3BuuOyZ.css +1 -0
  6. package/dist/assets/appearance-settings-page-fLIJbx1C.js +1 -0
  7. package/dist/assets/{book-open-Ba2f7YiG.js → book-open-DWUjS6aW.js} +1 -1
  8. package/dist/assets/channels-list-page-CvCTJraa.js +8 -0
  9. package/dist/assets/chat-page-B42UUIFR.js +1 -0
  10. package/dist/assets/chunk-DseTPa7n.js +1 -0
  11. package/dist/assets/config-split-page-DBgt9S8b.js +1 -0
  12. package/dist/assets/confirm-dialog-GSXIvoRU.js +5 -0
  13. package/dist/assets/desktop-update-config-7bmNeWIC.js +1 -0
  14. package/dist/assets/dist-DC7f4TJH.js +41 -0
  15. package/dist/assets/doc-browser-CgdoLUT92.js +1 -0
  16. package/dist/assets/doc-browser-D0monHjG.js +1 -0
  17. package/dist/assets/doc-browser-context-CGwEOhBy.js +1 -0
  18. package/dist/assets/docx-preview-eFH9DRsS.js +20 -0
  19. package/dist/assets/download-DiP9zx-J.js +1 -0
  20. package/dist/assets/{external-link-DxuPaDoK.js → external-link-C6ZeSPef.js} +1 -1
  21. package/dist/assets/eye-suvKmtwn.js +1 -0
  22. package/dist/assets/folder-CMELbQGG.js +1 -0
  23. package/dist/assets/{form-actions-BztL7P_T.js → form-actions-CUAzpNe_.js} +1 -1
  24. package/dist/assets/index-BFeNDQ4R.css +1 -0
  25. package/dist/assets/index-D_6U-M94.js +2 -0
  26. package/dist/assets/jszip.min-Das2IAnb.js +2 -0
  27. package/dist/assets/loader-circle-tYHStefV.js +1 -0
  28. package/dist/assets/logo-badge-Cu5UGH_4.js +1 -0
  29. package/dist/assets/mcp-marketplace-page-BUzHDwW6.js +9 -0
  30. package/dist/assets/mcp-marketplace-page-ohUAdfqJ.js +1 -0
  31. package/dist/assets/model-config-page-CWP6pyBm.js +1 -0
  32. package/dist/assets/plus-tywizkeY.js +1 -0
  33. package/dist/assets/popover-DImguRok.js +1 -0
  34. package/dist/assets/provider-models-C_yOh6DE.js +1 -0
  35. package/dist/assets/provider-scoped-model-input-CE7p4SbJ.js +1 -0
  36. package/dist/assets/providers-config-page-BKPo8SjH.js +1 -0
  37. package/dist/assets/react-5sjAT_6d.js +8 -0
  38. package/dist/assets/{refresh-cw-eT8BF-8B.js → refresh-cw-CHLBzWWj.js} +1 -1
  39. package/dist/assets/remote-DXJryUxB.js +1 -0
  40. package/dist/assets/rotate-cw-BefO5brU.js +1 -0
  41. package/dist/assets/runtime-config-page-1Axv4QGu.js +1 -0
  42. package/dist/assets/{save-Cb2g0BOi.js → save-BABzyrjB.js} +1 -1
  43. package/dist/assets/{search-BcHjwdsb.js → search-BTqxG3ZG.js} +1 -1
  44. package/dist/assets/search-config-page-BkKKmxmS.js +1 -0
  45. package/dist/assets/secrets-config-page-CWJMyu-k.js +3 -0
  46. package/dist/assets/security-config-DeQUIXfi.js +1 -0
  47. package/dist/assets/select-TYZDBNmq.js +1 -0
  48. package/dist/assets/setting-row-DR23Z_5I.js +1 -0
  49. package/dist/assets/{settings-2-2DWauOjY.js → settings-2-D8WWcBnw.js} +1 -1
  50. package/dist/assets/{skeleton-P5oM14E1.js → skeleton-CloHeg0S.js} +1 -1
  51. package/dist/assets/tabs-CX-ckWoo.js +1 -0
  52. package/dist/assets/tag-chip-mCc23Rhb.js +1 -0
  53. package/dist/assets/trash-2-SAW1Z4Po.js +1 -0
  54. package/dist/assets/use-config-BucdaK23.js +1 -0
  55. package/dist/assets/use-confirm-dialog-C95F6sT1.js +1 -0
  56. package/dist/assets/use-viewport-layout-C6m3Nte7.js +1 -0
  57. package/dist/assets/x-DtCPHwCu.js +1 -0
  58. package/dist/assets/xlsx-B6zVt1RK.js +64 -0
  59. package/dist/index.html +34 -28
  60. package/package.json +14 -9
  61. package/src/app/components/layout/sidebar-items.tsx +3 -3
  62. package/src/app/components/layout/sidebar-rail.styles.ts +2 -2
  63. package/src/app/components/layout/sidebar.tsx +1 -1
  64. package/src/app/styles/design-system.css +17 -4
  65. package/src/features/chat/components/__tests__/chat-popover-content.test.tsx +41 -0
  66. package/src/features/chat/components/chat-popover-content.tsx +26 -0
  67. package/src/features/chat/components/conversation/__tests__/chat-conversation-content.test.tsx +13 -0
  68. package/src/features/chat/components/conversation/__tests__/chat-conversation-header-section.test.tsx +21 -11
  69. package/src/features/chat/components/conversation/__tests__/chat-conversation-header.test.tsx +2 -2
  70. package/src/features/chat/components/conversation/__tests__/chat-conversation-workspace-section.test.tsx +28 -1
  71. package/src/features/chat/components/conversation/chat-conversation-content.tsx +5 -4
  72. package/src/features/chat/components/conversation/chat-conversation-header-section.tsx +9 -18
  73. package/src/features/chat/components/conversation/chat-conversation-track.tsx +35 -0
  74. package/src/features/chat/components/conversation/chat-conversation-workspace-section.tsx +1 -0
  75. package/src/features/chat/components/layout/chat-sidebar-toolbar.tsx +7 -7
  76. package/src/features/chat/components/layout/chat-sidebar-utility-menu.tsx +4 -15
  77. package/src/features/chat/features/conversation/components/__tests__/session-conversation-input.streaming.test.tsx +20 -1
  78. package/src/features/chat/features/conversation/components/session-conversation-input.tsx +69 -54
  79. package/src/features/chat/features/conversation/components/session-queued-input-rows.tsx +1 -1
  80. package/src/features/chat/features/message/components/__tests__/chat-message-list.container.test.tsx +62 -1
  81. package/src/features/chat/features/message/components/chat-message-list.container.tsx +63 -2
  82. package/src/features/chat/features/message/utils/__tests__/chat-message-file-operation.utils.test.ts +30 -0
  83. package/src/features/chat/features/message/utils/__tests__/chat-message-process-summary.utils.test.ts +11 -1
  84. package/src/features/chat/features/message/utils/__tests__/chat-message-texts.utils.test.ts +23 -0
  85. package/src/features/chat/features/message/utils/chat-message-process-summary.utils.ts +5 -0
  86. package/src/features/chat/features/message/utils/chat-message-texts.utils.ts +138 -0
  87. package/src/features/chat/features/session/components/__tests__/chat-sidebar-session-item.test.tsx +37 -0
  88. package/src/features/chat/features/session/components/chat-sidebar-project-groups.tsx +5 -4
  89. package/src/features/chat/features/session/components/chat-sidebar-session-item.tsx +3 -8
  90. package/src/features/chat/features/session/components/session-header/__tests__/chat-session-header-actions.test.tsx +65 -20
  91. package/src/features/chat/features/session/components/session-header/chat-session-header-actions.tsx +26 -28
  92. package/src/features/chat/features/session/components/session-header/chat-session-project-badge.tsx +3 -3
  93. package/src/features/chat/features/session/components/session-header/chat-session-title-switcher.tsx +5 -5
  94. package/src/features/chat/features/welcome/components/chat-welcome-project-picker.tsx +4 -4
  95. package/src/features/chat/features/welcome/components/chat-welcome-session-type-picker.tsx +4 -4
  96. package/src/features/chat/features/workspace/components/__tests__/chat-session-workspace-file-preview.test.tsx +463 -99
  97. package/src/features/chat/features/workspace/components/__tests__/chat-session-workspace-panel-content.test.tsx +113 -2
  98. package/src/features/chat/features/workspace/components/__tests__/chat-session-workspace-panel.test.tsx +1 -0
  99. package/src/features/chat/features/workspace/components/chat-session-workspace-directory-browser.tsx +197 -44
  100. package/src/features/chat/features/workspace/components/chat-session-workspace-file-breadcrumb-browser.tsx +11 -5
  101. package/src/features/chat/features/workspace/components/chat-session-workspace-file-breadcrumbs.tsx +8 -6
  102. package/src/features/chat/features/workspace/components/chat-session-workspace-file-preview.tsx +99 -67
  103. package/src/features/chat/features/workspace/components/chat-session-workspace-panel-content.tsx +239 -1
  104. package/src/features/chat/features/workspace/components/chat-session-workspace-panel-nav.tsx +56 -6
  105. package/src/features/chat/features/workspace/components/chat-session-workspace-panel.tsx +29 -7
  106. package/src/features/chat/features/workspace/components/file-content-preview/use-workspace-file-buffer.ts +42 -0
  107. package/src/features/chat/features/workspace/components/file-content-preview/workspace-document-preview-state.tsx +18 -0
  108. package/src/features/chat/features/workspace/components/file-content-preview/workspace-docx-preview.css +89 -0
  109. package/src/features/chat/features/workspace/components/file-content-preview/workspace-docx-preview.tsx +87 -0
  110. package/src/features/chat/features/workspace/components/file-content-preview/workspace-presentation-preview.tsx +82 -0
  111. package/src/features/chat/features/workspace/components/file-content-preview/workspace-spreadsheet-preview.tsx +180 -0
  112. package/src/features/chat/features/workspace/components/workspace-file-content-preview.tsx +191 -0
  113. package/src/features/chat/features/workspace/hooks/use-chat-conversation-workspace-state.ts +1 -4
  114. package/src/features/chat/features/workspace/utils/__tests__/chat-workspace-panel-view-model.utils.test.ts +53 -10
  115. package/src/features/chat/features/workspace/utils/chat-thread-workspace-session.utils.ts +68 -2
  116. package/src/features/chat/features/workspace/utils/chat-workspace-file-tab-persistence.utils.ts +21 -0
  117. package/src/features/chat/features/workspace/utils/chat-workspace-file-tab.utils.ts +63 -0
  118. package/src/features/chat/features/workspace/utils/chat-workspace-file-viewer.utils.ts +49 -0
  119. package/src/features/chat/features/workspace/utils/chat-workspace-panel-layout.utils.ts +13 -0
  120. package/src/features/chat/features/workspace/utils/chat-workspace-panel-view-model.utils.ts +120 -38
  121. package/src/features/chat/index.ts +1 -0
  122. package/src/features/chat/managers/__tests__/chat-thread-workspace.manager.test.ts +76 -0
  123. package/src/features/chat/managers/__tests__/chat-thread.manager.test.ts +44 -60
  124. package/src/features/chat/managers/chat-thread.manager.ts +123 -124
  125. package/src/features/chat/pages/ncp-chat-page.tsx +1 -2
  126. package/src/features/chat/stores/__tests__/chat-thread.store.test.ts +78 -1
  127. package/src/features/chat/stores/chat-message-layout.store.test.ts +37 -0
  128. package/src/features/chat/stores/chat-message-layout.store.ts +41 -0
  129. package/src/features/chat/stores/chat-thread.store.ts +73 -13
  130. package/src/features/cron/components/cron-config.tsx +63 -80
  131. package/src/features/marketplace/components/detail-doc/__tests__/marketplace-detail-doc.test.tsx +44 -6
  132. package/src/features/marketplace/components/marketplace-detail-doc.tsx +8 -4
  133. package/src/features/marketplace/components/marketplace-page-parts.tsx +1 -1
  134. package/src/features/marketplace/hooks/use-marketplace-detail-doc-entry.ts +84 -0
  135. package/src/features/panel-apps/components/panel-apps-list.tsx +2 -2
  136. package/src/features/right-panel-resources/components/right-panel-resource-home-page.tsx +1 -1
  137. package/src/features/settings/pages/__tests__/appearance-settings-page.test.tsx +25 -8
  138. package/src/features/settings/pages/appearance-settings-page.tsx +92 -13
  139. package/src/features/side-dock/components/side-dock.tsx +3 -3
  140. package/src/index.css +275 -15
  141. package/src/shared/components/__tests__/file-type-icon.test.tsx +61 -0
  142. package/src/shared/components/actions/action-link.tsx +1 -1
  143. package/src/shared/components/doc-browser/__tests__/doc-browser-context.test.tsx +19 -0
  144. package/src/shared/components/doc-browser/__tests__/doc-browser.test.tsx +16 -0
  145. package/src/shared/components/doc-browser/doc-browser-context.tsx +1 -0
  146. package/src/shared/components/doc-browser/doc-browser-panel-parts.tsx +1 -1
  147. package/src/shared/components/doc-browser/doc-browser.tsx +11 -3
  148. package/src/shared/components/doc-browser/managers/doc-browser.manager.ts +5 -0
  149. package/src/shared/components/doc-browser/stores/doc-browser.store.ts +6 -1
  150. package/src/shared/components/doc-browser/types/doc-browser.types.ts +2 -0
  151. package/src/shared/components/doc-browser/utils/doc-browser-state.utils.ts +11 -0
  152. package/src/shared/components/file-type-icon.tsx +272 -0
  153. package/src/shared/components/resizable-right-panel/__tests__/resizable-right-panel.test.tsx +22 -0
  154. package/src/shared/components/resizable-right-panel/resizable-right-panel.tsx +17 -2
  155. package/src/shared/components/ui/actions/icon-action-button.tsx +1 -1
  156. package/src/shared/components/ui/button.tsx +1 -1
  157. package/src/shared/components/ui/dialog.tsx +1 -1
  158. package/src/shared/components/ui/input.tsx +1 -1
  159. package/src/shared/components/ui/select.tsx +1 -1
  160. package/src/shared/components/ui/switch.tsx +1 -1
  161. package/src/shared/components/ui/tab-strip/__tests__/compact-tab-strip.test.tsx +38 -0
  162. package/src/shared/components/ui/tab-strip/compact-tab-strip.tsx +82 -2
  163. package/src/shared/components/ui/tabs.tsx +1 -1
  164. package/src/shared/components/ui/textarea.tsx +1 -1
  165. package/src/shared/lib/api/__tests__/server-path.utils.test.ts +28 -0
  166. package/src/shared/lib/api/utils/server-path.utils.ts +19 -20
  167. package/src/shared/lib/i18n/locales/en-US/chat.json +99 -0
  168. package/src/shared/lib/i18n/locales/en-US/core.json +6 -0
  169. package/src/shared/lib/i18n/locales/zh-CN/chat.json +99 -0
  170. package/src/shared/lib/i18n/locales/zh-CN/core.json +6 -0
  171. package/dist/assets/api-B7Qigvtm.js +0 -13
  172. package/dist/assets/appearance-settings-page-CuE0Iiqj.js +0 -1
  173. package/dist/assets/channels-list-page-CTsM6TcP.js +0 -8
  174. package/dist/assets/chat-page-C_5dcuE3.js +0 -1
  175. package/dist/assets/config-split-page-Fbb4PtTf.js +0 -1
  176. package/dist/assets/confirm-dialog-D2EG3nqE.js +0 -5
  177. package/dist/assets/desktop-update-config-BGR3HUi1.js +0 -1
  178. package/dist/assets/dist-7JTjycxE.js +0 -1
  179. package/dist/assets/dist-wXLhk4hl.js +0 -1
  180. package/dist/assets/doc-browser-bHyXnxlj.js +0 -1
  181. package/dist/assets/doc-browser-context-CYAGm5d5.js +0 -1
  182. package/dist/assets/doc-browser-gTrQ3jCF.js +0 -1
  183. package/dist/assets/es2015-j1SRCIX5.js +0 -41
  184. package/dist/assets/i18n-provider-B4oMxOFx.js +0 -1
  185. package/dist/assets/index-CvrvblgX.css +0 -1
  186. package/dist/assets/index-Obd4JYU2.js +0 -104
  187. package/dist/assets/loader-circle-CdzgK4eH.js +0 -1
  188. package/dist/assets/mcp-marketplace-page-Bt7CuOoq.js +0 -9
  189. package/dist/assets/mcp-marketplace-page-Ck3t78_n.js +0 -1
  190. package/dist/assets/model-config-page-DeKk9gvE.js +0 -1
  191. package/dist/assets/plus-Bx7wQbhG.js +0 -1
  192. package/dist/assets/provider-scoped-model-input-DsH2LF1q.js +0 -1
  193. package/dist/assets/providers-config-page-BFnGp68F.js +0 -1
  194. package/dist/assets/react-UirzUuST.js +0 -8
  195. package/dist/assets/remote-CDkHZtA8.js +0 -1
  196. package/dist/assets/rotate-cw-1JqxHTuw.js +0 -1
  197. package/dist/assets/runtime-config-page-BelAqiNG.js +0 -1
  198. package/dist/assets/search-config-page-zPm6--Ig.js +0 -1
  199. package/dist/assets/secrets-config-page-Cxyp-KuQ.js +0 -3
  200. package/dist/assets/security-config-B_gCq9fn.js +0 -1
  201. package/dist/assets/select-CdSEOwVM.js +0 -1
  202. package/dist/assets/setting-row-CTAtGexr.js +0 -1
  203. package/dist/assets/side-dock-DPvakJ0a.js +0 -3
  204. package/dist/assets/switch-BU_UEULb.js +0 -1
  205. package/dist/assets/tag-chip-D-WsKoT1.js +0 -1
  206. package/dist/assets/tooltip-CjkOaHW7.js +0 -1
  207. package/dist/assets/use-config--hx5oxQn.js +0 -1
  208. package/dist/assets/use-confirm-dialog-CD9a_SK1.js +0 -1
  209. package/dist/assets/x-DT5x45JU.js +0 -1
  210. /package/dist/assets/{config-hints-C_iP1zl_.js → config-hints-fDrYfl0l.js} +0 -0
  211. /package/dist/assets/{host-capabilities-9bB_998Z.js → host-capabilities-zZvY9U63.js} +0 -0
  212. /package/dist/assets/{middleware-BILWbJE4.js → middleware-BLMHv9YF.js} +0 -0
@@ -3,9 +3,9 @@ import { IconActionButton } from "@/shared/components/ui/actions/icon-action-but
3
3
  import { Input } from "@/shared/components/ui/input";
4
4
  import {
5
5
  Popover,
6
- PopoverContent,
7
6
  PopoverTrigger,
8
7
  } from "@/shared/components/ui/popover";
8
+ import { ChatPopoverContent } from "@/features/chat/components/chat-popover-content";
9
9
  import { SessionContextIconNode } from "@/features/chat/features/session/components/session-context-icon";
10
10
  import { ChatSessionTypeMenu } from "@/features/chat/features/session-type/components/chat-session-type-menu";
11
11
  import type { ChatSessionTypeOption } from "@/features/chat/features/session-type/utils/chat-session-type.utils";
@@ -175,7 +175,7 @@ export function ChatSidebarDesktopToolbar(props: ChatSidebarToolbarProps) {
175
175
  )}
176
176
  />
177
177
  </PopoverTrigger>
178
- <PopoverContent
178
+ <ChatPopoverContent
179
179
  align="start"
180
180
  side="right"
181
181
  className="w-56 rounded-2xl border border-border bg-popover p-1.5 text-popover-foreground shadow-[0_24px_60px_-28px_rgba(15,23,42,0.38)]"
@@ -188,7 +188,7 @@ export function ChatSidebarDesktopToolbar(props: ChatSidebarToolbarProps) {
188
188
  onCreateMenuOpenChange(false);
189
189
  }}
190
190
  />
191
- </PopoverContent>
191
+ </ChatPopoverContent>
192
192
  </Popover>
193
193
  ) : null}
194
194
  </div>
@@ -238,7 +238,7 @@ export function ChatSidebarDesktopToolbar(props: ChatSidebarToolbarProps) {
238
238
  )}
239
239
  />
240
240
  </PopoverTrigger>
241
- <PopoverContent
241
+ <ChatPopoverContent
242
242
  align="end"
243
243
  className="w-56 rounded-2xl border border-border bg-popover p-1.5 text-popover-foreground shadow-[0_24px_60px_-28px_rgba(15,23,42,0.38)]"
244
244
  >
@@ -250,7 +250,7 @@ export function ChatSidebarDesktopToolbar(props: ChatSidebarToolbarProps) {
250
250
  onCreateMenuOpenChange(false);
251
251
  }}
252
252
  />
253
- </PopoverContent>
253
+ </ChatPopoverContent>
254
254
  </Popover>
255
255
  ) : null}
256
256
  </div>
@@ -315,7 +315,7 @@ export function ChatSidebarMobileToolbar(props: ChatSidebarToolbarProps) {
315
315
  className="h-9 w-9 shrink-0 rounded-full bg-muted text-foreground shadow-none hover:bg-accent hover:text-accent-foreground"
316
316
  />
317
317
  </PopoverTrigger>
318
- <PopoverContent
318
+ <ChatPopoverContent
319
319
  align="end"
320
320
  className="w-60 rounded-3xl border border-border bg-popover p-2 text-popover-foreground shadow-[0_24px_70px_-30px_rgba(15,23,42,0.45)]"
321
321
  >
@@ -329,7 +329,7 @@ export function ChatSidebarMobileToolbar(props: ChatSidebarToolbarProps) {
329
329
  onCreateMenuOpenChange(false);
330
330
  }}
331
331
  />
332
- </PopoverContent>
332
+ </ChatPopoverContent>
333
333
  </Popover>
334
334
  ) : (
335
335
  <IconActionButton
@@ -1,9 +1,9 @@
1
1
  import { NavLink } from "react-router-dom";
2
2
  import {
3
3
  Popover,
4
- PopoverContent,
5
4
  PopoverTrigger,
6
5
  } from "@/shared/components/ui/popover";
6
+ import { ChatPopoverContent } from "@/features/chat/components/chat-popover-content";
7
7
  import {
8
8
  Select,
9
9
  SelectContent,
@@ -52,12 +52,6 @@ type ChatSidebarUtilityMenuProps = {
52
52
  collapsed?: boolean;
53
53
  };
54
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
-
61
55
  export function ChatSidebarUtilityMenu({
62
56
  isOpen,
63
57
  onOpenChange,
@@ -108,7 +102,7 @@ export function ChatSidebarUtilityMenu({
108
102
  <button
109
103
  type="button"
110
104
  aria-label={t("settingsMenu")}
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-2 focus-visible:ring-primary/35"
105
+ 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"
112
106
  >
113
107
  <Settings className="h-4 w-4 shrink-0 text-muted-foreground/70" />
114
108
  <span className="min-w-0 flex-1 text-left">{t("settings")}</span>
@@ -118,15 +112,10 @@ export function ChatSidebarUtilityMenu({
118
112
  </button>
119
113
  </PopoverTrigger>
120
114
  )}
121
- <PopoverContent
115
+ <ChatPopoverContent
122
116
  side={collapsed ? "right" : "top"}
123
117
  align={collapsed ? "end" : "start"}
124
118
  className="w-64 p-2"
125
- onFocusOutside={(event) => {
126
- if (isChatComposerFocusTarget(event.detail.originalEvent.target)) {
127
- event.preventDefault();
128
- }
129
- }}
130
119
  >
131
120
  <div className="space-y-1">
132
121
  <NavLink
@@ -177,7 +166,7 @@ export function ChatSidebarUtilityMenu({
177
166
  onSelect={onSelectLanguage}
178
167
  onCloseMenu={() => onOpenChange(false)}
179
168
  />
180
- </PopoverContent>
169
+ </ChatPopoverContent>
181
170
  </Popover>
182
171
  );
183
172
  }
@@ -6,7 +6,7 @@ import {
6
6
  type MutableRefObject,
7
7
  } from 'react';
8
8
  import { act, fireEvent, render, screen, waitFor } from '@testing-library/react';
9
- import { describe, expect, it, vi } from 'vitest';
9
+ import { afterEach, describe, expect, it, vi } from 'vitest';
10
10
  import {
11
11
  createChatComposerTextNode,
12
12
  type ChatComposerNode,
@@ -26,9 +26,14 @@ import type {
26
26
  SessionConversationInputSnapshot,
27
27
  } from '@/features/chat/features/conversation/hooks/use-session-conversation-input-state';
28
28
  import { useSessionConversationInputState } from '@/features/chat/features/conversation/hooks/use-session-conversation-input-state';
29
+ import { useChatMessageLayoutStore } from '@/features/chat/stores/chat-message-layout.store';
29
30
 
30
31
  const uploadNcpAssetsMock = vi.hoisted(() => vi.fn());
31
32
 
33
+ afterEach(() => {
34
+ useChatMessageLayoutStore.getState().setLayout('card');
35
+ });
36
+
32
37
  vi.mock('@/app/hooks/use-viewport-layout', () => ({
33
38
  useViewportLayout: () => ({ isDesktop: true, isMobile: false }),
34
39
  }));
@@ -190,6 +195,20 @@ function StreamingSessionConversationInputHarness({
190
195
  }
191
196
 
192
197
  describe('SessionConversationInput streaming stability', () => {
198
+ it('aligns the default input with the flat message reading track', () => {
199
+ useChatMessageLayoutStore.getState().setLayout('flat');
200
+ const controlRef: MutableRefObject<StreamingInputControl | null> = { current: null };
201
+
202
+ render(<StreamingSessionConversationInputHarness controlRef={controlRef} />);
203
+
204
+ const track = document.querySelector(
205
+ '[data-chat-conversation-track="flat"][data-chat-conversation-track-width="composer"]',
206
+ );
207
+ expect(track?.className).toContain('max-w-[min(54rem,100%)]');
208
+ expect(track?.querySelector('.nextclaw-chat-input-bar-shell')).toBeTruthy();
209
+ expect(track?.firstElementChild?.className).toContain('px-0');
210
+ });
211
+
193
212
  it('keeps IME composition stable while simulated streamed output rerenders the owner', async () => {
194
213
  const controlRef: MutableRefObject<StreamingInputControl | null> = { current: null };
195
214
  render(<StreamingSessionConversationInputHarness controlRef={controlRef} />);
@@ -49,6 +49,8 @@ import {
49
49
  } from '@/features/chat/managers/chat-recent-skills.manager';
50
50
 
51
51
  import { useSessionConversationInputAttachments } from '@/features/chat/features/conversation/hooks/use-session-conversation-input-attachments';
52
+ import { ChatConversationTrack } from '@/features/chat/components/conversation/chat-conversation-track';
53
+ import { useChatMessageLayoutStore } from '@/features/chat/stores/chat-message-layout.store';
52
54
  import type { useSessionConversationInputQuery } from '@/features/chat/features/conversation/hooks/use-session-conversation-input-query';
53
55
  import type {
54
56
  SessionConversationQueuedInput,
@@ -187,6 +189,7 @@ export const SessionConversationInput = memo(function SessionConversationInput(p
187
189
  } = props;
188
190
  const presenter = usePresenter();
189
191
  const { language } = useI18n();
192
+ const messageLayout = useChatMessageLayoutStore((state) => state.layout);
190
193
  const { isMobile } = useViewportLayout();
191
194
  const inputBarRef = useRef<ChatInputBarHandle | null>(null);
192
195
  const fileInputRef = useRef<HTMLInputElement | null>(null);
@@ -389,62 +392,74 @@ export const SessionConversationInput = memo(function SessionConversationInput(p
389
392
  selectedSkills: inputSnapshot.selectedSkills,
390
393
  });
391
394
 
395
+ const useReadingTrack = surface === 'default' && messageLayout === 'flat';
396
+ const inputBar = (
397
+ <ChatInputBar
398
+ ref={inputBarRef}
399
+ surface={useReadingTrack ? 'embedded' : surface}
400
+ sendError={inputSnapshot.sendError}
401
+ sendErrorDetailsLabel={t('chatErrorDetails', language)}
402
+ topSlot={controller.queuedInputs.length > 0
403
+ ? <SessionQueuedInputRows controller={controller} />
404
+ : null}
405
+ composer={{
406
+ nodes: [...inputSnapshot.nodes],
407
+ placeholder: textareaPlaceholder,
408
+ disabled: inputDisabled,
409
+ onNodesChange: handleNodesChange,
410
+ onFilesAdd: handleFilesAdd,
411
+ inputSurfaceTriggerSpecs: inputSurfaceState.triggerSpecs,
412
+ onInputSurfaceTriggerChange: setInputSurfaceTrigger,
413
+ }}
414
+ inputSurface={inputSurfaceState.panel ?? undefined}
415
+ hint={buildModelStateHint({
416
+ isModelOptionsLoading,
417
+ isModelOptionsEmpty,
418
+ onGoToProviders: presenter.chatUiManager.goToProviders,
419
+ texts: {
420
+ noModelOptionsLabel: t('chatModelNoOptions'),
421
+ configureProviderLabel: t('chatGoConfigureProvider'),
422
+ },
423
+ })}
424
+ toolbar={{
425
+ selects: [],
426
+ trailingSelects: toolbarSelects,
427
+ accessories: [
428
+ {
429
+ key: 'attach',
430
+ label: t('chatInputAttach'),
431
+ icon: 'paperclip' as const,
432
+ iconOnly: true,
433
+ disabled: !attachmentSupported || inputDisabled,
434
+ onClick: () => fileInputRef.current?.click(),
435
+ },
436
+ ],
437
+ skillPicker,
438
+ actions: {
439
+ isSending: controller.isSending,
440
+ canStopGeneration: controller.canStopGeneration,
441
+ sendDisabled: controller.sendDisabled,
442
+ stopDisabled: controller.stopDisabled,
443
+ stopHint: t('chatStopUnavailable'),
444
+ sendButtonLabel: controller.isSending ? t('chatQueueSend') : t('chatSend'),
445
+ stopButtonLabel: t('chatStop'),
446
+ contextWindow,
447
+ onSend: controller.send,
448
+ onStop: controller.stop,
449
+ },
450
+ }}
451
+ />
452
+ );
453
+
392
454
  return (
393
455
  <>
394
- <ChatInputBar
395
- ref={inputBarRef}
396
- surface={surface}
397
- topSlot={controller.queuedInputs.length > 0
398
- ? <SessionQueuedInputRows controller={controller} />
399
- : null}
400
- composer={{
401
- nodes: [...inputSnapshot.nodes],
402
- placeholder: textareaPlaceholder,
403
- disabled: inputDisabled,
404
- onNodesChange: handleNodesChange,
405
- onFilesAdd: handleFilesAdd,
406
- inputSurfaceTriggerSpecs: inputSurfaceState.triggerSpecs,
407
- onInputSurfaceTriggerChange: setInputSurfaceTrigger,
408
- }}
409
- inputSurface={inputSurfaceState.panel ?? undefined}
410
- hint={buildModelStateHint({
411
- isModelOptionsLoading,
412
- isModelOptionsEmpty,
413
- onGoToProviders: presenter.chatUiManager.goToProviders,
414
- texts: {
415
- noModelOptionsLabel: t('chatModelNoOptions'),
416
- configureProviderLabel: t('chatGoConfigureProvider'),
417
- },
418
- })}
419
- toolbar={{
420
- selects: [],
421
- trailingSelects: toolbarSelects,
422
- accessories: [
423
- {
424
- key: 'attach',
425
- label: t('chatInputAttach'),
426
- icon: 'paperclip' as const,
427
- iconOnly: true,
428
- disabled: !attachmentSupported || inputDisabled,
429
- onClick: () => fileInputRef.current?.click(),
430
- },
431
- ],
432
- skillPicker,
433
- actions: {
434
- sendError: inputSnapshot.sendError,
435
- isSending: controller.isSending,
436
- canStopGeneration: controller.canStopGeneration,
437
- sendDisabled: controller.sendDisabled,
438
- stopDisabled: controller.stopDisabled,
439
- stopHint: t('chatStopUnavailable'),
440
- sendButtonLabel: controller.isSending ? t('chatQueueSend') : t('chatSend'),
441
- stopButtonLabel: t('chatStop'),
442
- contextWindow,
443
- onSend: controller.send,
444
- onStop: controller.stop,
445
- },
446
- }}
447
- />
456
+ {useReadingTrack ? (
457
+ <ChatConversationTrack className="pb-4 pt-2" width="composer">
458
+ {inputBar}
459
+ </ChatConversationTrack>
460
+ ) : (
461
+ inputBar
462
+ )}
448
463
  <input
449
464
  ref={fileInputRef}
450
465
  type="file"
@@ -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>
@@ -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
 
@@ -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,7 +24,12 @@ 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";
28
+ import { useChatSessionListStore } from "@/features/chat/stores/chat-session-list.store";
29
+ import { useChatMessageLayoutStore } from "@/features/chat/stores/chat-message-layout.store";
30
+ import { useNcpChatSelectedSession } from "@/features/chat/features/ncp/hooks/use-ncp-chat-derived-state";
26
31
  import { useI18n } from "@/app/components/i18n-provider";
32
+ import { buildServerPathContentUrl } from "@/shared/lib/api";
27
33
  import { formatDateTime, t } from "@/shared/lib/i18n";
28
34
 
29
35
  type ChatMessageListContainerProps = {
@@ -56,7 +62,6 @@ function renderChatInlineDisplay(display: ChatInlineDisplayViewModel) {
56
62
  />
57
63
  );
58
64
  }
59
-
60
65
  type ContextInheritanceTimelineView = {
61
66
  sourceSessionId: string;
62
67
  inheritedMessageCount: number;
@@ -284,6 +289,16 @@ export function ChatMessageListContainer({
284
289
  }: ChatMessageListContainerProps) {
285
290
  const presenter = usePresenter();
286
291
  const { language } = useI18n();
292
+ const messageLayout = useChatMessageLayoutStore((state) => state.layout);
293
+ const selectedSessionKey = useChatSessionListStore(
294
+ (state) => state.snapshot.selectedSessionKey,
295
+ );
296
+ const selectedSession = useNcpChatSelectedSession(selectedSessionKey);
297
+ const localFileBasePath = selectedSession?.workingDir ?? selectedSession?.projectRoot ?? null;
298
+ const resolveFileContentUrl = useCallback(
299
+ (action: { path: string }) => buildServerPathContentUrl(action.path, localFileBasePath),
300
+ [localFileBasePath],
301
+ );
287
302
  const texts = useMemo(
288
303
  () => buildChatMessageAdapterTexts(language),
289
304
  [language],
@@ -342,6 +357,48 @@ export function ChatMessageListContainer({
342
357
  () => buildTimelineItems({ rawMessages, messages }),
343
358
  [messages, rawMessages],
344
359
  );
360
+ const sessionSkillsQuery = useChatQueryStore((state) => state.snapshot.sessionSkillsQuery);
361
+ const handleInlineTokenClick = useCallback(
362
+ (token: ChatInlineTokenViewModel) => {
363
+ if (token.kind !== "skill") {
364
+ return;
365
+ }
366
+ const skillKey = token.key.trim();
367
+ if (!skillKey) {
368
+ return;
369
+ }
370
+ const records = sessionSkillsQuery?.data?.records ?? [];
371
+ const matched = records.find((record) => record.ref === skillKey || record.name === skillKey);
372
+ const skillPath = matched?.path?.trim();
373
+ if (!skillPath) {
374
+ return;
375
+ }
376
+ presenter.chatThreadManager.openFilePreview({
377
+ path: skillPath,
378
+ label: token.label || matched?.name || skillKey,
379
+ viewMode: "preview",
380
+ previewViewer: "rendered",
381
+ });
382
+ },
383
+ [presenter.chatThreadManager, sessionSkillsQuery],
384
+ );
385
+ const handleAttachmentOpen = useCallback(
386
+ (file: { label: string; mimeType: string; dataUrl?: string; sizeBytes?: number; isImage: boolean }) => {
387
+ const contentUrl = file.dataUrl?.trim();
388
+ if (!contentUrl) {
389
+ return;
390
+ }
391
+ const label = file.label.trim() || "attachment";
392
+ presenter.chatThreadManager.openFilePreview({
393
+ path: label,
394
+ label,
395
+ viewMode: "preview",
396
+ contentUrl,
397
+ mimeType: file.mimeType,
398
+ });
399
+ },
400
+ [presenter.chatThreadManager],
401
+ );
345
402
  return (
346
403
  <div className={className}>
347
404
  {timelineItems.map((item, index) =>
@@ -352,12 +409,16 @@ export function ChatMessageListContainer({
352
409
  ) : (
353
410
  <ChatMessageList
354
411
  key={item.key}
412
+ layout={messageLayout}
355
413
  messages={item.messages}
356
414
  isSending={index === timelineItems.length - 1 ? isSending : false}
357
415
  hasAssistantDraft={hasAssistantDraft}
358
416
  texts={messageTexts}
359
417
  onToolAction={presenter.chatThreadManager.handleToolAction}
360
418
  onFileOpen={presenter.chatThreadManager.openFilePreview}
419
+ onAttachmentOpen={handleAttachmentOpen}
420
+ onInlineTokenClick={handleInlineTokenClick}
421
+ resolveFileContentUrl={resolveFileContentUrl}
361
422
  renderInlineDisplay={renderChatInlineDisplay}
362
423
  renderToolAgent={(agentId) => (
363
424
  <AgentIdentityAvatar
@@ -63,6 +63,36 @@ it("builds edit-file previews from structured args before the tool finishes", ()
63
63
  expect(editLines[1]).not.toHaveProperty("newLineNumber");
64
64
  });
65
65
 
66
+ it("keeps incomplete edit-file arguments available before preview fields arrive", () => {
67
+ const partialArgs = '{"path":"src/app.ts","oldText":';
68
+ const adapted = adapt([
69
+ {
70
+ id: "assistant-partial-edit-input",
71
+ role: "assistant",
72
+ parts: [
73
+ {
74
+ type: "tool-invocation",
75
+ toolInvocation: {
76
+ status: ToolInvocationStatus.PARTIAL_CALL,
77
+ toolCallId: "edit-partial-1",
78
+ toolName: "edit_file",
79
+ args: partialArgs,
80
+ },
81
+ },
82
+ ],
83
+ },
84
+ ] as unknown as ChatMessageSource[]);
85
+
86
+ expect(adapted[0]?.parts[0]).toMatchObject({
87
+ type: "tool-card",
88
+ card: {
89
+ toolName: "edit_file",
90
+ input: partialArgs,
91
+ statusTone: "running",
92
+ },
93
+ });
94
+ });
95
+
66
96
  it("uses structured edit-file result line numbers after the tool finishes", () => {
67
97
  const adapted = adapt([
68
98
  {
@@ -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
+ });
@@ -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,