@nextclaw/ui 0.15.20 → 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 (191) hide show
  1. package/CHANGELOG.md +42 -0
  2. package/dist/assets/{api-tWUa6l9J.js → api-B1yHQYs-.js} +6 -6
  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-BQ29ZmDE.js → book-open-oSmyVXea.js} +1 -1
  6. package/dist/assets/{card-CtKueklV.js → card-CGvJZTZp.js} +1 -1
  7. package/dist/assets/{channels-list-page-C8y3rqYt.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-rYHnBo8t.js → dist-CxUhurq9.js} +1 -1
  13. package/dist/assets/doc-browser-0eL8AX42.js +1 -0
  14. package/dist/assets/{doc-browser-D8ng4EUc.js → doc-browser-DEqE9lsh.js} +1 -1
  15. package/dist/assets/{doc-browser-context-CPHNzQ8m.js → doc-browser-context-DkzFIUoK.js} +1 -1
  16. package/dist/assets/{eye-CiJz3ODY.js → eye-gOWQGt8N.js} +1 -1
  17. package/dist/assets/{form-actions-BnZVzLZf.js → form-actions-CgjYZUzM.js} +1 -1
  18. package/dist/assets/index-DSAUWCfl.js +2 -0
  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-BXI-Kdhd.js → logo-badge-BNcsDsUM.js} +1 -1
  22. package/dist/assets/{mcp-marketplace-page-CxNe-Q9Q.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-CR_mgEVp.js → model-config-page-7MIXg2fQ.js} +1 -1
  25. package/dist/assets/{navigation-link-C1cvB7y4.js → navigation-link-B3iFOdxx.js} +1 -1
  26. package/dist/assets/plus-yvhFgfal.js +1 -0
  27. package/dist/assets/{popover-ZoDwVwwd.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-DKewcdAl.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-DwjO-Vs6.js +1 -0
  36. package/dist/assets/{save-BrckuAXY.js → save-BINMWate.js} +1 -1
  37. package/dist/assets/{search-BhXQB2TO.js → search-7M38azRp.js} +1 -1
  38. package/dist/assets/{search-config-page-DFOQ6SDJ.js → search-config-page-T6tMhXbP.js} +1 -1
  39. package/dist/assets/{secrets-config-page-BqqeClqE.js → secrets-config-page-2mVejXf2.js} +2 -2
  40. package/dist/assets/security-config-DD5RrjDe.js +1 -0
  41. package/dist/assets/{select-CVzGGT38.js → select-BvidP5Oa.js} +1 -1
  42. package/dist/assets/{settings-2-BP6L2enG.js → settings-2-DqZstREV.js} +1 -1
  43. package/dist/assets/skeleton-BEQW2Dva.js +1 -0
  44. package/dist/assets/{tabs-B-QWjKCL.js → tabs-BeLxVmkt.js} +1 -1
  45. package/dist/assets/{tag-chip-C88mxjwx.js → tag-chip-BoDaLnWt.js} +1 -1
  46. package/dist/assets/{trash-2-ck3t33I3.js → trash-2-CYUskMMN.js} +1 -1
  47. package/dist/assets/{use-config-BAr5bH2c.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-k1isXfNm.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 +8 -8
  53. package/src/app/components/__tests__/app-notification-runtime.test.tsx +32 -0
  54. package/src/app/components/app-notification-runtime.tsx +15 -0
  55. package/src/app/components/layout/sidebar-items.tsx +23 -9
  56. package/src/app/components/layout/sidebar-rail.styles.ts +0 -2
  57. package/src/app/components/layout/sidebar.tsx +2 -2
  58. package/src/app/configs/app-navigation.config.ts +21 -0
  59. package/src/app/index.tsx +22 -2
  60. package/src/app/presenters/app.presenter.ts +8 -0
  61. package/src/app/styles/design-system.css +12 -0
  62. package/src/features/chat/components/chat-sidebar-list-mode-switch.tsx +2 -2
  63. package/src/features/chat/components/conversation/__tests__/chat-conversation-content.test.tsx +19 -1
  64. package/src/features/chat/components/conversation/__tests__/chat-conversation-header-section.test.tsx +1 -1
  65. package/src/features/chat/components/conversation/chat-conversation-content.tsx +5 -6
  66. package/src/features/chat/components/conversation/chat-conversation-header.tsx +1 -1
  67. package/src/features/chat/components/layout/__tests__/chat-sidebar-read-state.test.tsx +1 -1
  68. package/src/features/chat/components/layout/__tests__/chat-sidebar-session-area.test.tsx +52 -0
  69. package/src/features/chat/components/layout/__tests__/chat-sidebar.test.tsx +11 -17
  70. package/src/features/chat/components/layout/chat-page-shell.tsx +9 -13
  71. package/src/features/chat/components/layout/chat-sidebar-desktop-layout.tsx +40 -38
  72. package/src/features/chat/components/layout/chat-sidebar-toolbar.tsx +13 -64
  73. package/src/features/chat/components/layout/chat-sidebar-utility-menu.tsx +1 -4
  74. package/src/features/chat/features/conversation/components/__tests__/session-conversation-area.test.tsx +1 -1
  75. package/src/features/chat/features/conversation/components/__tests__/session-conversation-input.streaming.test.tsx +15 -4
  76. package/src/features/chat/features/conversation/components/session-conversation-area.tsx +7 -7
  77. package/src/features/chat/features/conversation/components/session-conversation-input.tsx +4 -2
  78. package/src/features/chat/features/conversation/hooks/use-session-conversation-input-attachments.ts +1 -0
  79. package/src/features/chat/features/conversation/utils/session-conversation-input-toolbar.utils.ts +1 -0
  80. package/src/features/chat/features/conversation/utils/session-queued-input.utils.ts +1 -0
  81. package/src/features/chat/features/input/utils/__tests__/chat-input-bar.utils.test.ts +3 -0
  82. package/src/features/chat/features/input/utils/chat-input-bar.utils.ts +0 -1
  83. package/src/features/chat/features/input/utils/chat-input-toolbar.utils.ts +3 -0
  84. package/src/features/chat/features/message/utils/__tests__/chat-message-timeline.utils.test.ts +29 -0
  85. package/src/features/chat/features/message/utils/chat-message-timeline.utils.ts +4 -1
  86. package/src/features/chat/features/session/components/chat-sidebar-session-item.tsx +10 -10
  87. package/src/features/chat/features/session/components/chat-sidebar-session-list.tsx +2 -2
  88. package/src/features/chat/features/session/components/session-header/chat-session-header-menu-item.tsx +1 -1
  89. package/src/features/chat/features/session/components/session-header/chat-session-project-badge.tsx +1 -1
  90. package/src/features/chat/features/session/components/session-header/chat-session-switcher-groups.tsx +1 -1
  91. package/src/features/chat/features/session/components/session-header/chat-session-title-switcher.tsx +1 -1
  92. package/src/features/chat/features/session/utils/__tests__/chat-session-display.utils.test.ts +26 -0
  93. package/src/features/chat/features/session/utils/__tests__/ncp-session-adapter.utils.test.ts +4 -2
  94. package/src/features/chat/features/session/utils/chat-session-display.utils.ts +48 -12
  95. package/src/features/chat/features/session/utils/ncp-session-adapter.utils.ts +13 -4
  96. package/src/features/chat/features/welcome/components/__tests__/chat-conversation-welcome.test.tsx +6 -0
  97. package/src/features/chat/features/welcome/components/__tests__/chat-welcome.test.tsx +10 -7
  98. package/src/features/chat/features/welcome/components/chat-welcome-agent-picker.tsx +1 -1
  99. package/src/features/chat/features/welcome/components/chat-welcome-project-picker.tsx +3 -14
  100. package/src/features/chat/features/welcome/components/chat-welcome-prompt-suggestions.tsx +37 -0
  101. package/src/features/chat/features/welcome/components/chat-welcome-session-type-picker.tsx +1 -1
  102. package/src/features/chat/features/welcome/components/chat-welcome.tsx +33 -31
  103. package/src/features/chat/features/workspace/components/__tests__/chat-session-workspace-panel-content.test.tsx +1 -1
  104. package/src/features/chat/features/workspace/components/__tests__/chat-session-workspace-panel.test.tsx +1 -1
  105. package/src/features/chat/index.ts +1 -0
  106. package/src/features/chat/managers/__tests__/chat-completion-notification.manager.test.ts +177 -0
  107. package/src/features/chat/managers/__tests__/chat-session-list.manager.test.ts +15 -3
  108. package/src/features/chat/managers/__tests__/chat-thread.manager.test.ts +3 -10
  109. package/src/features/chat/managers/chat-completion-notification.manager.ts +146 -0
  110. package/src/features/chat/managers/chat-session-list.manager.ts +3 -23
  111. package/src/features/chat/managers/chat-thread.manager.ts +2 -5
  112. package/src/features/chat/pages/__tests__/ncp-chat-page.test.tsx +32 -11
  113. package/src/features/chat/pages/ncp-chat-page.tsx +9 -1
  114. package/src/features/chat/types/chat-input-bar.types.ts +1 -0
  115. package/src/features/cron/components/cron-task-discovery.tsx +1 -1
  116. package/src/features/inbox/components/inbox-delivery-content.test.tsx +57 -0
  117. package/src/features/inbox/components/inbox-delivery-content.tsx +118 -0
  118. package/src/features/inbox/components/inbox-reader-dialog.test.tsx +77 -0
  119. package/src/features/inbox/components/inbox-reader-dialog.tsx +224 -0
  120. package/src/features/inbox/components/inbox-runtime.tsx +14 -0
  121. package/src/features/inbox/hooks/use-inbox-deliveries.ts +14 -0
  122. package/src/features/inbox/index.ts +7 -0
  123. package/src/features/inbox/managers/inbox.manager.test.ts +86 -0
  124. package/src/features/inbox/managers/inbox.manager.ts +178 -0
  125. package/src/features/inbox/pages/inbox-page.test.ts +28 -0
  126. package/src/features/inbox/pages/inbox-page.tsx +385 -0
  127. package/src/features/inbox/stores/inbox.store.ts +23 -0
  128. package/src/features/marketplace/components/marketplace-page-parts.tsx +1 -1
  129. package/src/features/notifications/components/__tests__/app-notification-toast.test.tsx +51 -0
  130. package/src/features/notifications/components/app-notification-toast.tsx +80 -0
  131. package/src/features/notifications/index.ts +8 -0
  132. package/src/features/notifications/managers/__tests__/app-notification.manager.test.ts +75 -0
  133. package/src/features/notifications/managers/app-notification.manager.ts +41 -0
  134. package/src/features/panel-apps/components/panel-app-list-item.tsx +1 -1
  135. package/src/features/panel-apps/components/panel-app-toolbar.tsx +2 -2
  136. package/src/features/panel-apps/components/panel-apps-list.tsx +4 -4
  137. package/src/features/service-apps/components/service-apps-panel.tsx +1 -1
  138. package/src/features/settings/components/config/provider-auth-section.tsx +90 -59
  139. package/src/features/settings/components/config/provider-form.tsx +10 -17
  140. package/src/features/settings/components/config/provider-status-badge.tsx +4 -2
  141. package/src/features/settings/pages/__tests__/providers-config-page.test.tsx +12 -0
  142. package/src/features/settings/pages/providers-config-page.tsx +2 -1
  143. package/src/platforms/mobile/components/__tests__/mobile-app-shell.test.tsx +13 -7
  144. package/src/platforms/mobile/components/__tests__/mobile-bottom-nav.test.tsx +18 -20
  145. package/src/platforms/mobile/components/mobile-bottom-nav.tsx +14 -7
  146. package/src/shared/components/doc-browser/doc-browser-panel-parts.tsx +1 -1
  147. package/src/shared/components/path-picker/server-path-picker-directory-list.tsx +1 -1
  148. package/src/shared/components/path-picker/server-path-picker-locations.tsx +1 -1
  149. package/src/shared/components/path-picker/server-path-picker-navigation-bar.tsx +3 -3
  150. package/src/shared/components/ui/actions/__tests__/icon-action-button.test.tsx +1 -1
  151. package/src/shared/components/ui/actions/icon-action-button.tsx +1 -1
  152. package/src/shared/components/ui/button.tsx +4 -4
  153. package/src/shared/components/ui/dialog.tsx +1 -1
  154. package/src/shared/components/ui/input.tsx +1 -1
  155. package/src/shared/components/ui/scroll-area.tsx +2 -0
  156. package/src/shared/components/ui/select.tsx +1 -1
  157. package/src/shared/components/ui/switch.tsx +1 -1
  158. package/src/shared/components/ui/tabs.tsx +1 -1
  159. package/src/shared/components/ui/textarea.tsx +1 -1
  160. package/src/shared/lib/api/ncp-session.types.ts +1 -0
  161. package/src/shared/lib/api/types.ts +2 -0
  162. package/src/shared/lib/i18n/index.ts +4 -0
  163. package/src/shared/lib/i18n/locales/en-US/chat.json +16 -14
  164. package/src/shared/lib/i18n/locales/en-US/inbox.json +30 -0
  165. package/src/shared/lib/i18n/locales/zh-CN/chat.json +16 -14
  166. package/src/shared/lib/i18n/locales/zh-CN/inbox.json +30 -0
  167. package/src/shared/lib/provider-models/__tests__/anonymous-provider-configuration.test.ts +40 -0
  168. package/src/shared/lib/provider-models/index.ts +4 -3
  169. package/dist/assets/app-presenter-provider-s-FrOinZ.js +0 -106
  170. package/dist/assets/appearance-settings-page-dMRBD7ni.js +0 -1
  171. package/dist/assets/chat-page-MXBltS3G.js +0 -1
  172. package/dist/assets/config-split-page-uC6d_r9c.js +0 -1
  173. package/dist/assets/confirm-dialog-C1pbqHZX.js +0 -5
  174. package/dist/assets/desktop-update-config-DssP8LCk.js +0 -1
  175. package/dist/assets/doc-browser-AXF1zRsM.js +0 -1
  176. package/dist/assets/index-CocHWEsI.js +0 -2
  177. package/dist/assets/index-DN6KM7WD.css +0 -1
  178. package/dist/assets/loader-circle-AOpK5kkW.js +0 -1
  179. package/dist/assets/mcp-marketplace-page-5ogeBHnl.js +0 -1
  180. package/dist/assets/plus-DI37QuHc.js +0 -1
  181. package/dist/assets/provider-scoped-model-input-DIL6Ex_d.js +0 -1
  182. package/dist/assets/providers-config-page-D2NXVctW.js +0 -1
  183. package/dist/assets/react-39CDNZlM.js +0 -8
  184. package/dist/assets/remote-DYOOO1YG.js +0 -1
  185. package/dist/assets/rotate-cw-PmqA8LQT.js +0 -1
  186. package/dist/assets/runtime-config-page-0lb5NyKc.js +0 -1
  187. package/dist/assets/security-config-13AtPIlm.js +0 -1
  188. package/dist/assets/skeleton-OwCNQJ7M.js +0 -1
  189. package/dist/assets/use-confirm-dialog-C4e0nG_4.js +0 -1
  190. package/dist/assets/x-C1ijNWcE.js +0 -1
  191. package/src/features/chat/features/welcome/components/chat-welcome-capability-grid.tsx +0 -59
@@ -16,6 +16,7 @@ import {
16
16
  type SidebarNavListItem,
17
17
  } from "@/app/components/layout/sidebar-items";
18
18
  import { IconActionButton } from "@/shared/components/ui/actions/icon-action-button";
19
+ import { SCROLL_BOTTOM_EDGE_FADE_CLASS } from "@/shared/components/ui/scroll-area";
19
20
  import { useAppPresenter } from "@/app/components/app-presenter-provider";
20
21
  import { useRemoteStatus } from "@/features/remote";
21
22
  import { getSettingsNavSections } from "@/app/configs/app-navigation.config";
@@ -29,7 +30,6 @@ import {
29
30
  SIDEBAR_RAIL_STACK_CLASS,
30
31
  SIDEBAR_RAIL_SURFACE_CLASS,
31
32
  SIDEBAR_RAIL_WIDTH_CLASS,
32
- SIDEBAR_SCROLL_EDGE_FADE_CLASS,
33
33
  } from "@/app/components/layout/sidebar-rail.styles";
34
34
 
35
35
  type SidebarNavSection = {
@@ -141,7 +141,7 @@ function SidebarNavigation({
141
141
  className={cn(
142
142
  "custom-scrollbar min-h-0 flex-1 overflow-y-auto",
143
143
  isCollapsed ? "pr-0" : "pr-1",
144
- SIDEBAR_SCROLL_EDGE_FADE_CLASS,
144
+ SCROLL_BOTTOM_EDGE_FADE_CLASS,
145
145
  )}
146
146
  >
147
147
  {!isCollapsed ? (
@@ -5,6 +5,7 @@ import {
5
5
  AlarmClock,
6
6
  Cpu,
7
7
  Download,
8
+ Inbox,
8
9
  KeyRound,
9
10
  MessageCircle,
10
11
  MessageSquare,
@@ -44,6 +45,8 @@ export function isMainWorkspaceRoute(pathname: string): boolean {
44
45
  return (
45
46
  normalized === "/chat" ||
46
47
  normalized.startsWith("/chat/") ||
48
+ normalized === "/inbox" ||
49
+ normalized.startsWith("/inbox/") ||
47
50
  normalized === "/skills" ||
48
51
  normalized.startsWith("/skills/") ||
49
52
  normalized === "/cron" ||
@@ -67,6 +70,11 @@ export function getMobileBottomNavItems(
67
70
  label: translate("chat"),
68
71
  icon: MessageCircle,
69
72
  },
73
+ {
74
+ target: "/inbox",
75
+ label: translate("inboxTitle"),
76
+ icon: Inbox,
77
+ },
70
78
  {
71
79
  target: "/skills",
72
80
  label: translate("marketplaceFilterSkills"),
@@ -94,6 +102,11 @@ export function getMainSidebarNavItems(
94
102
  label: translate("chat"),
95
103
  icon: MessageCircle,
96
104
  },
105
+ {
106
+ target: "/inbox",
107
+ label: translate("inboxTitle"),
108
+ icon: Inbox,
109
+ },
97
110
  {
98
111
  target: "/chat/cron",
99
112
  label: translate("cron"),
@@ -227,6 +240,14 @@ export function resolveMobileRouteMeta(
227
240
  };
228
241
  }
229
242
 
243
+ if (normalized === "/inbox" || normalized.startsWith("/inbox/")) {
244
+ return {
245
+ title: translate("inboxTitle"),
246
+ backTarget: normalized === "/inbox" ? null : "/inbox",
247
+ backLabel: translate("inboxTitle"),
248
+ };
249
+ }
250
+
230
251
  if (normalized === "/skills" || normalized.startsWith("/skills/")) {
231
252
  return {
232
253
  title: translate("marketplaceFilterSkills"),
package/src/app/index.tsx CHANGED
@@ -1,14 +1,17 @@
1
1
  import { lazy, Suspense, useEffect } from "react";
2
- import type { ReactElement } from "react";
2
+ import type { CSSProperties, ReactElement } from "react";
3
3
  import { QueryClientProvider } from "@tanstack/react-query";
4
4
  import { Navigate, Route, Routes } from "react-router-dom";
5
5
  import { Toaster } from "sonner";
6
6
  import { appQueryClient } from "@/app-query-client";
7
7
  import { AppPresenterProvider } from "@/app/components/app-presenter-provider";
8
+ import { AppNotificationRuntime } from "@/app/components/app-notification-runtime";
8
9
  import { AppLayout } from "@/app/components/layout/app-layout";
10
+ import { SIDEBAR_RAIL_WIDTH_PX } from "@/app/components/layout/sidebar-rail.styles";
9
11
  import { SettingsEntryPage } from "@/app/components/layout/settings-entry-page";
10
12
  import { LoginPage } from "@/components/auth/login-page";
11
13
  import { AccountPanel } from "@/features/account";
14
+ import { InboxRuntime } from "@/features/inbox";
12
15
  import { ServiceActionAuthorizationDialog } from "@/features/service-apps";
13
16
  import { runtimeUpdateManager, useSystemStatusSources } from "@/features/system-status";
14
17
  import {
@@ -21,6 +24,8 @@ import {
21
24
  } from "@/pwa/components/pwa-install-entry";
22
25
  import { startNextClawPwa } from "@/pwa/register-pwa";
23
26
 
27
+ const NOTIFICATION_TOASTER_STYLE = { "--width": "320px" } as CSSProperties;
28
+
24
29
  const ModelConfigPage = lazy(async () => ({
25
30
  default: (await import("@/features/settings/pages/model-config-page"))
26
31
  .ModelConfigPage,
@@ -117,6 +122,10 @@ const protectedRouteDefinitions: ProtectedRouteDefinition[] = [
117
122
  path: "/cron",
118
123
  element: createLazyElement(<ChatPage view="cron" />),
119
124
  },
125
+ {
126
+ path: "/inbox/:deliveryId?",
127
+ element: createLazyElement(<ChatPage view="inbox" />),
128
+ },
120
129
  {
121
130
  path: "/appearance",
122
131
  element: createLazyElement(<AppearanceSettingsPage />),
@@ -219,6 +228,8 @@ function ProtectedApp() {
219
228
 
220
229
  return (
221
230
  <AppPresenterProvider>
231
+ <AppNotificationRuntime />
232
+ <InboxRuntime />
222
233
  <AppLayout>
223
234
  <ProtectedRoutes />
224
235
  </AppLayout>
@@ -257,7 +268,16 @@ export default function AppContent() {
257
268
  <QueryClientProvider client={appQueryClient}>
258
269
  <AuthGate />
259
270
  <PwaInstallBanner />
260
- <Toaster position="top-right" richColors offset={window.nextclawDesktop?.platform === "win32" ? 56 : undefined} mobileOffset={window.nextclawDesktop?.platform === "win32" ? 56 : undefined} />
271
+ <Toaster
272
+ position="top-right"
273
+ richColors
274
+ offset={{
275
+ top: window.nextclawDesktop?.platform === "win32" ? 56 : 44,
276
+ right: SIDEBAR_RAIL_WIDTH_PX + 16,
277
+ }}
278
+ mobileOffset={window.nextclawDesktop?.platform === "win32" ? 56 : undefined}
279
+ style={NOTIFICATION_TOASTER_STYLE}
280
+ />
261
281
  </QueryClientProvider>
262
282
  );
263
283
  }
@@ -1,10 +1,13 @@
1
1
  import { AccountManager } from '@/features/account';
2
2
  import { viewportLayoutManager } from '@/app/managers/viewport-layout.manager';
3
3
  import {
4
+ ChatCompletionNotificationManager,
4
5
  ChatDraftIntentManager,
5
6
  useChatThreadStore,
6
7
  } from '@/features/chat';
8
+ import { AppNotificationManager } from '@/features/notifications';
7
9
  import { PanelAppBridgeManager } from '@/features/panel-apps';
10
+ import { InboxManager } from '@/features/inbox';
8
11
  import { RightPanelResourceRouteResolver } from '@/features/right-panel-resources';
9
12
  import { RemoteAccessManager } from '@/features/remote';
10
13
  import { ServiceActionAuthorizationManager } from '@/features/service-apps';
@@ -19,6 +22,11 @@ function isChatWorkspacePanelOpen(snapshot: ChatThreadSnapshot): boolean {
19
22
  }
20
23
 
21
24
  export class AppPresenter {
25
+ notificationManager = new AppNotificationManager();
26
+ inboxManager = new InboxManager();
27
+ chatCompletionNotificationManager = new ChatCompletionNotificationManager(
28
+ this.notificationManager,
29
+ );
22
30
  accountManager = new AccountManager();
23
31
  rightPanelResourceRouteResolver = new RightPanelResourceRouteResolver();
24
32
  notifyRightPanelOpened = () => {
@@ -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
  });
@@ -6,34 +6,26 @@ import { CronConfig } from "@/features/cron";
6
6
  import { MarketplacePage } from "@/features/marketplace";
7
7
  import { useViewportLayout } from "@/app/hooks/use-viewport-layout";
8
8
  import { ChatMobileShell } from "@/platforms/mobile";
9
- export type MainPanelView = "chat" | "cron" | "skills" | "agents";
9
+ import { InboxPage } from "@/features/inbox";
10
+ export type MainPanelView = "chat" | "cron" | "skills" | "agents" | "inbox";
10
11
  export type ChatPageProps = {
11
12
  view: MainPanelView;
12
13
  };
13
14
  type UseChatSessionSyncParams = {
14
- view: MainPanelView;
15
15
  routeSessionKey: string | null;
16
- syncRouteSessionSelection: (params: {
17
- isChatView: boolean;
18
- routeSessionKey: string | null;
19
- }) => void;
16
+ syncRouteSessionSelection: (routeSessionKey: string | null) => void;
20
17
  };
21
18
  export function useChatSessionSync(params: UseChatSessionSyncParams): void {
22
19
  const {
23
- view,
24
20
  routeSessionKey,
25
21
  syncRouteSessionSelection,
26
22
  } = params;
27
23
 
28
24
  useLayoutEffect(() => {
29
- syncRouteSessionSelection({
30
- isChatView: view === "chat",
31
- routeSessionKey,
32
- });
25
+ syncRouteSessionSelection(routeSessionKey);
33
26
  }, [
34
27
  routeSessionKey,
35
28
  syncRouteSessionSelection,
36
- view,
37
29
  ]);
38
30
  }
39
31
  type ChatPageLayoutProps = {
@@ -51,7 +43,11 @@ export function ChatPageLayout({ view, confirmDialog }: ChatPageLayoutProps) {
51
43
  isMobile ? <ChatMobileShell /> : <ChatConversationPanel />
52
44
  ) : (
53
45
  <section className="flex-1 min-h-0 overflow-hidden bg-background">
54
- {view === "cron" ? (
46
+ {view === "inbox" ? (
47
+ <div className="mx-auto flex h-full min-h-0 w-full max-w-[min(1180px,100%)] flex-col py-4 sm:px-6 sm:py-5">
48
+ <InboxPage />
49
+ </div>
50
+ ) : view === "cron" ? (
55
51
  <div className="h-full overflow-auto custom-scrollbar">
56
52
  <div className="mx-auto w-full max-w-[min(1120px,100%)] px-4 py-4 sm:px-6 sm:py-5">
57
53
  <CronConfig />