@nextclaw/ui 0.14.1 → 0.14.2

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 (165) hide show
  1. package/CHANGELOG.md +29 -0
  2. package/dist/assets/{api-DPcvCLfa.js → api-a14tR_z1.js} +7 -7
  3. package/dist/assets/channels-list-page-CFGkm4DT.js +8 -0
  4. package/dist/assets/chat-page-CEdavJXc.js +105 -0
  5. package/dist/assets/{config-split-page-BCJhqdK9.js → config-split-page-B5b9B0uE.js} +1 -1
  6. package/dist/assets/{confirm-dialog-CWh5FfK2.js → confirm-dialog-ClH_Ec7n.js} +1 -1
  7. package/dist/assets/{desktop-update-config-DsRhBizi.js → desktop-update-config-DPNDnVpk.js} +1 -1
  8. package/dist/assets/{dist-DPrgcEC0.js → dist-BtCS2Ucq.js} +1 -1
  9. package/dist/assets/{dist-DsE4OsSS.js → dist-CLGGLm8b.js} +1 -1
  10. package/dist/assets/doc-browser-5nMWYKBD.js +1 -0
  11. package/dist/assets/doc-browser-BJRLhztv.js +1 -0
  12. package/dist/assets/doc-browser-context-D4JwMMxK.js +1 -0
  13. package/dist/assets/{ellipsis-B81FStLm.js → ellipsis-CtJXjIol.js} +1 -1
  14. package/dist/assets/{external-link-C_dmG-WJ.js → external-link-CdJiG13p.js} +1 -1
  15. package/dist/assets/index-BDN1zzMQ.js +105 -0
  16. package/dist/assets/index-g3uSZXoc.css +1 -0
  17. package/dist/assets/mcp-marketplace-page-BU0kIjhR.js +40 -0
  18. package/dist/assets/mcp-marketplace-page-sMNFSq_T.js +1 -0
  19. package/dist/assets/middleware-CkFT3R4v.js +1 -0
  20. package/dist/assets/model-config-page-D6hYXROd.js +1 -0
  21. package/dist/assets/plus-DPjb0piO.js +1 -0
  22. package/dist/assets/provider-scoped-model-input-DAX2HgmR.js +1 -0
  23. package/dist/assets/providers-config-page-BzAeaACA.js +1 -0
  24. package/dist/assets/{react-sZz5HSCm.js → react-C6cZgP44.js} +1 -1
  25. package/dist/assets/remote-B0G80uPr.js +1 -0
  26. package/dist/assets/rotate-cw-DnEB1Rju.js +1 -0
  27. package/dist/assets/runtime-config-page-C5xKaV3i.js +1 -0
  28. package/dist/assets/{save-MzjoiHri.js → save-DKA0x4zk.js} +1 -1
  29. package/dist/assets/{search-B29dE4GN.js → search-BNlcoj5c.js} +1 -1
  30. package/dist/assets/{search-config-page-Dl5sZzob.js → search-config-page-Cq9uJ-N8.js} +1 -1
  31. package/dist/assets/{secrets-config-page-C_l-IFCK.js → secrets-config-page-CxSbZLBo.js} +2 -2
  32. package/dist/assets/{select-BKmh05hd.js → select-CvTlDZTQ.js} +1 -1
  33. package/dist/assets/{tag-chip-BS_7Ueom.js → tag-chip-8ffF2y2C.js} +1 -1
  34. package/dist/assets/use-config-DmT0o1gv.js +1 -0
  35. package/dist/assets/x-BiSSpgqB.js +1 -0
  36. package/dist/index.html +15 -14
  37. package/package.json +9 -9
  38. package/src/app/components/app-presenter-provider.tsx +11 -2
  39. package/src/app/components/layout/__tests__/app-layout.test.tsx +11 -2
  40. package/src/app/components/layout/__tests__/sidebar.layout.test.tsx +110 -79
  41. package/src/app/components/layout/app-layout.tsx +18 -4
  42. package/src/app/components/layout/sidebar-items.tsx +194 -71
  43. package/src/app/components/layout/sidebar-rail.styles.ts +15 -0
  44. package/src/app/components/layout/sidebar.tsx +340 -83
  45. package/src/app/managers/viewport-layout.manager.ts +10 -0
  46. package/src/app/stores/viewport-layout.store.ts +39 -3
  47. package/src/features/agents/components/__tests__/agents-page.test.tsx +62 -39
  48. package/src/features/agents/components/agents-page.tsx +10 -6
  49. package/src/features/chat/components/conversation/__tests__/chat-conversation-header-section.test.tsx +145 -0
  50. package/src/features/chat/components/conversation/__tests__/chat-conversation-panel.test.tsx +69 -762
  51. package/src/features/chat/components/conversation/__tests__/chat-conversation-workspace-section.test.tsx +132 -0
  52. package/src/features/chat/components/conversation/chat-conversation-content.tsx +22 -15
  53. package/src/features/chat/components/conversation/chat-conversation-header-section.tsx +14 -15
  54. package/src/features/chat/components/conversation/chat-conversation-panel.tsx +18 -18
  55. package/src/features/chat/components/conversation/chat-conversation-workspace-section.tsx +10 -4
  56. package/src/features/chat/components/layout/__tests__/chat-sidebar-read-state.test.tsx +52 -11
  57. package/src/features/chat/components/layout/__tests__/chat-sidebar.test.tsx +16 -14
  58. package/src/features/chat/components/layout/chat-sidebar-desktop-layout.tsx +247 -0
  59. package/src/features/chat/components/layout/chat-sidebar-toolbar.tsx +80 -7
  60. package/src/features/chat/components/layout/chat-sidebar-utility-menu.tsx +82 -25
  61. package/src/features/chat/components/layout/chat-sidebar.tsx +59 -92
  62. package/src/features/chat/components/providers/chat-presenter.provider.tsx +0 -6
  63. package/src/features/chat/features/conversation/components/session-conversation-area.tsx +289 -0
  64. package/src/features/chat/features/conversation/components/session-conversation-input.tsx +427 -0
  65. package/src/features/chat/features/conversation/hooks/use-session-conversation-controller.ts +204 -0
  66. package/src/features/chat/features/conversation/hooks/use-session-conversation-input-attachments.ts +66 -0
  67. package/src/features/chat/features/{input/hooks/use-chat-input-bar-query-state.ts → conversation/hooks/use-session-conversation-input-query.ts} +32 -17
  68. package/src/features/chat/features/conversation/hooks/use-session-conversation-input-state.ts +281 -0
  69. package/src/features/chat/features/conversation/utils/session-conversation-input-toolbar.utils.ts +158 -0
  70. package/src/features/chat/features/input/hooks/__tests__/use-chat-input-surface-state.test.tsx +56 -0
  71. package/src/features/chat/features/input/hooks/use-chat-input-surface-state.ts +117 -0
  72. package/src/features/chat/features/input/input-surface-plugins/__tests__/panel-app-reference-plugin.test.ts +77 -0
  73. package/src/features/chat/features/input/input-surface-plugins/__tests__/slash-command-plugin.test.ts +108 -0
  74. package/src/features/chat/features/input/input-surface-plugins/chat-input-product-plugin-adapters.types.ts +10 -0
  75. package/src/features/chat/features/input/input-surface-plugins/input-surface-search.utils.ts +108 -0
  76. package/src/features/chat/features/input/input-surface-plugins/panel-app-reference-plugin.utils.ts +108 -0
  77. package/src/features/chat/features/input/input-surface-plugins/slash-command-plugin.utils.ts +161 -0
  78. package/src/features/chat/features/input/utils/__tests__/chat-composer-state.utils.test.ts +22 -1
  79. package/src/features/chat/features/input/utils/__tests__/chat-inline-token.utils.test.ts +26 -13
  80. package/src/features/chat/features/input/utils/__tests__/ncp-chat-input-availability.utils.test.ts +7 -28
  81. package/src/features/chat/features/input/utils/chat-composer-state.utils.ts +6 -0
  82. package/src/features/chat/features/input/utils/chat-inline-token.utils.ts +26 -51
  83. package/src/features/chat/features/input/utils/chat-input-bar.utils.ts +2 -0
  84. package/src/features/chat/features/input/utils/ncp-chat-input-availability.utils.ts +5 -6
  85. package/src/features/chat/features/message/components/__tests__/chat-message-list.container.test.tsx +47 -12
  86. package/src/features/chat/features/message/components/chat-message-list.container.tsx +93 -12
  87. package/src/features/chat/features/message/utils/__tests__/chat-message.utils.test.ts +31 -12
  88. package/src/features/chat/features/message/utils/chat-message-markdown-part.utils.ts +38 -0
  89. package/src/features/chat/features/message/utils/chat-message-part.utils.ts +1 -1
  90. package/src/features/chat/features/session/components/chat-sidebar-project-groups.tsx +2 -2
  91. package/src/features/chat/features/session/components/chat-sidebar-session-list.tsx +2 -2
  92. package/src/features/chat/features/session/hooks/__tests__/use-chat-session-project.test.tsx +3 -30
  93. package/src/features/chat/features/session/hooks/use-chat-session-project.ts +0 -5
  94. package/src/features/chat/features/session/utils/chat-run-metadata.utils.ts +8 -0
  95. package/src/features/chat/features/session-type/components/__tests__/chat-session-type-menu.test.tsx +2 -2
  96. package/src/features/chat/features/session-type/components/__tests__/chat-session-type-option-item.test.tsx +2 -0
  97. package/src/features/chat/features/session-type/components/chat-session-type-menu.tsx +2 -2
  98. package/src/features/chat/features/session-type/components/chat-session-type-option-item.tsx +2 -2
  99. package/src/features/chat/features/welcome/components/__tests__/chat-conversation-welcome.test.tsx +49 -31
  100. package/src/features/chat/features/welcome/components/__tests__/chat-welcome.test.tsx +2 -2
  101. package/src/features/chat/features/welcome/components/chat-conversation-welcome.tsx +21 -10
  102. package/src/features/chat/features/welcome/components/chat-welcome-session-type-picker.tsx +2 -2
  103. package/src/features/chat/features/welcome/components/chat-welcome.tsx +2 -2
  104. package/src/features/chat/features/workspace/components/__tests__/chat-session-workspace-panel-content.test.tsx +54 -0
  105. package/src/features/chat/features/workspace/components/chat-session-workspace-panel-content.tsx +44 -76
  106. package/src/features/chat/features/workspace/components/chat-session-workspace-panel-nav.tsx +5 -1
  107. package/src/features/chat/features/workspace/components/chat-session-workspace-panel.tsx +7 -1
  108. package/src/features/chat/features/workspace/hooks/use-chat-conversation-workspace-state.ts +40 -31
  109. package/src/features/chat/features/workspace/utils/__tests__/chat-workspace-panel-view-model.utils.test.ts +59 -1
  110. package/src/features/chat/features/workspace/utils/chat-thread-workspace-session.utils.ts +109 -0
  111. package/src/features/chat/features/workspace/utils/chat-workspace-panel-view-model.utils.ts +44 -4
  112. package/src/features/chat/index.ts +0 -1
  113. package/src/features/chat/managers/__tests__/chat-session-list.manager.test.ts +51 -99
  114. package/src/features/chat/managers/__tests__/chat-thread.manager.test.ts +135 -21
  115. package/src/features/chat/managers/chat-session-list.manager.ts +12 -43
  116. package/src/features/chat/managers/chat-thread.manager.ts +108 -34
  117. package/src/features/chat/pages/__tests__/ncp-chat-page.test.ts +48 -1
  118. package/src/features/chat/pages/__tests__/ncp-chat-page.test.tsx +72 -0
  119. package/src/features/chat/pages/ncp-chat-page.tsx +0 -122
  120. package/src/features/chat/presenters/chat.presenter.ts +2 -19
  121. package/src/features/chat/stores/__tests__/chat-thread.store.test.ts +31 -0
  122. package/src/features/chat/stores/chat-thread.store.ts +42 -26
  123. package/src/features/panel-apps/hooks/use-panel-apps.ts +2 -1
  124. package/src/platforms/desktop/components/__tests__/desktop-app-shell.test.tsx +50 -10
  125. package/src/platforms/desktop/components/desktop-app-shell.tsx +34 -10
  126. package/src/platforms/desktop/components/desktop-window-chrome.tsx +41 -16
  127. package/src/shared/components/ui/tab-strip/__tests__/compact-tab-strip.test.tsx +24 -1
  128. package/src/shared/components/ui/tab-strip/compact-tab-strip.tsx +8 -4
  129. package/src/shared/lib/i18n/locales/en-US/chat.json +20 -2
  130. package/src/shared/lib/i18n/locales/en-US/core.json +2 -0
  131. package/src/shared/lib/i18n/locales/zh-CN/chat.json +20 -2
  132. package/src/shared/lib/i18n/locales/zh-CN/core.json +2 -0
  133. package/src/shared/lib/ui-document-title/__tests__/ui-document-title.test.ts +49 -0
  134. package/src/shared/lib/ui-document-title/index.ts +50 -21
  135. package/dist/assets/channels-list-page-D6jtziu4.js +0 -8
  136. package/dist/assets/chat-page-BBnNJoKG.js +0 -105
  137. package/dist/assets/doc-browser-C8xOF_9F.js +0 -1
  138. package/dist/assets/doc-browser-DgQedeAn.js +0 -1
  139. package/dist/assets/doc-browser-context-B46tQGmO.js +0 -1
  140. package/dist/assets/index-7o1WdUbU.js +0 -103
  141. package/dist/assets/index-DQWmEWzk.css +0 -1
  142. package/dist/assets/mcp-marketplace-page-CWZN7ohK.js +0 -1
  143. package/dist/assets/mcp-marketplace-page-Dzull_4-.js +0 -40
  144. package/dist/assets/model-config-page-Cv0O3BIj.js +0 -1
  145. package/dist/assets/plus-Bnt56oqG.js +0 -1
  146. package/dist/assets/provider-scoped-model-input-Duuh7i3h.js +0 -1
  147. package/dist/assets/providers-config-page-CPDz0Lxc.js +0 -1
  148. package/dist/assets/remote-B0LMs8u7.js +0 -1
  149. package/dist/assets/rotate-cw-BfyG_KEF.js +0 -1
  150. package/dist/assets/runtime-config-page-q0fvv8ME.js +0 -1
  151. package/dist/assets/use-config-DZkBWIdL.js +0 -1
  152. package/dist/assets/x-CW2KO-Hz.js +0 -1
  153. package/src/features/chat/components/conversation/chat-conversation-alerts.tsx +0 -37
  154. package/src/features/chat/features/input/components/__tests__/chat-attachment-upload-limit.test.ts +0 -38
  155. package/src/features/chat/features/input/components/chat-input-bar.container.tsx +0 -496
  156. package/src/features/chat/features/message/utils/chat-message-inline-content.utils.ts +0 -95
  157. package/src/features/chat/managers/__tests__/chat-input.manager.test.ts +0 -448
  158. package/src/features/chat/managers/__tests__/chat-run-snapshot.manager.test.ts +0 -129
  159. package/src/features/chat/managers/__tests__/chat-run.manager.test.ts +0 -236
  160. package/src/features/chat/managers/__tests__/chat-session-preference-sync.manager.test.ts +0 -219
  161. package/src/features/chat/managers/chat-input.manager.ts +0 -430
  162. package/src/features/chat/managers/chat-run.manager.ts +0 -110
  163. package/src/features/chat/managers/chat-session-preference-sync.manager.ts +0 -142
  164. package/src/features/chat/stores/chat-input.store.ts +0 -104
  165. /package/dist/assets/{config-hints-CTqBnCDp.js → config-hints-BkYBc7z0.js} +0 -0
@@ -1,7 +1,4 @@
1
1
  import { useEffect, useMemo, useState } from "react";
2
- import { BrandHeader } from "@/shared/components/common/brand-header";
3
- import { StatusBadge } from "@/shared/components/common/status-badge";
4
- import { ChatSidebarListModeSwitch } from "@/features/chat/components/chat-sidebar-list-mode-switch";
5
2
  import {
6
3
  getSessionTitle,
7
4
  groupChildSessionsByParentKey,
@@ -25,37 +22,26 @@ import { THEME_OPTIONS } from "@/shared/lib/theme";
25
22
  import { useI18n } from "@/app/components/i18n-provider";
26
23
  import { useTheme } from "@/app/components/theme-provider";
27
24
  import { useDocBrowser } from "@/shared/components/doc-browser";
28
- import { SidebarNavLinkItem } from "@/app/components/layout/sidebar-items";
29
- import { AlarmClock, Bot, BrainCircuit } from "lucide-react";
30
25
  import { ChatSidebarSessionEntry } from "@/features/chat/features/session/components/chat-sidebar-session-entry";
31
- import { ChatSidebarSessionList } from "@/features/chat/features/session/components/chat-sidebar-session-list";
32
26
  import {
33
27
  ChatSidebarDesktopToolbar,
34
28
  ChatSidebarMobileToolbar,
35
29
  } from "@/features/chat/components/layout/chat-sidebar-toolbar";
36
- import { ChatSidebarUtilityMenu } from "@/features/chat/components/layout/chat-sidebar-utility-menu";
30
+ import {
31
+ ChatSidebarDesktopFooter,
32
+ ChatSidebarDesktopHeader,
33
+ ChatSidebarDesktopNav,
34
+ ChatSidebarSessionArea,
35
+ } from "@/features/chat/components/layout/chat-sidebar-desktop-layout";
37
36
  import { openApps } from "@/features/panel-apps";
38
- import { isWindowsDesktopHost } from "@/platforms/desktop";
39
37
  import {
40
38
  buildSessionTypeOptions,
41
39
  DEFAULT_SESSION_TYPE,
42
40
  normalizeSessionType,
43
41
  } from "@/features/chat/features/session-type/utils/chat-session-type.utils";
44
42
  import { useChatNewSessionTypePreference } from "@/features/chat/features/session-type/hooks/use-chat-new-session-type-preference";
45
-
46
- const navItems = [
47
- {
48
- target: "/cron",
49
- label: () => t("chatSidebarScheduledTasks"),
50
- icon: AlarmClock,
51
- },
52
- {
53
- target: "/skills",
54
- label: () => t("chatSidebarSkills"),
55
- icon: BrainCircuit,
56
- },
57
- { target: "/agents", label: () => t("agentsPageTitle"), icon: Bot },
58
- ];
43
+ import { useViewportLayoutStore } from "@/app/stores/viewport-layout.store";
44
+ import { SIDEBAR_RAIL_WIDTH_CLASS } from "@/app/components/layout/sidebar-rail.styles";
59
45
 
60
46
  type ChatSidebarVariant = "desktop" | "mobile";
61
47
 
@@ -104,6 +90,9 @@ export function ChatSidebar({
104
90
  const isMobileVariant = variant === "mobile";
105
91
  const presenter = usePresenter();
106
92
  const docBrowser = useDocBrowser();
93
+ const isSidebarCollapsed = useViewportLayoutStore(
94
+ (state) => state.isSidebarCollapsed,
95
+ );
107
96
  const [isCreateMenuOpen, setIsCreateMenuOpen] = useState(false);
108
97
  const [isUtilityMenuOpen, setIsUtilityMenuOpen] = useState(false);
109
98
  const listSnapshot = useChatSessionListStore((state) => state.snapshot);
@@ -179,6 +168,7 @@ export function ChatSidebar({
179
168
  sessionTypeOptions,
180
169
  });
181
170
  const isProjectFirstView = listSnapshot.listMode === "project-first";
171
+ const shouldCollapse = !isMobileVariant && isSidebarCollapsed;
182
172
  const optimisticReadAtBySessionKey = useChatSessionUnreadState(
183
173
  items,
184
174
  listSnapshot.selectedSessionKey,
@@ -237,19 +227,23 @@ export function ChatSidebar({
237
227
  return (
238
228
  <aside
239
229
  className={cn(
240
- "flex h-full min-h-0 flex-col bg-secondary",
230
+ "flex h-full min-h-0 flex-col bg-secondary transition-[width] duration-200 ease-out",
241
231
  isMobileVariant
242
232
  ? "flex-1 overflow-hidden"
243
- : "w-[280px] shrink-0 border-r border-gray-200/60",
233
+ : shouldCollapse
234
+ ? cn(
235
+ SIDEBAR_RAIL_WIDTH_CLASS,
236
+ "shrink-0 border-r border-gray-200/60",
237
+ )
238
+ : "w-[280px] shrink-0 border-r border-gray-200/60",
244
239
  )}
240
+ data-sidebar-collapsed={shouldCollapse ? "true" : "false"}
245
241
  >
246
- {!isMobileVariant && !isWindowsDesktopHost() ? (
247
- <div className="px-5 py-2.5">
248
- <BrandHeader
249
- className="flex min-w-0 items-center gap-2"
250
- suffix={<StatusBadge status={systemStatus.connectionStatus} />}
251
- />
252
- </div>
242
+ {!isMobileVariant ? (
243
+ <ChatSidebarDesktopHeader
244
+ connectionStatus={systemStatus.connectionStatus}
245
+ isCollapsed={shouldCollapse}
246
+ />
253
247
  ) : null}
254
248
 
255
249
  {isMobileVariant ? (
@@ -287,75 +281,48 @@ export function ChatSidebar({
287
281
  newSessionTypePreference.setSelectedSessionType
288
282
  }
289
283
  onQueryChange={presenter.chatSessionListManager.setQuery}
284
+ collapsed={shouldCollapse}
290
285
  />
291
286
  )}
292
287
 
293
288
  {!isMobileVariant ? (
294
- <div className="px-3 pb-2">
295
- <ul className="space-y-0.5">
296
- {navItems.map((item) => (
297
- <li key={item.target}>
298
- <SidebarNavLinkItem
299
- to={item.target}
300
- label={item.label()}
301
- icon={item.icon}
302
- density="compact"
303
- />
304
- </li>
305
- ))}
306
- </ul>
307
- </div>
308
- ) : null}
309
-
310
- {!isMobileVariant ? (
311
- <div className="mx-4 border-t border-gray-200/60" />
289
+ <ChatSidebarDesktopNav isCollapsed={shouldCollapse} />
312
290
  ) : null}
313
291
 
314
- <div className="flex items-center justify-between px-5 pb-2 pt-3">
315
- <div className="text-[11px] font-medium uppercase tracking-wider text-gray-400">
316
- {t("chatSidebarTaskRecords")}
317
- </div>
318
- <ChatSidebarListModeSwitch
319
- isProjectFirstView={isProjectFirstView}
320
- onSelectMode={presenter.chatSessionListManager.setListMode}
321
- />
322
- </div>
323
-
324
- <div className="flex-1 min-h-0 overflow-y-auto custom-scrollbar px-3 py-2">
325
- <ChatSidebarSessionList
326
- isLoading={isLoading}
327
- isProjectFirstView={isProjectFirstView}
328
- groups={groups}
329
- projectGroups={projectGroups}
330
- defaultSessionType={newSessionTypePreference.selectedSessionType}
331
- sessionTypeOptions={sessionTypeOptions}
332
- renderSessionItem={renderSessionItem}
333
- onCreateSession={createSessionAndOpenIfNeeded}
334
- />
335
- </div>
292
+ <ChatSidebarSessionArea
293
+ defaultSessionType={newSessionTypePreference.selectedSessionType}
294
+ groups={groups}
295
+ isCollapsed={shouldCollapse}
296
+ isLoading={isLoading}
297
+ isProjectFirstView={isProjectFirstView}
298
+ onCreateSession={createSessionAndOpenIfNeeded}
299
+ onSelectMode={presenter.chatSessionListManager.setListMode}
300
+ projectGroups={projectGroups}
301
+ renderSessionItem={renderSessionItem}
302
+ sessionTypeOptions={sessionTypeOptions}
303
+ />
336
304
 
337
305
  {!isMobileVariant ? (
338
- <div className="px-3 py-3 border-t border-gray-200/60">
339
- <ChatSidebarUtilityMenu
340
- isOpen={isUtilityMenuOpen}
341
- onOpenChange={setIsUtilityMenuOpen}
342
- currentTheme={theme}
343
- currentThemeLabel={currentThemeLabel}
344
- themeOptions={utilityThemeOptions}
345
- onSelectTheme={setTheme}
346
- currentLanguage={language}
347
- currentLanguageLabel={currentLanguageLabel}
348
- languageOptions={utilityLanguageOptions}
349
- onSelectLanguage={handleLanguageSwitch}
350
- onOpenDocs={() =>
351
- docBrowser.open(undefined, {
352
- kind: "docs",
353
- title: t("docBrowserHelp"),
354
- })
355
- }
356
- onOpenApps={() => openApps(docBrowser)}
357
- />
358
- </div>
306
+ <ChatSidebarDesktopFooter
307
+ currentLanguage={language}
308
+ currentLanguageLabel={currentLanguageLabel}
309
+ currentTheme={theme}
310
+ currentThemeLabel={currentThemeLabel}
311
+ isCollapsed={shouldCollapse}
312
+ isOpen={isUtilityMenuOpen}
313
+ languageOptions={utilityLanguageOptions}
314
+ onOpenApps={() => openApps(docBrowser)}
315
+ onOpenChange={setIsUtilityMenuOpen}
316
+ onOpenDocs={() =>
317
+ docBrowser.open(undefined, {
318
+ kind: "docs",
319
+ title: t("docBrowserHelp"),
320
+ })
321
+ }
322
+ onSelectLanguage={handleLanguageSwitch}
323
+ onSelectTheme={setTheme}
324
+ themeOptions={utilityThemeOptions}
325
+ />
359
326
  ) : null}
360
327
  </aside>
361
328
  );
@@ -2,25 +2,19 @@ import { createContext, useContext, type ReactNode } from 'react';
2
2
  import type {
3
3
  ChatSessionListManager,
4
4
  } from '@/features/chat/managers/chat-session-list.manager';
5
- import type { ChatRunManager } from '@/features/chat/managers/chat-run.manager';
6
5
  import type { ChatUiManager } from '@/features/chat/managers/chat-ui.manager';
7
- import type { ChatInputManager } from '@/features/chat/managers/chat-input.manager';
8
6
  import type { ChatQueryManager } from '@/features/chat/managers/chat-query.manager';
9
7
  import type { ChatThreadManager } from '@/features/chat/managers/chat-thread.manager';
10
8
 
11
9
  type PublicManager<T extends object> = Pick<T, keyof T>;
12
10
 
13
- export type ChatInputManagerLike = PublicManager<ChatInputManager>;
14
11
  export type ChatThreadManagerLike = PublicManager<ChatThreadManager>;
15
12
 
16
13
  export type ChatPresenterLike = {
17
14
  chatUiManager: ChatUiManager;
18
- chatRunManager: ChatRunManager;
19
- chatInputManager: ChatInputManagerLike;
20
15
  chatQueryManager: ChatQueryManager;
21
16
  chatSessionListManager: ChatSessionListManager;
22
17
  chatThreadManager: ChatThreadManagerLike;
23
- startAgentCreationDraft: (prompt: string) => void;
24
18
  };
25
19
 
26
20
  const ChatPresenterContext = createContext<ChatPresenterLike | null>(null);
@@ -0,0 +1,289 @@
1
+ import { useCallback, useEffect, useMemo, useRef } from 'react';
2
+ import { useLocation } from 'react-router-dom';
3
+
4
+ import { useAppPresenter } from '@/app/components/app-presenter-provider';
5
+ import { ChatConversationContent } from '@/features/chat/components/conversation/chat-conversation-content';
6
+ import { usePresenter } from '@/features/chat/components/providers/chat-presenter.provider';
7
+ import {
8
+ isNcpAgentStartupUnavailableErrorMessage,
9
+ useNcpSessionConversation,
10
+ } from '@/features/chat/features/ncp/hooks/use-ncp-session-conversation';
11
+ import { isNcpChatRuntimeBlocked, resolveNcpChatSendErrorMessage } from '@/features/chat/features/runtime/utils/ncp-chat-runtime-availability.utils';
12
+ import { buildChatContextWindowIndicator } from '@/features/chat/features/session/utils/chat-context-window-indicator.utils';
13
+ import { readNcpContextWindowValue } from '@/features/chat/features/session/utils/ncp-session-context-metadata.utils';
14
+ import { ChatConversationWelcome } from '@/features/chat/features/welcome/components/chat-conversation-welcome';
15
+ import { useChatSessionListStore } from '@/features/chat/stores/chat-session-list.store';
16
+ import { useSystemStatus } from '@/features/system-status';
17
+ import { t } from '@/shared/lib/i18n';
18
+
19
+ import {
20
+ useSessionConversationController,
21
+ type SessionConversationMaterializationContext,
22
+ } from '@/features/chat/features/conversation/hooks/use-session-conversation-controller';
23
+ import { useSessionConversationInputQuery } from '@/features/chat/features/conversation/hooks/use-session-conversation-input-query';
24
+ import { useSessionConversationInputState } from '@/features/chat/features/conversation/hooks/use-session-conversation-input-state';
25
+ import { SessionConversationInput } from './session-conversation-input';
26
+
27
+ type SessionConversationAreaProps = {
28
+ readonly consumeDraftIntent?: boolean;
29
+ readonly materializationContext?: SessionConversationMaterializationContext | null;
30
+ readonly onSessionMaterialized?: (sessionKey: string) => void;
31
+ readonly sessionKey: string | null;
32
+ readonly showWelcomeForDraft?: boolean;
33
+ };
34
+
35
+ function useSessionConversationDraftIntent(params: {
36
+ readonly consumeDraftIntent: boolean;
37
+ readonly applyPromptSuggestion: (prompt: string) => void;
38
+ }) {
39
+ const { applyPromptSuggestion, consumeDraftIntent } = params;
40
+ const appPresenter = useAppPresenter();
41
+ const presenter = usePresenter();
42
+ useEffect(() => {
43
+ if (!consumeDraftIntent) {
44
+ return undefined;
45
+ }
46
+ const applyIntent = (intent: { id: number; prompt: string }) => {
47
+ presenter.chatSessionListManager.createSession();
48
+ presenter.chatSessionListManager.setSelectedAgentId('main');
49
+ applyPromptSuggestion(intent.prompt);
50
+ appPresenter.chatDraftIntentManager.markConsumed(intent.id);
51
+ };
52
+ const unsubscribe = appPresenter.chatDraftIntentManager.subscribe(applyIntent);
53
+ const pendingIntent = appPresenter.chatDraftIntentManager.consumePending();
54
+ if (pendingIntent) {
55
+ applyIntent(pendingIntent);
56
+ }
57
+ return unsubscribe;
58
+ }, [
59
+ appPresenter,
60
+ applyPromptSuggestion,
61
+ consumeDraftIntent,
62
+ presenter,
63
+ ]);
64
+ }
65
+
66
+ type ChatDraftRouteState = {
67
+ readonly sessionType?: unknown;
68
+ readonly projectRoot?: unknown;
69
+ };
70
+
71
+ function readChatDraftRouteState(value: unknown): ChatDraftRouteState | null {
72
+ if (!value || typeof value !== 'object') {
73
+ return null;
74
+ }
75
+ const draft = (value as { chatDraft?: unknown }).chatDraft;
76
+ if (!draft || typeof draft !== 'object') {
77
+ return null;
78
+ }
79
+ return draft as ChatDraftRouteState;
80
+ }
81
+
82
+ function useSessionConversationDraftRouteState(params: {
83
+ readonly sessionKey: string | null;
84
+ readonly setPendingProjectRoot: (projectRoot: string | null) => void;
85
+ readonly setPendingSessionType: (sessionType: string) => void;
86
+ }) {
87
+ const {
88
+ sessionKey,
89
+ setPendingProjectRoot,
90
+ setPendingSessionType,
91
+ } = params;
92
+ const location = useLocation();
93
+ const appliedRouteStateKeyRef = useRef<string | null>(null);
94
+ useEffect(() => {
95
+ if (sessionKey) {
96
+ return;
97
+ }
98
+ const draftState = readChatDraftRouteState(location.state);
99
+ if (!draftState) {
100
+ return;
101
+ }
102
+ const signature = [
103
+ location.key,
104
+ typeof draftState.sessionType === 'string' ? draftState.sessionType : '',
105
+ typeof draftState.projectRoot === 'string' ? draftState.projectRoot : '',
106
+ ].join(':');
107
+ if (appliedRouteStateKeyRef.current === signature) {
108
+ return;
109
+ }
110
+ appliedRouteStateKeyRef.current = signature;
111
+ if (typeof draftState.sessionType === 'string' && draftState.sessionType.trim()) {
112
+ setPendingSessionType(draftState.sessionType);
113
+ }
114
+ setPendingProjectRoot(
115
+ typeof draftState.projectRoot === 'string' && draftState.projectRoot.trim()
116
+ ? draftState.projectRoot
117
+ : null,
118
+ );
119
+ }, [
120
+ location.key,
121
+ location.state,
122
+ sessionKey,
123
+ setPendingProjectRoot,
124
+ setPendingSessionType,
125
+ ]);
126
+ }
127
+
128
+ function SessionConversationAlerts({
129
+ inputQuery,
130
+ }: {
131
+ readonly inputQuery: ReturnType<typeof useSessionConversationInputQuery>;
132
+ }) {
133
+ const presenter = usePresenter();
134
+ const shouldShowProviderHint =
135
+ inputQuery.isProviderStateResolved && inputQuery.modelOptions.length === 0;
136
+ const sessionTypeUnavailableMessage =
137
+ inputQuery.sessionTypeState.sessionTypeUnavailableMessage?.trim() || null;
138
+
139
+ return (
140
+ <>
141
+ {shouldShowProviderHint ? (
142
+ <div className="px-4 py-2.5 border-b border-amber-200/70 bg-amber-50/70 flex items-center justify-between gap-3 shrink-0 sm:px-5">
143
+ <span className="text-xs text-amber-800">
144
+ {t('chatModelNoOptions')}
145
+ </span>
146
+ <button
147
+ type="button"
148
+ onClick={presenter.chatUiManager.goToProviders}
149
+ className="text-xs font-semibold text-amber-900 underline-offset-2 hover:underline"
150
+ >
151
+ {t('chatGoConfigureProvider')}
152
+ </button>
153
+ </div>
154
+ ) : null}
155
+ {inputQuery.sessionTypeState.sessionTypeUnavailable && sessionTypeUnavailableMessage ? (
156
+ <div className="px-4 py-2.5 border-b border-amber-200/70 bg-amber-50/70 shrink-0 sm:px-5">
157
+ <span className="text-xs text-amber-800">
158
+ {sessionTypeUnavailableMessage}
159
+ </span>
160
+ </div>
161
+ ) : null}
162
+ </>
163
+ );
164
+ }
165
+
166
+ export function SessionConversationArea(props: SessionConversationAreaProps) {
167
+ const {
168
+ consumeDraftIntent = false,
169
+ materializationContext = null,
170
+ onSessionMaterialized,
171
+ sessionKey,
172
+ showWelcomeForDraft = true,
173
+ } = props;
174
+ const systemStatus = useSystemStatus();
175
+ const selectedAgentId = useChatSessionListStore(
176
+ (state) => state.snapshot.selectedAgentId,
177
+ );
178
+ const agent = useNcpSessionConversation(sessionKey ?? undefined);
179
+ const {
180
+ inputActions,
181
+ inputSnapshot,
182
+ } = useSessionConversationInputState();
183
+ const inputQuery = useSessionConversationInputQuery({
184
+ sessionKey,
185
+ inputSnapshot,
186
+ setPendingSessionType: inputActions.setPendingSessionType,
187
+ });
188
+ useSessionConversationDraftRouteState({
189
+ sessionKey,
190
+ setPendingProjectRoot: inputActions.setPendingProjectRoot,
191
+ setPendingSessionType: inputActions.setPendingSessionType,
192
+ });
193
+ const isRuntimeBlocked = isNcpChatRuntimeBlocked(systemStatus);
194
+ const currentSessionRunning = agent.isRunning || inputQuery.selectedSession?.status === 'running';
195
+ const rawLastSendError =
196
+ agent.hydrateError?.message ?? agent.snapshot.error?.message ?? null;
197
+ const filteredLastSendError =
198
+ systemStatus.phase === 'ready' &&
199
+ isNcpAgentStartupUnavailableErrorMessage(rawLastSendError)
200
+ ? null
201
+ : rawLastSendError;
202
+ const lastSendError =
203
+ isRuntimeBlocked
204
+ ? null
205
+ : systemStatus.phase === 'ready'
206
+ ? filteredLastSendError
207
+ : resolveNcpChatSendErrorMessage({
208
+ message: filteredLastSendError,
209
+ status: systemStatus,
210
+ });
211
+ const controllerAgent = useMemo(() => ({
212
+ ...agent,
213
+ isRunning: currentSessionRunning,
214
+ isSending: agent.isSending || currentSessionRunning,
215
+ }), [agent, currentSessionRunning]);
216
+ const controller = useSessionConversationController({
217
+ agent: controllerAgent,
218
+ inputSnapshot,
219
+ inputQuery,
220
+ isRuntimeBlocked,
221
+ materializationContext,
222
+ selectedAgentId,
223
+ sessionKey,
224
+ onSessionMaterialized,
225
+ resetComposer: inputActions.resetComposer,
226
+ restoreComposer: inputActions.restoreComposer,
227
+ setSendError: inputActions.setSendError,
228
+ });
229
+ const displayInputSnapshot = useMemo(() => ({
230
+ ...inputSnapshot,
231
+ sendError: lastSendError ?? inputSnapshot.sendError,
232
+ }), [inputSnapshot, lastSendError]);
233
+ const contextWindow = useMemo(
234
+ () => buildChatContextWindowIndicator(readNcpContextWindowValue(agent.snapshot.contextWindow)),
235
+ [agent.snapshot.contextWindow],
236
+ );
237
+ useSessionConversationDraftIntent({
238
+ consumeDraftIntent,
239
+ applyPromptSuggestion: inputActions.applyPromptSuggestion,
240
+ });
241
+ const renderInput = useCallback((surface: 'default' | 'embedded') => (
242
+ <SessionConversationInput
243
+ contextWindow={contextWindow}
244
+ controller={controller}
245
+ inputActions={inputActions}
246
+ inputQuery={inputQuery}
247
+ inputSnapshot={displayInputSnapshot}
248
+ surface={surface}
249
+ />
250
+ ), [
251
+ contextWindow,
252
+ controller,
253
+ displayInputSnapshot,
254
+ inputActions,
255
+ inputQuery,
256
+ ]);
257
+ const showWelcome =
258
+ showWelcomeForDraft &&
259
+ !sessionKey &&
260
+ agent.visibleMessages.length === 0 &&
261
+ !agent.isHydrating &&
262
+ !controller.isSending;
263
+
264
+ return (
265
+ <>
266
+ <SessionConversationAlerts inputQuery={inputQuery} />
267
+ <ChatConversationContent
268
+ isAwaitingAssistantOutput={controller.isSending && currentSessionRunning}
269
+ isHistoryLoading={agent.isHydrating}
270
+ isSending={controller.isSending}
271
+ messages={agent.visibleMessages}
272
+ sessionKey={sessionKey}
273
+ showWelcome={showWelcome}
274
+ welcomeSlot={
275
+ <ChatConversationWelcome
276
+ inputSlot={renderInput('embedded')}
277
+ pendingProjectRoot={inputSnapshot.pendingProjectRoot}
278
+ pendingSessionType={inputSnapshot.pendingSessionType}
279
+ selectedSessionTypeValue={inputSnapshot.selectedSessionType}
280
+ onSelectProjectRoot={inputActions.setPendingProjectRoot}
281
+ onSelectPrompt={inputActions.applyPromptSuggestion}
282
+ onSelectSessionType={inputActions.setPendingSessionType}
283
+ />
284
+ }
285
+ />
286
+ {showWelcome ? null : renderInput('default')}
287
+ </>
288
+ );
289
+ }