@nextclaw/ui 0.12.24 → 0.12.26

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 (210) hide show
  1. package/CHANGELOG.md +136 -29
  2. package/dist/assets/api-DGD9_Bg4.js +15 -0
  3. package/dist/assets/app-manager-provider-oYdeYPSv.js +1 -0
  4. package/dist/assets/{book-open-DDlN5MvX.js → book-open-BcnAiKde.js} +1 -1
  5. package/dist/assets/channels-list-page-HgLgrEg4.js +8 -0
  6. package/dist/assets/chat-page-DAKMFDrS.js +1 -0
  7. package/dist/assets/config-split-page-CcrEUtwu.js +1 -0
  8. package/dist/assets/cpu-DPPwMzoC.js +3 -0
  9. package/dist/assets/{createLucideIcon-BLMK3QUd.js → createLucideIcon-DzY6wN61.js} +1 -1
  10. package/dist/assets/desktop-DVUbOWbR.js +3 -0
  11. package/dist/assets/desktop-update-config-CP8dFYXK.js +1 -0
  12. package/dist/assets/{dialog-C3D7Be0p.js → dialog-BKo0RItd.js} +1 -1
  13. package/dist/assets/{dist-CPlbUgwU.js → dist-CFiwgaLs.js} +1 -1
  14. package/dist/assets/doc-browser-CAhfnm0D.js +1 -0
  15. package/dist/assets/{doc-browser-context-BJuMaI3o.js → doc-browser-context-FukQHvyo.js} +1 -1
  16. package/dist/assets/doc-browser-p9DDNPWB.js +1 -0
  17. package/dist/assets/doc-browser-rZIQIjuw.js +1 -0
  18. package/dist/assets/download-CMM8po31.js +1 -0
  19. package/dist/assets/{es2015-xqN1slyW.js → es2015-BhznEEyJ.js} +1 -1
  20. package/dist/assets/{external-link-DwfSfTLB.js → external-link-CpEvG65F.js} +1 -1
  21. package/dist/assets/i18n-D1144VAA.js +1 -0
  22. package/dist/assets/index-Cuwst6cc.js +100 -0
  23. package/dist/assets/index-dlcqieQ0.css +1 -0
  24. package/dist/assets/{key-round-CJ5gDAAG.js → key-round-DUq47t0P.js} +1 -1
  25. package/dist/assets/marketplace-page-BeFbwxR-.js +105 -0
  26. package/dist/assets/marketplace-page-CR4xq-TM.js +1 -0
  27. package/dist/assets/mcp-marketplace-page-DlRrSCj3.js +1 -0
  28. package/dist/assets/mcp-marketplace-page-DwnaLNTx.js +40 -0
  29. package/dist/assets/model-config-L2l6YAlQ.js +1 -0
  30. package/dist/assets/{notice-card-BFDbKQDA.js → notice-card-Dr6xCwva.js} +1 -1
  31. package/dist/assets/play-AqrNslHI.js +1 -0
  32. package/dist/assets/plus-B-YHtTNC.js +1 -0
  33. package/dist/assets/{popover-B86Dbfhf.js → popover-BDFNiLlg.js} +1 -1
  34. package/dist/assets/provider-scoped-model-input-BMTp4BEH.js +1 -0
  35. package/dist/assets/providers-list-DYAEunOp.js +1 -0
  36. package/dist/assets/refresh-cw-CrbD8EkT.js +1 -0
  37. package/dist/assets/remote-Dr3jcfWP.js +1 -0
  38. package/dist/assets/{rotate-cw-BZ2JObNs.js → rotate-cw-BN9yjccP.js} +1 -1
  39. package/dist/assets/runtime-config-page-BdeU8PEK.js +1 -0
  40. package/dist/assets/{save-euRxl8pI.js → save-CO_4qf6b.js} +1 -1
  41. package/dist/assets/{search-CLd7m0M7.js → search-CRtQwr-h.js} +1 -1
  42. package/dist/assets/search-config-CQUhd5RU.js +1 -0
  43. package/dist/assets/secrets-config-D-NWlW9q.js +3 -0
  44. package/dist/assets/{select-CJ0wbo3D.js → select-BUTwE_lC.js} +1 -1
  45. package/dist/assets/{setting-row-D1Yygqp7.js → setting-row-BavcnXw1.js} +1 -1
  46. package/dist/assets/settings-MWL2SMyk.js +1 -0
  47. package/dist/assets/{sparkles-DVfeSVJQ.js → sparkles-BmgOD4nY.js} +1 -1
  48. package/dist/assets/{status-dot-ChvPCib9.js → status-dot-l3kPFdq_.js} +1 -1
  49. package/dist/assets/{tabs-custom-Hia_ong0.js → tabs-custom-D48zdZoc.js} +1 -1
  50. package/dist/assets/{tag-chip-FrkmkT8r.js → tag-chip-Dm2Lqnpu.js} +1 -1
  51. package/dist/assets/use-config-Cyv5IuSt.js +1 -0
  52. package/dist/assets/use-infinite-scroll-loader-CFVdPpNv.js +1 -0
  53. package/dist/assets/x-BeyYA_h6.js +1 -0
  54. package/dist/index.html +29 -40
  55. package/package.json +9 -9
  56. package/src/app/components/layout/sidebar.layout.test.tsx +2 -4
  57. package/src/app/components/theme-provider.tsx +1 -0
  58. package/src/app/configs/app-navigation.config.ts +0 -6
  59. package/src/app/index.tsx +4 -7
  60. package/src/features/agents/components/agents-page.test.tsx +25 -15
  61. package/src/features/agents/components/agents-page.tsx +133 -172
  62. package/src/features/channels/components/config/channel-form.test.tsx +1 -0
  63. package/src/features/channels/components/config/channel-form.tsx +4 -3
  64. package/src/features/channels/components/config/weixin-channel-auth-section.test.tsx +38 -1
  65. package/src/features/channels/components/config/weixin-channel-auth-section.tsx +137 -40
  66. package/src/features/channels/index.ts +1 -1
  67. package/src/features/channels/utils/channel-form-fields.utils.test.ts +26 -0
  68. package/src/features/channels/utils/channel-form-fields.utils.ts +32 -18
  69. package/src/features/chat/components/chat-session-workspace-panel-nav.tsx +23 -4
  70. package/src/features/chat/components/chat-session-workspace-panel.tsx +53 -35
  71. package/src/features/chat/components/chat-sidebar-session-item.tsx +16 -12
  72. package/src/features/chat/components/conversation/chat-conversation-header.test.tsx +74 -0
  73. package/src/features/chat/components/conversation/chat-conversation-header.tsx +8 -2
  74. package/src/features/chat/components/conversation/chat-conversation-panel.test.tsx +262 -114
  75. package/src/features/chat/components/conversation/chat-conversation-panel.tsx +210 -174
  76. package/src/features/chat/components/conversation/chat-input-bar.container.tsx +11 -1
  77. package/src/features/chat/components/conversation/session-header/chat-session-header-actions.test.tsx +24 -0
  78. package/src/features/chat/components/conversation/session-header/chat-session-header-actions.tsx +27 -6
  79. package/src/features/chat/components/layout/chat-sidebar-utility-menu.tsx +174 -0
  80. package/src/features/chat/components/layout/chat-sidebar.test.tsx +45 -8
  81. package/src/features/chat/components/layout/chat-sidebar.tsx +29 -46
  82. package/src/features/chat/components/providers/chat-presenter.provider.tsx +4 -0
  83. package/src/features/chat/components/workspace/session-cron-job-content.tsx +103 -0
  84. package/src/features/chat/hooks/use-ncp-agent-runtime.test.tsx +153 -80
  85. package/src/features/chat/hooks/use-ncp-chat-page-data.test.tsx +70 -0
  86. package/src/features/chat/hooks/use-ncp-chat-page-data.ts +1 -1
  87. package/src/features/chat/hooks/use-ncp-child-session-tabs-view.ts +2 -8
  88. package/src/features/chat/hooks/use-ncp-session-list-view.ts +1 -2
  89. package/src/features/chat/managers/chat-session-list.manager.test.ts +7 -9
  90. package/src/features/chat/managers/chat-session-list.manager.ts +5 -10
  91. package/src/features/chat/managers/ncp-chat-input.manager.test.ts +20 -2
  92. package/src/features/chat/managers/ncp-chat-input.manager.ts +18 -0
  93. package/src/features/chat/managers/ncp-chat-presenter.manager.ts +7 -0
  94. package/src/features/chat/managers/ncp-chat-thread.manager.test.ts +52 -1
  95. package/src/features/chat/managers/ncp-chat-thread.manager.ts +21 -0
  96. package/src/features/chat/pages/ncp-chat-page.tsx +9 -5
  97. package/src/features/chat/stores/chat-input.store.ts +3 -1
  98. package/src/features/chat/stores/chat-session-list.store.ts +0 -2
  99. package/src/features/chat/stores/chat-thread.store.ts +4 -0
  100. package/src/features/chat/utils/chat-session-display.utils.test.ts +83 -1
  101. package/src/features/chat/utils/chat-session-display.utils.ts +73 -0
  102. package/src/features/chat/utils/ncp-chat-input-availability.utils.test.ts +1 -0
  103. package/src/features/chat/utils/ncp-session-adapter.utils.test.ts +22 -0
  104. package/src/features/chat/utils/ncp-session-adapter.utils.ts +32 -0
  105. package/src/features/marketplace/components/curated-shelves/marketplace-curated-scene-route.test.tsx +235 -0
  106. package/src/features/marketplace/components/curated-shelves/marketplace-curated-shelves.config.ts +162 -0
  107. package/src/features/marketplace/components/curated-shelves/marketplace-curated-shelves.tsx +355 -0
  108. package/src/features/marketplace/components/curated-shelves/marketplace-shelf-card.tsx +118 -0
  109. package/src/features/marketplace/components/detail-doc/marketplace-detail-doc-renderer.ts +201 -0
  110. package/src/features/marketplace/components/detail-doc/marketplace-detail-doc.test.ts +40 -0
  111. package/src/features/marketplace/components/marketplace-catalog-grid.tsx +114 -0
  112. package/src/features/marketplace/components/marketplace-detail-doc.ts +73 -24
  113. package/src/features/marketplace/components/marketplace-item-icon.tsx +45 -0
  114. package/src/features/marketplace/components/marketplace-list-card.tsx +177 -93
  115. package/src/features/marketplace/components/marketplace-page-detail.test.tsx +9 -2
  116. package/src/features/marketplace/components/marketplace-page-parts.tsx +1 -1
  117. package/src/features/marketplace/components/marketplace-page.test.tsx +25 -6
  118. package/src/features/marketplace/components/marketplace-page.tsx +154 -132
  119. package/src/features/marketplace/hooks/use-marketplace-curated-scene-route.ts +97 -0
  120. package/src/features/marketplace/hooks/use-marketplace.ts +59 -3
  121. package/src/features/system-status/components/config/runtime-agent-list-card.tsx +4 -8
  122. package/src/features/system-status/components/config/runtime-binding-list-card.tsx +5 -7
  123. package/src/features/system-status/components/config/runtime-config-editor.tsx +1 -19
  124. package/src/features/system-status/components/config/runtime-entry-list-card.tsx +10 -11
  125. package/src/features/system-status/components/config/runtime-settings-card.tsx +15 -23
  126. package/src/features/system-status/components/runtime-control-card.test.tsx +8 -6
  127. package/src/features/system-status/components/runtime-control-card.tsx +7 -6
  128. package/src/features/system-status/pages/runtime-config-page.test.tsx +19 -9
  129. package/src/features/system-status/pages/runtime-config-page.tsx +2 -3
  130. package/src/features/system-status/utils/runtime-config-agent.utils.ts +4 -4
  131. package/src/features/system-status/utils/system-status.utils.ts +31 -6
  132. package/src/index.css +8 -0
  133. package/src/platforms/desktop/components/desktop-app-shell.test.tsx +68 -0
  134. package/src/platforms/desktop/components/desktop-app-shell.tsx +46 -18
  135. package/src/platforms/desktop/components/desktop-window-chrome.tsx +30 -0
  136. package/src/platforms/desktop/index.ts +6 -0
  137. package/src/platforms/desktop/types/desktop-update.types.ts +3 -0
  138. package/src/platforms/desktop/utils/desktop-host.utils.ts +56 -0
  139. package/src/shared/components/common/brand-header.tsx +36 -16
  140. package/src/shared/components/config/provider-form-support.ts +2 -22
  141. package/src/shared/components/cron-config.tsx +12 -58
  142. package/src/shared/components/doc-browser/doc-browser.tsx +4 -4
  143. package/src/shared/components/ui/select.tsx +19 -7
  144. package/src/shared/lib/api/channel-auth.types.ts +1 -0
  145. package/src/shared/lib/api/ncp-session.types.ts +9 -0
  146. package/src/shared/lib/api/types.ts +12 -1
  147. package/src/shared/lib/api/utils/marketplace.utils.ts +7 -1
  148. package/src/shared/lib/cron/cron-job-view.utils.ts +59 -0
  149. package/src/shared/lib/cron/index.ts +1 -0
  150. package/src/shared/lib/i18n/{channel-auth.ts → channel-auth.constants.ts} +31 -0
  151. package/src/shared/lib/i18n/chat-labels.utils.ts +3 -2
  152. package/src/shared/lib/i18n/index.ts +20 -59
  153. package/src/shared/lib/i18n/{runtime-control.ts → runtime-control-labels.utils.ts} +30 -1
  154. package/src/shared/lib/provider-models/index.test.ts +39 -0
  155. package/src/shared/lib/provider-models/index.ts +1 -3
  156. package/src/shared/lib/ui-document-title/index.ts +0 -1
  157. package/tsconfig.json +1 -0
  158. package/vite.config.ts +1 -1
  159. package/vitest.config.ts +1 -1
  160. package/dist/assets/api-D2xRKmZd.js +0 -15
  161. package/dist/assets/app-manager-provider-CNaZboG4.js +0 -1
  162. package/dist/assets/app-navigation.config-Ihhrrt--.js +0 -1
  163. package/dist/assets/channels-list-page-p26lgxLk.js +0 -8
  164. package/dist/assets/chat-Dkh2qtuz.js +0 -61
  165. package/dist/assets/chat-page-DoTmE2wx.js +0 -1
  166. package/dist/assets/chunk-JZWAC4HX-Kydj4yEz.js +0 -3
  167. package/dist/assets/config-split-page-DIOCjj2Q.js +0 -1
  168. package/dist/assets/desktop-update-config-DlpzDfKM.js +0 -1
  169. package/dist/assets/doc-browser-C8FM5fC0.js +0 -1
  170. package/dist/assets/doc-browser-RJUOL_GO.js +0 -1
  171. package/dist/assets/doc-browser-p82AdNO-.js +0 -1
  172. package/dist/assets/folder-CeJKPx5P.js +0 -1
  173. package/dist/assets/hash-BqxRTZW5.js +0 -1
  174. package/dist/assets/i18n-DnTGDIRw.js +0 -1
  175. package/dist/assets/index-D8MKmXtO.css +0 -1
  176. package/dist/assets/index-pBvbJ5Mt.js +0 -2
  177. package/dist/assets/loader-circle-fd-vQKtW.js +0 -1
  178. package/dist/assets/logo-badge-KAe-7d8c.js +0 -1
  179. package/dist/assets/logos-C4sYP1Vl.js +0 -1
  180. package/dist/assets/marketplace-page-Cql0kDi-.js +0 -1
  181. package/dist/assets/marketplace-page-m4P5g_Ht.js +0 -49
  182. package/dist/assets/mcp-marketplace-page-9WVKl1m1.js +0 -1
  183. package/dist/assets/mcp-marketplace-page-ByzBQZcx.js +0 -40
  184. package/dist/assets/message-square-z_osm9c0.js +0 -1
  185. package/dist/assets/model-config-Dbr_0APb.js +0 -1
  186. package/dist/assets/play-Dv6Nr1Ew.js +0 -1
  187. package/dist/assets/plus-D8eKFY7h.js +0 -1
  188. package/dist/assets/provider-scoped-model-input-DFm6N2f7.js +0 -1
  189. package/dist/assets/providers-list-BJcLOjun.js +0 -1
  190. package/dist/assets/refresh-ccw-ByVwmnN_.js +0 -1
  191. package/dist/assets/refresh-cw-PcqoYB3K.js +0 -1
  192. package/dist/assets/remote-BOxo9iwd.js +0 -1
  193. package/dist/assets/runtime-config-page-CjLhnbSl.js +0 -1
  194. package/dist/assets/search-config-J4Htco-P.js +0 -1
  195. package/dist/assets/secrets-config-CUdERjco.js +0 -3
  196. package/dist/assets/sessions-config-page-DpK991fs.js +0 -2
  197. package/dist/assets/settings-drbWqzA4.js +0 -1
  198. package/dist/assets/skeleton-BK1SOSRA.js +0 -1
  199. package/dist/assets/theme-provider-0hxjiPc_.js +0 -2
  200. package/dist/assets/tooltip-Cj4yA0gH.js +0 -1
  201. package/dist/assets/trash-2-CBsHCfqq.js +0 -1
  202. package/dist/assets/use-config-38Ur-89i.js +0 -1
  203. package/dist/assets/use-confirm-dialog-DPQThaeU.js +0 -1
  204. package/dist/assets/use-infinite-scroll-loader-5Gf1xQi7.js +0 -1
  205. package/dist/assets/use-viewport-layout-D1XzKeip.js +0 -1
  206. package/dist/assets/x-CM-XDMpk.js +0 -1
  207. package/src/features/chat/components/config/sessions-config-detail-pane.tsx +0 -244
  208. package/src/features/chat/pages/sessions-config-page.test.tsx +0 -152
  209. package/src/features/chat/pages/sessions-config-page.tsx +0 -192
  210. /package/dist/assets/{config-hints-MogHYQ8G.js → config-hints-BNfpOL4J.js} +0 -0
@@ -1,6 +1,5 @@
1
- import { type ComponentProps, useRef } from "react";
1
+ import { useMemo, useRef } from "react";
2
2
  import { useStickyBottomScroll } from "@nextclaw/agent-chat-ui";
3
- import type { ChatFileOpenActionViewModel } from "@nextclaw/agent-chat-ui";
4
3
  import { ChatInputBarContainer } from "@/features/chat/components/conversation/chat-input-bar.container";
5
4
  import { ChatMessageListContainer } from "@/features/chat/components/conversation/chat-message-list.container";
6
5
  import {
@@ -8,11 +7,13 @@ import {
8
7
  ChatParentSessionBanner,
9
8
  } from "@/features/chat/components/conversation/chat-conversation-header";
10
9
  import { ChatWelcome } from "@/features/chat/components/chat-welcome";
11
- import { ChatSessionWorkspacePanel } from "@/features/chat";
10
+ import { ChatSessionWorkspacePanel } from "@/features/chat/components/chat-session-workspace-panel";
12
11
  import { usePresenter } from "@/features/chat/components/providers/chat-presenter.provider";
13
12
  import { resolveAgentRuntimeSessionType } from "@/features/chat/hooks/use-chat-session-type-state";
14
13
  import { useChatInputStore } from "@/features/chat/stores/chat-input.store";
15
14
  import { useChatThreadStore } from "@/features/chat/stores/chat-thread.store";
15
+ import { useCronJobs } from "@/shared/hooks/use-config";
16
+ import { isCronJobForSession } from "@/shared/lib/cron";
16
17
  import { Skeleton } from "@/shared/components/ui/skeleton";
17
18
  import { t } from "@/shared/lib/i18n";
18
19
  import { cn } from "@/shared/lib/utils";
@@ -101,26 +102,17 @@ function ChatConversationSkeleton() {
101
102
  );
102
103
  }
103
104
 
104
- type ChatThreadSnapshot = ReturnType<typeof useChatThreadStore.getState>["snapshot"];
105
- type ChatToolActionHandler = ComponentProps<
106
- typeof ChatMessageListContainer
107
- >["onToolAction"];
108
- type ChatFileOpenHandler = ComponentProps<
109
- typeof ChatMessageListContainer
110
- >["onFileOpen"];
105
+ type ChatThreadSnapshot = ReturnType<
106
+ typeof useChatThreadStore.getState
107
+ >["snapshot"];
108
+ type ChatConversationLayoutMode = "desktop" | "mobile";
109
+
110
+ function ChatConversationAlerts() {
111
+ const presenter = usePresenter();
112
+ const snapshot = useChatThreadStore((state) => state.snapshot);
113
+ const shouldShowProviderHint =
114
+ snapshot.isProviderStateResolved && snapshot.modelOptions.length === 0;
111
115
 
112
- type ChatConversationAlertsProps = {
113
- shouldShowProviderHint: boolean;
114
- sessionTypeUnavailable: boolean;
115
- sessionTypeUnavailableMessage: string | null;
116
- onGoToProviders: () => void;
117
- };
118
- function ChatConversationAlerts({
119
- shouldShowProviderHint,
120
- sessionTypeUnavailable,
121
- sessionTypeUnavailableMessage,
122
- onGoToProviders,
123
- }: ChatConversationAlertsProps) {
124
116
  return (
125
117
  <>
126
118
  {shouldShowProviderHint ? (
@@ -130,17 +122,18 @@ function ChatConversationAlerts({
130
122
  </span>
131
123
  <button
132
124
  type="button"
133
- onClick={onGoToProviders}
125
+ onClick={presenter.chatThreadManager.goToProviders}
134
126
  className="text-xs font-semibold text-amber-900 underline-offset-2 hover:underline"
135
127
  >
136
128
  {t("chatGoConfigureProvider")}
137
129
  </button>
138
130
  </div>
139
131
  ) : null}
140
- {sessionTypeUnavailable && sessionTypeUnavailableMessage?.trim() ? (
132
+ {snapshot.sessionTypeUnavailable &&
133
+ snapshot.sessionTypeUnavailableMessage?.trim() ? (
141
134
  <div className="px-4 py-2.5 border-b border-amber-200/70 bg-amber-50/70 shrink-0 sm:px-5">
142
135
  <span className="text-xs text-amber-800">
143
- {sessionTypeUnavailableMessage}
136
+ {snapshot.sessionTypeUnavailableMessage}
144
137
  </span>
145
138
  </div>
146
139
  ) : null}
@@ -148,31 +141,60 @@ function ChatConversationAlerts({
148
141
  );
149
142
  }
150
143
 
151
- type ChatConversationContentProps = {
152
- snapshot: ChatThreadSnapshot;
153
- availableAgents: NonNullable<ChatThreadSnapshot["availableAgents"]>;
154
- hideEmptyHint: boolean;
155
- showWelcome: boolean;
156
- threadRef: ComponentProps<"div">["ref"];
157
- onScroll: ComponentProps<"div">["onScroll"];
158
- onCreateSession: () => void;
159
- onSelectAgent: (agentId: string) => void;
160
- onToolAction: ChatToolActionHandler;
161
- onFileOpen: ChatFileOpenHandler;
162
- };
163
-
164
144
  function ChatConversationContent({
165
- snapshot,
166
- availableAgents,
167
- hideEmptyHint,
168
- showWelcome,
169
- threadRef,
170
- onScroll,
171
- onCreateSession,
172
- onSelectAgent,
173
- onToolAction,
174
- onFileOpen,
175
- }: ChatConversationContentProps) {
145
+ layoutMode,
146
+ }: {
147
+ layoutMode: ChatConversationLayoutMode;
148
+ }) {
149
+ const presenter = usePresenter();
150
+ const defaultSessionType = useChatInputStore(
151
+ (state) => state.snapshot.defaultSessionType,
152
+ );
153
+ const snapshot = useChatThreadStore((state) => state.snapshot);
154
+ const fallbackThreadRef = useRef<HTMLDivElement | null>(null);
155
+ const threadRef = snapshot.threadRef ?? fallbackThreadRef;
156
+ const availableAgents = snapshot.availableAgents ?? [];
157
+ const showWelcome =
158
+ !snapshot.canDeleteSession &&
159
+ !snapshot.hasSubmittedDraftMessage &&
160
+ snapshot.messages.length === 0 &&
161
+ !snapshot.isSending;
162
+ const hideEmptyHint =
163
+ snapshot.isHistoryLoading &&
164
+ snapshot.messages.length === 0 &&
165
+ !snapshot.isSending &&
166
+ !snapshot.isAwaitingAssistantOutput;
167
+ const resolveDraftAgent = (agentId: string) =>
168
+ availableAgents.find((agent) => agent.id === agentId) ?? null;
169
+ const createDraftSessionForAgent = () => {
170
+ const sessionType = resolveAgentRuntimeSessionType(
171
+ resolveDraftAgent(snapshot.agentId ?? "main"),
172
+ defaultSessionType,
173
+ );
174
+ presenter.chatSessionListManager.createSession(sessionType);
175
+ if (layoutMode === "mobile") presenter.chatUiManager.goToChatRoot();
176
+ };
177
+ const selectDraftAgent = (agentId: string) => {
178
+ presenter.chatSessionListManager.setSelectedAgentId(agentId);
179
+ presenter.chatInputManager.setPendingSessionType(
180
+ resolveAgentRuntimeSessionType(
181
+ resolveDraftAgent(agentId),
182
+ defaultSessionType,
183
+ ),
184
+ );
185
+ };
186
+ const { onScroll } = useStickyBottomScroll({
187
+ scrollRef: threadRef,
188
+ resetKey: snapshot.sessionKey,
189
+ isLoading: snapshot.isHistoryLoading,
190
+ hasContent: snapshot.messages.length > 0,
191
+ contentVersion: snapshot.messages[snapshot.messages.length - 1] ?? null,
192
+ });
193
+ const hasMessages = snapshot.messages.length > 0;
194
+ const isAwaitingAssistantOutput =
195
+ hasMessages && snapshot.isSending && snapshot.isAwaitingAssistantOutput;
196
+ const shouldShowMessages = hasMessages;
197
+
176
198
  return (
177
199
  <div
178
200
  ref={threadRef}
@@ -181,23 +203,18 @@ function ChatConversationContent({
181
203
  >
182
204
  {showWelcome ? (
183
205
  <ChatWelcome
184
- onCreateSession={onCreateSession}
206
+ onCreateSession={createDraftSessionForAgent}
185
207
  agents={availableAgents}
186
208
  selectedAgentId={snapshot.agentId ?? "main"}
187
- onSelectAgent={onSelectAgent}
209
+ onSelectAgent={selectDraftAgent}
188
210
  />
189
- ) : hideEmptyHint ? null : snapshot.messages.length === 0 ? (
190
- <div className="px-4 py-4 text-sm text-gray-500 sm:px-5 sm:py-5">
191
- {t("chatNoMessages")}
192
- </div>
193
- ) : (
211
+ ) : hideEmptyHint || !shouldShowMessages ? null : (
194
212
  <div className="mx-auto w-full max-w-[min(1120px,100%)] px-4 py-4 sm:px-6 sm:py-5">
195
213
  <ChatMessageListContainer
196
- key={snapshot.sessionKey ?? "draft"}
197
214
  messages={snapshot.messages}
198
- isSending={snapshot.isSending && snapshot.isAwaitingAssistantOutput}
199
- onToolAction={onToolAction}
200
- onFileOpen={onFileOpen}
215
+ isSending={isAwaitingAssistantOutput}
216
+ onToolAction={presenter.chatThreadManager.openSessionFromToolAction}
217
+ onFileOpen={presenter.chatThreadManager.openFilePreview}
201
218
  />
202
219
  </div>
203
220
  )}
@@ -209,82 +226,90 @@ function shouldShowWorkspacePanel(
209
226
  snapshot: ChatThreadSnapshot,
210
227
  childSessionTabs: ChatThreadSnapshot["childSessionTabs"],
211
228
  workspaceFileTabs: ChatThreadSnapshot["workspaceFileTabs"],
229
+ sessionCronJobCount: number,
212
230
  ) {
213
231
  if (snapshot.workspacePanelParentKey !== snapshot.sessionKey) {
214
232
  return false;
215
233
  }
216
- return childSessionTabs.length > 0 || workspaceFileTabs.length > 0;
234
+ return (
235
+ childSessionTabs.length > 0 ||
236
+ workspaceFileTabs.length > 0 ||
237
+ sessionCronJobCount > 0
238
+ );
217
239
  }
218
240
 
219
- export function ChatConversationPanel({
220
- layoutMode = "desktop",
221
- onBackToList,
222
- }: {
223
- layoutMode?: "desktop" | "mobile";
224
- onBackToList?: () => void;
225
- }) {
226
- const presenter = usePresenter();
227
- const defaultSessionType = useChatInputStore(
228
- (state) => state.snapshot.defaultSessionType,
241
+ function useSessionWorkspaceState(snapshot: ChatThreadSnapshot) {
242
+ const childSessionTabs = useMemo(
243
+ () =>
244
+ snapshot.childSessionTabs.filter(
245
+ (tab) => tab.parentSessionKey === snapshot.sessionKey,
246
+ ),
247
+ [snapshot.childSessionTabs, snapshot.sessionKey],
229
248
  );
230
- const snapshot = useChatThreadStore((state) => state.snapshot);
231
- const fallbackThreadRef = useRef<HTMLDivElement | null>(null);
232
- const threadRef = snapshot.threadRef ?? fallbackThreadRef;
233
- const childSessionTabs = snapshot.childSessionTabs.filter(
234
- (tab) => tab.parentSessionKey === snapshot.sessionKey,
249
+ const workspaceFileTabs = useMemo(
250
+ () =>
251
+ snapshot.workspaceFileTabs.filter(
252
+ (tab) => tab.parentSessionKey === snapshot.sessionKey,
253
+ ),
254
+ [snapshot.sessionKey, snapshot.workspaceFileTabs],
235
255
  );
236
- const workspaceFileTabs = snapshot.workspaceFileTabs.filter(
237
- (tab) => tab.parentSessionKey === snapshot.sessionKey,
256
+ const cronQuery = useCronJobs({ all: true });
257
+ const sessionCronJobs = useMemo(
258
+ () =>
259
+ (cronQuery.data?.jobs ?? []).filter((job) =>
260
+ isCronJobForSession(job, snapshot.sessionKey),
261
+ ),
262
+ [cronQuery.data?.jobs, snapshot.sessionKey],
238
263
  );
239
- const showWorkspacePanel = shouldShowWorkspacePanel(
240
- snapshot,
264
+ return {
241
265
  childSessionTabs,
242
266
  workspaceFileTabs,
267
+ sessionCronJobs,
268
+ showWorkspacePanel: shouldShowWorkspacePanel(
269
+ snapshot,
270
+ childSessionTabs,
271
+ workspaceFileTabs,
272
+ sessionCronJobs.length,
273
+ ),
274
+ };
275
+ }
276
+
277
+ function ChatParentSessionBannerContainer() {
278
+ const presenter = usePresenter();
279
+ const snapshot = useChatThreadStore((state) => state.snapshot);
280
+ return (
281
+ <ChatParentSessionBanner
282
+ parentSessionLabel={
283
+ snapshot.parentSessionKey ? (snapshot.parentSessionLabel ?? null) : null
284
+ }
285
+ onGoToParentSession={presenter.chatThreadManager.goToParentSession}
286
+ />
243
287
  );
288
+ }
289
+
290
+ function ChatConversationHeaderContainer({
291
+ layoutMode,
292
+ onBackToList,
293
+ }: {
294
+ layoutMode: ChatConversationLayoutMode;
295
+ onBackToList?: () => void;
296
+ }) {
297
+ const presenter = usePresenter();
298
+ const snapshot = useChatThreadStore((state) => state.snapshot);
299
+ const { childSessionTabs, sessionCronJobs } =
300
+ useSessionWorkspaceState(snapshot);
244
301
  const shouldShowSessionHeader = Boolean(
245
302
  snapshot.sessionKey || snapshot.sessionTypeLabel,
246
303
  );
247
304
  const sessionHeaderTitle =
248
305
  snapshot.sessionDisplayName ||
249
- (snapshot.canDeleteSession && snapshot.sessionKey ? snapshot.sessionKey : null) ||
306
+ (snapshot.canDeleteSession && snapshot.sessionKey
307
+ ? snapshot.sessionKey
308
+ : null) ||
250
309
  t("chatSidebarNewTask");
251
310
  const normalizedAgentId = snapshot.agentId?.trim() ?? "";
252
311
  const shouldShowHeaderAgentAvatar =
253
- normalizedAgentId.length > 0 &&
254
- normalizedAgentId.toLowerCase() !== "main";
255
-
256
- const showWelcome =
257
- !snapshot.canDeleteSession &&
258
- snapshot.messages.length === 0 &&
259
- !snapshot.isSending;
260
- const hasConfiguredModel = snapshot.modelOptions.length > 0;
261
- const shouldShowProviderHint =
262
- snapshot.isProviderStateResolved && !hasConfiguredModel;
263
- const hideEmptyHint =
264
- snapshot.isHistoryLoading &&
265
- snapshot.messages.length === 0 &&
266
- !snapshot.isSending &&
267
- !snapshot.isAwaitingAssistantOutput;
268
- const availableAgents = snapshot.availableAgents ?? [];
269
- const resolveDraftAgent = (agentId: string) =>
270
- availableAgents.find((agent) => agent.id === agentId) ?? null;
271
- const createDraftSessionForAgent = () => {
272
- const sessionType = resolveAgentRuntimeSessionType(
273
- resolveDraftAgent(snapshot.agentId ?? "main"),
274
- defaultSessionType,
275
- );
276
- presenter.chatSessionListManager.createSession(sessionType);
277
- if (layoutMode === "mobile") presenter.chatUiManager.goToChatRoot();
278
- };
279
- const selectDraftAgent = (agentId: string) => {
280
- presenter.chatSessionListManager.setSelectedAgentId(agentId);
281
- presenter.chatInputManager.setPendingSessionType(
282
- resolveAgentRuntimeSessionType(resolveDraftAgent(agentId), defaultSessionType),
283
- );
284
- };
285
- const openFilePreview = (action: ChatFileOpenActionViewModel) => {
286
- presenter.chatThreadManager.openFilePreview(action);
287
- };
312
+ normalizedAgentId.length > 0 && normalizedAgentId.toLowerCase() !== "main";
288
313
  const openChildSessions = () => {
289
314
  if (!snapshot.sessionKey) {
290
315
  return;
@@ -294,14 +319,71 @@ export function ChatConversationPanel({
294
319
  activeChildSessionKey: childSessionTabs[0]?.sessionKey ?? null,
295
320
  });
296
321
  };
322
+ const openSessionCronJobs = () => {
323
+ if (!snapshot.sessionKey || sessionCronJobs.length === 0) {
324
+ return;
325
+ }
326
+ presenter.chatThreadManager.openSessionCronPanel(snapshot.sessionKey);
327
+ };
297
328
 
298
- const { onScroll: handleScroll } = useStickyBottomScroll({
299
- scrollRef: threadRef,
300
- resetKey: snapshot.sessionKey,
301
- isLoading: snapshot.isHistoryLoading,
302
- hasContent: snapshot.messages.length > 0,
303
- contentVersion: snapshot.messages[snapshot.messages.length - 1] ?? null,
304
- });
329
+ return (
330
+ <ChatConversationHeader
331
+ snapshot={snapshot}
332
+ childSessionCount={childSessionTabs.length}
333
+ sessionCronJobCount={sessionCronJobs.length}
334
+ layoutMode={layoutMode}
335
+ normalizedAgentId={normalizedAgentId}
336
+ sessionHeaderTitle={sessionHeaderTitle}
337
+ shouldShowHeaderAgentAvatar={shouldShowHeaderAgentAvatar}
338
+ shouldShowSessionHeader={shouldShowSessionHeader}
339
+ onBackToList={onBackToList}
340
+ onOpenChildSessions={openChildSessions}
341
+ onOpenSessionCronJobs={openSessionCronJobs}
342
+ onDeleteSession={presenter.chatThreadManager.deleteSession}
343
+ />
344
+ );
345
+ }
346
+
347
+ function ChatSessionWorkspacePanelContainer({
348
+ layoutMode,
349
+ }: {
350
+ layoutMode: ChatConversationLayoutMode;
351
+ }) {
352
+ const snapshot = useChatThreadStore((state) => state.snapshot);
353
+ const {
354
+ childSessionTabs,
355
+ workspaceFileTabs,
356
+ sessionCronJobs,
357
+ showWorkspacePanel,
358
+ } = useSessionWorkspaceState(snapshot);
359
+
360
+ if (!showWorkspacePanel) {
361
+ return null;
362
+ }
363
+
364
+ return (
365
+ <ChatSessionWorkspacePanel
366
+ sessionKey={snapshot.sessionKey}
367
+ childSessionTabs={childSessionTabs}
368
+ activeChildSessionKey={snapshot.activeChildSessionKey ?? null}
369
+ workspaceFileTabs={workspaceFileTabs}
370
+ activeWorkspaceFileKey={snapshot.activeWorkspaceFileKey ?? null}
371
+ activePanelKind={snapshot.activeWorkspacePanelKind ?? null}
372
+ sessionCronJobs={sessionCronJobs}
373
+ sessionProjectRoot={snapshot.sessionProjectRoot ?? null}
374
+ displayMode={layoutMode === "mobile" ? "overlay" : "docked"}
375
+ />
376
+ );
377
+ }
378
+
379
+ export function ChatConversationPanel({
380
+ layoutMode = "desktop",
381
+ onBackToList,
382
+ }: {
383
+ layoutMode?: ChatConversationLayoutMode;
384
+ onBackToList?: () => void;
385
+ }) {
386
+ const snapshot = useChatThreadStore((state) => state.snapshot);
305
387
 
306
388
  if (!snapshot.isProviderStateResolved) {
307
389
  return <ChatConversationSkeleton />;
@@ -310,63 +392,17 @@ export function ChatConversationPanel({
310
392
  return (
311
393
  <section className="flex-1 min-h-0 flex overflow-hidden bg-gradient-to-b from-gray-50/60 to-white">
312
394
  <div className="flex min-h-0 flex-1 flex-col overflow-hidden">
313
- <ChatParentSessionBanner
314
- parentSessionLabel={
315
- snapshot.parentSessionKey ? (snapshot.parentSessionLabel ?? null) : null
316
- }
317
- onGoToParentSession={presenter.chatThreadManager.goToParentSession}
318
- />
319
- <ChatConversationHeader
320
- snapshot={snapshot}
321
- childSessionCount={childSessionTabs.length}
395
+ <ChatParentSessionBannerContainer />
396
+ <ChatConversationHeaderContainer
322
397
  layoutMode={layoutMode}
323
- normalizedAgentId={normalizedAgentId}
324
- sessionHeaderTitle={sessionHeaderTitle}
325
- shouldShowHeaderAgentAvatar={shouldShowHeaderAgentAvatar}
326
- shouldShowSessionHeader={shouldShowSessionHeader}
327
398
  onBackToList={onBackToList}
328
- onOpenChildSessions={openChildSessions}
329
- onDeleteSession={presenter.chatThreadManager.deleteSession}
330
- />
331
- <ChatConversationAlerts
332
- shouldShowProviderHint={shouldShowProviderHint}
333
- sessionTypeUnavailable={snapshot.sessionTypeUnavailable}
334
- sessionTypeUnavailableMessage={snapshot.sessionTypeUnavailableMessage ?? null}
335
- onGoToProviders={presenter.chatThreadManager.goToProviders}
336
399
  />
337
- <ChatConversationContent
338
- snapshot={snapshot}
339
- availableAgents={availableAgents}
340
- hideEmptyHint={hideEmptyHint}
341
- showWelcome={showWelcome}
342
- threadRef={threadRef}
343
- onScroll={handleScroll}
344
- onCreateSession={createDraftSessionForAgent}
345
- onSelectAgent={selectDraftAgent}
346
- onToolAction={presenter.chatThreadManager.openSessionFromToolAction}
347
- onFileOpen={openFilePreview}
348
- />
349
-
400
+ <ChatConversationAlerts />
401
+ <ChatConversationContent layoutMode={layoutMode} />
350
402
  <ChatInputBarContainer />
351
403
  </div>
352
404
 
353
- {showWorkspacePanel ? (
354
- <ChatSessionWorkspacePanel
355
- childSessionTabs={childSessionTabs}
356
- activeChildSessionKey={snapshot.activeChildSessionKey ?? null}
357
- workspaceFileTabs={workspaceFileTabs}
358
- activeWorkspaceFileKey={snapshot.activeWorkspaceFileKey ?? null}
359
- sessionProjectRoot={snapshot.sessionProjectRoot ?? null}
360
- displayMode={layoutMode === "mobile" ? "overlay" : "docked"}
361
- onSelectSession={presenter.chatThreadManager.selectChildSessionDetail}
362
- onSelectFile={presenter.chatThreadManager.selectWorkspaceFile}
363
- onCloseFile={presenter.chatThreadManager.closeWorkspaceFile}
364
- onClose={presenter.chatThreadManager.closeWorkspacePanel}
365
- onBackToParent={presenter.chatThreadManager.goToParentSession}
366
- onToolAction={presenter.chatThreadManager.openSessionFromToolAction}
367
- onFileOpen={openFilePreview}
368
- />
369
- ) : null}
405
+ <ChatSessionWorkspacePanelContainer layoutMode={layoutMode} />
370
406
  </section>
371
407
  );
372
408
  }
@@ -1,4 +1,4 @@
1
- import { useCallback, useMemo, useRef, useState, type ChangeEvent, type RefObject } from 'react';
1
+ import { useCallback, useEffect, useMemo, useRef, useState, type ChangeEvent, type RefObject } from 'react';
2
2
  import { ChatInputBar, type ChatInputBarHandle } from '@nextclaw/agent-chat-ui';
3
3
  import { DEFAULT_NCP_ATTACHMENT_MAX_BYTES, uploadFilesAsNcpDraftAttachments } from '@nextclaw/ncp-react';
4
4
  import { uploadNcpAssets } from '@/shared/lib/api';
@@ -234,6 +234,16 @@ export function ChatInputBarContainer() {
234
234
  ? t('chatStopPreparing')
235
235
  : snapshot.stopDisabledReason?.trim() || t('chatStopUnavailable');
236
236
  const { handleFilesAdd, handleFileInputChange } = useChatInputBarAttachments({ attachmentSupported, inputBarRef, presenter });
237
+ useEffect(() => {
238
+ const request = snapshot.composerFocusRequest;
239
+ if (!request) {
240
+ return;
241
+ }
242
+ if (request.placement === 'end') {
243
+ inputBarRef.current?.focusComposerAtEnd();
244
+ }
245
+ presenter.chatInputManager.consumeComposerFocusRequest(request.id);
246
+ }, [presenter.chatInputManager, snapshot.composerFocusRequest]);
237
247
  const toolbarSelects = buildToolbarSelects({
238
248
  allModelsLabel: labels.allModelsLabel,
239
249
  hasModelOptions,
@@ -7,6 +7,7 @@ const mocks = vi.hoisted(() => ({
7
7
  updateSessionProject: vi.fn(),
8
8
  onDeleteSession: vi.fn(),
9
9
  onOpenChildSessions: vi.fn(),
10
+ onOpenSessionCronJobs: vi.fn(),
10
11
  }));
11
12
 
12
13
  vi.mock('@/features/chat/hooks/use-chat-session-project', () => ({
@@ -22,6 +23,7 @@ describe('ChatSessionHeaderActions', () => {
22
23
  mocks.updateSessionProject.mockReset();
23
24
  mocks.onDeleteSession.mockReset();
24
25
  mocks.onOpenChildSessions.mockReset();
26
+ mocks.onOpenSessionCronJobs.mockReset();
25
27
  });
26
28
 
27
29
  it('keeps only the set-project action in the more-actions menu when a project is already attached', async () => {
@@ -86,4 +88,26 @@ describe('ChatSessionHeaderActions', () => {
86
88
 
87
89
  expect(mocks.onOpenChildSessions).toHaveBeenCalledTimes(1);
88
90
  });
91
+
92
+ it('uses a shared spaced action group for child, cron, and menu buttons', () => {
93
+ render(
94
+ <ChatSessionHeaderActions
95
+ sessionKey="session-actions"
96
+ canDeleteSession
97
+ isDeletePending={false}
98
+ projectRoot={null}
99
+ childSessionCount={1}
100
+ sessionCronJobCount={1}
101
+ onOpenChildSessions={mocks.onOpenChildSessions}
102
+ onOpenSessionCronJobs={mocks.onOpenSessionCronJobs}
103
+ onDeleteSession={mocks.onDeleteSession}
104
+ />
105
+ );
106
+
107
+ const actionGroup = screen.getByRole('button', { name: 'More actions' }).parentElement;
108
+
109
+ expect(actionGroup?.className).toContain('gap-1.5');
110
+ expect(screen.getByRole('button', { name: 'View child sessions' }).className).toContain('h-7');
111
+ expect(screen.getByRole('button', { name: 'View session cron jobs' }).className).toContain('w-7');
112
+ });
89
113
  });
@@ -1,5 +1,5 @@
1
1
  import { useState } from 'react';
2
- import { FolderOpen, GitBranch, MoreHorizontal, Trash2 } from 'lucide-react';
2
+ import { AlarmClock, FolderOpen, GitBranch, MoreVertical, Trash2 } from 'lucide-react';
3
3
  import { Button } from '@/shared/components/ui/button';
4
4
  import { Popover, PopoverContent, PopoverTrigger } from '@/shared/components/ui/popover';
5
5
  import { useChatSessionProject } from '@/features/chat/hooks/use-chat-session-project';
@@ -7,13 +7,18 @@ import { ChatSessionHeaderMenuItem } from './chat-session-header-menu-item';
7
7
  import { ChatSessionProjectDialog } from './chat-session-project-dialog';
8
8
  import { t } from '@/shared/lib/i18n';
9
9
 
10
+ const SESSION_HEADER_ACTION_GROUP_CLASS = 'flex shrink-0 items-center gap-1.5';
11
+ const SESSION_HEADER_ACTION_BUTTON_CLASS = 'h-7 w-7 rounded-lg shrink-0 text-gray-400 hover:text-gray-700';
12
+
10
13
  type ChatSessionHeaderActionsProps = {
11
14
  sessionKey: string;
12
15
  canDeleteSession: boolean;
13
16
  isDeletePending: boolean;
14
17
  projectRoot?: string | null;
15
18
  childSessionCount?: number;
19
+ sessionCronJobCount?: number;
16
20
  onOpenChildSessions?: () => void;
21
+ onOpenSessionCronJobs?: () => void;
17
22
  onDeleteSession: () => void;
18
23
  };
19
24
 
@@ -23,7 +28,9 @@ export function ChatSessionHeaderActions({
23
28
  isDeletePending,
24
29
  projectRoot,
25
30
  childSessionCount = 0,
31
+ sessionCronJobCount = 0,
26
32
  onOpenChildSessions,
33
+ onOpenSessionCronJobs,
27
34
  onDeleteSession,
28
35
  }: ChatSessionHeaderActionsProps) {
29
36
  const updateSessionProject = useChatSessionProject();
@@ -49,13 +56,13 @@ export function ChatSessionHeaderActions({
49
56
  };
50
57
 
51
58
  return (
52
- <>
59
+ <div className={SESSION_HEADER_ACTION_GROUP_CLASS}>
53
60
  {childSessionCount > 0 && onOpenChildSessions ? (
54
61
  <Button
55
62
  type="button"
56
63
  variant="ghost"
57
64
  size="icon"
58
- className="rounded-lg shrink-0 text-gray-400 hover:text-gray-700"
65
+ className={SESSION_HEADER_ACTION_BUTTON_CLASS}
59
66
  aria-label={t('chatSessionOpenChildSessions')}
60
67
  title={t('chatSessionOpenChildSessions')}
61
68
  onClick={onOpenChildSessions}
@@ -64,16 +71,30 @@ export function ChatSessionHeaderActions({
64
71
  <GitBranch className="h-4 w-4" />
65
72
  </Button>
66
73
  ) : null}
74
+ {sessionCronJobCount > 0 && onOpenSessionCronJobs ? (
75
+ <Button
76
+ type="button"
77
+ variant="ghost"
78
+ size="icon"
79
+ className={SESSION_HEADER_ACTION_BUTTON_CLASS}
80
+ aria-label={t('chatSessionOpenCronJobs')}
81
+ title={t('chatSessionOpenCronJobs')}
82
+ onClick={onOpenSessionCronJobs}
83
+ disabled={isBusy}
84
+ >
85
+ <AlarmClock className="h-4 w-4" />
86
+ </Button>
87
+ ) : null}
67
88
  <Popover open={isMenuOpen} onOpenChange={setIsMenuOpen}>
68
89
  <PopoverTrigger asChild>
69
90
  <Button
70
91
  variant="ghost"
71
92
  size="icon"
72
- className="rounded-lg shrink-0 text-gray-400 hover:text-gray-700"
93
+ className={SESSION_HEADER_ACTION_BUTTON_CLASS}
73
94
  aria-label={t('chatSessionMoreActions')}
74
95
  disabled={isBusy}
75
96
  >
76
- <MoreHorizontal className="h-4 w-4" />
97
+ <MoreVertical className="h-4 w-4" />
77
98
  </Button>
78
99
  </PopoverTrigger>
79
100
  <PopoverContent align="end" className="w-56 p-2">
@@ -108,6 +129,6 @@ export function ChatSessionHeaderActions({
108
129
  onOpenChange={setIsDialogOpen}
109
130
  onSave={runProjectUpdate}
110
131
  />
111
- </>
132
+ </div>
112
133
  );
113
134
  }