@v587d/capital-generation 2.13.1 → 2.14.0

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.
@@ -7,6 +7,7 @@ const { createElement: h, useEffect, useRef, useState } = require('react')
7
7
  const {
8
8
  IconAlarmClockOutlineRegular,
9
9
  IconChecklistOutlineRegular,
10
+ IconChevronDownOutlineRegular,
10
11
  IconChevronLeftOutlineRegular,
11
12
  IconChevronRightOutlineRegular,
12
13
  IconClockOutlineRegular,
@@ -23,6 +24,7 @@ const {
23
24
  IconPlayOutlineRegular,
24
25
  IconPlusOutlineRegular,
25
26
  IconRefreshOutlineRegular,
27
+ IconRightUpOutlineRegular,
26
28
  IconSearchOutlineRegular,
27
29
  IconSparkleRegular,
28
30
  IconTrashOutlineRegular,
@@ -39,6 +41,7 @@ const PANEL_ID = 'capital-automation'
39
41
  const STYLE_TAG = 'capital-automation-css'
40
42
  const ACTIVE_RUN_STATUSES = ['queued', 'starting', 'running']
41
43
  const FAILURE_RUN_STATUSES = ['failed', 'timed_out', 'blocked_requires_user', 'unknown_after_restart']
44
+ const SKIPPED_RUN_STATUSES = ['skipped_overlap', 'skipped_calendar_unavailable', 'missed']
42
45
  const DEFAULT_LIMITS = { runTimeoutMinutes: 30, idleTimeoutMinutes: 5, maxSteps: 300, maxToolCalls: 1200 }
43
46
  const WEEKDAYS = [
44
47
  { value: 1, label: '一' },
@@ -49,8 +52,28 @@ const WEEKDAYS = [
49
52
  { value: 6, label: '六' },
50
53
  { value: 7, label: '日' },
51
54
  ]
52
- const TIMEZONES = ['Asia/Shanghai', 'Asia/Hong_Kong', 'Asia/Singapore', 'Asia/Tokyo', 'UTC', 'Europe/London', 'America/New_York', 'America/Los_Angeles']
55
+ const TIMEZONES = [
56
+ 'Asia/Shanghai',
57
+ 'Asia/Hong_Kong',
58
+ 'Asia/Taipei',
59
+ 'Asia/Singapore',
60
+ 'Asia/Tokyo',
61
+ 'Asia/Seoul',
62
+ 'UTC',
63
+ 'Europe/London',
64
+ 'Europe/Paris',
65
+ 'Europe/Berlin',
66
+ 'America/New_York',
67
+ 'America/Chicago',
68
+ 'America/Los_Angeles',
69
+ 'America/Toronto',
70
+ 'America/Sao_Paulo',
71
+ 'Australia/Sydney',
72
+ ]
53
73
  const HISTORY_DAYS = 30
74
+ const DETAIL_RECENT_RUN_LIMIT = 3
75
+ const TITLE_MAX_LENGTH = 12
76
+ const DESCRIPTION_MAX_LENGTH = 100
54
77
 
55
78
  const zh = {
56
79
  panel: 'CapGen 投研自动化',
@@ -102,11 +125,8 @@ const zh = {
102
125
  filterLabel: '筛选任务',
103
126
  category: { news: '资讯', market: '行情', review: '复盘', research: '研究' },
104
127
  categoryField: '任务分类',
105
- categoryFixed: '内置模板的分类固定;复制为我的任务后可以调整。',
106
- modeField: '输出侧重',
107
- modeHint: '只影响输出结构,不改变 Agent preset;所有运行固定使用 Capital 自动模式。',
108
- mode: { news: '资讯', market: '行情', review: '复盘', research: '研究' },
109
- cadence: { daily: '每天', weekly: '每周', monthly: '每月' },
128
+ categoryFixed: `'复制为我的任务'后,可调整任务分类。`,
129
+ cadence: { daily: '每天', weekly: '每周', monthly: '每月', once: '单次' },
110
130
  stateLabel: '状态',
111
131
  state: { dormant: '未添加', active: '已启用', paused: '已暂停' },
112
132
  origin: { builtin: '内置模板', custom: '我的任务' },
@@ -133,11 +153,17 @@ const zh = {
133
153
  cancel: '取消',
134
154
  create: '创建任务',
135
155
  creating: '创建中…',
136
- required: '标题和任务指令不能为空。',
156
+ titleRequired: '标题不能为空。',
157
+ instructionRequired: '任务指令不能为空。',
158
+ invalidLimit: '请输入有效数字。',
137
159
  dirty: '有未保存的修改',
138
160
  noDirty: '配置已同步',
139
161
  nextRun: '下次运行',
162
+ nextRunDormant: '添加后开始调度',
163
+ nextRunPaused: '启用后恢复调度',
164
+ notScheduled: '当前不会自动运行',
140
165
  lastResult: '上次结果',
166
+ lastRunEmpty: '运行后可在这里查看结果',
141
167
  never: '暂无',
142
168
  calendarUnavailable: '日历不可用',
143
169
  unread: '未读',
@@ -145,25 +171,47 @@ const zh = {
145
171
  config: '配置',
146
172
  runs: '运行记录',
147
173
  notifications: '通知',
148
- descriptionField: '说明',
149
- titleField: '标题',
174
+ descriptionField: '任务说明',
175
+ titleField: '任务标题',
150
176
  instructionField: '任务指令',
151
177
  instructionPlaceholder: '例如:读取今日自选股行情、公告与相关新闻,按「结论—证据—风险」整理成一份可复核的盘后复盘。',
152
178
  titlePlaceholder: '给这个任务起个名字',
179
+ charCount: '{n}/{max}',
153
180
  descriptionPlaceholder: '一句话说明任务目标(可选)',
154
181
  triggerField: '触发器',
155
- triggerHint: '按本地墙钟时间计算;仅交易日任务由交易日历判断周末与节假日。',
156
- triggerHintNatural: '按本地墙钟时间计算:每天都触发,周末与节假日也运行。',
182
+ cadenceField: '频率',
183
+ triggerHint: '按设定时区的本地墙钟时间计算;仅交易日任务由交易日历判断周末与节假日。',
184
+ triggerHintNatural: '按设定时区的本地墙钟时间计算:每天都触发,周末与节假日也运行。',
185
+ triggerHintOnce: '单次任务只在指定日期运行一次;该日期不是交易日时,仅交易日设置会跳过本次运行。',
157
186
  timeField: '时间',
158
187
  timezoneField: '时区',
159
188
  weekdayField: '星期',
160
189
  dayField: '日期',
190
+ monthlyField: '每月日期',
191
+ monthlyFirst: '1 号',
192
+ monthlyMid: '月中',
193
+ monthlyLast: '月末',
194
+ monthlyCustom: '自定义',
195
+ customDayLabel: '第 {n} 日',
196
+ dailyNatural: '每个自然日运行',
197
+ dailyTrading: '每个交易日运行',
198
+ onceHint: '只在这天运行一次',
199
+ onceEnded: '单次任务已结束',
200
+ onceEndedHint: '指定日期已过;修改日期或改成重复计划后可再次运行。',
201
+ onceSkipped: '单次任务不会运行',
202
+ onceSkippedHint: '指定日期不是交易日;关闭仅交易日或改选日期后再启用。',
203
+ weekdayWorkdayQuick: '工作日',
204
+ weekdayWeekendQuick: '周末',
205
+ weekdayNone: '未选择',
206
+ weekdayEvery: '每天',
207
+ done: '完成',
161
208
  tradingDaysField: '仅交易日',
162
209
  weekdayRequired: '每周至少选择一天。',
210
+ dateRequired: '请选择运行日期。',
211
+ invalidDate: '请选择有效的日期。',
163
212
  invalidTime: '运行时间格式不正确,请选择 HH:mm。',
164
213
  invalidTimezone: '时区不能为空,请填写 IANA 时区。',
165
214
  invalidTimezoneValue: '时区不被识别,请填写有效的 IANA 时区。',
166
- invalidLimits: '运行上限必须是有效数字。',
167
215
  filterNoRuns: '没有符合筛选条件的运行记录。',
168
216
  schedulePrefix: '下次',
169
217
  workspaceField: '工作区',
@@ -182,16 +230,14 @@ const zh = {
182
230
  notifyFailure: '失败时通知',
183
231
  notifySuccess: '成功时通知',
184
232
  notifyHint: '通知只记录在当前 profile;不会发送邮件或外部消息。',
185
- presetLabel: 'Agent preset',
186
- presetValue: 'capital-generation(自动模式)',
187
- policy: '执行策略',
188
- policyText: '固定 preset:capital-generation(Capital 自动模式)。工作区来自已注册列表;未选择时使用专属自动化工作区。无人值守运行禁止提问、写文件与 shell 提权。',
233
+ presetLabel: '运行模式',
234
+ presetValue: '自动模式',
235
+ presetHint: '所有运行固定使用自动模式。',
189
236
  budgetHint: '工具调用与步骤预算覆盖整棵运行子树:子 Agent 与 PTC 派发都计入。',
190
237
  environment: '执行环境',
191
238
  policyField: '无人值守策略',
192
239
  cwdKind: '工作区来源',
193
240
  techInfo: '技术信息',
194
- version: '版本',
195
241
  profileKey: 'profileKey',
196
242
  runHistory: '运行历史',
197
243
  last30Days: '最近 30 天',
@@ -257,10 +303,9 @@ const zh = {
257
303
  schedulerBlocked: '调度器未取得运行权,暂时不能手动运行。',
258
304
  runBlocked: '任务正在运行,结束或取消后才能再次运行。',
259
305
  configWhileRunning: '任务正在运行;修改会在下一次运行时生效。',
260
- configBuiltinHint: '这是内置模板;修改只覆盖当前 profile,休眠后会恢复默认。',
261
- detailRecentRuns: '最近运行',
306
+ detailRecentRuns: '最近 {n} 次运行',
262
307
  detailNoRuns: '还没有运行记录。',
263
- detailAllRuns: '查看运行页',
308
+ detailAllRuns: '完整记录',
264
309
  detailTaskNotifications: '任务通知',
265
310
  taskRunsLimit: '这里显示当前任务最近 20 次运行;全部运行见「运行」页。',
266
311
  runErrors: '{n} 条错误',
@@ -276,7 +321,6 @@ const zh = {
276
321
  lastRun: '上次运行',
277
322
  neverRun: '尚未运行',
278
323
  relativeNow: '刚刚',
279
- timezoneHint: 'IANA 时区,例如 Asia/Shanghai。',
280
324
  nextRunUnavailable: '交易日历不可用,无法计算下一次运行。',
281
325
  copyHint: '复制后会生成一个暂停状态的自定义任务。',
282
326
  notificationTaskGone: '任务已不存在',
@@ -288,7 +332,9 @@ const zh = {
288
332
  everyPrefix: '每',
289
333
  weeklyPrefix: '每周',
290
334
  monthlyPrefix: '每月',
335
+ monthlyLastDay: '最后一天',
291
336
  dailyPrefix: '每天',
337
+ oncePrefix: '单次',
292
338
  tradingSuffix: '(仅交易日)',
293
339
  sessionMissing: '没有可打开的 session',
294
340
  }
@@ -467,8 +513,6 @@ function installStyles() {
467
513
  .capital-automation-herotitle { display: flex; flex-wrap: wrap; align-items: center; gap: 8px; }
468
514
  .capital-automation-herotitle h2 { margin: 0; font-size: 22px; line-height: 30px; font-weight: 600; letter-spacing: .1px; overflow-wrap: anywhere; }
469
515
  .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
516
  .capital-automation-heroactions { display: flex; flex-wrap: wrap; align-items: center; gap: 7px; flex: none; padding-top: 2px; }
473
517
  .capital-automation-detailtabs { display: flex; align-items: center; gap: 26px; overflow-x: auto; overflow-y: hidden; border-bottom: .5px solid var(--ca-line); }
474
518
  .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; }
@@ -487,11 +531,13 @@ function installStyles() {
487
531
  .capital-automation-statvalue[data-tone="ok"] { color: var(--ca-success-text); }
488
532
  .capital-automation-statvalue[data-tone="error"] { color: var(--ca-error-text); }
489
533
  .capital-automation-statvalue[data-tone="running"] { color: var(--ca-business-text); }
534
+ .capital-automation-statvalue[data-tone="warn"] { color: var(--ca-warn-text); }
490
535
  .capital-automation-statsub { color: var(--ca-caption); font-size: 11px; line-height: 16px; }
491
536
  .capital-automation-panel { padding: 16px 18px; }
492
537
  .capital-automation-panelhead { display: flex; flex-wrap: wrap; align-items: center; justify-content: space-between; gap: 8px 12px; margin-bottom: 12px; }
493
538
  .capital-automation-panelhead h3 { margin: 0; font-size: 13.5px; line-height: 20px; font-weight: 600; }
494
539
  .capital-automation-panelhead .capital-automation-btn { margin-left: auto; }
540
+ .capital-automation-panelhead .capital-automation-btn[data-variant="ghost"] { padding-right: 0; }
495
541
  .capital-automation-kv { display: grid; grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); gap: 12px 18px; }
496
542
  .capital-automation-kvitem { min-width: 0; }
497
543
  .capital-automation-kvlabel { display: block; color: var(--ca-caption); font-size: 11px; line-height: 16px; }
@@ -505,32 +551,73 @@ function installStyles() {
505
551
  .capital-automation-error { margin: 0; color: var(--ca-error-text); font-size: 12px; line-height: 18px; }
506
552
  .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
553
  .capital-automation-form { display: flex; flex-direction: column; gap: 16px; }
554
+ .capital-automation-instructionrow { display: grid; grid-template-columns: minmax(320px, 1fr) minmax(420px, 520px); align-items: stretch; gap: 16px; }
555
+ .capital-automation-instructionside { display: flex; flex-direction: column; gap: 12px; min-width: 0; }
508
556
  .capital-automation-formgrid { display: grid; grid-template-columns: repeat(auto-fit, minmax(170px, 1fr)); gap: 12px; }
557
+ .capital-automation-titlerow { display: grid; grid-template-columns: minmax(0, 1fr) minmax(150px, 210px); align-items: start; gap: 12px; }
509
558
  .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; }
559
+ .capital-automation-fieldhead { display: flex; flex-wrap: wrap; align-items: baseline; gap: 2px 8px; min-width: 0; }
560
+ .capital-automation-fieldlabel { color: var(--ca-muted); font-size: 11.5px; line-height: 17px; }
561
+ .capital-automation-fieldhint { color: var(--ca-caption); font-size: 11px; line-height: 16px; font-variant-numeric: tabular-nums; }
562
+ .capital-automation-field input { 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; }
563
+ .capital-automation-field input { height: 34px; }
564
+ .capital-automation-texteditor { color: var(--dsw-alias-label-primary); font: inherit; white-space: pre-wrap; overflow-wrap: anywhere; overflow-y: auto; outline: 0; cursor: text; }
565
+ .capital-automation-field .capital-automation-texteditor { box-sizing: border-box; flex: 1 1 auto; min-height: 110px; padding: 9px 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: 19px; white-space: pre-wrap; overflow-wrap: anywhere; overflow-y: auto; outline: 0; cursor: text; }
566
+ .capital-automation-field .capital-automation-texteditor.capital-automation-descriptioninput { flex: none; height: 58px; min-height: 58px; }
567
+ .capital-automation-texteditor:empty:before { content: attr(data-placeholder); color: var(--ca-caption); pointer-events: none; }
568
+ .capital-automation-field .capital-automation-texteditor:hover { border-color: var(--ca-line); }
569
+ .capital-automation-field .capital-automation-texteditor:focus-visible { border-color: var(--ca-business); outline: 2px solid color-mix(in srgb, var(--ca-business) 22%, transparent); outline-offset: 0; }
570
+ .capital-automation-field[data-invalid="true"] .capital-automation-texteditor { border-color: color-mix(in srgb, var(--ca-error) 58%, var(--ca-line-soft)); }
571
+ .capital-automation-field[data-invalid="true"] .capital-automation-texteditor:focus-visible { border-color: var(--ca-error); outline-color: color-mix(in srgb, var(--ca-error) 22%, transparent); }
572
+ .capital-automation-field .capital-automation-texteditor[contenteditable="false"] { color: var(--ca-muted); opacity: .8; cursor: not-allowed; }
573
+ .capital-automation-field input:hover { border-color: var(--ca-line); }
574
+ .capital-automation-field input:focus-visible { border-color: var(--ca-business); outline: 2px solid color-mix(in srgb, var(--ca-business) 22%, transparent); outline-offset: 0; }
575
+ .capital-automation-field[data-invalid="true"] input { border-color: color-mix(in srgb, var(--ca-error) 58%, var(--ca-line-soft)); }
576
+ .capital-automation-field[data-invalid="true"] input:focus-visible { border-color: var(--ca-error); outline-color: color-mix(in srgb, var(--ca-error) 22%, transparent); }
577
+ .capital-automation-field input:disabled { color: var(--ca-muted); opacity: .8; }
578
+ .capital-automation-fielderror { color: var(--ca-error-text); font-size: 11px; line-height: 16px; }
579
+ .capital-automation-select { box-sizing: border-box; display: inline-flex; align-items: center; justify-content: space-between; gap: 10px; min-width: 0; height: 34px; padding: 0 10px 0 12px; border: 0; border-radius: 10px; background: var(--ca-field); color: var(--dsw-alias-label-primary); font: inherit; font-size: 12.5px; line-height: 20px; text-align: left; cursor: pointer; transition: background .15s, opacity .15s; }
580
+ .capital-automation-select:hover:not(:disabled) { background: var(--ca-hover); }
581
+ .capital-automation-select[data-block="true"] { width: 100%; }
582
+ .capital-automation-select[data-size="sm"] { height: 28px; padding: 0 8px 0 9px; border-radius: 8px; font-size: 11.5px; }
583
+ .capital-automation-select:focus-visible { outline: 2px solid var(--dsw-focus-ring-color, var(--ca-business)); outline-offset: 0; }
584
+ .capital-automation-select:disabled { opacity: .5; cursor: not-allowed; }
585
+ .capital-automation-selectvalue { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
586
+ .capital-automation-select > svg { flex: none; color: var(--ca-muted); }
517
587
  .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
588
  .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
589
  .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; }
590
+ .capital-automation-triggerbox { display: flex; flex-direction: column; gap: 12px; padding: 14px 16px; border: .5px solid var(--ca-line); border-radius: 14px; background: var(--ca-field); }
591
+ .capital-automation-triggerbox[data-invalid="true"] { border-color: color-mix(in srgb, var(--ca-error) 52%, var(--ca-line)); box-shadow: 0 0 0 3px color-mix(in srgb, var(--ca-error) 9%, transparent); }
592
+ .capital-automation-triggerhead { display: flex; flex-wrap: nowrap; align-items: center; justify-content: space-between; gap: 12px; min-width: 0; }
593
+ .capital-automation-triggerhead > .capital-automation-triggerfield:first-child { flex: 1 1 auto; min-width: 0; }
594
+ .capital-automation-triggerhead > .capital-automation-triggerfield:last-child { flex: none; }
595
+ .capital-automation-triggerfield { display: inline-flex; align-items: center; gap: 7px; min-width: 0; }
596
+ .capital-automation-triggerfield .capital-automation-select { flex: none; width: 96px; height: 32px; padding: 0 8px 0 10px; border: .5px solid var(--ca-line-soft); border-radius: 9px; background: var(--ca-card); font-size: 12.5px; }
597
+ .capital-automation-triggerfield .capital-automation-select:hover:not(:disabled) { border-color: var(--ca-line); background: var(--ca-card); }
598
+ .capital-automation-triggerlabel { color: var(--ca-muted); font-size: 11.5px; line-height: 16px; white-space: nowrap; }
599
+ .capital-automation-triggerbox input.capital-automation-triggertime, .capital-automation-triggerbox input.capital-automation-triggerdate { box-sizing: border-box; flex: none; width: 112px; height: 32px; padding: 0 8px; border: .5px solid var(--ca-line-soft); border-radius: 10px; background: var(--ca-card); color: var(--dsw-alias-label-primary); font: inherit; font-size: 12.5px; font-variant-numeric: tabular-nums; line-height: normal; transition: border-color .15s, box-shadow .15s; }
600
+ .capital-automation-triggerbox input.capital-automation-triggerdate { flex: 0 1 146px; width: 146px; min-width: 120px; }
601
+ .capital-automation-triggerbox input.capital-automation-triggertime:hover:not(:disabled), .capital-automation-triggerbox input.capital-automation-triggerdate:hover:not(:disabled) { border-color: var(--ca-line); }
602
+ .capital-automation-triggerbox input.capital-automation-triggertime:focus-visible, .capital-automation-triggerbox input.capital-automation-triggerdate:focus-visible { border-color: var(--ca-business); outline: 2px solid color-mix(in srgb, var(--ca-business) 22%, transparent); outline-offset: 0; }
603
+ .capital-automation-triggerbox input.capital-automation-triggertime[data-invalid="true"], .capital-automation-triggerbox input.capital-automation-triggerdate[data-invalid="true"] { border-color: color-mix(in srgb, var(--ca-error) 58%, var(--ca-line-soft)); }
604
+ .capital-automation-triggerbox input.capital-automation-triggertime[data-invalid="true"]:focus-visible, .capital-automation-triggerbox input.capital-automation-triggerdate[data-invalid="true"]:focus-visible { border-color: var(--ca-error); outline-color: color-mix(in srgb, var(--ca-error) 22%, transparent); }
605
+ .capital-automation-triggerbox input.capital-automation-triggertime:disabled, .capital-automation-triggerbox input.capital-automation-triggerdate:disabled { opacity: .5; cursor: not-allowed; }
606
+ body[data-ds-dark-theme] .capital-automation-triggertime, body[data-ds-dark-theme] .capital-automation-triggerdate { color-scheme: dark; }
607
+ body:not([data-ds-dark-theme]) .capital-automation-triggertime, body:not([data-ds-dark-theme]) .capital-automation-triggerdate { color-scheme: light; }
608
+ .capital-automation-triggerdetail { display: flex; flex: 1 1 auto; align-items: center; gap: 6px; min-width: 0; min-height: 32px; }
609
+ .capital-automation-triggerdetail > .capital-automation-triggerfield { flex: 1 1 auto; min-width: 0; }
610
+ .capital-automation-triggerdetail .capital-automation-triggerpick { flex: 1 1 auto; width: auto; min-width: 0; max-width: 190px; height: 32px; padding: 0 8px 0 10px; border: .5px solid var(--ca-line-soft); border-radius: 9px; background: var(--ca-card); font-size: 12px; }
611
+ .capital-automation-triggerdetail .capital-automation-triggerpick:hover:not(:disabled) { border-color: var(--ca-line); background: var(--ca-card); }
612
+ .capital-automation-monthpick [role="menu"] [role="menu"] { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 2px; width: 196px; min-width: 0; max-height: 252px; overflow-y: auto; padding: 6px; }
613
+ .capital-automation-monthpick [role="menu"] [role="menu"] button { justify-content: center; min-width: 0; min-height: 26px; padding: 3px 4px; font-size: 12px; line-height: 18px; }
614
+ .capital-automation-triggersettings { display: flex; flex-wrap: wrap; align-items: center; justify-content: space-between; gap: 8px 16px; padding-top: 12px; border-top: .5px solid var(--ca-line); }
615
+ .capital-automation-triggerzone { display: inline-flex; align-items: center; gap: 8px; min-width: 0; }
616
+ .capital-automation-triggersettings .capital-automation-select { max-width: 190px; height: 28px; padding: 0 8px 0 10px; border: .5px solid var(--ca-line-soft); border-radius: 9px; background: var(--ca-card); font-size: 11.5px; }
617
+ .capital-automation-triggersettings .capital-automation-select:hover:not(:disabled) { border-color: var(--ca-line); background: var(--ca-card); }
618
+ .capital-automation-triggerswitch { display: inline-flex; flex: none; align-items: center; gap: 7px; color: var(--dsw-alias-label-secondary); font-size: 12px; cursor: pointer; user-select: none; }
619
+ .capital-automation-triggererrors { display: flex; flex-direction: column; gap: 4px; }
532
620
  .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
621
  .capital-automation-advanced { border-top: .5px solid var(--ca-line); padding-top: 12px; }
535
622
  .capital-automation-advanced summary { color: var(--ca-muted); font-size: 12px; line-height: 18px; cursor: pointer; user-select: none; }
536
623
  .capital-automation-advanced summary:hover { color: var(--dsw-alias-label-primary); }
@@ -589,19 +676,22 @@ function installStyles() {
589
676
  .capital-automation-modalheadline { display: flex; align-items: center; gap: 10px; }
590
677
  .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
678
  .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; }
679
+ .capital-automation-modalheadline[data-invalid="true"] .capital-automation-modaltitleinput { border-color: color-mix(in srgb, var(--ca-error) 58%, var(--ca-line-soft)); }
592
680
  .capital-automation-formlabel { display: block; margin-bottom: 6px; color: var(--ca-muted); font-size: 11.5px; line-height: 17px; }
681
+ .capital-automation-promptfield { display: flex; flex-direction: column; gap: 5px; }
682
+ .capital-automation-promptfield[data-invalid="true"] .capital-automation-promptbox { border-color: color-mix(in srgb, var(--ca-error) 58%, var(--ca-line)); }
593
683
  .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); }
684
+ .capital-automation-promptbox .capital-automation-texteditor { box-sizing: border-box; width: 100%; height: 120px; min-height: 120px; padding: 13px 14px; border: 0; border-radius: 0; background: transparent; font-size: 13px; line-height: 21px; }
685
+ .capital-automation-promptbox .capital-automation-texteditor:focus-visible { outline: 0; }
596
686
  .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; }
687
+ .capital-automation-prompttoolbar label, .capital-automation-toolbarfield { display: inline-flex; align-items: center; gap: 5px; color: var(--ca-muted); font-size: 11.5px; }
688
+ .capital-automation-prompttoolbar .capital-automation-select { max-width: 168px; background: var(--ca-card); }
599
689
  .capital-automation-prompttoolbar .capital-automation-presetchip { margin-left: auto; }
600
690
  .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
691
  .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; }
692
+ .capital-automation-technical { margin-top: 14px; padding-top: 12px; border-top: .5px solid var(--ca-line); color: var(--ca-caption); font-size: 11px; line-height: 17px; }
693
+ .capital-automation-technicaltitle { margin: 0 0 7px; color: var(--ca-muted); font-size: 11.5px; line-height: 17px; font-weight: 500; }
694
+ .capital-automation-technical dl { display: grid; grid-template-columns: auto 1fr; gap: 3px 10px; margin: 0; }
605
695
  .capital-automation-technical dt { color: var(--ca-caption); }
606
696
  .capital-automation-technical dd { min-width: 0; margin: 0; overflow-wrap: anywhere; color: var(--ca-muted); }
607
697
  .capital-automation-page :focus-visible { outline: 2px solid var(--dsw-focus-ring-color, var(--ca-business)); outline-offset: 2px; }
@@ -611,6 +701,9 @@ function installStyles() {
611
701
  .capital-automation-theadmain > :nth-child(3), .capital-automation-cell[data-cell="next"] { display: none; }
612
702
  .capital-automation-statgrid { grid-template-columns: repeat(2, minmax(0, 1fr)); }
613
703
  }
704
+ @media (max-width: 960px) {
705
+ .capital-automation-instructionrow { grid-template-columns: minmax(0, 1fr); }
706
+ }
614
707
  @media (max-width: 760px) {
615
708
  .capital-automation-head { padding-top: 14px; }
616
709
  .capital-automation-topbar { display: flex; flex-wrap: wrap; gap: 10px; }
@@ -628,8 +721,16 @@ function installStyles() {
628
721
  .capital-automation-templates { grid-template-columns: 1fr; }
629
722
  .capital-automation-sectiontools { width: 100%; margin-left: 0; }
630
723
  }
724
+ @media (max-width: 560px) {
725
+ .capital-automation-triggerhead { flex-wrap: wrap; gap: 10px 12px; }
726
+ .capital-automation-triggerhead > .capital-automation-triggerfield:first-child { flex: 1 1 100%; flex-wrap: wrap; }
727
+ .capital-automation-triggerdetail { flex: 1 1 100%; }
728
+ .capital-automation-triggerhead > .capital-automation-triggerfield:last-child { flex: 1 1 100%; justify-content: flex-start; }
729
+ .capital-automation-monthpick [role="menu"] [role="menu"] { right: auto; left: 0; }
730
+ }
631
731
  @media (max-width: 520px) {
632
732
  .capital-automation-brand h1 { font-size: 16px; }
733
+ .capital-automation-titlerow { grid-template-columns: minmax(0, 1fr); }
633
734
  .capital-automation-topactions .capital-automation-btn:not([data-variant="contrast"]) { width: auto; min-width: 32px; padding: 0 8px; }
634
735
  .capital-automation-topactions .capital-automation-btn[data-variant="contrast"] { width: 32px; padding: 0; }
635
736
  .capital-automation-topactions .capital-automation-btn .capital-automation-btnlabel { display: none; }
@@ -719,10 +820,6 @@ function clipTagLabel(label, limit = 12) {
719
820
  return chars.length > limit ? `${chars.slice(0, limit).join('')}…` : text
720
821
  }
721
822
 
722
- function modeLabel(mode) {
723
- return zh.mode[mode] ?? mode
724
- }
725
-
726
823
  function calendarLabel(calendar) {
727
824
  if (calendar === null || calendar === undefined || calendar.available === false) return zh.calendarSourceNone
728
825
  const source = calendar.source === 'szse'
@@ -824,26 +921,61 @@ function formatRangeLabel(date) {
824
921
  return new Intl.DateTimeFormat('zh-CN', { month: 'short', day: 'numeric' }).format(date)
825
922
  }
826
923
 
924
+ function weekdayText(trigger) {
925
+ return (trigger.weekdays ?? []).slice().sort((left, right) => left - right)
926
+ .map((day) => WEEKDAYS.find((item) => item.value === day)?.label ?? '')
927
+ .join('、')
928
+ }
929
+
930
+ function monthlySummary(trigger) {
931
+ if (trigger.dayOfMonth === 'last') return '每月最后一天'
932
+ return `每月 ${trigger.dayOfMonth ?? 1} 日`
933
+ }
934
+
827
935
  function scheduleSummary(trigger) {
828
936
  if (trigger === null || trigger === undefined) return zh.never
829
937
  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
- }
938
+ if (trigger.cadence === 'weekly') return `${zh.weeklyPrefix}${weekdayText(trigger)} ${trigger.time}${suffix}`
939
+ if (trigger.cadence === 'monthly') return `${monthlySummary(trigger)} ${trigger.time}${suffix}`
940
+ if (trigger.cadence === 'once') return `${formatDateLabel(trigger.date)} ${trigger.time}${suffix}`
837
941
  return `${zh.dailyPrefix} ${trigger.time}${suffix}`
838
942
  }
839
943
 
840
944
  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}`
945
+ if (trigger?.cadence === 'weekly') return `${zh.weeklyPrefix}${weekdayText(trigger)}`
946
+ if (trigger?.cadence === 'monthly') return monthlySummary(trigger)
947
+ if (trigger?.cadence === 'once') return formatDateLabel(trigger.date)
948
+ return zh.dailyPrefix
949
+ }
950
+
951
+ // Dormant/paused tasks still carry a computed nextRunLocal, but that instant will not fire until
952
+ // the task is activated; don't present it as a pending schedule.
953
+ function nextRunView(task) {
954
+ if (task?.state === 'dormant') return { value: zh.nextRunDormant, sub: zh.notScheduled }
955
+ if (task?.state === 'paused') return { value: zh.nextRunPaused, sub: zh.notScheduled }
956
+ if (task?.trigger?.cadence === 'once' && !task.nextRunAt && task.nextRunUnavailable !== true) {
957
+ return onceHasPassed(task.trigger)
958
+ ? { value: zh.onceEnded, sub: zh.onceEndedHint }
959
+ : { value: zh.onceSkipped, sub: zh.onceSkippedHint }
844
960
  }
845
- if (trigger?.cadence === 'monthly') return `每月 ${trigger.dayOfMonth ?? 1} 日`
846
- return '每天'
961
+ const value = task?.nextRunLocal ?? (task?.nextRunUnavailable ? zh.calendarUnavailable : zh.never)
962
+ const sub = task?.nextRunAt ? formatRelative(task.nextRunAt) : (task?.nextRunUnavailable ? zh.nextRunUnavailable : zh.never)
963
+ return { value, sub }
964
+ }
965
+
966
+ function lastRunView(run, timeZone) {
967
+ if (run === null || run === undefined) return { value: zh.neverRun, sub: zh.lastRunEmpty }
968
+ const active = isActiveRun(run)
969
+ const parts = [formatShortTime(run.queuedAt, timeZone)]
970
+ if (!active && typeof run.durationMs === 'number') parts.push(formatDuration(run.durationMs))
971
+ const errors = Array.isArray(run.errors) ? run.errors.length : 0
972
+ if (errors > 0) parts.push(zh.runErrors.replace('{n}', String(errors)))
973
+ let tone
974
+ if (run.status === 'succeeded') tone = 'ok'
975
+ else if (isFailureStatus(run.status)) tone = 'error'
976
+ else if (active) tone = 'running'
977
+ else if (SKIPPED_RUN_STATUSES.includes(run.status)) tone = 'warn'
978
+ return { value: statusLabel(run.status), sub: parts.filter(Boolean).join(' · '), tone }
847
979
  }
848
980
 
849
981
  function workspaceOptions(meta, current) {
@@ -864,16 +996,120 @@ function workspaceTitle(meta, id) {
864
996
  return workspaceOptions(meta, id).find((item) => item.id === id)?.title ?? zh.workspaceMissing
865
997
  }
866
998
 
999
+ function timezoneOptions(current) {
1000
+ const zone = String(current ?? '').trim()
1001
+ const zones = zone.length > 0 && !TIMEZONES.includes(zone) ? [zone, ...TIMEZONES] : TIMEZONES
1002
+ return zones.map((value) => ({ value, label: value }))
1003
+ }
1004
+
1005
+ function dateStringFromParts(year, month, day) {
1006
+ const date = new Date(Date.UTC(year, month - 1, day))
1007
+ return date.toISOString().slice(0, 10)
1008
+ }
1009
+
1010
+ function zonedToday(timeZone) {
1011
+ try {
1012
+ const parts = new Intl.DateTimeFormat('en-US', {
1013
+ timeZone,
1014
+ year: 'numeric',
1015
+ month: '2-digit',
1016
+ day: '2-digit',
1017
+ }).formatToParts(new Date())
1018
+ const read = (type) => Number(parts.find((part) => part.type === type)?.value)
1019
+ return dateStringFromParts(read('year'), read('month'), read('day'))
1020
+ } catch {
1021
+ return new Date().toISOString().slice(0, 10)
1022
+ }
1023
+ }
1024
+
1025
+ function tomorrowInZone(timeZone) {
1026
+ const [year, month, day] = zonedToday(timeZone).split('-').map(Number)
1027
+ return dateStringFromParts(year, month, day + 1)
1028
+ }
1029
+
1030
+ function nextWeekdayOnOrAfter(value) {
1031
+ let current = value
1032
+ for (let attempt = 0; attempt < 7; attempt += 1) {
1033
+ const weekday = new Date(`${current}T00:00:00Z`).getUTCDay()
1034
+ if (weekday !== 0 && weekday !== 6) return current
1035
+ const [year, month, day] = current.split('-').map(Number)
1036
+ current = dateStringFromParts(year, month, day + 1)
1037
+ }
1038
+ return current
1039
+ }
1040
+
1041
+ /** A default single-run date must not land on a weekend while trading-only is on. */
1042
+ function defaultOnceDate(timeZone, tradingDaysOnly) {
1043
+ const tomorrow = tomorrowInZone(timeZone)
1044
+ return tradingDaysOnly === true ? nextWeekdayOnOrAfter(tomorrow) : tomorrow
1045
+ }
1046
+
1047
+ function zonedNowParts(timeZone) {
1048
+ try {
1049
+ const parts = new Intl.DateTimeFormat('en-US', {
1050
+ timeZone,
1051
+ year: 'numeric',
1052
+ month: '2-digit',
1053
+ day: '2-digit',
1054
+ hour: '2-digit',
1055
+ minute: '2-digit',
1056
+ hourCycle: 'h23',
1057
+ }).formatToParts(new Date())
1058
+ const read = (type) => Number(parts.find((part) => part.type === type)?.value)
1059
+ return {
1060
+ date: dateStringFromParts(read('year'), read('month'), read('day')),
1061
+ minutes: read('hour') * 60 + read('minute'),
1062
+ }
1063
+ } catch {
1064
+ const now = new Date()
1065
+ return { date: now.toISOString().slice(0, 10), minutes: now.getUTCHours() * 60 + now.getUTCMinutes() }
1066
+ }
1067
+ }
1068
+
1069
+ /** Distinguishes an expired single run from a future date that the calendar will skip. */
1070
+ function onceHasPassed(trigger) {
1071
+ if (trigger?.cadence !== 'once' || !isValidDateInput(trigger.date)) return false
1072
+ const now = zonedNowParts(trigger.timezone)
1073
+ if (trigger.date !== now.date) return trigger.date < now.date
1074
+ const [hour, minute] = String(trigger.time ?? '').split(':').map(Number)
1075
+ if (!Number.isInteger(hour) || !Number.isInteger(minute)) return false
1076
+ return now.minutes >= hour * 60 + minute
1077
+ }
1078
+
1079
+ function isValidDateInput(value) {
1080
+ const match = /^(\d{4})-(\d{2})-(\d{2})$/.exec(String(value ?? ''))
1081
+ if (match === null) return false
1082
+ const year = Number(match[1])
1083
+ const month = Number(match[2])
1084
+ const day = Number(match[3])
1085
+ if (year < 1970 || month < 1 || month > 12 || day < 1 || day > 31) return false
1086
+ const date = new Date(Date.UTC(year, month - 1, day))
1087
+ return date.getUTCFullYear() === year && date.getUTCMonth() === month - 1 && date.getUTCDate() === day
1088
+ }
1089
+
1090
+ function formatDateLabel(value) {
1091
+ const match = /^(\d{4})-(\d{2})-(\d{2})$/.exec(String(value ?? ''))
1092
+ if (match === null) return String(value ?? '')
1093
+ return `${Number(match[1])} 年 ${Number(match[2])} 月 ${Number(match[3])} 日`
1094
+ }
1095
+
867
1096
  function normalizeTrigger(trigger) {
1097
+ const cadence = Object.prototype.hasOwnProperty.call(zh.cadence, trigger?.cadence) ? trigger.cadence : 'daily'
868
1098
  const next = {
869
1099
  kind: 'schedule',
870
- cadence: trigger?.cadence ?? 'daily',
1100
+ cadence,
871
1101
  time: trigger?.time ?? '09:00',
872
1102
  timezone: trigger?.timezone ?? 'Asia/Shanghai',
873
1103
  tradingDaysOnly: trigger?.tradingDaysOnly === true,
874
1104
  }
875
1105
  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
1106
+ if (next.cadence === 'monthly') {
1107
+ const day = trigger?.dayOfMonth
1108
+ next.dayOfMonth = day === 'last' || (Number.isInteger(day) && day >= 1 && day <= 31) ? day : 1
1109
+ }
1110
+ if (next.cadence === 'once') {
1111
+ next.date = typeof trigger?.date === 'string' ? trigger.date : defaultOnceDate(next.timezone, next.tradingDaysOnly)
1112
+ }
877
1113
  return next
878
1114
  }
879
1115
 
@@ -890,7 +1126,6 @@ function defaultDraft() {
890
1126
  instruction: '',
891
1127
  category: 'research',
892
1128
  trigger: { kind: 'schedule', cadence: 'daily', time: '09:00', timezone: 'Asia/Shanghai', tradingDaysOnly: true },
893
- mode: 'research',
894
1129
  workspaceId: '',
895
1130
  limits: { ...DEFAULT_LIMITS },
896
1131
  notifications: { inApp: true, onFailure: true, onSuccess: false },
@@ -904,7 +1139,6 @@ function draftPayload(task) {
904
1139
  instruction: task.instruction ?? '',
905
1140
  category: task.category,
906
1141
  trigger: normalizeTrigger(task.trigger),
907
- mode: task.mode,
908
1142
  workspaceId: task.workspaceId ?? '',
909
1143
  limits: task.limits,
910
1144
  notifications: task.notifications,
@@ -974,19 +1208,50 @@ function draftChanged(draft, task) {
974
1208
  return !samePayload(draft, task)
975
1209
  }
976
1210
 
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
1211
+ function draftIssues(task) {
1212
+ const issues = {}
1213
+ if (String(task.title ?? '').trim().length === 0) issues.title = zh.titleRequired
1214
+ if (String(task.instruction ?? '').trim().length === 0) issues.instruction = zh.instructionRequired
1215
+ const trigger = task.trigger ?? {}
1216
+ if (!/^([01]\d|2[0-3]):[0-5]\d$/.test(trigger.time ?? '')) issues.time = zh.invalidTime
1217
+ const timezone = String(trigger.timezone ?? '').trim()
1218
+ if (timezone.length === 0) issues.timezone = zh.invalidTimezone
1219
+ else {
1220
+ try {
1221
+ new Intl.DateTimeFormat('zh-CN', { timeZone: timezone })
1222
+ } catch {
1223
+ issues.timezone = zh.invalidTimezoneValue
1224
+ }
1225
+ }
1226
+ if (trigger.cadence === 'weekly' && (trigger.weekdays ?? []).length === 0) issues.weekdays = zh.weekdayRequired
1227
+ if (trigger.cadence === 'once') {
1228
+ if (typeof trigger.date !== 'string' || trigger.date.length === 0) issues.date = zh.dateRequired
1229
+ else if (!isValidDateInput(trigger.date)) issues.date = zh.invalidDate
985
1230
  }
986
- if (task.trigger?.cadence === 'weekly' && (task.trigger.weekdays ?? []).length === 0) return zh.weekdayRequired
987
1231
  const limits = task.limits ?? {}
988
- if (![limits.runTimeoutMinutes, limits.idleTimeoutMinutes, limits.maxSteps, limits.maxToolCalls].every((value) => Number.isFinite(value))) return zh.invalidLimits
989
- return ''
1232
+ for (const key of ['runTimeoutMinutes', 'idleTimeoutMinutes', 'maxSteps', 'maxToolCalls']) {
1233
+ if (!Number.isFinite(limits[key])) issues[key] = zh.invalidLimit
1234
+ }
1235
+ return issues
1236
+ }
1237
+
1238
+ /**
1239
+ * Tracks which fields the user has left so errors appear on blur, then update live.
1240
+ * `resetKey` clears that state when the edited task or modal session changes.
1241
+ */
1242
+ function useDraftIssues(task, resetKey) {
1243
+ const [touched, setTouched] = useState({})
1244
+ const issues = draftIssues(task)
1245
+ useEffect(() => { setTouched({}) }, [resetKey])
1246
+ const errorFor = (field) => (touched[field] === true ? issues[field] : undefined)
1247
+ const touch = (field) => setTouched((current) => (current[field] === true ? current : { ...current, [field]: true }))
1248
+ const touchAll = () => setTouched((current) => {
1249
+ const next = { ...current }
1250
+ for (const field of Object.keys(issues)) next[field] = true
1251
+ return next
1252
+ })
1253
+ const clear = () => setTouched({})
1254
+ return { issues, errorFor, touch, touchAll, clear }
990
1255
  }
991
1256
 
992
1257
  function limitValue(meta, key, fallback) {
@@ -1039,6 +1304,21 @@ function RunRow(props) {
1039
1304
  const errorCount = Array.isArray(run.errors) ? run.errors.length : 0
1040
1305
  const [errorsOpen, setErrorsOpen] = useState(false)
1041
1306
  const errorPanelId = `capital-automation-run-errors-${run.id}`
1307
+ const errorBadge = errorCount > 0
1308
+ ? h('button', {
1309
+ type: 'button',
1310
+ className: 'capital-automation-runerrorbadge',
1311
+ 'data-tone': isFailureStatus(run.status) ? 'error' : 'warn',
1312
+ 'aria-expanded': errorsOpen ? 'true' : 'false',
1313
+ 'aria-controls': errorPanelId,
1314
+ 'aria-label': `${zh.runErrors.replace('{n}', String(errorCount))},${errorsOpen ? zh.hideErrorDetails : zh.showErrorDetails}`,
1315
+ title: errorsOpen ? zh.hideErrorDetails : zh.showErrorDetails,
1316
+ onClick: () => setErrorsOpen((current) => !current),
1317
+ },
1318
+ h(IconWarningOutlineRegular, { size: 12 }),
1319
+ zh.runErrors.replace('{n}', String(errorCount)),
1320
+ h(IconChevronRightOutlineRegular, { size: 12 }))
1321
+ : null
1042
1322
  return h('div', { className: 'capital-automation-run' },
1043
1323
  h('div', { className: 'capital-automation-runhead' },
1044
1324
  h('div', { className: 'capital-automation-runmain' },
@@ -1050,27 +1330,15 @@ function RunRow(props) {
1050
1330
  h('div', { className: 'capital-automation-runmetaline' },
1051
1331
  h('span', { className: 'capital-automation-runmeta' }, `${formatShortTime(run.queuedAt, task?.trigger?.timezone)} · ${run.trigger === 'manual' ? zh.triggerManual : zh.triggerScheduled}`),
1052
1332
  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}`))),
1333
+ h('span', { className: 'capital-automation-runmeta' }, `${zh.steps} ${run.steps} · ${zh.tools} ${run.toolCalls}`),
1334
+ errorBadge)),
1054
1335
  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
1336
  active && typeof onCancel === 'function'
1071
1337
  ? h('button', { type: 'button', className: 'capital-automation-btn', 'data-size': 'sm', 'data-variant': 'danger', disabled: busy, onClick: () => onCancel(run) }, zh.cancelRun)
1072
1338
  : run.sessionId && onOpenSession
1073
- ? h('button', { type: 'button', className: 'capital-automation-btn', 'data-size': 'sm', onClick: () => onOpenSession(run.sessionId) }, zh.openSession)
1339
+ ? h('button', { type: 'button', className: 'capital-automation-btn', 'data-size': 'sm', 'data-variant': 'primary', onClick: () => onOpenSession(run.sessionId) },
1340
+ h(IconRightUpOutlineRegular, { size: 13 }),
1341
+ zh.openSession)
1074
1342
  : null)),
1075
1343
  errorsOpen && errorCount > 0
1076
1344
  ? h('div', { id: errorPanelId, className: 'capital-automation-runerrors', role: 'region', 'aria-label': zh.errorDetailsLabel },
@@ -1266,7 +1534,7 @@ function TaskRow(props) {
1266
1534
  const { task, onOpen, menuOpen, onMenuToggle, onMenuClose, onEdit, onAction, onRequestConfirm, canRun, busy, mutationsDisabled } = props
1267
1535
  const run = task.lastRun
1268
1536
  const running = isActiveRun(run)
1269
- const nextLabel = task.nextRunLocal ?? (task.nextRunUnavailable ? zh.calendarUnavailable : zh.never)
1537
+ const next = nextRunView(task)
1270
1538
  return h('div', { className: 'capital-automation-trow' },
1271
1539
  h('button', { type: 'button', className: 'capital-automation-trowmain', onClick: () => onOpen(task.id) },
1272
1540
  h('span', { className: 'capital-automation-cell capital-automation-identity' },
@@ -1281,8 +1549,8 @@ function TaskRow(props) {
1281
1549
  h('span', { className: 'capital-automation-schedule' }, scheduleSummary(task.trigger)),
1282
1550
  h('span', { className: 'capital-automation-schedulesub' }, task.trigger?.timezone ?? '')),
1283
1551
  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))),
1552
+ h('span', { className: 'capital-automation-nextmain' }, next.value),
1553
+ h('span', { className: 'capital-automation-nextsub' }, next.sub)),
1286
1554
  h('span', { className: 'capital-automation-cell' },
1287
1555
  h(StateBadge, { state: task.state, running }))),
1288
1556
  h('div', { className: 'capital-automation-trowmenu' },
@@ -1347,7 +1615,9 @@ function TemplateCard(props) {
1347
1615
  h('span', { className: 'capital-automation-templatemeta' },
1348
1616
  h(IconClockOutlineRegular, { size: 12 }),
1349
1617
  `${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)))
1618
+ h('button', { type: 'button', className: 'capital-automation-btn', 'data-size': 'sm', 'data-variant': 'primary', onClick: () => onEnable(task), disabled: busy || disabled },
1619
+ h(IconPlusOutlineRegular, { size: 12 }),
1620
+ busy ? zh.templateAdding : zh.templateAdd)))
1351
1621
  }
1352
1622
 
1353
1623
  function EmptyState(props) {
@@ -1597,75 +1867,233 @@ function TaskDashboard(props) {
1597
1867
  : null)
1598
1868
  }
1599
1869
 
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)
1870
+ function WeekdaySelect(props) {
1871
+ const { value, onChange, disabled } = props
1872
+ const [open, setOpen] = useState(false)
1873
+ const selected = Array.isArray(value) ? value : []
1874
+ const has = (day) => selected.includes(day)
1875
+ const exact = (days) => selected.length === days.length && days.every(has)
1876
+ const workday = exact([1, 2, 3, 4, 5])
1877
+ const weekend = exact([6, 7])
1878
+ const everyDay = exact(WEEKDAYS.map((day) => day.value))
1879
+ const summary = selected.length === 0
1880
+ ? zh.weekdayNone
1881
+ : everyDay
1882
+ ? zh.weekdayEvery
1883
+ : workday
1884
+ ? zh.weekdayWorkdayQuick
1885
+ : weekend
1886
+ ? zh.weekdayWeekendQuick
1887
+ : WEEKDAYS.filter((day) => has(day.value)).map((day) => day.label).join('、')
1888
+ useEffect(() => {
1889
+ if (disabled === true && open) setOpen(false)
1890
+ }, [disabled, open])
1891
+ const items = [
1892
+ { id: 'workday', label: zh.weekdayWorkdayQuick },
1893
+ { id: 'weekend', label: zh.weekdayWeekendQuick },
1894
+ { type: 'separator', id: 'weekday-separator' },
1895
+ ...WEEKDAYS.map((day) => ({ id: `weekday-${day.value}`, label: `周${day.label}` })),
1896
+ ]
1897
+ const selectedIds = [
1898
+ ...selected.map((day) => `weekday-${day}`),
1899
+ ...(workday ? ['workday'] : []),
1900
+ ...(weekend ? ['weekend'] : []),
1901
+ ]
1902
+ const pick = (id) => {
1903
+ if (id === 'weekday-done') { setOpen(false); return }
1904
+ if (id === 'workday') { onChange([1, 2, 3, 4, 5]); return }
1905
+ if (id === 'weekend') { onChange([6, 7]); return }
1906
+ const day = Number(id.slice('weekday-'.length))
1907
+ if (!WEEKDAYS.some((item) => item.value === day)) return
1908
+ const next = has(day) ? selected.filter((item) => item !== day) : [...selected, day].sort((left, right) => left - right)
1909
+ if (next.length > 0) onChange(next)
1606
1910
  }
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)
1911
+ return h(Menu, {
1912
+ open,
1913
+ anchor: h('button', {
1914
+ type: 'button',
1915
+ className: 'capital-automation-select capital-automation-triggerpick',
1916
+ disabled,
1917
+ 'aria-label': `${zh.weekdayField}:${summary}`,
1918
+ 'aria-haspopup': 'menu',
1919
+ 'aria-expanded': open ? 'true' : 'false',
1920
+ title: `${zh.weekdayField}:${summary}`,
1921
+ onClick: () => setOpen((current) => !current),
1922
+ },
1923
+ h('span', { className: 'capital-automation-selectvalue' }, summary),
1924
+ h(IconChevronDownOutlineRegular, { size: 14 })),
1925
+ items,
1926
+ selectedIds,
1927
+ onSelect: pick,
1928
+ onClose: () => setOpen(false),
1929
+ footer: [{ id: 'weekday-done', label: zh.done }],
1930
+ align: 'start',
1931
+ portal: true,
1932
+ })
1933
+ }
1934
+
1935
+ function MonthlySelect(props) {
1936
+ const { value, onChange, disabled } = props
1937
+ const [open, setOpen] = useState(false)
1938
+ const day = value === 'last' ? 'last' : Number(value ?? 1)
1939
+ const summary = day === 'last'
1940
+ ? zh.monthlyLast
1941
+ : day === 1
1942
+ ? zh.monthlyFirst
1943
+ : day === 15
1944
+ ? zh.monthlyMid
1945
+ : zh.dayOfMonth.replace('{n}', String(day))
1946
+ const selectedId = day === 'last' ? 'last' : day === 1 ? 'first' : day === 15 ? 'mid' : 'custom'
1947
+ useEffect(() => {
1948
+ if (disabled === true && open) setOpen(false)
1949
+ }, [disabled, open])
1950
+ const items = [
1951
+ { id: 'first', label: zh.monthlyFirst },
1952
+ { id: 'mid', label: zh.monthlyMid },
1953
+ { id: 'last', label: zh.monthlyLast },
1954
+ {
1955
+ id: 'custom',
1956
+ label: zh.monthlyCustom,
1957
+ submenu: Array.from({ length: 31 }, (_, index) => ({
1958
+ id: `monthly-day-${index + 1}`,
1959
+ label: zh.dayOfMonth.replace('{n}', String(index + 1)),
1960
+ })),
1961
+ },
1962
+ ]
1963
+ const pick = (id) => {
1964
+ if (id === 'first') { onChange(1); setOpen(false); return }
1965
+ if (id === 'mid') { onChange(15); setOpen(false); return }
1966
+ if (id === 'last') { onChange('last'); setOpen(false); return }
1967
+ if (id.startsWith('monthly-day-')) {
1968
+ onChange(Number(id.slice('monthly-day-'.length)))
1969
+ setOpen(false)
1614
1970
  }
1615
- update({ weekdays: [...selected].sort((left, right) => left - right) })
1616
1971
  }
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)
1972
+ return h(Menu, {
1973
+ open,
1974
+ className: 'capital-automation-monthpick',
1975
+ anchor: h('button', {
1976
+ type: 'button',
1977
+ className: 'capital-automation-select capital-automation-triggerpick',
1978
+ disabled,
1979
+ 'aria-label': `${zh.monthlyField}:${summary}`,
1980
+ 'aria-haspopup': 'menu',
1981
+ 'aria-expanded': open ? 'true' : 'false',
1982
+ title: `${zh.monthlyField}:${summary}`,
1983
+ onClick: () => setOpen((current) => !current),
1984
+ },
1985
+ h('span', { className: 'capital-automation-selectvalue' }, summary),
1986
+ h(IconChevronDownOutlineRegular, { size: 14 })),
1987
+ items,
1988
+ selectedId,
1989
+ onSelect: pick,
1990
+ onClose: () => setOpen(false),
1991
+ align: 'start',
1992
+ portal: false,
1993
+ })
1994
+ }
1995
+
1996
+ function TriggerFields(props) {
1997
+ const { trigger, onChange, disabled, meta, errorFor, touch } = props
1998
+ const update = (patch) => onChange(normalizeTrigger({ ...trigger, ...patch }))
1999
+ const setCadence = (cadence) => onChange(triggerChanged(cadence, trigger))
2000
+
2001
+ const issue = (field) => (typeof errorFor === 'function' ? errorFor(field) : undefined)
2002
+ const touchField = (field) => { if (typeof touch === 'function') touch(field) }
2003
+ const timeError = issue('time')
2004
+ const timezoneError = issue('timezone')
2005
+ const weekdayError = issue('weekdays')
2006
+ const dateError = issue('date')
2007
+ const errors = [timeError, timezoneError, weekdayError, dateError]
2008
+ .filter((message) => typeof message === 'string' && message.length > 0)
2009
+ const cadenceOptions = Object.entries(zh.cadence).map(([value, label]) => ({ value, label }))
2010
+
2011
+ return h('div', {
2012
+ className: 'capital-automation-triggerbox',
2013
+ 'data-cadence': trigger.cadence,
2014
+ 'data-invalid': errors.length > 0 ? 'true' : undefined,
2015
+ },
2016
+ h('div', { className: 'capital-automation-triggerhead' },
2017
+ h('div', { className: 'capital-automation-triggerfield' },
2018
+ h('span', { className: 'capital-automation-triggerlabel' }, zh.cadenceField),
2019
+ h(Select, {
2020
+ value: trigger.cadence,
2021
+ options: cadenceOptions,
2022
+ onChange: setCadence,
2023
+ disabled,
2024
+ label: zh.cadenceField,
2025
+ }),
2026
+ h('div', { className: 'capital-automation-triggerdetail', 'data-cadence': trigger.cadence },
2027
+ trigger.cadence === 'weekly'
2028
+ ? h('div', { className: 'capital-automation-triggerfield' },
2029
+ h('span', { className: 'capital-automation-triggerlabel' }, zh.weekdayField),
2030
+ h(WeekdaySelect, {
2031
+ value: trigger.weekdays ?? [],
2032
+ onChange: (weekdays) => update({ weekdays }),
2033
+ disabled,
2034
+ }))
2035
+ : trigger.cadence === 'monthly'
2036
+ ? h('div', { className: 'capital-automation-triggerfield' },
2037
+ h('span', { className: 'capital-automation-triggerlabel' }, zh.dayField),
2038
+ h(MonthlySelect, {
2039
+ value: trigger.dayOfMonth ?? 1,
2040
+ onChange: (dayOfMonth) => update({ dayOfMonth }),
2041
+ disabled,
2042
+ }))
2043
+ : trigger.cadence === 'once'
2044
+ ? h('div', { className: 'capital-automation-triggerfield' },
2045
+ h('span', { className: 'capital-automation-triggerlabel' }, zh.dayField),
2046
+ h('input', {
2047
+ type: 'date',
2048
+ className: 'capital-automation-triggerdate',
2049
+ value: trigger.date ?? '',
2050
+ disabled,
2051
+ title: zh.onceHint,
2052
+ 'aria-label': zh.dayField,
2053
+ 'aria-invalid': dateError ? 'true' : undefined,
2054
+ 'data-invalid': dateError ? 'true' : undefined,
2055
+ onChange: (event) => update({ date: event.target.value }),
2056
+ onBlur: () => touchField('date'),
2057
+ }))
2058
+ : null)),
2059
+ h('div', { className: 'capital-automation-triggerfield' },
2060
+ h('span', { className: 'capital-automation-triggerlabel' }, zh.timeField),
2061
+ h('input', {
2062
+ type: 'time',
2063
+ className: 'capital-automation-triggertime',
2064
+ value: trigger.time,
2065
+ disabled,
2066
+ 'aria-label': zh.timeField,
2067
+ 'aria-invalid': timeError ? 'true' : undefined,
2068
+ 'data-invalid': timeError ? 'true' : undefined,
2069
+ onChange: (event) => update({ time: event.target.value }),
2070
+ onBlur: () => touchField('time'),
2071
+ }))),
2072
+ h('div', { className: 'capital-automation-triggersettings' },
2073
+ h('span', { className: 'capital-automation-triggerzone' },
2074
+ h('span', { className: 'capital-automation-triggerlabel' }, zh.timezoneField),
2075
+ h(Select, {
2076
+ value: trigger.timezone,
2077
+ options: timezoneOptions(trigger.timezone),
2078
+ onChange: (timezone) => update({ timezone }),
2079
+ disabled,
2080
+ label: zh.timezoneField,
2081
+ size: 'sm',
2082
+ align: 'end',
2083
+ })),
2084
+ h('label', { className: 'capital-automation-triggerswitch' },
2085
+ h(Switch, {
2086
+ checked: trigger.tradingDaysOnly === true,
2087
+ onChange: (value) => update({ tradingDaysOnly: value }),
2088
+ label: zh.tradingDaysField,
2089
+ disabled,
2090
+ }),
2091
+ h('span', null, zh.tradingDaysField))),
2092
+ errors.length > 0
2093
+ ? h('div', { className: 'capital-automation-triggererrors' },
2094
+ errors.map((message) => h('span', { key: message, className: 'capital-automation-fielderror', role: 'alert' }, message)))
2095
+ : null,
2096
+ meta?.calendar?.available === false ? h('p', { className: 'capital-automation-hint' }, zh.nextRunUnavailable) : null)
1669
2097
  }
1670
2098
 
1671
2099
  function SwitchRow(props) {
@@ -1677,24 +2105,163 @@ function SwitchRow(props) {
1677
2105
  h(Switch, { checked: checked === true, onChange, label, disabled }))
1678
2106
  }
1679
2107
 
2108
+ function FieldHead(props) {
2109
+ const { label, note, error } = props
2110
+ const invalid = typeof error === 'string' && error.length > 0
2111
+ return h('span', { className: 'capital-automation-fieldhead' },
2112
+ h('span', { className: 'capital-automation-fieldlabel' }, label),
2113
+ invalid
2114
+ ? h('span', { className: 'capital-automation-fielderror', role: 'alert' }, error)
2115
+ : note ? h('span', { className: 'capital-automation-fieldhint' }, note) : null)
2116
+ }
2117
+
2118
+ /**
2119
+ * Fixed-height plain-text box. A native textarea brings a resize handle and browser chrome that
2120
+ * break the two-column height contract, so the editable surface is a contenteditable div; paste is
2121
+ * forced to plain text and maxLength is applied on input.
2122
+ */
2123
+ function PlainTextEditor(props) {
2124
+ const { value, onChange, onBlur, placeholder, disabled, ariaLabel, invalid, className, maxLength } = props
2125
+ const ref = useRef(null)
2126
+ const limit = typeof maxLength === 'number' ? maxLength : undefined
2127
+ useEffect(() => {
2128
+ const node = ref.current
2129
+ if (node === null || node === undefined) return
2130
+ const next = value ?? ''
2131
+ if (document.activeElement !== node && node.innerText !== next) node.innerText = next
2132
+ }, [value])
2133
+ const read = (node) => (node.innerText ?? '').replace(/\r\n?/g, '\n')
2134
+ const commit = (node) => {
2135
+ let text = read(node)
2136
+ if (limit !== undefined && Array.from(text).length > limit) {
2137
+ text = Array.from(text).slice(0, limit).join('')
2138
+ node.innerText = text
2139
+ const range = document.createRange()
2140
+ range.selectNodeContents(node)
2141
+ range.collapse(false)
2142
+ const selection = window.getSelection()
2143
+ selection?.removeAllRanges()
2144
+ selection?.addRange(range)
2145
+ }
2146
+ if (text.length === 0 && node.innerHTML !== '') node.innerHTML = ''
2147
+ onChange(text)
2148
+ }
2149
+ return h('div', {
2150
+ ref,
2151
+ className,
2152
+ contentEditable: disabled === true ? 'false' : 'true',
2153
+ role: 'textbox',
2154
+ 'aria-multiline': 'true',
2155
+ 'aria-label': ariaLabel,
2156
+ 'aria-invalid': invalid ? 'true' : undefined,
2157
+ 'data-placeholder': placeholder,
2158
+ spellCheck: false,
2159
+ suppressContentEditableWarning: true,
2160
+ onInput: (event) => commit(event.currentTarget),
2161
+ onBlur: (event) => { commit(event.currentTarget); if (typeof onBlur === 'function') onBlur() },
2162
+ onPaste: (event) => {
2163
+ const text = event.clipboardData?.getData('text/plain')
2164
+ if (typeof text !== 'string') return
2165
+ event.preventDefault()
2166
+ const selection = window.getSelection()
2167
+ if (selection === null || selection.rangeCount === 0) return
2168
+ selection.deleteFromDocument()
2169
+ const range = selection.getRangeAt(0)
2170
+ const node = document.createTextNode(text)
2171
+ range.insertNode(node)
2172
+ range.setStartAfter(node)
2173
+ range.collapse(true)
2174
+ selection.removeAllRanges()
2175
+ selection.addRange(range)
2176
+ commit(event.currentTarget)
2177
+ },
2178
+ })
2179
+ }
2180
+
1680
2181
  function Field(props) {
1681
- const { label, hint, children } = props
1682
- return h('label', { className: 'capital-automation-field' },
1683
- h('span', null, label),
2182
+ const { label, hint, note, error, children } = props
2183
+ const invalid = typeof error === 'string' && error.length > 0
2184
+ return h('label', { className: 'capital-automation-field', 'data-invalid': invalid ? 'true' : undefined },
2185
+ h(FieldHead, { label, note, error }),
1684
2186
  children,
1685
2187
  hint ? h('span', { className: 'capital-automation-hint' }, hint) : null)
1686
2188
  }
1687
2189
 
2190
+ /**
2191
+ * DSH General-settings style selector: a filled pill plus the shared Menu surface.
2192
+ * Native <select> is intentionally avoided so light/dark themes follow host tokens.
2193
+ */
2194
+ function Select(props) {
2195
+ const { value, options, onChange, disabled, label, block, size, align = 'start', title } = props
2196
+ const [open, setOpen] = useState(false)
2197
+ const triggerRef = useRef(null)
2198
+ const items = options.map((option) => ({
2199
+ id: String(option.value),
2200
+ label: option.label,
2201
+ disabled: option.disabled === true,
2202
+ }))
2203
+ const currentValue = String(value ?? '')
2204
+ const current = items.find((item) => item.id === currentValue) ?? items[0]
2205
+ useEffect(() => {
2206
+ if (disabled === true && open) setOpen(false)
2207
+ }, [disabled, open])
2208
+ const closeMenu = () => setOpen(false)
2209
+ const selectValue = (id) => {
2210
+ setOpen(false)
2211
+ triggerRef.current?.focus?.({ preventScroll: true })
2212
+ if (id !== currentValue) onChange(id)
2213
+ }
2214
+ const trigger = h('button', {
2215
+ ref: triggerRef,
2216
+ type: 'button',
2217
+ className: 'capital-automation-select',
2218
+ 'data-block': block === true ? 'true' : undefined,
2219
+ 'data-size': size,
2220
+ disabled,
2221
+ 'aria-label': current?.label ? `${label}:${current.label}` : label,
2222
+ 'aria-haspopup': 'menu',
2223
+ 'aria-expanded': open ? 'true' : 'false',
2224
+ title: title ?? current?.label,
2225
+ onClick: () => setOpen((currentOpen) => !currentOpen),
2226
+ },
2227
+ h('span', {
2228
+ className: 'capital-automation-selectvalue',
2229
+ title: title ?? current?.label,
2230
+ }, current?.label ?? ''),
2231
+ h(IconChevronDownOutlineRegular, { size: size === 'sm' ? 12 : 14 }))
2232
+ return h(Menu, {
2233
+ open,
2234
+ anchor: trigger,
2235
+ items,
2236
+ selectedId: currentValue,
2237
+ onSelect: selectValue,
2238
+ onClose: closeMenu,
2239
+ align,
2240
+ portal: true,
2241
+ })
2242
+ }
2243
+
2244
+ function SelectField(props) {
2245
+ const { label, hint, note, error, ariaLabel, block = true, title, ...rest } = props
2246
+ const invalid = typeof error === 'string' && error.length > 0
2247
+ return h('div', { className: 'capital-automation-field', title, 'data-invalid': invalid ? 'true' : undefined },
2248
+ h(FieldHead, { label, note, error }),
2249
+ h(Select, { ...rest, label: ariaLabel ?? label, block, title }),
2250
+ hint ? h('span', { className: 'capital-automation-hint' }, hint) : null)
2251
+ }
2252
+
1688
2253
  function NumberField(props) {
1689
- const { label, value, min, max, disabled, onChange } = props
1690
- return h(Field, { label },
2254
+ const { label, value, min, max, disabled, onChange, error, onBlur } = props
2255
+ return h(Field, { label, error },
1691
2256
  h('input', {
1692
2257
  type: 'number',
1693
2258
  min,
1694
2259
  max,
1695
2260
  value,
1696
2261
  disabled,
2262
+ 'aria-invalid': error ? 'true' : undefined,
1697
2263
  onChange: (event) => onChange(Number(event.target.value)),
2264
+ onBlur,
1698
2265
  }))
1699
2266
  }
1700
2267
 
@@ -1755,53 +2322,48 @@ function RunsPage(props) {
1755
2322
  }
1756
2323
 
1757
2324
  function OverviewTab(props) {
1758
- const { detail, meta, onGoRuns } = props
2325
+ const { detail, meta, onGoRuns, onCancelRun, onOpenSession } = props
1759
2326
  const latest = detail.runs[0]
1760
- const next = detail.task.nextRunLocal ?? (detail.task.nextRunUnavailable ? zh.calendarUnavailable : zh.never)
2327
+ const next = nextRunView(detail.task)
2328
+ const last = lastRunView(latest, detail.task.trigger.timezone)
1761
2329
  const anyRunning = detail.runs.some((run) => isActiveRun(run))
1762
2330
  const stateText = anyRunning ? zh.status.running : stateLabel(detail.task.state)
2331
+ const originLabel = detail.task.origin === 'builtin' ? zh.origin.builtin : zh.origin.custom
2332
+ const scheduleText = `${scheduleRangeSummary(detail.task.trigger)}${detail.task.trigger.tradingDaysOnly === true ? ` · ${zh.tradingDaysField}` : ''}`
2333
+ const scheduleHint = detail.task.trigger.cadence === 'once'
2334
+ ? zh.triggerHintOnce
2335
+ : detail.task.trigger.tradingDaysOnly === true ? zh.triggerHint : zh.triggerHintNatural
1763
2336
  return h('div', { className: 'capital-automation-overview' },
1764
2337
  h('div', { className: 'capital-automation-statgrid' },
1765
2338
  h('div', { className: 'capital-automation-stat' },
1766
2339
  h('span', { className: 'capital-automation-statlabel' }, zh.stateLabel),
1767
2340
  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)),
2341
+ h('span', { className: 'capital-automation-statsub' }, originLabel)),
1769
2342
  h('div', { className: 'capital-automation-stat' },
1770
2343
  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))),
2344
+ h('span', { className: 'capital-automation-statvalue' }, next.value),
2345
+ h('span', { className: 'capital-automation-statsub' }, next.sub)),
1773
2346
  h('div', { className: 'capital-automation-stat' },
1774
2347
  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' },
2348
+ h('span', { className: 'capital-automation-statvalue', 'data-tone': last.tone }, last.value),
2349
+ h('span', { className: 'capital-automation-statsub' }, last.sub)),
2350
+ h('div', { className: 'capital-automation-stat', title: scheduleHint },
1778
2351
  h('span', { className: 'capital-automation-statlabel' }, zh.triggerSummary),
1779
- h('span', { className: 'capital-automation-statvalue' }, scheduleRangeSummary(detail.task.trigger)),
2352
+ h('span', { className: 'capital-automation-statvalue' }, scheduleText),
1780
2353
  h('span', { className: 'capital-automation-statsub' }, `${detail.task.trigger.time} · ${detail.task.trigger.timezone}`))),
1781
2354
  h('section', { className: 'capital-automation-card capital-automation-panel' },
1782
2355
  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),
2356
+ h('h3', null, zh.detailRecentRuns.replace('{n}', String(DETAIL_RECENT_RUN_LIMIT))),
1801
2357
  h('button', { type: 'button', className: 'capital-automation-btn', 'data-size': 'sm', 'data-variant': 'ghost', onClick: onGoRuns },
1802
2358
  zh.detailAllRuns,
1803
2359
  h(IconChevronRightOutlineRegular, { size: 13 }))),
1804
- h(RunList, { runs: detail.runs.slice(0, 3), tasks: [detail.task], compact: true })),
2360
+ h(RunList, {
2361
+ runs: detail.runs.slice(0, DETAIL_RECENT_RUN_LIMIT),
2362
+ tasks: [detail.task],
2363
+ onCancel: onCancelRun,
2364
+ onOpenSession,
2365
+ compact: true,
2366
+ })),
1805
2367
  h('section', { className: 'capital-automation-card capital-automation-panel' },
1806
2368
  h('div', { className: 'capital-automation-panelhead' },
1807
2369
  h('h3', null, zh.environment)),
@@ -1814,66 +2376,82 @@ function OverviewTab(props) {
1814
2376
  h('span', { className: 'capital-automation-kvvalue' }, workspaceTitle(meta, detail.task.workspaceId))),
1815
2377
  h('div', { className: 'capital-automation-kvitem' },
1816
2378
  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),
2379
+ h('span', { className: 'capital-automation-kvvalue' }, categoryLabel(detail.task.category)))),
2380
+ h('section', { className: 'capital-automation-technical' },
2381
+ h('h4', { className: 'capital-automation-technicaltitle' }, zh.techInfo),
1826
2382
  h('dl', null,
1827
2383
  h('dt', null, zh.profileKey),
1828
- h('dd', null, meta?.profile?.key ?? '—'),
2384
+ h('dd', { title: 'profileKey 是 profile 名称的稳定哈希;与 session 标题里的 run id 无关。' }, meta?.profile?.key ?? '—'),
1829
2385
  h('dt', null, zh.presetLabel),
1830
2386
  h('dd', null, zh.presetValue),
1831
2387
  h('dt', null, zh.policyField),
1832
2388
  h('dd', null, meta?.execution?.policy ?? 'automation-v1'),
1833
2389
  h('dt', null, zh.cwdKind),
1834
- h('dd', null, meta?.execution?.cwdKind ?? 'profile'),
1835
- h('dt', null, 'profile'),
2390
+ h('dd', { title: '当前固定为 profile(专属自动化工作区);没有 storage 等其他来源。' }, meta?.execution?.cwdKind ?? 'profile'),
2391
+ h('dt', null, 'profile 名称'),
1836
2392
  h('dd', null, meta?.profile?.name ?? '—'),
1837
2393
  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}` : '—')))))
2394
+ h('dd', null, calendarLabel(meta?.calendar))))))
1841
2395
  }
1842
2396
 
1843
2397
  function ConfigTab(props) {
1844
2398
  const { draft, setDraft, meta, busy, dirty, save, reset, disabled } = props
1845
2399
  const task = draft
2400
+ const { issues, errorFor, touch, touchAll, clear } = useDraftIssues(task, task.id)
2401
+ const formRef = useRef(null)
2402
+ const titleError = errorFor('title')
2403
+ const instructionError = errorFor('instruction')
1846
2404
  const set = (patch) => setDraft((current) => ({
1847
2405
  ...current,
1848
2406
  ...(typeof patch === 'function' ? patch(current) : patch),
1849
2407
  }))
2408
+ const submit = () => {
2409
+ if (Object.keys(issues).length > 0) {
2410
+ touchAll()
2411
+ setTimeout(() => {
2412
+ const target = formRef.current?.querySelector('[data-invalid="true"] input, [data-invalid="true"] .capital-automation-texteditor')
2413
+ target?.focus?.()
2414
+ }, 0)
2415
+ return
2416
+ }
2417
+ void save()
2418
+ }
1850
2419
  const timeoutRange = limitValue(meta, 'runTimeoutMinutes', [5, 120])
1851
2420
  const idleRange = limitValue(meta, 'idleTimeoutMinutes', [1, 30])
1852
2421
  const stepsRange = limitValue(meta, 'maxSteps', [10, 2000])
1853
2422
  const toolsRange = limitValue(meta, 'maxToolCalls', [10, 6000])
2423
+ const instructionEditor = h(Field, { label: zh.instructionField, error: instructionError },
2424
+ h(PlainTextEditor, {
2425
+ className: 'capital-automation-texteditor',
2426
+ value: task.instruction,
2427
+ disabled: busy || disabled,
2428
+ placeholder: zh.instructionPlaceholder,
2429
+ ariaLabel: zh.instructionField,
2430
+ invalid: instructionError ? true : undefined,
2431
+ onChange: (instruction) => set({ instruction }),
2432
+ onBlur: () => touch('instruction'),
2433
+ }))
2434
+ const instructionSide = h('div', { className: 'capital-automation-instructionside' },
2435
+ h(SelectField, {
2436
+ label: zh.workspaceField,
2437
+ value: task.workspaceId ?? '',
2438
+ options: workspaceOptions(meta, task.workspaceId).map((option) => ({ value: option.id, label: option.title })),
2439
+ disabled: busy || disabled,
2440
+ onChange: (workspaceId) => set({ workspaceId }),
2441
+ }),
2442
+ meta?.workspaceError ? h('p', { className: 'capital-automation-hint', 'data-tone': 'error' }, zh.workspaceUnavailable) : null,
2443
+ h('div', null,
2444
+ h('span', { className: 'capital-automation-formlabel' }, zh.triggerField),
2445
+ h(TriggerFields, { trigger: task.trigger, disabled: busy || disabled, onChange: (trigger) => set({ trigger }), meta, errorFor, touch })))
1854
2446
  const advanced = h('details', { className: 'capital-automation-advanced' },
1855
2447
  h('summary', null, zh.advanced),
1856
2448
  h('div', { className: 'capital-automation-advancedbody' },
1857
2449
  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))
2450
+ h(NumberField, { label: zh.runTimeout, value: task.limits.runTimeoutMinutes, min: timeoutRange[0], max: timeoutRange[1], disabled: busy || disabled, error: errorFor('runTimeoutMinutes'), onBlur: () => touch('runTimeoutMinutes'), onChange: (value) => set((current) => ({ limits: { ...current.limits, runTimeoutMinutes: value } })) }),
2451
+ h(NumberField, { label: zh.idleTimeout, value: task.limits.idleTimeoutMinutes, min: idleRange[0], max: idleRange[1], disabled: busy || disabled, error: errorFor('idleTimeoutMinutes'), onBlur: () => touch('idleTimeoutMinutes'), onChange: (value) => set((current) => ({ limits: { ...current.limits, idleTimeoutMinutes: value } })) }),
2452
+ h(NumberField, { label: zh.maxSteps, value: task.limits.maxSteps, min: stepsRange[0], max: stepsRange[1], disabled: busy || disabled, error: errorFor('maxSteps'), onBlur: () => touch('maxSteps'), onChange: (value) => set((current) => ({ limits: { ...current.limits, maxSteps: value } })) }),
2453
+ h(NumberField, { label: zh.maxToolCalls, value: task.limits.maxToolCalls, min: toolsRange[0], max: toolsRange[1], disabled: busy || disabled, error: errorFor('maxToolCalls'), onBlur: () => touch('maxToolCalls'), onChange: (value) => set((current) => ({ limits: { ...current.limits, maxToolCalls: value } })) })),
2454
+ h('p', { className: 'capital-automation-hint' }, zh.budgetHint)))
1877
2455
  const notificationRows = h('div', null,
1878
2456
  h('span', { className: 'capital-automation-formlabel' }, zh.notifyField),
1879
2457
  h('div', { className: 'capital-automation-switchlist' },
@@ -1881,34 +2459,52 @@ function ConfigTab(props) {
1881
2459
  h(SwitchRow, { label: zh.notifyFailure, checked: task.notifications.onFailure, disabled: busy || disabled, onChange: (value) => set((current) => ({ notifications: { ...current.notifications, onFailure: value } })) }),
1882
2460
  h(SwitchRow, { label: zh.notifySuccess, checked: task.notifications.onSuccess, disabled: busy || disabled, onChange: (value) => set((current) => ({ notifications: { ...current.notifications, onSuccess: value } })) })),
1883
2461
  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', {
2462
+ return h('div', { className: 'capital-automation-form', ref: formRef },
2463
+ h('div', { className: 'capital-automation-titlerow' },
2464
+ h(Field, {
2465
+ label: zh.titleField,
2466
+ error: titleError,
2467
+ note: zh.charCount.replace('{n}', String(Array.from(task.title ?? '').length)).replace('{max}', String(TITLE_MAX_LENGTH)),
2468
+ },
2469
+ h('input', {
2470
+ value: task.title,
2471
+ maxLength: TITLE_MAX_LENGTH,
2472
+ disabled: busy || disabled,
2473
+ 'aria-invalid': titleError ? 'true' : undefined,
2474
+ onChange: (event) => set({ title: event.target.value }),
2475
+ onBlur: () => touch('title'),
2476
+ })),
2477
+ h(SelectField, {
2478
+ label: zh.categoryField,
2479
+ title: task.origin === 'builtin' ? zh.categoryFixed : undefined,
1891
2480
  value: task.category,
2481
+ options: Object.entries(zh.category).map(([value, label]) => ({ value, label })),
1892
2482
  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 }),
2483
+ onChange: (category) => set({ category }),
1901
2484
  })),
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 })),
2485
+ h(Field, {
2486
+ label: zh.descriptionField,
2487
+ note: zh.charCount.replace('{n}', String(Array.from(task.description ?? '').length)).replace('{max}', String(DESCRIPTION_MAX_LENGTH)),
2488
+ },
2489
+ h(PlainTextEditor, {
2490
+ className: 'capital-automation-texteditor capital-automation-descriptioninput',
2491
+ value: task.description ?? '',
2492
+ maxLength: DESCRIPTION_MAX_LENGTH,
2493
+ disabled: busy || disabled,
2494
+ placeholder: zh.descriptionPlaceholder,
2495
+ ariaLabel: zh.descriptionField,
2496
+ onChange: (description) => set({ description }),
2497
+ })),
2498
+ h('div', { className: 'capital-automation-instructionrow' },
2499
+ instructionEditor,
2500
+ instructionSide),
1905
2501
  advanced,
1906
2502
  notificationRows,
1907
2503
  h('div', { className: 'capital-automation-formactions', 'data-sticky': 'true' },
1908
2504
  h('span', { className: 'capital-automation-formstatus' },
1909
2505
  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)))
2506
+ h('button', { type: 'button', className: 'capital-automation-btn', disabled: busy || !dirty, onClick: () => { clear(); reset() } }, zh.cancel),
2507
+ h('button', { type: 'button', className: 'capital-automation-btn', 'data-variant': 'primary', disabled: busy || !dirty || disabled, onClick: submit }, busy === 'save' ? zh.saving : zh.save)))
1912
2508
  }
1913
2509
 
1914
2510
  function NotificationsTab(props) {
@@ -1957,7 +2553,7 @@ function DetailBar(props) {
1957
2553
  }
1958
2554
 
1959
2555
  function TaskDetailPage(props) {
1960
- const { detail, meta, api, onChanged, openSession, tab, onTab, onRequestConfirm, onRequestCancelRun, onOpenAllRuns, schedulerActive, storageReady } = props
2556
+ const { detail, meta, api, onChanged, openSession, tab, onTab, onRequestConfirm, onRequestCancelRun, schedulerActive, storageReady } = props
1961
2557
  const [draft, setDraft] = useState(null)
1962
2558
  const [busy, setBusy] = useState('')
1963
2559
  const [notice, setNotice] = useState('')
@@ -2011,11 +2607,6 @@ function TaskDetailPage(props) {
2011
2607
  }
2012
2608
 
2013
2609
  async function save() {
2014
- const invalid = validateDraft(task)
2015
- if (invalid.length > 0) {
2016
- setError(invalid)
2017
- return
2018
- }
2019
2610
  await act('save', async () => {
2020
2611
  await api.patch(detail.task.id, draftPayload(task))
2021
2612
  }, zh.saved)
@@ -2063,7 +2654,7 @@ function TaskDetailPage(props) {
2063
2654
  disabled: busy || !canRun,
2064
2655
  title: schedulerActive === false ? zh.schedulerBlocked : running ? zh.runBlocked : undefined,
2065
2656
  onClick: () => { void runNow() },
2066
- }, zh.runNow)
2657
+ }, h(IconPlayOutlineRegular, { size: 14 }), zh.runNow)
2067
2658
 
2068
2659
  const resetDraft = () => {
2069
2660
  setDraft({
@@ -2084,13 +2675,7 @@ function TaskDetailPage(props) {
2084
2675
  h('h2', null, detail.task.title),
2085
2676
  h(StateBadge, { state: detail.task.state, running }),
2086
2677
  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))),
2678
+ h('p', { className: 'capital-automation-herodesc' }, detail.task.description || zh.subtitle)),
2094
2679
  h('div', { className: 'capital-automation-heroactions' },
2095
2680
  runButton,
2096
2681
  running
@@ -2101,7 +2686,7 @@ function TaskDetailPage(props) {
2101
2686
  'data-variant': detail.task.state === 'active' ? undefined : 'primary',
2102
2687
  disabled: busy || !storageReadyNow,
2103
2688
  onClick: () => { void toggleState() },
2104
- }, activationLabel(detail.task)),
2689
+ }, h(activationIcon(detail.task), { size: 14 }), activationLabel(detail.task)),
2105
2690
  h(TaskActionsMenu, {
2106
2691
  task: detail.task,
2107
2692
  open: menuOpen,
@@ -2135,9 +2720,14 @@ function TaskDetailPage(props) {
2135
2720
  error.length > 0 ? h('p', { className: 'capital-automation-error', role: 'alert' }, error) : null)
2136
2721
  : null,
2137
2722
  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
2723
  h('div', { className: 'capital-automation-detailbody' },
2140
- tab === 'overview' ? h(OverviewTab, { detail, meta, onGoRuns: onOpenAllRuns }) : null,
2724
+ tab === 'overview' ? h(OverviewTab, {
2725
+ detail,
2726
+ meta,
2727
+ onGoRuns: () => onTab('runs'),
2728
+ onCancelRun: onRequestCancelRun,
2729
+ onOpenSession: openSession,
2730
+ }) : null,
2141
2731
  tab === 'config' ? h(ConfigTab, {
2142
2732
  draft,
2143
2733
  setDraft,
@@ -2171,6 +2761,9 @@ function NewTaskModal(props) {
2171
2761
  const [draft, setDraft] = useState(defaultDraft)
2172
2762
  const [busy, setBusy] = useState(false)
2173
2763
  const [error, setError] = useState('')
2764
+ const { issues, errorFor, touch, touchAll } = useDraftIssues(draft, open)
2765
+ const titleError = errorFor('title')
2766
+ const instructionError = errorFor('instruction')
2174
2767
  useEffect(() => {
2175
2768
  if (!open) return
2176
2769
  setDraft(defaultDraft())
@@ -2186,9 +2779,8 @@ function NewTaskModal(props) {
2186
2779
  const stepsRange = limitValue(meta, 'maxSteps', [10, 2000])
2187
2780
  const toolsRange = limitValue(meta, 'maxToolCalls', [10, 6000])
2188
2781
  async function create() {
2189
- const invalid = validateDraft(draft)
2190
- if (invalid.length > 0) {
2191
- setError(invalid)
2782
+ if (Object.keys(issues).length > 0) {
2783
+ touchAll()
2192
2784
  return
2193
2785
  }
2194
2786
  setBusy(true)
@@ -2215,67 +2807,81 @@ function NewTaskModal(props) {
2215
2807
  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
2808
  },
2217
2809
  h('div', { className: 'capital-automation-wizard' },
2218
- h('div', { className: 'capital-automation-modalheadline' },
2810
+ h('div', { className: 'capital-automation-modalheadline', 'data-invalid': titleError ? 'true' : undefined },
2219
2811
  h(Glyph, { task: draft, size: 42 }),
2220
2812
  h('input', {
2221
2813
  className: 'capital-automation-modaltitleinput',
2222
2814
  value: draft.title,
2815
+ maxLength: TITLE_MAX_LENGTH,
2223
2816
  placeholder: zh.titlePlaceholder,
2224
2817
  'aria-label': zh.titleField,
2818
+ 'aria-invalid': titleError ? 'true' : undefined,
2225
2819
  onChange: (event) => set({ title: event.target.value }),
2820
+ onBlur: () => touch('title'),
2226
2821
  })),
2822
+ titleError ? h('span', { className: 'capital-automation-fielderror', role: 'alert' }, titleError) : null,
2227
2823
  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
- })),
2824
+ h(Field, {
2825
+ label: zh.descriptionField,
2826
+ note: zh.charCount.replace('{n}', String(Array.from(draft.description ?? '').length)).replace('{max}', String(DESCRIPTION_MAX_LENGTH)),
2827
+ },
2828
+ h('input', {
2829
+ value: draft.description,
2830
+ maxLength: DESCRIPTION_MAX_LENGTH,
2831
+ placeholder: zh.descriptionPlaceholder,
2832
+ onChange: (event) => set({ description: event.target.value }),
2833
+ })),
2234
2834
  h('div', null,
2235
2835
  h('span', { className: 'capital-automation-formlabel' }, zh.triggerField),
2236
- h(TriggerFields, { trigger: draft.trigger, onChange: (trigger) => set({ trigger }), meta })),
2836
+ h(TriggerFields, { trigger: draft.trigger, onChange: (trigger) => set({ trigger }), meta, errorFor, touch })),
2237
2837
  h('div', null,
2238
2838
  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),
2839
+ h('div', { className: 'capital-automation-promptfield', 'data-invalid': instructionError ? 'true' : undefined },
2840
+ h('div', { className: 'capital-automation-promptbox' },
2841
+ h(PlainTextEditor, {
2842
+ className: 'capital-automation-texteditor',
2843
+ value: draft.instruction,
2844
+ placeholder: zh.instructionPlaceholder,
2845
+ ariaLabel: zh.instructionField,
2846
+ invalid: instructionError ? true : undefined,
2847
+ onChange: (instruction) => set({ instruction }),
2848
+ onBlur: () => touch('instruction'),
2849
+ }),
2850
+ h('div', { className: 'capital-automation-prompttoolbar' },
2851
+ h('div', { className: 'capital-automation-toolbarfield' },
2852
+ h(IconFolderOpenOutlineRegular, { size: 13 }),
2853
+ h('span', null, zh.workspaceField),
2854
+ h(Select, {
2855
+ value: draft.workspaceId ?? '',
2856
+ options: workspaceOptions(meta, draft.workspaceId).map((option) => ({ value: option.id, label: option.title })),
2857
+ onChange: (workspaceId) => set({ workspaceId }),
2858
+ label: zh.workspaceField,
2859
+ size: 'sm',
2860
+ align: 'end',
2861
+ })),
2862
+ h('div', { className: 'capital-automation-toolbarfield' },
2863
+ h('span', null, zh.categoryField),
2864
+ h(Select, {
2865
+ value: draft.category,
2866
+ options: Object.entries(zh.category).map(([value, label]) => ({ value, label })),
2867
+ onChange: (category) => set({ category }),
2868
+ label: zh.categoryField,
2869
+ size: 'sm',
2870
+ align: 'end',
2871
+ })),
2872
+ h('span', { className: 'capital-automation-presetchip', title: zh.presetHint },
2873
+ h(IconSparkleRegular, { size: 12 }),
2874
+ zh.presetValue))),
2875
+ instructionError ? h('span', { className: 'capital-automation-fielderror', role: 'alert' }, instructionError) : null)),
2270
2876
  meta?.workspaceError ? h('p', { className: 'capital-automation-hint', 'data-tone': 'error' }, zh.workspaceUnavailable) : null,
2271
2877
  h('details', { className: 'capital-automation-advanced' },
2272
2878
  h('summary', null, zh.advanced),
2273
2879
  h('div', { className: 'capital-automation-advancedbody' },
2274
2880
  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 } })) })),
2881
+ h(NumberField, { label: zh.runTimeout, value: draft.limits.runTimeoutMinutes, min: timeoutRange[0], max: timeoutRange[1], error: errorFor('runTimeoutMinutes'), onBlur: () => touch('runTimeoutMinutes'), onChange: (value) => set((current) => ({ limits: { ...current.limits, runTimeoutMinutes: value } })) }),
2882
+ h(NumberField, { label: zh.idleTimeout, value: draft.limits.idleTimeoutMinutes, min: idleRange[0], max: idleRange[1], error: errorFor('idleTimeoutMinutes'), onBlur: () => touch('idleTimeoutMinutes'), onChange: (value) => set((current) => ({ limits: { ...current.limits, idleTimeoutMinutes: value } })) }),
2883
+ h(NumberField, { label: zh.maxSteps, value: draft.limits.maxSteps, min: stepsRange[0], max: stepsRange[1], error: errorFor('maxSteps'), onBlur: () => touch('maxSteps'), onChange: (value) => set((current) => ({ limits: { ...current.limits, maxSteps: value } })) }),
2884
+ h(NumberField, { label: zh.maxToolCalls, value: draft.limits.maxToolCalls, min: toolsRange[0], max: toolsRange[1], error: errorFor('maxToolCalls'), onBlur: () => touch('maxToolCalls'), onChange: (value) => set((current) => ({ limits: { ...current.limits, maxToolCalls: value } })) })),
2279
2885
  h('p', { className: 'capital-automation-hint' }, zh.advancedHint))),
2280
2886
  h('div', null,
2281
2887
  h('span', { className: 'capital-automation-formlabel' }, zh.notifyField),
@@ -2604,8 +3210,6 @@ function AutomationPage(props) {
2604
3210
  const initialLoading = meta === null && loadError.length === 0
2605
3211
 
2606
3212
  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
3213
  detailOpen ? null : h(Header, {
2610
3214
  tab,
2611
3215
  onTab: switchTab,
@@ -2631,7 +3235,6 @@ function AutomationPage(props) {
2631
3235
  onTab: setDetailTab,
2632
3236
  onRequestConfirm: requestConfirm,
2633
3237
  onRequestCancelRun: (run) => requestConfirm('cancel-run', selectedTask ?? { id: run.taskId, title: run.taskTitle }, run),
2634
- onOpenAllRuns: () => switchTab('runs'),
2635
3238
  schedulerActive,
2636
3239
  storageReady,
2637
3240
  onChanged: async (taskId) => {