@v587d/capital-generation 2.12.0 → 2.13.1

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.
@@ -0,0 +1,2734 @@
1
+ /**
2
+ * Browser half of `@v587d/capital-automation`: the `capital-automation` main key plus the
3
+ * adjacent sidebar row. The page owns no schedule identity and reads only the profile the
4
+ * server put it in; `/capital-automation` is the only data source.
5
+ */
6
+ const { createElement: h, useEffect, useRef, useState } = require('react')
7
+ const {
8
+ IconAlarmClockOutlineRegular,
9
+ IconChecklistOutlineRegular,
10
+ IconChevronLeftOutlineRegular,
11
+ IconChevronRightOutlineRegular,
12
+ IconClockOutlineRegular,
13
+ IconCopyOutlineRegular,
14
+ IconEditOutlineRegular,
15
+ IconEllipsisOutlineRegular,
16
+ IconFolderOpenOutlineRegular,
17
+ IconGaugeOutlineRegular,
18
+ IconGlobeOutlineRegular,
19
+ IconInfoOutlineRegular,
20
+ IconLightOutlineMedium,
21
+ IconListPenOutlineRegular,
22
+ IconPauseOutlineRegular,
23
+ IconPlayOutlineRegular,
24
+ IconPlusOutlineRegular,
25
+ IconRefreshOutlineRegular,
26
+ IconSearchOutlineRegular,
27
+ IconSparkleRegular,
28
+ IconTrashOutlineRegular,
29
+ IconWarningOutlineRegular,
30
+ Menu,
31
+ MenuItemButton,
32
+ Modal,
33
+ Switch,
34
+ } = require('@deepseek-ai/dsh-client-ui-primitives')
35
+
36
+ const ENTRY_NAME = 'capital-automation'
37
+ const ROUTE = '/capital-automation'
38
+ const PANEL_ID = 'capital-automation'
39
+ const STYLE_TAG = 'capital-automation-css'
40
+ const ACTIVE_RUN_STATUSES = ['queued', 'starting', 'running']
41
+ const FAILURE_RUN_STATUSES = ['failed', 'timed_out', 'blocked_requires_user', 'unknown_after_restart']
42
+ const DEFAULT_LIMITS = { runTimeoutMinutes: 30, idleTimeoutMinutes: 5, maxSteps: 300, maxToolCalls: 1200 }
43
+ const WEEKDAYS = [
44
+ { value: 1, label: '一' },
45
+ { value: 2, label: '二' },
46
+ { value: 3, label: '三' },
47
+ { value: 4, label: '四' },
48
+ { value: 5, label: '五' },
49
+ { value: 6, label: '六' },
50
+ { value: 7, label: '日' },
51
+ ]
52
+ const TIMEZONES = ['Asia/Shanghai', 'Asia/Hong_Kong', 'Asia/Singapore', 'Asia/Tokyo', 'UTC', 'Europe/London', 'America/New_York', 'America/Los_Angeles']
53
+ const HISTORY_DAYS = 30
54
+
55
+ const zh = {
56
+ panel: 'CapGen 投研自动化',
57
+ title: 'CapGen 投研自动化',
58
+ subtitle: '定时投研,自动执行,结果可追溯',
59
+ tabs: { tasks: '任务', runs: '运行' },
60
+ storageUnavailable: '存储不可用,页面只读',
61
+ standbyBanner: '调度器未取得运行权:定时触发与手动运行已暂停,任务配置仍可查看和编辑。',
62
+ storageBanner: '投研自动化存储暂时不可用:页面只读,调度器不会启动。',
63
+ calendarBanner: '交易日历暂时不可用:仅交易日的任务会跳过,其他任务仍按设定时间运行。',
64
+ calendarCoverageBanner: '交易日历只覆盖到 {through}:之后仅交易日的任务会按 fail-closed 跳过;交易所公布新一年安排后会自动接上。',
65
+ calendarField: '交易日历',
66
+ calendarSourceSzse: '深交所官方日历',
67
+ calendarSourceBundled: '内置表',
68
+ calendarSourceNone: '不可用',
69
+ calendarThrough: '覆盖至 {date}',
70
+ calendarUnknown: '未知',
71
+ loadRetry: '重新读取',
72
+ loading: '读取中…',
73
+ empty: '还没有任务。',
74
+ emptyTasksTitle: '还没有添加的任务',
75
+ emptyTasksBody: '从下方添加一个内置模板,或新建一个自己的投研任务。',
76
+ emptyNoTemplates: '还没有任务',
77
+ emptyNoTemplatesBody: '点击右上角「新建任务」,创建第一个定时投研任务。',
78
+ emptyFiltered: '没有匹配的任务。',
79
+ clearFilter: '清除筛选',
80
+ newTask: '新建任务',
81
+ newTaskTitle: '新建投研任务',
82
+ newTaskHint: '任务会在后台新建一个 session 执行,不需要你守在页面上。',
83
+ editTask: '编辑任务',
84
+ allTasks: '全部任务',
85
+ backToTasks: '所有任务',
86
+ taskSection: '任务',
87
+ taskSectionHint: '点击任务行查看详情、运行记录和配置。',
88
+ templateSection: '模板',
89
+ templateAdd: '添加',
90
+ templateAdding: '添加中…',
91
+ templateMeta: '内置模板',
92
+ runningSection: '正在运行',
93
+ runningSince: '开始于',
94
+ cancelRun: '取消运行',
95
+ searchPlaceholder: '搜索任务',
96
+ filterAll: '全部',
97
+ filterCustom: '我的任务',
98
+ filterNews: '资讯',
99
+ filterMarket: '行情',
100
+ filterReview: '复盘',
101
+ filterResearch: '研究',
102
+ filterLabel: '筛选任务',
103
+ category: { news: '资讯', market: '行情', review: '复盘', research: '研究' },
104
+ categoryField: '任务分类',
105
+ categoryFixed: '内置模板的分类固定;复制为我的任务后可以调整。',
106
+ modeField: '输出侧重',
107
+ modeHint: '只影响输出结构,不改变 Agent preset;所有运行固定使用 Capital 自动模式。',
108
+ mode: { news: '资讯', market: '行情', review: '复盘', research: '研究' },
109
+ cadence: { daily: '每天', weekly: '每周', monthly: '每月' },
110
+ stateLabel: '状态',
111
+ state: { dormant: '未添加', active: '已启用', paused: '已暂停' },
112
+ origin: { builtin: '内置模板', custom: '我的任务' },
113
+ status: {
114
+ queued: '排队中', starting: '启动中', running: '运行中', succeeded: '成功', failed: '失败',
115
+ canceled: '已取消', timed_out: '超时', blocked_requires_user: '需要用户确认',
116
+ unknown_after_restart: '重启后未知', skipped_overlap: '重叠跳过',
117
+ skipped_calendar_unavailable: '日历不可用跳过', missed: '错过',
118
+ },
119
+ runNow: '立即运行',
120
+ runStarted: '已提交运行',
121
+ pause: '暂停',
122
+ activate: '启用',
123
+ add: '添加',
124
+ hibernate: '休眠并恢复默认',
125
+ copy: '复制为我的任务',
126
+ remove: '删除任务',
127
+ edit: '编辑',
128
+ more: '更多操作',
129
+ save: '保存',
130
+ saving: '保存中…',
131
+ processing: '处理中…',
132
+ saved: '已保存',
133
+ cancel: '取消',
134
+ create: '创建任务',
135
+ creating: '创建中…',
136
+ required: '标题和任务指令不能为空。',
137
+ dirty: '有未保存的修改',
138
+ noDirty: '配置已同步',
139
+ nextRun: '下次运行',
140
+ lastResult: '上次结果',
141
+ never: '暂无',
142
+ calendarUnavailable: '日历不可用',
143
+ unread: '未读',
144
+ overview: '概览',
145
+ config: '配置',
146
+ runs: '运行记录',
147
+ notifications: '通知',
148
+ descriptionField: '说明',
149
+ titleField: '标题',
150
+ instructionField: '任务指令',
151
+ instructionPlaceholder: '例如:读取今日自选股行情、公告与相关新闻,按「结论—证据—风险」整理成一份可复核的盘后复盘。',
152
+ titlePlaceholder: '给这个任务起个名字',
153
+ descriptionPlaceholder: '一句话说明任务目标(可选)',
154
+ triggerField: '触发器',
155
+ triggerHint: '按本地墙钟时间计算;仅交易日任务由交易日历判断周末与节假日。',
156
+ triggerHintNatural: '按本地墙钟时间计算:每天都触发,周末与节假日也运行。',
157
+ timeField: '时间',
158
+ timezoneField: '时区',
159
+ weekdayField: '星期',
160
+ dayField: '日期',
161
+ tradingDaysField: '仅交易日',
162
+ weekdayRequired: '每周至少选择一天。',
163
+ invalidTime: '运行时间格式不正确,请选择 HH:mm。',
164
+ invalidTimezone: '时区不能为空,请填写 IANA 时区。',
165
+ invalidTimezoneValue: '时区不被识别,请填写有效的 IANA 时区。',
166
+ invalidLimits: '运行上限必须是有效数字。',
167
+ filterNoRuns: '没有符合筛选条件的运行记录。',
168
+ schedulePrefix: '下次',
169
+ workspaceField: '工作区',
170
+ workspaceDefault: '跟随默认工作区',
171
+ workspaceMissing: '已移除的工作区(请重新选择)',
172
+ workspaceUnavailable: '工作区服务暂时不可用,运行会退回默认目录。',
173
+ limitsField: '运行上限',
174
+ advanced: '高级设置',
175
+ advancedHint: '预算覆盖整棵运行子树:子 Agent 与 PTC 派发都计入。',
176
+ runTimeout: '最长运行(分钟)',
177
+ idleTimeout: '空闲超时(分钟)',
178
+ maxSteps: '最大步骤',
179
+ maxToolCalls: '最大工具调用',
180
+ notifyField: '通知',
181
+ notifyInApp: '应用内通知',
182
+ notifyFailure: '失败时通知',
183
+ notifySuccess: '成功时通知',
184
+ notifyHint: '通知只记录在当前 profile;不会发送邮件或外部消息。',
185
+ presetLabel: 'Agent preset',
186
+ presetValue: 'capital-generation(自动模式)',
187
+ policy: '执行策略',
188
+ policyText: '固定 preset:capital-generation(Capital 自动模式)。工作区来自已注册列表;未选择时使用专属自动化工作区。无人值守运行禁止提问、写文件与 shell 提权。',
189
+ budgetHint: '工具调用与步骤预算覆盖整棵运行子树:子 Agent 与 PTC 派发都计入。',
190
+ environment: '执行环境',
191
+ policyField: '无人值守策略',
192
+ cwdKind: '工作区来源',
193
+ techInfo: '技术信息',
194
+ version: '版本',
195
+ profileKey: 'profileKey',
196
+ runHistory: '运行历史',
197
+ last30Days: '最近 30 天',
198
+ runCount: '{n} 次运行',
199
+ successCount: '{n} 成功',
200
+ failureCount: '{n} 失败',
201
+ historyEmpty: '最近 30 天还没有运行记录。',
202
+ historyViewAll: '查看全部运行',
203
+ runFilterAll: '全部',
204
+ runFilterActive: '进行中',
205
+ runFilterSucceeded: '成功',
206
+ runFilterFailed: '失败',
207
+ runFilterLabel: '筛选运行记录',
208
+ runsTitle: '运行',
209
+ runsHint: '每次运行都使用新的 session;终态后才能打开 session。',
210
+ runsEmptyTitle: '尚未运行',
211
+ runsEmptyBody: '任务运行后,这里会显示状态、耗时与错误条数;结果与过程在 session 里。',
212
+ runsEmptyAction: '去添加模板',
213
+ runsLimitHint: '仅显示最近 {n} 条运行记录。',
214
+ cancelRunConfirmTitle: '取消这次运行?',
215
+ cancelRunConfirmBody: '取消会向当前运行发送停止信号;已经产生的中间结果会留在 session 里。',
216
+ cancelRunConfirm: '确认取消',
217
+ duration: '耗时',
218
+ steps: '步骤',
219
+ tools: '工具调用',
220
+ errorTitle: '错误',
221
+ errorDetailsLabel: '错误详情',
222
+ showErrorDetails: '查看错误详情',
223
+ hideErrorDetails: '收起错误详情',
224
+ triggerManual: '手动',
225
+ triggerScheduled: '定时',
226
+ openSession: '打开 session',
227
+ runSummaryFallback: '运行结束但没有摘要。',
228
+ notificationsEmpty: '没有通知。',
229
+ markRead: '全部标为已读',
230
+ markTaskRead: '本任务标为已读',
231
+ notificationsTitle: '通知',
232
+ notificationsHint: '运行失败、超时、调度器待命与交易日历问题会出现在这里。',
233
+ notificationsUnread: '{n} 条未读',
234
+ allRead: '全部已读',
235
+ noNotifications: '没有未读或历史通知。',
236
+ notificationOpenTask: '查看任务',
237
+ close: '关闭',
238
+ confirmDeleteTitle: '删除这个任务?',
239
+ confirmDeleteBody: '任务配置、运行记录与相关通知会一起删除,无法恢复。',
240
+ confirmHibernateTitle: '休眠并恢复默认?',
241
+ confirmHibernateBody: '当前对内置模板的修改会被清空,任务回到模板区;历史运行记录保留。',
242
+ confirmDelete: '确认删除',
243
+ confirmHibernate: '确认休眠',
244
+ actionDone: '操作完成',
245
+ created: '任务已创建',
246
+ activated: '已启用「{title}」',
247
+ added: '已添加「{title}」',
248
+ paused: '已暂停「{title}」',
249
+ copied: '已复制为「{title}」',
250
+ deleted: '已删除「{title}」',
251
+ hibernated: '已休眠「{title}」',
252
+ runSubmitted: '已提交「{title}」运行',
253
+ runCanceled: '已取消运行',
254
+ markReadDone: '已标为已读',
255
+ errorGeneric: '操作失败,请稍后重试。',
256
+ needConfirm: '需要用户确认',
257
+ schedulerBlocked: '调度器未取得运行权,暂时不能手动运行。',
258
+ runBlocked: '任务正在运行,结束或取消后才能再次运行。',
259
+ configWhileRunning: '任务正在运行;修改会在下一次运行时生效。',
260
+ configBuiltinHint: '这是内置模板;修改只覆盖当前 profile,休眠后会恢复默认。',
261
+ detailRecentRuns: '最近运行',
262
+ detailNoRuns: '还没有运行记录。',
263
+ detailAllRuns: '查看运行页',
264
+ detailTaskNotifications: '任务通知',
265
+ taskRunsLimit: '这里显示当前任务最近 20 次运行;全部运行见「运行」页。',
266
+ runErrors: '{n} 条错误',
267
+ runNoErrors: '无错误',
268
+ notificationFallbackFailed: '运行失败;打开对应任务或 session 查看详情。',
269
+ notificationFallbackTimeout: '运行超时;打开对应任务或 session 查看详情。',
270
+ notificationFallbackSuccess: '运行完成。',
271
+ notificationFallbackStandby: '调度器未取得运行权,定时触发暂停。',
272
+ notificationFallbackCalendar: '交易日历不可用,交易日任务已跳过。',
273
+ notificationFallbackSkipped: '本次计划已跳过。',
274
+ statusLabel: '状态',
275
+ triggerSummary: '日程',
276
+ lastRun: '上次运行',
277
+ neverRun: '尚未运行',
278
+ relativeNow: '刚刚',
279
+ timezoneHint: 'IANA 时区,例如 Asia/Shanghai。',
280
+ nextRunUnavailable: '交易日历不可用,无法计算下一次运行。',
281
+ copyHint: '复制后会生成一个暂停状态的自定义任务。',
282
+ notificationTaskGone: '任务已不存在',
283
+ noSelection: '从列表中选择一个任务。',
284
+ details: '详情',
285
+ actions: '操作',
286
+ rosterCount: '{n} 个任务',
287
+ dayOfMonth: '{n} 日',
288
+ everyPrefix: '每',
289
+ weeklyPrefix: '每周',
290
+ monthlyPrefix: '每月',
291
+ dailyPrefix: '每天',
292
+ tradingSuffix: '(仅交易日)',
293
+ sessionMissing: '没有可打开的 session',
294
+ }
295
+
296
+ function installStyles() {
297
+ if (typeof document === 'undefined' || document.querySelector(`style[data-${STYLE_TAG}]`)) return () => {}
298
+ const style = document.createElement('style')
299
+ style.setAttribute(`data-${STYLE_TAG}`, 'true')
300
+ style.textContent = `
301
+ .capital-automation-page, .capital-automation-modal { --ca-card: var(--dsw-alias-bg-layer-2, var(--dsw-alias-bg-layer-1)); --ca-line: var(--dsw-alias-border-l3, var(--dsw-alias-border-l1)); --ca-line-soft: var(--dsw-alias-border-l2, var(--dsw-alias-border-l1)); --ca-table-line: var(--ca-line); --ca-muted: var(--dsw-alias-label-tertiary); --ca-caption: var(--dsw-alias-label-caption, var(--dsw-alias-label-tertiary)); --ca-hover: var(--dsw-alias-interactive-bg-hover); --ca-field: var(--dsw-alias-bg-module-platform); --ca-business: var(--dsw-alias-state-business-primary, var(--dsw-alias-brand-primary)); --ca-success: var(--dsw-alias-state-success-primary); --ca-error: var(--dsw-alias-state-error-primary); --ca-warn: var(--dsw-alias-state-warn-label); --ca-business-text: var(--ca-business); --ca-success-text: var(--ca-success); --ca-error-text: var(--ca-error); --ca-warn-text: var(--ca-warn); }
302
+ body:not([data-ds-dark-theme]) .capital-automation-page, body:not([data-ds-dark-theme]) .capital-automation-modal { --ca-line: #0000001f; --ca-line-soft: #0000001a; --ca-table-line: #00000026; --ca-muted: #5d626a; --ca-caption: #6a6f77; --ca-hover: #eef1f5; --ca-field: #f7f8fa; --ca-business: #2f63d6; --ca-success: #16a34a; --ca-error: #dc2626; --ca-warn: #d97706; --ca-business-text: #2f63d6; --ca-success-text: #15803d; --ca-error-text: #c81e1e; --ca-warn-text: #b45309; --ca-tag-news: #2f6fd0; --ca-tag-market: #a85a08; --ca-tag-review: #6d4ac4; --ca-tag-research: #0f766e; --ca-tag-custom: #b3246d; }
303
+ .capital-automation-page { display: flex; flex-direction: column; min-height: 0; height: 100%; color: var(--dsw-alias-label-primary); }
304
+ .capital-automation-head { flex: none; padding: 18px 0 0; }
305
+ .capital-automation-headinner { box-sizing: border-box; width: min(1080px, 100%); margin: 0 auto; padding: 0 clamp(20px, 3vw, 36px); }
306
+ .capital-automation-topbar { display: grid; grid-template-columns: minmax(0, 1fr) auto minmax(0, 1fr); align-items: center; gap: 12px 18px; min-width: 0; }
307
+ .capital-automation-brand { display: flex; align-items: center; gap: 10px; min-width: 0; justify-self: start; }
308
+ .capital-automation-brandmark { flex: none; width: 28px; height: 28px; display: grid; place-items: center; border-radius: 9px; color: #fff; background: linear-gradient(135deg, #4f8cff, #7c3aed); box-shadow: inset 0 1px 0 rgba(255,255,255,.24); }
309
+ .capital-automation-brand h1 { margin: 0; font-size: 17px; line-height: 24px; font-weight: 600; letter-spacing: .1px; white-space: nowrap; }
310
+ .capital-automation-tabs { display: inline-flex; align-items: center; gap: 2px; padding: 3px; border: .5px solid var(--ca-line-soft); border-radius: 999px; background: color-mix(in srgb, var(--dsw-alias-label-primary) 5%, var(--ca-card)); box-shadow: inset 0 1px 2px color-mix(in srgb, var(--dsw-alias-label-primary) 4%, transparent); justify-self: center; }
311
+ .capital-automation-tab { height: 28px; padding: 0 14px; border: 0; border-radius: 999px; background: transparent; color: var(--ca-muted); font: inherit; font-size: 12.5px; font-weight: 500; line-height: 28px; cursor: pointer; transition: background .15s, color .15s, box-shadow .15s; }
312
+ .capital-automation-tab:hover:not([aria-selected="true"]) { background: color-mix(in srgb, var(--dsw-alias-label-primary) 5%, transparent); color: var(--dsw-alias-label-primary); }
313
+ .capital-automation-tab[aria-selected="true"] { background: var(--dsw-alias-label-primary); color: var(--dsw-alias-bg-base); font-weight: 600; box-shadow: 0 1px 2px rgba(0,0,0,.18), 0 2px 8px color-mix(in srgb, var(--dsw-alias-label-primary) 12%, transparent); }
314
+ .capital-automation-tab:focus-visible { outline: 2px solid var(--dsw-focus-ring-color, var(--ca-business)); outline-offset: 2px; }
315
+ .capital-automation-topactions { display: flex; align-items: center; gap: 8px; justify-self: end; }
316
+ .capital-automation-btn { box-sizing: border-box; display: inline-flex; align-items: center; justify-content: center; gap: 6px; height: 32px; padding: 0 12px; border: .5px solid var(--ca-line-soft); border-radius: 999px; background: transparent; color: var(--dsw-alias-label-secondary); font: inherit; font-size: 12.5px; line-height: 20px; white-space: nowrap; cursor: pointer; transition: background .15s, border-color .15s, color .15s, opacity .15s; }
317
+ .capital-automation-btn:hover:not(:disabled) { background: var(--ca-hover); color: var(--dsw-alias-label-primary); }
318
+ .capital-automation-btn[data-variant="primary"] { border-color: transparent; background: var(--ca-business); color: var(--dsw-alias-bg-base); }
319
+ .capital-automation-btn[data-variant="primary"]:hover:not(:disabled) { background: var(--ca-business); color: var(--dsw-alias-bg-base); filter: brightness(1.08); }
320
+ .capital-automation-templatefoot .capital-automation-btn[data-variant="primary"] { min-width: 54px; padding: 0 12px; font-weight: 600; box-shadow: inset 0 1px 0 rgba(255,255,255,.16), 0 1px 2px rgba(0,0,0,.16); }
321
+ .capital-automation-templatefoot .capital-automation-btn[data-variant="primary"]:hover:not(:disabled) { box-shadow: inset 0 1px 0 rgba(255,255,255,.16), 0 2px 8px color-mix(in srgb, var(--ca-business) 32%, transparent); }
322
+ .capital-automation-btn[data-variant="contrast"] { border-color: transparent; background: var(--dsw-alias-label-primary); color: var(--dsw-alias-bg-base); }
323
+ .capital-automation-btn[data-variant="contrast"]:hover:not(:disabled) { background: var(--dsw-alias-label-primary); color: var(--dsw-alias-bg-base); opacity: .88; }
324
+ .capital-automation-btn[data-variant="ghost"] { border-color: transparent; background: transparent; color: var(--ca-muted); }
325
+ .capital-automation-btn[data-variant="ghost"]:hover:not(:disabled) { background: var(--ca-hover); color: var(--dsw-alias-label-primary); }
326
+ .capital-automation-btn[data-variant="danger"] { border-color: transparent; background: var(--ca-error); color: var(--dsw-alias-bg-base); }
327
+ .capital-automation-btn[data-variant="danger"]:hover:not(:disabled) { background: var(--ca-error); color: var(--dsw-alias-bg-base); filter: brightness(1.08); }
328
+ .capital-automation-btn[data-size="sm"] { height: 28px; padding: 0 10px; font-size: 12px; }
329
+ .capital-automation-btn[data-size="icon"] { width: 30px; height: 30px; padding: 0; border-radius: 9px; }
330
+ .capital-automation-btn:disabled { opacity: .42; cursor: not-allowed; }
331
+ .capital-automation-iconbtn { display: inline-grid; place-items: center; width: 30px; height: 30px; padding: 0; border: 0; border-radius: 8px; background: transparent; color: var(--ca-muted); cursor: pointer; }
332
+ .capital-automation-iconbtn:hover:not(:disabled) { background: var(--ca-hover); color: var(--dsw-alias-label-primary); }
333
+ .capital-automation-iconbtn:disabled { opacity: .4; cursor: not-allowed; }
334
+ .capital-automation-count { display: inline-flex; align-items: center; justify-content: center; min-width: 16px; height: 16px; padding: 0 4px; border-radius: 999px; background: var(--ca-business); color: var(--dsw-alias-bg-base); font-size: 10px; line-height: 16px; }
335
+ .capital-automation-banner { box-sizing: border-box; width: min(1080px, 100%); margin: 12px auto 0; padding: 10px 13px; border: .5px solid var(--ca-line-soft); border-radius: 12px; background: var(--ca-card); color: var(--dsw-alias-label-secondary); font-size: 12px; line-height: 19px; }
336
+ .capital-automation-banner[data-tone="warn"] { border-color: color-mix(in srgb, var(--ca-warn) 38%, var(--ca-line-soft)); color: var(--ca-warn-text); }
337
+ .capital-automation-banner[data-tone="error"] { border-color: color-mix(in srgb, var(--ca-error) 38%, var(--ca-line-soft)); color: var(--ca-error-text); }
338
+ .capital-automation-banner .capital-automation-btn { margin-left: 8px; vertical-align: middle; }
339
+ .capital-automation-scroll { flex: 1 1 auto; min-height: 0; overflow: auto; }
340
+ .capital-automation-container { box-sizing: border-box; width: min(1080px, 100%); margin: 0 auto; padding: 20px clamp(20px, 3vw, 36px) 72px; }
341
+ .capital-automation-card { box-sizing: border-box; border: .5px solid var(--ca-line); border-radius: 16px; background: var(--ca-card); }
342
+ .capital-automation-history { padding: 18px 20px 14px; }
343
+ .capital-automation-historyhead { display: flex; flex-wrap: wrap; align-items: flex-start; justify-content: space-between; gap: 10px 18px; }
344
+ .capital-automation-historytitle { display: flex; flex-wrap: wrap; align-items: baseline; gap: 8px; }
345
+ .capital-automation-historytitle h2 { margin: 0; font-size: 14px; line-height: 22px; font-weight: 600; }
346
+ .capital-automation-historytitle span { color: var(--ca-muted); font-size: 12px; line-height: 20px; }
347
+ .capital-automation-historystats { display: flex; align-items: center; gap: 14px; color: var(--ca-muted); font-size: 12px; line-height: 20px; }
348
+ .capital-automation-historystat { display: inline-flex; align-items: center; gap: 5px; font-variant-numeric: tabular-nums; }
349
+ .capital-automation-historystat[data-tone="ok"] { color: var(--ca-success-text); }
350
+ .capital-automation-historystat[data-tone="error"] { color: var(--ca-error-text); }
351
+ .capital-automation-statusdot { display: inline-block; width: 6px; height: 6px; border-radius: 50%; background: currentColor; flex: none; }
352
+ .capital-automation-historylink { display: inline-flex; align-items: center; gap: 2px; height: 24px; padding: 0 6px; border: 0; border-radius: 7px; background: transparent; color: var(--ca-muted); font: inherit; font-size: 12px; cursor: pointer; }
353
+ .capital-automation-historylink:hover { background: var(--ca-hover); color: var(--dsw-alias-label-primary); }
354
+ .capital-automation-timeline { display: grid; grid-template-columns: repeat(${HISTORY_DAYS}, minmax(0, 1fr)); align-items: end; gap: 2px; height: 28px; margin-top: 18px; border-bottom: 1px dashed var(--ca-line); }
355
+ .capital-automation-timeline-day { position: relative; display: flex; align-items: flex-end; justify-content: center; height: 100%; padding-bottom: 3px; }
356
+ .capital-automation-timeline-dot { width: 7px; height: 7px; border-radius: 50%; background: var(--ca-caption); box-shadow: 0 0 0 2px var(--ca-card); }
357
+ .capital-automation-timeline-dot[data-tone="ok"] { background: var(--ca-success); }
358
+ .capital-automation-timeline-dot[data-tone="error"] { background: var(--ca-error); }
359
+ .capital-automation-timeline-dot[data-tone="active"] { background: var(--ca-business); }
360
+ .capital-automation-timeline-dot[data-tone="warn"] { background: var(--ca-warn); }
361
+ .capital-automation-timelinelabels { display: flex; justify-content: space-between; margin-top: 7px; color: var(--ca-caption); font-size: 11px; line-height: 16px; }
362
+ .capital-automation-historyempty { margin-top: 12px; text-align: center; color: var(--ca-muted); font-size: 12px; line-height: 18px; }
363
+ .capital-automation-section { margin-top: 26px; }
364
+ .capital-automation-sectionhead { display: flex; flex-wrap: wrap; align-items: center; gap: 8px 14px; margin-bottom: 10px; }
365
+ .capital-automation-sectionhead h2 { margin: 0; font-size: 14px; line-height: 22px; font-weight: 600; }
366
+ .capital-automation-sectionhead > span { color: var(--ca-muted); font-size: 12px; line-height: 18px; }
367
+ .capital-automation-sectiontools { display: flex; flex-wrap: wrap; align-items: center; gap: 8px; margin-left: auto; }
368
+ .capital-automation-filters { display: flex; flex-wrap: wrap; align-items: center; gap: 4px; }
369
+ .capital-automation-filter { display: inline-flex; align-items: center; justify-content: center; gap: 6px; height: 28px; padding: 0 10px; border: 0; border-radius: 999px; background: transparent; color: var(--ca-muted); font: inherit; font-size: 12px; line-height: 28px; cursor: pointer; transition: background .15s, color .15s, box-shadow .15s; }
370
+ .capital-automation-filtercount { display: inline-flex; align-items: center; justify-content: center; min-width: 17px; height: 17px; padding: 0 4px; border-radius: 999px; background: color-mix(in srgb, currentColor 13%, transparent); font-size: 10.5px; line-height: 17px; font-variant-numeric: tabular-nums; }
371
+ .capital-automation-filter[aria-pressed="true"] .capital-automation-filtercount { background: var(--ca-business); color: var(--dsw-alias-bg-base); }
372
+ .capital-automation-filter:hover { background: var(--ca-hover); color: var(--dsw-alias-label-primary); }
373
+ .capital-automation-filter[aria-pressed="true"] { background: color-mix(in srgb, var(--ca-business) 11%, var(--ca-card)); box-shadow: inset 0 0 0 .5px color-mix(in srgb, var(--ca-business) 28%, transparent); color: var(--ca-business-text); font-weight: 600; }
374
+ .capital-automation-filter:focus-visible { outline: 2px solid var(--dsw-focus-ring-color, var(--ca-business)); outline-offset: 1px; }
375
+ .capital-automation-search { box-sizing: border-box; display: flex; align-items: center; gap: 6px; height: 30px; padding: 0 9px; border: .5px solid var(--ca-line-soft); border-radius: 999px; background: var(--ca-card); color: var(--ca-muted); }
376
+ .capital-automation-search:focus-within { border-color: var(--ca-business); }
377
+ .capital-automation-search input { width: 128px; min-width: 0; padding: 0; border: 0; outline: 0; background: transparent; color: var(--dsw-alias-label-primary); font: inherit; font-size: 12px; }
378
+ .capital-automation-search input::placeholder { color: var(--ca-caption); }
379
+ .capital-automation-running { display: flex; flex-wrap: wrap; align-items: center; gap: 10px 14px; margin-bottom: 10px; padding: 12px 14px; border: .5px solid color-mix(in srgb, var(--ca-business) 30%, var(--ca-line)); border-radius: 14px; background: color-mix(in srgb, var(--ca-business) 7%, var(--ca-card)); }
380
+ .capital-automation-runningrow { display: flex; flex-wrap: wrap; align-items: center; gap: 10px; width: 100%; min-width: 0; }
381
+ .capital-automation-runningactions { display: flex; flex: none; align-items: center; gap: 8px; margin-left: auto; }
382
+ .capital-automation-live { display: inline-flex; align-items: center; gap: 6px; color: var(--ca-business-text); font-size: 12px; }
383
+ .capital-automation-livedot { width: 7px; height: 7px; border-radius: 50%; background: currentColor; animation: capital-automation-pulse 1.6s ease-in-out infinite; }
384
+ @keyframes capital-automation-pulse { 0%, 100% { opacity: 1; box-shadow: 0 0 0 0 color-mix(in srgb, currentColor 30%, transparent); } 50% { opacity: .55; box-shadow: 0 0 0 4px color-mix(in srgb, currentColor 0%, transparent); } }
385
+ @media (prefers-reduced-motion: reduce) { .capital-automation-livedot { animation: none; } .capital-automation-skeleton { animation: none; } }
386
+ .capital-automation-table { overflow: hidden; border: .5px solid var(--ca-table-line); border-radius: 16px; background: var(--ca-card); }
387
+ .capital-automation-thead { display: grid; grid-template-columns: minmax(0, 1fr) 36px; align-items: center; padding-right: 18px; border-bottom: .5px solid var(--ca-table-line); background: color-mix(in srgb, var(--dsw-alias-label-primary) 3.5%, var(--ca-card)); color: var(--ca-muted); font-size: 11.5px; font-weight: 500; letter-spacing: .02em; line-height: 16px; }
388
+ .capital-automation-theadmain { display: grid; grid-template-columns: minmax(0, 1.8fr) minmax(110px, .85fr) minmax(128px, .95fr) 88px; gap: 14px; padding: 9px 0 9px 16px; }
389
+ .capital-automation-trow { display: grid; grid-template-columns: minmax(0, 1fr) 36px; align-items: stretch; padding-right: 18px; border-top: .5px solid var(--ca-table-line); transition: background .15s; }
390
+ .capital-automation-trow:first-of-type { border-top: 0; }
391
+ .capital-automation-trow:hover { background: color-mix(in srgb, var(--dsw-alias-label-primary) 5%, var(--ca-card)); }
392
+ .capital-automation-trowmain { display: grid; grid-template-columns: minmax(0, 1.8fr) minmax(110px, .85fr) minmax(128px, .95fr) 88px; align-items: center; gap: 14px; min-width: 0; padding: 11px 0 11px 16px; border: 0; background: transparent; color: inherit; font: inherit; text-align: left; cursor: pointer; }
393
+ .capital-automation-trowmain:focus-visible { outline: 2px solid var(--dsw-focus-ring-color, var(--ca-business)); outline-offset: -2px; }
394
+ .capital-automation-trowmenu { display: flex; align-items: center; justify-content: center; }
395
+ .capital-automation-cell { min-width: 0; }
396
+ .capital-automation-identity { display: flex; align-items: center; gap: 10px; min-width: 0; }
397
+ .capital-automation-glyph { flex: none; display: inline-grid; place-items: center; width: 34px; height: 34px; border-radius: 10px; color: #fff; box-shadow: inset 0 1px 0 rgba(255,255,255,.22); }
398
+ .capital-automation-glyph[data-tone="news"] { background: linear-gradient(135deg, #5b9dff, #2563eb); }
399
+ .capital-automation-glyph[data-tone="market"] { background: linear-gradient(135deg, #ffbd4a, #f97316); }
400
+ .capital-automation-glyph[data-tone="review"] { background: linear-gradient(135deg, #b18cff, #7c3aed); }
401
+ .capital-automation-glyph[data-tone="research"] { background: linear-gradient(135deg, #39d7a8, #0d9488); }
402
+ .capital-automation-glyph[data-tone="custom"] { background: linear-gradient(135deg, #f472b6, #db2777); }
403
+ .capital-automation-identitytext { min-width: 0; }
404
+ .capital-automation-tasktitlerow { display: flex; flex-wrap: wrap; align-items: center; gap: 2px 6px; min-width: 0; }
405
+ .capital-automation-tasktitlerow .capital-automation-tasktags { flex: 0 1 auto; flex-wrap: wrap; max-width: 100%; margin-top: 0; }
406
+ .capital-automation-tasktitle { display: block; flex: 0 1 auto; min-width: 0; overflow: hidden; color: var(--dsw-alias-label-primary); font-size: 13.5px; line-height: 20px; font-weight: 500; text-overflow: ellipsis; white-space: nowrap; }
407
+ .capital-automation-taskdesc { display: block; overflow: hidden; margin-top: 1px; color: var(--ca-muted); font-size: 11.5px; line-height: 17px; text-overflow: ellipsis; white-space: nowrap; }
408
+ .capital-automation-tasktags { display: flex; flex-wrap: wrap; align-items: center; gap: 4px; margin-top: 3px; }
409
+ .capital-automation-tag { display: inline-flex; align-items: center; gap: 4px; max-width: 13em; overflow: hidden; padding: 1px 6px; border-radius: 999px; background: var(--dsw-alias-bg-module-platform); color: var(--ca-muted); font-size: 11px; font-weight: 500; line-height: 17px; white-space: nowrap; }
410
+ .capital-automation-tag[data-tone="news"] { color: #6ba6ff; }
411
+ .capital-automation-tag[data-tone="market"] { color: #ffb45c; }
412
+ .capital-automation-tag[data-tone="review"] { color: #b79aff; }
413
+ .capital-automation-tag[data-tone="research"] { color: #56d9b4; }
414
+ .capital-automation-tag[data-tone="custom"] { color: #f79bc3; }
415
+ body:not([data-ds-dark-theme]) .capital-automation-tag { background: color-mix(in srgb, currentColor 9%, var(--ca-card)); }
416
+ body:not([data-ds-dark-theme]) .capital-automation-tag[data-tone="news"] { color: var(--ca-tag-news); }
417
+ body:not([data-ds-dark-theme]) .capital-automation-tag[data-tone="market"] { color: var(--ca-tag-market); }
418
+ body:not([data-ds-dark-theme]) .capital-automation-tag[data-tone="review"] { color: var(--ca-tag-review); }
419
+ body:not([data-ds-dark-theme]) .capital-automation-tag[data-tone="research"] { color: var(--ca-tag-research); }
420
+ body:not([data-ds-dark-theme]) .capital-automation-tag[data-tone="custom"] { color: var(--ca-tag-custom); }
421
+ .capital-automation-schedule { display: block; color: var(--dsw-alias-label-secondary); font-size: 12px; line-height: 18px; }
422
+ .capital-automation-schedulesub { display: block; margin-top: 1px; color: var(--ca-caption); font-size: 11px; line-height: 16px; }
423
+ .capital-automation-nextmain { display: block; color: var(--dsw-alias-label-secondary); font-size: 12px; line-height: 18px; }
424
+ .capital-automation-nextsub { display: block; margin-top: 1px; color: var(--ca-caption); font-size: 11px; line-height: 16px; }
425
+ .capital-automation-state { display: inline-flex; align-items: center; gap: 5px; max-width: 100%; height: 22px; padding: 0 8px; border-radius: 999px; background: var(--dsw-alias-bg-module-platform); color: var(--ca-muted); font-size: 11px; line-height: 22px; white-space: nowrap; }
426
+ .capital-automation-state[data-state="active"] { color: var(--ca-success-text); }
427
+ .capital-automation-state[data-state="paused"] { color: var(--ca-warn-text); }
428
+ .capital-automation-state[data-state="running"] { color: var(--ca-business-text); }
429
+ .capital-automation-state[data-state="dormant"] { color: var(--ca-muted); }
430
+ body:not([data-ds-dark-theme]) .capital-automation-state { background: color-mix(in srgb, currentColor 9%, var(--ca-card)); }
431
+ .capital-automation-statedot { width: 5px; height: 5px; border-radius: 50%; background: currentColor; flex: none; }
432
+ .capital-automation-state[data-state="running"] .capital-automation-statedot, .capital-automation-runstatus[data-status="running"] .capital-automation-rundot { animation: capital-automation-pulse 1.6s ease-in-out infinite; }
433
+ .capital-automation-empty { display: flex; flex-direction: column; align-items: center; gap: 6px; padding: 42px 20px; border: .5px dashed var(--ca-line); border-radius: 16px; color: var(--ca-muted); text-align: center; }
434
+ .capital-automation-emptyicon { display: grid; place-items: center; width: 38px; height: 38px; margin-bottom: 4px; border-radius: 12px; background: var(--dsw-alias-bg-module-platform); color: var(--ca-caption); }
435
+ .capital-automation-empty h3 { margin: 0; color: var(--dsw-alias-label-secondary); font-size: 13.5px; font-weight: 500; line-height: 20px; }
436
+ .capital-automation-empty p { max-width: 460px; margin: 0; font-size: 12px; line-height: 18px; }
437
+ .capital-automation-empty .capital-automation-btn { margin-top: 8px; }
438
+ .capital-automation-loading { display: flex; flex-direction: column; pointer-events: none; }
439
+ .capital-automation-skeletonlabel { position: absolute; width: 1px; height: 1px; overflow: hidden; clip-path: inset(50%); white-space: nowrap; }
440
+ .capital-automation-skeleton { display: block; flex: none; max-width: 100%; border-radius: 6px; background: var(--dsw-alias-bg-skeleton); animation: capital-automation-breathe 1.8s ease-in-out infinite; }
441
+ .capital-automation-skeletontimeline { display: flex; align-items: flex-end; gap: 2px; height: 28px; margin-top: 18px; border-bottom: 1px dashed var(--ca-line); }
442
+ .capital-automation-skeletontimeline > .capital-automation-skeleton { flex: 1 1 0; min-width: 0; }
443
+ .capital-automation-skeletonstack { display: flex; flex: 1 1 auto; flex-direction: column; gap: 6px; min-width: 0; }
444
+ .capital-automation-skeletonrow .capital-automation-trowmain { min-height: 57px; cursor: default; }
445
+ .capital-automation-skeletonrow .capital-automation-cell[data-cell] { display: flex; flex-direction: column; gap: 5px; }
446
+ .capital-automation-skeletonrun .capital-automation-runline { min-height: 20px; }
447
+ .capital-automation-skeletonrun .capital-automation-runmetaline { min-height: 17px; }
448
+ .capital-automation-skeletontemplatemain { display: flex; align-items: flex-start; gap: 11px; min-width: 0; }
449
+ @keyframes capital-automation-breathe { 0% { opacity: 1; } 40% { opacity: .55; } 80%, 100% { opacity: 1; } }
450
+ .capital-automation-templates { display: grid; grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)); gap: 12px; }
451
+ .capital-automation-template { box-sizing: border-box; display: flex; flex-direction: column; gap: 12px; min-height: 152px; padding: 15px 16px 13px; border: .5px solid var(--ca-line); border-radius: 16px; background: var(--ca-card); transition: border-color .15s, background .15s; }
452
+ .capital-automation-template:hover { border-color: color-mix(in srgb, var(--ca-business) 28%, var(--ca-line)); background: color-mix(in srgb, var(--ca-hover) 55%, var(--ca-card)); }
453
+ .capital-automation-templatemain { display: flex; align-items: flex-start; gap: 11px; min-width: 0; padding: 0; border: 0; background: transparent; color: inherit; font: inherit; text-align: left; cursor: pointer; }
454
+ .capital-automation-templatemain:focus-visible { outline: 2px solid var(--dsw-focus-ring-color, var(--ca-business)); outline-offset: 4px; border-radius: 8px; }
455
+ .capital-automation-templatebody { min-width: 0; }
456
+ .capital-automation-templatetitlerow { display: flex; flex-wrap: wrap; align-items: center; gap: 2px 6px; min-width: 0; }
457
+ .capital-automation-templatetitlerow .capital-automation-tag { flex: none; }
458
+ .capital-automation-templatebody strong { display: block; min-width: 0; color: var(--dsw-alias-label-primary); font-size: 13.5px; line-height: 20px; font-weight: 500; }
459
+ .capital-automation-templatedesc { display: -webkit-box; overflow: hidden; margin-top: 3px; color: var(--ca-muted); font-size: 12px; line-height: 18px; -webkit-line-clamp: 2; -webkit-box-orient: vertical; }
460
+ .capital-automation-templatefoot { display: flex; align-items: center; justify-content: space-between; gap: 8px; margin-top: auto; }
461
+ .capital-automation-templatemeta { display: inline-flex; align-items: center; gap: 4px; color: var(--ca-caption); font-size: 11px; line-height: 16px; }
462
+ .capital-automation-detailbar { display: flex; align-items: center; min-height: 34px; margin: 0 0 6px; }
463
+ .capital-automation-back { display: inline-flex; align-items: center; gap: 2px; height: 30px; margin-left: -8px; padding: 0 8px; border: 0; border-radius: 8px; background: transparent; color: var(--ca-muted); font: inherit; font-size: 12.5px; cursor: pointer; }
464
+ .capital-automation-back:hover { background: var(--ca-hover); color: var(--dsw-alias-label-primary); }
465
+ .capital-automation-hero { display: flex; align-items: flex-start; gap: 14px; padding: 2px 0 18px; border-bottom: .5px solid var(--ca-line); }
466
+ .capital-automation-herobody { flex: 1 1 auto; min-width: 0; }
467
+ .capital-automation-herotitle { display: flex; flex-wrap: wrap; align-items: center; gap: 8px; }
468
+ .capital-automation-herotitle h2 { margin: 0; font-size: 22px; line-height: 30px; font-weight: 600; letter-spacing: .1px; overflow-wrap: anywhere; }
469
+ .capital-automation-herodesc { max-width: 760px; margin: 5px 0 0; color: var(--ca-muted); font-size: 12.5px; line-height: 19px; }
470
+ .capital-automation-herometa { display: flex; flex-wrap: wrap; align-items: center; gap: 5px 14px; margin-top: 10px; color: var(--ca-muted); font-size: 11.5px; line-height: 18px; }
471
+ .capital-automation-herometa > span { display: inline-flex; align-items: center; gap: 5px; }
472
+ .capital-automation-heroactions { display: flex; flex-wrap: wrap; align-items: center; gap: 7px; flex: none; padding-top: 2px; }
473
+ .capital-automation-detailtabs { display: flex; align-items: center; gap: 26px; overflow-x: auto; overflow-y: hidden; border-bottom: .5px solid var(--ca-line); }
474
+ .capital-automation-detailtab { position: relative; flex: none; height: 42px; padding: 0 1px; border: 0; background: transparent; color: var(--ca-muted); font: inherit; font-size: 12.5px; font-weight: 500; cursor: pointer; }
475
+ .capital-automation-detailtab:hover { color: var(--dsw-alias-label-primary); }
476
+ .capital-automation-detailtab[aria-selected="true"] { color: var(--ca-business-text); font-weight: 600; }
477
+ .capital-automation-detailtab[aria-selected="true"]:after { content: ""; position: absolute; right: 0; bottom: -1px; left: 0; height: 2px; border-radius: 2px; background: var(--ca-business-text); }
478
+ .capital-automation-detailtab:focus-visible { outline: 2px solid var(--dsw-focus-ring-color, var(--ca-business)); outline-offset: -2px; border-radius: 4px; }
479
+ .capital-automation-detailbody { display: flex; flex-direction: column; gap: 16px; padding-top: 18px; }
480
+ .capital-automation-overview { display: flex; flex-direction: column; gap: 16px; }
481
+ .capital-automation-statgrid { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 10px; }
482
+ .capital-automation-stat { display: flex; flex-direction: column; gap: 4px; min-height: 78px; padding: 13px 14px; border: .5px solid var(--ca-line); border-radius: 14px; background: var(--ca-card); }
483
+ body:not([data-ds-dark-theme]) .capital-automation-card, body:not([data-ds-dark-theme]) .capital-automation-table, body:not([data-ds-dark-theme]) .capital-automation-template, body:not([data-ds-dark-theme]) .capital-automation-stat { box-shadow: 0 1px 2px rgba(15,17,21,.035), 0 10px 28px rgba(15,17,21,.025); }
484
+ .capital-automation-runmeta, .capital-automation-statvalue, .capital-automation-notificationtime { font-variant-numeric: tabular-nums; }
485
+ .capital-automation-statlabel { color: var(--ca-muted); font-size: 11px; line-height: 16px; }
486
+ .capital-automation-statvalue { color: var(--dsw-alias-label-primary); font-size: 15px; line-height: 22px; font-weight: 500; overflow-wrap: anywhere; }
487
+ .capital-automation-statvalue[data-tone="ok"] { color: var(--ca-success-text); }
488
+ .capital-automation-statvalue[data-tone="error"] { color: var(--ca-error-text); }
489
+ .capital-automation-statvalue[data-tone="running"] { color: var(--ca-business-text); }
490
+ .capital-automation-statsub { color: var(--ca-caption); font-size: 11px; line-height: 16px; }
491
+ .capital-automation-panel { padding: 16px 18px; }
492
+ .capital-automation-panelhead { display: flex; flex-wrap: wrap; align-items: center; justify-content: space-between; gap: 8px 12px; margin-bottom: 12px; }
493
+ .capital-automation-panelhead h3 { margin: 0; font-size: 13.5px; line-height: 20px; font-weight: 600; }
494
+ .capital-automation-panelhead .capital-automation-btn { margin-left: auto; }
495
+ .capital-automation-kv { display: grid; grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); gap: 12px 18px; }
496
+ .capital-automation-kvitem { min-width: 0; }
497
+ .capital-automation-kvlabel { display: block; color: var(--ca-caption); font-size: 11px; line-height: 16px; }
498
+ .capital-automation-kvvalue { display: block; margin-top: 2px; color: var(--dsw-alias-label-secondary); font-size: 12.5px; line-height: 19px; overflow-wrap: anywhere; }
499
+ .capital-automation-policy { color: var(--ca-muted); font-size: 12px; line-height: 19px; }
500
+ .capital-automation-policy p { margin: 0; }
501
+ .capital-automation-policy p + p { margin-top: 5px; }
502
+ .capital-automation-hint { margin: 0; color: var(--ca-muted); font-size: 11.5px; line-height: 18px; }
503
+ .capital-automation-hint[data-tone="error"] { color: var(--ca-error-text); }
504
+ .capital-automation-notice { margin: 0; color: var(--ca-success-text); font-size: 12px; line-height: 18px; }
505
+ .capital-automation-error { margin: 0; color: var(--ca-error-text); font-size: 12px; line-height: 18px; }
506
+ .capital-automation-toast { position: fixed; bottom: 26px; left: 50%; z-index: 1300; max-width: min(520px, calc(100vw - 40px)); transform: translateX(-50%); padding: 8px 15px; border: .5px solid var(--ca-line-soft); border-radius: 999px; background: var(--dsw-alias-label-primary); color: var(--dsw-alias-bg-base); box-shadow: 0 8px 26px rgba(0,0,0,.22); font-size: 12px; line-height: 18px; }
507
+ .capital-automation-form { display: flex; flex-direction: column; gap: 16px; }
508
+ .capital-automation-formgrid { display: grid; grid-template-columns: repeat(auto-fit, minmax(170px, 1fr)); gap: 12px; }
509
+ .capital-automation-field { display: flex; flex-direction: column; gap: 5px; min-width: 0; }
510
+ .capital-automation-field > span { color: var(--ca-muted); font-size: 11.5px; line-height: 17px; }
511
+ .capital-automation-field input, .capital-automation-field select, .capital-automation-field textarea { box-sizing: border-box; width: 100%; padding: 0 10px; border: .5px solid var(--ca-line-soft); border-radius: 10px; background: var(--ca-field); color: var(--dsw-alias-label-primary); font: inherit; font-size: 12.5px; line-height: 32px; }
512
+ .capital-automation-field input, .capital-automation-field select { height: 34px; }
513
+ .capital-automation-field textarea { min-height: 110px; padding: 9px 10px; line-height: 19px; resize: vertical; }
514
+ .capital-automation-field input:hover, .capital-automation-field select:hover, .capital-automation-field textarea:hover { border-color: var(--ca-line); }
515
+ .capital-automation-field input:focus-visible, .capital-automation-field select:focus-visible, .capital-automation-field textarea:focus-visible { border-color: var(--ca-business); outline: 2px solid color-mix(in srgb, var(--ca-business) 22%, transparent); outline-offset: 0; }
516
+ .capital-automation-field input:disabled, .capital-automation-field select:disabled, .capital-automation-field textarea:disabled { color: var(--ca-muted); opacity: .8; }
517
+ .capital-automation-formactions { display: flex; flex-wrap: wrap; align-items: center; justify-content: flex-end; gap: 8px; padding-top: 14px; border-top: .5px solid var(--ca-line); }
518
+ .capital-automation-formactions[data-sticky="true"] { position: sticky; bottom: -1px; z-index: 2; margin-top: 2px; padding: 12px 0 4px; background: var(--dsw-alias-bg-base); }
519
+ .capital-automation-formstatus { margin-right: auto; color: var(--ca-caption); font-size: 11.5px; line-height: 18px; }
520
+ .capital-automation-triggerbox { display: flex; flex-direction: column; gap: 12px; padding: 13px 14px; border: .5px solid var(--ca-line); border-radius: 14px; background: var(--ca-field); }
521
+ .capital-automation-triggerrow { display: flex; flex-wrap: wrap; align-items: center; gap: 8px; }
522
+ .capital-automation-triggerclock { display: inline-grid; place-items: center; width: 32px; height: 32px; border-radius: 10px; background: linear-gradient(135deg, #5b9dff, #2563eb); color: #fff; flex: none; }
523
+ .capital-automation-triggerrow label { display: inline-flex; align-items: center; gap: 6px; color: var(--ca-muted); font-size: 12px; }
524
+ .capital-automation-triggerrow input, .capital-automation-triggerrow select { box-sizing: border-box; height: 32px; padding: 0 9px; border: .5px solid var(--ca-line-soft); border-radius: 9px; background: var(--ca-card); color: var(--dsw-alias-label-primary); font: inherit; font-size: 12px; }
525
+ .capital-automation-triggerrow input[type="time"] { width: 112px; }
526
+ .capital-automation-triggerrow select { max-width: 190px; }
527
+ .capital-automation-weekdays { display: flex; flex-wrap: wrap; gap: 4px; }
528
+ .capital-automation-weekday { width: 30px; height: 30px; padding: 0; border: .5px solid var(--ca-line-soft); border-radius: 999px; background: transparent; color: var(--ca-muted); font: inherit; font-size: 11.5px; cursor: pointer; }
529
+ .capital-automation-weekday:hover:not(:disabled) { border-color: var(--ca-line); color: var(--dsw-alias-label-primary); }
530
+ .capital-automation-weekday[aria-pressed="true"] { border-color: transparent; background: var(--dsw-alias-label-primary); color: var(--dsw-alias-bg-base); }
531
+ .capital-automation-weekday:disabled { opacity: .45; cursor: not-allowed; }
532
+ .capital-automation-switch { display: inline-flex; align-items: center; gap: 7px; color: var(--ca-muted); font-size: 12px; }
533
+ .capital-automation-triggersummary { margin: 0; color: var(--ca-caption); font-size: 11.5px; line-height: 18px; }
534
+ .capital-automation-advanced { border-top: .5px solid var(--ca-line); padding-top: 12px; }
535
+ .capital-automation-advanced summary { color: var(--ca-muted); font-size: 12px; line-height: 18px; cursor: pointer; user-select: none; }
536
+ .capital-automation-advanced summary:hover { color: var(--dsw-alias-label-primary); }
537
+ .capital-automation-advancedbody { display: flex; flex-direction: column; gap: 12px; padding-top: 12px; }
538
+ .capital-automation-switchlist { display: flex; flex-direction: column; }
539
+ .capital-automation-switchrow { display: flex; align-items: center; justify-content: space-between; gap: 12px; min-height: 38px; padding: 3px 0; border-bottom: .5px solid var(--ca-line); }
540
+ .capital-automation-switchrow:last-child { border-bottom: 0; }
541
+ .capital-automation-switchrow span { color: var(--dsw-alias-label-secondary); font-size: 12px; line-height: 18px; }
542
+ .capital-automation-switchrow small { display: block; margin-top: 1px; color: var(--ca-caption); font-size: 11px; line-height: 16px; }
543
+ .capital-automation-runs { display: flex; flex-direction: column; }
544
+ .capital-automation-run { padding: 13px 0; border-bottom: .5px solid var(--ca-line); }
545
+ .capital-automation-run:first-child { padding-top: 0; }
546
+ .capital-automation-run:last-child { padding-bottom: 0; border-bottom: 0; }
547
+ .capital-automation-runhead { display: grid; grid-template-columns: minmax(0, 1fr) auto; align-items: center; gap: 8px 16px; min-width: 0; }
548
+ .capital-automation-runmain { min-width: 0; }
549
+ .capital-automation-runline { display: flex; flex-wrap: wrap; align-items: center; gap: 6px 8px; min-width: 0; }
550
+ .capital-automation-runtitle { color: var(--dsw-alias-label-primary); font-size: 12.5px; line-height: 20px; font-weight: 500; overflow-wrap: anywhere; }
551
+ .capital-automation-runmaintitle { padding: 0; border: 0; background: transparent; color: inherit; font: inherit; text-align: left; cursor: pointer; }
552
+ .capital-automation-runmaintitle:hover { color: var(--ca-business-text); }
553
+ .capital-automation-runmetaline { display: flex; flex-wrap: wrap; align-items: center; gap: 4px 14px; margin-top: 4px; }
554
+ .capital-automation-runmeta { color: var(--ca-caption); font-size: 11px; line-height: 17px; }
555
+ .capital-automation-runactions { display: flex; align-items: center; gap: 8px; flex: none; }
556
+ .capital-automation-runstatus { display: inline-flex; align-items: center; gap: 5px; color: var(--ca-muted); font-size: 11.5px; font-weight: 500; line-height: 18px; }
557
+ .capital-automation-runstatus[data-status="succeeded"] { color: var(--ca-success-text); }
558
+ .capital-automation-runstatus[data-status="failed"], .capital-automation-runstatus[data-status="timed_out"], .capital-automation-runstatus[data-status="blocked_requires_user"], .capital-automation-runstatus[data-status="unknown_after_restart"] { color: var(--ca-error-text); }
559
+ .capital-automation-runstatus[data-status="queued"], .capital-automation-runstatus[data-status="starting"], .capital-automation-runstatus[data-status="running"] { color: var(--ca-business-text); }
560
+ .capital-automation-runstatus[data-status="skipped_overlap"], .capital-automation-runstatus[data-status="skipped_calendar_unavailable"], .capital-automation-runstatus[data-status="missed"] { color: var(--ca-warn-text); }
561
+ .capital-automation-rundot { width: 5px; height: 5px; border-radius: 50%; background: currentColor; flex: none; }
562
+ .capital-automation-runspacer { flex: 1 1 auto; }
563
+ .capital-automation-runerrorbadge { box-sizing: border-box; display: inline-flex; align-items: center; gap: 4px; height: 20px; padding: 0 8px; border: .5px solid color-mix(in srgb, currentColor 38%, var(--ca-line)); border-radius: 999px; background: color-mix(in srgb, currentColor 8%, var(--ca-card)); color: var(--ca-warn-text); font: inherit; font-size: 11px; font-weight: 500; line-height: 18px; white-space: nowrap; cursor: pointer; }
564
+ .capital-automation-runerrorbadge:hover { background: color-mix(in srgb, currentColor 14%, var(--ca-card)); }
565
+ .capital-automation-runerrorbadge[data-tone="error"] { color: var(--ca-error-text); }
566
+ .capital-automation-runerrorbadge svg:last-child { transition: transform .15s; }
567
+ .capital-automation-runerrorbadge[aria-expanded="true"] svg:last-child { transform: rotate(90deg); }
568
+ .capital-automation-runerrors { display: flex; flex-direction: column; gap: 6px; margin-top: 8px; padding: 9px 11px; border: .5px solid var(--ca-line); border-radius: 10px; background: var(--ca-field); }
569
+ .capital-automation-runerror { display: grid; grid-template-columns: 6px minmax(0, 1fr) auto; align-items: start; gap: 8px; color: var(--ca-muted); font-size: 11.5px; line-height: 17px; }
570
+ .capital-automation-runerrordot { width: 6px; height: 6px; margin-top: 5px; border-radius: 50%; background: var(--ca-warn); flex: none; }
571
+ .capital-automation-runerror[data-severity="error"] .capital-automation-runerrordot { background: var(--ca-error); }
572
+ .capital-automation-runerrormessage { min-width: 0; overflow-wrap: anywhere; }
573
+ .capital-automation-runerrorcode { max-width: 220px; overflow: hidden; color: var(--ca-caption); font-size: 10.5px; line-height: 16px; text-overflow: ellipsis; white-space: nowrap; }
574
+ .capital-automation-notification { display: flex; align-items: flex-start; gap: 10px; width: 100%; padding: 11px 0; border: 0; border-bottom: .5px solid var(--ca-line); background: transparent; color: inherit; font: inherit; text-align: left; }
575
+ .capital-automation-notification:last-child { border-bottom: 0; }
576
+ .capital-automation-notification[data-clickable="true"] { cursor: pointer; }
577
+ .capital-automation-notification[data-clickable="true"]:hover { color: var(--dsw-alias-label-primary); }
578
+ .capital-automation-notificationicon { display: inline-grid; place-items: center; width: 28px; height: 28px; margin-top: 1px; border-radius: 9px; background: var(--dsw-alias-bg-module-platform); color: var(--ca-muted); flex: none; }
579
+ .capital-automation-notificationicon[data-tone="error"] { color: var(--ca-error-text); }
580
+ .capital-automation-notificationicon[data-tone="warn"] { color: var(--ca-warn-text); }
581
+ .capital-automation-notificationbody { min-width: 0; }
582
+ .capital-automation-notificationtitle { display: flex; flex-wrap: wrap; align-items: center; gap: 6px; color: var(--dsw-alias-label-primary); font-size: 12.5px; line-height: 19px; font-weight: 500; }
583
+ .capital-automation-notificationmessage { display: -webkit-box; overflow: hidden; margin-top: 2px; color: var(--ca-muted); font-size: 11.5px; line-height: 18px; overflow-wrap: anywhere; -webkit-line-clamp: 3; -webkit-box-orient: vertical; }
584
+ .capital-automation-notificationtime { display: block; margin-top: 3px; color: var(--ca-caption); font-size: 11px; line-height: 16px; }
585
+ .capital-automation-modal { width: min(720px, calc(100vw - 32px)); max-width: min(720px, calc(100vw - 32px)); max-height: calc(100dvh - 40px); }
586
+ .capital-automation-modal-content { max-height: calc(100dvh - 128px); overflow: auto; }
587
+ .capital-automation-modal-narrow { width: min(480px, calc(100vw - 32px)); max-width: min(480px, calc(100vw - 32px)); }
588
+ .capital-automation-wizard { display: flex; flex-direction: column; gap: 14px; }
589
+ .capital-automation-modalheadline { display: flex; align-items: center; gap: 10px; }
590
+ .capital-automation-modaltitleinput { flex: 1 1 auto; min-width: 0; height: 42px; padding: 0 12px; border: .5px solid var(--ca-line-soft); border-radius: 12px; background: var(--dsw-alias-bg-module-platform); color: var(--dsw-alias-label-primary); font: inherit; font-size: 15px; font-weight: 500; }
591
+ .capital-automation-modaltitleinput:focus-visible { border-color: var(--ca-business); outline: 2px solid color-mix(in srgb, var(--ca-business) 22%, transparent); outline-offset: 0; }
592
+ .capital-automation-formlabel { display: block; margin-bottom: 6px; color: var(--ca-muted); font-size: 11.5px; line-height: 17px; }
593
+ .capital-automation-promptbox { overflow: hidden; border: .5px solid var(--ca-line); border-radius: 16px; background: var(--dsw-alias-bg-module-platform); }
594
+ .capital-automation-promptbox textarea { box-sizing: border-box; width: 100%; min-height: 120px; max-height: 320px; padding: 13px 14px; border: 0; outline: 0; background: transparent; color: var(--dsw-alias-label-primary); font: inherit; font-size: 13px; line-height: 21px; resize: vertical; }
595
+ .capital-automation-promptbox textarea::placeholder { color: var(--ca-caption); }
596
+ .capital-automation-prompttoolbar { display: flex; flex-wrap: wrap; align-items: center; gap: 8px; padding: 8px 10px; border-top: .5px solid var(--ca-line); }
597
+ .capital-automation-prompttoolbar label { display: inline-flex; align-items: center; gap: 5px; color: var(--ca-muted); font-size: 11.5px; }
598
+ .capital-automation-prompttoolbar select { max-width: 168px; height: 28px; padding: 0 8px; border: .5px solid var(--ca-line-soft); border-radius: 8px; background: var(--ca-card); color: var(--dsw-alias-label-primary); font: inherit; font-size: 11.5px; }
599
+ .capital-automation-prompttoolbar .capital-automation-presetchip { margin-left: auto; }
600
+ .capital-automation-presetchip { display: inline-flex; align-items: center; gap: 5px; height: 26px; padding: 0 9px; border-radius: 999px; background: color-mix(in srgb, var(--ca-business) 12%, var(--ca-card)); color: var(--ca-business-text); font-size: 11px; font-weight: 500; white-space: nowrap; }
601
+ .capital-automation-checklist { display: flex; flex-direction: column; gap: 2px; }
602
+ .capital-automation-technical { margin-top: 8px; color: var(--ca-caption); font-size: 11px; line-height: 17px; }
603
+ .capital-automation-technical summary { cursor: pointer; }
604
+ .capital-automation-technical dl { display: grid; grid-template-columns: auto 1fr; gap: 3px 10px; margin: 7px 0 0; }
605
+ .capital-automation-technical dt { color: var(--ca-caption); }
606
+ .capital-automation-technical dd { min-width: 0; margin: 0; overflow-wrap: anywhere; color: var(--ca-muted); }
607
+ .capital-automation-page :focus-visible { outline: 2px solid var(--dsw-focus-ring-color, var(--ca-business)); outline-offset: 2px; }
608
+ .capital-automation-trowmain:focus-visible, .capital-automation-template .capital-automation-btn:focus-visible { outline-offset: 2px; }
609
+ @media (max-width: 1000px) {
610
+ .capital-automation-theadmain, .capital-automation-trowmain { grid-template-columns: minmax(0, 1.7fr) minmax(110px, .9fr) 88px; }
611
+ .capital-automation-theadmain > :nth-child(3), .capital-automation-cell[data-cell="next"] { display: none; }
612
+ .capital-automation-statgrid { grid-template-columns: repeat(2, minmax(0, 1fr)); }
613
+ }
614
+ @media (max-width: 760px) {
615
+ .capital-automation-head { padding-top: 14px; }
616
+ .capital-automation-topbar { display: flex; flex-wrap: wrap; gap: 10px; }
617
+ .capital-automation-brand { flex: 1 1 auto; }
618
+ .capital-automation-tabs { order: 3; width: 100%; max-width: 320px; margin: 0 auto; justify-content: center; }
619
+ .capital-automation-topactions { margin-left: auto; }
620
+ .capital-automation-theadmain, .capital-automation-trowmain { grid-template-columns: minmax(0, 1fr) 80px; gap: 10px; }
621
+ .capital-automation-theadmain > :nth-child(2), .capital-automation-theadmain > :nth-child(3), .capital-automation-cell[data-cell="schedule"], .capital-automation-cell[data-cell="next"] { display: none; }
622
+ .capital-automation-tasktitlerow .capital-automation-tasktags { gap: 3px; }
623
+ .capital-automation-tasktitlerow .capital-automation-tag { padding: 0 5px; font-size: 10px; line-height: 16px; }
624
+ .capital-automation-hero { flex-wrap: wrap; }
625
+ .capital-automation-heroactions { width: 100%; padding-left: 58px; }
626
+ .capital-automation-runhead { grid-template-columns: minmax(0, 1fr); }
627
+ .capital-automation-runactions { justify-content: flex-start; }
628
+ .capital-automation-templates { grid-template-columns: 1fr; }
629
+ .capital-automation-sectiontools { width: 100%; margin-left: 0; }
630
+ }
631
+ @media (max-width: 520px) {
632
+ .capital-automation-brand h1 { font-size: 16px; }
633
+ .capital-automation-topactions .capital-automation-btn:not([data-variant="contrast"]) { width: auto; min-width: 32px; padding: 0 8px; }
634
+ .capital-automation-topactions .capital-automation-btn[data-variant="contrast"] { width: 32px; padding: 0; }
635
+ .capital-automation-topactions .capital-automation-btn .capital-automation-btnlabel { display: none; }
636
+ .capital-automation-statgrid { grid-template-columns: 1fr; }
637
+ .capital-automation-heroactions { padding-left: 0; }
638
+ .capital-automation-modal-content { max-height: calc(100dvh - 112px); }
639
+ }
640
+ `
641
+ document.head.appendChild(style)
642
+ return () => style.remove()
643
+ }
644
+
645
+ function createApi() {
646
+ async function request(path, options = {}) {
647
+ const controller = typeof AbortController === 'function' ? new AbortController() : undefined
648
+ const timeout = setTimeout(() => controller?.abort(), 20_000)
649
+ let response
650
+ let payload
651
+ try {
652
+ response = await fetch(`${ROUTE}${path}`, {
653
+ method: options.method ?? 'GET',
654
+ credentials: 'same-origin',
655
+ headers: options.body === undefined ? {} : { 'content-type': 'application/json' },
656
+ body: options.body === undefined ? undefined : JSON.stringify(options.body),
657
+ ...(controller === undefined ? {} : { signal: controller.signal }),
658
+ })
659
+ try {
660
+ payload = await response.json()
661
+ } catch {
662
+ throw new Error(zh.errorGeneric)
663
+ }
664
+ } finally {
665
+ clearTimeout(timeout)
666
+ }
667
+ if (!response.ok || payload?.ok === false) {
668
+ const error = new Error(payload?.message ?? zh.errorGeneric)
669
+ error.code = payload?.code
670
+ throw error
671
+ }
672
+ return payload
673
+ }
674
+ return {
675
+ meta: () => request('/meta'),
676
+ tasks: () => request('/tasks'),
677
+ detail: (id) => request(`/tasks/${encodeURIComponent(id)}`),
678
+ create: (body) => request('/tasks', { method: 'POST', body }),
679
+ patch: (id, body) => request(`/tasks/${encodeURIComponent(id)}`, { method: 'PATCH', body }),
680
+ remove: (id) => request(`/tasks/${encodeURIComponent(id)}`, { method: 'DELETE' }),
681
+ action: (id, action) => request(`/tasks/${encodeURIComponent(id)}/${action}`, { method: 'POST' }),
682
+ run: (id) => request(`/tasks/${encodeURIComponent(id)}/run`, { method: 'POST' }),
683
+ cancel: (id) => request(`/runs/${encodeURIComponent(id)}/cancel`, { method: 'POST' }),
684
+ runs: (query = '') => request(`/runs${query}`),
685
+ notifications: () => request('/notifications'),
686
+ markRead: (body) => request('/notifications/read', { method: 'POST', body }),
687
+ }
688
+ }
689
+
690
+ function statusLabel(status) {
691
+ return zh.status[status] ?? status
692
+ }
693
+
694
+ function stateLabel(state) {
695
+ return zh.state[state] ?? state
696
+ }
697
+
698
+ function activationLabel(task) {
699
+ if (task?.state === 'active') return zh.pause
700
+ return task?.state === 'dormant' ? zh.add : zh.activate
701
+ }
702
+
703
+ function activationIcon(task) {
704
+ if (task?.state === 'active') return IconPauseOutlineRegular
705
+ return task?.state === 'dormant' ? IconPlusOutlineRegular : IconPlayOutlineRegular
706
+ }
707
+
708
+ function activationNotice(task) {
709
+ return (task?.state === 'dormant' ? zh.added : zh.activated).replace('{title}', task?.title ?? '')
710
+ }
711
+
712
+ function categoryLabel(category) {
713
+ return zh.category[category] ?? category
714
+ }
715
+
716
+ function clipTagLabel(label, limit = 12) {
717
+ const text = String(label ?? '')
718
+ const chars = Array.from(text)
719
+ return chars.length > limit ? `${chars.slice(0, limit).join('')}…` : text
720
+ }
721
+
722
+ function modeLabel(mode) {
723
+ return zh.mode[mode] ?? mode
724
+ }
725
+
726
+ function calendarLabel(calendar) {
727
+ if (calendar === null || calendar === undefined || calendar.available === false) return zh.calendarSourceNone
728
+ const source = calendar.source === 'szse'
729
+ ? zh.calendarSourceSzse
730
+ : calendar.source === 'bundled' ? zh.calendarSourceBundled : zh.calendarSourceNone
731
+ const through = calendar.through ?? zh.calendarUnknown
732
+ return `${source} · ${zh.calendarThrough.replace('{date}', through)}`
733
+ }
734
+
735
+ function isActiveRun(run) {
736
+ return run !== null && run !== undefined && ACTIVE_RUN_STATUSES.includes(run.status)
737
+ }
738
+
739
+ function isFailureStatus(status) {
740
+ return FAILURE_RUN_STATUSES.includes(status)
741
+ }
742
+
743
+ function toneOf(task) {
744
+ return task?.origin === 'custom' ? 'custom' : (task?.category ?? 'news')
745
+ }
746
+
747
+ function taskGlyph(task) {
748
+ if (task?.origin === 'custom') return IconSparkleRegular
749
+ if (task?.sourceBuiltinId === 'morning-brief') return IconLightOutlineMedium
750
+ if (task?.category === 'market') return IconGaugeOutlineRegular
751
+ if (task?.category === 'review') return IconListPenOutlineRegular
752
+ if (task?.category === 'research') return IconSearchOutlineRegular
753
+ return IconGlobeOutlineRegular
754
+ }
755
+
756
+ function Glyph(props) {
757
+ const { task, size = 34, large = false } = props
758
+ const GlyphIcon = taskGlyph(task)
759
+ const edge = large ? 44 : size
760
+ return h('span', {
761
+ className: 'capital-automation-glyph',
762
+ 'data-tone': toneOf(task),
763
+ style: { width: edge, height: edge, borderRadius: large ? 13 : 10 },
764
+ }, h(GlyphIcon, { size: large ? 22 : 17 }))
765
+ }
766
+
767
+ function StateBadge(props) {
768
+ const { state, running } = props
769
+ const label = running ? zh.status.running : stateLabel(state)
770
+ const value = running ? 'running' : state
771
+ return h('span', { className: 'capital-automation-state', 'data-state': value },
772
+ h('span', { className: 'capital-automation-statedot' }),
773
+ label)
774
+ }
775
+
776
+ function formatTime(value, options = {}) {
777
+ if (typeof value !== 'string' || value.length === 0) return zh.never
778
+ const date = new Date(value)
779
+ if (Number.isNaN(date.getTime())) return value
780
+ return new Intl.DateTimeFormat('zh-CN', {
781
+ year: options.short === true ? undefined : 'numeric',
782
+ month: '2-digit',
783
+ day: '2-digit',
784
+ hour: '2-digit',
785
+ minute: '2-digit',
786
+ hourCycle: 'h23',
787
+ ...(typeof options.timeZone === 'string' && options.timeZone.length > 0 ? { timeZone: options.timeZone } : {}),
788
+ }).format(date)
789
+ }
790
+
791
+ function formatShortTime(value, timeZone) {
792
+ return formatTime(value, { short: true, timeZone })
793
+ }
794
+
795
+ function formatDuration(ms) {
796
+ if (typeof ms !== 'number' || ms < 0) return zh.never
797
+ const seconds = Math.round(ms / 1000)
798
+ if (seconds < 60) return `${seconds}s`
799
+ const minutes = Math.floor(seconds / 60)
800
+ if (minutes < 60) return `${minutes}m ${seconds % 60}s`
801
+ return `${Math.floor(minutes / 60)}h ${minutes % 60}m`
802
+ }
803
+
804
+ function formatRelative(value) {
805
+ if (typeof value !== 'string' || value.length === 0) return ''
806
+ const target = Date.parse(value)
807
+ if (Number.isNaN(target)) return ''
808
+ const diff = target - Date.now()
809
+ const abs = Math.abs(diff)
810
+ if (abs < 45_000) return zh.relativeNow
811
+ const units = [
812
+ [86_400_000, 'day'],
813
+ [3_600_000, 'hour'],
814
+ [60_000, 'minute'],
815
+ ]
816
+ const formatter = new Intl.RelativeTimeFormat('zh-CN', { numeric: 'auto' })
817
+ for (const [unitMs, unit] of units) {
818
+ if (abs >= unitMs || unit === 'minute') return formatter.format(Math.round(diff / unitMs), unit)
819
+ }
820
+ return ''
821
+ }
822
+
823
+ function formatRangeLabel(date) {
824
+ return new Intl.DateTimeFormat('zh-CN', { month: 'short', day: 'numeric' }).format(date)
825
+ }
826
+
827
+ function scheduleSummary(trigger) {
828
+ if (trigger === null || trigger === undefined) return zh.never
829
+ const suffix = trigger.tradingDaysOnly === true ? zh.tradingSuffix : ''
830
+ if (trigger.cadence === 'weekly') {
831
+ const days = (trigger.weekdays ?? []).slice().sort((left, right) => left - right).map((day) => WEEKDAYS.find((item) => item.value === day)?.label ?? '').join('、')
832
+ return `${zh.weeklyPrefix}${days} ${trigger.time}${suffix}`
833
+ }
834
+ if (trigger.cadence === 'monthly') {
835
+ return `${zh.monthlyPrefix} ${trigger.dayOfMonth ?? 1} 日 ${trigger.time}${suffix}`
836
+ }
837
+ return `${zh.dailyPrefix} ${trigger.time}${suffix}`
838
+ }
839
+
840
+ function scheduleRangeSummary(trigger) {
841
+ if (trigger?.cadence === 'weekly') {
842
+ const days = (trigger.weekdays ?? []).slice().sort((left, right) => left - right).map((day) => WEEKDAYS.find((item) => item.value === day)?.label ?? '').join('、')
843
+ return `每周 ${days}`
844
+ }
845
+ if (trigger?.cadence === 'monthly') return `每月 ${trigger.dayOfMonth ?? 1} 日`
846
+ return '每天'
847
+ }
848
+
849
+ function workspaceOptions(meta, current) {
850
+ const options = [{ id: '', title: zh.workspaceDefault }]
851
+ const list = Array.isArray(meta?.workspaces) ? meta.workspaces : []
852
+ for (const item of list) {
853
+ if (typeof item?.id !== 'string' || item.id.length === 0) continue
854
+ options.push({ id: item.id, title: item.default === true ? `${item.title}(默认)` : item.title })
855
+ }
856
+ if (typeof current === 'string' && current.length > 0 && !options.some((item) => item.id === current)) {
857
+ options.push({ id: current, title: zh.workspaceMissing })
858
+ }
859
+ return options
860
+ }
861
+
862
+ function workspaceTitle(meta, id) {
863
+ if (typeof id !== 'string' || id.length === 0) return zh.workspaceDefault
864
+ return workspaceOptions(meta, id).find((item) => item.id === id)?.title ?? zh.workspaceMissing
865
+ }
866
+
867
+ function normalizeTrigger(trigger) {
868
+ const next = {
869
+ kind: 'schedule',
870
+ cadence: trigger?.cadence ?? 'daily',
871
+ time: trigger?.time ?? '09:00',
872
+ timezone: trigger?.timezone ?? 'Asia/Shanghai',
873
+ tradingDaysOnly: trigger?.tradingDaysOnly === true,
874
+ }
875
+ if (next.cadence === 'weekly') next.weekdays = (trigger?.weekdays ?? [1, 3, 5]).slice().sort((left, right) => left - right)
876
+ if (next.cadence === 'monthly') next.dayOfMonth = trigger?.dayOfMonth ?? 1
877
+ return next
878
+ }
879
+
880
+ function triggerChanged(cadence, trigger) {
881
+ const next = normalizeTrigger({ ...trigger, cadence })
882
+ if (cadence === 'weekly' && (next.weekdays ?? []).length === 0) next.weekdays = [1, 3, 5]
883
+ return next
884
+ }
885
+
886
+ function defaultDraft() {
887
+ return {
888
+ title: '我的投研任务',
889
+ description: '',
890
+ instruction: '',
891
+ category: 'research',
892
+ trigger: { kind: 'schedule', cadence: 'daily', time: '09:00', timezone: 'Asia/Shanghai', tradingDaysOnly: true },
893
+ mode: 'research',
894
+ workspaceId: '',
895
+ limits: { ...DEFAULT_LIMITS },
896
+ notifications: { inApp: true, onFailure: true, onSuccess: false },
897
+ }
898
+ }
899
+
900
+ function draftPayload(task) {
901
+ return {
902
+ title: String(task.title ?? '').trim(),
903
+ description: task.description ?? '',
904
+ instruction: task.instruction ?? '',
905
+ category: task.category,
906
+ trigger: normalizeTrigger(task.trigger),
907
+ mode: task.mode,
908
+ workspaceId: task.workspaceId ?? '',
909
+ limits: task.limits,
910
+ notifications: task.notifications,
911
+ }
912
+ }
913
+
914
+ function samePayload(left, right) {
915
+ return JSON.stringify(draftPayload(left)) === JSON.stringify(draftPayload(right))
916
+ }
917
+
918
+ function groupRunCounts(runs) {
919
+ const since = new Date()
920
+ since.setHours(0, 0, 0, 0)
921
+ since.setDate(since.getDate() - (HISTORY_DAYS - 1))
922
+ const days = []
923
+ const byKey = new Map()
924
+ for (let index = 0; index < HISTORY_DAYS; index += 1) {
925
+ const date = new Date(since)
926
+ date.setDate(since.getDate() + index)
927
+ const key = `${date.getFullYear()}-${date.getMonth()}-${date.getDate()}`
928
+ const bucket = { date, key, runs: [] }
929
+ days.push(bucket)
930
+ byKey.set(key, bucket)
931
+ }
932
+ let success = 0
933
+ let failure = 0
934
+ let total = 0
935
+ for (const run of runs) {
936
+ const date = new Date(run.queuedAt)
937
+ if (Number.isNaN(date.getTime()) || date < since) continue
938
+ if (run.status === 'succeeded') success += 1
939
+ if (isFailureStatus(run.status)) failure += 1
940
+ total += 1
941
+ const key = `${date.getFullYear()}-${date.getMonth()}-${date.getDate()}`
942
+ const bucket = byKey.get(key)
943
+ if (bucket !== undefined) bucket.runs.push(run)
944
+ }
945
+ return { days, success, failure, total }
946
+ }
947
+
948
+ function dayTone(bucket) {
949
+ if (bucket.runs.some((run) => isActiveRun(run))) return 'active'
950
+ if (bucket.runs.some((run) => isFailureStatus(run.status))) return 'error'
951
+ if (bucket.runs.some((run) => run.status === 'succeeded')) return 'ok'
952
+ if (bucket.runs.length > 0) return 'warn'
953
+ return null
954
+ }
955
+
956
+ function eventTone(kind) {
957
+ if (kind === 'run_failed' || kind === 'run_timeout') return 'error'
958
+ if (kind === 'scheduler_standby' || kind === 'calendar_unavailable') return 'warn'
959
+ return 'neutral'
960
+ }
961
+
962
+ function notificationMessage(item) {
963
+ if (typeof item.message === 'string' && item.message.length > 0) return item.message
964
+ if (item.kind === 'run_failed') return zh.notificationFallbackFailed
965
+ if (item.kind === 'run_timeout') return zh.notificationFallbackTimeout
966
+ if (item.kind === 'run_succeeded') return zh.notificationFallbackSuccess
967
+ if (item.kind === 'scheduler_standby') return zh.notificationFallbackStandby
968
+ if (item.kind === 'calendar_unavailable') return zh.notificationFallbackCalendar
969
+ if (item.kind === 'skipped') return zh.notificationFallbackSkipped
970
+ return ''
971
+ }
972
+
973
+ function draftChanged(draft, task) {
974
+ return !samePayload(draft, task)
975
+ }
976
+
977
+ function validateDraft(task) {
978
+ if (String(task.title ?? '').trim().length === 0 || String(task.instruction ?? '').trim().length === 0) return zh.required
979
+ if (!/^([01]\d|2[0-3]):[0-5]\d$/.test(task.trigger?.time ?? '')) return zh.invalidTime
980
+ if (String(task.trigger?.timezone ?? '').trim().length === 0) return zh.invalidTimezone
981
+ try {
982
+ new Intl.DateTimeFormat('zh-CN', { timeZone: task.trigger.timezone })
983
+ } catch {
984
+ return zh.invalidTimezoneValue
985
+ }
986
+ if (task.trigger?.cadence === 'weekly' && (task.trigger.weekdays ?? []).length === 0) return zh.weekdayRequired
987
+ const limits = task.limits ?? {}
988
+ if (![limits.runTimeoutMinutes, limits.idleTimeoutMinutes, limits.maxSteps, limits.maxToolCalls].every((value) => Number.isFinite(value))) return zh.invalidLimits
989
+ return ''
990
+ }
991
+
992
+ function limitValue(meta, key, fallback) {
993
+ const range = meta?.execution?.limits?.[key]
994
+ return Array.isArray(range) && range.length === 2 ? range : fallback
995
+ }
996
+
997
+ function RunHistoryCard(props) {
998
+ const { runs, onViewAll, showLink = true } = props
999
+ const { days, success, failure, total } = groupRunCounts(runs)
1000
+ return h('section', { className: 'capital-automation-card capital-automation-history', 'aria-label': zh.runHistory },
1001
+ h('div', { className: 'capital-automation-historyhead' },
1002
+ h('div', { className: 'capital-automation-historytitle' },
1003
+ h('h2', null, zh.runHistory),
1004
+ h('span', null, zh.last30Days)),
1005
+ h('div', { className: 'capital-automation-historystats' },
1006
+ h('span', { className: 'capital-automation-historystat', 'data-tone': 'ok' },
1007
+ h('span', { className: 'capital-automation-statusdot' }),
1008
+ zh.successCount.replace('{n}', String(success))),
1009
+ h('span', { className: 'capital-automation-historystat', 'data-tone': 'error' },
1010
+ h('span', { className: 'capital-automation-statusdot' }),
1011
+ zh.failureCount.replace('{n}', String(failure))),
1012
+ showLink && onViewAll !== undefined
1013
+ ? h('button', { type: 'button', className: 'capital-automation-historylink', onClick: onViewAll },
1014
+ zh.runCount.replace('{n}', String(total)),
1015
+ h(IconChevronRightOutlineRegular, { size: 13 }))
1016
+ : h('span', { className: 'capital-automation-historystat' }, zh.runCount.replace('{n}', String(total))))),
1017
+ h('div', { className: 'capital-automation-timeline', 'aria-hidden': 'true' },
1018
+ days.map((bucket) => {
1019
+ const tone = dayTone(bucket)
1020
+ return h('div', { key: bucket.key, className: 'capital-automation-timeline-day', title: `${formatRangeLabel(bucket.date)}:${bucket.runs.length} 次运行` },
1021
+ tone === null ? null : h('span', { className: 'capital-automation-timeline-dot', 'data-tone': tone }))
1022
+ })),
1023
+ h('div', { className: 'capital-automation-timelinelabels' },
1024
+ h('span', null, formatRangeLabel(days[0].date)),
1025
+ h('span', null, formatRangeLabel(days[days.length - 1].date))),
1026
+ total === 0 ? h('p', { className: 'capital-automation-historyempty' }, zh.historyEmpty) : null)
1027
+ }
1028
+
1029
+ function RunStatus(props) {
1030
+ const { status } = props
1031
+ return h('span', { className: 'capital-automation-runstatus', 'data-status': status },
1032
+ h('span', { className: 'capital-automation-rundot' }),
1033
+ statusLabel(status))
1034
+ }
1035
+
1036
+ function RunRow(props) {
1037
+ const { run, task, onCancel, onOpenSession, onOpenTask, busy } = props
1038
+ const active = isActiveRun(run)
1039
+ const errorCount = Array.isArray(run.errors) ? run.errors.length : 0
1040
+ const [errorsOpen, setErrorsOpen] = useState(false)
1041
+ const errorPanelId = `capital-automation-run-errors-${run.id}`
1042
+ return h('div', { className: 'capital-automation-run' },
1043
+ h('div', { className: 'capital-automation-runhead' },
1044
+ h('div', { className: 'capital-automation-runmain' },
1045
+ h('div', { className: 'capital-automation-runline' },
1046
+ task !== undefined && onOpenTask !== undefined
1047
+ ? h('button', { type: 'button', className: 'capital-automation-runtitle capital-automation-runmaintitle', onClick: () => onOpenTask(task.id) }, task.title)
1048
+ : h('span', { className: 'capital-automation-runtitle' }, run.taskTitle),
1049
+ h(RunStatus, { status: run.status })),
1050
+ h('div', { className: 'capital-automation-runmetaline' },
1051
+ h('span', { className: 'capital-automation-runmeta' }, `${formatShortTime(run.queuedAt, task?.trigger?.timezone)} · ${run.trigger === 'manual' ? zh.triggerManual : zh.triggerScheduled}`),
1052
+ typeof run.durationMs === 'number' ? h('span', { className: 'capital-automation-runmeta' }, `${zh.duration} ${formatDuration(run.durationMs)}`) : null,
1053
+ h('span', { className: 'capital-automation-runmeta' }, `${zh.steps} ${run.steps} · ${zh.tools} ${run.toolCalls}`))),
1054
+ h('div', { className: 'capital-automation-runactions' },
1055
+ errorCount > 0
1056
+ ? h('button', {
1057
+ type: 'button',
1058
+ className: 'capital-automation-runerrorbadge',
1059
+ 'data-tone': isFailureStatus(run.status) ? 'error' : 'warn',
1060
+ 'aria-expanded': errorsOpen ? 'true' : 'false',
1061
+ 'aria-controls': errorPanelId,
1062
+ 'aria-label': `${zh.runErrors.replace('{n}', String(errorCount))},${errorsOpen ? zh.hideErrorDetails : zh.showErrorDetails}`,
1063
+ title: errorsOpen ? zh.hideErrorDetails : zh.showErrorDetails,
1064
+ onClick: () => setErrorsOpen((current) => !current),
1065
+ },
1066
+ h(IconWarningOutlineRegular, { size: 12 }),
1067
+ zh.runErrors.replace('{n}', String(errorCount)),
1068
+ h(IconChevronRightOutlineRegular, { size: 12 }))
1069
+ : null,
1070
+ active && typeof onCancel === 'function'
1071
+ ? h('button', { type: 'button', className: 'capital-automation-btn', 'data-size': 'sm', 'data-variant': 'danger', disabled: busy, onClick: () => onCancel(run) }, zh.cancelRun)
1072
+ : run.sessionId && onOpenSession
1073
+ ? h('button', { type: 'button', className: 'capital-automation-btn', 'data-size': 'sm', onClick: () => onOpenSession(run.sessionId) }, zh.openSession)
1074
+ : null)),
1075
+ errorsOpen && errorCount > 0
1076
+ ? h('div', { id: errorPanelId, className: 'capital-automation-runerrors', role: 'region', 'aria-label': zh.errorDetailsLabel },
1077
+ run.errors.map((item, index) => h('div', { key: `${run.id}-error-${index}`, className: 'capital-automation-runerror', 'data-severity': item.severity },
1078
+ h('span', { className: 'capital-automation-runerrordot' }),
1079
+ h('span', { className: 'capital-automation-runerrormessage' }, item.message),
1080
+ item.code ? h('code', { className: 'capital-automation-runerrorcode', title: item.code }, item.code) : null)))
1081
+ : null)
1082
+ }
1083
+
1084
+ function RunList(props) {
1085
+ const { runs, tasks, onCancel, onOpenSession, onOpenTask, busy, compact } = props
1086
+ const taskMap = new Map(tasks.map((task) => [task.id, task]))
1087
+ if (runs.length === 0) {
1088
+ if (compact) return h('p', { className: 'capital-automation-hint' }, zh.detailNoRuns)
1089
+ return h('div', { className: 'capital-automation-empty' },
1090
+ h('span', { className: 'capital-automation-emptyicon' }, h(IconClockOutlineRegular, { size: 19 })),
1091
+ h('h3', null, zh.detailNoRuns),
1092
+ h('p', null, zh.runsEmptyBody))
1093
+ }
1094
+ return h('div', { className: 'capital-automation-runs' },
1095
+ runs.map((run) => h(RunRow, {
1096
+ key: run.id,
1097
+ run,
1098
+ task: taskMap.get(run.taskId),
1099
+ onCancel,
1100
+ onOpenSession,
1101
+ onOpenTask,
1102
+ busy,
1103
+ })))
1104
+ }
1105
+
1106
+ function NotificationsButton(props) {
1107
+ const { unread, onClick } = props
1108
+ return h('button', {
1109
+ type: 'button',
1110
+ className: 'capital-automation-btn',
1111
+ 'data-variant': 'ghost',
1112
+ onClick,
1113
+ title: zh.notificationsTitle,
1114
+ 'aria-label': zh.notificationsTitle,
1115
+ },
1116
+ h(IconAlarmClockOutlineRegular, { size: 14 }),
1117
+ h('span', { className: 'capital-automation-btnlabel' }, zh.notifications),
1118
+ unread > 0 ? h('span', { className: 'capital-automation-count' }, String(unread > 99 ? '99+' : unread)) : null)
1119
+ }
1120
+
1121
+ function Header(props) {
1122
+ const { tab, onTab, onNewTask, unread, onOpenInbox } = props
1123
+ return h('header', { className: 'capital-automation-head' },
1124
+ h('div', { className: 'capital-automation-headinner' },
1125
+ h('div', { className: 'capital-automation-topbar' },
1126
+ h('div', { className: 'capital-automation-brand' },
1127
+ h('span', { className: 'capital-automation-brandmark', 'aria-hidden': 'true' }, h(IconChecklistOutlineRegular, { size: 16 })),
1128
+ h('h1', null, zh.title)),
1129
+ h('div', { className: 'capital-automation-tabs', role: 'tablist', 'aria-label': zh.title },
1130
+ ['tasks', 'runs'].map((value) => h('button', {
1131
+ key: value,
1132
+ type: 'button',
1133
+ role: 'tab',
1134
+ className: 'capital-automation-tab',
1135
+ 'aria-selected': tab === value ? 'true' : 'false',
1136
+ onClick: () => onTab(value),
1137
+ }, zh.tabs[value]))),
1138
+ h('div', { className: 'capital-automation-topactions' },
1139
+ h(NotificationsButton, { unread, onClick: onOpenInbox }),
1140
+ h('button', { type: 'button', className: 'capital-automation-btn', 'data-variant': 'contrast', onClick: onNewTask, title: zh.newTask, 'aria-label': zh.newTask },
1141
+ h(IconPlusOutlineRegular, { size: 14 }),
1142
+ h('span', { className: 'capital-automation-btnlabel' }, zh.newTask))))))
1143
+ }
1144
+
1145
+ function PageBanners(props) {
1146
+ const { meta, loadError, onRetry } = props
1147
+ const banners = []
1148
+ if (meta?.storage?.state === 'unavailable') {
1149
+ banners.push(h('div', { key: 'storage', className: 'capital-automation-banner', 'data-tone': 'error', role: 'alert' },
1150
+ `${zh.storageBanner}${meta.storage.error ? `:${meta.storage.error}` : ''}`))
1151
+ }
1152
+ const schedulerState = meta?.scheduler?.state
1153
+ if (meta !== null && meta !== undefined && meta.storage?.state !== 'unavailable' && (schedulerState === 'standby' || schedulerState === 'stopped')) {
1154
+ banners.push(h('div', { key: 'standby', className: 'capital-automation-banner', 'data-tone': 'warn', role: 'status' }, zh.standbyBanner))
1155
+ }
1156
+ const calendarUnavailable = meta !== null && meta !== undefined && meta.calendar?.available === false
1157
+ if (meta !== null && meta !== undefined && meta.storage?.state !== 'unavailable' && calendarUnavailable) {
1158
+ banners.push(h('div', { key: 'calendar', className: 'capital-automation-banner', 'data-tone': 'warn', role: 'status' }, zh.calendarBanner))
1159
+ }
1160
+ const calendarCoverage = meta?.calendar?.coverageDays
1161
+ if (meta !== null && meta !== undefined && meta.storage?.state !== 'unavailable' && !calendarUnavailable
1162
+ && typeof calendarCoverage === 'number' && calendarCoverage >= 0 && calendarCoverage < 30) {
1163
+ banners.push(h('div', { key: 'calendar-coverage', className: 'capital-automation-banner', 'data-tone': 'warn', role: 'status' },
1164
+ zh.calendarCoverageBanner.replace('{through}', meta.calendar.through ?? zh.calendarUnknown)))
1165
+ }
1166
+ if (loadError.length > 0) {
1167
+ banners.push(h('div', { key: 'load', className: 'capital-automation-banner', 'data-tone': 'error', role: 'alert' },
1168
+ loadError,
1169
+ h('button', { type: 'button', className: 'capital-automation-btn', 'data-size': 'sm', onClick: onRetry }, zh.loadRetry)))
1170
+ }
1171
+ if (banners.length === 0) return null
1172
+ return h('div', { className: 'capital-automation-headinner', style: { paddingBottom: 0 } }, banners)
1173
+ }
1174
+
1175
+ function FilterButton(props) {
1176
+ const { active, count, label, onClick } = props
1177
+ return h('button', {
1178
+ type: 'button',
1179
+ className: 'capital-automation-filter',
1180
+ 'aria-pressed': active ? 'true' : 'false',
1181
+ onClick,
1182
+ },
1183
+ h('span', { className: 'capital-automation-filterlabel' }, label),
1184
+ count > 0 ? h('span', { className: 'capital-automation-filtercount' }, String(count)) : null)
1185
+ }
1186
+
1187
+ function TaskTags(props) {
1188
+ const { task } = props
1189
+ const tags = [[categoryLabel(task.category), toneOf(task)]]
1190
+ if (task.origin === 'builtin') tags.push([zh.origin.builtin, undefined])
1191
+ if (task.origin === 'custom') tags.push([zh.origin.custom, undefined])
1192
+ return h('span', { className: 'capital-automation-tasktags' },
1193
+ tags.map(([label, tone]) => h('span', {
1194
+ key: label,
1195
+ className: 'capital-automation-tag',
1196
+ 'data-tone': tone,
1197
+ title: label,
1198
+ }, clipTagLabel(label))))
1199
+ }
1200
+
1201
+ function TaskActionsMenu(props) {
1202
+ const { task, open, onToggle, onClose, onEdit, onAction, onRequestConfirm, canRun, running, busy, disabled, align = 'end' } = props
1203
+ const toggleId = task.state === 'active' ? 'pause' : 'activate'
1204
+ const blocked = busy || disabled === true
1205
+ const items = [
1206
+ { id: 'edit', label: zh.edit, icon: h(IconEditOutlineRegular, { size: 14 }), disabled: busy },
1207
+ { id: toggleId, label: activationLabel(task), icon: h(activationIcon(task), { size: 14 }), disabled: blocked },
1208
+ { id: 'run', label: zh.runNow, icon: h(IconPlayOutlineRegular, { size: 14 }), disabled: blocked || !canRun || running },
1209
+ ...(task.origin === 'builtin' ? [{ id: 'copy', label: zh.copy, icon: h(IconCopyOutlineRegular, { size: 14 }), disabled: blocked }] : []),
1210
+ ]
1211
+ const children = []
1212
+ if (task.origin === 'builtin' && task.state !== 'dormant') {
1213
+ children.push(h(MenuItemButton, {
1214
+ key: 'hibernate',
1215
+ separatorBefore: true,
1216
+ icon: h(IconRefreshOutlineRegular, { size: 14 }),
1217
+ disabled: blocked || running,
1218
+ onSelect: () => {
1219
+ onClose()
1220
+ onRequestConfirm('hibernate', task)
1221
+ },
1222
+ }, zh.hibernate))
1223
+ }
1224
+ if (task.origin === 'custom') {
1225
+ children.push(h(MenuItemButton, {
1226
+ key: 'remove',
1227
+ danger: true,
1228
+ separatorBefore: true,
1229
+ icon: h(IconTrashOutlineRegular, { size: 14 }),
1230
+ disabled: blocked || running,
1231
+ onSelect: () => {
1232
+ onClose()
1233
+ onRequestConfirm('delete', task)
1234
+ },
1235
+ }, zh.remove))
1236
+ }
1237
+ return h(Menu, {
1238
+ open,
1239
+ anchor: h('button', {
1240
+ type: 'button',
1241
+ className: 'capital-automation-iconbtn',
1242
+ disabled: busy,
1243
+ title: zh.more,
1244
+ 'aria-label': `${zh.more}:${task.title}`,
1245
+ 'aria-haspopup': 'menu',
1246
+ 'aria-expanded': open ? 'true' : 'false',
1247
+ onClick: onToggle,
1248
+ }, h(IconEllipsisOutlineRegular, { size: 16 })),
1249
+ items,
1250
+ children,
1251
+ onSelect: (id) => {
1252
+ onClose()
1253
+ if (id === 'edit') {
1254
+ onEdit(task.id)
1255
+ return
1256
+ }
1257
+ onAction(task, id)
1258
+ },
1259
+ onClose,
1260
+ align,
1261
+ portal: true,
1262
+ })
1263
+ }
1264
+
1265
+ function TaskRow(props) {
1266
+ const { task, onOpen, menuOpen, onMenuToggle, onMenuClose, onEdit, onAction, onRequestConfirm, canRun, busy, mutationsDisabled } = props
1267
+ const run = task.lastRun
1268
+ const running = isActiveRun(run)
1269
+ const nextLabel = task.nextRunLocal ?? (task.nextRunUnavailable ? zh.calendarUnavailable : zh.never)
1270
+ return h('div', { className: 'capital-automation-trow' },
1271
+ h('button', { type: 'button', className: 'capital-automation-trowmain', onClick: () => onOpen(task.id) },
1272
+ h('span', { className: 'capital-automation-cell capital-automation-identity' },
1273
+ h(Glyph, { task }),
1274
+ h('span', { className: 'capital-automation-identitytext' },
1275
+ h('span', { className: 'capital-automation-tasktitlerow' },
1276
+ h('span', { className: 'capital-automation-tasktitle' }, task.title),
1277
+ h(TaskTags, { task }),
1278
+ (task.unread ?? 0) > 0 ? h('span', { className: 'capital-automation-count', title: zh.unread }, String(task.unread)) : null),
1279
+ h('span', { className: 'capital-automation-taskdesc' }, task.description || zh.subtitle))),
1280
+ h('span', { className: 'capital-automation-cell', 'data-cell': 'schedule' },
1281
+ h('span', { className: 'capital-automation-schedule' }, scheduleSummary(task.trigger)),
1282
+ h('span', { className: 'capital-automation-schedulesub' }, task.trigger?.timezone ?? '')),
1283
+ h('span', { className: 'capital-automation-cell', 'data-cell': 'next' },
1284
+ h('span', { className: 'capital-automation-nextmain' }, nextLabel),
1285
+ h('span', { className: 'capital-automation-nextsub' }, task.nextRunAt ? formatRelative(task.nextRunAt) : (task.nextRunUnavailable ? zh.nextRunUnavailable : zh.never))),
1286
+ h('span', { className: 'capital-automation-cell' },
1287
+ h(StateBadge, { state: task.state, running }))),
1288
+ h('div', { className: 'capital-automation-trowmenu' },
1289
+ h(TaskActionsMenu, {
1290
+ task,
1291
+ open: menuOpen,
1292
+ onToggle: onMenuToggle,
1293
+ onClose: onMenuClose,
1294
+ onEdit,
1295
+ onAction,
1296
+ onRequestConfirm,
1297
+ canRun,
1298
+ running,
1299
+ busy,
1300
+ disabled: mutationsDisabled,
1301
+ })))
1302
+ }
1303
+
1304
+ function TaskTable(props) {
1305
+ const { tasks, onOpen, onAction, onRequestConfirm, onEdit, schedulerActive, storageReady, busyTaskId } = props
1306
+ const [menuTaskId, setMenuTaskId] = useState(null)
1307
+ const canRun = storageReady && schedulerActive
1308
+ return h('div', { className: 'capital-automation-table', 'aria-label': zh.taskSection },
1309
+ h('div', { className: 'capital-automation-thead' },
1310
+ h('div', { className: 'capital-automation-theadmain' },
1311
+ h('span', null, zh.taskSection),
1312
+ h('span', null, zh.triggerSummary),
1313
+ h('span', null, zh.nextRun),
1314
+ h('span', null, zh.stateLabel)),
1315
+ h('span', null)),
1316
+ tasks.map((task) => h(TaskRow, {
1317
+ key: task.id,
1318
+ task,
1319
+ onOpen,
1320
+ menuOpen: menuTaskId === task.id,
1321
+ onMenuToggle: () => setMenuTaskId((current) => current === task.id ? null : task.id),
1322
+ onMenuClose: () => setMenuTaskId(null),
1323
+ onEdit,
1324
+ onAction,
1325
+ onRequestConfirm,
1326
+ canRun,
1327
+ busy: busyTaskId === task.id,
1328
+ mutationsDisabled: !storageReady,
1329
+ })))
1330
+ }
1331
+
1332
+ function TemplateCard(props) {
1333
+ const { task, onOpen, onEnable, busy, disabled } = props
1334
+ return h('article', { className: 'capital-automation-template' },
1335
+ h('button', { type: 'button', className: 'capital-automation-templatemain', onClick: () => onOpen(task.id) },
1336
+ h(Glyph, { task, size: 38 }),
1337
+ h('span', { className: 'capital-automation-templatebody' },
1338
+ h('span', { className: 'capital-automation-templatetitlerow' },
1339
+ h('strong', null, task.title),
1340
+ h('span', {
1341
+ className: 'capital-automation-tag',
1342
+ 'data-tone': toneOf(task),
1343
+ title: categoryLabel(task.category),
1344
+ }, clipTagLabel(categoryLabel(task.category)))),
1345
+ h('span', { className: 'capital-automation-templatedesc' }, task.description || zh.subtitle))),
1346
+ h('div', { className: 'capital-automation-templatefoot' },
1347
+ h('span', { className: 'capital-automation-templatemeta' },
1348
+ h(IconClockOutlineRegular, { size: 12 }),
1349
+ `${zh.templateMeta} · ${scheduleSummary(task.trigger)}`),
1350
+ h('button', { type: 'button', className: 'capital-automation-btn', 'data-size': 'sm', 'data-variant': 'primary', onClick: () => onEnable(task), disabled: busy || disabled }, busy ? zh.templateAdding : zh.templateAdd)))
1351
+ }
1352
+
1353
+ function EmptyState(props) {
1354
+ const { title, body, action, actionLabel, icon } = props
1355
+ return h('div', { className: 'capital-automation-empty' },
1356
+ h('span', { className: 'capital-automation-emptyicon' }, h(icon ?? IconChecklistOutlineRegular, { size: 19 })),
1357
+ h('h3', null, title),
1358
+ h('p', null, body),
1359
+ action && actionLabel ? h('button', { type: 'button', className: 'capital-automation-btn', onClick: action }, actionLabel) : null)
1360
+ }
1361
+
1362
+ // Skeleton mirrors the real dashboard/runs layout so the first paint never claims "no tasks".
1363
+ function SkeletonBlock(props) {
1364
+ const { width = '100%', height = 10, radius = 6 } = props
1365
+ return h('span', {
1366
+ className: 'capital-automation-skeleton',
1367
+ style: { width, height, borderRadius: radius },
1368
+ 'aria-hidden': 'true',
1369
+ })
1370
+ }
1371
+
1372
+ function SkeletonHistory() {
1373
+ return h('section', { className: 'capital-automation-card capital-automation-history', 'aria-hidden': 'true' },
1374
+ h('div', { className: 'capital-automation-historyhead' },
1375
+ h('div', { className: 'capital-automation-historytitle' },
1376
+ h(SkeletonBlock, { width: 92, height: 14 }),
1377
+ h(SkeletonBlock, { width: 52, height: 11 })),
1378
+ h('div', { className: 'capital-automation-historystats' },
1379
+ h(SkeletonBlock, { width: 64, height: 12 }),
1380
+ h(SkeletonBlock, { width: 64, height: 12 }),
1381
+ h(SkeletonBlock, { width: 56, height: 12 }))),
1382
+ h('div', { className: 'capital-automation-skeletontimeline' },
1383
+ Array.from({ length: HISTORY_DAYS }, (_, index) => h(SkeletonBlock, {
1384
+ key: index,
1385
+ height: 6 + ((index * 11) % 22),
1386
+ radius: 2,
1387
+ }))),
1388
+ h('div', { className: 'capital-automation-timelinelabels' },
1389
+ h(SkeletonBlock, { width: 34, height: 9 }),
1390
+ h(SkeletonBlock, { width: 34, height: 9 })))
1391
+ }
1392
+
1393
+ function SkeletonSectionHead(props) {
1394
+ const { trailing = [210, 132] } = props
1395
+ return h('div', { className: 'capital-automation-sectionhead' },
1396
+ h(SkeletonBlock, { width: 78, height: 14 }),
1397
+ h(SkeletonBlock, { width: 210, height: 11 }),
1398
+ trailing.length === 0 ? null : h('div', { className: 'capital-automation-sectiontools' },
1399
+ trailing.map((width, index) => h(SkeletonBlock, { key: index, width, height: index === 0 ? 28 : 30, radius: 999 }))))
1400
+ }
1401
+
1402
+ function SkeletonTaskRow() {
1403
+ return h('div', { className: 'capital-automation-trow capital-automation-skeletonrow', 'aria-hidden': 'true' },
1404
+ h('div', { className: 'capital-automation-trowmain' },
1405
+ h('span', { className: 'capital-automation-cell capital-automation-identity' },
1406
+ h(SkeletonBlock, { width: 34, height: 34, radius: 10 }),
1407
+ h('span', { className: 'capital-automation-skeletonstack' },
1408
+ h(SkeletonBlock, { width: '52%', height: 12 }),
1409
+ h(SkeletonBlock, { width: '82%', height: 9 }),
1410
+ h(SkeletonBlock, { width: 92, height: 16, radius: 999 }))),
1411
+ h('span', { className: 'capital-automation-cell', 'data-cell': 'schedule' },
1412
+ h(SkeletonBlock, { width: '64%', height: 10 }),
1413
+ h(SkeletonBlock, { width: '42%', height: 8 })),
1414
+ h('span', { className: 'capital-automation-cell', 'data-cell': 'next' },
1415
+ h(SkeletonBlock, { width: '58%', height: 10 }),
1416
+ h(SkeletonBlock, { width: '36%', height: 8 })),
1417
+ h('span', { className: 'capital-automation-cell' },
1418
+ h(SkeletonBlock, { width: 52, height: 22, radius: 999 }))),
1419
+ h('div', { className: 'capital-automation-trowmenu' },
1420
+ h(SkeletonBlock, { width: 18, height: 18, radius: 6 })))
1421
+ }
1422
+
1423
+ function SkeletonTaskTable(props) {
1424
+ const { rows = 4 } = props
1425
+ return h('div', { className: 'capital-automation-table', 'aria-hidden': 'true' },
1426
+ h('div', { className: 'capital-automation-thead' },
1427
+ h('div', { className: 'capital-automation-theadmain' },
1428
+ h(SkeletonBlock, { width: '52%', height: 9 }),
1429
+ h(SkeletonBlock, { width: '58%', height: 9 }),
1430
+ h(SkeletonBlock, { width: '56%', height: 9 }),
1431
+ h(SkeletonBlock, { width: 44, height: 9 })),
1432
+ h('span', null)),
1433
+ Array.from({ length: rows }, (_, index) => h(SkeletonTaskRow, { key: index })))
1434
+ }
1435
+
1436
+ function SkeletonTemplateCard() {
1437
+ return h('div', { className: 'capital-automation-template capital-automation-skeletontemplate', 'aria-hidden': 'true' },
1438
+ h('div', { className: 'capital-automation-skeletontemplatemain' },
1439
+ h(SkeletonBlock, { width: 38, height: 38, radius: 11 }),
1440
+ h('div', { className: 'capital-automation-skeletonstack' },
1441
+ h(SkeletonBlock, { width: '58%', height: 11 }),
1442
+ h(SkeletonBlock, { width: '100%', height: 9 }),
1443
+ h(SkeletonBlock, { width: '76%', height: 9 }))),
1444
+ h('div', { className: 'capital-automation-templatefoot' },
1445
+ h(SkeletonBlock, { width: 118, height: 11 }),
1446
+ h(SkeletonBlock, { width: 64, height: 28, radius: 999 })))
1447
+ }
1448
+
1449
+ function SkeletonRunRow() {
1450
+ return h('div', { className: 'capital-automation-run capital-automation-skeletonrun', 'aria-hidden': 'true' },
1451
+ h('div', { className: 'capital-automation-runhead' },
1452
+ h('div', { className: 'capital-automation-runmain' },
1453
+ h('div', { className: 'capital-automation-runline' },
1454
+ h(SkeletonBlock, { width: 168, height: 11 }),
1455
+ h(SkeletonBlock, { width: 54, height: 10, radius: 999 })),
1456
+ h('div', { className: 'capital-automation-runmetaline' },
1457
+ h(SkeletonBlock, { width: 86, height: 9 }),
1458
+ h(SkeletonBlock, { width: 58, height: 9 }),
1459
+ h(SkeletonBlock, { width: 104, height: 9 }))),
1460
+ h('div', { className: 'capital-automation-runactions' },
1461
+ h(SkeletonBlock, { width: 64, height: 26, radius: 999 }))))
1462
+ }
1463
+
1464
+ function LoadingSkeleton(props) {
1465
+ const { children } = props
1466
+ // aria-busy on the live region would suppress its announcement until false; it unmounts instead.
1467
+ return h('div', { className: 'capital-automation-loading', 'aria-busy': 'true' },
1468
+ h('span', {
1469
+ className: 'capital-automation-skeletonlabel',
1470
+ role: 'status',
1471
+ 'aria-live': 'polite',
1472
+ }, zh.loading),
1473
+ children)
1474
+ }
1475
+
1476
+ function TaskDashboardSkeleton() {
1477
+ return h(LoadingSkeleton, null,
1478
+ h(SkeletonHistory),
1479
+ h('section', { className: 'capital-automation-section' },
1480
+ h(SkeletonSectionHead),
1481
+ h(SkeletonTaskTable, { rows: 4 })),
1482
+ h('section', { className: 'capital-automation-section' },
1483
+ h(SkeletonSectionHead, { trailing: [64] }),
1484
+ h('div', { className: 'capital-automation-templates' },
1485
+ [0, 1, 2].map((index) => h(SkeletonTemplateCard, { key: index })))))
1486
+ }
1487
+
1488
+ function RunsSkeleton() {
1489
+ return h(LoadingSkeleton, null,
1490
+ h(SkeletonHistory),
1491
+ h('section', { className: 'capital-automation-section' },
1492
+ h(SkeletonSectionHead, { trailing: [260] }),
1493
+ h('section', { className: 'capital-automation-card capital-automation-panel' },
1494
+ h('div', { className: 'capital-automation-runs' },
1495
+ [0, 1, 2, 3].map((index) => h(SkeletonRunRow, { key: index }))))))
1496
+ }
1497
+
1498
+ function TaskDashboard(props) {
1499
+ const { tasks, templates, runs, onOpenTask, onNewTask, onEnableTemplate, onAction, onRequestConfirm, onEdit, onGoRuns, onRequestCancelRun, schedulerActive, storageReady, busyTaskId } = props
1500
+ const [filter, setFilter] = useState('all')
1501
+ const [query, setQuery] = useState('')
1502
+ const runningTasks = tasks.filter((task) => isActiveRun(task.lastRun))
1503
+ const templatesAvailable = templates.filter((task) => !isActiveRun(task.lastRun))
1504
+ const roster = tasks.filter((task) => !isActiveRun(task.lastRun) && !(task.origin === 'builtin' && task.state === 'dormant'))
1505
+ const visible = roster.filter((task) => {
1506
+ if (filter === 'custom' && task.origin !== 'custom') return false
1507
+ if (filter !== 'all' && filter !== 'custom' && task.category !== filter) return false
1508
+ if (query.trim().length === 0) return true
1509
+ const needle = query.trim().toLowerCase()
1510
+ return `${task.title} ${task.description ?? ''} ${task.instruction ?? ''}`.toLowerCase().includes(needle)
1511
+ })
1512
+ const filterCounts = {
1513
+ all: roster.length,
1514
+ custom: roster.filter((task) => task.origin === 'custom').length,
1515
+ news: roster.filter((task) => task.category === 'news').length,
1516
+ market: roster.filter((task) => task.category === 'market').length,
1517
+ review: roster.filter((task) => task.category === 'review').length,
1518
+ research: roster.filter((task) => task.category === 'research').length,
1519
+ }
1520
+ const filters = ['all', 'custom', 'news', 'market', 'review', 'research']
1521
+ return h('div', null,
1522
+ h(RunHistoryCard, { runs, onViewAll: onGoRuns }),
1523
+ h('section', { className: 'capital-automation-section' },
1524
+ h('div', { className: 'capital-automation-sectionhead' },
1525
+ h('h2', null, zh.taskSection),
1526
+ h('span', null, zh.taskSectionHint),
1527
+ h('div', { className: 'capital-automation-sectiontools' },
1528
+ h('div', { className: 'capital-automation-filters', role: 'group', 'aria-label': zh.filterLabel },
1529
+ filters.map((value) => h(FilterButton, {
1530
+ key: value,
1531
+ label: value === 'all' ? zh.filterAll : value === 'custom' ? zh.filterCustom : categoryLabel(value),
1532
+ count: filterCounts[value],
1533
+ active: filter === value,
1534
+ onClick: () => setFilter(value),
1535
+ }))),
1536
+ h('label', { className: 'capital-automation-search' },
1537
+ h(IconSearchOutlineRegular, { size: 13 }),
1538
+ h('input', {
1539
+ type: 'search',
1540
+ value: query,
1541
+ placeholder: zh.searchPlaceholder,
1542
+ 'aria-label': zh.searchPlaceholder,
1543
+ onChange: (event) => setQuery(event.target.value),
1544
+ })))),
1545
+ runningTasks.length > 0
1546
+ ? h('div', { className: 'capital-automation-running', role: 'status' },
1547
+ runningTasks.map((task) => h('div', { key: task.id, className: 'capital-automation-runningrow' },
1548
+ h('span', { className: 'capital-automation-live' },
1549
+ h('span', { className: 'capital-automation-livedot' }),
1550
+ zh.status.running),
1551
+ h(Glyph, { task, size: 28 }),
1552
+ h('button', { type: 'button', className: 'capital-automation-tasktitle', style: { padding: 0, border: 0, background: 'transparent', cursor: 'pointer' }, onClick: () => onOpenTask(task.id) }, task.title),
1553
+ h('span', { className: 'capital-automation-runmeta' }, `${zh.runningSince} ${formatRelative(task.lastRun.startedAt ?? task.lastRun.queuedAt)} · ${zh.steps} ${task.lastRun.steps} · ${zh.tools} ${task.lastRun.toolCalls}`),
1554
+ h('span', { className: 'capital-automation-runningactions' },
1555
+ h('button', { type: 'button', className: 'capital-automation-btn', 'data-size': 'sm', 'data-variant': 'danger', onClick: () => onRequestCancelRun(task, task.lastRun) }, zh.cancelRun),
1556
+ h('button', { type: 'button', className: 'capital-automation-btn', 'data-size': 'sm', onClick: () => onOpenTask(task.id) }, zh.details)))))
1557
+ : null,
1558
+ visible.length > 0
1559
+ ? h(TaskTable, {
1560
+ tasks: visible,
1561
+ onOpen: onOpenTask,
1562
+ onAction,
1563
+ onRequestConfirm,
1564
+ onEdit,
1565
+ schedulerActive,
1566
+ storageReady,
1567
+ busyTaskId,
1568
+ })
1569
+ : roster.length === 0
1570
+ ? runningTasks.length > 0
1571
+ ? null
1572
+ : h(EmptyState, {
1573
+ title: templatesAvailable.length > 0 ? zh.emptyTasksTitle : zh.emptyNoTemplates,
1574
+ body: templatesAvailable.length > 0 ? zh.emptyTasksBody : zh.emptyNoTemplatesBody,
1575
+ action: templatesAvailable.length === 0 ? onNewTask : undefined,
1576
+ actionLabel: templatesAvailable.length === 0 ? zh.newTask : undefined,
1577
+ })
1578
+ : h(EmptyState, {
1579
+ title: zh.emptyFiltered,
1580
+ body: zh.emptyTasksBody,
1581
+ action: () => { setFilter('all'); setQuery('') },
1582
+ actionLabel: zh.clearFilter,
1583
+ })),
1584
+ templatesAvailable.length > 0
1585
+ ? h('section', { className: 'capital-automation-section' },
1586
+ h('div', { className: 'capital-automation-sectionhead' },
1587
+ h('h2', null, zh.templateSection)),
1588
+ h('div', { className: 'capital-automation-templates' },
1589
+ templatesAvailable.map((task) => h(TemplateCard, {
1590
+ key: task.id,
1591
+ task,
1592
+ onOpen: onOpenTask,
1593
+ onEnable: onEnableTemplate,
1594
+ busy: busyTaskId === task.id,
1595
+ disabled: !storageReady,
1596
+ }))))
1597
+ : null)
1598
+ }
1599
+
1600
+ function TriggerFields(props) {
1601
+ const { trigger, onChange, disabled, meta } = props
1602
+ const update = (patch) => onChange(normalizeTrigger({ ...trigger, ...patch }))
1603
+ const setCadence = (cadence) => {
1604
+ const next = triggerChanged(cadence, trigger)
1605
+ onChange(next)
1606
+ }
1607
+ const toggleWeekday = (day) => {
1608
+ const selected = new Set(trigger.weekdays ?? [])
1609
+ if (selected.has(day)) {
1610
+ if (selected.size <= 1) return
1611
+ selected.delete(day)
1612
+ } else {
1613
+ selected.add(day)
1614
+ }
1615
+ update({ weekdays: [...selected].sort((left, right) => left - right) })
1616
+ }
1617
+ return h('div', { className: 'capital-automation-triggerbox' },
1618
+ h('div', { className: 'capital-automation-triggerrow' },
1619
+ h('span', { className: 'capital-automation-triggerclock', 'aria-hidden': 'true' }, h(IconAlarmClockOutlineRegular, { size: 16 })),
1620
+ h('label', null,
1621
+ h('span', null, '重复'),
1622
+ h('select', { value: trigger.cadence, disabled, onChange: (event) => setCadence(event.target.value) },
1623
+ Object.entries(zh.cadence).map(([value, label]) => h('option', { key: value, value }, label)))),
1624
+ h('label', null,
1625
+ h('span', null, zh.timeField),
1626
+ h('input', { type: 'time', value: trigger.time, disabled, onChange: (event) => update({ time: event.target.value }) })),
1627
+ h('label', null,
1628
+ h('span', null, zh.timezoneField),
1629
+ h('input', {
1630
+ type: 'text',
1631
+ list: 'capital-automation-timezones',
1632
+ value: trigger.timezone,
1633
+ disabled,
1634
+ onChange: (event) => update({ timezone: event.target.value }),
1635
+ title: zh.timezoneHint,
1636
+ }))),
1637
+ h('div', { className: 'capital-automation-triggerrow' },
1638
+ trigger.cadence === 'weekly'
1639
+ ? h('div', { className: 'capital-automation-weekdays', role: 'group', 'aria-label': zh.weekdayField },
1640
+ WEEKDAYS.map((day) => h('button', {
1641
+ key: day.value,
1642
+ type: 'button',
1643
+ className: 'capital-automation-weekday',
1644
+ 'aria-pressed': (trigger.weekdays ?? []).includes(day.value) ? 'true' : 'false',
1645
+ disabled,
1646
+ onClick: () => toggleWeekday(day.value),
1647
+ }, day.label)))
1648
+ : null,
1649
+ trigger.cadence === 'monthly'
1650
+ ? h('label', null,
1651
+ h('span', null, zh.dayField),
1652
+ h('select', {
1653
+ value: trigger.dayOfMonth ?? 1,
1654
+ disabled,
1655
+ onChange: (event) => update({ dayOfMonth: Number(event.target.value) }),
1656
+ }, Array.from({ length: 28 }, (_, index) => index + 1).map((day) => h('option', { key: day, value: day }, zh.dayOfMonth.replace('{n}', String(day))))))
1657
+ : null,
1658
+ trigger.cadence === 'daily' ? h('span', { className: 'capital-automation-hint' }, trigger.tradingDaysOnly === true ? zh.triggerHint : zh.triggerHintNatural) : null,
1659
+ h('label', { className: 'capital-automation-switch', style: { marginLeft: 'auto' } },
1660
+ h(Switch, {
1661
+ checked: trigger.tradingDaysOnly === true,
1662
+ onChange: (value) => update({ tradingDaysOnly: value }),
1663
+ label: zh.tradingDaysField,
1664
+ disabled,
1665
+ }),
1666
+ h('span', null, zh.tradingDaysField))),
1667
+ h('p', { className: 'capital-automation-triggersummary' }, `${scheduleSummary(trigger)} · ${trigger.timezone}`),
1668
+ meta?.calendar?.available === false ? h('p', { className: 'capital-automation-hint' }, zh.nextRunUnavailable) : null)
1669
+ }
1670
+
1671
+ function SwitchRow(props) {
1672
+ const { label, hint, checked, onChange, disabled } = props
1673
+ return h('div', { className: 'capital-automation-switchrow' },
1674
+ h('div', null,
1675
+ h('span', null, label),
1676
+ hint ? h('small', null, hint) : null),
1677
+ h(Switch, { checked: checked === true, onChange, label, disabled }))
1678
+ }
1679
+
1680
+ function Field(props) {
1681
+ const { label, hint, children } = props
1682
+ return h('label', { className: 'capital-automation-field' },
1683
+ h('span', null, label),
1684
+ children,
1685
+ hint ? h('span', { className: 'capital-automation-hint' }, hint) : null)
1686
+ }
1687
+
1688
+ function NumberField(props) {
1689
+ const { label, value, min, max, disabled, onChange } = props
1690
+ return h(Field, { label },
1691
+ h('input', {
1692
+ type: 'number',
1693
+ min,
1694
+ max,
1695
+ value,
1696
+ disabled,
1697
+ onChange: (event) => onChange(Number(event.target.value)),
1698
+ }))
1699
+ }
1700
+
1701
+ function RunFilters(props) {
1702
+ const { value, counts, onChange } = props
1703
+ const options = [
1704
+ ['all', zh.runFilterAll],
1705
+ ['active', zh.runFilterActive],
1706
+ ['succeeded', zh.runFilterSucceeded],
1707
+ ['failed', zh.runFilterFailed],
1708
+ ]
1709
+ return h('div', { className: 'capital-automation-filters', role: 'group', 'aria-label': zh.runFilterLabel },
1710
+ options.map(([id, label]) => h(FilterButton, {
1711
+ key: id,
1712
+ label,
1713
+ count: counts[id],
1714
+ active: value === id,
1715
+ onClick: () => onChange(id),
1716
+ })))
1717
+ }
1718
+
1719
+ function RunsPage(props) {
1720
+ const { runs, tasks, onCancel, onOpenSession, onOpenTask, onGoTasks, onNewTask, busy } = props
1721
+ const [filter, setFilter] = useState('all')
1722
+ const counts = {
1723
+ all: runs.length,
1724
+ active: runs.filter((run) => isActiveRun(run)).length,
1725
+ succeeded: runs.filter((run) => run.status === 'succeeded').length,
1726
+ failed: runs.filter((run) => isFailureStatus(run.status)).length,
1727
+ }
1728
+ const visible = runs.filter((run) => {
1729
+ if (filter === 'active') return isActiveRun(run)
1730
+ if (filter === 'succeeded') return run.status === 'succeeded'
1731
+ if (filter === 'failed') return isFailureStatus(run.status)
1732
+ return true
1733
+ })
1734
+ return h('div', null,
1735
+ h(RunHistoryCard, { runs, showLink: false }),
1736
+ h('section', { className: 'capital-automation-section' },
1737
+ h('div', { className: 'capital-automation-sectionhead' },
1738
+ h('h2', null, zh.runsTitle),
1739
+ h('span', null, zh.runsHint),
1740
+ h('div', { className: 'capital-automation-sectiontools' },
1741
+ h(RunFilters, { value: filter, counts, onChange: setFilter }))),
1742
+ runs.length === 0
1743
+ ? h('div', { className: 'capital-automation-empty' },
1744
+ h('span', { className: 'capital-automation-emptyicon' }, h(IconClockOutlineRegular, { size: 19 })),
1745
+ h('h3', null, zh.runsEmptyTitle),
1746
+ h('p', null, zh.runsEmptyBody),
1747
+ h('div', { style: { display: 'flex', gap: 8, marginTop: 8 } },
1748
+ h('button', { type: 'button', className: 'capital-automation-btn', onClick: onGoTasks }, zh.runsEmptyAction),
1749
+ h('button', { type: 'button', className: 'capital-automation-btn', 'data-variant': 'primary', onClick: onNewTask }, zh.newTask)))
1750
+ : visible.length === 0
1751
+ ? h(EmptyState, { title: zh.emptyFiltered, body: zh.filterNoRuns, action: () => setFilter('all'), actionLabel: zh.clearFilter })
1752
+ : h('section', { className: 'capital-automation-card capital-automation-panel' },
1753
+ h(RunList, { runs: visible, tasks, onCancel, onOpenSession, onOpenTask, busy }),
1754
+ runs.length >= 200 ? h('p', { className: 'capital-automation-hint', style: { paddingTop: 10 } }, zh.runsLimitHint.replace('{n}', '200')) : null)))
1755
+ }
1756
+
1757
+ function OverviewTab(props) {
1758
+ const { detail, meta, onGoRuns } = props
1759
+ const latest = detail.runs[0]
1760
+ const next = detail.task.nextRunLocal ?? (detail.task.nextRunUnavailable ? zh.calendarUnavailable : zh.never)
1761
+ const anyRunning = detail.runs.some((run) => isActiveRun(run))
1762
+ const stateText = anyRunning ? zh.status.running : stateLabel(detail.task.state)
1763
+ return h('div', { className: 'capital-automation-overview' },
1764
+ h('div', { className: 'capital-automation-statgrid' },
1765
+ h('div', { className: 'capital-automation-stat' },
1766
+ h('span', { className: 'capital-automation-statlabel' }, zh.stateLabel),
1767
+ h('span', { className: 'capital-automation-statvalue', 'data-tone': anyRunning ? 'running' : undefined }, stateText),
1768
+ h('span', { className: 'capital-automation-statsub' }, detail.task.origin === 'builtin' ? zh.origin.builtin : zh.origin.custom)),
1769
+ h('div', { className: 'capital-automation-stat' },
1770
+ h('span', { className: 'capital-automation-statlabel' }, zh.nextRun),
1771
+ h('span', { className: 'capital-automation-statvalue' }, next),
1772
+ h('span', { className: 'capital-automation-statsub' }, detail.task.nextRunAt ? formatRelative(detail.task.nextRunAt) : (detail.task.nextRunUnavailable ? zh.nextRunUnavailable : zh.never))),
1773
+ h('div', { className: 'capital-automation-stat' },
1774
+ h('span', { className: 'capital-automation-statlabel' }, zh.lastResult),
1775
+ h('span', { className: 'capital-automation-statvalue', 'data-tone': latest?.status === 'succeeded' ? 'ok' : latest && isFailureStatus(latest.status) ? 'error' : undefined }, latest ? statusLabel(latest.status) : zh.neverRun),
1776
+ h('span', { className: 'capital-automation-statsub' }, latest ? `${formatShortTime(latest.queuedAt, detail.task.trigger.timezone)}${typeof latest.durationMs === 'number' ? ` · ${formatDuration(latest.durationMs)}` : ''}` : zh.never)),
1777
+ h('div', { className: 'capital-automation-stat' },
1778
+ h('span', { className: 'capital-automation-statlabel' }, zh.triggerSummary),
1779
+ h('span', { className: 'capital-automation-statvalue' }, scheduleRangeSummary(detail.task.trigger)),
1780
+ h('span', { className: 'capital-automation-statsub' }, `${detail.task.trigger.time} · ${detail.task.trigger.timezone}`))),
1781
+ h('section', { className: 'capital-automation-card capital-automation-panel' },
1782
+ h('div', { className: 'capital-automation-panelhead' },
1783
+ h('h3', null, zh.triggerSummary),
1784
+ h('span', { className: 'capital-automation-hint' }, detail.task.trigger.tradingDaysOnly === true ? zh.triggerHint : zh.triggerHintNatural)),
1785
+ h('div', { className: 'capital-automation-kv' },
1786
+ h('div', { className: 'capital-automation-kvitem' },
1787
+ h('span', { className: 'capital-automation-kvlabel' }, zh.triggerField),
1788
+ h('span', { className: 'capital-automation-kvvalue' }, scheduleSummary(detail.task.trigger))),
1789
+ h('div', { className: 'capital-automation-kvitem' },
1790
+ h('span', { className: 'capital-automation-kvlabel' }, zh.timezoneField),
1791
+ h('span', { className: 'capital-automation-kvvalue' }, detail.task.trigger.timezone)),
1792
+ h('div', { className: 'capital-automation-kvitem' },
1793
+ h('span', { className: 'capital-automation-kvlabel' }, zh.nextRun),
1794
+ h('span', { className: 'capital-automation-kvvalue' }, `${next}${detail.task.nextRunAt ? ` · ${formatRelative(detail.task.nextRunAt)}` : ''}`)),
1795
+ h('div', { className: 'capital-automation-kvitem' },
1796
+ h('span', { className: 'capital-automation-kvlabel' }, zh.stateLabel),
1797
+ h('span', { className: 'capital-automation-kvvalue' }, `${stateText}${detail.task.trigger.tradingDaysOnly ? ` · ${zh.tradingDaysField}` : ''}`)))),
1798
+ h('section', { className: 'capital-automation-card capital-automation-panel' },
1799
+ h('div', { className: 'capital-automation-panelhead' },
1800
+ h('h3', null, zh.detailRecentRuns),
1801
+ h('button', { type: 'button', className: 'capital-automation-btn', 'data-size': 'sm', 'data-variant': 'ghost', onClick: onGoRuns },
1802
+ zh.detailAllRuns,
1803
+ h(IconChevronRightOutlineRegular, { size: 13 }))),
1804
+ h(RunList, { runs: detail.runs.slice(0, 3), tasks: [detail.task], compact: true })),
1805
+ h('section', { className: 'capital-automation-card capital-automation-panel' },
1806
+ h('div', { className: 'capital-automation-panelhead' },
1807
+ h('h3', null, zh.environment)),
1808
+ h('div', { className: 'capital-automation-kv' },
1809
+ h('div', { className: 'capital-automation-kvitem' },
1810
+ h('span', { className: 'capital-automation-kvlabel' }, zh.presetLabel),
1811
+ h('span', { className: 'capital-automation-kvvalue' }, zh.presetValue)),
1812
+ h('div', { className: 'capital-automation-kvitem' },
1813
+ h('span', { className: 'capital-automation-kvlabel' }, zh.workspaceField),
1814
+ h('span', { className: 'capital-automation-kvvalue' }, workspaceTitle(meta, detail.task.workspaceId))),
1815
+ h('div', { className: 'capital-automation-kvitem' },
1816
+ h('span', { className: 'capital-automation-kvlabel' }, zh.categoryField),
1817
+ h('span', { className: 'capital-automation-kvvalue' }, categoryLabel(detail.task.category))),
1818
+ h('div', { className: 'capital-automation-kvitem' },
1819
+ h('span', { className: 'capital-automation-kvlabel' }, zh.modeField),
1820
+ h('span', { className: 'capital-automation-kvvalue' }, modeLabel(detail.task.mode)))),
1821
+ h('div', { className: 'capital-automation-policy', style: { marginTop: 14 } },
1822
+ h('p', null, zh.policyText),
1823
+ h('p', null, zh.budgetHint)),
1824
+ h('details', { className: 'capital-automation-technical' },
1825
+ h('summary', null, zh.techInfo),
1826
+ h('dl', null,
1827
+ h('dt', null, zh.profileKey),
1828
+ h('dd', null, meta?.profile?.key ?? '—'),
1829
+ h('dt', null, zh.presetLabel),
1830
+ h('dd', null, zh.presetValue),
1831
+ h('dt', null, zh.policyField),
1832
+ h('dd', null, meta?.execution?.policy ?? 'automation-v1'),
1833
+ h('dt', null, zh.cwdKind),
1834
+ h('dd', null, meta?.execution?.cwdKind ?? 'profile'),
1835
+ h('dt', null, 'profile'),
1836
+ h('dd', null, meta?.profile?.name ?? '—'),
1837
+ h('dt', null, zh.calendarField),
1838
+ h('dd', null, calendarLabel(meta?.calendar)),
1839
+ h('dt', null, zh.version),
1840
+ h('dd', null, meta?.version ? `v${meta.version}` : '—')))))
1841
+ }
1842
+
1843
+ function ConfigTab(props) {
1844
+ const { draft, setDraft, meta, busy, dirty, save, reset, disabled } = props
1845
+ const task = draft
1846
+ const set = (patch) => setDraft((current) => ({
1847
+ ...current,
1848
+ ...(typeof patch === 'function' ? patch(current) : patch),
1849
+ }))
1850
+ const timeoutRange = limitValue(meta, 'runTimeoutMinutes', [5, 120])
1851
+ const idleRange = limitValue(meta, 'idleTimeoutMinutes', [1, 30])
1852
+ const stepsRange = limitValue(meta, 'maxSteps', [10, 2000])
1853
+ const toolsRange = limitValue(meta, 'maxToolCalls', [10, 6000])
1854
+ const advanced = h('details', { className: 'capital-automation-advanced' },
1855
+ h('summary', null, zh.advanced),
1856
+ h('div', { className: 'capital-automation-advancedbody' },
1857
+ h('div', { className: 'capital-automation-formgrid' },
1858
+ h(Field, { label: zh.workspaceField },
1859
+ h('select', {
1860
+ value: task.workspaceId ?? '',
1861
+ disabled: busy || disabled,
1862
+ onChange: (event) => set({ workspaceId: event.target.value }),
1863
+ }, workspaceOptions(meta, task.workspaceId).map((option) => h('option', { key: option.id, value: option.id }, option.title)))),
1864
+ h(Field, { label: zh.modeField, hint: zh.modeHint },
1865
+ h('select', {
1866
+ value: task.mode,
1867
+ disabled: busy || disabled,
1868
+ onChange: (event) => set({ mode: event.target.value }),
1869
+ }, Object.entries(zh.mode).map(([value, label]) => h('option', { key: value, value }, label))))),
1870
+ h('div', { className: 'capital-automation-formgrid' },
1871
+ h(NumberField, { label: zh.runTimeout, value: task.limits.runTimeoutMinutes, min: timeoutRange[0], max: timeoutRange[1], disabled: busy || disabled, onChange: (value) => set((current) => ({ limits: { ...current.limits, runTimeoutMinutes: value } })) }),
1872
+ h(NumberField, { label: zh.idleTimeout, value: task.limits.idleTimeoutMinutes, min: idleRange[0], max: idleRange[1], disabled: busy || disabled, onChange: (value) => set((current) => ({ limits: { ...current.limits, idleTimeoutMinutes: value } })) }),
1873
+ h(NumberField, { label: zh.maxSteps, value: task.limits.maxSteps, min: stepsRange[0], max: stepsRange[1], disabled: busy || disabled, onChange: (value) => set((current) => ({ limits: { ...current.limits, maxSteps: value } })) }),
1874
+ h(NumberField, { label: zh.maxToolCalls, value: task.limits.maxToolCalls, min: toolsRange[0], max: toolsRange[1], disabled: busy || disabled, onChange: (value) => set((current) => ({ limits: { ...current.limits, maxToolCalls: value } })) })),
1875
+ h('p', { className: 'capital-automation-hint' }, zh.budgetHint),
1876
+ meta?.workspaceError ? h('p', { className: 'capital-automation-hint', 'data-tone': 'error' }, zh.workspaceUnavailable) : null))
1877
+ const notificationRows = h('div', null,
1878
+ h('span', { className: 'capital-automation-formlabel' }, zh.notifyField),
1879
+ h('div', { className: 'capital-automation-switchlist' },
1880
+ h(SwitchRow, { label: zh.notifyInApp, checked: task.notifications.inApp, disabled: busy || disabled, onChange: (value) => set((current) => ({ notifications: { ...current.notifications, inApp: value } })) }),
1881
+ h(SwitchRow, { label: zh.notifyFailure, checked: task.notifications.onFailure, disabled: busy || disabled, onChange: (value) => set((current) => ({ notifications: { ...current.notifications, onFailure: value } })) }),
1882
+ h(SwitchRow, { label: zh.notifySuccess, checked: task.notifications.onSuccess, disabled: busy || disabled, onChange: (value) => set((current) => ({ notifications: { ...current.notifications, onSuccess: value } })) })),
1883
+ h('p', { className: 'capital-automation-hint' }, zh.notifyHint))
1884
+ return h('div', { className: 'capital-automation-form' },
1885
+ h(Field, { label: zh.titleField },
1886
+ h('input', { value: task.title, disabled: busy || disabled, onChange: (event) => set({ title: event.target.value }) })),
1887
+ h(Field, { label: zh.descriptionField },
1888
+ h('textarea', { value: task.description ?? '', disabled: busy || disabled, rows: 3, onChange: (event) => set({ description: event.target.value }) })),
1889
+ h(Field, { label: zh.categoryField, hint: task.origin === 'builtin' ? zh.categoryFixed : undefined },
1890
+ h('select', {
1891
+ value: task.category,
1892
+ disabled: busy || disabled || task.origin === 'builtin',
1893
+ onChange: (event) => set({ category: event.target.value }),
1894
+ }, Object.entries(zh.category).map(([value, label]) => h('option', { key: value, value }, label)))),
1895
+ h(Field, { label: zh.instructionField },
1896
+ h('textarea', {
1897
+ value: task.instruction,
1898
+ disabled: busy || disabled,
1899
+ rows: 14,
1900
+ onChange: (event) => set({ instruction: event.target.value }),
1901
+ })),
1902
+ h('div', null,
1903
+ h('span', { className: 'capital-automation-formlabel' }, zh.triggerField),
1904
+ h(TriggerFields, { trigger: task.trigger, disabled: busy || disabled, onChange: (trigger) => set({ trigger }), meta })),
1905
+ advanced,
1906
+ notificationRows,
1907
+ h('div', { className: 'capital-automation-formactions', 'data-sticky': 'true' },
1908
+ h('span', { className: 'capital-automation-formstatus' },
1909
+ disabled ? zh.storageUnavailable : dirty ? zh.dirty : zh.noDirty),
1910
+ h('button', { type: 'button', className: 'capital-automation-btn', disabled: busy || !dirty, onClick: reset }, zh.cancel),
1911
+ h('button', { type: 'button', className: 'capital-automation-btn', 'data-variant': 'primary', disabled: busy || !dirty || disabled, onClick: save }, busy === 'save' ? zh.saving : zh.save)))
1912
+ }
1913
+
1914
+ function NotificationsTab(props) {
1915
+ const { detail, busy, onMarkTaskRead } = props
1916
+ const items = detail.notifications
1917
+ const unread = items.filter((item) => item.readAt === undefined).length
1918
+ return h('section', { className: 'capital-automation-card capital-automation-panel' },
1919
+ h('div', { className: 'capital-automation-panelhead' },
1920
+ h('h3', null, zh.detailTaskNotifications),
1921
+ unread > 0
1922
+ ? h('button', { type: 'button', className: 'capital-automation-btn', 'data-size': 'sm', disabled: busy, onClick: onMarkTaskRead }, zh.markTaskRead)
1923
+ : null),
1924
+ items.length === 0
1925
+ ? h('p', { className: 'capital-automation-hint' }, zh.notificationsEmpty)
1926
+ : h('div', null, items.map((item) => h(NotificationItem, { key: item.id, item }))))
1927
+ }
1928
+
1929
+ function NotificationItem(props) {
1930
+ const { item, onClick, onOpenTask } = props
1931
+ const clickable = typeof onClick === 'function' && item.taskId !== undefined
1932
+ const tag = clickable ? 'button' : 'div'
1933
+ return h(tag, {
1934
+ type: clickable ? 'button' : undefined,
1935
+ className: 'capital-automation-notification',
1936
+ 'data-clickable': clickable ? 'true' : 'false',
1937
+ onClick: clickable ? onClick : undefined,
1938
+ },
1939
+ h('span', { className: 'capital-automation-notificationicon', 'data-tone': eventTone(item.kind) },
1940
+ eventTone(item.kind) === 'error' ? h(IconWarningOutlineRegular, { size: 15 }) : eventTone(item.kind) === 'warn' ? h(IconInfoOutlineRegular, { size: 15 }) : h(IconAlarmClockOutlineRegular, { size: 15 })),
1941
+ h('span', { className: 'capital-automation-notificationbody' },
1942
+ h('span', { className: 'capital-automation-notificationtitle' },
1943
+ item.title,
1944
+ item.readAt === undefined ? h('span', { className: 'capital-automation-count' }, zh.unread) : null),
1945
+ h('span', { className: 'capital-automation-notificationmessage' }, notificationMessage(item)),
1946
+ h('span', { className: 'capital-automation-notificationtime' }, formatShortTime(item.createdAt))),
1947
+ onOpenTask !== undefined && item.taskId !== undefined
1948
+ ? h('span', { className: 'capital-automation-hint' }, zh.notificationOpenTask)
1949
+ : null)
1950
+ }
1951
+
1952
+ function DetailBar(props) {
1953
+ return h('div', { className: 'capital-automation-detailbar' },
1954
+ h('button', { type: 'button', className: 'capital-automation-back', onClick: props.onBack },
1955
+ h(IconChevronLeftOutlineRegular, { size: 14 }),
1956
+ zh.backToTasks))
1957
+ }
1958
+
1959
+ function TaskDetailPage(props) {
1960
+ const { detail, meta, api, onChanged, openSession, tab, onTab, onRequestConfirm, onRequestCancelRun, onOpenAllRuns, schedulerActive, storageReady } = props
1961
+ const [draft, setDraft] = useState(null)
1962
+ const [busy, setBusy] = useState('')
1963
+ const [notice, setNotice] = useState('')
1964
+ const [error, setError] = useState('')
1965
+ const [menuOpen, setMenuOpen] = useState(false)
1966
+ const lastTaskIdRef = useRef(null)
1967
+
1968
+ useEffect(() => {
1969
+ setDraft(detail.task ? {
1970
+ ...detail.task,
1971
+ trigger: normalizeTrigger(detail.task.trigger),
1972
+ limits: { ...detail.task.limits },
1973
+ notifications: { ...detail.task.notifications },
1974
+ } : null)
1975
+ }, [detail])
1976
+
1977
+ useEffect(() => {
1978
+ const id = detail.task?.id ?? null
1979
+ if (lastTaskIdRef.current === id) return
1980
+ lastTaskIdRef.current = id
1981
+ setNotice('')
1982
+ setError('')
1983
+ }, [detail])
1984
+
1985
+ if (draft === null) return h('p', { className: 'capital-automation-empty' }, zh.loading)
1986
+ const task = draft
1987
+ const storageReadyNow = storageReady !== false
1988
+ const running = detail.runs.some((run) => isActiveRun(run))
1989
+ const activeRun = detail.runs.find((run) => isActiveRun(run))
1990
+ const unread = detail.notifications.filter((item) => item.readAt === undefined).length
1991
+ const dirty = draftChanged(draft, detail.task)
1992
+ const canRun = storageReadyNow && schedulerActive === true && !running
1993
+ const set = (patch) => setDraft((current) => ({
1994
+ ...current,
1995
+ ...(typeof patch === 'function' ? patch(current) : patch),
1996
+ }))
1997
+
1998
+ async function act(name, action, success) {
1999
+ if (busy) return
2000
+ setBusy(name)
2001
+ setError('')
2002
+ try {
2003
+ await action()
2004
+ await onChanged(detail.task.id)
2005
+ setNotice(success)
2006
+ } catch (failure) {
2007
+ setError(failure.message ?? zh.errorGeneric)
2008
+ } finally {
2009
+ setBusy('')
2010
+ }
2011
+ }
2012
+
2013
+ async function save() {
2014
+ const invalid = validateDraft(task)
2015
+ if (invalid.length > 0) {
2016
+ setError(invalid)
2017
+ return
2018
+ }
2019
+ await act('save', async () => {
2020
+ await api.patch(detail.task.id, draftPayload(task))
2021
+ }, zh.saved)
2022
+ }
2023
+
2024
+ async function toggleState() {
2025
+ const next = detail.task.state === 'active' ? 'pause' : 'activate'
2026
+ await act(next, async () => {
2027
+ await api.action(detail.task.id, next)
2028
+ }, next === 'pause' ? zh.paused.replace('{title}', detail.task.title) : activationNotice(detail.task))
2029
+ }
2030
+
2031
+ async function runNow() {
2032
+ await act('run', async () => {
2033
+ await api.run(detail.task.id)
2034
+ }, zh.runSubmitted.replace('{title}', detail.task.title))
2035
+ }
2036
+
2037
+ async function markTaskRead() {
2038
+ const ids = detail.notifications.filter((item) => item.readAt === undefined).map((item) => item.id)
2039
+ if (ids.length === 0) return
2040
+ await act('read-notifications', async () => {
2041
+ await api.markRead({ ids })
2042
+ }, zh.markReadDone)
2043
+ }
2044
+
2045
+ async function copyTask() {
2046
+ await act('copy', async () => {
2047
+ await api.action(detail.task.id, 'copy')
2048
+ }, zh.copied.replace('{title}', detail.task.title))
2049
+ }
2050
+
2051
+ const runButton = running
2052
+ ? h('button', {
2053
+ type: 'button',
2054
+ className: 'capital-automation-btn',
2055
+ 'data-variant': 'danger',
2056
+ disabled: busy || !storageReadyNow || activeRun === undefined,
2057
+ onClick: () => { if (activeRun !== undefined) onRequestCancelRun(activeRun) },
2058
+ }, zh.cancelRun)
2059
+ : h('button', {
2060
+ type: 'button',
2061
+ className: 'capital-automation-btn',
2062
+ 'data-variant': detail.task.state === 'active' ? 'primary' : undefined,
2063
+ disabled: busy || !canRun,
2064
+ title: schedulerActive === false ? zh.schedulerBlocked : running ? zh.runBlocked : undefined,
2065
+ onClick: () => { void runNow() },
2066
+ }, zh.runNow)
2067
+
2068
+ const resetDraft = () => {
2069
+ setDraft({
2070
+ ...detail.task,
2071
+ trigger: normalizeTrigger(detail.task.trigger),
2072
+ limits: { ...detail.task.limits },
2073
+ notifications: { ...detail.task.notifications },
2074
+ })
2075
+ setError('')
2076
+ setNotice('')
2077
+ }
2078
+
2079
+ return h('div', null,
2080
+ h('div', { className: 'capital-automation-hero' },
2081
+ h(Glyph, { task, large: true }),
2082
+ h('div', { className: 'capital-automation-herobody' },
2083
+ h('div', { className: 'capital-automation-herotitle' },
2084
+ h('h2', null, detail.task.title),
2085
+ h(StateBadge, { state: detail.task.state, running }),
2086
+ unread > 0 ? h('span', { className: 'capital-automation-count', title: zh.unread }, String(unread)) : null),
2087
+ h('p', { className: 'capital-automation-herodesc' }, detail.task.description || zh.subtitle),
2088
+ h('div', { className: 'capital-automation-herometa' },
2089
+ h('span', null, h(IconClockOutlineRegular, { size: 13 }), scheduleSummary(detail.task.trigger)),
2090
+ h('span', null, `${zh.nextRun} ${detail.task.nextRunLocal ?? (detail.task.nextRunUnavailable ? zh.calendarUnavailable : zh.never)}`),
2091
+ h('span', null, `${zh.lastResult} ${detail.runs[0] ? statusLabel(detail.runs[0].status) : zh.never}`),
2092
+ h('span', null, categoryLabel(detail.task.category)),
2093
+ detail.task.origin === 'builtin' ? h('span', { className: 'capital-automation-tag' }, zh.origin.builtin) : h('span', { className: 'capital-automation-tag', 'data-tone': 'custom' }, zh.origin.custom))),
2094
+ h('div', { className: 'capital-automation-heroactions' },
2095
+ runButton,
2096
+ running
2097
+ ? null
2098
+ : h('button', {
2099
+ type: 'button',
2100
+ className: 'capital-automation-btn',
2101
+ 'data-variant': detail.task.state === 'active' ? undefined : 'primary',
2102
+ disabled: busy || !storageReadyNow,
2103
+ onClick: () => { void toggleState() },
2104
+ }, activationLabel(detail.task)),
2105
+ h(TaskActionsMenu, {
2106
+ task: detail.task,
2107
+ open: menuOpen,
2108
+ onToggle: () => setMenuOpen((current) => !current),
2109
+ onClose: () => setMenuOpen(false),
2110
+ onEdit: () => { setMenuOpen(false); onTab('config') },
2111
+ onAction: (target, id) => {
2112
+ if (id === 'run') void runNow()
2113
+ else if (id === 'copy') void copyTask()
2114
+ else if (id === 'activate' || id === 'pause') void toggleState()
2115
+ },
2116
+ onRequestConfirm: (kind, target) => { setMenuOpen(false); onRequestConfirm(kind, target) },
2117
+ canRun,
2118
+ running,
2119
+ busy: busy.length > 0,
2120
+ disabled: !storageReadyNow,
2121
+ align: 'end',
2122
+ }))),
2123
+ h('div', { className: 'capital-automation-detailtabs', role: 'tablist', 'aria-label': zh.details },
2124
+ [['overview', zh.overview], ['config', zh.config], ['runs', zh.runs], ['notifications', zh.notifications]].map(([id, label]) => h('button', {
2125
+ key: id,
2126
+ type: 'button',
2127
+ role: 'tab',
2128
+ className: 'capital-automation-detailtab',
2129
+ 'aria-selected': tab === id ? 'true' : 'false',
2130
+ onClick: () => onTab(id),
2131
+ }, label))),
2132
+ notice.length > 0 || error.length > 0
2133
+ ? h('div', { style: { paddingTop: 12 } },
2134
+ notice.length > 0 ? h('p', { className: 'capital-automation-notice', role: 'status' }, notice) : null,
2135
+ error.length > 0 ? h('p', { className: 'capital-automation-error', role: 'alert' }, error) : null)
2136
+ : null,
2137
+ running ? h('p', { className: 'capital-automation-hint', style: { paddingTop: 10 } }, zh.configWhileRunning) : null,
2138
+ detail.task.origin === 'builtin' ? h('p', { className: 'capital-automation-hint', style: { paddingTop: 8 } }, zh.configBuiltinHint) : null,
2139
+ h('div', { className: 'capital-automation-detailbody' },
2140
+ tab === 'overview' ? h(OverviewTab, { detail, meta, onGoRuns: onOpenAllRuns }) : null,
2141
+ tab === 'config' ? h(ConfigTab, {
2142
+ draft,
2143
+ setDraft,
2144
+ meta,
2145
+ busy,
2146
+ dirty,
2147
+ save,
2148
+ reset: resetDraft,
2149
+ disabled: !storageReadyNow,
2150
+ }) : null,
2151
+ tab === 'runs' ? h('section', { className: 'capital-automation-card capital-automation-panel' },
2152
+ h('div', { className: 'capital-automation-panelhead' },
2153
+ h('h3', null, zh.runs),
2154
+ h('span', { className: 'capital-automation-hint' }, zh.taskRunsLimit)),
2155
+ h(RunList, {
2156
+ runs: detail.runs,
2157
+ tasks: [detail.task],
2158
+ onCancel: (run) => onRequestCancelRun(run),
2159
+ onOpenSession: openSession,
2160
+ busy: busy.length > 0,
2161
+ })) : null,
2162
+ tab === 'notifications' ? h(NotificationsTab, {
2163
+ detail,
2164
+ busy: busy.length > 0,
2165
+ onMarkTaskRead: () => { void markTaskRead() },
2166
+ }) : null))
2167
+ }
2168
+
2169
+ function NewTaskModal(props) {
2170
+ const { open, onClose, api, onCreated, meta } = props
2171
+ const [draft, setDraft] = useState(defaultDraft)
2172
+ const [busy, setBusy] = useState(false)
2173
+ const [error, setError] = useState('')
2174
+ useEffect(() => {
2175
+ if (!open) return
2176
+ setDraft(defaultDraft())
2177
+ setBusy(false)
2178
+ setError('')
2179
+ }, [open])
2180
+ const set = (patch) => setDraft((current) => ({
2181
+ ...current,
2182
+ ...(typeof patch === 'function' ? patch(current) : patch),
2183
+ }))
2184
+ const timeoutRange = limitValue(meta, 'runTimeoutMinutes', [5, 120])
2185
+ const idleRange = limitValue(meta, 'idleTimeoutMinutes', [1, 30])
2186
+ const stepsRange = limitValue(meta, 'maxSteps', [10, 2000])
2187
+ const toolsRange = limitValue(meta, 'maxToolCalls', [10, 6000])
2188
+ async function create() {
2189
+ const invalid = validateDraft(draft)
2190
+ if (invalid.length > 0) {
2191
+ setError(invalid)
2192
+ return
2193
+ }
2194
+ setBusy(true)
2195
+ setError('')
2196
+ try {
2197
+ const payload = await api.create(draftPayload(draft))
2198
+ onCreated(payload?.task?.id ?? null)
2199
+ } catch (failure) {
2200
+ setError(failure.message ?? zh.errorGeneric)
2201
+ } finally {
2202
+ setBusy(false)
2203
+ }
2204
+ }
2205
+ if (!open) return null
2206
+ return h(Modal, {
2207
+ open,
2208
+ onClose: () => { if (!busy) onClose() },
2209
+ title: zh.newTaskTitle,
2210
+ closeLabel: zh.close,
2211
+ className: 'capital-automation-modal',
2212
+ contentClassName: 'capital-automation-modal-content',
2213
+ footer: h('div', { className: 'capital-automation-formactions', style: { borderTop: 0, paddingTop: 0 } },
2214
+ h('button', { type: 'button', className: 'capital-automation-btn', disabled: busy, onClick: onClose }, zh.cancel),
2215
+ h('button', { type: 'button', className: 'capital-automation-btn', 'data-variant': 'primary', disabled: busy || meta?.storage?.state === 'unavailable', onClick: create }, busy ? zh.creating : zh.create)),
2216
+ },
2217
+ h('div', { className: 'capital-automation-wizard' },
2218
+ h('div', { className: 'capital-automation-modalheadline' },
2219
+ h(Glyph, { task: draft, size: 42 }),
2220
+ h('input', {
2221
+ className: 'capital-automation-modaltitleinput',
2222
+ value: draft.title,
2223
+ placeholder: zh.titlePlaceholder,
2224
+ 'aria-label': zh.titleField,
2225
+ onChange: (event) => set({ title: event.target.value }),
2226
+ })),
2227
+ h('p', { className: 'capital-automation-hint' }, zh.newTaskHint),
2228
+ h(Field, { label: zh.descriptionField },
2229
+ h('input', {
2230
+ value: draft.description,
2231
+ placeholder: zh.descriptionPlaceholder,
2232
+ onChange: (event) => set({ description: event.target.value }),
2233
+ })),
2234
+ h('div', null,
2235
+ h('span', { className: 'capital-automation-formlabel' }, zh.triggerField),
2236
+ h(TriggerFields, { trigger: draft.trigger, onChange: (trigger) => set({ trigger }), meta })),
2237
+ h('div', null,
2238
+ h('span', { className: 'capital-automation-formlabel' }, zh.instructionField),
2239
+ h('div', { className: 'capital-automation-promptbox' },
2240
+ h('textarea', {
2241
+ value: draft.instruction,
2242
+ placeholder: zh.instructionPlaceholder,
2243
+ 'aria-label': zh.instructionField,
2244
+ onChange: (event) => set({ instruction: event.target.value }),
2245
+ }),
2246
+ h('div', { className: 'capital-automation-prompttoolbar' },
2247
+ h('label', null,
2248
+ h(IconFolderOpenOutlineRegular, { size: 13 }),
2249
+ h('span', null, zh.workspaceField),
2250
+ h('select', {
2251
+ value: draft.workspaceId ?? '',
2252
+ onChange: (event) => set({ workspaceId: event.target.value }),
2253
+ }, workspaceOptions(meta, draft.workspaceId).map((option) => h('option', { key: option.id, value: option.id }, option.title)))),
2254
+ h('label', null,
2255
+ h('span', null, zh.categoryField),
2256
+ h('select', {
2257
+ value: draft.category,
2258
+ onChange: (event) => set({ category: event.target.value }),
2259
+ }, Object.entries(zh.category).map(([value, label]) => h('option', { key: value, value }, label)))),
2260
+ h('label', null,
2261
+ h('span', null, zh.modeField),
2262
+ h('select', {
2263
+ value: draft.mode,
2264
+ onChange: (event) => set({ mode: event.target.value }),
2265
+ }, Object.entries(zh.mode).map(([value, label]) => h('option', { key: value, value }, label)))),
2266
+ h('span', { className: 'capital-automation-presetchip', title: zh.modeHint },
2267
+ h(IconSparkleRegular, { size: 12 }),
2268
+ zh.presetValue)))),
2269
+ h('p', { className: 'capital-automation-hint' }, zh.modeHint),
2270
+ meta?.workspaceError ? h('p', { className: 'capital-automation-hint', 'data-tone': 'error' }, zh.workspaceUnavailable) : null,
2271
+ h('details', { className: 'capital-automation-advanced' },
2272
+ h('summary', null, zh.advanced),
2273
+ h('div', { className: 'capital-automation-advancedbody' },
2274
+ h('div', { className: 'capital-automation-formgrid' },
2275
+ h(NumberField, { label: zh.runTimeout, value: draft.limits.runTimeoutMinutes, min: timeoutRange[0], max: timeoutRange[1], onChange: (value) => set((current) => ({ limits: { ...current.limits, runTimeoutMinutes: value } })) }),
2276
+ h(NumberField, { label: zh.idleTimeout, value: draft.limits.idleTimeoutMinutes, min: idleRange[0], max: idleRange[1], onChange: (value) => set((current) => ({ limits: { ...current.limits, idleTimeoutMinutes: value } })) }),
2277
+ h(NumberField, { label: zh.maxSteps, value: draft.limits.maxSteps, min: stepsRange[0], max: stepsRange[1], onChange: (value) => set((current) => ({ limits: { ...current.limits, maxSteps: value } })) }),
2278
+ h(NumberField, { label: zh.maxToolCalls, value: draft.limits.maxToolCalls, min: toolsRange[0], max: toolsRange[1], onChange: (value) => set((current) => ({ limits: { ...current.limits, maxToolCalls: value } })) })),
2279
+ h('p', { className: 'capital-automation-hint' }, zh.advancedHint))),
2280
+ h('div', null,
2281
+ h('span', { className: 'capital-automation-formlabel' }, zh.notifyField),
2282
+ h('div', { className: 'capital-automation-switchlist' },
2283
+ h(SwitchRow, { label: zh.notifyInApp, checked: draft.notifications.inApp, onChange: (value) => set((current) => ({ notifications: { ...current.notifications, inApp: value } })) }),
2284
+ h(SwitchRow, { label: zh.notifyFailure, checked: draft.notifications.onFailure, onChange: (value) => set((current) => ({ notifications: { ...current.notifications, onFailure: value } })) }),
2285
+ h(SwitchRow, { label: zh.notifySuccess, checked: draft.notifications.onSuccess, onChange: (value) => set((current) => ({ notifications: { ...current.notifications, onSuccess: value } })) })),
2286
+ h('p', { className: 'capital-automation-hint' }, zh.notifyHint)),
2287
+ error.length > 0 ? h('p', { className: 'capital-automation-error', role: 'alert' }, error) : null))
2288
+ }
2289
+
2290
+ function ConfirmModal(props) {
2291
+ const { confirm, busy, error, onCancel, onConfirm } = props
2292
+ const open = confirm !== null
2293
+ const cancelling = confirm?.kind === 'cancel-run'
2294
+ const deleting = confirm?.kind === 'delete'
2295
+ const title = cancelling ? zh.cancelRunConfirmTitle : deleting ? zh.confirmDeleteTitle : zh.confirmHibernateTitle
2296
+ const body = cancelling ? zh.cancelRunConfirmBody : deleting ? zh.confirmDeleteBody : zh.confirmHibernateBody
2297
+ const confirmLabel = cancelling ? zh.cancelRunConfirm : deleting ? zh.confirmDelete : zh.confirmHibernate
2298
+ return h(Modal, {
2299
+ open,
2300
+ onClose: () => { if (!busy) onCancel() },
2301
+ title,
2302
+ closeLabel: zh.close,
2303
+ className: 'capital-automation-modal capital-automation-modal-narrow',
2304
+ footer: h('div', { className: 'capital-automation-formactions', style: { borderTop: 0, paddingTop: 0 } },
2305
+ h('button', { type: 'button', className: 'capital-automation-btn', disabled: busy, onClick: onCancel }, zh.cancel),
2306
+ h('button', {
2307
+ type: 'button',
2308
+ className: 'capital-automation-btn',
2309
+ 'data-variant': 'danger',
2310
+ disabled: busy,
2311
+ onClick: onConfirm,
2312
+ }, busy ? zh.processing : confirmLabel)),
2313
+ },
2314
+ h('div', null,
2315
+ h('p', { className: 'capital-automation-policy' }, body),
2316
+ error.length > 0 ? h('p', { className: 'capital-automation-error', role: 'alert', style: { marginTop: 8 } }, error) : null))
2317
+ }
2318
+
2319
+ function InboxModal(props) {
2320
+ const { open, onClose, notifications, onMarkAllRead, onOpenTask, busy } = props
2321
+ const unread = notifications.filter((item) => item.readAt === undefined).length
2322
+ return h(Modal, {
2323
+ open,
2324
+ onClose,
2325
+ title: zh.notificationsTitle,
2326
+ closeLabel: zh.close,
2327
+ className: 'capital-automation-modal',
2328
+ contentClassName: 'capital-automation-modal-content',
2329
+ footer: h('div', { className: 'capital-automation-formactions', style: { borderTop: 0, paddingTop: 0 } },
2330
+ h('span', { className: 'capital-automation-formstatus' },
2331
+ unread > 0 ? zh.notificationsUnread.replace('{n}', String(unread)) : notifications.length > 0 ? zh.allRead : zh.notificationsEmpty),
2332
+ h('button', { type: 'button', className: 'capital-automation-btn', disabled: busy || unread === 0, onClick: onMarkAllRead }, zh.markRead),
2333
+ h('button', { type: 'button', className: 'capital-automation-btn', 'data-variant': 'primary', onClick: onClose }, zh.close)),
2334
+ },
2335
+ notifications.length === 0
2336
+ ? h('div', { className: 'capital-automation-empty' },
2337
+ h('span', { className: 'capital-automation-emptyicon' }, h(IconAlarmClockOutlineRegular, { size: 19 })),
2338
+ h('h3', null, zh.notificationsEmpty),
2339
+ h('p', null, zh.notificationsHint))
2340
+ : h('div', null,
2341
+ h('p', { className: 'capital-automation-hint', style: { marginBottom: 4 } }, zh.notificationsHint),
2342
+ notifications.map((item) => h(NotificationItem, {
2343
+ key: item.id,
2344
+ item,
2345
+ onOpenTask: onOpenTask,
2346
+ onClick: item.taskId === undefined ? undefined : () => onOpenTask(item.taskId),
2347
+ }))))
2348
+ }
2349
+
2350
+ function AutomationPage(props) {
2351
+ const { api, openSession } = props
2352
+ const [meta, setMeta] = useState(null)
2353
+ const [tasks, setTasks] = useState([])
2354
+ const [runs, setRuns] = useState([])
2355
+ const [notifications, setNotifications] = useState([])
2356
+ const [unread, setUnread] = useState(0)
2357
+ const [selectedId, setSelectedId] = useState(null)
2358
+ const [detail, setDetail] = useState(null)
2359
+ const [tab, setTab] = useState('tasks')
2360
+ const [detailTab, setDetailTab] = useState('overview')
2361
+ const [wizard, setWizard] = useState(false)
2362
+ const [inbox, setInbox] = useState(false)
2363
+ const [confirm, setConfirm] = useState(null)
2364
+ const [confirmBusy, setConfirmBusy] = useState(false)
2365
+ const [confirmError, setConfirmError] = useState('')
2366
+ const [busyTaskId, setBusyTaskId] = useState(null)
2367
+ const [inboxBusy, setInboxBusy] = useState(false)
2368
+ const [notice, setNotice] = useState('')
2369
+ const [loadError, setLoadError] = useState('')
2370
+ const mountedRef = useRef(false)
2371
+ const tasksRef = useRef([])
2372
+ const selectedIdRef = useRef(null)
2373
+ const refreshInFlightRef = useRef(null)
2374
+ const refreshSequenceRef = useRef(0)
2375
+ const detailSequenceRef = useRef(0)
2376
+ const scrollRef = useRef(null)
2377
+
2378
+ useEffect(() => {
2379
+ if (notice.length === 0) return undefined
2380
+ const timer = setTimeout(() => setNotice(''), 3_200)
2381
+ return () => clearTimeout(timer)
2382
+ }, [notice])
2383
+
2384
+ async function refresh() {
2385
+ if (refreshInFlightRef.current !== null) return refreshInFlightRef.current
2386
+ const requestSequence = refreshSequenceRef.current + 1
2387
+ refreshSequenceRef.current = requestSequence
2388
+ const request = (async () => {
2389
+ try {
2390
+ const [nextMeta, nextTasks, nextRuns, nextNotifications] = await Promise.all([
2391
+ api.meta(),
2392
+ api.tasks(),
2393
+ api.runs('?limit=200'),
2394
+ api.notifications(),
2395
+ ])
2396
+ if (!mountedRef.current || requestSequence !== refreshSequenceRef.current) return
2397
+ const nextTaskList = Array.isArray(nextTasks.tasks) ? nextTasks.tasks : []
2398
+ setMeta(nextMeta)
2399
+ tasksRef.current = nextTaskList
2400
+ setTasks(nextTaskList)
2401
+ setRuns(Array.isArray(nextRuns.runs) ? nextRuns.runs : [])
2402
+ setNotifications(Array.isArray(nextNotifications.notifications) ? nextNotifications.notifications : [])
2403
+ setUnread(typeof nextNotifications.unread === 'number' ? nextNotifications.unread : 0)
2404
+ setLoadError('')
2405
+ const currentId = selectedIdRef.current
2406
+ const nextId = currentId !== null && nextTaskList.some((task) => task.id === currentId) ? currentId : null
2407
+ if (nextId !== currentId) {
2408
+ selectedIdRef.current = nextId
2409
+ detailSequenceRef.current += 1
2410
+ setDetail(null)
2411
+ setSelectedId(nextId)
2412
+ if (nextId === null) setDetailTab('overview')
2413
+ }
2414
+ } catch (error) {
2415
+ if (mountedRef.current && requestSequence === refreshSequenceRef.current) {
2416
+ setLoadError(error.message ?? zh.errorGeneric)
2417
+ }
2418
+ }
2419
+ })()
2420
+ refreshInFlightRef.current = request
2421
+ try {
2422
+ await request
2423
+ } finally {
2424
+ if (refreshInFlightRef.current === request) refreshInFlightRef.current = null
2425
+ }
2426
+ }
2427
+
2428
+ async function loadDetail(id) {
2429
+ const requestSequence = detailSequenceRef.current + 1
2430
+ detailSequenceRef.current = requestSequence
2431
+ try {
2432
+ const value = await api.detail(id)
2433
+ if (!mountedRef.current || requestSequence !== detailSequenceRef.current || selectedIdRef.current !== id) return
2434
+ setDetail(value)
2435
+ } catch (error) {
2436
+ if (mountedRef.current && requestSequence === detailSequenceRef.current && selectedIdRef.current === id) {
2437
+ setLoadError(error.message ?? zh.errorGeneric)
2438
+ }
2439
+ }
2440
+ }
2441
+
2442
+ function scrollToTop() {
2443
+ const node = scrollRef.current
2444
+ if (node !== null && typeof node.scrollTo === 'function') node.scrollTo({ top: 0, behavior: 'auto' })
2445
+ }
2446
+
2447
+ function selectTask(id, nextTab = 'overview') {
2448
+ selectedIdRef.current = id
2449
+ detailSequenceRef.current += 1
2450
+ setDetailTab(nextTab)
2451
+ setDetail(null)
2452
+ setSelectedId(id)
2453
+ setTab('tasks')
2454
+ scrollToTop()
2455
+ }
2456
+
2457
+ function backToTasks() {
2458
+ selectedIdRef.current = null
2459
+ detailSequenceRef.current += 1
2460
+ setDetail(null)
2461
+ setSelectedId(null)
2462
+ setDetailTab('overview')
2463
+ scrollToTop()
2464
+ }
2465
+
2466
+ function switchTab(nextTab) {
2467
+ if (nextTab === tab && nextTab === 'tasks' && selectedIdRef.current !== null) return
2468
+ if (nextTab === 'runs' && selectedIdRef.current !== null) {
2469
+ selectedIdRef.current = null
2470
+ detailSequenceRef.current += 1
2471
+ setDetail(null)
2472
+ setSelectedId(null)
2473
+ }
2474
+ setTab(nextTab)
2475
+ scrollToTop()
2476
+ }
2477
+
2478
+ function requestConfirm(kind, task, run) {
2479
+ setConfirmError('')
2480
+ setConfirm({ kind, task, run })
2481
+ }
2482
+
2483
+ async function performTaskAction(task, action) {
2484
+ if (busyTaskId !== null) return
2485
+ setBusyTaskId(task.id)
2486
+ setLoadError('')
2487
+ try {
2488
+ if (action === 'run') {
2489
+ await api.run(task.id)
2490
+ setNotice(zh.runSubmitted.replace('{title}', task.title))
2491
+ } else if (action === 'copy') {
2492
+ const payload = await api.action(task.id, 'copy')
2493
+ setNotice(zh.copied.replace('{title}', task.title))
2494
+ if (payload?.task?.id) {
2495
+ await refresh()
2496
+ selectTask(payload.task.id, 'config')
2497
+ return
2498
+ }
2499
+ } else {
2500
+ await api.action(task.id, action)
2501
+ setNotice(action === 'pause' ? zh.paused.replace('{title}', task.title) : activationNotice(task))
2502
+ }
2503
+ await refresh()
2504
+ if (selectedIdRef.current === task.id) await loadDetail(task.id)
2505
+ } catch (failure) {
2506
+ setLoadError(failure.message ?? zh.errorGeneric)
2507
+ } finally {
2508
+ setBusyTaskId(null)
2509
+ }
2510
+ }
2511
+
2512
+ async function enableTemplate(task) {
2513
+ await performTaskAction(task, 'activate')
2514
+ }
2515
+
2516
+ async function confirmAction() {
2517
+ if (confirm === null || confirmBusy) return
2518
+ const { kind, task } = confirm
2519
+ setConfirmBusy(true)
2520
+ setConfirmError('')
2521
+ try {
2522
+ if (kind === 'cancel-run') {
2523
+ await api.cancel(confirm.run.id)
2524
+ setNotice(zh.runCanceled)
2525
+ } else if (kind === 'delete') {
2526
+ await api.remove(task.id)
2527
+ setNotice(zh.deleted.replace('{title}', task.title))
2528
+ } else {
2529
+ await api.action(task.id, 'hibernate')
2530
+ setNotice(zh.hibernated.replace('{title}', task.title))
2531
+ }
2532
+ setConfirm(null)
2533
+ setConfirmError('')
2534
+ await refresh()
2535
+ if (selectedIdRef.current === task.id) await loadDetail(task.id)
2536
+ } catch (failure) {
2537
+ setConfirmError(failure.message ?? zh.errorGeneric)
2538
+ } finally {
2539
+ setConfirmBusy(false)
2540
+ }
2541
+ }
2542
+
2543
+ async function markAllRead() {
2544
+ if (inboxBusy) return
2545
+ setInboxBusy(true)
2546
+ try {
2547
+ await api.markRead({ all: true })
2548
+ setNotice(zh.markReadDone)
2549
+ await refresh()
2550
+ } catch (failure) {
2551
+ setLoadError(failure.message ?? zh.errorGeneric)
2552
+ } finally {
2553
+ setInboxBusy(false)
2554
+ }
2555
+ }
2556
+
2557
+ async function openTaskFromNotification(taskId) {
2558
+ setInbox(false)
2559
+ if (tasksRef.current.some((task) => task.id === taskId)) {
2560
+ selectTask(taskId)
2561
+ return
2562
+ }
2563
+ await refresh()
2564
+ if (tasksRef.current.some((task) => task.id === taskId)) selectTask(taskId)
2565
+ }
2566
+
2567
+ useEffect(() => {
2568
+ mountedRef.current = true
2569
+ void refresh()
2570
+ const timer = setInterval(() => {
2571
+ if (document.visibilityState !== 'hidden') void refresh()
2572
+ }, 15_000)
2573
+ const onVisibilityChange = () => {
2574
+ if (document.visibilityState === 'visible') void refresh()
2575
+ }
2576
+ document.addEventListener?.('visibilitychange', onVisibilityChange)
2577
+ return () => {
2578
+ mountedRef.current = false
2579
+ refreshSequenceRef.current += 1
2580
+ detailSequenceRef.current += 1
2581
+ refreshInFlightRef.current = null
2582
+ clearInterval(timer)
2583
+ document.removeEventListener?.('visibilitychange', onVisibilityChange)
2584
+ }
2585
+ }, [])
2586
+
2587
+ useEffect(() => {
2588
+ if (selectedId === null) {
2589
+ setDetail(null)
2590
+ detailSequenceRef.current += 1
2591
+ return undefined
2592
+ }
2593
+ setDetail(null)
2594
+ void loadDetail(selectedId)
2595
+ return () => { detailSequenceRef.current += 1 }
2596
+ }, [selectedId])
2597
+
2598
+ const schedulerActive = meta?.scheduler?.state === 'active'
2599
+ const storageReady = meta?.storage?.state !== 'unavailable'
2600
+ const templates = tasks.filter((task) => task.origin === 'builtin' && task.state === 'dormant')
2601
+ const selectedTask = tasks.find((task) => task.id === selectedId)
2602
+ const detailOpen = tab === 'tasks' && selectedId !== null
2603
+ // `/meta` arrives in the same batch as `/tasks`; until then the empty state would be a lie.
2604
+ const initialLoading = meta === null && loadError.length === 0
2605
+
2606
+ return h('div', { className: 'capital-automation-page' },
2607
+ h('datalist', { id: 'capital-automation-timezones' },
2608
+ TIMEZONES.map((zone) => h('option', { key: zone, value: zone }))),
2609
+ detailOpen ? null : h(Header, {
2610
+ tab,
2611
+ onTab: switchTab,
2612
+ onNewTask: () => setWizard(true),
2613
+ unread,
2614
+ onOpenInbox: () => setInbox(true),
2615
+ }),
2616
+ h(PageBanners, { meta, loadError, onRetry: () => { void refresh() } }),
2617
+ h('div', { className: 'capital-automation-scroll', ref: scrollRef },
2618
+ h('div', { className: 'capital-automation-container' },
2619
+ tab === 'tasks'
2620
+ ? selectedId !== null
2621
+ ? h('div', null,
2622
+ h(DetailBar, { onBack: backToTasks }),
2623
+ detail === null
2624
+ ? h('div', { className: 'capital-automation-empty' }, zh.loading)
2625
+ : h(TaskDetailPage, {
2626
+ detail,
2627
+ meta,
2628
+ api,
2629
+ openSession,
2630
+ tab: detailTab,
2631
+ onTab: setDetailTab,
2632
+ onRequestConfirm: requestConfirm,
2633
+ onRequestCancelRun: (run) => requestConfirm('cancel-run', selectedTask ?? { id: run.taskId, title: run.taskTitle }, run),
2634
+ onOpenAllRuns: () => switchTab('runs'),
2635
+ schedulerActive,
2636
+ storageReady,
2637
+ onChanged: async (taskId) => {
2638
+ await refresh()
2639
+ const id = taskId ?? selectedIdRef.current
2640
+ if (id !== null && selectedIdRef.current === id) await loadDetail(id)
2641
+ },
2642
+ }))
2643
+ : initialLoading
2644
+ ? h(TaskDashboardSkeleton)
2645
+ : h(TaskDashboard, {
2646
+ tasks,
2647
+ templates,
2648
+ runs,
2649
+ onOpenTask: selectTask,
2650
+ onNewTask: () => setWizard(true),
2651
+ onEnableTemplate: (task) => { void enableTemplate(task) },
2652
+ onAction: (task, action) => { void performTaskAction(task, action) },
2653
+ onRequestConfirm: requestConfirm,
2654
+ onEdit: (id) => selectTask(id, 'config'),
2655
+ onGoRuns: () => switchTab('runs'),
2656
+ onRequestCancelRun: (task, run) => requestConfirm('cancel-run', task, run),
2657
+ schedulerActive,
2658
+ storageReady,
2659
+ busyTaskId,
2660
+ })
2661
+ : initialLoading
2662
+ ? h(RunsSkeleton)
2663
+ : h(RunsPage, {
2664
+ runs,
2665
+ tasks,
2666
+ onCancel: (run) => requestConfirm('cancel-run', { id: run.taskId, title: run.taskTitle }, run),
2667
+ onOpenSession: openSession,
2668
+ onOpenTask: (id) => { if (tasksRef.current.some((task) => task.id === id)) selectTask(id) },
2669
+ onGoTasks: () => switchTab('tasks'),
2670
+ onNewTask: () => setWizard(true),
2671
+ busy: confirmBusy,
2672
+ }))),
2673
+ h(NewTaskModal, {
2674
+ open: wizard,
2675
+ onClose: () => setWizard(false),
2676
+ api,
2677
+ meta,
2678
+ onCreated: async (taskId) => {
2679
+ setWizard(false)
2680
+ setNotice(zh.created)
2681
+ await refresh()
2682
+ if (taskId !== null && tasksRef.current.some((task) => task.id === taskId)) selectTask(taskId)
2683
+ },
2684
+ }),
2685
+ h(InboxModal, {
2686
+ open: inbox,
2687
+ onClose: () => setInbox(false),
2688
+ notifications,
2689
+ onMarkAllRead: () => { void markAllRead() },
2690
+ onOpenTask: (taskId) => { void openTaskFromNotification(taskId) },
2691
+ busy: inboxBusy,
2692
+ }),
2693
+ h(ConfirmModal, {
2694
+ confirm,
2695
+ busy: confirmBusy,
2696
+ error: confirmError,
2697
+ onCancel: () => { if (!confirmBusy) { setConfirm(null); setConfirmError('') } },
2698
+ onConfirm: () => { void confirmAction() },
2699
+ }),
2700
+ notice.length > 0 ? h('div', { className: 'capital-automation-toast', role: 'status' }, notice) : null)
2701
+ }
2702
+
2703
+ function AutomationIcon() {
2704
+ return h(IconChecklistOutlineRegular, { width: 18, height: 18 })
2705
+ }
2706
+
2707
+ function apply(ctx) {
2708
+ const api = createApi()
2709
+ const openSession = (sessionId) => {
2710
+ try {
2711
+ const workspace = ctx.get('uiWorkspace')
2712
+ workspace?.openSession?.(sessionId)
2713
+ } catch {
2714
+ // Session navigation is optional; the run detail stays readable without it.
2715
+ }
2716
+ }
2717
+ ctx.effect(() => installStyles(), `${ENTRY_NAME}: styles`)
2718
+ ctx.slots.inject('main', () => ctx.slots.register({
2719
+ name: 'main',
2720
+ key: PANEL_ID,
2721
+ inject: () => ({ api, openSession }),
2722
+ }, AutomationPage))
2723
+ ctx.slots.inject('sidebar.panellist', () => ctx.slots.register({
2724
+ name: 'sidebar.panellist',
2725
+ id: PANEL_ID,
2726
+ order: 11,
2727
+ label: () => zh.panel,
2728
+ }, AutomationIcon))
2729
+ ctx.logger?.info?.(`${ENTRY_NAME}: 客户端半边已挂载(页面 + 侧栏入口)`)
2730
+ }
2731
+
2732
+ exports.name = ENTRY_NAME
2733
+ exports.inject = ['slots']
2734
+ exports.apply = apply