@nextclaw/ui 0.12.23 → 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 (215) hide show
  1. package/CHANGELOG.md +136 -0
  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-dxsKz7jJ.js → dialog-BKo0RItd.js} +1 -1
  13. package/dist/assets/{dist-DsYTOyq7.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-V75WQJ2s.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-D1RNsTn_.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-BMyiifTA.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-DTdzR8j8.js → select-BUTwE_lC.js} +1 -1
  45. package/dist/assets/{setting-row-CvKngoNI.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-BywQeHJj.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 +56 -25
  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 +119 -8
  80. package/src/features/chat/components/layout/chat-sidebar.tsx +57 -75
  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-hydrated-ncp-agent.test.tsx +6 -0
  84. package/src/features/chat/hooks/use-ncp-agent-runtime.test.tsx +172 -69
  85. package/src/features/chat/hooks/use-ncp-chat-derived-state.ts +2 -2
  86. package/src/features/chat/hooks/use-ncp-chat-page-data.test.tsx +70 -0
  87. package/src/features/chat/hooks/use-ncp-chat-page-data.ts +7 -7
  88. package/src/features/chat/hooks/use-ncp-child-session-tabs-view.ts +2 -8
  89. package/src/features/chat/hooks/use-ncp-session-conversation.test.tsx +10 -0
  90. package/src/features/chat/hooks/use-ncp-session-conversation.ts +2 -1
  91. package/src/features/chat/hooks/use-ncp-session-list-view.ts +1 -2
  92. package/src/features/chat/hooks/use-selected-session-context-window-indicator.ts +2 -4
  93. package/src/features/chat/managers/chat-session-list.manager.test.ts +21 -20
  94. package/src/features/chat/managers/chat-session-list.manager.ts +15 -24
  95. package/src/features/chat/managers/ncp-chat-input.manager.test.ts +22 -13
  96. package/src/features/chat/managers/ncp-chat-input.manager.ts +4 -2
  97. package/src/features/chat/managers/ncp-chat-presenter.manager.ts +6 -0
  98. package/src/features/chat/managers/ncp-chat-thread.manager.test.ts +52 -1
  99. package/src/features/chat/managers/ncp-chat-thread.manager.ts +21 -0
  100. package/src/features/chat/pages/ncp-chat-page.tsx +28 -17
  101. package/src/features/chat/stores/chat-session-list.store.ts +0 -3
  102. package/src/features/chat/stores/chat-thread.store.ts +4 -0
  103. package/src/features/chat/types/chat-stream.types.ts +1 -1
  104. package/src/features/chat/utils/chat-session-display.utils.test.ts +83 -1
  105. package/src/features/chat/utils/chat-session-display.utils.ts +73 -0
  106. package/src/features/chat/utils/ncp-session-adapter.utils.test.ts +22 -0
  107. package/src/features/chat/utils/ncp-session-adapter.utils.ts +33 -1
  108. package/src/features/marketplace/components/curated-shelves/marketplace-curated-scene-route.test.tsx +235 -0
  109. package/src/features/marketplace/components/curated-shelves/marketplace-curated-shelves.config.ts +162 -0
  110. package/src/features/marketplace/components/curated-shelves/marketplace-curated-shelves.tsx +355 -0
  111. package/src/features/marketplace/components/curated-shelves/marketplace-shelf-card.tsx +118 -0
  112. package/src/features/marketplace/components/detail-doc/marketplace-detail-doc-renderer.ts +201 -0
  113. package/src/features/marketplace/components/detail-doc/marketplace-detail-doc.test.ts +40 -0
  114. package/src/features/marketplace/components/marketplace-catalog-grid.tsx +114 -0
  115. package/src/features/marketplace/components/marketplace-detail-doc.ts +73 -24
  116. package/src/features/marketplace/components/marketplace-item-icon.tsx +45 -0
  117. package/src/features/marketplace/components/marketplace-list-card.tsx +177 -93
  118. package/src/features/marketplace/components/marketplace-page-detail.test.tsx +9 -2
  119. package/src/features/marketplace/components/marketplace-page-parts.tsx +1 -1
  120. package/src/features/marketplace/components/marketplace-page.test.tsx +25 -6
  121. package/src/features/marketplace/components/marketplace-page.tsx +154 -132
  122. package/src/features/marketplace/hooks/use-marketplace-curated-scene-route.ts +97 -0
  123. package/src/features/marketplace/hooks/use-marketplace.ts +59 -3
  124. package/src/features/system-status/components/config/runtime-agent-list-card.tsx +4 -8
  125. package/src/features/system-status/components/config/runtime-binding-list-card.tsx +5 -7
  126. package/src/features/system-status/components/config/runtime-config-editor.tsx +1 -19
  127. package/src/features/system-status/components/config/runtime-entry-list-card.tsx +10 -11
  128. package/src/features/system-status/components/config/runtime-settings-card.tsx +15 -23
  129. package/src/features/system-status/components/runtime-control-card.test.tsx +8 -6
  130. package/src/features/system-status/components/runtime-control-card.tsx +7 -6
  131. package/src/features/system-status/pages/runtime-config-page.test.tsx +19 -9
  132. package/src/features/system-status/pages/runtime-config-page.tsx +2 -3
  133. package/src/features/system-status/utils/runtime-config-agent.utils.ts +4 -4
  134. package/src/features/system-status/utils/system-status.utils.ts +31 -6
  135. package/src/index.css +8 -0
  136. package/src/platforms/desktop/components/desktop-app-shell.test.tsx +67 -0
  137. package/src/platforms/desktop/components/desktop-app-shell.tsx +46 -18
  138. package/src/platforms/desktop/components/desktop-window-chrome.tsx +30 -0
  139. package/src/platforms/desktop/index.ts +6 -0
  140. package/src/platforms/desktop/types/desktop-update.types.ts +3 -0
  141. package/src/platforms/desktop/utils/desktop-host.utils.ts +56 -0
  142. package/src/shared/components/common/brand-header.tsx +36 -16
  143. package/src/shared/components/config/provider-form-support.ts +2 -22
  144. package/src/shared/components/cron-config.tsx +12 -58
  145. package/src/shared/components/doc-browser/doc-browser.tsx +4 -4
  146. package/src/shared/components/ui/select.tsx +19 -7
  147. package/src/shared/lib/api/channel-auth.types.ts +1 -0
  148. package/src/shared/lib/api/ncp-session-query-cache.test.ts +26 -1
  149. package/src/shared/lib/api/ncp-session-query-cache.ts +5 -1
  150. package/src/shared/lib/api/ncp-session.types.ts +9 -0
  151. package/src/shared/lib/api/types.ts +12 -1
  152. package/src/shared/lib/api/utils/marketplace.utils.ts +7 -1
  153. package/src/shared/lib/cron/cron-job-view.utils.ts +59 -0
  154. package/src/shared/lib/cron/index.ts +1 -0
  155. package/src/shared/lib/i18n/{channel-auth.ts → channel-auth.constants.ts} +31 -0
  156. package/src/shared/lib/i18n/chat-labels.utils.ts +3 -2
  157. package/src/shared/lib/i18n/index.ts +20 -59
  158. package/src/shared/lib/i18n/{runtime-control.ts → runtime-control-labels.utils.ts} +30 -1
  159. package/src/shared/lib/provider-models/index.test.ts +39 -0
  160. package/src/shared/lib/provider-models/index.ts +1 -3
  161. package/src/shared/lib/ui-document-title/index.ts +0 -1
  162. package/tsconfig.json +1 -0
  163. package/vite.config.ts +1 -1
  164. package/vitest.config.ts +1 -1
  165. package/dist/assets/api-BGd3rgv_.js +0 -15
  166. package/dist/assets/app-manager-provider-BuJ_U9eC.js +0 -1
  167. package/dist/assets/app-navigation.config-BTdUuqXS.js +0 -1
  168. package/dist/assets/channels-list-page-BrwymXPe.js +0 -8
  169. package/dist/assets/chat-DGM6K3Qs.js +0 -61
  170. package/dist/assets/chat-page-DpmXMWNS.js +0 -1
  171. package/dist/assets/chunk-JZWAC4HX-Kydj4yEz.js +0 -3
  172. package/dist/assets/config-split-page-DIOCjj2Q.js +0 -1
  173. package/dist/assets/desktop-update-config-BGKiqc6q.js +0 -1
  174. package/dist/assets/doc-browser-C8FM5fC0.js +0 -1
  175. package/dist/assets/doc-browser-RJUOL_GO.js +0 -1
  176. package/dist/assets/doc-browser-p82AdNO-.js +0 -1
  177. package/dist/assets/folder-CeJKPx5P.js +0 -1
  178. package/dist/assets/hash-BqxRTZW5.js +0 -1
  179. package/dist/assets/i18n-DnTGDIRw.js +0 -1
  180. package/dist/assets/index-BrEdR78s.js +0 -2
  181. package/dist/assets/index-D8MKmXtO.css +0 -1
  182. package/dist/assets/loader-circle-fd-vQKtW.js +0 -1
  183. package/dist/assets/logo-badge-KAe-7d8c.js +0 -1
  184. package/dist/assets/logos-C4sYP1Vl.js +0 -1
  185. package/dist/assets/marketplace-page-B2Pm2RDJ.js +0 -1
  186. package/dist/assets/marketplace-page-CPHxlYL8.js +0 -49
  187. package/dist/assets/mcp-marketplace-page-BcjVmw36.js +0 -1
  188. package/dist/assets/mcp-marketplace-page-CswPXSjf.js +0 -40
  189. package/dist/assets/message-square-z_osm9c0.js +0 -1
  190. package/dist/assets/model-config-Cmruiqdx.js +0 -1
  191. package/dist/assets/play-Dv6Nr1Ew.js +0 -1
  192. package/dist/assets/plus-D8eKFY7h.js +0 -1
  193. package/dist/assets/provider-scoped-model-input-D7ACiMAO.js +0 -1
  194. package/dist/assets/providers-list-gg7LrfuB.js +0 -1
  195. package/dist/assets/refresh-ccw-ByVwmnN_.js +0 -1
  196. package/dist/assets/refresh-cw-PcqoYB3K.js +0 -1
  197. package/dist/assets/remote-Db2M39Cv.js +0 -1
  198. package/dist/assets/runtime-config-page-BT_VV41p.js +0 -1
  199. package/dist/assets/search-config-0VTPpz-w.js +0 -1
  200. package/dist/assets/secrets-config-DwQbLLEy.js +0 -3
  201. package/dist/assets/sessions-config-page-CAG7Zevv.js +0 -2
  202. package/dist/assets/settings-drbWqzA4.js +0 -1
  203. package/dist/assets/skeleton-BK1SOSRA.js +0 -1
  204. package/dist/assets/theme-provider-COAwWFv8.js +0 -2
  205. package/dist/assets/tooltip-BOYp8Ue7.js +0 -1
  206. package/dist/assets/trash-2-CBsHCfqq.js +0 -1
  207. package/dist/assets/use-config-DTwhNDQE.js +0 -1
  208. package/dist/assets/use-confirm-dialog-oeSqhmrx.js +0 -1
  209. package/dist/assets/use-infinite-scroll-loader-X3KGuME8.js +0 -1
  210. package/dist/assets/use-viewport-layout-C0NJAVXs.js +0 -1
  211. package/dist/assets/x-CM-XDMpk.js +0 -1
  212. package/src/features/chat/components/config/sessions-config-detail-pane.tsx +0 -244
  213. package/src/features/chat/pages/sessions-config-page.test.tsx +0 -152
  214. package/src/features/chat/pages/sessions-config-page.tsx +0 -192
  215. /package/dist/assets/{config-hints-MogHYQ8G.js → config-hints-BNfpOL4J.js} +0 -0
@@ -9,13 +9,64 @@ import { formatDateTime, t } from '@/shared/lib/i18n';
9
9
  import { cn } from '@/shared/lib/utils';
10
10
  import type { ChannelAuthPollResult, ChannelAuthStartResult } from '@/shared/lib/api';
11
11
 
12
- type WeixinChannelAuthSectionProps = {
12
+ type QrChannelAuthSectionProps = {
13
13
  channelConfig: Record<string, unknown>;
14
14
  formData: Record<string, unknown>;
15
+ channelName: 'weixin' | 'feishu';
15
16
  channelEnabled: boolean;
16
17
  disabled?: boolean;
17
18
  };
18
19
 
20
+ type WeixinChannelAuthSectionProps = Omit<QrChannelAuthSectionProps, 'channelName'>;
21
+
22
+ type QrChannelAuthCopy = {
23
+ title: string;
24
+ description: string;
25
+ hint: string;
26
+ capabilityHint: string;
27
+ disabledHint: string;
28
+ connect: string;
29
+ qrAlt: string;
30
+ scanPrompt: string;
31
+ readyTitle: string;
32
+ readyDescription: string;
33
+ advancedTitle: string;
34
+ advancedDescription: string;
35
+ domainLabel?: string;
36
+ };
37
+
38
+ const QR_AUTH_COPY: Record<QrChannelAuthSectionProps['channelName'], QrChannelAuthCopy> = {
39
+ weixin: {
40
+ title: 'weixinAuthTitle',
41
+ description: 'weixinAuthDescription',
42
+ hint: 'weixinAuthHint',
43
+ capabilityHint: 'weixinAuthCapabilityHint',
44
+ disabledHint: 'weixinAuthDisabledHint',
45
+ connect: 'weixinAuthConnect',
46
+ qrAlt: 'weixinAuthQrAlt',
47
+ scanPrompt: 'weixinAuthScanPrompt',
48
+ readyTitle: 'weixinAuthReadyTitle',
49
+ readyDescription: 'weixinAuthReadyDescription',
50
+ advancedTitle: 'weixinAuthAdvancedTitle',
51
+ advancedDescription: 'weixinAuthAdvancedDescription'
52
+ },
53
+ feishu: {
54
+ title: 'feishuAuthTitle',
55
+ description: 'feishuAuthDescription',
56
+ hint: 'feishuAuthHint',
57
+ capabilityHint: 'feishuAuthCapabilityHint',
58
+ disabledHint: 'feishuAuthDisabledHint',
59
+ connect: 'feishuAuthConnect',
60
+ qrAlt: 'feishuAuthQrAlt',
61
+ scanPrompt: 'feishuAuthScanPrompt',
62
+ readyTitle: 'feishuAuthReadyTitle',
63
+ readyDescription: 'feishuAuthReadyDescription',
64
+ advancedTitle: 'feishuAuthAdvancedTitle',
65
+ advancedDescription: 'feishuAuthAdvancedDescription',
66
+ domainLabel: 'feishuAuthDomain'
67
+ }
68
+ };
69
+
19
70
  function resolveConnectedAccountIds(channelConfig: Record<string, unknown>): string[] {
20
71
  const { accounts } = channelConfig;
21
72
  const ids = new Set<string>();
@@ -43,21 +94,45 @@ function resolveBaseUrl(formData: Record<string, unknown>, channelConfig: Record
43
94
  return undefined;
44
95
  }
45
96
 
46
- function useWeixinQrDataUrl(qrCodeUrl: string | undefined) {
97
+ function resolveDomain(formData: Record<string, unknown>, channelConfig: Record<string, unknown>): string | undefined {
98
+ const formDomain = typeof formData.domain === 'string' ? formData.domain.trim() : '';
99
+ if (formDomain) {
100
+ return formDomain;
101
+ }
102
+ const configDomain = typeof channelConfig.domain === 'string' ? channelConfig.domain.trim() : '';
103
+ return configDomain || undefined;
104
+ }
105
+
106
+ function useQrDataUrl(channelName: string, qrCodeUrl: string | undefined) {
47
107
  return useQuery({
48
- queryKey: ['weixin-channel-qr', qrCodeUrl],
108
+ queryKey: ['channel-qr', channelName, qrCodeUrl],
49
109
  enabled: Boolean(qrCodeUrl),
50
110
  queryFn: () => toDataURL(qrCodeUrl!, { errorCorrectionLevel: 'M', margin: 1, width: 480 })
51
111
  }).data ?? null;
52
112
  }
53
113
 
54
- function WeixinAuthSummary(props: {
114
+ function QrAuthSummary({
115
+ activeSession,
116
+ baseUrl,
117
+ channelEnabled,
118
+ copy,
119
+ connectButtonLabel,
120
+ connectedAccountIds,
121
+ disabled,
122
+ domain,
123
+ handleStartAuth,
124
+ hasConnectedAccount,
125
+ primaryAccountId,
126
+ statusLabel
127
+ }: {
55
128
  activeSession: ChannelAuthStartResult | null;
56
129
  baseUrl?: string;
57
130
  channelEnabled: boolean;
131
+ copy: QrChannelAuthCopy;
58
132
  connectButtonLabel: string;
59
133
  connectedAccountIds: string[];
60
134
  disabled: boolean;
135
+ domain?: string;
61
136
  handleStartAuth: () => Promise<void>;
62
137
  hasConnectedAccount: boolean;
63
138
  primaryAccountId?: string;
@@ -67,46 +142,53 @@ function WeixinAuthSummary(props: {
67
142
  <div className="space-y-3">
68
143
  <div className="inline-flex items-center gap-2 rounded-full bg-white/90 px-3 py-1 text-xs font-medium text-primary shadow-sm">
69
144
  <QrCode className="h-3.5 w-3.5" />
70
- {t('weixinAuthTitle')}
145
+ {t(copy.title)}
71
146
  </div>
72
147
  <div>
73
- <h4 className="text-base font-semibold text-gray-900">{t('weixinAuthDescription')}</h4>
74
- <p className="mt-1 text-sm text-gray-600">{t('weixinAuthHint')}</p>
148
+ <h4 className="text-base font-semibold text-gray-900">{t(copy.description)}</h4>
149
+ <p className="mt-1 text-sm text-gray-600">{t(copy.hint)}</p>
75
150
  </div>
76
151
  <div
77
152
  className={cn(
78
153
  'inline-flex w-fit items-center gap-2 rounded-full px-3 py-1 text-xs font-medium',
79
- props.activeSession ? 'bg-amber-50 text-amber-700' : props.hasConnectedAccount ? 'bg-emerald-50 text-emerald-700' : 'bg-gray-100 text-gray-600'
154
+ activeSession ? 'bg-amber-50 text-amber-700' : hasConnectedAccount ? 'bg-emerald-50 text-emerald-700' : 'bg-gray-100 text-gray-600'
80
155
  )}
81
156
  >
82
- {props.activeSession ? <Loader2 className="h-3.5 w-3.5 animate-spin" /> : <MessageCircleMore className="h-3.5 w-3.5" />}
83
- {props.statusLabel}
157
+ {activeSession ? <Loader2 className="h-3.5 w-3.5 animate-spin" /> : <MessageCircleMore className="h-3.5 w-3.5" />}
158
+ {statusLabel}
84
159
  </div>
85
160
  <div className="space-y-1 text-sm text-gray-600">
86
- <p>{props.channelEnabled || !props.hasConnectedAccount ? t('weixinAuthCapabilityHint') : t('weixinAuthDisabledHint')}</p>
87
- {props.primaryAccountId ? <p>{t('weixinAuthPrimaryAccount')}: <span className="font-mono text-xs text-gray-900">{props.primaryAccountId}</span></p> : null}
88
- {props.connectedAccountIds.length > 1 ? <p>{t('weixinAuthConnectedAccounts')}: <span className="font-mono text-xs text-gray-900">{props.connectedAccountIds.join(', ')}</span></p> : null}
89
- {props.baseUrl ? <p>{t('weixinAuthBaseUrl')}: <span className="font-mono text-xs text-gray-900">{props.baseUrl}</span></p> : null}
161
+ <p>{channelEnabled || !hasConnectedAccount ? t(copy.capabilityHint) : t(copy.disabledHint)}</p>
162
+ {primaryAccountId ? <p>{t('weixinAuthPrimaryAccount')}: <span className="font-mono text-xs text-gray-900">{primaryAccountId}</span></p> : null}
163
+ {connectedAccountIds.length > 1 ? <p>{t('weixinAuthConnectedAccounts')}: <span className="font-mono text-xs text-gray-900">{connectedAccountIds.join(', ')}</span></p> : null}
164
+ {baseUrl ? <p>{t('weixinAuthBaseUrl')}: <span className="font-mono text-xs text-gray-900">{baseUrl}</span></p> : null}
165
+ {domain && copy.domainLabel ? <p>{t(copy.domainLabel)}: <span className="font-mono text-xs text-gray-900">{domain}</span></p> : null}
90
166
  </div>
91
- <Button type="button" onClick={() => void props.handleStartAuth()} disabled={props.disabled} className="rounded-xl">
92
- {props.connectButtonLabel}
167
+ <Button type="button" onClick={() => void handleStartAuth()} disabled={disabled} className="rounded-xl">
168
+ {connectButtonLabel}
93
169
  </Button>
94
170
  </div>
95
171
  );
96
172
  }
97
173
 
98
- function WeixinAuthQrPanel(props: {
174
+ function QrAuthPanel({
175
+ activeSession,
176
+ authMessage,
177
+ copy,
178
+ qrDataUrl
179
+ }: {
99
180
  activeSession: ChannelAuthStartResult | null;
100
181
  authMessage?: string;
182
+ copy: QrChannelAuthCopy;
101
183
  qrDataUrl: string | null;
102
184
  }) {
103
185
  return (
104
186
  <div className="w-full max-w-sm rounded-2xl border border-dashed border-primary/25 bg-white/85 p-4 shadow-sm">
105
- {props.activeSession ? (
187
+ {activeSession ? (
106
188
  <div className="space-y-3">
107
189
  <div className="overflow-hidden rounded-2xl border border-gray-100 bg-white p-3">
108
- {props.qrDataUrl ? (
109
- <img src={props.qrDataUrl} alt={t('weixinAuthQrAlt')} className="mx-auto aspect-square w-full max-w-[240px] object-contain" />
190
+ {qrDataUrl ? (
191
+ <img src={qrDataUrl} alt={t(copy.qrAlt)} className="mx-auto aspect-square w-full max-w-[240px] object-contain" />
110
192
  ) : (
111
193
  <div className="flex aspect-square w-full items-center justify-center rounded-xl bg-gray-50 text-gray-500">
112
194
  <div className="flex flex-col items-center gap-2 text-center">
@@ -117,10 +199,10 @@ function WeixinAuthQrPanel(props: {
117
199
  )}
118
200
  </div>
119
201
  <div className="space-y-1 text-xs text-gray-500">
120
- <p>{props.authMessage || props.activeSession.note || t('weixinAuthScanPrompt')}</p>
121
- <p>{t('weixinAuthExpiresAt')}: {formatDateTime(props.activeSession.expiresAt)}</p>
202
+ <p>{authMessage || activeSession.note || t(copy.scanPrompt)}</p>
203
+ <p>{t('weixinAuthExpiresAt')}: {formatDateTime(activeSession.expiresAt)}</p>
122
204
  </div>
123
- <a href={props.activeSession.qrCodeUrl} target="_blank" rel="noreferrer" className="inline-flex items-center gap-1.5 text-xs text-primary transition-colors hover:text-primary-hover">
205
+ <a href={activeSession.qrCodeUrl} target="_blank" rel="noreferrer" className="inline-flex items-center gap-1.5 text-xs text-primary transition-colors hover:text-primary-hover">
124
206
  <ExternalLink className="h-3.5 w-3.5" />
125
207
  {t('weixinAuthOpenQr')}
126
208
  </a>
@@ -128,27 +210,35 @@ function WeixinAuthQrPanel(props: {
128
210
  ) : (
129
211
  <div className="flex min-h-[280px] flex-col items-center justify-center rounded-2xl bg-gray-50/80 px-6 text-center">
130
212
  <QrCode className="h-9 w-9 text-gray-300" />
131
- <p className="mt-3 text-sm font-medium text-gray-700">{t('weixinAuthReadyTitle')}</p>
132
- <p className="mt-1 text-xs leading-5 text-gray-500">{t('weixinAuthReadyDescription')}</p>
213
+ <p className="mt-3 text-sm font-medium text-gray-700">{t(copy.readyTitle)}</p>
214
+ <p className="mt-1 text-xs leading-5 text-gray-500">{t(copy.readyDescription)}</p>
133
215
  </div>
134
216
  )}
135
217
  </div>
136
218
  );
137
219
  }
138
220
 
139
- export function WeixinChannelAuthSection(props: WeixinChannelAuthSectionProps) {
221
+ export function QrChannelAuthSection({
222
+ channelConfig,
223
+ channelEnabled,
224
+ channelName,
225
+ disabled,
226
+ formData
227
+ }: QrChannelAuthSectionProps) {
140
228
  const queryClient = useQueryClient();
141
229
  const startChannelAuth = useStartChannelAuth();
142
230
  const pollChannelAuth = usePollChannelAuth();
143
231
  const [activeSession, setActiveSession] = useState<ChannelAuthStartResult | null>(null);
144
232
  const [authState, setAuthState] = useState<ChannelAuthPollResult | null>(null);
145
233
  const [sessionStartedWhileConnected, setSessionStartedWhileConnected] = useState(false);
146
- const connectedAccountIds = useMemo(() => resolveConnectedAccountIds(props.channelConfig), [props.channelConfig]);
234
+ const connectedAccountIds = useMemo(() => resolveConnectedAccountIds(channelConfig), [channelConfig]);
147
235
  const primaryAccountId = connectedAccountIds[0];
148
- const baseUrl = resolveBaseUrl(props.formData, props.channelConfig);
236
+ const baseUrl = resolveBaseUrl(formData, channelConfig);
237
+ const domain = channelName === 'feishu' ? resolveDomain(formData, channelConfig) : undefined;
149
238
  const hasConnectedAccount = connectedAccountIds.length > 0;
150
239
  const effectiveActiveSession = hasConnectedAccount && !sessionStartedWhileConnected ? null : activeSession;
151
- const qrDataUrl = useWeixinQrDataUrl(effectiveActiveSession?.qrCodeUrl);
240
+ const qrDataUrl = useQrDataUrl(channelName, effectiveActiveSession?.qrCodeUrl);
241
+ const copy = QR_AUTH_COPY[channelName];
152
242
 
153
243
  useEffect(() => {
154
244
  if (!effectiveActiveSession) {
@@ -158,7 +248,7 @@ export function WeixinChannelAuthSection(props: WeixinChannelAuthSectionProps) {
158
248
  let timer: ReturnType<typeof setTimeout> | null = null;
159
249
  const runPoll = async () => {
160
250
  try {
161
- const result = await pollChannelAuth.mutateAsync({ channel: 'weixin', data: { sessionId: effectiveActiveSession.sessionId } });
251
+ const result = await pollChannelAuth.mutateAsync({ channel: channelName, data: { sessionId: effectiveActiveSession.sessionId } });
162
252
  if (cancelled) {
163
253
  return;
164
254
  }
@@ -190,20 +280,21 @@ export function WeixinChannelAuthSection(props: WeixinChannelAuthSectionProps) {
190
280
  clearTimeout(timer);
191
281
  }
192
282
  };
193
- }, [effectiveActiveSession, pollChannelAuth, queryClient]);
283
+ }, [channelName, effectiveActiveSession, pollChannelAuth, queryClient]);
194
284
 
195
285
  const handleStartAuth = async () => {
196
286
  try {
197
287
  const result = await startChannelAuth.mutateAsync({
198
- channel: 'weixin',
288
+ channel: channelName,
199
289
  data: {
200
290
  baseUrl,
201
- accountId: typeof props.formData.defaultAccountId === 'string' && props.formData.defaultAccountId.trim() ? props.formData.defaultAccountId.trim() : undefined
291
+ domain,
292
+ accountId: typeof formData.defaultAccountId === 'string' && formData.defaultAccountId.trim() ? formData.defaultAccountId.trim() : undefined
202
293
  }
203
294
  });
204
295
  setSessionStartedWhileConnected(hasConnectedAccount);
205
296
  setActiveSession(result);
206
- setAuthState({ channel: 'weixin', status: 'pending', message: result.note, nextPollMs: result.intervalMs });
297
+ setAuthState({ channel: channelName, status: 'pending', message: result.note, nextPollMs: result.intervalMs });
207
298
  } catch (error) {
208
299
  toast.error(`${t('error')}: ${error instanceof Error ? error.message : String(error)}`);
209
300
  }
@@ -212,7 +303,7 @@ export function WeixinChannelAuthSection(props: WeixinChannelAuthSectionProps) {
212
303
  const statusLabel = effectiveActiveSession
213
304
  ? authState?.status === 'scanned' ? t('weixinAuthScanned') : t('weixinAuthWaiting')
214
305
  : hasConnectedAccount
215
- ? props.channelEnabled ? t('weixinAuthAuthorized') : t('weixinAuthConnectedDisabled')
306
+ ? channelEnabled ? t('weixinAuthAuthorized') : t('weixinAuthConnectedDisabled')
216
307
  : t('weixinAuthNotConnected');
217
308
  const connectButtonLabel = startChannelAuth.isPending
218
309
  ? t('weixinAuthStarting')
@@ -220,26 +311,32 @@ export function WeixinChannelAuthSection(props: WeixinChannelAuthSectionProps) {
220
311
  ? t('weixinAuthWaiting')
221
312
  : hasConnectedAccount
222
313
  ? t('weixinAuthReconnect')
223
- : t('weixinAuthConnect');
314
+ : t(copy.connect);
224
315
  const authMessage = hasConnectedAccount ? t('weixinAuthAuthorized') : authState?.message;
225
316
 
226
317
  return (
227
318
  <section className="rounded-2xl border border-primary/20 bg-gradient-to-br from-primary-50/70 via-white to-emerald-50/60 p-5">
228
319
  <div className="flex flex-col gap-4 lg:flex-row lg:items-start lg:justify-between">
229
- <WeixinAuthSummary
320
+ <QrAuthSummary
230
321
  activeSession={effectiveActiveSession}
231
322
  baseUrl={baseUrl}
232
- channelEnabled={props.channelEnabled}
323
+ channelEnabled={channelEnabled}
324
+ copy={copy}
233
325
  connectButtonLabel={connectButtonLabel}
234
326
  connectedAccountIds={connectedAccountIds}
235
- disabled={props.disabled || startChannelAuth.isPending || Boolean(effectiveActiveSession)}
327
+ disabled={disabled || startChannelAuth.isPending || Boolean(effectiveActiveSession)}
328
+ domain={domain}
236
329
  handleStartAuth={handleStartAuth}
237
330
  hasConnectedAccount={hasConnectedAccount}
238
331
  primaryAccountId={primaryAccountId}
239
332
  statusLabel={statusLabel}
240
333
  />
241
- <WeixinAuthQrPanel activeSession={effectiveActiveSession} authMessage={authMessage} qrDataUrl={qrDataUrl} />
334
+ <QrAuthPanel activeSession={effectiveActiveSession} authMessage={authMessage} copy={copy} qrDataUrl={qrDataUrl} />
242
335
  </div>
243
336
  </section>
244
337
  );
245
338
  }
339
+
340
+ export function WeixinChannelAuthSection(props: WeixinChannelAuthSectionProps) {
341
+ return <QrChannelAuthSection {...props} channelName="weixin" />;
342
+ }
@@ -1,5 +1,5 @@
1
1
  export { ChannelForm } from './components/config/channel-form';
2
2
  export { ChannelFormFieldsSection } from './components/channel-form-fields-section';
3
- export { WeixinChannelAuthSection } from './components/config/weixin-channel-auth-section';
3
+ export { QrChannelAuthSection, WeixinChannelAuthSection } from './components/config/weixin-channel-auth-section';
4
4
  export { ChannelsList } from './pages/channels-list-page';
5
5
  export { buildChannelFields, buildChannelFormDefinitions, type ChannelField, type ChannelFieldType, type ChannelFormBlock, type ChannelFormDefinition, type ChannelFormFieldSection, type ChannelOption } from './utils/channel-form-fields.utils';
@@ -25,4 +25,30 @@ describe('buildChannelFormDefinitions', () => {
25
25
  }
26
26
  ]);
27
27
  });
28
+
29
+ it('declares feishu as a QR-first extension channel layout', () => {
30
+ const definitions = buildChannelFormDefinitions();
31
+
32
+ expect(definitions.feishu?.fields.map((field) => field.name)).toEqual([
33
+ 'enabled',
34
+ 'defaultAccountId',
35
+ 'domain',
36
+ 'allowFrom',
37
+ 'groupPolicy',
38
+ 'requireMention',
39
+ 'accounts'
40
+ ]);
41
+ expect(definitions.feishu?.layout).toEqual([
42
+ { type: 'fields', section: 'primary' },
43
+ { type: 'custom', sectionId: 'feishu-auth' },
44
+ {
45
+ type: 'fields',
46
+ section: 'advanced',
47
+ collapsible: {
48
+ title: 'Advanced settings',
49
+ description: 'Expand these fields only when you need to switch Feishu/Lark domains, choose a default account, or adjust allowlist and group policies.'
50
+ }
51
+ }
52
+ ]);
53
+ });
28
54
  });
@@ -41,6 +41,11 @@ const GROUP_POLICY_OPTIONS: ChannelOption[] = [
41
41
  { value: 'disabled', label: 'disabled' }
42
42
  ];
43
43
 
44
+ const FEISHU_DOMAIN_OPTIONS: ChannelOption[] = [
45
+ { value: 'feishu', label: 'feishu' },
46
+ { value: 'lark', label: 'lark' }
47
+ ];
48
+
44
49
  const STREAMING_MODE_OPTIONS: ChannelOption[] = [
45
50
  { value: 'off', label: 'off' },
46
51
  { value: 'partial', label: 'partial' },
@@ -48,6 +53,32 @@ const STREAMING_MODE_OPTIONS: ChannelOption[] = [
48
53
  { value: 'progress', label: 'progress' }
49
54
  ];
50
55
 
56
+ function buildFeishuFormDefinition(): ChannelFormDefinition {
57
+ return {
58
+ fields: [
59
+ { name: 'enabled', type: 'boolean', label: t('enabled'), section: 'primary' },
60
+ { name: 'defaultAccountId', type: 'text', label: t('defaultAccountId') },
61
+ { name: 'domain', type: 'select', label: t('feishuAuthDomain'), options: FEISHU_DOMAIN_OPTIONS },
62
+ { name: 'allowFrom', type: 'tags', label: t('allowFrom') },
63
+ { name: 'groupPolicy', type: 'select', label: t('groupPolicy'), options: GROUP_POLICY_OPTIONS },
64
+ { name: 'requireMention', type: 'boolean', label: t('requireMention') },
65
+ { name: 'accounts', type: 'json', label: t('accountsJson') }
66
+ ],
67
+ layout: [
68
+ { type: 'fields', section: 'primary' },
69
+ { type: 'custom', sectionId: 'feishu-auth' },
70
+ {
71
+ type: 'fields',
72
+ section: 'advanced',
73
+ collapsible: {
74
+ title: t('feishuAuthAdvancedTitle'),
75
+ description: t('feishuAuthAdvancedDescription')
76
+ }
77
+ }
78
+ ]
79
+ };
80
+ }
81
+
51
82
  export function buildChannelFormDefinitions(): Record<string, ChannelFormDefinition> {
52
83
  return {
53
84
  telegram: {
@@ -94,24 +125,7 @@ export function buildChannelFormDefinitions(): Record<string, ChannelFormDefinit
94
125
  { name: 'allowFrom', type: 'tags', label: t('allowFrom') }
95
126
  ]
96
127
  },
97
- feishu: {
98
- fields: [
99
- { name: 'enabled', type: 'boolean', label: t('enabled') },
100
- { name: 'appId', type: 'text', label: t('appId') },
101
- { name: 'appSecret', type: 'password', label: t('appSecret') },
102
- { name: 'encryptKey', type: 'password', label: t('encryptKey') },
103
- { name: 'verificationToken', type: 'password', label: t('verificationToken') },
104
- { name: 'domain', type: 'text', label: 'Domain' },
105
- { name: 'allowFrom', type: 'tags', label: t('allowFrom') },
106
- { name: 'dmPolicy', type: 'select', label: t('dmPolicy'), options: DM_POLICY_OPTIONS },
107
- { name: 'groupPolicy', type: 'select', label: t('groupPolicy'), options: GROUP_POLICY_OPTIONS },
108
- { name: 'groupAllowFrom', type: 'tags', label: t('groupAllowFrom') },
109
- { name: 'requireMention', type: 'boolean', label: t('requireMention') },
110
- { name: 'mentionPatterns', type: 'tags', label: t('mentionPatterns') },
111
- { name: 'groups', type: 'json', label: t('groupRulesJson') },
112
- { name: 'accounts', type: 'json', label: t('accountsJson') }
113
- ]
114
- },
128
+ feishu: buildFeishuFormDefinition(),
115
129
  dingtalk: {
116
130
  fields: [
117
131
  { name: 'enabled', type: 'boolean', label: t('enabled') },
@@ -1,4 +1,4 @@
1
- import { FileCode2, MessageSquareText, X } from "lucide-react";
1
+ import { AlarmClock, FileCode2, MessageSquareText, X } from "lucide-react";
2
2
  import type { ResolvedChildSessionTab } from "@/features/chat/hooks/use-ncp-child-session-tabs-view";
3
3
  import type { ChatWorkspaceFileTab } from "@/features/chat/stores/chat-thread.store";
4
4
  import { AgentIdentityAvatar } from "@/shared/components/common/agent-identity";
@@ -14,11 +14,14 @@ export type WorkspaceSelection =
14
14
  | {
15
15
  kind: "file";
16
16
  file: ChatWorkspaceFileTab;
17
+ }
18
+ | {
19
+ kind: "cron";
17
20
  };
18
21
 
19
22
  export type WorkspaceTabViewModel = {
20
23
  key: string;
21
- kind: "child-session" | "file";
24
+ kind: "child-session" | "file" | "cron";
22
25
  title: string;
23
26
  tooltip: string;
24
27
  active: boolean;
@@ -38,19 +41,27 @@ export function readWorkspaceFileTitle(file: ChatWorkspaceFileTab): string {
38
41
  }
39
42
 
40
43
  export function resolveWorkspaceSelection(params: {
44
+ activePanelKind?: "child-session" | "file" | "cron" | null;
41
45
  activeChildSessionKey: string | null;
42
46
  activeWorkspaceFileKey: string | null;
43
47
  childSessionTabs: ResolvedChildSessionTab[];
44
48
  workspaceFileTabs: readonly ChatWorkspaceFileTab[];
49
+ sessionCronJobCount: number;
45
50
  }): WorkspaceSelection | null {
46
51
  const {
52
+ activePanelKind,
47
53
  activeChildSessionKey,
48
54
  activeWorkspaceFileKey,
49
55
  childSessionTabs,
50
56
  workspaceFileTabs,
57
+ sessionCronJobCount,
51
58
  } = params;
52
59
 
53
- if (activeWorkspaceFileKey) {
60
+ if (activePanelKind === "cron" && sessionCronJobCount > 0) {
61
+ return { kind: "cron" };
62
+ }
63
+
64
+ if (activePanelKind !== "child-session" && activeWorkspaceFileKey) {
54
65
  const activeFile = workspaceFileTabs.find(
55
66
  (file) => file.key === activeWorkspaceFileKey,
56
67
  );
@@ -62,7 +73,7 @@ export function resolveWorkspaceSelection(params: {
62
73
  }
63
74
  }
64
75
 
65
- if (activeChildSessionKey) {
76
+ if (activePanelKind !== "file" && activeChildSessionKey) {
66
77
  const activeChild = childSessionTabs.find(
67
78
  (tab) => tab.sessionKey === activeChildSessionKey,
68
79
  );
@@ -88,10 +99,18 @@ export function resolveWorkspaceSelection(params: {
88
99
  };
89
100
  }
90
101
 
102
+ if (sessionCronJobCount > 0) {
103
+ return { kind: "cron" };
104
+ }
105
+
91
106
  return null;
92
107
  }
93
108
 
94
109
  function WorkspaceTabIcon({ agentId, kind }: Pick<WorkspaceTabViewModel, "agentId" | "kind">) {
110
+ if (kind === "cron") {
111
+ return <AlarmClock className="h-3.5 w-3.5 shrink-0 text-gray-400" />;
112
+ }
113
+
95
114
  if (kind === "file") {
96
115
  return <FileCode2 className="h-3.5 w-3.5 shrink-0 text-gray-400" />;
97
116
  }
@@ -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,6 +29,7 @@ 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
 
@@ -36,11 +38,14 @@ type ChatSessionWorkspacePanelProps = {
36
38
  activeChildSessionKey: string | null;
37
39
  workspaceFileTabs: readonly ChatWorkspaceFileTab[];
38
40
  activeWorkspaceFileKey: string | null;
41
+ activePanelKind?: "child-session" | "file" | "cron" | null;
42
+ sessionCronJobs?: readonly CronJobView[];
39
43
  sessionProjectRoot: string | null;
40
44
  displayMode?: "docked" | "overlay";
41
45
  onSelectSession: (sessionKey: string) => void;
42
46
  onSelectFile: (fileKey: string) => void;
43
47
  onCloseFile: (fileKey: string) => void;
48
+ onSelectCronJobs?: () => void;
44
49
  onClose: () => void;
45
50
  onBackToParent: () => void;
46
51
  onToolAction?: (action: ChatToolActionViewModel) => void;
@@ -167,20 +172,24 @@ function WorkspaceActiveChildHeader({
167
172
  function buildWorkspaceTabsViewModel(params: {
168
173
  resolvedChildTabs: ResolvedChildSessionTab[];
169
174
  workspaceFileTabs: readonly ChatWorkspaceFileTab[];
175
+ sessionCronJobCount: number;
170
176
  activeSelection: ReturnType<typeof resolveWorkspaceSelection>;
171
177
  optimisticReadAtBySessionKey: Record<string, string>;
172
178
  onSelectSession: (sessionKey: string) => void;
173
179
  onSelectFile: (fileKey: string) => void;
174
180
  onCloseFile: (fileKey: string) => void;
181
+ onSelectCronJobs: () => void;
175
182
  }): WorkspaceTabViewModel[] {
176
183
  const {
177
184
  resolvedChildTabs,
178
185
  workspaceFileTabs,
186
+ sessionCronJobCount,
179
187
  activeSelection,
180
188
  optimisticReadAtBySessionKey,
181
189
  onSelectSession,
182
190
  onSelectFile,
183
191
  onCloseFile,
192
+ onSelectCronJobs,
184
193
  } = params;
185
194
 
186
195
  const childTabs = resolvedChildTabs.map((tab) => {
@@ -225,7 +234,19 @@ function buildWorkspaceTabsViewModel(params: {
225
234
  onClose: () => onCloseFile(file.key),
226
235
  }));
227
236
 
228
- return [...childTabs, ...fileTabs];
237
+ const cronTab =
238
+ sessionCronJobCount > 0
239
+ ? [{
240
+ key: "cron:session",
241
+ kind: "cron" as const,
242
+ title: t("chatWorkspaceSessionCronJobs"),
243
+ tooltip: t("chatWorkspaceSessionCronJobs"),
244
+ active: activeSelection?.kind === "cron",
245
+ onSelect: onSelectCronJobs,
246
+ }]
247
+ : [];
248
+
249
+ return [...childTabs, ...fileTabs, ...cronTab];
229
250
  }
230
251
 
231
252
  export function ChatSessionWorkspacePanel({
@@ -233,11 +254,14 @@ export function ChatSessionWorkspacePanel({
233
254
  activeChildSessionKey,
234
255
  workspaceFileTabs,
235
256
  activeWorkspaceFileKey,
257
+ activePanelKind,
258
+ sessionCronJobs = [],
236
259
  sessionProjectRoot,
237
260
  displayMode = "docked",
238
261
  onSelectSession,
239
262
  onSelectFile,
240
263
  onCloseFile,
264
+ onSelectCronJobs = () => {},
241
265
  onClose,
242
266
  onBackToParent,
243
267
  onToolAction,
@@ -251,8 +275,10 @@ export function ChatSessionWorkspacePanel({
251
275
  const activeSelection = resolveWorkspaceSelection({
252
276
  activeChildSessionKey,
253
277
  activeWorkspaceFileKey,
278
+ activePanelKind,
254
279
  childSessionTabs: resolvedChildTabs,
255
280
  workspaceFileTabs,
281
+ sessionCronJobCount: sessionCronJobs.length,
256
282
  });
257
283
  const hasParentSession = resolvedChildTabs.some((tab) =>
258
284
  Boolean(tab.parentSessionKey),
@@ -278,20 +304,24 @@ export function ChatSessionWorkspacePanel({
278
304
  buildWorkspaceTabsViewModel({
279
305
  resolvedChildTabs,
280
306
  workspaceFileTabs,
307
+ sessionCronJobCount: sessionCronJobs.length,
281
308
  activeSelection,
282
309
  optimisticReadAtBySessionKey,
283
310
  onSelectSession,
284
311
  onSelectFile,
285
312
  onCloseFile,
313
+ onSelectCronJobs,
286
314
  }),
287
315
  [
288
316
  activeSelection,
289
317
  onCloseFile,
318
+ onSelectCronJobs,
290
319
  onSelectFile,
291
320
  onSelectSession,
292
321
  optimisticReadAtBySessionKey,
293
322
  resolvedChildTabs,
294
323
  workspaceFileTabs,
324
+ sessionCronJobs.length,
295
325
  ],
296
326
  );
297
327
 
@@ -344,12 +374,14 @@ export function ChatSessionWorkspacePanel({
344
374
  />
345
375
  </div>
346
376
  </>
347
- ) : (
377
+ ) : activeSelection.kind === "file" ? (
348
378
  <ChatSessionWorkspaceFilePreview
349
379
  file={activeSelection.file}
350
380
  sessionProjectRoot={sessionProjectRoot}
351
381
  onFileOpen={onFileOpen}
352
382
  />
383
+ ) : (
384
+ <SessionCronJobContent jobs={sessionCronJobs} />
353
385
  )}
354
386
  </div>
355
387
  </div>