@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.
- package/CHANGELOG.md +54 -0
- package/dist/assets/api-DLtsiBKm.js +13 -0
- package/dist/assets/appearance-settings-page-CtkeYXux.js +1 -0
- package/dist/assets/book-open-BQwWBDKb.js +1 -0
- package/dist/assets/channels-list-page-Cv8o9Hxg.js +8 -0
- package/dist/assets/chat-page-BtA7H1Ea.js +1 -0
- package/dist/assets/{config-split-page-CRhlZ_pH.js → config-split-page-EidRBpP2.js} +1 -1
- package/dist/assets/confirm-dialog-iIJFFp2T.js +5 -0
- package/dist/assets/desktop-update-config-C6n_TB1r.js +1 -0
- package/dist/assets/{dist-CikkhOlq.js → dist-CBxWOazz.js} +1 -1
- package/dist/assets/{dist-PVA_xL1D.js → dist-D2eZC3OR.js} +1 -1
- package/dist/assets/doc-browser-B76XOXmW.js +1 -0
- package/dist/assets/doc-browser-DBnj9UMB.js +1 -0
- package/dist/assets/{doc-browser-context-MTw4EvKd.js → doc-browser-context-Sv-044Cl.js} +1 -1
- package/dist/assets/{ellipsis-y6vDtq4M.js → ellipsis-2uzhypw5.js} +1 -1
- package/dist/assets/es2015-DhQo_oS6.js +41 -0
- package/dist/assets/{external-link-DVJIQf_G.js → external-link-CM5dnUI6.js} +1 -1
- package/dist/assets/host-capabilities-9bB_998Z.js +1 -0
- package/dist/assets/i18n-provider-di1waTGX.js +1 -0
- package/dist/assets/index-CBRP-7-a.css +1 -0
- package/dist/assets/index-Z1fYpbNd.js +104 -0
- package/dist/assets/loader-circle-BjmN5DeJ.js +1 -0
- package/dist/assets/mcp-marketplace-page-C_MVQUbT.js +9 -0
- package/dist/assets/mcp-marketplace-page-CoZIiD6G.js +1 -0
- package/dist/assets/model-config-page-Cdr9P6TQ.js +1 -0
- package/dist/assets/plus-3ztRZD_l.js +1 -0
- package/dist/assets/provider-scoped-model-input-g1WkGiJ8.js +1 -0
- package/dist/assets/providers-config-page-CWlO7J4A.js +1 -0
- package/dist/assets/{react-VNqT5Zp7.js → react-DjpzoZXB.js} +1 -1
- package/dist/assets/refresh-cw-DaTv3zxE.js +1 -0
- package/dist/assets/remote-CKWZfVVB.js +1 -0
- package/dist/assets/rotate-cw-CHcaRUDz.js +1 -0
- package/dist/assets/runtime-config-page-CfTqh1nv.js +1 -0
- package/dist/assets/{save-BOJ477H-.js → save-BZof1uie.js} +1 -1
- package/dist/assets/{search-BEti5beA.js → search-MxAarS_g.js} +1 -1
- package/dist/assets/search-config-page-Dm8daYny.js +1 -0
- package/dist/assets/{secrets-config-page--IzrIOid.js → secrets-config-page-D8AbcR05.js} +2 -2
- package/dist/assets/security-config-Ds0LsJmo.js +1 -0
- package/dist/assets/select-D7GcNP0z.js +1 -0
- package/dist/assets/setting-row-CuozlnJd.js +1 -0
- package/dist/assets/side-dock-BcLau5S_.js +3 -0
- package/dist/assets/skeleton-shfmXuYp.js +1 -0
- package/dist/assets/switch-C1BWMU5c.js +1 -0
- package/dist/assets/tag-chip-DCUrQUCP.js +1 -0
- package/dist/assets/tooltip-DfAHHA76.js +1 -0
- package/dist/assets/use-config-DAdbT5QJ.js +1 -0
- package/dist/assets/use-confirm-dialog-eWcJ6V-s.js +1 -0
- package/dist/assets/x-DqL1mYLC.js +1 -0
- package/dist/index.html +53 -17
- package/index.html +25 -2
- package/package.json +9 -9
- package/src/app/components/layout/__tests__/app-layout.test.tsx +27 -0
- package/src/app/components/layout/__tests__/settings-entry-page.test.tsx +9 -1
- package/src/app/components/layout/__tests__/sidebar.layout.test.tsx +6 -3
- package/src/app/components/layout/app-layout.tsx +10 -3
- package/src/app/components/layout/sidebar-items.tsx +8 -8
- package/src/app/components/layout/sidebar-rail.styles.ts +4 -2
- package/src/app/components/layout/sidebar.tsx +21 -28
- package/src/app/configs/app-navigation.config.ts +11 -5
- package/src/app/index.tsx +8 -0
- package/src/app/managers/__tests__/viewport-layout.manager.test.ts +59 -0
- package/src/app/managers/viewport-layout.manager.ts +39 -0
- package/src/app/presenters/app.presenter.ts +25 -2
- package/src/app/styles/design-system.css +91 -4
- package/src/features/chat/components/conversation/__tests__/chat-conversation-content.test.tsx +86 -0
- package/src/features/chat/components/conversation/chat-conversation-content.tsx +47 -17
- package/src/features/chat/components/layout/__tests__/chat-sidebar-read-state.test.tsx +7 -0
- package/src/features/chat/components/layout/chat-sidebar-desktop-layout.tsx +9 -5
- package/src/features/chat/components/layout/chat-sidebar-utility-menu.tsx +5 -5
- package/src/features/chat/features/conversation/components/__tests__/session-conversation-area.test.tsx +51 -2
- package/src/features/chat/features/conversation/components/__tests__/session-conversation-input.streaming.test.tsx +170 -1
- package/src/features/chat/features/conversation/components/session-conversation-area.tsx +23 -1
- package/src/features/chat/features/conversation/components/session-conversation-input.tsx +24 -7
- package/src/features/chat/features/conversation/components/session-queued-input-rows.tsx +96 -0
- package/src/features/chat/features/conversation/hooks/__tests__/use-session-conversation-controller.test.tsx +181 -0
- package/src/features/chat/features/conversation/hooks/use-session-conversation-controller.ts +351 -89
- package/src/features/chat/features/input/hooks/__tests__/use-chat-input-surface-state.test.tsx +29 -4
- package/src/features/chat/features/input/hooks/use-chat-input-surface-state.ts +28 -3
- package/src/features/chat/features/input/input-surface-plugins/__tests__/slash-command-plugin.test.ts +108 -8
- package/src/features/chat/features/input/input-surface-plugins/panel-app-reference-plugin.utils.ts +50 -14
- package/src/features/chat/features/input/input-surface-plugins/slash-command-plugin.utils.ts +77 -6
- package/src/features/chat/features/message/components/__tests__/chat-inline-panel-app-card.test.tsx +21 -12
- package/src/features/chat/features/message/components/__tests__/chat-message-list.container.test.tsx +166 -0
- package/src/features/chat/features/message/components/chat-inline-panel-app-card.tsx +19 -11
- package/src/features/chat/features/message/components/chat-message-list.container.tsx +38 -64
- package/src/features/chat/features/message/utils/__tests__/chat-message-process-summary.utils.test.ts +59 -0
- package/src/features/chat/features/message/utils/__tests__/chat-message-show-content-tool-card.utils.test.ts +56 -1
- package/src/features/chat/features/message/utils/chat-inline-panel-app-card.utils.ts +0 -2
- package/src/features/chat/features/message/utils/chat-message-process-summary.utils.ts +80 -0
- package/src/features/chat/features/message/utils/chat-message-show-content-tool-card.utils.ts +12 -3
- package/src/features/chat/features/message/utils/chat-message-texts.utils.ts +60 -0
- package/src/features/chat/features/message/utils/chat-message.utils.ts +3 -0
- package/src/features/chat/features/ncp/hooks/__tests__/use-ncp-agent-runtime.test.tsx +39 -0
- package/src/features/chat/features/session/components/chat-sidebar-project-groups.tsx +2 -2
- package/src/features/chat/features/session/components/chat-sidebar-session-item.tsx +1 -1
- package/src/features/chat/features/session/components/session-header/__tests__/chat-session-header-actions.test.tsx +3 -1
- package/src/features/chat/features/session/components/session-header/chat-session-metadata-dialog.tsx +1 -1
- package/src/features/chat/features/workspace/components/__tests__/chat-session-workspace-file-preview.test.tsx +270 -115
- package/src/features/chat/features/workspace/components/chat-session-workspace-file-breadcrumb-browser.tsx +121 -0
- package/src/features/chat/features/workspace/components/chat-session-workspace-file-breadcrumbs.tsx +75 -20
- package/src/features/chat/features/workspace/components/chat-session-workspace-file-preview.tsx +70 -5
- package/src/features/chat/features/workspace/utils/chat-workspace-file-tab-persistence.utils.ts +83 -0
- package/src/features/chat/managers/__tests__/chat-thread.manager.test.ts +79 -1
- package/src/features/chat/managers/__tests__/chat-ui.manager.test.ts +2 -0
- package/src/features/chat/managers/chat-thread.manager.ts +9 -3
- package/src/features/chat/managers/chat-ui.manager.ts +5 -1
- package/src/features/chat/presenters/chat.presenter.ts +2 -1
- package/src/features/chat/stores/__tests__/chat-thread.store.test.ts +4 -0
- package/src/features/chat/stores/chat-thread.store.ts +6 -64
- package/src/features/chat/types/chat-message.types.ts +4 -0
- package/src/features/marketplace/components/__tests__/marketplace-page-detail.test.tsx +33 -26
- package/src/features/marketplace/components/__tests__/marketplace-page.test.tsx +1 -0
- package/src/features/marketplace/components/curated-shelves/__tests__/marketplace-curated-scene-route.test.tsx +1 -0
- package/src/features/marketplace/components/detail-doc/__tests__/marketplace-detail-doc.test.tsx +115 -0
- package/src/features/marketplace/components/marketplace-detail-doc.tsx +441 -0
- package/src/features/marketplace/components/mcp/__tests__/mcp-marketplace-page.test.tsx +3 -3
- package/src/features/marketplace/components/mcp/mcp-marketplace-doc.ts +0 -60
- package/src/features/marketplace/components/mcp/mcp-marketplace-page.tsx +64 -29
- package/src/features/marketplace/hooks/use-marketplace-item-detail.ts +42 -31
- package/src/features/marketplace/index.ts +3 -0
- package/src/features/marketplace/stores/marketplace-detail-doc.store.ts +38 -0
- package/src/features/panel-apps/components/__tests__/panel-apps-list.test.tsx +32 -2
- package/src/features/pwa/managers/__tests__/pwa-shell-theme.manager.test.ts +11 -0
- package/src/features/pwa/managers/pwa-shell-theme.manager.ts +8 -3
- package/src/features/settings/pages/__tests__/appearance-settings-page.test.tsx +25 -0
- package/src/features/settings/pages/appearance-settings-page.tsx +40 -0
- package/src/features/side-dock/components/__tests__/side-dock.test.tsx +44 -2
- package/src/features/side-dock/components/side-dock.tsx +100 -13
- package/src/features/side-dock/configs/side-dock-built-in-items.config.ts +10 -0
- package/src/features/side-dock/managers/__tests__/side-dock.manager.test.ts +32 -2
- package/src/features/side-dock/managers/side-dock.manager.ts +17 -7
- package/src/features/side-dock/stores/side-dock.store.ts +9 -0
- package/src/features/side-dock/types/side-dock.types.ts +10 -3
- package/src/features/side-dock/utils/side-dock-item.utils.ts +15 -2
- package/src/index.css +501 -27
- package/src/platforms/desktop/components/desktop-window-chrome.tsx +1 -1
- package/src/shared/components/doc-browser/__tests__/doc-browser-context.test.tsx +30 -1
- package/src/shared/components/doc-browser/__tests__/doc-browser.test.tsx +206 -38
- package/src/shared/components/doc-browser/doc-browser-panel-parts.tsx +34 -11
- package/src/shared/components/doc-browser/doc-browser-renderer.types.ts +1 -1
- package/src/shared/components/doc-browser/doc-browser-tab-strip.tsx +23 -5
- package/src/shared/components/doc-browser/doc-browser.tsx +128 -75
- package/src/shared/components/doc-browser/managers/doc-browser.manager.ts +9 -1
- package/src/shared/components/doc-browser/utils/doc-browser-floating-panel.utils.ts +83 -0
- package/src/shared/components/ui/__tests__/dialog.test.tsx +27 -0
- package/src/shared/components/ui/actions/icon-action-button.tsx +14 -2
- package/src/shared/components/ui/dialog.tsx +2 -2
- package/src/shared/components/ui/popover.tsx +1 -1
- package/src/shared/components/ui/select.tsx +1 -1
- package/src/shared/components/ui/switch.tsx +4 -3
- package/src/shared/components/ui/tab-strip/__tests__/compact-tab-strip.test.tsx +54 -0
- package/src/shared/components/ui/tab-strip/compact-tab-strip.tsx +4 -5
- package/src/shared/components/ui/tooltip.tsx +1 -1
- package/src/shared/lib/api/utils/server-path.utils.ts +27 -1
- package/src/shared/lib/i18n/locales/en-US/chat.json +15 -6
- package/src/shared/lib/i18n/locales/en-US/core.json +2 -0
- package/src/shared/lib/i18n/locales/en-US/doc-browser.json +11 -0
- package/src/shared/lib/i18n/locales/en-US/marketplace.json +5 -0
- package/src/shared/lib/i18n/locales/zh-CN/chat.json +15 -6
- package/src/shared/lib/i18n/locales/zh-CN/core.json +2 -0
- package/src/shared/lib/i18n/locales/zh-CN/doc-browser.json +11 -0
- package/src/shared/lib/i18n/locales/zh-CN/marketplace.json +5 -0
- package/src/shared/lib/session-project/__tests__/workspace-file-breadcrumb.test.ts +14 -0
- package/src/shared/lib/session-project/index.ts +1 -1
- package/src/shared/lib/session-project/{workspace-file-breadcrumb.ts → workspace-file-breadcrumb.utils.ts} +62 -8
- package/src/shared/lib/theme/index.ts +51 -13
- package/dist/assets/api-BP_8IaxB.js +0 -13
- package/dist/assets/channels-list-page-DEh5Jjqx.js +0 -8
- package/dist/assets/chat-page-BRB0N5WW.js +0 -105
- package/dist/assets/confirm-dialog-DsivFJ4r.js +0 -5
- package/dist/assets/desktop-update-config-B0w9p2ep.js +0 -1
- package/dist/assets/doc-browser-BoNJ7-tE.js +0 -1
- package/dist/assets/doc-browser-CdJ_d4ik.js +0 -1
- package/dist/assets/index-BiE-K8-q.css +0 -1
- package/dist/assets/index-D9BAE_L_.js +0 -105
- package/dist/assets/mcp-marketplace-page-B1SZNY5m.js +0 -40
- package/dist/assets/mcp-marketplace-page-Cq_Tw42T.js +0 -1
- package/dist/assets/model-config-page-DotaTUHb.js +0 -1
- package/dist/assets/plus-BbZqquJe.js +0 -1
- package/dist/assets/provider-scoped-model-input-DsFRuWJH.js +0 -1
- package/dist/assets/providers-config-page-DW4tQSLN.js +0 -1
- package/dist/assets/remote-B8TiUfwW.js +0 -1
- package/dist/assets/rotate-cw-DjHDTkCv.js +0 -1
- package/dist/assets/runtime-config-page-S6ErRHR8.js +0 -1
- package/dist/assets/search-config-page-DGj5am03.js +0 -1
- package/dist/assets/select-hoj8dK4K.js +0 -41
- package/dist/assets/tag-chip-T8-h-die.js +0 -1
- package/dist/assets/use-config-CjRV2hwL.js +0 -1
- package/dist/assets/x-D9PXs_xP.js +0 -1
- package/src/features/marketplace/components/detail-doc/__tests__/marketplace-detail-doc.test.ts +0 -40
- package/src/features/marketplace/components/detail-doc/marketplace-detail-doc-renderer.ts +0 -201
- package/src/features/marketplace/components/marketplace-detail-doc.ts +0 -142
- /package/dist/assets/{config-hints-BkYBc7z0.js → config-hints-fGnUjDe9.js} +0 -0
- /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-
|
|
204
|
-
--z-modal:
|
|
205
|
-
--z-
|
|
206
|
-
--z-
|
|
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
|
/* ========================================
|
package/src/features/chat/components/conversation/__tests__/chat-conversation-content.test.tsx
ADDED
|
@@ -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 {
|
|
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
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
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
|
|
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
|
-
"
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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: () =>
|
|
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
|
});
|