@nextclaw/ui 0.15.21 → 0.15.22

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 (161) hide show
  1. package/CHANGELOG.md +20 -0
  2. package/dist/assets/{api-CHo2gekJ.js → api-B1yHQYs-.js} +3 -3
  3. package/dist/assets/app-presenter-provider-uP9waOzv.js +108 -0
  4. package/dist/assets/appearance-settings-page-C9Zq3hP9.js +1 -0
  5. package/dist/assets/{book-open-BrZL4o9L.js → book-open-oSmyVXea.js} +1 -1
  6. package/dist/assets/{card-B5rRLu_6.js → card-CGvJZTZp.js} +1 -1
  7. package/dist/assets/{channels-list-page-C1dnm68I.js → channels-list-page-5BTxpQHw.js} +2 -2
  8. package/dist/assets/chat-page-jTvli8Jp.js +1 -0
  9. package/dist/assets/config-split-page-BgBsUmp1.js +1 -0
  10. package/dist/assets/confirm-dialog-h849QRWO.js +5 -0
  11. package/dist/assets/desktop-update-config-BGGHxClR.js +1 -0
  12. package/dist/assets/{dist-C9EX46wM.js → dist-CxUhurq9.js} +1 -1
  13. package/dist/assets/doc-browser-0eL8AX42.js +1 -0
  14. package/dist/assets/{doc-browser-DH-3hDnI.js → doc-browser-DEqE9lsh.js} +1 -1
  15. package/dist/assets/{doc-browser-context-TU2heon0.js → doc-browser-context-DkzFIUoK.js} +1 -1
  16. package/dist/assets/{eye-B-3dyZNA.js → eye-gOWQGt8N.js} +1 -1
  17. package/dist/assets/{form-actions-DvvxCpL4.js → form-actions-CgjYZUzM.js} +1 -1
  18. package/dist/assets/{index-k_eWScvN.js → index-DSAUWCfl.js} +2 -2
  19. package/dist/assets/index-G7cBryJ4.css +1 -0
  20. package/dist/assets/loader-circle-BAuycGf6.js +1 -0
  21. package/dist/assets/{logo-badge-DB51c0cS.js → logo-badge-BNcsDsUM.js} +1 -1
  22. package/dist/assets/{mcp-marketplace-page-DWtsIqml.js → mcp-marketplace-page-BX39MtPa.js} +3 -3
  23. package/dist/assets/mcp-marketplace-page-CkDtOEA4.js +1 -0
  24. package/dist/assets/{model-config-page-D6cGNnMM.js → model-config-page-7MIXg2fQ.js} +1 -1
  25. package/dist/assets/{navigation-link-BSJEUiwD.js → navigation-link-B3iFOdxx.js} +1 -1
  26. package/dist/assets/plus-yvhFgfal.js +1 -0
  27. package/dist/assets/{popover-Bp9IpE_N.js → popover-Cpep_rJe.js} +1 -1
  28. package/dist/assets/{provider-models-CXUgDxGR.js → provider-models-CUrptZ-o.js} +1 -1
  29. package/dist/assets/provider-scoped-model-input-CHmOzz7x.js +1 -0
  30. package/dist/assets/providers-config-page-A6J7Y0II.js +1 -0
  31. package/dist/assets/react-BCJGCjrP.js +8 -0
  32. package/dist/assets/{refresh-cw-C2e94pmp.js → refresh-cw-CkKwPztT.js} +1 -1
  33. package/dist/assets/remote-FD-ZfSIn.js +1 -0
  34. package/dist/assets/rotate-cw-DrJMfHhp.js +1 -0
  35. package/dist/assets/{runtime-config-page-C57OLdPm.js → runtime-config-page-DwjO-Vs6.js} +1 -1
  36. package/dist/assets/{save-CkwDOgvt.js → save-BINMWate.js} +1 -1
  37. package/dist/assets/{search-CLXM2reO.js → search-7M38azRp.js} +1 -1
  38. package/dist/assets/{search-config-page-tnJXaaSr.js → search-config-page-T6tMhXbP.js} +1 -1
  39. package/dist/assets/{secrets-config-page-DQ_Curaj.js → secrets-config-page-2mVejXf2.js} +2 -2
  40. package/dist/assets/security-config-DD5RrjDe.js +1 -0
  41. package/dist/assets/{select-CNFBf00l.js → select-BvidP5Oa.js} +1 -1
  42. package/dist/assets/{settings-2-B9IT2HGd.js → settings-2-DqZstREV.js} +1 -1
  43. package/dist/assets/skeleton-BEQW2Dva.js +1 -0
  44. package/dist/assets/{tabs-BuNr8LSO.js → tabs-BeLxVmkt.js} +1 -1
  45. package/dist/assets/{tag-chip-CDkB5smU.js → tag-chip-BoDaLnWt.js} +1 -1
  46. package/dist/assets/{trash-2-GchaJuEM.js → trash-2-CYUskMMN.js} +1 -1
  47. package/dist/assets/{use-config-C69rR15i.js → use-config-CxKRXXCl.js} +1 -1
  48. package/dist/assets/use-confirm-dialog-HASTvOP2.js +1 -0
  49. package/dist/assets/{use-viewport-layout-BACNZyMY.js → use-viewport-layout-CJxkjoRz.js} +1 -1
  50. package/dist/assets/x-DdUwjre5.js +1 -0
  51. package/dist/index.html +29 -29
  52. package/package.json +7 -7
  53. package/src/app/components/layout/sidebar-rail.styles.ts +0 -2
  54. package/src/app/components/layout/sidebar.tsx +2 -2
  55. package/src/app/styles/design-system.css +12 -0
  56. package/src/features/chat/components/chat-sidebar-list-mode-switch.tsx +2 -2
  57. package/src/features/chat/components/conversation/__tests__/chat-conversation-content.test.tsx +19 -1
  58. package/src/features/chat/components/conversation/__tests__/chat-conversation-header-section.test.tsx +1 -1
  59. package/src/features/chat/components/conversation/chat-conversation-content.tsx +5 -6
  60. package/src/features/chat/components/conversation/chat-conversation-header.tsx +1 -1
  61. package/src/features/chat/components/layout/__tests__/chat-sidebar-read-state.test.tsx +1 -1
  62. package/src/features/chat/components/layout/__tests__/chat-sidebar-session-area.test.tsx +52 -0
  63. package/src/features/chat/components/layout/__tests__/chat-sidebar.test.tsx +11 -17
  64. package/src/features/chat/components/layout/chat-page-shell.tsx +2 -11
  65. package/src/features/chat/components/layout/chat-sidebar-desktop-layout.tsx +32 -42
  66. package/src/features/chat/components/layout/chat-sidebar-toolbar.tsx +13 -64
  67. package/src/features/chat/components/layout/chat-sidebar-utility-menu.tsx +1 -4
  68. package/src/features/chat/features/conversation/components/__tests__/session-conversation-area.test.tsx +1 -1
  69. package/src/features/chat/features/conversation/components/__tests__/session-conversation-input.streaming.test.tsx +15 -4
  70. package/src/features/chat/features/conversation/components/session-conversation-area.tsx +7 -7
  71. package/src/features/chat/features/conversation/components/session-conversation-input.tsx +4 -2
  72. package/src/features/chat/features/conversation/hooks/use-session-conversation-input-attachments.ts +1 -0
  73. package/src/features/chat/features/conversation/utils/session-conversation-input-toolbar.utils.ts +1 -0
  74. package/src/features/chat/features/conversation/utils/session-queued-input.utils.ts +1 -0
  75. package/src/features/chat/features/input/utils/__tests__/chat-input-bar.utils.test.ts +3 -0
  76. package/src/features/chat/features/input/utils/chat-input-bar.utils.ts +0 -1
  77. package/src/features/chat/features/input/utils/chat-input-toolbar.utils.ts +3 -0
  78. package/src/features/chat/features/message/utils/__tests__/chat-message-timeline.utils.test.ts +29 -0
  79. package/src/features/chat/features/message/utils/chat-message-timeline.utils.ts +4 -1
  80. package/src/features/chat/features/session/components/chat-sidebar-session-item.tsx +10 -10
  81. package/src/features/chat/features/session/components/chat-sidebar-session-list.tsx +2 -2
  82. package/src/features/chat/features/session/components/session-header/chat-session-header-menu-item.tsx +1 -1
  83. package/src/features/chat/features/session/components/session-header/chat-session-project-badge.tsx +1 -1
  84. package/src/features/chat/features/session/components/session-header/chat-session-switcher-groups.tsx +1 -1
  85. package/src/features/chat/features/session/components/session-header/chat-session-title-switcher.tsx +1 -1
  86. package/src/features/chat/features/session/utils/__tests__/chat-session-display.utils.test.ts +26 -0
  87. package/src/features/chat/features/session/utils/__tests__/ncp-session-adapter.utils.test.ts +4 -2
  88. package/src/features/chat/features/session/utils/chat-session-display.utils.ts +48 -12
  89. package/src/features/chat/features/session/utils/ncp-session-adapter.utils.ts +13 -4
  90. package/src/features/chat/features/welcome/components/__tests__/chat-conversation-welcome.test.tsx +6 -0
  91. package/src/features/chat/features/welcome/components/__tests__/chat-welcome.test.tsx +10 -7
  92. package/src/features/chat/features/welcome/components/chat-welcome-agent-picker.tsx +1 -1
  93. package/src/features/chat/features/welcome/components/chat-welcome-project-picker.tsx +3 -14
  94. package/src/features/chat/features/welcome/components/chat-welcome-prompt-suggestions.tsx +37 -0
  95. package/src/features/chat/features/welcome/components/chat-welcome-session-type-picker.tsx +1 -1
  96. package/src/features/chat/features/welcome/components/chat-welcome.tsx +33 -31
  97. package/src/features/chat/features/workspace/components/__tests__/chat-session-workspace-panel-content.test.tsx +1 -1
  98. package/src/features/chat/features/workspace/components/__tests__/chat-session-workspace-panel.test.tsx +1 -1
  99. package/src/features/chat/managers/__tests__/chat-session-list.manager.test.ts +15 -3
  100. package/src/features/chat/managers/__tests__/chat-thread.manager.test.ts +3 -10
  101. package/src/features/chat/managers/chat-session-list.manager.ts +3 -23
  102. package/src/features/chat/managers/chat-thread.manager.ts +2 -5
  103. package/src/features/chat/pages/__tests__/ncp-chat-page.test.tsx +3 -9
  104. package/src/features/chat/pages/ncp-chat-page.tsx +0 -1
  105. package/src/features/chat/types/chat-input-bar.types.ts +1 -0
  106. package/src/features/cron/components/cron-task-discovery.tsx +1 -1
  107. package/src/features/marketplace/components/marketplace-page-parts.tsx +1 -1
  108. package/src/features/notifications/components/__tests__/app-notification-toast.test.tsx +3 -0
  109. package/src/features/notifications/components/app-notification-toast.tsx +1 -1
  110. package/src/features/panel-apps/components/panel-app-list-item.tsx +1 -1
  111. package/src/features/panel-apps/components/panel-app-toolbar.tsx +2 -2
  112. package/src/features/panel-apps/components/panel-apps-list.tsx +4 -4
  113. package/src/features/service-apps/components/service-apps-panel.tsx +1 -1
  114. package/src/features/settings/components/config/provider-auth-section.tsx +90 -59
  115. package/src/features/settings/components/config/provider-form.tsx +10 -17
  116. package/src/features/settings/components/config/provider-status-badge.tsx +4 -2
  117. package/src/features/settings/pages/__tests__/providers-config-page.test.tsx +12 -0
  118. package/src/features/settings/pages/providers-config-page.tsx +2 -1
  119. package/src/platforms/mobile/components/__tests__/mobile-app-shell.test.tsx +13 -7
  120. package/src/platforms/mobile/components/__tests__/mobile-bottom-nav.test.tsx +18 -20
  121. package/src/shared/components/doc-browser/doc-browser-panel-parts.tsx +1 -1
  122. package/src/shared/components/path-picker/server-path-picker-directory-list.tsx +1 -1
  123. package/src/shared/components/path-picker/server-path-picker-locations.tsx +1 -1
  124. package/src/shared/components/path-picker/server-path-picker-navigation-bar.tsx +3 -3
  125. package/src/shared/components/ui/actions/__tests__/icon-action-button.test.tsx +1 -1
  126. package/src/shared/components/ui/actions/icon-action-button.tsx +1 -1
  127. package/src/shared/components/ui/button.tsx +4 -4
  128. package/src/shared/components/ui/dialog.tsx +1 -1
  129. package/src/shared/components/ui/input.tsx +1 -1
  130. package/src/shared/components/ui/scroll-area.tsx +2 -0
  131. package/src/shared/components/ui/select.tsx +1 -1
  132. package/src/shared/components/ui/switch.tsx +1 -1
  133. package/src/shared/components/ui/tabs.tsx +1 -1
  134. package/src/shared/components/ui/textarea.tsx +1 -1
  135. package/src/shared/lib/api/ncp-session.types.ts +1 -0
  136. package/src/shared/lib/api/types.ts +2 -0
  137. package/src/shared/lib/i18n/locales/en-US/chat.json +13 -14
  138. package/src/shared/lib/i18n/locales/zh-CN/chat.json +13 -14
  139. package/src/shared/lib/provider-models/__tests__/anonymous-provider-configuration.test.ts +40 -0
  140. package/src/shared/lib/provider-models/index.ts +4 -3
  141. package/dist/assets/app-presenter-provider-2_DmWYT9.js +0 -108
  142. package/dist/assets/appearance-settings-page-BEy2UE7R.js +0 -1
  143. package/dist/assets/chat-page-Dz6tvZ91.js +0 -1
  144. package/dist/assets/config-split-page-6y6W6dz4.js +0 -1
  145. package/dist/assets/confirm-dialog-q1CpEmR8.js +0 -5
  146. package/dist/assets/desktop-update-config-DrdlwP4j.js +0 -1
  147. package/dist/assets/doc-browser-BfttDSti.js +0 -1
  148. package/dist/assets/index-CXDPLDFD.css +0 -1
  149. package/dist/assets/loader-circle-B35NlHTI.js +0 -1
  150. package/dist/assets/mcp-marketplace-page-CkPtwPBk.js +0 -1
  151. package/dist/assets/plus-BRVe5jSJ.js +0 -1
  152. package/dist/assets/provider-scoped-model-input-dk7fz9NL.js +0 -1
  153. package/dist/assets/providers-config-page-DTP2YfxA.js +0 -1
  154. package/dist/assets/react-D6ShNq95.js +0 -8
  155. package/dist/assets/remote-FllhXW3o.js +0 -1
  156. package/dist/assets/rotate-cw-CTC-Qszi.js +0 -1
  157. package/dist/assets/security-config-S00DjFfO.js +0 -1
  158. package/dist/assets/skeleton-Dc06gatK.js +0 -1
  159. package/dist/assets/use-confirm-dialog-DF_EgPYx.js +0 -1
  160. package/dist/assets/x-BXFquY9e.js +0 -1
  161. package/src/features/chat/features/welcome/components/chat-welcome-capability-grid.tsx +0 -59
@@ -5,6 +5,12 @@
5
5
  */
6
6
 
7
7
  @layer base {
8
+ input:focus,
9
+ textarea:focus {
10
+ outline: none;
11
+ box-shadow: none;
12
+ }
13
+
8
14
  :root {
9
15
  color-scheme: light;
10
16
 
@@ -70,6 +76,9 @@
70
76
  /* Accent */
71
77
  --accent: var(--brand-50);
72
78
  --accent-foreground: var(--brand-700);
79
+ --interaction-hover: hsl(0 0% 96%);
80
+ --interaction-selection: hsl(211 100% 92%);
81
+ --interaction-selection-border: hsl(211 75% 84%);
73
82
 
74
83
  /* Muted */
75
84
  --muted: var(--gray-100);
@@ -717,6 +726,9 @@
717
726
  --secondary-foreground: var(--gray-800);
718
727
  --accent: 151 32% 18%;
719
728
  --accent-foreground: 148 34% 84%;
729
+ --interaction-hover: hsl(var(--gray-200));
730
+ --interaction-selection: hsl(211 48% 30%);
731
+ --interaction-selection-border: hsl(211 48% 42%);
720
732
  --muted: var(--gray-100);
721
733
  --muted-foreground: var(--gray-500);
722
734
  --destructive: 0 70% 52%;
@@ -10,7 +10,7 @@ export function ChatSidebarListModeSwitch({ isProjectFirstView, onSelectMode }:
10
10
  type="button"
11
11
  aria-pressed={!isProjectFirstView}
12
12
  onClick={() => onSelectMode('time-first')}
13
- className={cn('transition-colors', isProjectFirstView ? 'text-muted-foreground/65 hover:text-muted-foreground' : 'font-medium text-foreground')}
13
+ className={cn('rounded-sm transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-border', isProjectFirstView ? 'text-muted-foreground/65 hover:text-muted-foreground' : 'font-medium text-foreground')}
14
14
  >
15
15
  {t('chatSidebarViewTime')}
16
16
  </button>
@@ -19,7 +19,7 @@ export function ChatSidebarListModeSwitch({ isProjectFirstView, onSelectMode }:
19
19
  type="button"
20
20
  aria-pressed={isProjectFirstView}
21
21
  onClick={() => onSelectMode('project-first')}
22
- className={cn('transition-colors', isProjectFirstView ? 'font-medium text-foreground' : 'text-muted-foreground/65 hover:text-muted-foreground')}
22
+ className={cn('rounded-sm transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-border', isProjectFirstView ? 'font-medium text-foreground' : 'text-muted-foreground/65 hover:text-muted-foreground')}
23
23
  >
24
24
  {t('chatSidebarViewProject')}
25
25
  </button>
@@ -60,6 +60,7 @@ function renderContent(
60
60
  isContextCompacting?: boolean;
61
61
  isLoadingPreviousMessages?: boolean;
62
62
  onLoadPreviousMessages?: () => Promise<void>;
63
+ showWelcome?: boolean;
63
64
  } = {},
64
65
  ) {
65
66
  return render(
@@ -74,8 +75,9 @@ function renderContent(
74
75
  isSending={false}
75
76
  messages={messages}
76
77
  sessionKey="session-1"
77
- showWelcome={false}
78
+ showWelcome={options.showWelcome ?? false}
78
79
  onLoadPreviousMessages={options.onLoadPreviousMessages ?? vi.fn()}
80
+ welcomeSlot={<div data-testid="conversation-welcome" />}
79
81
  />,
80
82
  );
81
83
  }
@@ -94,6 +96,22 @@ it("renders manual context compaction feedback after the message list", () => {
94
96
  expect(screen.getByTestId("context-compaction-pending")).toBeTruthy();
95
97
  });
96
98
 
99
+ it("fades the conversation into the docked input without obscuring the final message", () => {
100
+ const { container } = renderContent();
101
+ const scrollContainer = container.querySelector<HTMLElement>(
102
+ '[data-chat-scroll-container="true"]',
103
+ );
104
+ expect(scrollContainer?.className).toContain("[mask-image:linear-gradient");
105
+ expect(scrollContainer?.className).toContain("[-webkit-mask-image:linear-gradient");
106
+ expect(scrollContainer?.firstElementChild?.className).toContain("pb-7");
107
+
108
+ const { container: welcomeContainer } = renderContent({ showWelcome: true });
109
+ const welcomeScrollContainer = welcomeContainer.querySelector<HTMLElement>(
110
+ '[data-chat-scroll-container="true"]',
111
+ );
112
+ expect(welcomeScrollContainer?.className).not.toContain("[mask-image:linear-gradient");
113
+ });
114
+
97
115
  it("uses the centered reading track in flat message layout", () => {
98
116
  useChatMessageLayoutStore.getState().setLayout("flat");
99
117
 
@@ -279,7 +279,7 @@ describe("ChatConversationHeaderSection", () => {
279
279
  renderHeaderSection("mobile");
280
280
  await user.click(screen.getByRole("button", { name: "New Task" }));
281
281
 
282
- expect(mocks.createSession).toHaveBeenCalledWith("codex");
282
+ expect(mocks.createSession).toHaveBeenCalledWith({ sessionType: "codex" });
283
283
  });
284
284
 
285
285
  it("keeps the new-session shortcut mobile-only", () => {
@@ -13,6 +13,7 @@ import {
13
13
  } from "@/features/chat/features/message/components/chat-message-list.container";
14
14
  import { ChatConversationTrack } from "@/features/chat/components/conversation/chat-conversation-track";
15
15
  import { IconActionButton } from "@/shared/components/ui/actions/icon-action-button";
16
+ import { SCROLL_BOTTOM_EDGE_FADE_CLASS } from "@/shared/components/ui/scroll-area";
16
17
  import { t } from "@/shared/lib/i18n";
17
18
 
18
19
  type ChatConversationContentProps = {
@@ -81,13 +82,13 @@ export function ChatConversationContent({
81
82
  ref={threadRef}
82
83
  onScroll={handleScroll}
83
84
  data-chat-scroll-container="true"
84
- className="h-full overflow-y-auto custom-scrollbar"
85
+ className={showWelcome ? "h-full overflow-y-auto custom-scrollbar" : `h-full overflow-y-auto custom-scrollbar ${SCROLL_BOTTOM_EDGE_FADE_CLASS}`}
85
86
  style={{ overflowAnchor: "none" }}
86
87
  >
87
88
  {showWelcome ? (
88
89
  (welcomeSlot ?? null)
89
90
  ) : (
90
- <div ref={contentRef}>
91
+ <div ref={contentRef} className="pb-7">
91
92
  {hasMessages ? (
92
93
  <ChatConversationTrack className="relative py-4 sm:py-5">
93
94
  {historyError ? (
@@ -109,9 +110,7 @@ export function ChatConversationContent({
109
110
  ) : null}
110
111
  <ChatMessageListContainer
111
112
  messages={messages}
112
- isSending={
113
- hasMessages && isSending && isAwaitingAssistantOutput
114
- }
113
+ isSending={isSending && isAwaitingAssistantOutput}
115
114
  scrollRef={threadRef}
116
115
  sessionKey={sessionKey}
117
116
  />
@@ -132,7 +131,7 @@ export function ChatConversationContent({
132
131
  label={t("chatScrollToBottom")}
133
132
  onClick={scrollToBottom}
134
133
  tooltipSide="top"
135
- 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"
134
+ 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-[var(--interaction-hover)] hover:text-accent-foreground"
136
135
  />
137
136
  ) : null}
138
137
  </div>
@@ -76,7 +76,7 @@ export function ChatConversationHeader({
76
76
  icon={<ArrowLeft className="h-4 w-4" />}
77
77
  label={t("chat")}
78
78
  onClick={onBackToList}
79
- className="inline-flex h-8 w-8 shrink-0 items-center justify-center rounded-full text-muted-foreground transition-colors hover:bg-accent hover:text-accent-foreground"
79
+ className="inline-flex h-8 w-8 shrink-0 items-center justify-center rounded-full text-muted-foreground transition-colors hover:bg-[var(--interaction-hover)] hover:text-accent-foreground"
80
80
  />
81
81
  ) : null}
82
82
  {leading}
@@ -189,7 +189,7 @@ describe("ChatSidebar read state sync", () => {
189
189
  const { container } = renderReadStateSidebar(createTestQueryClient());
190
190
 
191
191
  expect(
192
- screen.getByText("Sessions").parentElement?.nextElementSibling?.className,
192
+ container.querySelector(".custom-scrollbar")?.className,
193
193
  ).toContain("[mask-image:linear-gradient");
194
194
  expect(
195
195
  screen.getByRole("button", { name: "Settings menu" }).parentElement
@@ -0,0 +1,52 @@
1
+ import { fireEvent, render, screen } from "@testing-library/react";
2
+ import { describe, expect, it, vi } from "vitest";
3
+ import { ChatSidebarSessionArea } from "@/features/chat/components/layout/chat-sidebar-desktop-layout";
4
+
5
+ function renderSessionArea(isProjectFirstView: boolean) {
6
+ const onAddProject = vi.fn();
7
+ const onSelectMode = vi.fn();
8
+
9
+ render(
10
+ <ChatSidebarSessionArea
11
+ defaultSessionType="native"
12
+ groups={[]}
13
+ isCollapsed={false}
14
+ isLoading={false}
15
+ isProjectFirstView={isProjectFirstView}
16
+ onAddProject={onAddProject}
17
+ onSelectMode={onSelectMode}
18
+ projectGroups={[]}
19
+ renderSessionItem={() => <></>}
20
+ sessionTypeOptions={[]}
21
+ />,
22
+ );
23
+
24
+ return { onAddProject, onSelectMode };
25
+ }
26
+
27
+ describe("ChatSidebarSessionArea", () => {
28
+ it("reserves the action-row height while switching list modes", () => {
29
+ const { onSelectMode } = renderSessionArea(false);
30
+
31
+ expect(
32
+ screen.getByRole("button", { name: "Time" }).parentElement?.parentElement?.className,
33
+ ).toContain("h-7");
34
+ fireEvent.click(screen.getByRole("button", { name: "Project" }));
35
+
36
+ expect(onSelectMode).toHaveBeenCalledWith("project-first");
37
+ });
38
+
39
+ it("uses a folder-plus icon for the add-project action", () => {
40
+ const { onAddProject } = renderSessionArea(true);
41
+ const addProjectButton = screen.getByRole("button", { name: "Add Project" });
42
+
43
+ expect(
44
+ addProjectButton
45
+ .querySelector("svg")
46
+ ?.classList.contains("lucide-folder-plus"),
47
+ ).toBe(true);
48
+ fireEvent.click(addProjectButton);
49
+
50
+ expect(onAddProject).toHaveBeenCalledOnce();
51
+ });
52
+ });
@@ -81,6 +81,9 @@ function expectCodexSelectedInSessionTypeMenu() {
81
81
  screen.getByRole("button", { name: /Codex/i }).getAttribute("aria-pressed"),
82
82
  ).toBe("true");
83
83
  }
84
+ function expectSessionCreated(sessionType: string, projectRoot?: string) {
85
+ expect(mocks.createSession).toHaveBeenCalledWith({ projectRoot, sessionType });
86
+ }
84
87
 
85
88
  vi.mock("@/features/chat/components/providers/chat-presenter.provider", () => ({
86
89
  usePresenter: () => ({
@@ -249,7 +252,7 @@ describe("ChatSidebar create and list basics", () => {
249
252
 
250
253
  fireEvent.click(screen.getByRole("button", { name: "New Task" }));
251
254
 
252
- expect(mocks.createSession).toHaveBeenCalledWith("codex", undefined);
255
+ expectSessionCreated("codex");
253
256
  });
254
257
 
255
258
  it("hydrates the desktop new-session type from stored preferences", async () => {
@@ -265,7 +268,7 @@ describe("ChatSidebar create and list basics", () => {
265
268
 
266
269
  fireEvent.click(screen.getByRole("button", { name: "New Task" }));
267
270
 
268
- expect(mocks.createSession).toHaveBeenCalledWith("codex", undefined);
271
+ expectSessionCreated("codex");
269
272
  });
270
273
 
271
274
  it("does not animate the desktop create task button width during runtime option hydration", () => {
@@ -311,7 +314,7 @@ describe("ChatSidebar create and list basics", () => {
311
314
  it("renders the lightweight list mode switch in the session header row and toggles to project view", () => {
312
315
  renderSidebar();
313
316
 
314
- expect(screen.getByText("Sessions")).not.toBeNull();
317
+ expect(screen.queryByText("Sessions")).toBeNull();
315
318
  fireEvent.click(screen.getByRole("button", { name: "Project" }));
316
319
 
317
320
  expect(mocks.setListMode).toHaveBeenCalledWith("project-first");
@@ -332,7 +335,7 @@ describe("ChatSidebar create and list basics", () => {
332
335
  fireEvent.click(screen.getByText("Codex"));
333
336
 
334
337
  expect(mocks.setQuery).toHaveBeenCalledWith("release notes");
335
- expect(mocks.createSession).toHaveBeenCalledWith("codex", undefined);
338
+ expectSessionCreated("codex");
336
339
  expect(mocks.goToChatRoot).not.toHaveBeenCalled();
337
340
  });
338
341
 
@@ -377,7 +380,7 @@ describe("ChatSidebar create and list basics", () => {
377
380
 
378
381
  fireEvent.click(screen.getByRole("button", { name: "New Task" }));
379
382
 
380
- expect(mocks.createSession).toHaveBeenCalledWith("native", undefined);
383
+ expectSessionCreated("native");
381
384
  expect(mocks.goToChatRoot).not.toHaveBeenCalled();
382
385
  });
383
386
 
@@ -582,10 +585,7 @@ describe("ChatSidebar project-first mode", () => {
582
585
  );
583
586
  fireEvent.click(screen.getByText("Codex"));
584
587
 
585
- expect(mocks.createSession).toHaveBeenCalledWith(
586
- "codex",
587
- "/tmp/project-beta",
588
- );
588
+ expectSessionCreated("codex", "/tmp/project-beta");
589
589
  expect(mocks.goToSession).not.toHaveBeenCalled();
590
590
  });
591
591
 
@@ -617,10 +617,7 @@ describe("ChatSidebar project-first mode", () => {
617
617
  screen.getByRole("button", { name: "New Task · project-gamma" }),
618
618
  );
619
619
 
620
- expect(mocks.createSession).toHaveBeenCalledWith(
621
- "native",
622
- "/tmp/project-gamma",
623
- );
620
+ expectSessionCreated("native", "/tmp/project-gamma");
624
621
  expect(mocks.goToSession).not.toHaveBeenCalled();
625
622
  });
626
623
 
@@ -652,10 +649,7 @@ describe("ChatSidebar project-first mode", () => {
652
649
  );
653
650
  fireEvent.click(screen.getByText("Codex"));
654
651
 
655
- expect(mocks.createSession).toHaveBeenCalledWith(
656
- "codex",
657
- "/tmp/project-mobile",
658
- );
652
+ expectSessionCreated("codex", "/tmp/project-mobile");
659
653
  expect(mocks.goToChatRoot).not.toHaveBeenCalled();
660
654
  });
661
655
  });
@@ -12,29 +12,20 @@ export type ChatPageProps = {
12
12
  view: MainPanelView;
13
13
  };
14
14
  type UseChatSessionSyncParams = {
15
- view: MainPanelView;
16
15
  routeSessionKey: string | null;
17
- syncRouteSessionSelection: (params: {
18
- isChatView: boolean;
19
- routeSessionKey: string | null;
20
- }) => void;
16
+ syncRouteSessionSelection: (routeSessionKey: string | null) => void;
21
17
  };
22
18
  export function useChatSessionSync(params: UseChatSessionSyncParams): void {
23
19
  const {
24
- view,
25
20
  routeSessionKey,
26
21
  syncRouteSessionSelection,
27
22
  } = params;
28
23
 
29
24
  useLayoutEffect(() => {
30
- syncRouteSessionSelection({
31
- isChatView: view === "chat",
32
- routeSessionKey,
33
- });
25
+ syncRouteSessionSelection(routeSessionKey);
34
26
  }, [
35
27
  routeSessionKey,
36
28
  syncRouteSessionSelection,
37
- view,
38
29
  ]);
39
30
  }
40
31
  type ChatPageLayoutProps = {
@@ -12,6 +12,7 @@ import { t, type I18nLanguage } from "@/shared/lib/i18n";
12
12
  import type { UiTheme } from "@/shared/lib/theme";
13
13
  import { SidebarNavLinkItem } from "@/app/components/layout/sidebar-items";
14
14
  import { IconActionButton } from "@/shared/components/ui/actions/icon-action-button";
15
+ import { SCROLL_BOTTOM_EDGE_FADE_CLASS } from "@/shared/components/ui/scroll-area";
15
16
  import {
16
17
  AlarmClock,
17
18
  Bot,
@@ -19,7 +20,7 @@ import {
19
20
  Inbox,
20
21
  PanelLeftClose,
21
22
  PanelLeftOpen,
22
- Plus,
23
+ FolderPlus,
23
24
  } from "lucide-react";
24
25
  import { useInboxUnreadCount } from "@/features/inbox";
25
26
  import { ChatSidebarSessionList } from "@/features/chat/features/session/components/chat-sidebar-session-list";
@@ -32,7 +33,6 @@ import {
32
33
  SIDEBAR_RAIL_PADDING_X_CLASS,
33
34
  SIDEBAR_RAIL_STACK_CLASS,
34
35
  SIDEBAR_RAIL_SURFACE_CLASS,
35
- SIDEBAR_SCROLL_EDGE_FADE_CLASS,
36
36
  } from "@/app/components/layout/sidebar-rail.styles";
37
37
 
38
38
  const navItems = [
@@ -86,7 +86,7 @@ export function ChatSidebarDesktopHeader({
86
86
  ? "justify-center px-2 py-1.5"
87
87
  : isWindowsHost
88
88
  ? "justify-end px-3 py-1.5"
89
- : "gap-2 px-5 py-2",
89
+ : "gap-2 px-4 py-2",
90
90
  isCollapsed && shouldReserveMacWindowControls ? "pt-8" : null,
91
91
  )}
92
92
  >
@@ -108,36 +108,29 @@ export function ChatSidebarDesktopNav({
108
108
  }) {
109
109
  const unreadCount = useInboxUnreadCount();
110
110
  return (
111
- <>
112
- <div className={cn("pb-2", isCollapsed ? "px-0" : "px-3")}>
113
- <ul className={isCollapsed ? SIDEBAR_RAIL_STACK_CLASS : "space-y-0.5"}>
114
- {navItems.map((item) => (
115
- <li
116
- key={item.target}
117
- className={isCollapsed ? "flex justify-center" : undefined}
118
- >
119
- <SidebarNavLinkItem
120
- to={item.target}
121
- label={item.label()}
122
- icon={item.icon}
123
- density="compact"
124
- collapsed={isCollapsed}
125
- indicator={item.target === "/inbox" && unreadCount > 0}
126
- trailing={item.target === "/inbox" && unreadCount > 0
127
- ? unreadCount > 99 ? "99+" : unreadCount
128
- : undefined}
129
- />
130
- </li>
131
- ))}
132
- </ul>
133
- </div>
134
- <div
135
- className={cn(
136
- "border-t border-border/70",
137
- isCollapsed ? "mx-2 my-1.5" : "mx-4",
138
- )}
139
- />
140
- </>
111
+ <div className={cn("pb-1", isCollapsed ? "px-0" : "px-3")}>
112
+ <ul className={isCollapsed ? SIDEBAR_RAIL_STACK_CLASS : "space-y-0.5"}>
113
+ {navItems.map((item) => (
114
+ <li
115
+ key={item.target}
116
+ className={isCollapsed ? "flex justify-center" : undefined}
117
+ >
118
+ <SidebarNavLinkItem
119
+ to={item.target}
120
+ label={item.label()}
121
+ icon={item.icon}
122
+ density="compact"
123
+ collapsed={isCollapsed}
124
+ indicator={item.target === "/inbox" && unreadCount > 0}
125
+ trailing={item.target === "/inbox" && unreadCount > 0
126
+ ? unreadCount > 99 ? "99+" : unreadCount
127
+ : undefined}
128
+ className={isCollapsed ? undefined : "rounded-lg px-2.5 py-1.5"}
129
+ />
130
+ </li>
131
+ ))}
132
+ </ul>
133
+ </div>
141
134
  );
142
135
  }
143
136
 
@@ -172,14 +165,11 @@ export function ChatSidebarSessionArea({
172
165
 
173
166
  return (
174
167
  <>
175
- <div className="flex items-center justify-between px-5 pb-2 pt-3">
176
- <div className="text-[11px] font-medium uppercase tracking-wider text-muted-foreground/75">
177
- {t("chatSidebarTaskRecords")}
178
- </div>
179
- <div className="flex items-center gap-1">
168
+ <div className="flex items-center justify-end px-4 pb-1 pt-2">
169
+ <div className="flex h-7 items-center gap-1.5">
180
170
  {isProjectFirstView ? (
181
171
  <IconActionButton
182
- icon={<Plus className="h-3.5 w-3.5" />}
172
+ icon={<FolderPlus className="h-3.5 w-3.5" />}
183
173
  label={t("chatProjectAdd")}
184
174
  onClick={onAddProject}
185
175
  />
@@ -193,8 +183,8 @@ export function ChatSidebarSessionArea({
193
183
 
194
184
  <div
195
185
  className={cn(
196
- "custom-scrollbar min-h-0 flex-1 overflow-y-auto px-3 pb-7 pt-2",
197
- SIDEBAR_SCROLL_EDGE_FADE_CLASS,
186
+ "custom-scrollbar min-h-0 flex-1 overflow-y-auto px-3 pb-4 pt-1",
187
+ SCROLL_BOTTOM_EDGE_FADE_CLASS,
198
188
  )}
199
189
  >
200
190
  <ChatSidebarSessionList
@@ -246,7 +236,7 @@ export function ChatSidebarDesktopFooter({
246
236
  return (
247
237
  <div
248
238
  className={cn(
249
- "py-3",
239
+ "py-2",
250
240
  isCollapsed
251
241
  ? cn("flex justify-center", SIDEBAR_RAIL_PADDING_X_CLASS)
252
242
  : "px-3",
@@ -1,4 +1,4 @@
1
- import { Button, type ButtonProps } from "@/shared/components/ui/button";
1
+ import { Button } from "@/shared/components/ui/button";
2
2
  import { IconActionButton } from "@/shared/components/ui/actions/icon-action-button";
3
3
  import { Input } from "@/shared/components/ui/input";
4
4
  import {
@@ -20,51 +20,6 @@ import {
20
20
  } from "@/app/components/layout/sidebar-rail.styles";
21
21
 
22
22
  type SessionTypeOption = ChatSessionTypeOption;
23
- type NewSessionActionStyleVariant =
24
- | "neutralSurface"
25
- | "brandSoft"
26
- | "brandTextSurface"
27
- | "brandSolid";
28
-
29
- const NEW_SESSION_ACTION_STYLE_VARIANT: NewSessionActionStyleVariant =
30
- "brandSoft";
31
-
32
- const NEW_SESSION_ACTION_STYLE_CLASSES: Record<
33
- NewSessionActionStyleVariant,
34
- {
35
- leftVariant: ButtonProps["variant"];
36
- leftClassName: string;
37
- rightClassName: string;
38
- }
39
- > = {
40
- neutralSurface: {
41
- leftVariant: "ghost",
42
- leftClassName:
43
- "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",
44
- rightClassName:
45
- "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",
46
- },
47
- brandSoft: {
48
- leftVariant: "ghost",
49
- leftClassName:
50
- "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",
51
- rightClassName:
52
- "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",
53
- },
54
- brandTextSurface: {
55
- leftVariant: "ghost",
56
- leftClassName:
57
- "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",
58
- rightClassName:
59
- "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",
60
- },
61
- brandSolid: {
62
- leftVariant: "primary",
63
- leftClassName: "",
64
- rightClassName:
65
- "bg-primary text-primary-foreground shadow-sm hover:bg-primary-600 hover:text-primary-foreground active:bg-primary-700",
66
- },
67
- };
68
23
 
69
24
  type ChatSidebarToolbarProps = {
70
25
  query: string;
@@ -117,8 +72,6 @@ export function ChatSidebarDesktopToolbar(props: ChatSidebarToolbarProps) {
117
72
  collapsed = false,
118
73
  } = props;
119
74
  const supportsSessionTypeSwitch = sessionTypeOptions.length > 1;
120
- const actionStyle =
121
- NEW_SESSION_ACTION_STYLE_CLASSES[NEW_SESSION_ACTION_STYLE_VARIANT];
122
75
 
123
76
  if (collapsed) {
124
77
  return (
@@ -183,21 +136,20 @@ export function ChatSidebarDesktopToolbar(props: ChatSidebarToolbarProps) {
183
136
 
184
137
  return (
185
138
  <>
186
- <div className="px-4 pb-3">
187
- <div className="flex items-center gap-2">
139
+ <div className="px-4 pb-2">
140
+ <div className="flex overflow-hidden rounded-lg bg-primary/10 text-primary ring-1 ring-primary/10">
188
141
  <Button
189
- variant={actionStyle.leftVariant}
142
+ variant="ghost"
190
143
  className={cn(
191
- "min-w-0 rounded-xl",
192
- actionStyle.leftClassName,
193
- supportsSessionTypeSwitch ? "flex-1 rounded-r-md" : "w-full",
144
+ "min-w-0 rounded-none bg-transparent px-3 text-primary shadow-none hover:bg-primary/10 hover:text-primary-700 active:bg-primary/15",
145
+ supportsSessionTypeSwitch ? "flex-1" : "w-full",
194
146
  )}
195
147
  onClick={() => {
196
148
  onCreateMenuOpenChange(false);
197
149
  onCreateSession(selectedNewSessionType);
198
150
  }}
199
151
  >
200
- <Plus className="mr-2 h-4 w-4" />
152
+ <Plus className="mr-1.5 h-4 w-4" />
201
153
  {t("chatSidebarNewTask")}
202
154
  </Button>
203
155
  {supportsSessionTypeSwitch ? (
@@ -208,19 +160,16 @@ export function ChatSidebarDesktopToolbar(props: ChatSidebarToolbarProps) {
208
160
  <PopoverTrigger asChild>
209
161
  <IconActionButton
210
162
  icon={
211
- <span className="inline-flex items-center gap-0.5">
163
+ <span className="inline-flex items-center gap-1">
212
164
  <SessionTypeTriggerIcon
213
165
  option={selectedNewSessionTypeOption}
214
166
  />
215
- <ChevronDown className="h-3 w-3 opacity-60" />
167
+ <ChevronDown className="h-3 w-3 opacity-50" />
216
168
  </span>
217
169
  }
218
170
  label={t("chatSessionTypeLabel")}
219
171
  tooltip={t("chatSessionTypeLabel")}
220
- className={cn(
221
- "h-9 w-12 shrink-0 rounded-xl rounded-l-md",
222
- actionStyle.rightClassName,
223
- )}
172
+ className="h-9 w-12 shrink-0 rounded-none border-l border-primary/10 bg-transparent text-primary shadow-none hover:bg-primary/10 hover:text-primary-700 active:bg-primary/15"
224
173
  />
225
174
  </PopoverTrigger>
226
175
  <ChatPopoverContent
@@ -241,14 +190,14 @@ export function ChatSidebarDesktopToolbar(props: ChatSidebarToolbarProps) {
241
190
  </div>
242
191
  </div>
243
192
 
244
- <div className="px-4 pb-3">
193
+ <div className="px-4 pb-2">
245
194
  <div className="relative">
246
- <Search className="pointer-events-none absolute left-3 top-2.5 h-3.5 w-3.5 text-muted-foreground/70" />
195
+ <Search className="pointer-events-none absolute left-2.5 top-1/2 h-3.5 w-3.5 -translate-y-1/2 text-muted-foreground/60" />
247
196
  <Input
248
197
  value={query}
249
198
  onChange={(event) => onQueryChange(event.target.value)}
250
199
  placeholder={t("chatSidebarSearchPlaceholder")}
251
- className="h-9 rounded-lg pl-8 text-xs"
200
+ className="h-8 rounded-lg border-transparent bg-background/55 pl-8 text-xs shadow-none hover:bg-background/75"
252
201
  />
253
202
  </div>
254
203
  </div>
@@ -102,13 +102,10 @@ export function ChatSidebarUtilityMenu({
102
102
  <button
103
103
  type="button"
104
104
  aria-label={t("settingsMenu")}
105
- className="flex w-full items-center gap-2.5 rounded-xl px-3 py-2 text-[13px] font-medium text-muted-foreground transition-colors duration-base hover:bg-gray-200/60 hover:text-gray-900 focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-border"
105
+ className="flex w-full items-center gap-2.5 rounded-lg px-2.5 py-1.5 text-[13px] font-medium text-muted-foreground transition-colors duration-base hover:bg-background/70 hover:text-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-border"
106
106
  >
107
107
  <Settings className="h-4 w-4 shrink-0 text-muted-foreground/70" />
108
108
  <span className="min-w-0 flex-1 text-left">{t("settings")}</span>
109
- <span className="max-w-[112px] truncate text-[13px] text-muted-foreground">
110
- {currentThemeLabel} / {currentLanguageLabel}
111
- </span>
112
109
  </button>
113
110
  </PopoverTrigger>
114
111
  )}
@@ -389,7 +389,7 @@ describe("SessionConversationArea input boundary", () => {
389
389
 
390
390
  expect(screen.getByTestId("conversation-bottom-slot")).toBeTruthy();
391
391
  expect(screen.getByText(/出错了|Something went wrong/)).toBeTruthy();
392
- expect(screen.getByText("Run failed: Invalid API Key")).toBeTruthy();
392
+ expect(screen.getByText(/Failed · Invalid API Key|失败 · Invalid API Key/)).toBeTruthy();
393
393
  });
394
394
 
395
395
  it("does not surface user-cancelled previews as conversation errors", () => {