@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.
Files changed (129) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/dist/assets/{api-a14tR_z1.js → api-BP_8IaxB.js} +3 -3
  3. package/dist/assets/{channels-list-page-CFGkm4DT.js → channels-list-page-DEh5Jjqx.js} +1 -1
  4. package/dist/assets/chat-page-BRB0N5WW.js +105 -0
  5. package/dist/assets/config-split-page-CRhlZ_pH.js +1 -0
  6. package/dist/assets/{confirm-dialog-ClH_Ec7n.js → confirm-dialog-DsivFJ4r.js} +2 -2
  7. package/dist/assets/{desktop-update-config-DPNDnVpk.js → desktop-update-config-B0w9p2ep.js} +1 -1
  8. package/dist/assets/{dist-BtCS2Ucq.js → dist-CikkhOlq.js} +1 -1
  9. package/dist/assets/{dist-CLGGLm8b.js → dist-PVA_xL1D.js} +1 -1
  10. package/dist/assets/doc-browser-BoNJ7-tE.js +1 -0
  11. package/dist/assets/doc-browser-CdJ_d4ik.js +1 -0
  12. package/dist/assets/{doc-browser-context-D4JwMMxK.js → doc-browser-context-MTw4EvKd.js} +1 -1
  13. package/dist/assets/{ellipsis-CtJXjIol.js → ellipsis-y6vDtq4M.js} +1 -1
  14. package/dist/assets/{external-link-CdJiG13p.js → external-link-DVJIQf_G.js} +1 -1
  15. package/dist/assets/index-BiE-K8-q.css +1 -0
  16. package/dist/assets/index-D9BAE_L_.js +105 -0
  17. package/dist/assets/{mcp-marketplace-page-BU0kIjhR.js → mcp-marketplace-page-B1SZNY5m.js} +1 -1
  18. package/dist/assets/mcp-marketplace-page-Cq_Tw42T.js +1 -0
  19. package/dist/assets/{model-config-page-D6hYXROd.js → model-config-page-DotaTUHb.js} +1 -1
  20. package/dist/assets/plus-BbZqquJe.js +1 -0
  21. package/dist/assets/{provider-scoped-model-input-DAX2HgmR.js → provider-scoped-model-input-DsFRuWJH.js} +1 -1
  22. package/dist/assets/providers-config-page-DW4tQSLN.js +1 -0
  23. package/dist/assets/{react-C6cZgP44.js → react-VNqT5Zp7.js} +1 -1
  24. package/dist/assets/remote-B8TiUfwW.js +1 -0
  25. package/dist/assets/rotate-cw-DjHDTkCv.js +1 -0
  26. package/dist/assets/{runtime-config-page-C5xKaV3i.js → runtime-config-page-S6ErRHR8.js} +1 -1
  27. package/dist/assets/{save-DKA0x4zk.js → save-BOJ477H-.js} +1 -1
  28. package/dist/assets/{search-BNlcoj5c.js → search-BEti5beA.js} +1 -1
  29. package/dist/assets/{search-config-page-Cq9uJ-N8.js → search-config-page-DGj5am03.js} +1 -1
  30. package/dist/assets/{secrets-config-page-CxSbZLBo.js → secrets-config-page--IzrIOid.js} +1 -1
  31. package/dist/assets/{select-CvTlDZTQ.js → select-hoj8dK4K.js} +2 -2
  32. package/dist/assets/{tag-chip-8ffF2y2C.js → tag-chip-T8-h-die.js} +1 -1
  33. package/dist/assets/{use-config-DmT0o1gv.js → use-config-CjRV2hwL.js} +1 -1
  34. package/dist/assets/x-D9PXs_xP.js +1 -0
  35. package/dist/index.html +69 -19
  36. package/index.html +55 -5
  37. package/package.json +7 -7
  38. package/src/app/components/layout/__tests__/sidebar.layout.test.tsx +3 -3
  39. package/src/app/components/layout/header.tsx +3 -3
  40. package/src/app/components/layout/page-layout.tsx +2 -2
  41. package/src/app/components/layout/runtime-status-entry.tsx +7 -7
  42. package/src/app/components/layout/sidebar-items.tsx +10 -10
  43. package/src/app/components/layout/sidebar-rail.styles.ts +2 -2
  44. package/src/app/components/layout/sidebar.tsx +6 -6
  45. package/src/app/styles/design-system.css +388 -26
  46. package/src/app/test/vitest-setup.ts +31 -0
  47. package/src/features/chat/components/chat-sidebar-list-mode-switch.tsx +3 -3
  48. package/src/features/chat/components/conversation/__tests__/chat-conversation-header.test.tsx +4 -2
  49. package/src/features/chat/components/conversation/chat-conversation-header-section.tsx +1 -1
  50. package/src/features/chat/components/conversation/chat-conversation-header.tsx +7 -6
  51. package/src/features/chat/components/conversation/chat-conversation-panel.tsx +1 -1
  52. package/src/features/chat/components/conversation/chat-conversation-skeleton.tsx +13 -13
  53. package/src/features/chat/components/layout/__tests__/chat-sidebar.test.tsx +2 -2
  54. package/src/features/chat/components/layout/chat-page-shell.tsx +1 -1
  55. package/src/features/chat/components/layout/chat-sidebar-desktop-layout.tsx +3 -3
  56. package/src/features/chat/components/layout/chat-sidebar-toolbar.tsx +14 -14
  57. package/src/features/chat/components/layout/chat-sidebar-utility-menu.tsx +15 -15
  58. package/src/features/chat/components/layout/chat-sidebar.tsx +3 -3
  59. package/src/features/chat/features/conversation/components/__tests__/session-conversation-area.test.tsx +225 -0
  60. package/src/features/chat/features/conversation/components/__tests__/session-conversation-input.streaming.test.tsx +199 -0
  61. package/src/features/chat/features/conversation/components/session-conversation-area.tsx +28 -9
  62. package/src/features/chat/features/conversation/components/session-conversation-input.tsx +18 -6
  63. package/src/features/chat/features/conversation/hooks/use-session-conversation-controller.ts +3 -13
  64. package/src/features/chat/features/conversation/hooks/use-session-conversation-input-query.ts +19 -3
  65. package/src/features/chat/features/message/components/chat-inline-panel-app-card.tsx +7 -7
  66. package/src/features/chat/features/message/components/chat-message-list.container.tsx +6 -6
  67. package/src/features/chat/features/session/components/chat-sidebar-project-groups.tsx +5 -5
  68. package/src/features/chat/features/session/components/chat-sidebar-session-list.tsx +4 -4
  69. package/src/features/chat/features/session/components/session-context-icon.tsx +2 -2
  70. package/src/features/chat/features/session/components/session-header/__tests__/chat-session-project-badge.test.tsx +2 -2
  71. package/src/features/chat/features/session/components/session-header/chat-session-header-actions.tsx +1 -1
  72. package/src/features/chat/features/session/components/session-header/chat-session-header-menu-item.tsx +1 -1
  73. package/src/features/chat/features/session/components/session-header/chat-session-metadata-dialog.tsx +1 -1
  74. package/src/features/chat/features/session/components/session-header/chat-session-project-badge.tsx +3 -3
  75. package/src/features/chat/features/session/components/session-run-badge.tsx +1 -1
  76. package/src/features/chat/features/session-type/hooks/use-chat-session-type-state.ts +10 -2
  77. package/src/features/chat/features/welcome/components/__tests__/chat-welcome.test.tsx +0 -7
  78. package/src/features/chat/features/welcome/components/chat-welcome-agent-picker.tsx +3 -3
  79. package/src/features/chat/features/welcome/components/chat-welcome-capability-grid.tsx +4 -4
  80. package/src/features/chat/features/welcome/components/chat-welcome-project-picker.tsx +14 -19
  81. package/src/features/chat/features/welcome/components/chat-welcome-session-type-picker.tsx +3 -3
  82. package/src/features/chat/features/welcome/components/chat-welcome.tsx +4 -4
  83. package/src/features/chat/features/welcome/utils/__tests__/chat-welcome-project-options.utils.test.ts +6 -8
  84. package/src/features/chat/features/welcome/utils/chat-welcome-project-options.utils.ts +1 -16
  85. package/src/features/panel-apps/components/panel-app-list-item.tsx +8 -8
  86. package/src/features/panel-apps/components/panel-app-toolbar.tsx +4 -4
  87. package/src/features/panel-apps/components/panel-apps-list.tsx +20 -20
  88. package/src/features/pwa/managers/__tests__/pwa-shell-theme.manager.test.ts +70 -6
  89. package/src/features/pwa/managers/pwa-shell-theme.manager.ts +10 -5
  90. package/src/features/service-apps/components/service-action-authorization-dialog.tsx +8 -8
  91. package/src/features/service-apps/components/service-apps-panel.tsx +24 -24
  92. package/src/features/settings/components/config/provider-advanced-settings-section.tsx +5 -5
  93. package/src/features/settings/components/config/provider-auth-section.tsx +7 -7
  94. package/src/features/settings/components/config/provider-form.tsx +10 -10
  95. package/src/features/settings/components/config/provider-models-section.tsx +16 -16
  96. package/src/features/settings/components/config/provider-pill-selector.tsx +1 -1
  97. package/src/features/settings/pages/providers-config-page.tsx +17 -17
  98. package/src/shared/components/config-split-page.tsx +11 -11
  99. package/src/shared/components/doc-browser/doc-browser-home-page.tsx +2 -2
  100. package/src/shared/components/doc-browser/doc-browser-panel-parts.tsx +4 -4
  101. package/src/shared/components/doc-browser/doc-browser-tab-strip.tsx +4 -4
  102. package/src/shared/components/doc-browser/doc-browser.tsx +3 -3
  103. package/src/shared/components/resizable-right-panel/resizable-right-panel.tsx +1 -1
  104. package/src/shared/components/ui/actions/icon-action-button.tsx +1 -1
  105. package/src/shared/components/ui/button.tsx +4 -4
  106. package/src/shared/components/ui/card.tsx +4 -4
  107. package/src/shared/components/ui/dialog.tsx +5 -5
  108. package/src/shared/components/ui/input.tsx +1 -1
  109. package/src/shared/components/ui/label.tsx +1 -1
  110. package/src/shared/components/ui/popover.tsx +1 -1
  111. package/src/shared/components/ui/select.tsx +3 -3
  112. package/src/shared/components/ui/switch.tsx +3 -3
  113. package/src/shared/components/ui/tabs.tsx +4 -4
  114. package/src/shared/components/ui/textarea.tsx +1 -1
  115. package/src/shared/lib/i18n/locales/en-US/core.json +7 -2
  116. package/src/shared/lib/i18n/locales/zh-CN/core.json +7 -2
  117. package/src/shared/lib/theme/index.ts +94 -52
  118. package/dist/assets/chat-page-CEdavJXc.js +0 -105
  119. package/dist/assets/config-split-page-B5b9B0uE.js +0 -1
  120. package/dist/assets/doc-browser-5nMWYKBD.js +0 -1
  121. package/dist/assets/doc-browser-BJRLhztv.js +0 -1
  122. package/dist/assets/index-BDN1zzMQ.js +0 -105
  123. package/dist/assets/index-g3uSZXoc.css +0 -1
  124. package/dist/assets/mcp-marketplace-page-sMNFSq_T.js +0 -1
  125. package/dist/assets/plus-DPjb0piO.js +0 -1
  126. package/dist/assets/providers-config-page-BzAeaACA.js +0 -1
  127. package/dist/assets/remote-B0G80uPr.js +0 -1
  128. package/dist/assets/rotate-cw-DnEB1Rju.js +0 -1
  129. 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: "Cool" }));
369
+ fireEvent.click(screen.getByRole("option", { name: "Probe" }));
370
370
 
371
- expect(mocks.setTheme).toHaveBeenCalledWith("cool");
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-gradient-to-b from-gray-50/60 to-white">
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-gray-200/60",
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-gray-400">
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-gray-200/60 py-3",
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-white/90 text-gray-700 shadow-none transition-[background-color,color,box-shadow] hover:bg-white hover:text-gray-950 hover:shadow-sm active:bg-white/95",
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-white/90 text-gray-600 shadow-none transition-[background-color,color,box-shadow] hover:bg-white hover:text-gray-900 hover:shadow-sm active:bg-white/95",
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-white/90 text-primary shadow-none transition-[background-color,color,box-shadow] hover:bg-primary/10 hover:text-primary-700 hover:shadow-sm active:bg-primary/15",
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-white/90 text-primary shadow-none transition-[background-color,color,box-shadow] hover:bg-primary/10 hover:text-primary-700 hover:shadow-sm active:bg-primary/15",
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-gray-200/80 bg-white p-1.5 shadow-[0_24px_60px_-28px_rgba(15,23,42,0.38)]"
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-gray-200/80 bg-white p-1.5 shadow-[0_24px_60px_-28px_rgba(15,23,42,0.38)]"
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-gray-400" />
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-gray-400" />
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-gray-100/80 pl-9 pr-3 text-[13px] shadow-none focus:border-gray-200/80 focus:bg-white"
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-gray-100/80 text-gray-700 shadow-none hover:bg-gray-200/80 hover:text-gray-900"
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-gray-200/80 bg-white p-2 shadow-[0_24px_70px_-30px_rgba(15,23,42,0.45)]"
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-gray-100/80 text-gray-700 shadow-none hover:bg-gray-200/80 hover:text-gray-900"
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-gray-500",
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-gray-600 transition-all duration-base hover:bg-gray-200/60 hover:text-gray-800 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-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-gray-400" />
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-gray-500">
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-gray-700 transition-colors hover:bg-gray-100"
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-gray-400" />
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-gray-700 transition-colors hover:bg-gray-100"
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-gray-400" />
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-gray-700 transition-colors hover:bg-gray-100"
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-gray-400" />
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-gray-200/70" />
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-gray-700 shadow-none hover:bg-gray-100 focus:ring-0"
216
- indicator={<ChevronRight className="h-4 w-4 text-gray-400" />}
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-gray-400" />
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-gray-500">
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 ?? "themeWarm",
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-gray-200/60",
236
+ "shrink-0 border-r border-border/70",
237
237
  )
238
- : "w-[280px] shrink-0 border-r border-gray-200/60",
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
+ });