@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
@@ -4,6 +4,7 @@ import type {
4
4
  ChatFileOpenActionViewModel,
5
5
  ChatToolActionViewModel,
6
6
  } from "@nextclaw/agent-chat-ui";
7
+ import type { CronJobView } from "@/shared/lib/api";
7
8
  import { useStickyBottomScroll } from "@nextclaw/agent-chat-ui";
8
9
  import { ChatMessageListContainer } from "@/features/chat/components/conversation/chat-message-list.container";
9
10
  import {
@@ -28,23 +29,20 @@ import {
28
29
  import { usePresenter } from "@/features/chat/components/providers/chat-presenter.provider";
29
30
  import { ChatSessionWorkspaceFilePreview } from "./chat-session-workspace-file-preview";
30
31
  import { AgentIdentityAvatar } from "@/shared/components/common/agent-identity";
32
+ import { SessionCronJobContent } from "@/features/chat/components/workspace/session-cron-job-content";
31
33
  import { t } from "@/shared/lib/i18n";
32
34
  import { cn } from "@/shared/lib/utils";
33
35
 
34
36
  type ChatSessionWorkspacePanelProps = {
37
+ sessionKey: string | null;
35
38
  childSessionTabs: readonly ChatChildSessionTab[];
36
39
  activeChildSessionKey: string | null;
37
40
  workspaceFileTabs: readonly ChatWorkspaceFileTab[];
38
41
  activeWorkspaceFileKey: string | null;
42
+ activePanelKind?: "child-session" | "file" | "cron" | null;
43
+ sessionCronJobs?: readonly CronJobView[];
39
44
  sessionProjectRoot: string | null;
40
45
  displayMode?: "docked" | "overlay";
41
- onSelectSession: (sessionKey: string) => void;
42
- onSelectFile: (fileKey: string) => void;
43
- onCloseFile: (fileKey: string) => void;
44
- onClose: () => void;
45
- onBackToParent: () => void;
46
- onToolAction?: (action: ChatToolActionViewModel) => void;
47
- onFileOpen: (action: ChatFileOpenActionViewModel) => void;
48
46
  };
49
47
 
50
48
  function ChildSessionContent({
@@ -141,11 +139,7 @@ function ChildSessionMetaStrip({ tab }: { tab: ResolvedChildSessionTab }) {
141
139
  );
142
140
  }
143
141
 
144
- function WorkspaceActiveChildHeader({
145
- tab,
146
- }: {
147
- tab: ResolvedChildSessionTab;
148
- }) {
142
+ function WorkspaceActiveChildHeader({ tab }: { tab: ResolvedChildSessionTab }) {
149
143
  return (
150
144
  <div className="border-b border-gray-200/70 px-4 py-3">
151
145
  <div className="flex min-w-0 items-center gap-2 text-sm font-semibold text-gray-900">
@@ -167,20 +161,24 @@ function WorkspaceActiveChildHeader({
167
161
  function buildWorkspaceTabsViewModel(params: {
168
162
  resolvedChildTabs: ResolvedChildSessionTab[];
169
163
  workspaceFileTabs: readonly ChatWorkspaceFileTab[];
164
+ sessionCronJobCount: number;
170
165
  activeSelection: ReturnType<typeof resolveWorkspaceSelection>;
171
166
  optimisticReadAtBySessionKey: Record<string, string>;
172
167
  onSelectSession: (sessionKey: string) => void;
173
168
  onSelectFile: (fileKey: string) => void;
174
169
  onCloseFile: (fileKey: string) => void;
170
+ onSelectCronJobs: () => void;
175
171
  }): WorkspaceTabViewModel[] {
176
172
  const {
177
173
  resolvedChildTabs,
178
174
  workspaceFileTabs,
175
+ sessionCronJobCount,
179
176
  activeSelection,
180
177
  optimisticReadAtBySessionKey,
181
178
  onSelectSession,
182
179
  onSelectFile,
183
180
  onCloseFile,
181
+ onSelectCronJobs,
184
182
  } = params;
185
183
 
186
184
  const childTabs = resolvedChildTabs.map((tab) => {
@@ -190,7 +188,7 @@ function buildWorkspaceTabsViewModel(params: {
190
188
  ? optimisticReadAt.localeCompare(tab.readAt) > 0
191
189
  ? optimisticReadAt
192
190
  : tab.readAt
193
- : optimisticReadAt ?? tab.readAt;
191
+ : (optimisticReadAt ?? tab.readAt);
194
192
  return {
195
193
  key: `child:${tab.sessionKey}`,
196
194
  kind: "child-session" as const,
@@ -219,29 +217,38 @@ function buildWorkspaceTabsViewModel(params: {
219
217
  tooltip: file.path,
220
218
  viewMode: file.viewMode,
221
219
  active:
222
- activeSelection?.kind === "file" &&
223
- activeSelection.file.key === file.key,
220
+ activeSelection?.kind === "file" && activeSelection.file.key === file.key,
224
221
  onSelect: () => onSelectFile(file.key),
225
222
  onClose: () => onCloseFile(file.key),
226
223
  }));
227
224
 
228
- return [...childTabs, ...fileTabs];
225
+ const cronTab =
226
+ sessionCronJobCount > 0
227
+ ? [
228
+ {
229
+ key: "cron:session",
230
+ kind: "cron" as const,
231
+ title: t("chatWorkspaceSessionCronJobs"),
232
+ tooltip: t("chatWorkspaceSessionCronJobs"),
233
+ active: activeSelection?.kind === "cron",
234
+ onSelect: onSelectCronJobs,
235
+ },
236
+ ]
237
+ : [];
238
+
239
+ return [...childTabs, ...fileTabs, ...cronTab];
229
240
  }
230
241
 
231
242
  export function ChatSessionWorkspacePanel({
243
+ sessionKey,
232
244
  childSessionTabs,
233
245
  activeChildSessionKey,
234
246
  workspaceFileTabs,
235
247
  activeWorkspaceFileKey,
248
+ activePanelKind,
249
+ sessionCronJobs = [],
236
250
  sessionProjectRoot,
237
251
  displayMode = "docked",
238
- onSelectSession,
239
- onSelectFile,
240
- onCloseFile,
241
- onClose,
242
- onBackToParent,
243
- onToolAction,
244
- onFileOpen,
245
252
  }: ChatSessionWorkspacePanelProps) {
246
253
  const presenter = usePresenter();
247
254
  const resolvedChildTabs = useNcpChildSessionTabsView(childSessionTabs);
@@ -251,8 +258,10 @@ export function ChatSessionWorkspacePanel({
251
258
  const activeSelection = resolveWorkspaceSelection({
252
259
  activeChildSessionKey,
253
260
  activeWorkspaceFileKey,
261
+ activePanelKind,
254
262
  childSessionTabs: resolvedChildTabs,
255
263
  workspaceFileTabs,
264
+ sessionCronJobCount: sessionCronJobs.length,
256
265
  });
257
266
  const hasParentSession = resolvedChildTabs.some((tab) =>
258
267
  Boolean(tab.parentSessionKey),
@@ -278,20 +287,25 @@ export function ChatSessionWorkspacePanel({
278
287
  buildWorkspaceTabsViewModel({
279
288
  resolvedChildTabs,
280
289
  workspaceFileTabs,
290
+ sessionCronJobCount: sessionCronJobs.length,
281
291
  activeSelection,
282
292
  optimisticReadAtBySessionKey,
283
- onSelectSession,
284
- onSelectFile,
285
- onCloseFile,
293
+ onSelectSession: presenter.chatThreadManager.selectChildSessionDetail,
294
+ onSelectFile: presenter.chatThreadManager.selectWorkspaceFile,
295
+ onCloseFile: presenter.chatThreadManager.closeWorkspaceFile,
296
+ onSelectCronJobs: () => {
297
+ if (sessionKey)
298
+ presenter.chatThreadManager.openSessionCronPanel(sessionKey);
299
+ },
286
300
  }),
287
301
  [
288
302
  activeSelection,
289
- onCloseFile,
290
- onSelectFile,
291
- onSelectSession,
292
303
  optimisticReadAtBySessionKey,
304
+ presenter.chatThreadManager,
293
305
  resolvedChildTabs,
306
+ sessionKey,
294
307
  workspaceFileTabs,
308
+ sessionCronJobs.length,
295
309
  ],
296
310
  );
297
311
 
@@ -311,7 +325,7 @@ export function ChatSessionWorkspacePanel({
311
325
  <div className="flex items-center justify-between gap-3 border-b border-gray-200/70 px-4 py-2.5">
312
326
  <button
313
327
  type="button"
314
- onClick={onBackToParent}
328
+ onClick={presenter.chatThreadManager.goToParentSession}
315
329
  className={cn(
316
330
  "inline-flex items-center gap-1 text-xs font-medium text-gray-600 transition-colors hover:text-gray-900",
317
331
  !hasParentSession && "pointer-events-none opacity-0",
@@ -322,7 +336,7 @@ export function ChatSessionWorkspacePanel({
322
336
  </button>
323
337
  <button
324
338
  type="button"
325
- onClick={onClose}
339
+ onClick={presenter.chatThreadManager.closeWorkspacePanel}
326
340
  className="rounded-full border border-gray-200/80 p-1.5 text-gray-500 transition-colors hover:bg-gray-100 hover:text-gray-900"
327
341
  aria-label={t("chatWorkspaceClosePanel")}
328
342
  >
@@ -339,17 +353,21 @@ export function ChatSessionWorkspacePanel({
339
353
  <div className="flex-1 min-h-0">
340
354
  <ChildSessionContent
341
355
  sessionKey={activeSelection.tab.sessionKey}
342
- onToolAction={onToolAction}
343
- onFileOpen={onFileOpen}
356
+ onToolAction={
357
+ presenter.chatThreadManager.openSessionFromToolAction
358
+ }
359
+ onFileOpen={presenter.chatThreadManager.openFilePreview}
344
360
  />
345
361
  </div>
346
362
  </>
347
- ) : (
363
+ ) : activeSelection.kind === "file" ? (
348
364
  <ChatSessionWorkspaceFilePreview
349
365
  file={activeSelection.file}
350
366
  sessionProjectRoot={sessionProjectRoot}
351
- onFileOpen={onFileOpen}
367
+ onFileOpen={presenter.chatThreadManager.openFilePreview}
352
368
  />
369
+ ) : (
370
+ <SessionCronJobContent jobs={sessionCronJobs} />
353
371
  )}
354
372
  </div>
355
373
  </div>
@@ -5,9 +5,13 @@ import { SessionRunBadge } from '@/features/chat/components/session/session-run-
5
5
  import { Button } from '@/shared/components/ui/button';
6
6
  import { Input } from '@/shared/components/ui/input';
7
7
  import { type SessionContextView } from '@/features/chat/utils/session-context.utils';
8
+ import {
9
+ formatSessionListTime,
10
+ sessionActivityPreviewText
11
+ } from '@/features/chat/utils/chat-session-display.utils';
8
12
  import type { SessionRunStatus } from '@/features/chat/types/session-run-status.types';
9
13
  import { cn } from '@/shared/lib/utils';
10
- import { formatDateShort, t } from '@/shared/lib/i18n';
14
+ import { t } from '@/shared/lib/i18n';
11
15
  import { Check, GitBranch, Pencil, X } from 'lucide-react';
12
16
 
13
17
  type ChatSidebarSessionItemProps = {
@@ -116,11 +120,13 @@ function ChatSidebarSessionDisplayView({
116
120
  onStartEditing
117
121
  }: ChatSidebarSessionDisplayViewProps) {
118
122
  const trailingControlsClassName = childSessionCount > 0 && onOpenChildSessions ? 'pr-14' : 'pr-6';
123
+ const previewText = sessionActivityPreviewText(session);
124
+ const fallbackPreviewText = `${agentLabel?.trim() ? `${agentLabel} · ` : ''}${session.messageCount}`;
119
125
 
120
126
  return (
121
127
  <div className="group/session relative">
122
128
  <button type="button" onClick={onSelect} className="w-full text-left">
123
- <div className={cn('grid grid-cols-[minmax(0,1fr)_auto] items-start gap-2', trailingControlsClassName)}>
129
+ <div className={cn('flex min-w-0 items-start', trailingControlsClassName)}>
124
130
  <span className="flex min-w-0 items-center gap-1.5">
125
131
  {agentId?.trim() && agentId.trim().toLowerCase() !== 'main' ? (
126
132
  <AgentAvatar
@@ -149,15 +155,10 @@ function ChatSidebarSessionDisplayView({
149
155
  </span>
150
156
  ) : null}
151
157
  </span>
152
- {runStatus ? (
153
- <span className="inline-flex shrink-0 items-center justify-end gap-1.5 pt-0.5">
154
- <SessionRunBadge status={runStatus} />
155
- </span>
156
- ) : null}
157
158
  </div>
158
159
  <div className="mt-1 flex items-center gap-2 text-[11px] text-gray-400">
159
160
  <span className="min-w-0 truncate">
160
- {agentLabel?.trim() ? `${agentLabel} · ` : ''}{session.messageCount}
161
+ {previewText ?? fallbackPreviewText}
161
162
  </span>
162
163
  {showUnreadDot ? (
163
164
  <span
@@ -165,7 +166,7 @@ function ChatSidebarSessionDisplayView({
165
166
  className="ml-auto h-2 w-2 shrink-0 rounded-full bg-primary"
166
167
  />
167
168
  ) : (
168
- <span className="ml-auto shrink-0">{formatDateShort(session.lastMessageAt ?? session.createdAt)}</span>
169
+ <span className="ml-auto shrink-0">{formatSessionListTime(session.lastMessageAt ?? session.createdAt)}</span>
169
170
  )}
170
171
  </div>
171
172
  </button>
@@ -189,6 +190,11 @@ function ChatSidebarSessionDisplayView({
189
190
  <span>{childSessionCount}</span>
190
191
  </button>
191
192
  ) : null}
193
+ {runStatus ? (
194
+ <span className="absolute right-0 top-0 inline-flex h-5 w-5 items-center justify-center transition-opacity group-hover/session:opacity-0 group-focus-within/session:opacity-0">
195
+ <SessionRunBadge status={runStatus} />
196
+ </span>
197
+ ) : null}
192
198
  <button
193
199
  type="button"
194
200
  onClick={(event) => {
@@ -197,9 +203,7 @@ function ChatSidebarSessionDisplayView({
197
203
  }}
198
204
  className={cn(
199
205
  'absolute right-0 top-0 inline-flex h-5 w-5 items-center justify-center rounded-md text-gray-400 transition-all hover:bg-white hover:text-gray-900',
200
- active
201
- ? 'opacity-100'
202
- : 'opacity-0 group-hover/session:opacity-100 group-focus-within/session:opacity-100'
206
+ 'opacity-0 group-hover/session:opacity-100 group-focus-within/session:opacity-100'
203
207
  )}
204
208
  aria-label={t('edit')}
205
209
  >
@@ -0,0 +1,74 @@
1
+ import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
2
+ import { render, screen } from "@testing-library/react";
3
+ import { describe, expect, it, vi } from "vitest";
4
+ import { ChatConversationHeader } from "@/features/chat/components/conversation/chat-conversation-header";
5
+ import { useChatThreadStore } from "@/features/chat/stores/chat-thread.store";
6
+
7
+ vi.mock("@/shared/components/common/agent-avatar", () => ({
8
+ AgentAvatar: ({ agentId }: { agentId: string }) => (
9
+ <div data-testid="agent-avatar">{agentId}</div>
10
+ ),
11
+ }));
12
+
13
+ function renderHeader(
14
+ snapshotPatch: Partial<ReturnType<typeof useChatThreadStore.getState>["snapshot"]>,
15
+ ) {
16
+ const queryClient = new QueryClient();
17
+ const snapshot = {
18
+ ...useChatThreadStore.getState().snapshot,
19
+ isProviderStateResolved: true,
20
+ modelOptions: [],
21
+ sessionTypeLabel: "Codex",
22
+ sessionKey: null,
23
+ agentId: "main",
24
+ canDeleteSession: false,
25
+ messages: [],
26
+ ...snapshotPatch,
27
+ };
28
+
29
+ return render(
30
+ <QueryClientProvider client={queryClient}>
31
+ <ChatConversationHeader
32
+ snapshot={snapshot}
33
+ childSessionCount={0}
34
+ sessionCronJobCount={0}
35
+ layoutMode="desktop"
36
+ normalizedAgentId={snapshot.agentId ?? ""}
37
+ sessionHeaderTitle={snapshot.sessionDisplayName ?? "New Task"}
38
+ shouldShowHeaderAgentAvatar={false}
39
+ shouldShowSessionHeader={Boolean(
40
+ snapshot.sessionKey || snapshot.sessionTypeLabel,
41
+ )}
42
+ onOpenChildSessions={vi.fn()}
43
+ onOpenSessionCronJobs={vi.fn()}
44
+ onDeleteSession={vi.fn()}
45
+ />
46
+ </QueryClientProvider>,
47
+ );
48
+ }
49
+
50
+ describe("ChatConversationHeader", () => {
51
+ it("uses a stable desktop height before and after session materialization", () => {
52
+ renderHeader({});
53
+
54
+ const header = screen.getByText("New Task").closest(".border-b");
55
+
56
+ expect(header?.className).toContain("h-[52px]");
57
+ expect(header?.className).not.toContain("transition-all");
58
+ });
59
+
60
+ it("uses the standard session-header action button density after the session is materialized", () => {
61
+ renderHeader({
62
+ sessionKey: "session-1",
63
+ canDeleteSession: true,
64
+ sessionDisplayName: "First message",
65
+ });
66
+
67
+ const moreActions = screen.getByRole("button", { name: "More actions" });
68
+ const header = screen.getByText("First message").closest(".border-b");
69
+
70
+ expect(header?.className).toContain("h-[52px]");
71
+ expect(moreActions.className).toContain("h-7");
72
+ expect(moreActions.className).toContain("w-7");
73
+ });
74
+ });
@@ -44,6 +44,7 @@ export function ChatParentSessionBanner({
44
44
  export function ChatConversationHeader({
45
45
  snapshot,
46
46
  childSessionCount,
47
+ sessionCronJobCount,
47
48
  layoutMode,
48
49
  normalizedAgentId,
49
50
  sessionHeaderTitle,
@@ -51,10 +52,12 @@ export function ChatConversationHeader({
51
52
  shouldShowSessionHeader,
52
53
  onBackToList,
53
54
  onOpenChildSessions,
55
+ onOpenSessionCronJobs,
54
56
  onDeleteSession,
55
57
  }: {
56
58
  snapshot: ChatThreadSnapshot;
57
59
  childSessionCount: number;
60
+ sessionCronJobCount: number;
58
61
  layoutMode: "desktop" | "mobile";
59
62
  normalizedAgentId: string;
60
63
  sessionHeaderTitle: string;
@@ -62,6 +65,7 @@ export function ChatConversationHeader({
62
65
  shouldShowSessionHeader: boolean;
63
66
  onBackToList?: () => void;
64
67
  onOpenChildSessions: () => void;
68
+ onOpenSessionCronJobs: () => void;
65
69
  onDeleteSession: ChatHeaderDeleteHandler;
66
70
  }) {
67
71
  const isMobileLayout = layoutMode === "mobile";
@@ -69,10 +73,10 @@ export function ChatConversationHeader({
69
73
  return (
70
74
  <div
71
75
  className={cn(
72
- "border-b border-gray-200/60 bg-white/80 backdrop-blur-sm flex items-center justify-between shrink-0 overflow-hidden transition-all duration-200",
76
+ "border-b border-gray-200/60 bg-white/80 backdrop-blur-sm flex items-center justify-between shrink-0 overflow-hidden transition-colors duration-200",
73
77
  isMobileLayout ? "px-3 sm:px-3" : "px-4 sm:px-5",
74
78
  shouldShowSessionHeader ? "opacity-100" : "h-0 py-0 opacity-0 border-b-0",
75
- shouldShowSessionHeader && (isMobileLayout ? "pb-2 pt-2" : "py-3"),
79
+ shouldShowSessionHeader && (isMobileLayout ? "min-h-12 pb-2 pt-2" : "h-[52px]"),
76
80
  )}
77
81
  style={
78
82
  isMobileLayout && shouldShowSessionHeader
@@ -137,7 +141,9 @@ export function ChatConversationHeader({
137
141
  isDeletePending={snapshot.isDeletePending}
138
142
  projectRoot={snapshot.sessionProjectRoot}
139
143
  childSessionCount={childSessionCount}
144
+ sessionCronJobCount={sessionCronJobCount}
140
145
  onOpenChildSessions={onOpenChildSessions}
146
+ onOpenSessionCronJobs={onOpenSessionCronJobs}
141
147
  onDeleteSession={onDeleteSession}
142
148
  />
143
149
  ) : null}