@nextclaw/ui 0.12.24 → 0.12.25

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 (206) hide show
  1. package/CHANGELOG.md +68 -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-FJDuPwU6.js +8 -0
  6. package/dist/assets/chat-page-D1fMNBrT.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-kk7qvZ-v.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-D-AAMKCt.js +103 -0
  23. package/dist/assets/index-DnBeV2Xm.css +1 -0
  24. package/dist/assets/{key-round-CJ5gDAAG.js → key-round-DUq47t0P.js} +1 -1
  25. package/dist/assets/marketplace-page-BrCLRIc4.js +105 -0
  26. package/dist/assets/marketplace-page-odDpPYEs.js +1 -0
  27. package/dist/assets/mcp-marketplace-page-CfbOBgKK.js +1 -0
  28. package/dist/assets/mcp-marketplace-page-DIq_SpMe.js +40 -0
  29. package/dist/assets/model-config-Bc6VVnxy.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-DN0tvISH.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-CRWOwBbl.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-C4c1yZSP.js +1 -0
  43. package/dist/assets/secrets-config-zAF30YfO.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-Cvz8ZteY.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 +34 -2
  71. package/src/features/chat/components/chat-sidebar-session-item.tsx +9 -3
  72. package/src/features/chat/components/conversation/chat-conversation-header.test.tsx +71 -0
  73. package/src/features/chat/components/conversation/chat-conversation-header.tsx +6 -0
  74. package/src/features/chat/components/conversation/chat-conversation-panel.test.tsx +181 -61
  75. package/src/features/chat/components/conversation/chat-conversation-panel.tsx +54 -23
  76. package/src/features/chat/components/conversation/session-header/chat-session-header-actions.test.tsx +24 -0
  77. package/src/features/chat/components/conversation/session-header/chat-session-header-actions.tsx +26 -5
  78. package/src/features/chat/components/layout/chat-sidebar-utility-menu.tsx +174 -0
  79. package/src/features/chat/components/layout/chat-sidebar.test.tsx +45 -8
  80. package/src/features/chat/components/layout/chat-sidebar.tsx +29 -46
  81. package/src/features/chat/components/providers/chat-presenter.provider.tsx +2 -0
  82. package/src/features/chat/components/workspace/session-cron-job-content.tsx +103 -0
  83. package/src/features/chat/hooks/use-ncp-agent-runtime.test.tsx +14 -0
  84. package/src/features/chat/hooks/use-ncp-chat-page-data.test.tsx +70 -0
  85. package/src/features/chat/hooks/use-ncp-chat-page-data.ts +1 -1
  86. package/src/features/chat/hooks/use-ncp-child-session-tabs-view.ts +2 -8
  87. package/src/features/chat/hooks/use-ncp-session-list-view.ts +1 -2
  88. package/src/features/chat/managers/chat-session-list.manager.test.ts +7 -9
  89. package/src/features/chat/managers/chat-session-list.manager.ts +5 -10
  90. package/src/features/chat/managers/ncp-chat-input.manager.test.ts +0 -2
  91. package/src/features/chat/managers/ncp-chat-presenter.manager.ts +6 -0
  92. package/src/features/chat/managers/ncp-chat-thread.manager.test.ts +52 -1
  93. package/src/features/chat/managers/ncp-chat-thread.manager.ts +21 -0
  94. package/src/features/chat/pages/ncp-chat-page.tsx +5 -4
  95. package/src/features/chat/stores/chat-session-list.store.ts +0 -2
  96. package/src/features/chat/stores/chat-thread.store.ts +4 -0
  97. package/src/features/chat/utils/chat-session-display.utils.test.ts +83 -1
  98. package/src/features/chat/utils/chat-session-display.utils.ts +73 -0
  99. package/src/features/chat/utils/ncp-session-adapter.utils.test.ts +22 -0
  100. package/src/features/chat/utils/ncp-session-adapter.utils.ts +32 -0
  101. package/src/features/marketplace/components/curated-shelves/marketplace-curated-scene-route.test.tsx +235 -0
  102. package/src/features/marketplace/components/curated-shelves/marketplace-curated-shelves.config.ts +162 -0
  103. package/src/features/marketplace/components/curated-shelves/marketplace-curated-shelves.tsx +355 -0
  104. package/src/features/marketplace/components/curated-shelves/marketplace-shelf-card.tsx +118 -0
  105. package/src/features/marketplace/components/detail-doc/marketplace-detail-doc-renderer.ts +201 -0
  106. package/src/features/marketplace/components/detail-doc/marketplace-detail-doc.test.ts +40 -0
  107. package/src/features/marketplace/components/marketplace-catalog-grid.tsx +114 -0
  108. package/src/features/marketplace/components/marketplace-detail-doc.ts +73 -24
  109. package/src/features/marketplace/components/marketplace-item-icon.tsx +45 -0
  110. package/src/features/marketplace/components/marketplace-list-card.tsx +177 -93
  111. package/src/features/marketplace/components/marketplace-page-detail.test.tsx +9 -2
  112. package/src/features/marketplace/components/marketplace-page-parts.tsx +1 -1
  113. package/src/features/marketplace/components/marketplace-page.test.tsx +25 -6
  114. package/src/features/marketplace/components/marketplace-page.tsx +154 -132
  115. package/src/features/marketplace/hooks/use-marketplace-curated-scene-route.ts +97 -0
  116. package/src/features/marketplace/hooks/use-marketplace.ts +59 -3
  117. package/src/features/system-status/components/config/runtime-agent-list-card.tsx +4 -8
  118. package/src/features/system-status/components/config/runtime-binding-list-card.tsx +5 -7
  119. package/src/features/system-status/components/config/runtime-config-editor.tsx +1 -19
  120. package/src/features/system-status/components/config/runtime-entry-list-card.tsx +10 -11
  121. package/src/features/system-status/components/config/runtime-settings-card.tsx +15 -23
  122. package/src/features/system-status/components/runtime-control-card.test.tsx +8 -6
  123. package/src/features/system-status/components/runtime-control-card.tsx +7 -6
  124. package/src/features/system-status/pages/runtime-config-page.test.tsx +19 -9
  125. package/src/features/system-status/pages/runtime-config-page.tsx +2 -3
  126. package/src/features/system-status/utils/runtime-config-agent.utils.ts +4 -4
  127. package/src/features/system-status/utils/system-status.utils.ts +31 -6
  128. package/src/index.css +8 -0
  129. package/src/platforms/desktop/components/desktop-app-shell.test.tsx +67 -0
  130. package/src/platforms/desktop/components/desktop-app-shell.tsx +46 -18
  131. package/src/platforms/desktop/components/desktop-window-chrome.tsx +30 -0
  132. package/src/platforms/desktop/index.ts +6 -0
  133. package/src/platforms/desktop/types/desktop-update.types.ts +3 -0
  134. package/src/platforms/desktop/utils/desktop-host.utils.ts +56 -0
  135. package/src/shared/components/common/brand-header.tsx +36 -16
  136. package/src/shared/components/config/provider-form-support.ts +2 -22
  137. package/src/shared/components/cron-config.tsx +12 -58
  138. package/src/shared/components/doc-browser/doc-browser.tsx +4 -4
  139. package/src/shared/components/ui/select.tsx +19 -7
  140. package/src/shared/lib/api/channel-auth.types.ts +1 -0
  141. package/src/shared/lib/api/ncp-session.types.ts +9 -0
  142. package/src/shared/lib/api/types.ts +12 -1
  143. package/src/shared/lib/api/utils/marketplace.utils.ts +7 -1
  144. package/src/shared/lib/cron/cron-job-view.utils.ts +59 -0
  145. package/src/shared/lib/cron/index.ts +1 -0
  146. package/src/shared/lib/i18n/{channel-auth.ts → channel-auth.constants.ts} +31 -0
  147. package/src/shared/lib/i18n/chat-labels.utils.ts +3 -2
  148. package/src/shared/lib/i18n/index.ts +20 -59
  149. package/src/shared/lib/i18n/{runtime-control.ts → runtime-control-labels.utils.ts} +30 -1
  150. package/src/shared/lib/provider-models/index.test.ts +39 -0
  151. package/src/shared/lib/provider-models/index.ts +1 -3
  152. package/src/shared/lib/ui-document-title/index.ts +0 -1
  153. package/tsconfig.json +1 -0
  154. package/vite.config.ts +1 -1
  155. package/vitest.config.ts +1 -1
  156. package/dist/assets/api-D2xRKmZd.js +0 -15
  157. package/dist/assets/app-manager-provider-CNaZboG4.js +0 -1
  158. package/dist/assets/app-navigation.config-Ihhrrt--.js +0 -1
  159. package/dist/assets/channels-list-page-p26lgxLk.js +0 -8
  160. package/dist/assets/chat-Dkh2qtuz.js +0 -61
  161. package/dist/assets/chat-page-DoTmE2wx.js +0 -1
  162. package/dist/assets/chunk-JZWAC4HX-Kydj4yEz.js +0 -3
  163. package/dist/assets/config-split-page-DIOCjj2Q.js +0 -1
  164. package/dist/assets/desktop-update-config-DlpzDfKM.js +0 -1
  165. package/dist/assets/doc-browser-C8FM5fC0.js +0 -1
  166. package/dist/assets/doc-browser-RJUOL_GO.js +0 -1
  167. package/dist/assets/doc-browser-p82AdNO-.js +0 -1
  168. package/dist/assets/folder-CeJKPx5P.js +0 -1
  169. package/dist/assets/hash-BqxRTZW5.js +0 -1
  170. package/dist/assets/i18n-DnTGDIRw.js +0 -1
  171. package/dist/assets/index-D8MKmXtO.css +0 -1
  172. package/dist/assets/index-pBvbJ5Mt.js +0 -2
  173. package/dist/assets/loader-circle-fd-vQKtW.js +0 -1
  174. package/dist/assets/logo-badge-KAe-7d8c.js +0 -1
  175. package/dist/assets/logos-C4sYP1Vl.js +0 -1
  176. package/dist/assets/marketplace-page-Cql0kDi-.js +0 -1
  177. package/dist/assets/marketplace-page-m4P5g_Ht.js +0 -49
  178. package/dist/assets/mcp-marketplace-page-9WVKl1m1.js +0 -1
  179. package/dist/assets/mcp-marketplace-page-ByzBQZcx.js +0 -40
  180. package/dist/assets/message-square-z_osm9c0.js +0 -1
  181. package/dist/assets/model-config-Dbr_0APb.js +0 -1
  182. package/dist/assets/play-Dv6Nr1Ew.js +0 -1
  183. package/dist/assets/plus-D8eKFY7h.js +0 -1
  184. package/dist/assets/provider-scoped-model-input-DFm6N2f7.js +0 -1
  185. package/dist/assets/providers-list-BJcLOjun.js +0 -1
  186. package/dist/assets/refresh-ccw-ByVwmnN_.js +0 -1
  187. package/dist/assets/refresh-cw-PcqoYB3K.js +0 -1
  188. package/dist/assets/remote-BOxo9iwd.js +0 -1
  189. package/dist/assets/runtime-config-page-CjLhnbSl.js +0 -1
  190. package/dist/assets/search-config-J4Htco-P.js +0 -1
  191. package/dist/assets/secrets-config-CUdERjco.js +0 -3
  192. package/dist/assets/sessions-config-page-DpK991fs.js +0 -2
  193. package/dist/assets/settings-drbWqzA4.js +0 -1
  194. package/dist/assets/skeleton-BK1SOSRA.js +0 -1
  195. package/dist/assets/theme-provider-0hxjiPc_.js +0 -2
  196. package/dist/assets/tooltip-Cj4yA0gH.js +0 -1
  197. package/dist/assets/trash-2-CBsHCfqq.js +0 -1
  198. package/dist/assets/use-config-38Ur-89i.js +0 -1
  199. package/dist/assets/use-confirm-dialog-DPQThaeU.js +0 -1
  200. package/dist/assets/use-infinite-scroll-loader-5Gf1xQi7.js +0 -1
  201. package/dist/assets/use-viewport-layout-D1XzKeip.js +0 -1
  202. package/dist/assets/x-CM-XDMpk.js +0 -1
  203. package/src/features/chat/components/config/sessions-config-detail-pane.tsx +0 -244
  204. package/src/features/chat/pages/sessions-config-page.test.tsx +0 -152
  205. package/src/features/chat/pages/sessions-config-page.tsx +0 -192
  206. /package/dist/assets/{config-hints-MogHYQ8G.js → config-hints-BNfpOL4J.js} +0 -0
@@ -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,6 +120,8 @@ 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">
@@ -157,7 +163,7 @@ function ChatSidebarSessionDisplayView({
157
163
  </div>
158
164
  <div className="mt-1 flex items-center gap-2 text-[11px] text-gray-400">
159
165
  <span className="min-w-0 truncate">
160
- {agentLabel?.trim() ? `${agentLabel} · ` : ''}{session.messageCount}
166
+ {previewText ?? fallbackPreviewText}
161
167
  </span>
162
168
  {showUnreadDot ? (
163
169
  <span
@@ -165,7 +171,7 @@ function ChatSidebarSessionDisplayView({
165
171
  className="ml-auto h-2 w-2 shrink-0 rounded-full bg-primary"
166
172
  />
167
173
  ) : (
168
- <span className="ml-auto shrink-0">{formatDateShort(session.lastMessageAt ?? session.createdAt)}</span>
174
+ <span className="ml-auto shrink-0">{formatSessionListTime(session.lastMessageAt ?? session.createdAt)}</span>
169
175
  )}
170
176
  </div>
171
177
  </button>
@@ -0,0 +1,71 @@
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("does not reserve extra height for draft sessions", () => {
52
+ renderHeader({});
53
+
54
+ const header = screen.getByText("New Task").closest(".border-b");
55
+
56
+ expect(header?.className).not.toContain("min-h-");
57
+ });
58
+
59
+ it("uses the standard session-header action button density after the session is materialized", () => {
60
+ renderHeader({
61
+ sessionKey: "session-1",
62
+ canDeleteSession: true,
63
+ sessionDisplayName: "First message",
64
+ });
65
+
66
+ const moreActions = screen.getByRole("button", { name: "More actions" });
67
+
68
+ expect(moreActions.className).toContain("h-7");
69
+ expect(moreActions.className).toContain("w-7");
70
+ });
71
+ });
@@ -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";
@@ -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}
@@ -4,6 +4,7 @@ import { beforeEach, describe, expect, it, vi } from "vitest";
4
4
  import { ChatConversationPanel } from "@/features/chat/components/conversation/chat-conversation-panel";
5
5
  import { ChatSessionWorkspacePanel } from "@/features/chat";
6
6
  import type { ResolvedChildSessionTab } from "@/features/chat/hooks/use-ncp-child-session-tabs-view";
7
+ import type { CronJobView } from "@/shared/lib/api";
7
8
  import { useChatInputStore } from "@/features/chat/stores/chat-input.store";
8
9
  import { useChatSessionListStore } from "@/features/chat/stores/chat-session-list.store";
9
10
  import { useChatThreadStore } from "@/features/chat/stores/chat-thread.store";
@@ -12,7 +13,11 @@ const mocks = vi.hoisted(() => ({
12
13
  deleteSession: vi.fn(),
13
14
  goToProviders: vi.fn(),
14
15
  createSession: vi.fn(() => "draft-session-2"),
16
+ goToChatRoot: vi.fn(),
15
17
  goToSession: vi.fn(),
18
+ openSessionCronPanel: vi.fn(),
19
+ deleteCronJob: vi.fn(),
20
+ cronJobs: [] as CronJobView[],
16
21
  setSelectedAgentId: vi.fn(),
17
22
  setPendingSessionType: vi.fn(),
18
23
  stickyBottomScroll: vi.fn(() => ({
@@ -48,7 +53,19 @@ vi.mock("@/features/chat/components/conversation/chat-input-bar.container", () =
48
53
  }));
49
54
 
50
55
  vi.mock("@/features/chat/components/conversation/chat-message-list.container", () => ({
51
- ChatMessageListContainer: () => <div data-testid="chat-message-list" />,
56
+ ChatMessageListContainer: ({
57
+ isSending,
58
+ messages,
59
+ }: {
60
+ isSending: boolean;
61
+ messages: readonly unknown[];
62
+ }) => (
63
+ <div
64
+ data-testid="chat-message-list"
65
+ data-message-count={String(messages.length)}
66
+ data-sending={String(isSending)}
67
+ />
68
+ ),
52
69
  }));
53
70
 
54
71
  vi.mock("@/features/chat/components/chat-session-workspace-file-preview", () => ({
@@ -81,12 +98,14 @@ vi.mock("@/features/chat/components/chat-welcome", () => ({
81
98
  vi.mock("@/features/chat/components/providers/chat-presenter.provider", () => ({
82
99
  usePresenter: () => ({
83
100
  chatUiManager: {
101
+ goToChatRoot: mocks.goToChatRoot,
84
102
  goToSession: mocks.goToSession,
85
103
  },
86
104
  chatThreadManager: {
87
105
  deleteSession: mocks.deleteSession,
88
106
  goToProviders: mocks.goToProviders,
89
107
  openChildSessionPanel: vi.fn(),
108
+ openSessionCronPanel: mocks.openSessionCronPanel,
90
109
  openFilePreview: vi.fn(),
91
110
  openSessionFromToolAction: vi.fn(),
92
111
  selectChildSessionDetail: vi.fn(),
@@ -116,6 +135,14 @@ vi.mock("@/features/chat/components/providers/chat-presenter.provider", () => ({
116
135
  }),
117
136
  }));
118
137
 
138
+ vi.mock("@/shared/hooks/use-config", () => ({
139
+ useCronJobs: () => ({ data: { jobs: mocks.cronJobs, total: mocks.cronJobs.length } }),
140
+ useDeleteCronJob: () => ({
141
+ mutate: mocks.deleteCronJob,
142
+ isPending: false,
143
+ }),
144
+ }));
145
+
119
146
  vi.mock("@/features/chat/components/conversation/session-header/chat-session-header-actions", () => ({
120
147
  ChatSessionHeaderActions: () => <button aria-label="More actions" />,
121
148
  }));
@@ -157,66 +184,73 @@ vi.mock("@/shared/components/common/agent-identity", () => ({
157
184
  ),
158
185
  }));
159
186
 
160
- describe("ChatConversationPanel", () => {
161
- beforeEach(() => {
162
- mocks.deleteSession.mockReset();
163
- mocks.goToProviders.mockReset();
164
- mocks.createSession.mockReset();
165
- mocks.createSession.mockReturnValue("draft-session-2");
166
- mocks.goToSession.mockReset();
167
- mocks.setSelectedAgentId.mockReset();
168
- mocks.setPendingSessionType.mockReset();
169
- mocks.stickyBottomScroll.mockClear();
170
- useChatInputStore.setState({
171
- snapshot: {
172
- ...useChatInputStore.getState().snapshot,
173
- defaultSessionType: "native",
174
- },
175
- });
176
- useChatThreadStore.setState({
177
- snapshot: {
178
- ...useChatThreadStore.getState().snapshot,
179
- isProviderStateResolved: true,
180
- modelOptions: [
181
- {
182
- value: "openai/gpt-5.1",
183
- modelLabel: "gpt-5.1",
184
- providerLabel: "OpenAI",
185
- } as never,
186
- ],
187
- sessionTypeLabel: "Codex",
188
- sessionKey: "draft-session-1",
189
- sessionDisplayName: undefined,
190
- agentId: null,
191
- agentDisplayName: null,
192
- sessionProjectRoot: null,
193
- sessionProjectName: null,
194
- canDeleteSession: false,
195
- isDeletePending: false,
196
- isHistoryLoading: false,
197
- messages: [],
198
- isSending: false,
199
- isAwaitingAssistantOutput: false,
200
- parentSessionKey: null,
201
- parentSessionLabel: null,
202
- workspacePanelParentKey: null,
203
- availableAgents: [
204
- { id: "main", displayName: "Main", runtime: "native" },
205
- { id: "engineer", displayName: "Engineer", runtime: "codex" },
206
- ],
207
- childSessionTabs: [],
208
- activeChildSessionKey: null,
209
- workspaceFileTabs: [],
210
- activeWorkspaceFileKey: null,
211
- },
212
- });
213
- useChatSessionListStore.setState({
214
- optimisticReadAtBySessionKey: {},
215
- snapshot: {
216
- ...useChatSessionListStore.getState().snapshot,
217
- },
218
- });
187
+ function resetChatConversationPanelTestState() {
188
+ mocks.deleteSession.mockReset();
189
+ mocks.goToProviders.mockReset();
190
+ mocks.createSession.mockReset();
191
+ mocks.createSession.mockReturnValue("draft-session-2");
192
+ mocks.goToChatRoot.mockReset();
193
+ mocks.goToSession.mockReset();
194
+ mocks.openSessionCronPanel.mockReset();
195
+ mocks.deleteCronJob.mockReset();
196
+ mocks.cronJobs = [];
197
+ mocks.setSelectedAgentId.mockReset();
198
+ mocks.setPendingSessionType.mockReset();
199
+ mocks.stickyBottomScroll.mockClear();
200
+ useChatInputStore.setState({
201
+ snapshot: {
202
+ ...useChatInputStore.getState().snapshot,
203
+ defaultSessionType: "native",
204
+ },
205
+ });
206
+ useChatThreadStore.setState({
207
+ snapshot: {
208
+ ...useChatThreadStore.getState().snapshot,
209
+ isProviderStateResolved: true,
210
+ modelOptions: [
211
+ {
212
+ value: "openai/gpt-5.1",
213
+ modelLabel: "gpt-5.1",
214
+ providerLabel: "OpenAI",
215
+ } as never,
216
+ ],
217
+ sessionTypeLabel: "Codex",
218
+ sessionKey: "draft-session-1",
219
+ sessionDisplayName: undefined,
220
+ agentId: null,
221
+ agentDisplayName: null,
222
+ sessionProjectRoot: null,
223
+ sessionProjectName: null,
224
+ canDeleteSession: false,
225
+ isDeletePending: false,
226
+ isHistoryLoading: false,
227
+ messages: [],
228
+ isSending: false,
229
+ isAwaitingAssistantOutput: false,
230
+ hasSubmittedDraftMessage: false,
231
+ parentSessionKey: null,
232
+ parentSessionLabel: null,
233
+ workspacePanelParentKey: null,
234
+ availableAgents: [
235
+ { id: "main", displayName: "Main", runtime: "native" },
236
+ { id: "engineer", displayName: "Engineer", runtime: "codex" },
237
+ ],
238
+ childSessionTabs: [],
239
+ activeChildSessionKey: null,
240
+ workspaceFileTabs: [],
241
+ activeWorkspaceFileKey: null,
242
+ },
243
+ });
244
+ useChatSessionListStore.setState({
245
+ optimisticReadAtBySessionKey: {},
246
+ snapshot: {
247
+ ...useChatSessionListStore.getState().snapshot,
248
+ },
219
249
  });
250
+ }
251
+
252
+ describe("ChatConversationPanel", () => {
253
+ beforeEach(resetChatConversationPanelTestState);
220
254
 
221
255
  it("shows the draft session type in the conversation header", () => {
222
256
  render(<ChatConversationPanel />);
@@ -247,7 +281,7 @@ describe("ChatConversationPanel", () => {
247
281
  await user.click(screen.getByRole("button", { name: "create draft session" }));
248
282
 
249
283
  expect(mocks.createSession).toHaveBeenCalledWith("native");
250
- expect(mocks.goToSession).toHaveBeenCalledWith("draft-session-2");
284
+ expect(mocks.goToChatRoot).toHaveBeenCalledTimes(1);
251
285
  });
252
286
 
253
287
  it("shows the selected session project badge and more actions trigger", () => {
@@ -334,6 +368,40 @@ describe("ChatConversationPanel", () => {
334
368
  expect(screen.queryByText("No messages yet. Send one to start.")).toBeNull();
335
369
  });
336
370
 
371
+ it("keeps the message list mounted while waiting for the first assistant token", () => {
372
+ useChatThreadStore.setState({
373
+ snapshot: {
374
+ ...useChatThreadStore.getState().snapshot,
375
+ messages: [],
376
+ isSending: true,
377
+ isAwaitingAssistantOutput: true,
378
+ },
379
+ });
380
+
381
+ render(<ChatConversationPanel />);
382
+
383
+ expect(screen.getByTestId("chat-message-list").dataset).toMatchObject({ messageCount: "0", sending: "true" });
384
+ expect(screen.queryByText("No messages yet. Send one to start.")).toBeNull();
385
+ });
386
+
387
+ it("does not reopen the welcome panel after a root draft send fails", () => {
388
+ useChatThreadStore.setState({
389
+ snapshot: {
390
+ ...useChatThreadStore.getState().snapshot,
391
+ sessionKey: null,
392
+ messages: [],
393
+ isSending: false,
394
+ isAwaitingAssistantOutput: false,
395
+ hasSubmittedDraftMessage: true,
396
+ },
397
+ });
398
+
399
+ render(<ChatConversationPanel />);
400
+
401
+ expect(screen.queryByTestId("chat-welcome")).toBeNull();
402
+ expect(screen.queryByText("No messages yet. Send one to start.")).toBeNull();
403
+ });
404
+
337
405
  it("does not render runtime lifecycle copy in the conversation alert strip", () => {
338
406
  render(<ChatConversationPanel />);
339
407
 
@@ -555,4 +623,56 @@ describe("ChatSessionWorkspacePanel", () => {
555
623
  "workspace-horizontal-scrollbar",
556
624
  );
557
625
  });
626
+
627
+ it("renders session cron jobs in the workspace sidebar and deletes with a neutral confirmation", async () => {
628
+ const user = userEvent.setup();
629
+ const job: CronJobView = {
630
+ id: "job-1",
631
+ name: "Follow up",
632
+ enabled: true,
633
+ schedule: { kind: "every", everyMs: 3600000 },
634
+ payload: {
635
+ kind: "agent_turn",
636
+ message: "Continue this session later",
637
+ sessionId: "parent-session-1",
638
+ },
639
+ state: {
640
+ nextRunAt: "2026-05-15T10:00:00.000Z",
641
+ lastRunAt: null,
642
+ lastStatus: null,
643
+ lastError: null,
644
+ },
645
+ createdAt: "2026-05-15T09:00:00.000Z",
646
+ updatedAt: "2026-05-15T09:00:00.000Z",
647
+ deleteAfterRun: false,
648
+ };
649
+
650
+ render(
651
+ <ChatSessionWorkspacePanel
652
+ childSessionTabs={[]}
653
+ activeChildSessionKey={null}
654
+ workspaceFileTabs={[]}
655
+ activeWorkspaceFileKey={null}
656
+ activePanelKind="cron"
657
+ sessionCronJobs={[job]}
658
+ sessionProjectRoot="/Users/demo/project-alpha"
659
+ onSelectSession={vi.fn()}
660
+ onSelectFile={vi.fn()}
661
+ onCloseFile={vi.fn()}
662
+ onClose={vi.fn()}
663
+ onBackToParent={vi.fn()}
664
+ onFileOpen={vi.fn()}
665
+ />,
666
+ );
667
+
668
+ expect(screen.getAllByText("Session cron jobs").length).toBeGreaterThan(0);
669
+ expect(screen.getByText("Follow up")).toBeTruthy();
670
+ expect(screen.getByText("Continue this session later")).toBeTruthy();
671
+
672
+ await user.click(screen.getByRole("button", { name: "Delete" }));
673
+ expect(screen.getByText("Delete cron job?")).toBeTruthy();
674
+ await user.click(screen.getAllByRole("button", { name: "Delete" }).at(-1)!);
675
+
676
+ expect(mocks.deleteCronJob).toHaveBeenCalledWith({ id: "job-1" });
677
+ });
558
678
  });
@@ -1,4 +1,4 @@
1
- import { type ComponentProps, useRef } from "react";
1
+ import { type ComponentProps, useMemo, useRef } from "react";
2
2
  import { useStickyBottomScroll } from "@nextclaw/agent-chat-ui";
3
3
  import type { ChatFileOpenActionViewModel } from "@nextclaw/agent-chat-ui";
4
4
  import { ChatInputBarContainer } from "@/features/chat/components/conversation/chat-input-bar.container";
@@ -13,6 +13,8 @@ import { usePresenter } from "@/features/chat/components/providers/chat-presente
13
13
  import { resolveAgentRuntimeSessionType } from "@/features/chat/hooks/use-chat-session-type-state";
14
14
  import { useChatInputStore } from "@/features/chat/stores/chat-input.store";
15
15
  import { useChatThreadStore } from "@/features/chat/stores/chat-thread.store";
16
+ import { useCronJobs } from "@/shared/hooks/use-config";
17
+ import { isCronJobForSession } from "@/shared/lib/cron";
16
18
  import { Skeleton } from "@/shared/components/ui/skeleton";
17
19
  import { t } from "@/shared/lib/i18n";
18
20
  import { cn } from "@/shared/lib/utils";
@@ -173,6 +175,11 @@ function ChatConversationContent({
173
175
  onToolAction,
174
176
  onFileOpen,
175
177
  }: ChatConversationContentProps) {
178
+ const isAwaitingAssistantOutput =
179
+ snapshot.isSending && snapshot.isAwaitingAssistantOutput;
180
+ const shouldShowMessages =
181
+ snapshot.messages.length > 0 || isAwaitingAssistantOutput;
182
+
176
183
  return (
177
184
  <div
178
185
  ref={threadRef}
@@ -186,16 +193,11 @@ function ChatConversationContent({
186
193
  selectedAgentId={snapshot.agentId ?? "main"}
187
194
  onSelectAgent={onSelectAgent}
188
195
  />
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
- ) : (
196
+ ) : hideEmptyHint || !shouldShowMessages ? null : (
194
197
  <div className="mx-auto w-full max-w-[min(1120px,100%)] px-4 py-4 sm:px-6 sm:py-5">
195
198
  <ChatMessageListContainer
196
- key={snapshot.sessionKey ?? "draft"}
197
199
  messages={snapshot.messages}
198
- isSending={snapshot.isSending && snapshot.isAwaitingAssistantOutput}
200
+ isSending={isAwaitingAssistantOutput}
199
201
  onToolAction={onToolAction}
200
202
  onFileOpen={onFileOpen}
201
203
  />
@@ -209,11 +211,39 @@ function shouldShowWorkspacePanel(
209
211
  snapshot: ChatThreadSnapshot,
210
212
  childSessionTabs: ChatThreadSnapshot["childSessionTabs"],
211
213
  workspaceFileTabs: ChatThreadSnapshot["workspaceFileTabs"],
214
+ sessionCronJobCount: number,
212
215
  ) {
213
216
  if (snapshot.workspacePanelParentKey !== snapshot.sessionKey) {
214
217
  return false;
215
218
  }
216
- return childSessionTabs.length > 0 || workspaceFileTabs.length > 0;
219
+ return childSessionTabs.length > 0 || workspaceFileTabs.length > 0 || sessionCronJobCount > 0;
220
+ }
221
+
222
+ function useSessionWorkspaceState(snapshot: ChatThreadSnapshot) {
223
+ const childSessionTabs = useMemo(
224
+ () => snapshot.childSessionTabs.filter((tab) => tab.parentSessionKey === snapshot.sessionKey),
225
+ [snapshot.childSessionTabs, snapshot.sessionKey],
226
+ );
227
+ const workspaceFileTabs = useMemo(
228
+ () => snapshot.workspaceFileTabs.filter((tab) => tab.parentSessionKey === snapshot.sessionKey),
229
+ [snapshot.sessionKey, snapshot.workspaceFileTabs],
230
+ );
231
+ const cronQuery = useCronJobs({ all: true });
232
+ const sessionCronJobs = useMemo(
233
+ () => (cronQuery.data?.jobs ?? []).filter((job) => isCronJobForSession(job, snapshot.sessionKey)),
234
+ [cronQuery.data?.jobs, snapshot.sessionKey],
235
+ );
236
+ return {
237
+ childSessionTabs,
238
+ workspaceFileTabs,
239
+ sessionCronJobs,
240
+ showWorkspacePanel: shouldShowWorkspacePanel(
241
+ snapshot,
242
+ childSessionTabs,
243
+ workspaceFileTabs,
244
+ sessionCronJobs.length,
245
+ ),
246
+ };
217
247
  }
218
248
 
219
249
  export function ChatConversationPanel({
@@ -224,23 +254,12 @@ export function ChatConversationPanel({
224
254
  onBackToList?: () => void;
225
255
  }) {
226
256
  const presenter = usePresenter();
227
- const defaultSessionType = useChatInputStore(
228
- (state) => state.snapshot.defaultSessionType,
229
- );
257
+ const defaultSessionType = useChatInputStore((state) => state.snapshot.defaultSessionType);
230
258
  const snapshot = useChatThreadStore((state) => state.snapshot);
231
259
  const fallbackThreadRef = useRef<HTMLDivElement | null>(null);
232
260
  const threadRef = snapshot.threadRef ?? fallbackThreadRef;
233
- const childSessionTabs = snapshot.childSessionTabs.filter(
234
- (tab) => tab.parentSessionKey === snapshot.sessionKey,
235
- );
236
- const workspaceFileTabs = snapshot.workspaceFileTabs.filter(
237
- (tab) => tab.parentSessionKey === snapshot.sessionKey,
238
- );
239
- const showWorkspacePanel = shouldShowWorkspacePanel(
240
- snapshot,
241
- childSessionTabs,
242
- workspaceFileTabs,
243
- );
261
+ const { childSessionTabs, workspaceFileTabs, sessionCronJobs, showWorkspacePanel } =
262
+ useSessionWorkspaceState(snapshot);
244
263
  const shouldShowSessionHeader = Boolean(
245
264
  snapshot.sessionKey || snapshot.sessionTypeLabel,
246
265
  );
@@ -255,6 +274,7 @@ export function ChatConversationPanel({
255
274
 
256
275
  const showWelcome =
257
276
  !snapshot.canDeleteSession &&
277
+ !snapshot.hasSubmittedDraftMessage &&
258
278
  snapshot.messages.length === 0 &&
259
279
  !snapshot.isSending;
260
280
  const hasConfiguredModel = snapshot.modelOptions.length > 0;
@@ -294,6 +314,12 @@ export function ChatConversationPanel({
294
314
  activeChildSessionKey: childSessionTabs[0]?.sessionKey ?? null,
295
315
  });
296
316
  };
317
+ const openSessionCronJobs = () => {
318
+ if (!snapshot.sessionKey || sessionCronJobs.length === 0) {
319
+ return;
320
+ }
321
+ presenter.chatThreadManager.openSessionCronPanel(snapshot.sessionKey);
322
+ };
297
323
 
298
324
  const { onScroll: handleScroll } = useStickyBottomScroll({
299
325
  scrollRef: threadRef,
@@ -319,6 +345,7 @@ export function ChatConversationPanel({
319
345
  <ChatConversationHeader
320
346
  snapshot={snapshot}
321
347
  childSessionCount={childSessionTabs.length}
348
+ sessionCronJobCount={sessionCronJobs.length}
322
349
  layoutMode={layoutMode}
323
350
  normalizedAgentId={normalizedAgentId}
324
351
  sessionHeaderTitle={sessionHeaderTitle}
@@ -326,6 +353,7 @@ export function ChatConversationPanel({
326
353
  shouldShowSessionHeader={shouldShowSessionHeader}
327
354
  onBackToList={onBackToList}
328
355
  onOpenChildSessions={openChildSessions}
356
+ onOpenSessionCronJobs={openSessionCronJobs}
329
357
  onDeleteSession={presenter.chatThreadManager.deleteSession}
330
358
  />
331
359
  <ChatConversationAlerts
@@ -356,11 +384,14 @@ export function ChatConversationPanel({
356
384
  activeChildSessionKey={snapshot.activeChildSessionKey ?? null}
357
385
  workspaceFileTabs={workspaceFileTabs}
358
386
  activeWorkspaceFileKey={snapshot.activeWorkspaceFileKey ?? null}
387
+ activePanelKind={snapshot.activeWorkspacePanelKind ?? null}
388
+ sessionCronJobs={sessionCronJobs}
359
389
  sessionProjectRoot={snapshot.sessionProjectRoot ?? null}
360
390
  displayMode={layoutMode === "mobile" ? "overlay" : "docked"}
361
391
  onSelectSession={presenter.chatThreadManager.selectChildSessionDetail}
362
392
  onSelectFile={presenter.chatThreadManager.selectWorkspaceFile}
363
393
  onCloseFile={presenter.chatThreadManager.closeWorkspaceFile}
394
+ onSelectCronJobs={() => snapshot.sessionKey ? presenter.chatThreadManager.openSessionCronPanel(snapshot.sessionKey) : undefined}
364
395
  onClose={presenter.chatThreadManager.closeWorkspacePanel}
365
396
  onBackToParent={presenter.chatThreadManager.goToParentSession}
366
397
  onToolAction={presenter.chatThreadManager.openSessionFromToolAction}
@@ -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
  });