@nextclaw/ui 0.14.2 → 0.14.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +12 -0
- package/dist/assets/{api-a14tR_z1.js → api-BP_8IaxB.js} +3 -3
- package/dist/assets/{channels-list-page-CFGkm4DT.js → channels-list-page-DEh5Jjqx.js} +1 -1
- package/dist/assets/chat-page-BRB0N5WW.js +105 -0
- package/dist/assets/config-split-page-CRhlZ_pH.js +1 -0
- package/dist/assets/{confirm-dialog-ClH_Ec7n.js → confirm-dialog-DsivFJ4r.js} +2 -2
- package/dist/assets/{desktop-update-config-DPNDnVpk.js → desktop-update-config-B0w9p2ep.js} +1 -1
- package/dist/assets/{dist-BtCS2Ucq.js → dist-CikkhOlq.js} +1 -1
- package/dist/assets/{dist-CLGGLm8b.js → dist-PVA_xL1D.js} +1 -1
- package/dist/assets/doc-browser-BoNJ7-tE.js +1 -0
- package/dist/assets/doc-browser-CdJ_d4ik.js +1 -0
- package/dist/assets/{doc-browser-context-D4JwMMxK.js → doc-browser-context-MTw4EvKd.js} +1 -1
- package/dist/assets/{ellipsis-CtJXjIol.js → ellipsis-y6vDtq4M.js} +1 -1
- package/dist/assets/{external-link-CdJiG13p.js → external-link-DVJIQf_G.js} +1 -1
- package/dist/assets/index-BiE-K8-q.css +1 -0
- package/dist/assets/index-D9BAE_L_.js +105 -0
- package/dist/assets/{mcp-marketplace-page-BU0kIjhR.js → mcp-marketplace-page-B1SZNY5m.js} +1 -1
- package/dist/assets/mcp-marketplace-page-Cq_Tw42T.js +1 -0
- package/dist/assets/{model-config-page-D6hYXROd.js → model-config-page-DotaTUHb.js} +1 -1
- package/dist/assets/plus-BbZqquJe.js +1 -0
- package/dist/assets/{provider-scoped-model-input-DAX2HgmR.js → provider-scoped-model-input-DsFRuWJH.js} +1 -1
- package/dist/assets/providers-config-page-DW4tQSLN.js +1 -0
- package/dist/assets/{react-C6cZgP44.js → react-VNqT5Zp7.js} +1 -1
- package/dist/assets/remote-B8TiUfwW.js +1 -0
- package/dist/assets/rotate-cw-DjHDTkCv.js +1 -0
- package/dist/assets/{runtime-config-page-C5xKaV3i.js → runtime-config-page-S6ErRHR8.js} +1 -1
- package/dist/assets/{save-DKA0x4zk.js → save-BOJ477H-.js} +1 -1
- package/dist/assets/{search-BNlcoj5c.js → search-BEti5beA.js} +1 -1
- package/dist/assets/{search-config-page-Cq9uJ-N8.js → search-config-page-DGj5am03.js} +1 -1
- package/dist/assets/{secrets-config-page-CxSbZLBo.js → secrets-config-page--IzrIOid.js} +1 -1
- package/dist/assets/{select-CvTlDZTQ.js → select-hoj8dK4K.js} +2 -2
- package/dist/assets/{tag-chip-8ffF2y2C.js → tag-chip-T8-h-die.js} +1 -1
- package/dist/assets/{use-config-DmT0o1gv.js → use-config-CjRV2hwL.js} +1 -1
- package/dist/assets/x-D9PXs_xP.js +1 -0
- package/dist/index.html +69 -19
- package/index.html +55 -5
- package/package.json +7 -7
- package/src/app/components/layout/__tests__/sidebar.layout.test.tsx +3 -3
- package/src/app/components/layout/header.tsx +3 -3
- package/src/app/components/layout/page-layout.tsx +2 -2
- package/src/app/components/layout/runtime-status-entry.tsx +7 -7
- package/src/app/components/layout/sidebar-items.tsx +10 -10
- package/src/app/components/layout/sidebar-rail.styles.ts +2 -2
- package/src/app/components/layout/sidebar.tsx +6 -6
- package/src/app/styles/design-system.css +388 -26
- package/src/app/test/vitest-setup.ts +31 -0
- package/src/features/chat/components/chat-sidebar-list-mode-switch.tsx +3 -3
- package/src/features/chat/components/conversation/__tests__/chat-conversation-header.test.tsx +4 -2
- package/src/features/chat/components/conversation/chat-conversation-header-section.tsx +1 -1
- package/src/features/chat/components/conversation/chat-conversation-header.tsx +7 -6
- package/src/features/chat/components/conversation/chat-conversation-panel.tsx +1 -1
- package/src/features/chat/components/conversation/chat-conversation-skeleton.tsx +13 -13
- package/src/features/chat/components/layout/__tests__/chat-sidebar.test.tsx +2 -2
- package/src/features/chat/components/layout/chat-page-shell.tsx +1 -1
- package/src/features/chat/components/layout/chat-sidebar-desktop-layout.tsx +3 -3
- package/src/features/chat/components/layout/chat-sidebar-toolbar.tsx +14 -14
- package/src/features/chat/components/layout/chat-sidebar-utility-menu.tsx +15 -15
- package/src/features/chat/components/layout/chat-sidebar.tsx +3 -3
- package/src/features/chat/features/conversation/components/__tests__/session-conversation-area.test.tsx +225 -0
- package/src/features/chat/features/conversation/components/__tests__/session-conversation-input.streaming.test.tsx +199 -0
- package/src/features/chat/features/conversation/components/session-conversation-area.tsx +28 -9
- package/src/features/chat/features/conversation/components/session-conversation-input.tsx +18 -6
- package/src/features/chat/features/conversation/hooks/use-session-conversation-controller.ts +3 -13
- package/src/features/chat/features/conversation/hooks/use-session-conversation-input-query.ts +19 -3
- package/src/features/chat/features/message/components/chat-inline-panel-app-card.tsx +7 -7
- package/src/features/chat/features/message/components/chat-message-list.container.tsx +6 -6
- package/src/features/chat/features/session/components/chat-sidebar-project-groups.tsx +5 -5
- package/src/features/chat/features/session/components/chat-sidebar-session-list.tsx +4 -4
- package/src/features/chat/features/session/components/session-context-icon.tsx +2 -2
- package/src/features/chat/features/session/components/session-header/__tests__/chat-session-project-badge.test.tsx +2 -2
- package/src/features/chat/features/session/components/session-header/chat-session-header-actions.tsx +1 -1
- package/src/features/chat/features/session/components/session-header/chat-session-header-menu-item.tsx +1 -1
- package/src/features/chat/features/session/components/session-header/chat-session-metadata-dialog.tsx +1 -1
- package/src/features/chat/features/session/components/session-header/chat-session-project-badge.tsx +3 -3
- package/src/features/chat/features/session/components/session-run-badge.tsx +1 -1
- package/src/features/chat/features/session-type/hooks/use-chat-session-type-state.ts +10 -2
- package/src/features/chat/features/welcome/components/__tests__/chat-welcome.test.tsx +0 -7
- package/src/features/chat/features/welcome/components/chat-welcome-agent-picker.tsx +3 -3
- package/src/features/chat/features/welcome/components/chat-welcome-capability-grid.tsx +4 -4
- package/src/features/chat/features/welcome/components/chat-welcome-project-picker.tsx +14 -19
- package/src/features/chat/features/welcome/components/chat-welcome-session-type-picker.tsx +3 -3
- package/src/features/chat/features/welcome/components/chat-welcome.tsx +4 -4
- package/src/features/chat/features/welcome/utils/__tests__/chat-welcome-project-options.utils.test.ts +6 -8
- package/src/features/chat/features/welcome/utils/chat-welcome-project-options.utils.ts +1 -16
- package/src/features/panel-apps/components/panel-app-list-item.tsx +8 -8
- package/src/features/panel-apps/components/panel-app-toolbar.tsx +4 -4
- package/src/features/panel-apps/components/panel-apps-list.tsx +20 -20
- package/src/features/pwa/managers/__tests__/pwa-shell-theme.manager.test.ts +70 -6
- package/src/features/pwa/managers/pwa-shell-theme.manager.ts +10 -5
- package/src/features/service-apps/components/service-action-authorization-dialog.tsx +8 -8
- package/src/features/service-apps/components/service-apps-panel.tsx +24 -24
- package/src/features/settings/components/config/provider-advanced-settings-section.tsx +5 -5
- package/src/features/settings/components/config/provider-auth-section.tsx +7 -7
- package/src/features/settings/components/config/provider-form.tsx +10 -10
- package/src/features/settings/components/config/provider-models-section.tsx +16 -16
- package/src/features/settings/components/config/provider-pill-selector.tsx +1 -1
- package/src/features/settings/pages/providers-config-page.tsx +17 -17
- package/src/shared/components/config-split-page.tsx +11 -11
- package/src/shared/components/doc-browser/doc-browser-home-page.tsx +2 -2
- package/src/shared/components/doc-browser/doc-browser-panel-parts.tsx +4 -4
- package/src/shared/components/doc-browser/doc-browser-tab-strip.tsx +4 -4
- package/src/shared/components/doc-browser/doc-browser.tsx +3 -3
- package/src/shared/components/resizable-right-panel/resizable-right-panel.tsx +1 -1
- package/src/shared/components/ui/actions/icon-action-button.tsx +1 -1
- package/src/shared/components/ui/button.tsx +4 -4
- package/src/shared/components/ui/card.tsx +4 -4
- package/src/shared/components/ui/dialog.tsx +5 -5
- package/src/shared/components/ui/input.tsx +1 -1
- package/src/shared/components/ui/label.tsx +1 -1
- package/src/shared/components/ui/popover.tsx +1 -1
- package/src/shared/components/ui/select.tsx +3 -3
- package/src/shared/components/ui/switch.tsx +3 -3
- package/src/shared/components/ui/tabs.tsx +4 -4
- package/src/shared/components/ui/textarea.tsx +1 -1
- package/src/shared/lib/i18n/locales/en-US/core.json +7 -2
- package/src/shared/lib/i18n/locales/zh-CN/core.json +7 -2
- package/src/shared/lib/theme/index.ts +94 -52
- package/dist/assets/chat-page-CEdavJXc.js +0 -105
- package/dist/assets/config-split-page-B5b9B0uE.js +0 -1
- package/dist/assets/doc-browser-5nMWYKBD.js +0 -1
- package/dist/assets/doc-browser-BJRLhztv.js +0 -1
- package/dist/assets/index-BDN1zzMQ.js +0 -105
- package/dist/assets/index-g3uSZXoc.css +0 -1
- package/dist/assets/mcp-marketplace-page-sMNFSq_T.js +0 -1
- package/dist/assets/plus-DPjb0piO.js +0 -1
- package/dist/assets/providers-config-page-BzAeaACA.js +0 -1
- package/dist/assets/remote-B0G80uPr.js +0 -1
- package/dist/assets/rotate-cw-DnEB1Rju.js +0 -1
- package/dist/assets/x-BiSSpgqB.js +0 -1
|
@@ -366,9 +366,9 @@ describe("ChatSidebar create and list basics", () => {
|
|
|
366
366
|
|
|
367
367
|
fireEvent.click(screen.getByRole("button", { name: "Settings menu" }));
|
|
368
368
|
fireEvent.click(screen.getByRole("combobox", { name: "Theme" }));
|
|
369
|
-
fireEvent.click(screen.getByRole("option", { name: "
|
|
369
|
+
fireEvent.click(screen.getByRole("option", { name: "Probe" }));
|
|
370
370
|
|
|
371
|
-
expect(mocks.setTheme).toHaveBeenCalledWith("
|
|
371
|
+
expect(mocks.setTheme).toHaveBeenCalledWith("probe");
|
|
372
372
|
});
|
|
373
373
|
|
|
374
374
|
it("creates the default session directly from the compact mobile add button when no menu is needed", () => {
|
|
@@ -50,7 +50,7 @@ export function ChatPageLayout({ view, confirmDialog }: ChatPageLayoutProps) {
|
|
|
50
50
|
{view === "chat" ? (
|
|
51
51
|
isMobile ? <ChatMobileShell /> : <ChatConversationPanel />
|
|
52
52
|
) : (
|
|
53
|
-
<section className="flex-1 min-h-0 overflow-hidden bg-
|
|
53
|
+
<section className="flex-1 min-h-0 overflow-hidden bg-background">
|
|
54
54
|
{view === "cron" ? (
|
|
55
55
|
<div className="h-full overflow-auto custom-scrollbar">
|
|
56
56
|
<div className="mx-auto w-full max-w-[min(1120px,100%)] px-4 py-4 sm:px-6 sm:py-5">
|
|
@@ -121,7 +121,7 @@ export function ChatSidebarDesktopNav({
|
|
|
121
121
|
</div>
|
|
122
122
|
<div
|
|
123
123
|
className={cn(
|
|
124
|
-
"border-t border-
|
|
124
|
+
"border-t border-border/70",
|
|
125
125
|
isCollapsed ? "mx-2 my-1.5" : "mx-4",
|
|
126
126
|
)}
|
|
127
127
|
/>
|
|
@@ -161,7 +161,7 @@ export function ChatSidebarSessionArea({
|
|
|
161
161
|
return (
|
|
162
162
|
<>
|
|
163
163
|
<div className="flex items-center justify-between px-5 pb-2 pt-3">
|
|
164
|
-
<div className="text-[11px] font-medium uppercase tracking-wider text-
|
|
164
|
+
<div className="text-[11px] font-medium uppercase tracking-wider text-muted-foreground/75">
|
|
165
165
|
{t("chatSidebarTaskRecords")}
|
|
166
166
|
</div>
|
|
167
167
|
<ChatSidebarListModeSwitch
|
|
@@ -221,7 +221,7 @@ export function ChatSidebarDesktopFooter({
|
|
|
221
221
|
return (
|
|
222
222
|
<div
|
|
223
223
|
className={cn(
|
|
224
|
-
"border-t border-
|
|
224
|
+
"border-t border-border/70 py-3",
|
|
225
225
|
isCollapsed
|
|
226
226
|
? cn("flex justify-center", SIDEBAR_RAIL_PADDING_X_CLASS)
|
|
227
227
|
: "px-3",
|
|
@@ -41,23 +41,23 @@ const NEW_SESSION_ACTION_STYLE_CLASSES: Record<
|
|
|
41
41
|
neutralSurface: {
|
|
42
42
|
leftVariant: "ghost",
|
|
43
43
|
leftClassName:
|
|
44
|
-
"bg-
|
|
44
|
+
"bg-card text-foreground shadow-none transition-[background-color,color,box-shadow] hover:bg-accent hover:text-accent-foreground hover:shadow-sm active:bg-accent",
|
|
45
45
|
rightClassName:
|
|
46
|
-
"bg-
|
|
46
|
+
"bg-card text-muted-foreground shadow-none transition-[background-color,color,box-shadow] hover:bg-accent hover:text-accent-foreground hover:shadow-sm active:bg-accent",
|
|
47
47
|
},
|
|
48
48
|
brandSoft: {
|
|
49
49
|
leftVariant: "ghost",
|
|
50
50
|
leftClassName:
|
|
51
|
-
"bg-primary/10 text-primary shadow-none transition-[background-color,color,box-shadow] hover:bg-primary/15 hover:text-primary-700 hover:shadow-sm active:bg-primary/20",
|
|
51
|
+
"bg-primary/10 text-primary shadow-none ring-1 ring-primary/10 transition-[background-color,color,box-shadow] hover:bg-primary/15 hover:text-primary-700 hover:shadow-sm active:bg-primary/20",
|
|
52
52
|
rightClassName:
|
|
53
|
-
"bg-primary/10 text-primary shadow-none transition-[background-color,color,box-shadow] hover:bg-primary/15 hover:text-primary-700 hover:shadow-sm active:bg-primary/20",
|
|
53
|
+
"bg-primary/10 text-primary shadow-none ring-1 ring-primary/10 transition-[background-color,color,box-shadow] hover:bg-primary/15 hover:text-primary-700 hover:shadow-sm active:bg-primary/20",
|
|
54
54
|
},
|
|
55
55
|
brandTextSurface: {
|
|
56
56
|
leftVariant: "ghost",
|
|
57
57
|
leftClassName:
|
|
58
|
-
"bg-
|
|
58
|
+
"bg-card text-primary shadow-none transition-[background-color,color,box-shadow] hover:bg-accent hover:text-primary-700 hover:shadow-sm active:bg-primary/15",
|
|
59
59
|
rightClassName:
|
|
60
|
-
"bg-
|
|
60
|
+
"bg-card text-primary shadow-none transition-[background-color,color,box-shadow] hover:bg-accent hover:text-primary-700 hover:shadow-sm active:bg-primary/15",
|
|
61
61
|
},
|
|
62
62
|
brandSolid: {
|
|
63
63
|
leftVariant: "primary",
|
|
@@ -179,7 +179,7 @@ export function ChatSidebarDesktopToolbar(props: ChatSidebarToolbarProps) {
|
|
|
179
179
|
<PopoverContent
|
|
180
180
|
align="start"
|
|
181
181
|
side="right"
|
|
182
|
-
className="w-56 rounded-2xl border border-
|
|
182
|
+
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)]"
|
|
183
183
|
>
|
|
184
184
|
<ChatSessionTypeMenu
|
|
185
185
|
options={sessionTypeOptions}
|
|
@@ -241,7 +241,7 @@ export function ChatSidebarDesktopToolbar(props: ChatSidebarToolbarProps) {
|
|
|
241
241
|
</PopoverTrigger>
|
|
242
242
|
<PopoverContent
|
|
243
243
|
align="end"
|
|
244
|
-
className="w-56 rounded-2xl border border-
|
|
244
|
+
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)]"
|
|
245
245
|
>
|
|
246
246
|
<ChatSessionTypeMenu
|
|
247
247
|
options={sessionTypeOptions}
|
|
@@ -259,7 +259,7 @@ export function ChatSidebarDesktopToolbar(props: ChatSidebarToolbarProps) {
|
|
|
259
259
|
|
|
260
260
|
<div className="px-4 pb-3">
|
|
261
261
|
<div className="relative">
|
|
262
|
-
<Search className="absolute left-3 top-2.5 h-3.5 w-3.5 text-
|
|
262
|
+
<Search className="absolute left-3 top-2.5 h-3.5 w-3.5 text-muted-foreground/70" />
|
|
263
263
|
<Input
|
|
264
264
|
value={query}
|
|
265
265
|
onChange={(event) => onQueryChange(event.target.value)}
|
|
@@ -294,12 +294,12 @@ export function ChatSidebarMobileToolbar(props: ChatSidebarToolbarProps) {
|
|
|
294
294
|
<div className="px-4 pb-2 pt-1">
|
|
295
295
|
<div className="flex items-center gap-2">
|
|
296
296
|
<div className="relative min-w-0 flex-1">
|
|
297
|
-
<Search className="absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-
|
|
297
|
+
<Search className="absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-muted-foreground/70" />
|
|
298
298
|
<Input
|
|
299
299
|
value={query}
|
|
300
300
|
onChange={(event) => onQueryChange(event.target.value)}
|
|
301
301
|
placeholder={t("chatSidebarSearchPlaceholder")}
|
|
302
|
-
className="h-9 rounded-full border-transparent bg-
|
|
302
|
+
className="h-9 rounded-full border-transparent bg-muted pl-9 pr-3 text-[13px] shadow-none focus:border-border focus:bg-card"
|
|
303
303
|
/>
|
|
304
304
|
</div>
|
|
305
305
|
|
|
@@ -313,12 +313,12 @@ export function ChatSidebarMobileToolbar(props: ChatSidebarToolbarProps) {
|
|
|
313
313
|
icon={<Plus className="h-4 w-4" />}
|
|
314
314
|
label={t("chatSidebarNewTask")}
|
|
315
315
|
tooltip={false}
|
|
316
|
-
className="h-9 w-9 shrink-0 rounded-full bg-
|
|
316
|
+
className="h-9 w-9 shrink-0 rounded-full bg-muted text-foreground shadow-none hover:bg-accent hover:text-accent-foreground"
|
|
317
317
|
/>
|
|
318
318
|
</PopoverTrigger>
|
|
319
319
|
<PopoverContent
|
|
320
320
|
align="end"
|
|
321
|
-
className="w-60 rounded-3xl border border-
|
|
321
|
+
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)]"
|
|
322
322
|
>
|
|
323
323
|
<ChatSessionTypeMenu
|
|
324
324
|
options={createOptions}
|
|
@@ -336,7 +336,7 @@ export function ChatSidebarMobileToolbar(props: ChatSidebarToolbarProps) {
|
|
|
336
336
|
<IconActionButton
|
|
337
337
|
icon={<Plus className="h-4 w-4" />}
|
|
338
338
|
label={t("chatSidebarNewTask")}
|
|
339
|
-
className="h-9 w-9 shrink-0 rounded-full bg-
|
|
339
|
+
className="h-9 w-9 shrink-0 rounded-full bg-muted text-foreground shadow-none hover:bg-accent hover:text-accent-foreground"
|
|
340
340
|
onClick={() => onCreateSession(defaultSessionType)}
|
|
341
341
|
/>
|
|
342
342
|
)}
|
|
@@ -99,7 +99,7 @@ export function ChatSidebarUtilityMenu({
|
|
|
99
99
|
<Settings
|
|
100
100
|
className={cn(
|
|
101
101
|
SIDEBAR_RAIL_ICON_CLASS,
|
|
102
|
-
"shrink-0 text-
|
|
102
|
+
"shrink-0 text-muted-foreground",
|
|
103
103
|
)}
|
|
104
104
|
/>
|
|
105
105
|
</button>
|
|
@@ -115,11 +115,11 @@ 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-
|
|
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"
|
|
119
119
|
>
|
|
120
|
-
<Settings className="h-4 w-4 shrink-0 text-
|
|
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>
|
|
122
|
-
<span className="max-w-[112px] truncate text-[13px] text-
|
|
122
|
+
<span className="max-w-[112px] truncate text-[13px] text-muted-foreground">
|
|
123
123
|
{currentThemeLabel} / {currentLanguageLabel}
|
|
124
124
|
</span>
|
|
125
125
|
</button>
|
|
@@ -134,30 +134,30 @@ 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-
|
|
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"
|
|
138
138
|
>
|
|
139
|
-
<Settings className="h-4 w-4 text-
|
|
139
|
+
<Settings className="h-4 w-4 text-muted-foreground/70" />
|
|
140
140
|
<span className="flex-1 text-left">{t("settings")}</span>
|
|
141
141
|
</NavLink>
|
|
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-
|
|
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"
|
|
146
146
|
>
|
|
147
|
-
<BookOpen className="h-4 w-4 text-
|
|
147
|
+
<BookOpen className="h-4 w-4 text-muted-foreground/70" />
|
|
148
148
|
<span className="flex-1 text-left">{t("docBrowserHelp")}</span>
|
|
149
149
|
</button>
|
|
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-
|
|
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"
|
|
154
154
|
>
|
|
155
|
-
<Boxes className="h-4 w-4 text-
|
|
155
|
+
<Boxes className="h-4 w-4 text-muted-foreground/70" />
|
|
156
156
|
<span className="flex-1 text-left">{t("appsTitle")}</span>
|
|
157
157
|
</button>
|
|
158
158
|
</div>
|
|
159
159
|
|
|
160
|
-
<div className="my-2 h-px bg-
|
|
160
|
+
<div className="my-2 h-px bg-border" />
|
|
161
161
|
|
|
162
162
|
<ChatSidebarUtilitySelect
|
|
163
163
|
icon={Palette}
|
|
@@ -212,14 +212,14 @@ 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-
|
|
216
|
-
indicator={<ChevronRight className="h-4 w-4 text-
|
|
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"
|
|
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">
|
|
219
|
-
<Icon className="h-4 w-4 shrink-0 text-
|
|
219
|
+
<Icon className="h-4 w-4 shrink-0 text-muted-foreground/70" />
|
|
220
220
|
<span className="text-left">{label}</span>
|
|
221
221
|
</div>
|
|
222
|
-
<span className="ml-auto max-w-[96px] truncate text-[13px] text-
|
|
222
|
+
<span className="ml-auto max-w-[96px] truncate text-[13px] text-muted-foreground">
|
|
223
223
|
{valueLabel}
|
|
224
224
|
</span>
|
|
225
225
|
</SelectTrigger>
|
|
@@ -105,7 +105,7 @@ export function ChatSidebar({
|
|
|
105
105
|
const { language, setLanguage } = useI18n();
|
|
106
106
|
const { theme, setTheme } = useTheme();
|
|
107
107
|
const currentThemeLabel = t(
|
|
108
|
-
THEME_OPTIONS.find((o) => o.value === theme)?.labelKey ?? "
|
|
108
|
+
THEME_OPTIONS.find((o) => o.value === theme)?.labelKey ?? "themeNatural",
|
|
109
109
|
);
|
|
110
110
|
const currentLanguageLabel =
|
|
111
111
|
LANGUAGE_OPTIONS.find((o) => o.value === language)?.label ?? language;
|
|
@@ -233,9 +233,9 @@ export function ChatSidebar({
|
|
|
233
233
|
: shouldCollapse
|
|
234
234
|
? cn(
|
|
235
235
|
SIDEBAR_RAIL_WIDTH_CLASS,
|
|
236
|
-
"shrink-0 border-r border-
|
|
236
|
+
"shrink-0 border-r border-border/70",
|
|
237
237
|
)
|
|
238
|
-
: "w-[280px] shrink-0 border-r border-
|
|
238
|
+
: "w-[280px] shrink-0 border-r border-border/70",
|
|
239
239
|
)}
|
|
240
240
|
data-sidebar-collapsed={shouldCollapse ? "true" : "false"}
|
|
241
241
|
>
|
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
import { memo, type ReactNode } from 'react';
|
|
2
|
+
import { render, screen } from '@testing-library/react';
|
|
3
|
+
import { MemoryRouter } from 'react-router-dom';
|
|
4
|
+
import { describe, expect, it, vi, beforeEach } from 'vitest';
|
|
5
|
+
|
|
6
|
+
import { SessionConversationArea } from '@/features/chat/features/conversation/components/session-conversation-area';
|
|
7
|
+
|
|
8
|
+
const mocks = vi.hoisted(() => {
|
|
9
|
+
const inputRenderSpy = vi.fn();
|
|
10
|
+
const inputActions = {
|
|
11
|
+
update: vi.fn(),
|
|
12
|
+
syncComposer: vi.fn(),
|
|
13
|
+
resetComposer: vi.fn(),
|
|
14
|
+
restoreComposer: vi.fn(),
|
|
15
|
+
applyPromptSuggestion: vi.fn(),
|
|
16
|
+
requestComposerFocusAtEnd: vi.fn(),
|
|
17
|
+
consumeComposerFocusRequest: vi.fn(),
|
|
18
|
+
setAttachments: vi.fn(),
|
|
19
|
+
addAttachments: vi.fn(() => []),
|
|
20
|
+
removeAttachment: vi.fn(),
|
|
21
|
+
setSelectedModel: vi.fn(),
|
|
22
|
+
setSelectedThinkingLevel: vi.fn(),
|
|
23
|
+
setPendingSessionType: vi.fn(),
|
|
24
|
+
setPendingProjectRoot: vi.fn(),
|
|
25
|
+
setSelectedSkills: vi.fn(),
|
|
26
|
+
setSendError: vi.fn(),
|
|
27
|
+
};
|
|
28
|
+
const inputSnapshot = {
|
|
29
|
+
text: '',
|
|
30
|
+
nodes: [],
|
|
31
|
+
selectedSkills: [],
|
|
32
|
+
skillRecords: [],
|
|
33
|
+
attachments: [],
|
|
34
|
+
selectedModel: undefined,
|
|
35
|
+
selectedThinkingLevel: null,
|
|
36
|
+
pendingSessionType: 'default',
|
|
37
|
+
selectedSessionType: 'default',
|
|
38
|
+
pendingProjectRoot: null,
|
|
39
|
+
composerFocusRequestId: 0,
|
|
40
|
+
sendError: null,
|
|
41
|
+
};
|
|
42
|
+
const inputQuery = {
|
|
43
|
+
defaultModel: undefined,
|
|
44
|
+
defaultProjectRoot: null,
|
|
45
|
+
fallbackPreferredModel: undefined,
|
|
46
|
+
fallbackPreferredThinking: null,
|
|
47
|
+
isProviderStateResolved: true,
|
|
48
|
+
isSkillsLoading: false,
|
|
49
|
+
modelOptions: [],
|
|
50
|
+
selectedSession: null,
|
|
51
|
+
selectedSessionKey: null,
|
|
52
|
+
sessionTypeState: {
|
|
53
|
+
sessionTypeOptions: [],
|
|
54
|
+
selectedSessionTypeOption: null,
|
|
55
|
+
defaultSessionType: 'default',
|
|
56
|
+
selectedSessionType: 'default',
|
|
57
|
+
canEditSessionType: true,
|
|
58
|
+
sessionTypeUnavailable: false,
|
|
59
|
+
sessionTypeUnavailableMessage: null,
|
|
60
|
+
},
|
|
61
|
+
skillRecords: [],
|
|
62
|
+
};
|
|
63
|
+
const controller = {
|
|
64
|
+
canStopGeneration: true,
|
|
65
|
+
hasSendableDraft: true,
|
|
66
|
+
isSending: true,
|
|
67
|
+
send: vi.fn(),
|
|
68
|
+
sendDisabled: true,
|
|
69
|
+
stop: vi.fn(),
|
|
70
|
+
stopDisabled: false,
|
|
71
|
+
};
|
|
72
|
+
const agent = {
|
|
73
|
+
visibleMessages: [] as unknown[],
|
|
74
|
+
isHydrating: false,
|
|
75
|
+
isRunning: true,
|
|
76
|
+
isSending: true,
|
|
77
|
+
hydrateError: null,
|
|
78
|
+
snapshot: {
|
|
79
|
+
activeRun: null,
|
|
80
|
+
contextWindow: null,
|
|
81
|
+
error: null,
|
|
82
|
+
},
|
|
83
|
+
send: vi.fn(),
|
|
84
|
+
abort: vi.fn(),
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
return {
|
|
88
|
+
agent,
|
|
89
|
+
controller,
|
|
90
|
+
inputActions,
|
|
91
|
+
inputQuery,
|
|
92
|
+
inputRenderSpy,
|
|
93
|
+
inputSnapshot,
|
|
94
|
+
presenter: {
|
|
95
|
+
chatUiManager: {
|
|
96
|
+
goToProviders: vi.fn(),
|
|
97
|
+
},
|
|
98
|
+
chatSessionListManager: {
|
|
99
|
+
createSession: vi.fn(),
|
|
100
|
+
setSelectedAgentId: vi.fn(),
|
|
101
|
+
},
|
|
102
|
+
},
|
|
103
|
+
appPresenter: {
|
|
104
|
+
chatDraftIntentManager: {
|
|
105
|
+
consumePending: vi.fn(() => null),
|
|
106
|
+
markConsumed: vi.fn(),
|
|
107
|
+
subscribe: vi.fn(() => vi.fn()),
|
|
108
|
+
},
|
|
109
|
+
},
|
|
110
|
+
};
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
vi.mock('@/app/components/app-presenter-provider', () => ({
|
|
114
|
+
useAppPresenter: () => mocks.appPresenter,
|
|
115
|
+
}));
|
|
116
|
+
|
|
117
|
+
vi.mock('@/features/chat/components/providers/chat-presenter.provider', () => ({
|
|
118
|
+
usePresenter: () => mocks.presenter,
|
|
119
|
+
}));
|
|
120
|
+
|
|
121
|
+
vi.mock('@/features/chat/components/conversation/chat-conversation-content', () => ({
|
|
122
|
+
ChatConversationContent: ({
|
|
123
|
+
messages,
|
|
124
|
+
showWelcome,
|
|
125
|
+
welcomeSlot,
|
|
126
|
+
}: {
|
|
127
|
+
messages: readonly unknown[];
|
|
128
|
+
showWelcome: boolean;
|
|
129
|
+
welcomeSlot?: ReactNode;
|
|
130
|
+
}) => (
|
|
131
|
+
<div data-testid="conversation-content" data-show-welcome={String(showWelcome)}>
|
|
132
|
+
{showWelcome ? welcomeSlot : <div data-testid="message-count">{messages.length}</div>}
|
|
133
|
+
</div>
|
|
134
|
+
),
|
|
135
|
+
}));
|
|
136
|
+
|
|
137
|
+
vi.mock('@/features/chat/features/welcome/components/chat-conversation-welcome', () => ({
|
|
138
|
+
ChatConversationWelcome: ({ inputSlot }: { inputSlot: ReactNode }) => (
|
|
139
|
+
<div data-testid="welcome">{inputSlot}</div>
|
|
140
|
+
),
|
|
141
|
+
}));
|
|
142
|
+
|
|
143
|
+
vi.mock('@/features/chat/features/ncp/hooks/use-ncp-session-conversation', () => ({
|
|
144
|
+
isNcpAgentStartupUnavailableErrorMessage: () => false,
|
|
145
|
+
useNcpSessionConversation: () => mocks.agent,
|
|
146
|
+
}));
|
|
147
|
+
|
|
148
|
+
vi.mock('@/features/chat/features/runtime/utils/ncp-chat-runtime-availability.utils', () => ({
|
|
149
|
+
isNcpChatRuntimeBlocked: () => false,
|
|
150
|
+
resolveNcpChatSendErrorMessage: ({ message }: { message: string | null }) => message,
|
|
151
|
+
}));
|
|
152
|
+
|
|
153
|
+
vi.mock('@/features/chat/features/conversation/hooks/use-session-conversation-input-state', () => ({
|
|
154
|
+
useSessionConversationInputState: () => ({
|
|
155
|
+
inputActions: mocks.inputActions,
|
|
156
|
+
inputSnapshot: mocks.inputSnapshot,
|
|
157
|
+
}),
|
|
158
|
+
}));
|
|
159
|
+
|
|
160
|
+
vi.mock('@/features/chat/features/conversation/hooks/use-session-conversation-input-query', () => ({
|
|
161
|
+
useSessionConversationInputQuery: () => mocks.inputQuery,
|
|
162
|
+
}));
|
|
163
|
+
|
|
164
|
+
vi.mock('@/features/chat/features/conversation/hooks/use-session-conversation-controller', () => ({
|
|
165
|
+
useSessionConversationController: () => mocks.controller,
|
|
166
|
+
}));
|
|
167
|
+
|
|
168
|
+
vi.mock('@/features/chat/stores/chat-session-list.store', () => ({
|
|
169
|
+
useChatSessionListStore: (selector: (state: { snapshot: { selectedAgentId: string } }) => unknown) =>
|
|
170
|
+
selector({ snapshot: { selectedAgentId: 'main' } }),
|
|
171
|
+
}));
|
|
172
|
+
|
|
173
|
+
vi.mock('@/features/system-status', () => ({
|
|
174
|
+
useSystemStatus: () => ({ phase: 'ready', lastReadyAt: 1 }),
|
|
175
|
+
}));
|
|
176
|
+
|
|
177
|
+
vi.mock('../session-conversation-input', () => ({
|
|
178
|
+
SessionConversationInput: memo((props: unknown) => {
|
|
179
|
+
mocks.inputRenderSpy(props);
|
|
180
|
+
return <div data-testid="conversation-input" />;
|
|
181
|
+
}),
|
|
182
|
+
}));
|
|
183
|
+
|
|
184
|
+
function renderArea(sessionKey: string | null = 'session-1') {
|
|
185
|
+
return render(
|
|
186
|
+
<MemoryRouter>
|
|
187
|
+
<SessionConversationArea sessionKey={sessionKey} />
|
|
188
|
+
</MemoryRouter>,
|
|
189
|
+
);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
describe('SessionConversationArea input boundary', () => {
|
|
193
|
+
beforeEach(() => {
|
|
194
|
+
vi.clearAllMocks();
|
|
195
|
+
mocks.agent.visibleMessages = [];
|
|
196
|
+
mocks.agent.isHydrating = false;
|
|
197
|
+
mocks.agent.isRunning = true;
|
|
198
|
+
mocks.agent.isSending = true;
|
|
199
|
+
mocks.agent.snapshot.contextWindow = null;
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
it('keeps the composer input subtree stable when only streamed messages change', () => {
|
|
203
|
+
const rendered = renderArea('session-1');
|
|
204
|
+
|
|
205
|
+
expect(mocks.inputRenderSpy).toHaveBeenCalledOnce();
|
|
206
|
+
|
|
207
|
+
mocks.agent.visibleMessages = [{ id: 'message-1' }];
|
|
208
|
+
rendered.rerender(
|
|
209
|
+
<MemoryRouter>
|
|
210
|
+
<SessionConversationArea sessionKey="session-1" />
|
|
211
|
+
</MemoryRouter>,
|
|
212
|
+
);
|
|
213
|
+
|
|
214
|
+
expect(screen.getByTestId('message-count').textContent).toBe('1');
|
|
215
|
+
expect(mocks.inputRenderSpy).toHaveBeenCalledOnce();
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
it('does not replace the welcome composer just because draft send starts', () => {
|
|
219
|
+
renderArea(null);
|
|
220
|
+
|
|
221
|
+
expect(screen.getByTestId('conversation-content').dataset.showWelcome).toBe('true');
|
|
222
|
+
expect(screen.getByTestId('welcome')).toBeTruthy();
|
|
223
|
+
expect(screen.getByTestId('conversation-input')).toBeTruthy();
|
|
224
|
+
});
|
|
225
|
+
});
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
import {
|
|
2
|
+
useCallback,
|
|
3
|
+
useEffect,
|
|
4
|
+
useMemo,
|
|
5
|
+
useState,
|
|
6
|
+
type MutableRefObject,
|
|
7
|
+
} from 'react';
|
|
8
|
+
import { act, fireEvent, render, screen, waitFor } from '@testing-library/react';
|
|
9
|
+
import { describe, expect, it, vi } from 'vitest';
|
|
10
|
+
import {
|
|
11
|
+
createChatComposerTextNode,
|
|
12
|
+
type ChatComposerNode,
|
|
13
|
+
} from '@nextclaw/agent-chat-ui';
|
|
14
|
+
|
|
15
|
+
import { I18nProvider } from '@/app/components/i18n-provider';
|
|
16
|
+
import { ChatPresenterProvider, type ChatPresenterLike } from '@/features/chat/components/providers/chat-presenter.provider';
|
|
17
|
+
import {
|
|
18
|
+
SessionConversationInput,
|
|
19
|
+
type SessionConversationInputController,
|
|
20
|
+
} from '@/features/chat/features/conversation/components/session-conversation-input';
|
|
21
|
+
import type {
|
|
22
|
+
SessionConversationInputActions,
|
|
23
|
+
SessionConversationInputPatch,
|
|
24
|
+
SessionConversationInputSnapshot,
|
|
25
|
+
} from '@/features/chat/features/conversation/hooks/use-session-conversation-input-state';
|
|
26
|
+
|
|
27
|
+
vi.mock('@/app/hooks/use-viewport-layout', () => ({
|
|
28
|
+
useViewportLayout: () => ({ isDesktop: true, isMobile: false }),
|
|
29
|
+
}));
|
|
30
|
+
|
|
31
|
+
vi.mock('@/features/panel-apps', () => ({
|
|
32
|
+
usePanelApps: () => ({
|
|
33
|
+
data: { entries: [] },
|
|
34
|
+
isFetching: false,
|
|
35
|
+
isLoading: false,
|
|
36
|
+
}),
|
|
37
|
+
}));
|
|
38
|
+
|
|
39
|
+
vi.mock('@/features/chat/features/input/hooks/use-chat-model-favorites', () => ({
|
|
40
|
+
useChatModelFavorites: () => ({
|
|
41
|
+
favoriteModelValues: [],
|
|
42
|
+
isLoading: false,
|
|
43
|
+
setModelFavorite: vi.fn(),
|
|
44
|
+
}),
|
|
45
|
+
}));
|
|
46
|
+
|
|
47
|
+
vi.mock('@/shared/lib/api', async (importOriginal) => {
|
|
48
|
+
const actual = await importOriginal();
|
|
49
|
+
return {
|
|
50
|
+
...(actual as object),
|
|
51
|
+
updateNcpSession: vi.fn(),
|
|
52
|
+
};
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
type StreamingInputControl = {
|
|
56
|
+
bumpStream: () => void;
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
const presenter = {
|
|
60
|
+
chatThreadManager: {
|
|
61
|
+
openSideChatDraft: vi.fn(),
|
|
62
|
+
},
|
|
63
|
+
chatUiManager: {
|
|
64
|
+
goToProviders: vi.fn(),
|
|
65
|
+
},
|
|
66
|
+
} as unknown as ChatPresenterLike;
|
|
67
|
+
|
|
68
|
+
const controller: SessionConversationInputController = {
|
|
69
|
+
canStopGeneration: true,
|
|
70
|
+
isSending: true,
|
|
71
|
+
send: vi.fn(),
|
|
72
|
+
sendDisabled: true,
|
|
73
|
+
stop: vi.fn(),
|
|
74
|
+
stopDisabled: false,
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
function createStreamingInputSnapshot(
|
|
78
|
+
nodes: readonly ChatComposerNode[],
|
|
79
|
+
): SessionConversationInputSnapshot {
|
|
80
|
+
return {
|
|
81
|
+
attachments: [],
|
|
82
|
+
composerFocusRequestId: 0,
|
|
83
|
+
nodes,
|
|
84
|
+
pendingProjectRoot: null,
|
|
85
|
+
pendingSessionType: 'default',
|
|
86
|
+
selectedModel: undefined,
|
|
87
|
+
selectedSessionType: 'default',
|
|
88
|
+
selectedSkills: [],
|
|
89
|
+
selectedThinkingLevel: null,
|
|
90
|
+
sendError: null,
|
|
91
|
+
skillRecords: [],
|
|
92
|
+
text: nodes
|
|
93
|
+
.map((node) => (node.type === 'text' ? node.text : ''))
|
|
94
|
+
.join(''),
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function StreamingSessionConversationInputHarness({
|
|
99
|
+
controlRef,
|
|
100
|
+
}: {
|
|
101
|
+
controlRef: MutableRefObject<StreamingInputControl | null>;
|
|
102
|
+
}) {
|
|
103
|
+
const [nodes, setNodes] = useState<readonly ChatComposerNode[]>([
|
|
104
|
+
createChatComposerTextNode(''),
|
|
105
|
+
]);
|
|
106
|
+
const [streamChunk, setStreamChunk] = useState(0);
|
|
107
|
+
const bumpStream = useCallback(() => setStreamChunk((chunk) => chunk + 1), []);
|
|
108
|
+
|
|
109
|
+
useEffect(() => {
|
|
110
|
+
controlRef.current = { bumpStream };
|
|
111
|
+
return () => {
|
|
112
|
+
controlRef.current = null;
|
|
113
|
+
};
|
|
114
|
+
}, [bumpStream, controlRef]);
|
|
115
|
+
|
|
116
|
+
const inputSnapshot = useMemo(() => createStreamingInputSnapshot(nodes), [nodes]);
|
|
117
|
+
const inputActions: SessionConversationInputActions = useMemo(() => ({
|
|
118
|
+
addAttachments: vi.fn(() => []),
|
|
119
|
+
applyPromptSuggestion: vi.fn(),
|
|
120
|
+
consumeComposerFocusRequest: vi.fn(),
|
|
121
|
+
removeAttachment: vi.fn(),
|
|
122
|
+
requestComposerFocusAtEnd: vi.fn(),
|
|
123
|
+
resetComposer: vi.fn(),
|
|
124
|
+
restoreComposer: vi.fn(),
|
|
125
|
+
setAttachments: vi.fn(),
|
|
126
|
+
setPendingProjectRoot: vi.fn(),
|
|
127
|
+
setPendingSessionType: vi.fn(),
|
|
128
|
+
setSelectedModel: vi.fn(),
|
|
129
|
+
setSelectedSkills: vi.fn(),
|
|
130
|
+
setSelectedThinkingLevel: vi.fn(),
|
|
131
|
+
setSendError: vi.fn(),
|
|
132
|
+
syncComposer: vi.fn(),
|
|
133
|
+
update: (patch: SessionConversationInputPatch) => {
|
|
134
|
+
setNodes((currentNodes) => {
|
|
135
|
+
const resolvedPatch = typeof patch === 'function'
|
|
136
|
+
? patch(createStreamingInputSnapshot(currentNodes))
|
|
137
|
+
: patch;
|
|
138
|
+
return resolvedPatch.nodes ?? currentNodes;
|
|
139
|
+
});
|
|
140
|
+
},
|
|
141
|
+
}), []);
|
|
142
|
+
const inputQuery = useMemo(() => ({
|
|
143
|
+
defaultModel: undefined,
|
|
144
|
+
defaultProjectRoot: null,
|
|
145
|
+
fallbackPreferredModel: undefined,
|
|
146
|
+
fallbackPreferredThinking: undefined,
|
|
147
|
+
isProviderStateResolved: true,
|
|
148
|
+
isSkillsLoading: false,
|
|
149
|
+
modelOptions: [],
|
|
150
|
+
selectedSession: null,
|
|
151
|
+
selectedSessionKey: null,
|
|
152
|
+
sessionTypeState: {
|
|
153
|
+
canEditSessionType: true,
|
|
154
|
+
defaultSessionType: 'default',
|
|
155
|
+
selectedSessionType: 'default',
|
|
156
|
+
selectedSessionTypeOption: null,
|
|
157
|
+
sessionTypeOptions: [],
|
|
158
|
+
sessionTypeUnavailable: false,
|
|
159
|
+
sessionTypeUnavailableMessage: null,
|
|
160
|
+
},
|
|
161
|
+
skillRecords: [],
|
|
162
|
+
}), []);
|
|
163
|
+
|
|
164
|
+
return (
|
|
165
|
+
<I18nProvider>
|
|
166
|
+
<ChatPresenterProvider presenter={presenter}>
|
|
167
|
+
<div data-testid="stream-chunk">{streamChunk}</div>
|
|
168
|
+
<SessionConversationInput
|
|
169
|
+
contextWindow={null}
|
|
170
|
+
controller={controller}
|
|
171
|
+
inputActions={inputActions}
|
|
172
|
+
inputQuery={inputQuery}
|
|
173
|
+
inputSnapshot={inputSnapshot}
|
|
174
|
+
/>
|
|
175
|
+
</ChatPresenterProvider>
|
|
176
|
+
</I18nProvider>
|
|
177
|
+
);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
describe('SessionConversationInput streaming stability', () => {
|
|
181
|
+
it('keeps IME composition stable while simulated streamed output rerenders the owner', async () => {
|
|
182
|
+
const controlRef: MutableRefObject<StreamingInputControl | null> = { current: null };
|
|
183
|
+
render(<StreamingSessionConversationInputHarness controlRef={controlRef} />);
|
|
184
|
+
|
|
185
|
+
const textbox = screen.getByRole('textbox');
|
|
186
|
+
fireEvent.focus(textbox);
|
|
187
|
+
fireEvent.compositionStart(textbox);
|
|
188
|
+
|
|
189
|
+
act(() => {
|
|
190
|
+
controlRef.current?.bumpStream();
|
|
191
|
+
controlRef.current?.bumpStream();
|
|
192
|
+
});
|
|
193
|
+
expect(screen.getByTestId('stream-chunk').textContent).toBe('2');
|
|
194
|
+
|
|
195
|
+
fireEvent.compositionEnd(textbox, { data: '你' });
|
|
196
|
+
|
|
197
|
+
await waitFor(() => expect(textbox.textContent).toBe('你'));
|
|
198
|
+
});
|
|
199
|
+
});
|