@nextclaw/ui 0.14.4 → 0.15.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (194) hide show
  1. package/CHANGELOG.md +54 -0
  2. package/dist/assets/api-DLtsiBKm.js +13 -0
  3. package/dist/assets/appearance-settings-page-CtkeYXux.js +1 -0
  4. package/dist/assets/book-open-BQwWBDKb.js +1 -0
  5. package/dist/assets/channels-list-page-Cv8o9Hxg.js +8 -0
  6. package/dist/assets/chat-page-BtA7H1Ea.js +1 -0
  7. package/dist/assets/{config-split-page-CRhlZ_pH.js → config-split-page-EidRBpP2.js} +1 -1
  8. package/dist/assets/confirm-dialog-iIJFFp2T.js +5 -0
  9. package/dist/assets/desktop-update-config-C6n_TB1r.js +1 -0
  10. package/dist/assets/{dist-CikkhOlq.js → dist-CBxWOazz.js} +1 -1
  11. package/dist/assets/{dist-PVA_xL1D.js → dist-D2eZC3OR.js} +1 -1
  12. package/dist/assets/doc-browser-B76XOXmW.js +1 -0
  13. package/dist/assets/doc-browser-DBnj9UMB.js +1 -0
  14. package/dist/assets/{doc-browser-context-MTw4EvKd.js → doc-browser-context-Sv-044Cl.js} +1 -1
  15. package/dist/assets/{ellipsis-y6vDtq4M.js → ellipsis-2uzhypw5.js} +1 -1
  16. package/dist/assets/es2015-DhQo_oS6.js +41 -0
  17. package/dist/assets/{external-link-DVJIQf_G.js → external-link-CM5dnUI6.js} +1 -1
  18. package/dist/assets/host-capabilities-9bB_998Z.js +1 -0
  19. package/dist/assets/i18n-provider-di1waTGX.js +1 -0
  20. package/dist/assets/index-CBRP-7-a.css +1 -0
  21. package/dist/assets/index-Z1fYpbNd.js +104 -0
  22. package/dist/assets/loader-circle-BjmN5DeJ.js +1 -0
  23. package/dist/assets/mcp-marketplace-page-C_MVQUbT.js +9 -0
  24. package/dist/assets/mcp-marketplace-page-CoZIiD6G.js +1 -0
  25. package/dist/assets/model-config-page-Cdr9P6TQ.js +1 -0
  26. package/dist/assets/plus-3ztRZD_l.js +1 -0
  27. package/dist/assets/provider-scoped-model-input-g1WkGiJ8.js +1 -0
  28. package/dist/assets/providers-config-page-CWlO7J4A.js +1 -0
  29. package/dist/assets/{react-VNqT5Zp7.js → react-DjpzoZXB.js} +1 -1
  30. package/dist/assets/refresh-cw-DaTv3zxE.js +1 -0
  31. package/dist/assets/remote-CKWZfVVB.js +1 -0
  32. package/dist/assets/rotate-cw-CHcaRUDz.js +1 -0
  33. package/dist/assets/runtime-config-page-CfTqh1nv.js +1 -0
  34. package/dist/assets/{save-BOJ477H-.js → save-BZof1uie.js} +1 -1
  35. package/dist/assets/{search-BEti5beA.js → search-MxAarS_g.js} +1 -1
  36. package/dist/assets/search-config-page-Dm8daYny.js +1 -0
  37. package/dist/assets/{secrets-config-page--IzrIOid.js → secrets-config-page-D8AbcR05.js} +2 -2
  38. package/dist/assets/security-config-Ds0LsJmo.js +1 -0
  39. package/dist/assets/select-D7GcNP0z.js +1 -0
  40. package/dist/assets/setting-row-CuozlnJd.js +1 -0
  41. package/dist/assets/side-dock-BcLau5S_.js +3 -0
  42. package/dist/assets/skeleton-shfmXuYp.js +1 -0
  43. package/dist/assets/switch-C1BWMU5c.js +1 -0
  44. package/dist/assets/tag-chip-DCUrQUCP.js +1 -0
  45. package/dist/assets/tooltip-DfAHHA76.js +1 -0
  46. package/dist/assets/use-config-DAdbT5QJ.js +1 -0
  47. package/dist/assets/use-confirm-dialog-eWcJ6V-s.js +1 -0
  48. package/dist/assets/x-DqL1mYLC.js +1 -0
  49. package/dist/index.html +53 -17
  50. package/index.html +25 -2
  51. package/package.json +9 -9
  52. package/src/app/components/layout/__tests__/app-layout.test.tsx +27 -0
  53. package/src/app/components/layout/__tests__/settings-entry-page.test.tsx +9 -1
  54. package/src/app/components/layout/__tests__/sidebar.layout.test.tsx +6 -3
  55. package/src/app/components/layout/app-layout.tsx +10 -3
  56. package/src/app/components/layout/sidebar-items.tsx +8 -8
  57. package/src/app/components/layout/sidebar-rail.styles.ts +4 -2
  58. package/src/app/components/layout/sidebar.tsx +21 -28
  59. package/src/app/configs/app-navigation.config.ts +11 -5
  60. package/src/app/index.tsx +8 -0
  61. package/src/app/managers/__tests__/viewport-layout.manager.test.ts +59 -0
  62. package/src/app/managers/viewport-layout.manager.ts +39 -0
  63. package/src/app/presenters/app.presenter.ts +25 -2
  64. package/src/app/styles/design-system.css +91 -4
  65. package/src/features/chat/components/conversation/__tests__/chat-conversation-content.test.tsx +86 -0
  66. package/src/features/chat/components/conversation/chat-conversation-content.tsx +47 -17
  67. package/src/features/chat/components/layout/__tests__/chat-sidebar-read-state.test.tsx +7 -0
  68. package/src/features/chat/components/layout/chat-sidebar-desktop-layout.tsx +9 -5
  69. package/src/features/chat/components/layout/chat-sidebar-utility-menu.tsx +5 -5
  70. package/src/features/chat/features/conversation/components/__tests__/session-conversation-area.test.tsx +51 -2
  71. package/src/features/chat/features/conversation/components/__tests__/session-conversation-input.streaming.test.tsx +170 -1
  72. package/src/features/chat/features/conversation/components/session-conversation-area.tsx +23 -1
  73. package/src/features/chat/features/conversation/components/session-conversation-input.tsx +24 -7
  74. package/src/features/chat/features/conversation/components/session-queued-input-rows.tsx +96 -0
  75. package/src/features/chat/features/conversation/hooks/__tests__/use-session-conversation-controller.test.tsx +181 -0
  76. package/src/features/chat/features/conversation/hooks/use-session-conversation-controller.ts +351 -89
  77. package/src/features/chat/features/input/hooks/__tests__/use-chat-input-surface-state.test.tsx +29 -4
  78. package/src/features/chat/features/input/hooks/use-chat-input-surface-state.ts +28 -3
  79. package/src/features/chat/features/input/input-surface-plugins/__tests__/slash-command-plugin.test.ts +108 -8
  80. package/src/features/chat/features/input/input-surface-plugins/panel-app-reference-plugin.utils.ts +50 -14
  81. package/src/features/chat/features/input/input-surface-plugins/slash-command-plugin.utils.ts +77 -6
  82. package/src/features/chat/features/message/components/__tests__/chat-inline-panel-app-card.test.tsx +21 -12
  83. package/src/features/chat/features/message/components/__tests__/chat-message-list.container.test.tsx +166 -0
  84. package/src/features/chat/features/message/components/chat-inline-panel-app-card.tsx +19 -11
  85. package/src/features/chat/features/message/components/chat-message-list.container.tsx +38 -64
  86. package/src/features/chat/features/message/utils/__tests__/chat-message-process-summary.utils.test.ts +59 -0
  87. package/src/features/chat/features/message/utils/__tests__/chat-message-show-content-tool-card.utils.test.ts +56 -1
  88. package/src/features/chat/features/message/utils/chat-inline-panel-app-card.utils.ts +0 -2
  89. package/src/features/chat/features/message/utils/chat-message-process-summary.utils.ts +80 -0
  90. package/src/features/chat/features/message/utils/chat-message-show-content-tool-card.utils.ts +12 -3
  91. package/src/features/chat/features/message/utils/chat-message-texts.utils.ts +60 -0
  92. package/src/features/chat/features/message/utils/chat-message.utils.ts +3 -0
  93. package/src/features/chat/features/ncp/hooks/__tests__/use-ncp-agent-runtime.test.tsx +39 -0
  94. package/src/features/chat/features/session/components/chat-sidebar-project-groups.tsx +2 -2
  95. package/src/features/chat/features/session/components/chat-sidebar-session-item.tsx +1 -1
  96. package/src/features/chat/features/session/components/session-header/__tests__/chat-session-header-actions.test.tsx +3 -1
  97. package/src/features/chat/features/session/components/session-header/chat-session-metadata-dialog.tsx +1 -1
  98. package/src/features/chat/features/workspace/components/__tests__/chat-session-workspace-file-preview.test.tsx +270 -115
  99. package/src/features/chat/features/workspace/components/chat-session-workspace-file-breadcrumb-browser.tsx +121 -0
  100. package/src/features/chat/features/workspace/components/chat-session-workspace-file-breadcrumbs.tsx +75 -20
  101. package/src/features/chat/features/workspace/components/chat-session-workspace-file-preview.tsx +70 -5
  102. package/src/features/chat/features/workspace/utils/chat-workspace-file-tab-persistence.utils.ts +83 -0
  103. package/src/features/chat/managers/__tests__/chat-thread.manager.test.ts +79 -1
  104. package/src/features/chat/managers/__tests__/chat-ui.manager.test.ts +2 -0
  105. package/src/features/chat/managers/chat-thread.manager.ts +9 -3
  106. package/src/features/chat/managers/chat-ui.manager.ts +5 -1
  107. package/src/features/chat/presenters/chat.presenter.ts +2 -1
  108. package/src/features/chat/stores/__tests__/chat-thread.store.test.ts +4 -0
  109. package/src/features/chat/stores/chat-thread.store.ts +6 -64
  110. package/src/features/chat/types/chat-message.types.ts +4 -0
  111. package/src/features/marketplace/components/__tests__/marketplace-page-detail.test.tsx +33 -26
  112. package/src/features/marketplace/components/__tests__/marketplace-page.test.tsx +1 -0
  113. package/src/features/marketplace/components/curated-shelves/__tests__/marketplace-curated-scene-route.test.tsx +1 -0
  114. package/src/features/marketplace/components/detail-doc/__tests__/marketplace-detail-doc.test.tsx +115 -0
  115. package/src/features/marketplace/components/marketplace-detail-doc.tsx +441 -0
  116. package/src/features/marketplace/components/mcp/__tests__/mcp-marketplace-page.test.tsx +3 -3
  117. package/src/features/marketplace/components/mcp/mcp-marketplace-doc.ts +0 -60
  118. package/src/features/marketplace/components/mcp/mcp-marketplace-page.tsx +64 -29
  119. package/src/features/marketplace/hooks/use-marketplace-item-detail.ts +42 -31
  120. package/src/features/marketplace/index.ts +3 -0
  121. package/src/features/marketplace/stores/marketplace-detail-doc.store.ts +38 -0
  122. package/src/features/panel-apps/components/__tests__/panel-apps-list.test.tsx +32 -2
  123. package/src/features/pwa/managers/__tests__/pwa-shell-theme.manager.test.ts +11 -0
  124. package/src/features/pwa/managers/pwa-shell-theme.manager.ts +8 -3
  125. package/src/features/settings/pages/__tests__/appearance-settings-page.test.tsx +25 -0
  126. package/src/features/settings/pages/appearance-settings-page.tsx +40 -0
  127. package/src/features/side-dock/components/__tests__/side-dock.test.tsx +44 -2
  128. package/src/features/side-dock/components/side-dock.tsx +100 -13
  129. package/src/features/side-dock/configs/side-dock-built-in-items.config.ts +10 -0
  130. package/src/features/side-dock/managers/__tests__/side-dock.manager.test.ts +32 -2
  131. package/src/features/side-dock/managers/side-dock.manager.ts +17 -7
  132. package/src/features/side-dock/stores/side-dock.store.ts +9 -0
  133. package/src/features/side-dock/types/side-dock.types.ts +10 -3
  134. package/src/features/side-dock/utils/side-dock-item.utils.ts +15 -2
  135. package/src/index.css +501 -27
  136. package/src/platforms/desktop/components/desktop-window-chrome.tsx +1 -1
  137. package/src/shared/components/doc-browser/__tests__/doc-browser-context.test.tsx +30 -1
  138. package/src/shared/components/doc-browser/__tests__/doc-browser.test.tsx +206 -38
  139. package/src/shared/components/doc-browser/doc-browser-panel-parts.tsx +34 -11
  140. package/src/shared/components/doc-browser/doc-browser-renderer.types.ts +1 -1
  141. package/src/shared/components/doc-browser/doc-browser-tab-strip.tsx +23 -5
  142. package/src/shared/components/doc-browser/doc-browser.tsx +128 -75
  143. package/src/shared/components/doc-browser/managers/doc-browser.manager.ts +9 -1
  144. package/src/shared/components/doc-browser/utils/doc-browser-floating-panel.utils.ts +83 -0
  145. package/src/shared/components/ui/__tests__/dialog.test.tsx +27 -0
  146. package/src/shared/components/ui/actions/icon-action-button.tsx +14 -2
  147. package/src/shared/components/ui/dialog.tsx +2 -2
  148. package/src/shared/components/ui/popover.tsx +1 -1
  149. package/src/shared/components/ui/select.tsx +1 -1
  150. package/src/shared/components/ui/switch.tsx +4 -3
  151. package/src/shared/components/ui/tab-strip/__tests__/compact-tab-strip.test.tsx +54 -0
  152. package/src/shared/components/ui/tab-strip/compact-tab-strip.tsx +4 -5
  153. package/src/shared/components/ui/tooltip.tsx +1 -1
  154. package/src/shared/lib/api/utils/server-path.utils.ts +27 -1
  155. package/src/shared/lib/i18n/locales/en-US/chat.json +15 -6
  156. package/src/shared/lib/i18n/locales/en-US/core.json +2 -0
  157. package/src/shared/lib/i18n/locales/en-US/doc-browser.json +11 -0
  158. package/src/shared/lib/i18n/locales/en-US/marketplace.json +5 -0
  159. package/src/shared/lib/i18n/locales/zh-CN/chat.json +15 -6
  160. package/src/shared/lib/i18n/locales/zh-CN/core.json +2 -0
  161. package/src/shared/lib/i18n/locales/zh-CN/doc-browser.json +11 -0
  162. package/src/shared/lib/i18n/locales/zh-CN/marketplace.json +5 -0
  163. package/src/shared/lib/session-project/__tests__/workspace-file-breadcrumb.test.ts +14 -0
  164. package/src/shared/lib/session-project/index.ts +1 -1
  165. package/src/shared/lib/session-project/{workspace-file-breadcrumb.ts → workspace-file-breadcrumb.utils.ts} +62 -8
  166. package/src/shared/lib/theme/index.ts +51 -13
  167. package/dist/assets/api-BP_8IaxB.js +0 -13
  168. package/dist/assets/channels-list-page-DEh5Jjqx.js +0 -8
  169. package/dist/assets/chat-page-BRB0N5WW.js +0 -105
  170. package/dist/assets/confirm-dialog-DsivFJ4r.js +0 -5
  171. package/dist/assets/desktop-update-config-B0w9p2ep.js +0 -1
  172. package/dist/assets/doc-browser-BoNJ7-tE.js +0 -1
  173. package/dist/assets/doc-browser-CdJ_d4ik.js +0 -1
  174. package/dist/assets/index-BiE-K8-q.css +0 -1
  175. package/dist/assets/index-D9BAE_L_.js +0 -105
  176. package/dist/assets/mcp-marketplace-page-B1SZNY5m.js +0 -40
  177. package/dist/assets/mcp-marketplace-page-Cq_Tw42T.js +0 -1
  178. package/dist/assets/model-config-page-DotaTUHb.js +0 -1
  179. package/dist/assets/plus-BbZqquJe.js +0 -1
  180. package/dist/assets/provider-scoped-model-input-DsFRuWJH.js +0 -1
  181. package/dist/assets/providers-config-page-DW4tQSLN.js +0 -1
  182. package/dist/assets/remote-B8TiUfwW.js +0 -1
  183. package/dist/assets/rotate-cw-DjHDTkCv.js +0 -1
  184. package/dist/assets/runtime-config-page-S6ErRHR8.js +0 -1
  185. package/dist/assets/search-config-page-DGj5am03.js +0 -1
  186. package/dist/assets/select-hoj8dK4K.js +0 -41
  187. package/dist/assets/tag-chip-T8-h-die.js +0 -1
  188. package/dist/assets/use-config-CjRV2hwL.js +0 -1
  189. package/dist/assets/x-D9PXs_xP.js +0 -1
  190. package/src/features/marketplace/components/detail-doc/__tests__/marketplace-detail-doc.test.ts +0 -40
  191. package/src/features/marketplace/components/detail-doc/marketplace-detail-doc-renderer.ts +0 -201
  192. package/src/features/marketplace/components/marketplace-detail-doc.ts +0 -142
  193. /package/dist/assets/{config-hints-BkYBc7z0.js → config-hints-fGnUjDe9.js} +0 -0
  194. /package/dist/assets/{middleware-CkFT3R4v.js → middleware-BILWbJE4.js} +0 -0
@@ -6,6 +6,8 @@
6
6
 
7
7
  @layer base {
8
8
  :root {
9
+ color-scheme: light;
10
+
9
11
  /* ========================================
10
12
  BRAND COLORS (Natural Olive)
11
13
  ======================================== */
@@ -200,10 +202,11 @@
200
202
  --z-dropdown: 100;
201
203
  --z-sticky: 200;
202
204
  --z-fixed: 300;
203
- --z-modal-backdrop: 400;
204
- --z-modal: 500;
205
- --z-popover: 600;
206
- --z-tooltip: 700;
205
+ --z-floating-panel: 9999;
206
+ --z-modal-backdrop: 10000;
207
+ --z-modal: 10050;
208
+ --z-popover: 10100;
209
+ --z-tooltip: 10150;
207
210
  }
208
211
  }
209
212
 
@@ -569,6 +572,90 @@
569
572
  }
570
573
  }
571
574
 
575
+
576
+ @layer base {
577
+ :root[data-theme="night"] {
578
+ color-scheme: dark;
579
+
580
+ /* ========================================
581
+ BRAND COLORS (Night Moss)
582
+ ======================================== */
583
+ --brand-50: 148 36% 91%;
584
+ --brand-100: 148 34% 84%;
585
+ --brand-200: 149 32% 72%;
586
+ --brand-300: 149 33% 60%;
587
+ --brand-400: 150 36% 52%;
588
+ --brand-500: 150 42% 46%;
589
+ --brand-600: 150 46% 38%;
590
+ --brand-700: 151 48% 30%;
591
+ --brand-800: 152 48% 22%;
592
+ --brand-900: 153 46% 15%;
593
+
594
+ /* ========================================
595
+ NEUTRAL COLORS (Deep Workbench)
596
+ ======================================== */
597
+ --gray-50: 220 18% 8%;
598
+ --gray-100: 220 16% 11%;
599
+ --gray-200: 219 14% 16%;
600
+ --gray-300: 218 12% 24%;
601
+ --gray-400: 215 10% 46%;
602
+ --gray-500: 213 12% 62%;
603
+ --gray-600: 212 16% 74%;
604
+ --gray-700: 43 22% 82%;
605
+ --gray-800: 44 32% 90%;
606
+ --gray-900: 44 40% 96%;
607
+
608
+ /* Semantic Colors */
609
+ --background: var(--gray-50);
610
+ --background-secondary: var(--gray-100);
611
+ --background-tertiary: var(--gray-200);
612
+ --foreground: var(--gray-800);
613
+ --foreground-secondary: var(--gray-700);
614
+ --foreground-tertiary: var(--gray-500);
615
+ --foreground-muted: var(--gray-400);
616
+ --primary: var(--brand-400);
617
+ --primary-hover: var(--brand-300);
618
+ --primary-active: var(--brand-200);
619
+ --primary-foreground: 220 18% 8%;
620
+ --secondary: var(--gray-100);
621
+ --secondary-hover: var(--gray-200);
622
+ --secondary-foreground: var(--gray-800);
623
+ --accent: 151 32% 18%;
624
+ --accent-foreground: 148 34% 84%;
625
+ --muted: var(--gray-100);
626
+ --muted-foreground: var(--gray-500);
627
+ --destructive: 0 70% 52%;
628
+ --destructive-foreground: 0 0% 100%;
629
+ --success: 150 46% 44%;
630
+ --success-foreground: 220 18% 8%;
631
+ --warning: 42 82% 58%;
632
+ --warning-foreground: 220 18% 8%;
633
+ --card: 220 16% 10%;
634
+ --card-foreground: var(--gray-800);
635
+ --card-border: 218 13% 20%;
636
+ --popover: 220 16% 10%;
637
+ --popover-foreground: var(--gray-800);
638
+ --border: 218 13% 20%;
639
+ --border-hover: 218 12% 30%;
640
+ --border-active: var(--brand-400);
641
+ --input: 220 16% 12%;
642
+ --input-border: 218 13% 24%;
643
+ --input-focus: var(--brand-400);
644
+ --ring: var(--brand-400);
645
+ --ring-offset: var(--gray-50);
646
+
647
+ /* Shadows (deep neutral tint) */
648
+ --shadow-xs: 0 1px 2px 0 rgb(0 0 0 / 0.28);
649
+ --shadow-sm: 0 1px 3px 0 rgb(0 0 0 / 0.34), 0 1px 2px -1px rgb(0 0 0 / 0.3);
650
+ --shadow-md: 0 8px 18px -10px rgb(0 0 0 / 0.42), 0 4px 8px -6px rgb(0 0 0 / 0.34);
651
+ --shadow-lg: 0 16px 28px -16px rgb(0 0 0 / 0.48), 0 8px 14px -10px rgb(0 0 0 / 0.38);
652
+ --shadow-xl: 0 24px 40px -20px rgb(0 0 0 / 0.56), 0 10px 18px -12px rgb(0 0 0 / 0.42);
653
+ --shadow-2xl: 0 32px 60px -24px rgb(0 0 0 / 0.65);
654
+ --shadow-card: 0 0 0 1px hsl(var(--border) / 0.82), 0 18px 48px -34px rgb(0 0 0 / 0.72);
655
+ --shadow-card-hover: 0 0 0 1px hsl(var(--border-hover) / 0.88), 0 24px 60px -36px rgb(0 0 0 / 0.82);
656
+ }
657
+ }
658
+
572
659
  @layer base {
573
660
  :root[data-theme="probe"] {
574
661
  /* ========================================
@@ -0,0 +1,86 @@
1
+ import { render, screen } from "@testing-library/react";
2
+ import userEvent from "@testing-library/user-event";
3
+ import type { NcpMessage } from "@nextclaw/ncp";
4
+ import { beforeEach, expect, it, vi } from "vitest";
5
+ import { ChatConversationContent } from "@/features/chat/components/conversation/chat-conversation-content";
6
+
7
+ const captures = vi.hoisted(() => ({
8
+ isAtBottom: true,
9
+ onScroll: vi.fn(),
10
+ scrollToBottom: vi.fn(),
11
+ }));
12
+
13
+ vi.mock("@nextclaw/agent-chat-ui", () => ({
14
+ useStickyBottomScroll: () => ({
15
+ isAtBottom: captures.isAtBottom,
16
+ onScroll: captures.onScroll,
17
+ scrollToBottom: captures.scrollToBottom,
18
+ }),
19
+ }));
20
+
21
+ vi.mock("@/features/chat/features/message/components/chat-message-list.container", () => ({
22
+ ChatMessageListContainer: () => <div data-testid="chat-message-list" />,
23
+ }));
24
+
25
+ vi.mock("@/shared/lib/i18n", () => ({
26
+ t: (key: string) => key,
27
+ }));
28
+
29
+ const messages = [
30
+ {
31
+ id: "message-1",
32
+ sessionId: "session-1",
33
+ role: "user",
34
+ status: "final",
35
+ timestamp: "2026-07-04T10:00:00.000Z",
36
+ parts: [{ type: "text", text: "hello" }],
37
+ },
38
+ ] as unknown as readonly NcpMessage[];
39
+
40
+ function renderContent(options: { bottomSlot?: React.ReactNode } = {}) {
41
+ return render(
42
+ <ChatConversationContent
43
+ bottomSlot={options.bottomSlot}
44
+ isAwaitingAssistantOutput={false}
45
+ isHistoryLoading={false}
46
+ isSending={false}
47
+ messages={messages}
48
+ sessionKey="session-1"
49
+ showWelcome={false}
50
+ />,
51
+ );
52
+ }
53
+
54
+ beforeEach(() => {
55
+ captures.isAtBottom = true;
56
+ captures.onScroll.mockReset();
57
+ captures.scrollToBottom.mockReset();
58
+ });
59
+
60
+ it("hides the scroll-to-bottom action while the conversation is already at the bottom", () => {
61
+ renderContent();
62
+
63
+ expect(screen.queryByRole("button", { name: "chatScrollToBottom" })).toBeNull();
64
+ });
65
+
66
+ it("scrolls to the latest message when the user clicks the floating action", async () => {
67
+ const user = userEvent.setup();
68
+ captures.isAtBottom = false;
69
+
70
+ renderContent();
71
+ await user.click(screen.getByRole("button", { name: "chatScrollToBottom" }));
72
+
73
+ expect(captures.scrollToBottom).toHaveBeenCalledOnce();
74
+ });
75
+
76
+ it("renders bottom slot after the message list", () => {
77
+ renderContent({
78
+ bottomSlot: <div data-testid="conversation-bottom-slot">Run failed</div>,
79
+ });
80
+
81
+ const messageList = screen.getByTestId("chat-message-list");
82
+ const bottomSlot = screen.getByTestId("conversation-bottom-slot");
83
+ expect(
84
+ messageList.compareDocumentPosition(bottomSlot) & Node.DOCUMENT_POSITION_FOLLOWING,
85
+ ).toBeTruthy();
86
+ });
@@ -1,9 +1,13 @@
1
1
  import { useRef, type ReactNode } from "react";
2
2
  import { useStickyBottomScroll } from "@nextclaw/agent-chat-ui";
3
3
  import type { NcpMessage } from "@nextclaw/ncp";
4
+ import { ArrowDown } from "lucide-react";
4
5
  import { ChatMessageListContainer } from "@/features/chat/features/message/components/chat-message-list.container";
6
+ import { IconActionButton } from "@/shared/components/ui/actions/icon-action-button";
7
+ import { t } from "@/shared/lib/i18n";
5
8
 
6
9
  type ChatConversationContentProps = {
10
+ bottomSlot?: ReactNode;
7
11
  isAwaitingAssistantOutput: boolean;
8
12
  isHistoryLoading: boolean;
9
13
  isSending: boolean;
@@ -14,6 +18,7 @@ type ChatConversationContentProps = {
14
18
  };
15
19
 
16
20
  export function ChatConversationContent({
21
+ bottomSlot,
17
22
  isAwaitingAssistantOutput,
18
23
  isHistoryLoading,
19
24
  isSending,
@@ -28,7 +33,11 @@ export function ChatConversationContent({
28
33
  messages.length === 0 &&
29
34
  !isSending &&
30
35
  !isAwaitingAssistantOutput;
31
- const { onScroll } = useStickyBottomScroll({
36
+ const {
37
+ isAtBottom,
38
+ onScroll,
39
+ scrollToBottom,
40
+ } = useStickyBottomScroll({
32
41
  scrollRef: threadRef,
33
42
  resetKey: sessionKey,
34
43
  isLoading: isHistoryLoading,
@@ -36,24 +45,45 @@ export function ChatConversationContent({
36
45
  contentVersion: messages[messages.length - 1] ?? null,
37
46
  });
38
47
  const hasMessages = messages.length > 0;
48
+ const showScrollToBottom = hasMessages && !showWelcome && !isAtBottom;
39
49
 
40
50
  return (
41
- <div
42
- ref={threadRef}
43
- onScroll={onScroll}
44
- data-chat-scroll-container="true"
45
- className="flex-1 min-h-0 overflow-y-auto custom-scrollbar"
46
- >
47
- {showWelcome ? (
48
- welcomeSlot ?? null
49
- ) : hideEmptyHint || !hasMessages ? null : (
50
- <div className="mx-auto w-full max-w-[min(1120px,100%)] px-4 py-4 sm:px-6 sm:py-5">
51
- <ChatMessageListContainer
52
- messages={messages}
53
- isSending={hasMessages && isSending && isAwaitingAssistantOutput}
54
- />
55
- </div>
56
- )}
51
+ <div className="relative min-h-0 flex-1">
52
+ <div
53
+ ref={threadRef}
54
+ onScroll={onScroll}
55
+ data-chat-scroll-container="true"
56
+ className="h-full overflow-y-auto custom-scrollbar"
57
+ >
58
+ {showWelcome ? (
59
+ welcomeSlot ?? null
60
+ ) : (
61
+ <>
62
+ {hideEmptyHint || !hasMessages ? null : (
63
+ <div className="mx-auto w-full max-w-[min(1120px,100%)] px-4 py-4 sm:px-6 sm:py-5">
64
+ <ChatMessageListContainer
65
+ messages={messages}
66
+ isSending={hasMessages && isSending && isAwaitingAssistantOutput}
67
+ />
68
+ </div>
69
+ )}
70
+ {bottomSlot ? (
71
+ <div className="mx-auto w-full max-w-[min(1120px,100%)] px-4 pb-4 sm:px-6 sm:pb-5">
72
+ {bottomSlot}
73
+ </div>
74
+ ) : null}
75
+ </>
76
+ )}
77
+ </div>
78
+ {showScrollToBottom ? (
79
+ <IconActionButton
80
+ icon={<ArrowDown className="h-4 w-4" />}
81
+ label={t("chatScrollToBottom")}
82
+ onClick={scrollToBottom}
83
+ tooltipSide="top"
84
+ className="absolute bottom-4 left-1/2 z-10 h-9 w-9 -translate-x-1/2 rounded-full border border-border bg-background/90 text-foreground shadow-lg backdrop-blur hover:bg-accent hover:text-accent-foreground"
85
+ />
86
+ ) : null}
57
87
  </div>
58
88
  );
59
89
  }
@@ -188,6 +188,13 @@ describe("ChatSidebar read state sync", () => {
188
188
  it("collapses the desktop sidebar into accessible icon actions", () => {
189
189
  const { container } = renderReadStateSidebar(createTestQueryClient());
190
190
 
191
+ expect(
192
+ screen.getByText("Sessions").parentElement?.nextElementSibling?.className,
193
+ ).toContain("[mask-image:linear-gradient");
194
+ expect(
195
+ screen.getByRole("button", { name: "Settings menu" }).parentElement
196
+ ?.className,
197
+ ).not.toContain("border-t");
191
198
  fireEvent.click(screen.getByRole("button", { name: "Collapse sidebar" }));
192
199
 
193
200
  const persistedState = JSON.parse(
@@ -29,6 +29,7 @@ import {
29
29
  SIDEBAR_RAIL_PADDING_X_CLASS,
30
30
  SIDEBAR_RAIL_STACK_CLASS,
31
31
  SIDEBAR_RAIL_SURFACE_CLASS,
32
+ SIDEBAR_SCROLL_EDGE_FADE_CLASS,
32
33
  } from "@/app/components/layout/sidebar-rail.styles";
33
34
 
34
35
  const navItems = [
@@ -67,9 +68,7 @@ export function ChatSidebarDesktopHeader({
67
68
  isCollapsed: boolean;
68
69
  }) {
69
70
  const isWindowsHost = isWindowsDesktopHost();
70
- const shouldReserveMacWindowControls =
71
- typeof window !== "undefined" &&
72
- window.nextclawDesktop?.platform === "darwin";
71
+ const shouldReserveMacWindowControls = typeof window !== "undefined" && window.nextclawDesktop?.platform === "darwin";
73
72
 
74
73
  return (
75
74
  <div
@@ -170,7 +169,12 @@ export function ChatSidebarSessionArea({
170
169
  />
171
170
  </div>
172
171
 
173
- <div className="flex-1 min-h-0 overflow-y-auto custom-scrollbar px-3 py-2">
172
+ <div
173
+ className={cn(
174
+ "custom-scrollbar min-h-0 flex-1 overflow-y-auto px-3 pb-7 pt-2",
175
+ SIDEBAR_SCROLL_EDGE_FADE_CLASS,
176
+ )}
177
+ >
174
178
  <ChatSidebarSessionList
175
179
  isLoading={isLoading}
176
180
  isProjectFirstView={isProjectFirstView}
@@ -221,7 +225,7 @@ export function ChatSidebarDesktopFooter({
221
225
  return (
222
226
  <div
223
227
  className={cn(
224
- "border-t border-border/70 py-3",
228
+ "py-3",
225
229
  isCollapsed
226
230
  ? cn("flex justify-center", SIDEBAR_RAIL_PADDING_X_CLASS)
227
231
  : "px-3",
@@ -115,7 +115,7 @@ export function ChatSidebarUtilityMenu({
115
115
  <button
116
116
  type="button"
117
117
  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-all duration-base hover:bg-accent hover:text-accent-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary/35"
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"
119
119
  >
120
120
  <Settings className="h-4 w-4 shrink-0 text-muted-foreground/70" />
121
121
  <span className="min-w-0 flex-1 text-left">{t("settings")}</span>
@@ -134,7 +134,7 @@ export function ChatSidebarUtilityMenu({
134
134
  <NavLink
135
135
  to="/settings"
136
136
  onClick={() => onOpenChange(false)}
137
- 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-accent hover:text-accent-foreground"
137
+ 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"
138
138
  >
139
139
  <Settings className="h-4 w-4 text-muted-foreground/70" />
140
140
  <span className="flex-1 text-left">{t("settings")}</span>
@@ -142,7 +142,7 @@ export function ChatSidebarUtilityMenu({
142
142
  <button
143
143
  type="button"
144
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-accent hover:text-accent-foreground"
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
146
  >
147
147
  <BookOpen className="h-4 w-4 text-muted-foreground/70" />
148
148
  <span className="flex-1 text-left">{t("docBrowserHelp")}</span>
@@ -150,7 +150,7 @@ export function ChatSidebarUtilityMenu({
150
150
  <button
151
151
  type="button"
152
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-accent hover:text-accent-foreground"
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
154
  >
155
155
  <Boxes className="h-4 w-4 text-muted-foreground/70" />
156
156
  <span className="flex-1 text-left">{t("appsTitle")}</span>
@@ -212,7 +212,7 @@ function ChatSidebarUtilitySelect<Value extends string>({
212
212
  >
213
213
  <SelectTrigger
214
214
  aria-label={label}
215
- className="h-auto w-full rounded-lg border-0 bg-transparent px-3 py-2 text-[13px] font-medium text-foreground shadow-none hover:bg-accent hover:text-accent-foreground focus:ring-0"
215
+ className="h-auto w-full rounded-lg border-0 bg-transparent px-3 py-2 text-[13px] font-medium text-foreground shadow-none hover:bg-gray-200/60 hover:text-gray-900 focus:ring-0"
216
216
  indicator={<ChevronRight className="h-4 w-4 text-muted-foreground/70" />}
217
217
  >
218
218
  <div className="flex min-w-0 items-center gap-2.5">
@@ -7,6 +7,7 @@ import { SessionConversationArea } from '@/features/chat/features/conversation/c
7
7
 
8
8
  const mocks = vi.hoisted(() => {
9
9
  const inputRenderSpy = vi.fn();
10
+ const controllerParamsSpy = vi.fn();
10
11
  const inputActions = {
11
12
  update: vi.fn(),
12
13
  syncComposer: vi.fn(),
@@ -47,7 +48,14 @@ const mocks = vi.hoisted(() => {
47
48
  isProviderStateResolved: true,
48
49
  isSkillsLoading: false,
49
50
  modelOptions: [],
50
- selectedSession: null,
51
+ selectedSession: null as null | {
52
+ activityPreview?: {
53
+ state: 'running' | 'completed' | 'failed' | 'idle';
54
+ statusText?: string;
55
+ replyText?: string;
56
+ };
57
+ status?: string;
58
+ },
51
59
  selectedSessionKey: null,
52
60
  sessionTypeState: {
53
61
  sessionTypeOptions: [],
@@ -62,8 +70,11 @@ const mocks = vi.hoisted(() => {
62
70
  };
63
71
  const controller = {
64
72
  canStopGeneration: true,
73
+ deleteQueuedInput: vi.fn(),
74
+ editQueuedInput: vi.fn(),
65
75
  hasSendableDraft: true,
66
76
  isSending: true,
77
+ queuedInputs: [],
67
78
  send: vi.fn(),
68
79
  sendDisabled: true,
69
80
  stop: vi.fn(),
@@ -87,6 +98,7 @@ const mocks = vi.hoisted(() => {
87
98
  return {
88
99
  agent,
89
100
  controller,
101
+ controllerParamsSpy,
90
102
  inputActions,
91
103
  inputQuery,
92
104
  inputRenderSpy,
@@ -120,16 +132,19 @@ vi.mock('@/features/chat/components/providers/chat-presenter.provider', () => ({
120
132
 
121
133
  vi.mock('@/features/chat/components/conversation/chat-conversation-content', () => ({
122
134
  ChatConversationContent: ({
135
+ bottomSlot,
123
136
  messages,
124
137
  showWelcome,
125
138
  welcomeSlot,
126
139
  }: {
140
+ bottomSlot?: ReactNode;
127
141
  messages: readonly unknown[];
128
142
  showWelcome: boolean;
129
143
  welcomeSlot?: ReactNode;
130
144
  }) => (
131
145
  <div data-testid="conversation-content" data-show-welcome={String(showWelcome)}>
132
146
  {showWelcome ? welcomeSlot : <div data-testid="message-count">{messages.length}</div>}
147
+ {bottomSlot ? <div data-testid="conversation-bottom-slot">{bottomSlot}</div> : null}
133
148
  </div>
134
149
  ),
135
150
  }));
@@ -162,7 +177,10 @@ vi.mock('@/features/chat/features/conversation/hooks/use-session-conversation-in
162
177
  }));
163
178
 
164
179
  vi.mock('@/features/chat/features/conversation/hooks/use-session-conversation-controller', () => ({
165
- useSessionConversationController: () => mocks.controller,
180
+ useSessionConversationController: (params: unknown) => {
181
+ mocks.controllerParamsSpy(params);
182
+ return mocks.controller;
183
+ },
166
184
  }));
167
185
 
168
186
  vi.mock('@/features/chat/stores/chat-session-list.store', () => ({
@@ -197,6 +215,21 @@ describe('SessionConversationArea input boundary', () => {
197
215
  mocks.agent.isRunning = true;
198
216
  mocks.agent.isSending = true;
199
217
  mocks.agent.snapshot.contextWindow = null;
218
+ mocks.inputQuery.selectedSession = null;
219
+ });
220
+
221
+ it('passes running state without treating an active run as a send request in flight', () => {
222
+ mocks.agent.isRunning = true;
223
+ mocks.agent.isSending = false;
224
+
225
+ renderArea('session-1');
226
+
227
+ expect(mocks.controllerParamsSpy).toHaveBeenCalled();
228
+ const params = mocks.controllerParamsSpy.mock.calls[0]?.[0] as {
229
+ agent: { isRunning: boolean; isSending: boolean };
230
+ };
231
+ expect(params.agent.isRunning).toBe(true);
232
+ expect(params.agent.isSending).toBe(false);
200
233
  });
201
234
 
202
235
  it('keeps the composer input subtree stable when only streamed messages change', () => {
@@ -222,4 +255,20 @@ describe('SessionConversationArea input boundary', () => {
222
255
  expect(screen.getByTestId('welcome')).toBeTruthy();
223
256
  expect(screen.getByTestId('conversation-input')).toBeTruthy();
224
257
  });
258
+
259
+ it('surfaces selected-session failure previews at the conversation bottom', () => {
260
+ mocks.inputQuery.selectedSession = {
261
+ activityPreview: {
262
+ state: 'failed',
263
+ statusText: '运行出错:Invalid API Key',
264
+ },
265
+ status: 'idle',
266
+ };
267
+
268
+ renderArea('session-1');
269
+
270
+ expect(screen.getByTestId('conversation-bottom-slot')).toBeTruthy();
271
+ expect(screen.getByText(/出错了|Something went wrong/)).toBeTruthy();
272
+ expect(screen.getByText('运行出错:Invalid API Key')).toBeTruthy();
273
+ });
225
274
  });