@v587d/capital-generation 2.13.2 → 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.
@@ -52,9 +52,28 @@ const WEEKDAYS = [
52
52
  { value: 6, label: '六' },
53
53
  { value: 7, label: '日' },
54
54
  ]
55
- 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
+ ]
56
73
  const HISTORY_DAYS = 30
57
74
  const DETAIL_RECENT_RUN_LIMIT = 3
75
+ const TITLE_MAX_LENGTH = 12
76
+ const DESCRIPTION_MAX_LENGTH = 100
58
77
 
59
78
  const zh = {
60
79
  panel: 'CapGen 投研自动化',
@@ -106,11 +125,8 @@ const zh = {
106
125
  filterLabel: '筛选任务',
107
126
  category: { news: '资讯', market: '行情', review: '复盘', research: '研究' },
108
127
  categoryField: '任务分类',
109
- categoryFixed: '内置模板的分类固定;复制为我的任务后可以调整。',
110
- modeField: '输出侧重',
111
- modeHint: '只影响输出结构;所有运行固定使用自动模式。',
112
- mode: { news: '资讯', market: '行情', review: '复盘', research: '研究' },
113
- cadence: { daily: '每天', weekly: '每周', monthly: '每月' },
128
+ categoryFixed: `'复制为我的任务'后,可调整任务分类。`,
129
+ cadence: { daily: '每天', weekly: '每周', monthly: '每月', once: '单次' },
114
130
  stateLabel: '状态',
115
131
  state: { dormant: '未添加', active: '已启用', paused: '已暂停' },
116
132
  origin: { builtin: '内置模板', custom: '我的任务' },
@@ -137,7 +153,9 @@ const zh = {
137
153
  cancel: '取消',
138
154
  create: '创建任务',
139
155
  creating: '创建中…',
140
- required: '标题和任务指令不能为空。',
156
+ titleRequired: '标题不能为空。',
157
+ instructionRequired: '任务指令不能为空。',
158
+ invalidLimit: '请输入有效数字。',
141
159
  dirty: '有未保存的修改',
142
160
  noDirty: '配置已同步',
143
161
  nextRun: '下次运行',
@@ -153,25 +171,47 @@ const zh = {
153
171
  config: '配置',
154
172
  runs: '运行记录',
155
173
  notifications: '通知',
156
- descriptionField: '说明',
157
- titleField: '标题',
174
+ descriptionField: '任务说明',
175
+ titleField: '任务标题',
158
176
  instructionField: '任务指令',
159
177
  instructionPlaceholder: '例如:读取今日自选股行情、公告与相关新闻,按「结论—证据—风险」整理成一份可复核的盘后复盘。',
160
178
  titlePlaceholder: '给这个任务起个名字',
179
+ charCount: '{n}/{max}',
161
180
  descriptionPlaceholder: '一句话说明任务目标(可选)',
162
181
  triggerField: '触发器',
163
- triggerHint: '按本地墙钟时间计算;仅交易日任务由交易日历判断周末与节假日。',
164
- triggerHintNatural: '按本地墙钟时间计算:每天都触发,周末与节假日也运行。',
182
+ cadenceField: '频率',
183
+ triggerHint: '按设定时区的本地墙钟时间计算;仅交易日任务由交易日历判断周末与节假日。',
184
+ triggerHintNatural: '按设定时区的本地墙钟时间计算:每天都触发,周末与节假日也运行。',
185
+ triggerHintOnce: '单次任务只在指定日期运行一次;该日期不是交易日时,仅交易日设置会跳过本次运行。',
165
186
  timeField: '时间',
166
187
  timezoneField: '时区',
167
188
  weekdayField: '星期',
168
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: '完成',
169
208
  tradingDaysField: '仅交易日',
170
209
  weekdayRequired: '每周至少选择一天。',
210
+ dateRequired: '请选择运行日期。',
211
+ invalidDate: '请选择有效的日期。',
171
212
  invalidTime: '运行时间格式不正确,请选择 HH:mm。',
172
213
  invalidTimezone: '时区不能为空,请填写 IANA 时区。',
173
214
  invalidTimezoneValue: '时区不被识别,请填写有效的 IANA 时区。',
174
- invalidLimits: '运行上限必须是有效数字。',
175
215
  filterNoRuns: '没有符合筛选条件的运行记录。',
176
216
  schedulePrefix: '下次',
177
217
  workspaceField: '工作区',
@@ -192,6 +232,7 @@ const zh = {
192
232
  notifyHint: '通知只记录在当前 profile;不会发送邮件或外部消息。',
193
233
  presetLabel: '运行模式',
194
234
  presetValue: '自动模式',
235
+ presetHint: '所有运行固定使用自动模式。',
195
236
  budgetHint: '工具调用与步骤预算覆盖整棵运行子树:子 Agent 与 PTC 派发都计入。',
196
237
  environment: '执行环境',
197
238
  policyField: '无人值守策略',
@@ -262,7 +303,6 @@ const zh = {
262
303
  schedulerBlocked: '调度器未取得运行权,暂时不能手动运行。',
263
304
  runBlocked: '任务正在运行,结束或取消后才能再次运行。',
264
305
  configWhileRunning: '任务正在运行;修改会在下一次运行时生效。',
265
- configBuiltinHint: '这是内置模板;修改只覆盖当前 profile,休眠后会恢复默认。',
266
306
  detailRecentRuns: '最近 {n} 次运行',
267
307
  detailNoRuns: '还没有运行记录。',
268
308
  detailAllRuns: '完整记录',
@@ -281,7 +321,6 @@ const zh = {
281
321
  lastRun: '上次运行',
282
322
  neverRun: '尚未运行',
283
323
  relativeNow: '刚刚',
284
- timezoneHint: 'IANA 时区,例如 Asia/Shanghai。',
285
324
  nextRunUnavailable: '交易日历不可用,无法计算下一次运行。',
286
325
  copyHint: '复制后会生成一个暂停状态的自定义任务。',
287
326
  notificationTaskGone: '任务已不存在',
@@ -293,7 +332,9 @@ const zh = {
293
332
  everyPrefix: '每',
294
333
  weeklyPrefix: '每周',
295
334
  monthlyPrefix: '每月',
335
+ monthlyLastDay: '最后一天',
296
336
  dailyPrefix: '每天',
337
+ oncePrefix: '单次',
297
338
  tradingSuffix: '(仅交易日)',
298
339
  sessionMissing: '没有可打开的 session',
299
340
  }
@@ -510,17 +551,31 @@ function installStyles() {
510
551
  .capital-automation-error { margin: 0; color: var(--ca-error-text); font-size: 12px; line-height: 18px; }
511
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; }
512
553
  .capital-automation-form { display: flex; flex-direction: column; gap: 16px; }
513
- .capital-automation-formnote { display: flex; align-items: flex-start; gap: 7px; padding: 9px 11px; border: .5px solid var(--ca-line); border-radius: 10px; background: var(--ca-field); color: var(--ca-muted); font-size: 11.5px; line-height: 18px; }
514
- .capital-automation-formnote svg { flex: none; margin-top: 2px; }
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; }
515
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; }
516
558
  .capital-automation-field { display: flex; flex-direction: column; gap: 5px; min-width: 0; }
517
- .capital-automation-field > span { color: var(--ca-muted); font-size: 11.5px; line-height: 17px; }
518
- .capital-automation-field input, .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; }
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; }
519
563
  .capital-automation-field input { height: 34px; }
520
- .capital-automation-field textarea { min-height: 110px; padding: 9px 10px; line-height: 19px; resize: vertical; }
521
- .capital-automation-field input:hover, .capital-automation-field textarea:hover { border-color: var(--ca-line); }
522
- .capital-automation-field input: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; }
523
- .capital-automation-field input:disabled, .capital-automation-field textarea:disabled { color: var(--ca-muted); opacity: .8; }
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; }
524
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; }
525
580
  .capital-automation-select:hover:not(:disabled) { background: var(--ca-hover); }
526
581
  .capital-automation-select[data-block="true"] { width: 100%; }
@@ -532,20 +587,37 @@ function installStyles() {
532
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); }
533
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); }
534
589
  .capital-automation-formstatus { margin-right: auto; color: var(--ca-caption); font-size: 11.5px; line-height: 18px; }
535
- .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); }
536
- .capital-automation-triggerrow { display: flex; flex-wrap: wrap; align-items: center; gap: 8px; }
537
- .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; }
538
- .capital-automation-triggerrow label, .capital-automation-triggerfield { display: inline-flex; align-items: center; gap: 6px; color: var(--ca-muted); font-size: 12px; }
539
- .capital-automation-triggerrow input { 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; }
540
- .capital-automation-triggerrow input[type="time"] { width: 112px; }
541
- .capital-automation-triggerrow .capital-automation-select { max-width: 190px; height: 32px; background: var(--ca-card); }
542
- .capital-automation-weekdays { display: flex; flex-wrap: wrap; gap: 4px; }
543
- .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; }
544
- .capital-automation-weekday:hover:not(:disabled) { border-color: var(--ca-line); color: var(--dsw-alias-label-primary); }
545
- .capital-automation-weekday[aria-pressed="true"] { border-color: transparent; background: var(--dsw-alias-label-primary); color: var(--dsw-alias-bg-base); }
546
- .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; }
547
620
  .capital-automation-switch { display: inline-flex; align-items: center; gap: 7px; color: var(--ca-muted); font-size: 12px; }
548
- .capital-automation-triggersummary { margin: 0; color: var(--ca-caption); font-size: 11.5px; line-height: 18px; }
549
621
  .capital-automation-advanced { border-top: .5px solid var(--ca-line); padding-top: 12px; }
550
622
  .capital-automation-advanced summary { color: var(--ca-muted); font-size: 12px; line-height: 18px; cursor: pointer; user-select: none; }
551
623
  .capital-automation-advanced summary:hover { color: var(--dsw-alias-label-primary); }
@@ -604,10 +676,13 @@ function installStyles() {
604
676
  .capital-automation-modalheadline { display: flex; align-items: center; gap: 10px; }
605
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; }
606
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)); }
607
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)); }
608
683
  .capital-automation-promptbox { overflow: hidden; border: .5px solid var(--ca-line); border-radius: 16px; background: var(--dsw-alias-bg-module-platform); }
609
- .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; }
610
- .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; }
611
686
  .capital-automation-prompttoolbar { display: flex; flex-wrap: wrap; align-items: center; gap: 8px; padding: 8px 10px; border-top: .5px solid var(--ca-line); }
612
687
  .capital-automation-prompttoolbar label, .capital-automation-toolbarfield { display: inline-flex; align-items: center; gap: 5px; color: var(--ca-muted); font-size: 11.5px; }
613
688
  .capital-automation-prompttoolbar .capital-automation-select { max-width: 168px; background: var(--ca-card); }
@@ -626,6 +701,9 @@ function installStyles() {
626
701
  .capital-automation-theadmain > :nth-child(3), .capital-automation-cell[data-cell="next"] { display: none; }
627
702
  .capital-automation-statgrid { grid-template-columns: repeat(2, minmax(0, 1fr)); }
628
703
  }
704
+ @media (max-width: 960px) {
705
+ .capital-automation-instructionrow { grid-template-columns: minmax(0, 1fr); }
706
+ }
629
707
  @media (max-width: 760px) {
630
708
  .capital-automation-head { padding-top: 14px; }
631
709
  .capital-automation-topbar { display: flex; flex-wrap: wrap; gap: 10px; }
@@ -643,8 +721,16 @@ function installStyles() {
643
721
  .capital-automation-templates { grid-template-columns: 1fr; }
644
722
  .capital-automation-sectiontools { width: 100%; margin-left: 0; }
645
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
+ }
646
731
  @media (max-width: 520px) {
647
732
  .capital-automation-brand h1 { font-size: 16px; }
733
+ .capital-automation-titlerow { grid-template-columns: minmax(0, 1fr); }
648
734
  .capital-automation-topactions .capital-automation-btn:not([data-variant="contrast"]) { width: auto; min-width: 32px; padding: 0 8px; }
649
735
  .capital-automation-topactions .capital-automation-btn[data-variant="contrast"] { width: 32px; padding: 0; }
650
736
  .capital-automation-topactions .capital-automation-btn .capital-automation-btnlabel { display: none; }
@@ -734,10 +820,6 @@ function clipTagLabel(label, limit = 12) {
734
820
  return chars.length > limit ? `${chars.slice(0, limit).join('')}…` : text
735
821
  }
736
822
 
737
- function modeLabel(mode) {
738
- return zh.mode[mode] ?? mode
739
- }
740
-
741
823
  function calendarLabel(calendar) {
742
824
  if (calendar === null || calendar === undefined || calendar.available === false) return zh.calendarSourceNone
743
825
  const source = calendar.source === 'szse'
@@ -839,26 +921,31 @@ function formatRangeLabel(date) {
839
921
  return new Intl.DateTimeFormat('zh-CN', { month: 'short', day: 'numeric' }).format(date)
840
922
  }
841
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
+
842
935
  function scheduleSummary(trigger) {
843
936
  if (trigger === null || trigger === undefined) return zh.never
844
937
  const suffix = trigger.tradingDaysOnly === true ? zh.tradingSuffix : ''
845
- if (trigger.cadence === 'weekly') {
846
- const days = (trigger.weekdays ?? []).slice().sort((left, right) => left - right).map((day) => WEEKDAYS.find((item) => item.value === day)?.label ?? '').join('、')
847
- return `${zh.weeklyPrefix}${days} ${trigger.time}${suffix}`
848
- }
849
- if (trigger.cadence === 'monthly') {
850
- return `${zh.monthlyPrefix} ${trigger.dayOfMonth ?? 1} 日 ${trigger.time}${suffix}`
851
- }
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}`
852
941
  return `${zh.dailyPrefix} ${trigger.time}${suffix}`
853
942
  }
854
943
 
855
944
  function scheduleRangeSummary(trigger) {
856
- if (trigger?.cadence === 'weekly') {
857
- const days = (trigger.weekdays ?? []).slice().sort((left, right) => left - right).map((day) => WEEKDAYS.find((item) => item.value === day)?.label ?? '').join('、')
858
- return `每周 ${days}`
859
- }
860
- if (trigger?.cadence === 'monthly') return `每月 ${trigger.dayOfMonth ?? 1} 日`
861
- return '每天'
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
862
949
  }
863
950
 
864
951
  // Dormant/paused tasks still carry a computed nextRunLocal, but that instant will not fire until
@@ -866,6 +953,11 @@ function scheduleRangeSummary(trigger) {
866
953
  function nextRunView(task) {
867
954
  if (task?.state === 'dormant') return { value: zh.nextRunDormant, sub: zh.notScheduled }
868
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 }
960
+ }
869
961
  const value = task?.nextRunLocal ?? (task?.nextRunUnavailable ? zh.calendarUnavailable : zh.never)
870
962
  const sub = task?.nextRunAt ? formatRelative(task.nextRunAt) : (task?.nextRunUnavailable ? zh.nextRunUnavailable : zh.never)
871
963
  return { value, sub }
@@ -904,16 +996,120 @@ function workspaceTitle(meta, id) {
904
996
  return workspaceOptions(meta, id).find((item) => item.id === id)?.title ?? zh.workspaceMissing
905
997
  }
906
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
+
907
1096
  function normalizeTrigger(trigger) {
1097
+ const cadence = Object.prototype.hasOwnProperty.call(zh.cadence, trigger?.cadence) ? trigger.cadence : 'daily'
908
1098
  const next = {
909
1099
  kind: 'schedule',
910
- cadence: trigger?.cadence ?? 'daily',
1100
+ cadence,
911
1101
  time: trigger?.time ?? '09:00',
912
1102
  timezone: trigger?.timezone ?? 'Asia/Shanghai',
913
1103
  tradingDaysOnly: trigger?.tradingDaysOnly === true,
914
1104
  }
915
1105
  if (next.cadence === 'weekly') next.weekdays = (trigger?.weekdays ?? [1, 3, 5]).slice().sort((left, right) => left - right)
916
- 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
+ }
917
1113
  return next
918
1114
  }
919
1115
 
@@ -930,7 +1126,6 @@ function defaultDraft() {
930
1126
  instruction: '',
931
1127
  category: 'research',
932
1128
  trigger: { kind: 'schedule', cadence: 'daily', time: '09:00', timezone: 'Asia/Shanghai', tradingDaysOnly: true },
933
- mode: 'research',
934
1129
  workspaceId: '',
935
1130
  limits: { ...DEFAULT_LIMITS },
936
1131
  notifications: { inApp: true, onFailure: true, onSuccess: false },
@@ -944,7 +1139,6 @@ function draftPayload(task) {
944
1139
  instruction: task.instruction ?? '',
945
1140
  category: task.category,
946
1141
  trigger: normalizeTrigger(task.trigger),
947
- mode: task.mode,
948
1142
  workspaceId: task.workspaceId ?? '',
949
1143
  limits: task.limits,
950
1144
  notifications: task.notifications,
@@ -1014,19 +1208,50 @@ function draftChanged(draft, task) {
1014
1208
  return !samePayload(draft, task)
1015
1209
  }
1016
1210
 
1017
- function validateDraft(task) {
1018
- if (String(task.title ?? '').trim().length === 0 || String(task.instruction ?? '').trim().length === 0) return zh.required
1019
- if (!/^([01]\d|2[0-3]):[0-5]\d$/.test(task.trigger?.time ?? '')) return zh.invalidTime
1020
- if (String(task.trigger?.timezone ?? '').trim().length === 0) return zh.invalidTimezone
1021
- try {
1022
- new Intl.DateTimeFormat('zh-CN', { timeZone: task.trigger.timezone })
1023
- } catch {
1024
- 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
1025
1230
  }
1026
- if (task.trigger?.cadence === 'weekly' && (task.trigger.weekdays ?? []).length === 0) return zh.weekdayRequired
1027
1231
  const limits = task.limits ?? {}
1028
- if (![limits.runTimeoutMinutes, limits.idleTimeoutMinutes, limits.maxSteps, limits.maxToolCalls].every((value) => Number.isFinite(value))) return zh.invalidLimits
1029
- 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 }
1030
1255
  }
1031
1256
 
1032
1257
  function limitValue(meta, key, fallback) {
@@ -1642,85 +1867,233 @@ function TaskDashboard(props) {
1642
1867
  : null)
1643
1868
  }
1644
1869
 
1645
- function TriggerFields(props) {
1646
- const { trigger, onChange, disabled, meta } = props
1647
- const update = (patch) => onChange(normalizeTrigger({ ...trigger, ...patch }))
1648
- const setCadence = (cadence) => {
1649
- const next = triggerChanged(cadence, trigger)
1650
- 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)
1651
1910
  }
1652
- const toggleWeekday = (day) => {
1653
- const selected = new Set(trigger.weekdays ?? [])
1654
- if (selected.has(day)) {
1655
- if (selected.size <= 1) return
1656
- selected.delete(day)
1657
- } else {
1658
- 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)
1659
1970
  }
1660
- update({ weekdays: [...selected].sort((left, right) => left - right) })
1661
1971
  }
1662
- return h('div', { className: 'capital-automation-triggerbox' },
1663
- h('div', { className: 'capital-automation-triggerrow' },
1664
- h('span', { className: 'capital-automation-triggerclock', 'aria-hidden': 'true' }, h(IconAlarmClockOutlineRegular, { size: 16 })),
1665
- h('div', { className: 'capital-automation-triggerfield' },
1666
- h('span', null, '重复'),
1667
- h(Select, {
1668
- value: trigger.cadence,
1669
- options: Object.entries(zh.cadence).map(([value, label]) => ({ value, label })),
1670
- onChange: setCadence,
1671
- disabled,
1672
- label: '重复',
1673
- })),
1674
- h('label', { className: 'capital-automation-triggerfield' },
1675
- h('span', null, zh.timeField),
1676
- h('input', { type: 'time', value: trigger.time, disabled, onChange: (event) => update({ time: event.target.value }) })),
1677
- h('label', { className: 'capital-automation-triggerfield' },
1678
- h('span', null, zh.timezoneField),
1679
- h('input', {
1680
- type: 'text',
1681
- list: 'capital-automation-timezones',
1682
- value: trigger.timezone,
1683
- disabled,
1684
- onChange: (event) => update({ timezone: event.target.value }),
1685
- title: zh.timezoneHint,
1686
- }))),
1687
- h('div', { className: 'capital-automation-triggerrow' },
1688
- trigger.cadence === 'weekly'
1689
- ? h('div', { className: 'capital-automation-weekdays', role: 'group', 'aria-label': zh.weekdayField },
1690
- WEEKDAYS.map((day) => h('button', {
1691
- key: day.value,
1692
- type: 'button',
1693
- className: 'capital-automation-weekday',
1694
- 'aria-pressed': (trigger.weekdays ?? []).includes(day.value) ? 'true' : 'false',
1695
- disabled,
1696
- onClick: () => toggleWeekday(day.value),
1697
- }, day.label)))
1698
- : null,
1699
- trigger.cadence === 'monthly'
1700
- ? h('div', { className: 'capital-automation-triggerfield' },
1701
- h('span', null, zh.dayField),
1702
- h(Select, {
1703
- value: String(trigger.dayOfMonth ?? 1),
1704
- options: Array.from({ length: 28 }, (_, index) => {
1705
- const day = index + 1
1706
- return { value: String(day), label: zh.dayOfMonth.replace('{n}', String(day)) }
1707
- }),
1708
- onChange: (day) => update({ dayOfMonth: Number(day) }),
1709
- disabled,
1710
- label: zh.dayField,
1711
- }))
1712
- : null,
1713
- trigger.cadence === 'daily' ? h('span', { className: 'capital-automation-hint' }, trigger.tradingDaysOnly === true ? zh.triggerHint : zh.triggerHintNatural) : null,
1714
- h('label', { className: 'capital-automation-switch', style: { marginLeft: 'auto' } },
1715
- h(Switch, {
1716
- checked: trigger.tradingDaysOnly === true,
1717
- onChange: (value) => update({ tradingDaysOnly: value }),
1718
- label: zh.tradingDaysField,
1719
- disabled,
1720
- }),
1721
- h('span', null, zh.tradingDaysField))),
1722
- h('p', { className: 'capital-automation-triggersummary' }, `${scheduleSummary(trigger)} · ${trigger.timezone}`),
1723
- 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)
1724
2097
  }
1725
2098
 
1726
2099
  function SwitchRow(props) {
@@ -1732,10 +2105,84 @@ function SwitchRow(props) {
1732
2105
  h(Switch, { checked: checked === true, onChange, label, disabled }))
1733
2106
  }
1734
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
+
1735
2181
  function Field(props) {
1736
- const { label, hint, children } = props
1737
- return h('label', { className: 'capital-automation-field' },
1738
- 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 }),
1739
2186
  children,
1740
2187
  hint ? h('span', { className: 'capital-automation-hint' }, hint) : null)
1741
2188
  }
@@ -1745,7 +2192,7 @@ function Field(props) {
1745
2192
  * Native <select> is intentionally avoided so light/dark themes follow host tokens.
1746
2193
  */
1747
2194
  function Select(props) {
1748
- const { value, options, onChange, disabled, label, block, size, align = 'start' } = props
2195
+ const { value, options, onChange, disabled, label, block, size, align = 'start', title } = props
1749
2196
  const [open, setOpen] = useState(false)
1750
2197
  const triggerRef = useRef(null)
1751
2198
  const items = options.map((option) => ({
@@ -1774,11 +2221,12 @@ function Select(props) {
1774
2221
  'aria-label': current?.label ? `${label}:${current.label}` : label,
1775
2222
  'aria-haspopup': 'menu',
1776
2223
  'aria-expanded': open ? 'true' : 'false',
2224
+ title: title ?? current?.label,
1777
2225
  onClick: () => setOpen((currentOpen) => !currentOpen),
1778
2226
  },
1779
2227
  h('span', {
1780
2228
  className: 'capital-automation-selectvalue',
1781
- title: current?.label,
2229
+ title: title ?? current?.label,
1782
2230
  }, current?.label ?? ''),
1783
2231
  h(IconChevronDownOutlineRegular, { size: size === 'sm' ? 12 : 14 }))
1784
2232
  return h(Menu, {
@@ -1794,23 +2242,26 @@ function Select(props) {
1794
2242
  }
1795
2243
 
1796
2244
  function SelectField(props) {
1797
- const { label, hint, ariaLabel, block = true, ...rest } = props
1798
- return h('div', { className: 'capital-automation-field' },
1799
- h('span', null, label),
1800
- h(Select, { ...rest, label: ariaLabel ?? label, block }),
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 }),
1801
2250
  hint ? h('span', { className: 'capital-automation-hint' }, hint) : null)
1802
2251
  }
1803
2252
 
1804
2253
  function NumberField(props) {
1805
- const { label, value, min, max, disabled, onChange } = props
1806
- return h(Field, { label },
2254
+ const { label, value, min, max, disabled, onChange, error, onBlur } = props
2255
+ return h(Field, { label, error },
1807
2256
  h('input', {
1808
2257
  type: 'number',
1809
2258
  min,
1810
2259
  max,
1811
2260
  value,
1812
2261
  disabled,
2262
+ 'aria-invalid': error ? 'true' : undefined,
1813
2263
  onChange: (event) => onChange(Number(event.target.value)),
2264
+ onBlur,
1814
2265
  }))
1815
2266
  }
1816
2267
 
@@ -1879,13 +2330,15 @@ function OverviewTab(props) {
1879
2330
  const stateText = anyRunning ? zh.status.running : stateLabel(detail.task.state)
1880
2331
  const originLabel = detail.task.origin === 'builtin' ? zh.origin.builtin : zh.origin.custom
1881
2332
  const scheduleText = `${scheduleRangeSummary(detail.task.trigger)}${detail.task.trigger.tradingDaysOnly === true ? ` · ${zh.tradingDaysField}` : ''}`
1882
- const scheduleHint = detail.task.trigger.tradingDaysOnly === true ? zh.triggerHint : zh.triggerHintNatural
2333
+ const scheduleHint = detail.task.trigger.cadence === 'once'
2334
+ ? zh.triggerHintOnce
2335
+ : detail.task.trigger.tradingDaysOnly === true ? zh.triggerHint : zh.triggerHintNatural
1883
2336
  return h('div', { className: 'capital-automation-overview' },
1884
2337
  h('div', { className: 'capital-automation-statgrid' },
1885
2338
  h('div', { className: 'capital-automation-stat' },
1886
2339
  h('span', { className: 'capital-automation-statlabel' }, zh.stateLabel),
1887
2340
  h('span', { className: 'capital-automation-statvalue', 'data-tone': anyRunning ? 'running' : undefined }, stateText),
1888
- h('span', { className: 'capital-automation-statsub', title: detail.task.origin === 'builtin' ? zh.configBuiltinHint : undefined }, originLabel)),
2341
+ h('span', { className: 'capital-automation-statsub' }, originLabel)),
1889
2342
  h('div', { className: 'capital-automation-stat' },
1890
2343
  h('span', { className: 'capital-automation-statlabel' }, zh.nextRun),
1891
2344
  h('span', { className: 'capital-automation-statvalue' }, next.value),
@@ -1923,10 +2376,7 @@ function OverviewTab(props) {
1923
2376
  h('span', { className: 'capital-automation-kvvalue' }, workspaceTitle(meta, detail.task.workspaceId))),
1924
2377
  h('div', { className: 'capital-automation-kvitem' },
1925
2378
  h('span', { className: 'capital-automation-kvlabel' }, zh.categoryField),
1926
- h('span', { className: 'capital-automation-kvvalue' }, categoryLabel(detail.task.category))),
1927
- h('div', { className: 'capital-automation-kvitem' },
1928
- h('span', { className: 'capital-automation-kvlabel' }, zh.modeField),
1929
- h('span', { className: 'capital-automation-kvvalue' }, modeLabel(detail.task.mode)))),
2379
+ h('span', { className: 'capital-automation-kvvalue' }, categoryLabel(detail.task.category)))),
1930
2380
  h('section', { className: 'capital-automation-technical' },
1931
2381
  h('h4', { className: 'capital-automation-technicaltitle' }, zh.techInfo),
1932
2382
  h('dl', null,
@@ -1947,40 +2397,61 @@ function OverviewTab(props) {
1947
2397
  function ConfigTab(props) {
1948
2398
  const { draft, setDraft, meta, busy, dirty, save, reset, disabled } = props
1949
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')
1950
2404
  const set = (patch) => setDraft((current) => ({
1951
2405
  ...current,
1952
2406
  ...(typeof patch === 'function' ? patch(current) : patch),
1953
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
+ }
1954
2419
  const timeoutRange = limitValue(meta, 'runTimeoutMinutes', [5, 120])
1955
2420
  const idleRange = limitValue(meta, 'idleTimeoutMinutes', [1, 30])
1956
2421
  const stepsRange = limitValue(meta, 'maxSteps', [10, 2000])
1957
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 })))
1958
2446
  const advanced = h('details', { className: 'capital-automation-advanced' },
1959
2447
  h('summary', null, zh.advanced),
1960
2448
  h('div', { className: 'capital-automation-advancedbody' },
1961
2449
  h('div', { className: 'capital-automation-formgrid' },
1962
- h(SelectField, {
1963
- label: zh.workspaceField,
1964
- value: task.workspaceId ?? '',
1965
- options: workspaceOptions(meta, task.workspaceId).map((option) => ({ value: option.id, label: option.title })),
1966
- disabled: busy || disabled,
1967
- onChange: (workspaceId) => set({ workspaceId }),
1968
- }),
1969
- h(SelectField, {
1970
- label: zh.modeField,
1971
- hint: zh.modeHint,
1972
- value: task.mode,
1973
- options: Object.entries(zh.mode).map(([value, label]) => ({ value, label })),
1974
- disabled: busy || disabled,
1975
- onChange: (mode) => set({ mode }),
1976
- })),
1977
- h('div', { className: 'capital-automation-formgrid' },
1978
- 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 } })) }),
1979
- 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 } })) }),
1980
- 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 } })) }),
1981
- 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 } })) })),
1982
- h('p', { className: 'capital-automation-hint' }, zh.budgetHint),
1983
- 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)))
1984
2455
  const notificationRows = h('div', null,
1985
2456
  h('span', { className: 'capital-automation-formlabel' }, zh.notifyField),
1986
2457
  h('div', { className: 'capital-automation-switchlist' },
@@ -1988,41 +2459,52 @@ function ConfigTab(props) {
1988
2459
  h(SwitchRow, { label: zh.notifyFailure, checked: task.notifications.onFailure, disabled: busy || disabled, onChange: (value) => set((current) => ({ notifications: { ...current.notifications, onFailure: value } })) }),
1989
2460
  h(SwitchRow, { label: zh.notifySuccess, checked: task.notifications.onSuccess, disabled: busy || disabled, onChange: (value) => set((current) => ({ notifications: { ...current.notifications, onSuccess: value } })) })),
1990
2461
  h('p', { className: 'capital-automation-hint' }, zh.notifyHint))
1991
- return h('div', { className: 'capital-automation-form' },
1992
- task.origin === 'builtin'
1993
- ? h('div', { className: 'capital-automation-formnote' },
1994
- h(IconInfoOutlineRegular, { size: 13 }),
1995
- h('span', null, zh.configBuiltinHint))
1996
- : null,
1997
- h(Field, { label: zh.titleField },
1998
- h('input', { value: task.title, disabled: busy || disabled, onChange: (event) => set({ title: event.target.value }) })),
1999
- h(Field, { label: zh.descriptionField },
2000
- h('textarea', { value: task.description ?? '', disabled: busy || disabled, rows: 3, onChange: (event) => set({ description: event.target.value }) })),
2001
- h(SelectField, {
2002
- label: zh.categoryField,
2003
- hint: task.origin === 'builtin' ? zh.categoryFixed : undefined,
2004
- value: task.category,
2005
- options: Object.entries(zh.category).map(([value, label]) => ({ value, label })),
2006
- disabled: busy || disabled || task.origin === 'builtin',
2007
- onChange: (category) => set({ category }),
2008
- }),
2009
- h(Field, { label: zh.instructionField },
2010
- h('textarea', {
2011
- value: task.instruction,
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,
2012
2472
  disabled: busy || disabled,
2013
- rows: 14,
2014
- onChange: (event) => set({ instruction: event.target.value }),
2473
+ 'aria-invalid': titleError ? 'true' : undefined,
2474
+ onChange: (event) => set({ title: event.target.value }),
2475
+ onBlur: () => touch('title'),
2015
2476
  })),
2016
- h('div', null,
2017
- h('span', { className: 'capital-automation-formlabel' }, zh.triggerField),
2018
- h(TriggerFields, { trigger: task.trigger, disabled: busy || disabled, onChange: (trigger) => set({ trigger }), meta })),
2477
+ h(SelectField, {
2478
+ label: zh.categoryField,
2479
+ title: task.origin === 'builtin' ? zh.categoryFixed : undefined,
2480
+ value: task.category,
2481
+ options: Object.entries(zh.category).map(([value, label]) => ({ value, label })),
2482
+ disabled: busy || disabled || task.origin === 'builtin',
2483
+ onChange: (category) => set({ category }),
2484
+ })),
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),
2019
2501
  advanced,
2020
2502
  notificationRows,
2021
2503
  h('div', { className: 'capital-automation-formactions', 'data-sticky': 'true' },
2022
2504
  h('span', { className: 'capital-automation-formstatus' },
2023
2505
  disabled ? zh.storageUnavailable : dirty ? zh.dirty : zh.noDirty),
2024
- h('button', { type: 'button', className: 'capital-automation-btn', disabled: busy || !dirty, onClick: reset }, zh.cancel),
2025
- 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)))
2026
2508
  }
2027
2509
 
2028
2510
  function NotificationsTab(props) {
@@ -2125,11 +2607,6 @@ function TaskDetailPage(props) {
2125
2607
  }
2126
2608
 
2127
2609
  async function save() {
2128
- const invalid = validateDraft(task)
2129
- if (invalid.length > 0) {
2130
- setError(invalid)
2131
- return
2132
- }
2133
2610
  await act('save', async () => {
2134
2611
  await api.patch(detail.task.id, draftPayload(task))
2135
2612
  }, zh.saved)
@@ -2284,6 +2761,9 @@ function NewTaskModal(props) {
2284
2761
  const [draft, setDraft] = useState(defaultDraft)
2285
2762
  const [busy, setBusy] = useState(false)
2286
2763
  const [error, setError] = useState('')
2764
+ const { issues, errorFor, touch, touchAll } = useDraftIssues(draft, open)
2765
+ const titleError = errorFor('title')
2766
+ const instructionError = errorFor('instruction')
2287
2767
  useEffect(() => {
2288
2768
  if (!open) return
2289
2769
  setDraft(defaultDraft())
@@ -2299,9 +2779,8 @@ function NewTaskModal(props) {
2299
2779
  const stepsRange = limitValue(meta, 'maxSteps', [10, 2000])
2300
2780
  const toolsRange = limitValue(meta, 'maxToolCalls', [10, 6000])
2301
2781
  async function create() {
2302
- const invalid = validateDraft(draft)
2303
- if (invalid.length > 0) {
2304
- setError(invalid)
2782
+ if (Object.keys(issues).length > 0) {
2783
+ touchAll()
2305
2784
  return
2306
2785
  }
2307
2786
  setBusy(true)
@@ -2328,79 +2807,81 @@ function NewTaskModal(props) {
2328
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)),
2329
2808
  },
2330
2809
  h('div', { className: 'capital-automation-wizard' },
2331
- h('div', { className: 'capital-automation-modalheadline' },
2810
+ h('div', { className: 'capital-automation-modalheadline', 'data-invalid': titleError ? 'true' : undefined },
2332
2811
  h(Glyph, { task: draft, size: 42 }),
2333
2812
  h('input', {
2334
2813
  className: 'capital-automation-modaltitleinput',
2335
2814
  value: draft.title,
2815
+ maxLength: TITLE_MAX_LENGTH,
2336
2816
  placeholder: zh.titlePlaceholder,
2337
2817
  'aria-label': zh.titleField,
2818
+ 'aria-invalid': titleError ? 'true' : undefined,
2338
2819
  onChange: (event) => set({ title: event.target.value }),
2820
+ onBlur: () => touch('title'),
2339
2821
  })),
2822
+ titleError ? h('span', { className: 'capital-automation-fielderror', role: 'alert' }, titleError) : null,
2340
2823
  h('p', { className: 'capital-automation-hint' }, zh.newTaskHint),
2341
- h(Field, { label: zh.descriptionField },
2342
- h('input', {
2343
- value: draft.description,
2344
- placeholder: zh.descriptionPlaceholder,
2345
- onChange: (event) => set({ description: event.target.value }),
2346
- })),
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
+ })),
2347
2834
  h('div', null,
2348
2835
  h('span', { className: 'capital-automation-formlabel' }, zh.triggerField),
2349
- h(TriggerFields, { trigger: draft.trigger, onChange: (trigger) => set({ trigger }), meta })),
2836
+ h(TriggerFields, { trigger: draft.trigger, onChange: (trigger) => set({ trigger }), meta, errorFor, touch })),
2350
2837
  h('div', null,
2351
2838
  h('span', { className: 'capital-automation-formlabel' }, zh.instructionField),
2352
- h('div', { className: 'capital-automation-promptbox' },
2353
- h('textarea', {
2354
- value: draft.instruction,
2355
- placeholder: zh.instructionPlaceholder,
2356
- 'aria-label': zh.instructionField,
2357
- onChange: (event) => set({ instruction: event.target.value }),
2358
- }),
2359
- h('div', { className: 'capital-automation-prompttoolbar' },
2360
- h('div', { className: 'capital-automation-toolbarfield' },
2361
- h(IconFolderOpenOutlineRegular, { size: 13 }),
2362
- h('span', null, zh.workspaceField),
2363
- h(Select, {
2364
- value: draft.workspaceId ?? '',
2365
- options: workspaceOptions(meta, draft.workspaceId).map((option) => ({ value: option.id, label: option.title })),
2366
- onChange: (workspaceId) => set({ workspaceId }),
2367
- label: zh.workspaceField,
2368
- size: 'sm',
2369
- align: 'end',
2370
- })),
2371
- h('div', { className: 'capital-automation-toolbarfield' },
2372
- h('span', null, zh.categoryField),
2373
- h(Select, {
2374
- value: draft.category,
2375
- options: Object.entries(zh.category).map(([value, label]) => ({ value, label })),
2376
- onChange: (category) => set({ category }),
2377
- label: zh.categoryField,
2378
- size: 'sm',
2379
- align: 'end',
2380
- })),
2381
- h('div', { className: 'capital-automation-toolbarfield' },
2382
- h('span', null, zh.modeField),
2383
- h(Select, {
2384
- value: draft.mode,
2385
- options: Object.entries(zh.mode).map(([value, label]) => ({ value, label })),
2386
- onChange: (mode) => set({ mode }),
2387
- label: zh.modeField,
2388
- size: 'sm',
2389
- align: 'end',
2390
- })),
2391
- h('span', { className: 'capital-automation-presetchip', title: zh.modeHint },
2392
- h(IconSparkleRegular, { size: 12 }),
2393
- zh.presetValue)))),
2394
- 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)),
2395
2876
  meta?.workspaceError ? h('p', { className: 'capital-automation-hint', 'data-tone': 'error' }, zh.workspaceUnavailable) : null,
2396
2877
  h('details', { className: 'capital-automation-advanced' },
2397
2878
  h('summary', null, zh.advanced),
2398
2879
  h('div', { className: 'capital-automation-advancedbody' },
2399
2880
  h('div', { className: 'capital-automation-formgrid' },
2400
- h(NumberField, { label: zh.runTimeout, value: draft.limits.runTimeoutMinutes, min: timeoutRange[0], max: timeoutRange[1], onChange: (value) => set((current) => ({ limits: { ...current.limits, runTimeoutMinutes: value } })) }),
2401
- h(NumberField, { label: zh.idleTimeout, value: draft.limits.idleTimeoutMinutes, min: idleRange[0], max: idleRange[1], onChange: (value) => set((current) => ({ limits: { ...current.limits, idleTimeoutMinutes: value } })) }),
2402
- h(NumberField, { label: zh.maxSteps, value: draft.limits.maxSteps, min: stepsRange[0], max: stepsRange[1], onChange: (value) => set((current) => ({ limits: { ...current.limits, maxSteps: value } })) }),
2403
- 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 } })) })),
2404
2885
  h('p', { className: 'capital-automation-hint' }, zh.advancedHint))),
2405
2886
  h('div', null,
2406
2887
  h('span', { className: 'capital-automation-formlabel' }, zh.notifyField),
@@ -2729,8 +3210,6 @@ function AutomationPage(props) {
2729
3210
  const initialLoading = meta === null && loadError.length === 0
2730
3211
 
2731
3212
  return h('div', { className: 'capital-automation-page' },
2732
- h('datalist', { id: 'capital-automation-timezones' },
2733
- TIMEZONES.map((zone) => h('option', { key: zone, value: zone }))),
2734
3213
  detailOpen ? null : h(Header, {
2735
3214
  tab,
2736
3215
  onTab: switchTab,