@v587d/capital-generation 2.14.1 → 2.15.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (39) hide show
  1. package/CHANGELOG.md +137 -0
  2. package/README.md +7 -12
  3. package/THIRD_PARTY_NOTICES.md +1 -0
  4. package/automation/TABLER-ICONS-LICENSE +21 -0
  5. package/automation/builtins.js +237 -10
  6. package/automation/client.js +37 -5
  7. package/automation/client.src.cjs +578 -126
  8. package/automation/defaults.js +4 -4
  9. package/automation/index.js +1 -0
  10. package/automation/package.json +4 -2
  11. package/automation/route.js +1 -1
  12. package/automation/runner.js +5 -0
  13. package/automation/scheduler.js +34 -29
  14. package/automation/schema.js +39 -5
  15. package/automation/service.js +13 -10
  16. package/automation/store.js +64 -21
  17. package/automation/task-icons.js +75 -0
  18. package/chart-ui/client.js +2 -2
  19. package/chart-ui/package.json +1 -1
  20. package/config/client.js +12 -5
  21. package/config/client.src.cjs +143 -45
  22. package/config/index.js +5 -5
  23. package/config/package.json +1 -1
  24. package/lib/chart/runtime.d.ts +1 -1
  25. package/lib/chart/runtime.js +95 -0
  26. package/lib/chart/series.d.ts +4 -4
  27. package/lib/chart/series.js +44 -10
  28. package/lib/index.js +5 -5
  29. package/lib/web-retriever/local-fetch.d.ts +1 -1
  30. package/lib/web-retriever/local-fetch.js +1 -1
  31. package/lib/web-retriever/sources.d.ts +4 -2
  32. package/lib/web-retriever/sources.js +73 -17
  33. package/lib/web-retriever/wind-client.d.ts +1 -1
  34. package/lib/web-retriever/wind-client.js +1 -1
  35. package/package.json +6 -6
  36. package/preset/capital-generation/README.md +1 -1
  37. package/preset/capital-generation/skills/capital-chart-protocol/SKILL.md +6 -3
  38. package/preset/capital-generation/skills/capital-visualization-protocol/SKILL.md +2 -0
  39. package/watchlist/package.json +1 -1
@@ -15,11 +15,7 @@ const {
15
15
  IconEditOutlineRegular,
16
16
  IconEllipsisOutlineRegular,
17
17
  IconFolderOpenOutlineRegular,
18
- IconGaugeOutlineRegular,
19
- IconGlobeOutlineRegular,
20
18
  IconInfoOutlineRegular,
21
- IconLightOutlineMedium,
22
- IconListPenOutlineRegular,
23
19
  IconPauseOutlineRegular,
24
20
  IconPlayOutlineRegular,
25
21
  IconPlusOutlineRegular,
@@ -34,6 +30,12 @@ const {
34
30
  Modal,
35
31
  Switch,
36
32
  } = require('@deepseek-ai/dsh-client-ui-primitives')
33
+ const {
34
+ TASK_ICON_CATALOG,
35
+ TASK_ICON_IDS,
36
+ DEFAULT_CUSTOM_ICON,
37
+ builtinTaskIcon,
38
+ } = require('./task-icons.js')
37
39
 
38
40
  const ENTRY_NAME = 'capital-automation'
39
41
  const ROUTE = '/capital-automation'
@@ -42,7 +44,7 @@ const STYLE_TAG = 'capital-automation-css'
42
44
  const ACTIVE_RUN_STATUSES = ['queued', 'starting', 'running']
43
45
  const FAILURE_RUN_STATUSES = ['failed', 'timed_out', 'blocked_requires_user', 'unknown_after_restart']
44
46
  const SKIPPED_RUN_STATUSES = ['skipped_overlap', 'skipped_calendar_unavailable', 'missed']
45
- const DEFAULT_LIMITS = { runTimeoutMinutes: 30, idleTimeoutMinutes: 5, maxSteps: 300, maxToolCalls: 1200 }
47
+ const DEFAULT_LIMITS = { runTimeoutMinutes: 60, idleTimeoutMinutes: 5, maxSteps: 300, maxToolCalls: 1200 }
46
48
  const WEEKDAYS = [
47
49
  { value: 1, label: '一' },
48
50
  { value: 2, label: '二' },
@@ -74,6 +76,8 @@ const HISTORY_DAYS = 30
74
76
  const DETAIL_RECENT_RUN_LIMIT = 3
75
77
  const TITLE_MAX_LENGTH = 12
76
78
  const DESCRIPTION_MAX_LENGTH = 100
79
+ const INSTRUCTION_MAX_LENGTH = 12_000
80
+ const AUTOSAVE_DELAY_MS = 600
77
81
 
78
82
  const zh = {
79
83
  panel: 'CapGen 投研自动化',
@@ -81,10 +85,10 @@ const zh = {
81
85
  subtitle: '定时投研,自动执行,结果可追溯',
82
86
  tabs: { tasks: '任务', runs: '运行' },
83
87
  storageUnavailable: '存储不可用,页面只读',
84
- standbyBanner: '调度器未取得运行权:定时触发与手动运行已暂停,任务配置仍可查看和编辑。',
88
+ standbyBanner: '本窗口的定时任务已暂停:定时触发与手动运行不可用;若另一个 CapGen 窗口在运行,定时任务仍会由它执行。',
85
89
  storageBanner: '投研自动化存储暂时不可用:页面只读,调度器不会启动。',
86
90
  calendarBanner: '交易日历暂时不可用:仅交易日的任务会跳过,其他任务仍按设定时间运行。',
87
- calendarCoverageBanner: '交易日历只覆盖到 {through}:之后仅交易日的任务会按 fail-closed 跳过;交易所公布新一年安排后会自动接上。',
91
+ calendarCoverageBanner: '交易日历只覆盖到 {through}:之后仅交易日的任务会跳过;交易所公布新一年安排后会自动接上。',
88
92
  calendarField: '交易日历',
89
93
  calendarSourceSzse: '深交所官方日历',
90
94
  calendarSourceBundled: '内置表',
@@ -102,7 +106,6 @@ const zh = {
102
106
  clearFilter: '清除筛选',
103
107
  newTask: '新建任务',
104
108
  newTaskTitle: '新建投研任务',
105
- newTaskHint: '任务会在后台新建一个 session 执行,不需要你守在页面上。',
106
109
  editTask: '编辑任务',
107
110
  allTasks: '全部任务',
108
111
  backToTasks: '所有任务',
@@ -123,7 +126,7 @@ const zh = {
123
126
  filterReview: '复盘',
124
127
  filterResearch: '研究',
125
128
  filterLabel: '筛选任务',
126
- category: { news: '资讯', market: '行情', review: '复盘', research: '研究' },
129
+ category: { news: '资讯', market: '行情', review: '复盘', research: '研究', portfolio: '组合', monitor: '监控' },
127
130
  categoryField: '任务分类',
128
131
  categoryFixed: `'复制为我的任务'后,可调整任务分类。`,
129
132
  cadence: { daily: '每天', weekly: '每周', monthly: '每月', once: '单次' },
@@ -146,17 +149,21 @@ const zh = {
146
149
  remove: '删除任务',
147
150
  edit: '编辑',
148
151
  more: '更多操作',
149
- save: '保存',
150
152
  saving: '保存中…',
151
153
  processing: '处理中…',
152
- saved: '已保存',
153
154
  cancel: '取消',
154
155
  create: '创建任务',
155
156
  creating: '创建中…',
156
157
  titleRequired: '标题不能为空。',
157
158
  instructionRequired: '任务指令不能为空。',
158
159
  invalidLimit: '请输入有效数字。',
159
- dirty: '有未保存的修改',
160
+ invalidLimitRange: '请输入 {min}–{max} 的整数。',
161
+ autosaveHint: '修改会自动保存',
162
+ autosavePending: '等待保存…',
163
+ autosaveSaved: '已自动保存',
164
+ autosaveFailed: '保存失败',
165
+ autosaveRetry: '重试',
166
+ autosaveInvalid: '填写完整后自动保存',
160
167
  noDirty: '配置已同步',
161
168
  nextRun: '下次运行',
162
169
  nextRunDormant: '添加后开始调度',
@@ -173,6 +180,9 @@ const zh = {
173
180
  notifications: '通知',
174
181
  descriptionField: '任务说明',
175
182
  titleField: '任务标题',
183
+ iconField: '任务图标',
184
+ iconPickHint: '点击图标更换',
185
+ iconBuiltinHint: '内置模板的图标固定;复制为我的任务后可更换。',
176
186
  instructionField: '任务指令',
177
187
  instructionPlaceholder: '例如:读取今日自选股行情、公告与相关新闻,按「结论—证据—风险」整理成一份可复核的盘后复盘。',
178
188
  titlePlaceholder: '给这个任务起个名字',
@@ -225,10 +235,10 @@ const zh = {
225
235
  maxSteps: '最大步骤',
226
236
  maxToolCalls: '最大工具调用',
227
237
  notifyField: '通知',
228
- notifyInApp: '应用内通知',
229
- notifyFailure: '失败时通知',
238
+ notifyInApp: '写入任务通知',
239
+ notifyFailure: '失败、超时或需人工处理时',
230
240
  notifySuccess: '成功时通知',
231
- notifyHint: '通知只记录在当前 profile;不会发送邮件或外部消息。',
241
+ notifyHint: '只写入当前 profile 的任务通知中心;不会发送邮件或外部消息。',
232
242
  presetLabel: '运行模式',
233
243
  presetValue: '自动模式',
234
244
  presetHint: '所有运行固定使用自动模式。',
@@ -270,15 +280,32 @@ const zh = {
270
280
  triggerScheduled: '定时',
271
281
  openSession: '打开 session',
272
282
  runSummaryFallback: '运行结束但没有摘要。',
273
- notificationsEmpty: '没有通知。',
283
+ notificationsEmpty: '没有任务通知。',
274
284
  markRead: '全部标为已读',
275
285
  markTaskRead: '本任务标为已读',
276
- notificationsTitle: '通知',
277
- notificationsHint: '运行失败、超时、调度器待命与交易日历问题会出现在这里。',
286
+ notificationsTitle: '任务通知',
287
+ notificationsEntry: '任务通知',
288
+ notificationsHint: '运行失败、超时、成功与交易日历问题会出现在这里。',
278
289
  notificationsUnread: '{n} 条未读',
279
290
  allRead: '全部已读',
280
- noNotifications: '没有未读或历史通知。',
291
+ noNotifications: '没有未读或历史任务通知。',
281
292
  notificationOpenTask: '查看任务',
293
+ systemStatus: '系统状态',
294
+ systemActive: '定时调度正常',
295
+ systemStandby: '本窗口待命,不启动新的定时运行',
296
+ systemStarting: '定时调度启动中',
297
+ systemStopped: '定时调度未运行',
298
+ systemUnavailable: '定时调度不可用',
299
+ systemCalendarOk: '交易日历正常',
300
+ systemCalendarUnavailable: '交易日历不可用',
301
+ systemDetails: '详情',
302
+ systemHideDetails: '收起',
303
+ systemLastStandby: '上次中断',
304
+ systemNoStandby: '没有调度中断记录。',
305
+ systemReasonLeaseHeld: '另一个 CapGen 窗口正在负责定时任务。',
306
+ systemReasonLeaseLost: '本窗口的调度权已失效,可能已由另一个窗口接管。',
307
+ systemReasonUnverified: '本窗口的调度权校验未通过。',
308
+ systemTechnical: '技术信息',
282
309
  close: '关闭',
283
310
  confirmDeleteTitle: '删除这个任务?',
284
311
  confirmDeleteBody: '任务配置、运行记录与相关通知会一起删除,无法恢复。',
@@ -299,7 +326,7 @@ const zh = {
299
326
  markReadDone: '已标为已读',
300
327
  errorGeneric: '操作失败,请稍后重试。',
301
328
  needConfirm: '需要用户确认',
302
- schedulerBlocked: '调度器未取得运行权,暂时不能手动运行。',
329
+ schedulerBlocked: '本窗口的定时任务已暂停,暂时不能手动运行。',
303
330
  runBlocked: '任务正在运行,结束或取消后才能再次运行。',
304
331
  configWhileRunning: '任务正在运行;修改会在下一次运行时生效。',
305
332
  detailRecentRuns: '最近 {n} 次运行',
@@ -312,8 +339,8 @@ const zh = {
312
339
  notificationFallbackFailed: '运行失败;打开对应任务或 session 查看详情。',
313
340
  notificationFallbackTimeout: '运行超时;打开对应任务或 session 查看详情。',
314
341
  notificationFallbackSuccess: '运行完成。',
315
- notificationFallbackStandby: '调度器未取得运行权,定时触发暂停。',
316
- notificationFallbackCalendar: '交易日历不可用,交易日任务已跳过。',
342
+ notificationFallbackStandby: '定时触发已暂停。',
343
+ notificationFallbackCalendar: '暂时无法确认交易日,相关任务已跳过。',
317
344
  notificationFallbackSkipped: '本次计划已跳过。',
318
345
  statusLabel: '状态',
319
346
  triggerSummary: '日程',
@@ -344,7 +371,7 @@ function installStyles() {
344
371
  style.setAttribute(`data-${STYLE_TAG}`, 'true')
345
372
  style.textContent = `
346
373
  .capital-automation-page, .capital-automation-modal { --ca-card: var(--dsw-alias-bg-layer-2, var(--dsw-alias-bg-layer-1)); --ca-line: var(--dsw-alias-border-l3, var(--dsw-alias-border-l1)); --ca-line-soft: var(--dsw-alias-border-l2, var(--dsw-alias-border-l1)); --ca-table-line: var(--ca-line); --ca-muted: var(--dsw-alias-label-tertiary); --ca-caption: var(--dsw-alias-label-caption, var(--dsw-alias-label-tertiary)); --ca-hover: var(--dsw-alias-interactive-bg-hover); --ca-field: var(--dsw-alias-bg-module-platform); --ca-business: var(--dsw-alias-state-business-primary, var(--dsw-alias-brand-primary)); --ca-success: var(--dsw-alias-state-success-primary); --ca-error: var(--dsw-alias-state-error-primary); --ca-warn: var(--dsw-alias-state-warn-label); --ca-business-text: var(--ca-business); --ca-success-text: var(--ca-success); --ca-error-text: var(--ca-error); --ca-warn-text: var(--ca-warn); }
347
- body:not([data-ds-dark-theme]) .capital-automation-page, body:not([data-ds-dark-theme]) .capital-automation-modal { --ca-line: #0000001f; --ca-line-soft: #0000001a; --ca-table-line: #00000026; --ca-muted: #5d626a; --ca-caption: #6a6f77; --ca-hover: #eef1f5; --ca-field: #f7f8fa; --ca-business: #2f63d6; --ca-success: #16a34a; --ca-error: #dc2626; --ca-warn: #d97706; --ca-business-text: #2f63d6; --ca-success-text: #15803d; --ca-error-text: #c81e1e; --ca-warn-text: #b45309; --ca-tag-news: #2f6fd0; --ca-tag-market: #a85a08; --ca-tag-review: #6d4ac4; --ca-tag-research: #0f766e; --ca-tag-custom: #b3246d; }
374
+ body:not([data-ds-dark-theme]) .capital-automation-page, body:not([data-ds-dark-theme]) .capital-automation-modal { --ca-line: #0000001f; --ca-line-soft: #0000001a; --ca-table-line: #00000026; --ca-muted: #5d626a; --ca-caption: #6a6f77; --ca-hover: #eef1f5; --ca-field: #f7f8fa; --ca-business: #2f63d6; --ca-success: #16a34a; --ca-error: #dc2626; --ca-warn: #d97706; --ca-business-text: #2f63d6; --ca-success-text: #15803d; --ca-error-text: #c81e1e; --ca-warn-text: #b45309; --ca-tag-news: #2f6fd0; --ca-tag-market: #a85a08; --ca-tag-review: #6d4ac4; --ca-tag-research: #0f766e; --ca-tag-portfolio: #4d7c0f; --ca-tag-monitor: #0e7490; --ca-tag-custom: #b3246d; }
348
375
  .capital-automation-page { display: flex; flex-direction: column; min-height: 0; height: 100%; color: var(--dsw-alias-label-primary); }
349
376
  .capital-automation-head { flex: none; padding: 18px 0 0; }
350
377
  .capital-automation-headinner { box-sizing: border-box; width: min(1080px, 100%); margin: 0 auto; padding: 0 clamp(20px, 3vw, 36px); }
@@ -444,6 +471,8 @@ function installStyles() {
444
471
  .capital-automation-glyph[data-tone="market"] { background: linear-gradient(135deg, #ffbd4a, #f97316); }
445
472
  .capital-automation-glyph[data-tone="review"] { background: linear-gradient(135deg, #b18cff, #7c3aed); }
446
473
  .capital-automation-glyph[data-tone="research"] { background: linear-gradient(135deg, #39d7a8, #0d9488); }
474
+ .capital-automation-glyph[data-tone="portfolio"] { background: linear-gradient(135deg, #84cc16, #4d7c0f); }
475
+ .capital-automation-glyph[data-tone="monitor"] { background: linear-gradient(135deg, #06b6d4, #0e7490); }
447
476
  .capital-automation-glyph[data-tone="custom"] { background: linear-gradient(135deg, #f472b6, #db2777); }
448
477
  .capital-automation-identitytext { min-width: 0; }
449
478
  .capital-automation-tasktitlerow { display: flex; flex-wrap: wrap; align-items: center; gap: 2px 6px; min-width: 0; }
@@ -456,12 +485,16 @@ function installStyles() {
456
485
  .capital-automation-tag[data-tone="market"] { color: #ffb45c; }
457
486
  .capital-automation-tag[data-tone="review"] { color: #b79aff; }
458
487
  .capital-automation-tag[data-tone="research"] { color: #56d9b4; }
488
+ .capital-automation-tag[data-tone="portfolio"] { color: #a3e635; }
489
+ .capital-automation-tag[data-tone="monitor"] { color: #22d3ee; }
459
490
  .capital-automation-tag[data-tone="custom"] { color: #f79bc3; }
460
491
  body:not([data-ds-dark-theme]) .capital-automation-tag { background: color-mix(in srgb, currentColor 9%, var(--ca-card)); }
461
492
  body:not([data-ds-dark-theme]) .capital-automation-tag[data-tone="news"] { color: var(--ca-tag-news); }
462
493
  body:not([data-ds-dark-theme]) .capital-automation-tag[data-tone="market"] { color: var(--ca-tag-market); }
463
494
  body:not([data-ds-dark-theme]) .capital-automation-tag[data-tone="review"] { color: var(--ca-tag-review); }
464
495
  body:not([data-ds-dark-theme]) .capital-automation-tag[data-tone="research"] { color: var(--ca-tag-research); }
496
+ body:not([data-ds-dark-theme]) .capital-automation-tag[data-tone="portfolio"] { color: var(--ca-tag-portfolio); }
497
+ body:not([data-ds-dark-theme]) .capital-automation-tag[data-tone="monitor"] { color: var(--ca-tag-monitor); }
465
498
  body:not([data-ds-dark-theme]) .capital-automation-tag[data-tone="custom"] { color: var(--ca-tag-custom); }
466
499
  .capital-automation-schedule { display: block; color: var(--dsw-alias-label-secondary); font-size: 12px; line-height: 18px; }
467
500
  .capital-automation-schedulesub { display: block; margin-top: 1px; color: var(--ca-caption); font-size: 11px; line-height: 16px; }
@@ -558,6 +591,22 @@ function installStyles() {
558
591
  .capital-automation-fieldhead { display: flex; flex-wrap: wrap; align-items: baseline; gap: 2px 8px; min-width: 0; }
559
592
  .capital-automation-fieldlabel { color: var(--ca-muted); font-size: 11.5px; line-height: 17px; }
560
593
  .capital-automation-fieldhint { color: var(--ca-caption); font-size: 11px; line-height: 16px; font-variant-numeric: tabular-nums; }
594
+ .capital-automation-glyphbtn { display: inline-flex; flex: none; align-items: center; gap: 4px; padding: 0; border: 0; border-radius: 14px; background: transparent; cursor: pointer; }
595
+ .capital-automation-glyphbtn:hover:not(:disabled) .capital-automation-glyph { box-shadow: inset 0 1px 0 rgba(255,255,255,.22), 0 0 0 2px color-mix(in srgb, var(--ca-business) 52%, transparent); }
596
+ .capital-automation-glyphbtn:focus-visible { outline: 2px solid var(--dsw-focus-ring-color, var(--ca-business)); outline-offset: 2px; }
597
+ .capital-automation-glyphbtn:disabled { cursor: not-allowed; opacity: .55; }
598
+ .capital-automation-glyphbtnchevron { display: grid; flex: none; place-items: center; width: 12px; color: var(--ca-muted); }
599
+ .capital-automation-glyphbtn:hover:not(:disabled) .capital-automation-glyphbtnchevron { color: var(--ca-business-text); }
600
+ .capital-automation-iconrow { display: flex; align-items: center; gap: 12px; min-width: 0; }
601
+ .capital-automation-iconrowbody { display: flex; flex-direction: column; gap: 2px; min-width: 0; }
602
+ /* The menu is portaled to body, outside the page's --ca-* scope; use host tokens. */
603
+ .capital-automation-iconmenu { box-sizing: border-box; width: 400px; max-width: calc(100vw - 24px); padding: 10px; }
604
+ .capital-automation-iconmenu-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(32px, 1fr)); gap: 6px; }
605
+ button.capital-automation-iconmenu-item { box-sizing: border-box; display: inline-grid; place-items: center; width: 100%; max-width: 34px; aspect-ratio: 1; justify-self: center; padding: 0; border: 1px solid transparent; border-radius: 9px; background: transparent; color: var(--dsw-alias-label-tertiary); cursor: pointer; transition: background .15s, border-color .15s, color .15s; }
606
+ button.capital-automation-iconmenu-item:hover:not(:disabled) { background: var(--dsw-alias-interactive-bg-hover); color: var(--dsw-alias-label-primary); }
607
+ button.capital-automation-iconmenu-item[data-selected="true"] { border-color: var(--dsw-alias-brand-primary); background: color-mix(in srgb, var(--dsw-alias-brand-primary) 14%, transparent); color: var(--dsw-alias-brand-primary); }
608
+ button.capital-automation-iconmenu-item:focus-visible { outline: 2px solid var(--dsw-focus-ring-color, var(--dsw-alias-brand-primary)); outline-offset: 2px; }
609
+ button.capital-automation-iconmenu-item:disabled { opacity: .4; cursor: not-allowed; }
561
610
  .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; }
562
611
  .capital-automation-field input { height: 34px; }
563
612
  .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; }
@@ -586,6 +635,9 @@ function installStyles() {
586
635
  .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); }
587
636
  .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); }
588
637
  .capital-automation-formstatus { margin-right: auto; color: var(--ca-caption); font-size: 11.5px; line-height: 18px; }
638
+ .capital-automation-autosavestate { min-width: 0; max-width: min(520px, 70%); overflow: hidden; color: var(--ca-caption); font-size: 11.5px; line-height: 18px; font-variant-numeric: tabular-nums; text-overflow: ellipsis; white-space: nowrap; }
639
+ .capital-automation-autosavestate[data-tone="ok"] { color: var(--ca-success-text); }
640
+ .capital-automation-autosavestate[data-tone="error"] { color: var(--ca-error-text); }
589
641
  .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); }
590
642
  .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); }
591
643
  .capital-automation-triggerhead { display: flex; flex-wrap: nowrap; align-items: center; justify-content: space-between; gap: 12px; min-width: 0; }
@@ -668,6 +720,13 @@ function installStyles() {
668
720
  .capital-automation-notificationtitle { display: flex; flex-wrap: wrap; align-items: center; gap: 6px; color: var(--dsw-alias-label-primary); font-size: 12.5px; line-height: 19px; font-weight: 500; }
669
721
  .capital-automation-notificationmessage { display: -webkit-box; overflow: hidden; margin-top: 2px; color: var(--ca-muted); font-size: 11.5px; line-height: 18px; overflow-wrap: anywhere; -webkit-line-clamp: 3; -webkit-box-orient: vertical; }
670
722
  .capital-automation-notificationtime { display: block; margin-top: 3px; color: var(--ca-caption); font-size: 11px; line-height: 16px; }
723
+ .capital-automation-systemstatus { margin-top: 22px; padding: 12px 0 4px; border-top: .5px solid var(--ca-line); color: var(--ca-caption); font-size: 11px; line-height: 17px; }
724
+ .capital-automation-systemline { display: flex; align-items: center; justify-content: space-between; gap: 10px; flex-wrap: wrap; }
725
+ .capital-automation-systemtoggle { border: 0; padding: 0; background: transparent; color: var(--ca-muted); font: inherit; cursor: pointer; }
726
+ .capital-automation-systemtoggle:hover { color: var(--dsw-alias-label-primary); text-decoration: underline; }
727
+ .capital-automation-systemdetail { margin-top: 6px; color: var(--ca-muted); }
728
+ .capital-automation-systemdetail p { margin: 2px 0; }
729
+ .capital-automation-systemdetail summary { color: var(--ca-caption); cursor: pointer; }
671
730
  .capital-automation-modal { width: min(720px, calc(100vw - 32px)); max-width: min(720px, calc(100vw - 32px)); max-height: calc(100dvh - 40px); }
672
731
  .capital-automation-modal-wizard { width: min(600px, calc(100vw - 32px)); max-width: min(600px, calc(100vw - 32px)); }
673
732
  .capital-automation-modal-content { max-height: calc(100dvh - 128px); overflow: auto; }
@@ -755,6 +814,7 @@ function createApi() {
755
814
  credentials: 'same-origin',
756
815
  headers: options.body === undefined ? {} : { 'content-type': 'application/json' },
757
816
  body: options.body === undefined ? undefined : JSON.stringify(options.body),
817
+ ...(options.keepalive === true ? { keepalive: true } : {}),
758
818
  ...(controller === undefined ? {} : { signal: controller.signal }),
759
819
  })
760
820
  try {
@@ -777,7 +837,7 @@ function createApi() {
777
837
  tasks: () => request('/tasks'),
778
838
  detail: (id) => request(`/tasks/${encodeURIComponent(id)}`),
779
839
  create: (body) => request('/tasks', { method: 'POST', body }),
780
- patch: (id, body) => request(`/tasks/${encodeURIComponent(id)}`, { method: 'PATCH', body }),
840
+ patch: (id, body) => request(`/tasks/${encodeURIComponent(id)}`, { method: 'PATCH', body, keepalive: true }),
781
841
  remove: (id) => request(`/tasks/${encodeURIComponent(id)}`, { method: 'DELETE' }),
782
842
  action: (id, action) => request(`/tasks/${encodeURIComponent(id)}/${action}`, { method: 'POST' }),
783
843
  run: (id) => request(`/tasks/${encodeURIComponent(id)}/run`, { method: 'POST' }),
@@ -841,24 +901,124 @@ function toneOf(task) {
841
901
  return task?.origin === 'custom' ? 'custom' : (task?.category ?? 'news')
842
902
  }
843
903
 
844
- function taskGlyph(task) {
845
- if (task?.origin === 'custom') return IconSparkleRegular
846
- if (task?.sourceBuiltinId === 'morning-brief') return IconLightOutlineMedium
847
- if (task?.category === 'market') return IconGaugeOutlineRegular
848
- if (task?.category === 'review') return IconListPenOutlineRegular
849
- if (task?.category === 'research') return IconSearchOutlineRegular
850
- return IconGlobeOutlineRegular
904
+ // Twelve templates cannot stay in record-id order: read them daily → weekly → monthly, then by
905
+ // day and wall-clock time, so the card grid matches how the user plans the week.
906
+ const CADENCE_RANK = { daily: 0, weekly: 1, monthly: 2, once: 3 }
907
+ function firstWeekday(trigger) {
908
+ const days = Array.isArray(trigger?.weekdays) ? trigger.weekdays : []
909
+ return days.length === 0 ? 0 : Math.min(...days)
910
+ }
911
+ function compareTemplates(left, right) {
912
+ const rank = (CADENCE_RANK[left.trigger?.cadence] ?? 9) - (CADENCE_RANK[right.trigger?.cadence] ?? 9)
913
+ if (rank !== 0) return rank
914
+ const day = firstWeekday(left.trigger) - firstWeekday(right.trigger)
915
+ if (day !== 0) return day
916
+ return String(left.trigger?.time ?? '').localeCompare(String(right.trigger?.time ?? ''))
917
+ || String(left.title).localeCompare(String(right.title))
918
+ }
919
+
920
+ function iconDefinition(id) {
921
+ return typeof id === 'string' && Object.prototype.hasOwnProperty.call(TASK_ICON_CATALOG, id)
922
+ ? TASK_ICON_CATALOG[id]
923
+ : undefined
924
+ }
925
+
926
+ function taskIconId(task) {
927
+ if (iconDefinition(task?.icon) !== undefined) return task.icon
928
+ if (task?.origin === 'custom') return DEFAULT_CUSTOM_ICON
929
+ return builtinTaskIcon(task?.sourceBuiltinId)
930
+ }
931
+
932
+ function taskIcon(task) {
933
+ return iconDefinition(taskIconId(task)) ?? TASK_ICON_CATALOG[DEFAULT_CUSTOM_ICON]
934
+ }
935
+
936
+ // Tabler glyphs are a 24 grid with 2px strokes; the tile supplies size and color so one drawing
937
+ // works on every category tone.
938
+ function TaskGlyph(props) {
939
+ const { task, size } = props
940
+ const icon = taskIcon(task)
941
+ return h('svg', {
942
+ viewBox: '0 0 24 24',
943
+ width: size,
944
+ height: size,
945
+ fill: 'none',
946
+ stroke: 'currentColor',
947
+ strokeWidth: 2,
948
+ strokeLinecap: 'round',
949
+ strokeLinejoin: 'round',
950
+ 'aria-hidden': 'true',
951
+ focusable: 'false',
952
+ }, h('path', { d: icon.d }))
851
953
  }
852
954
 
853
955
  function Glyph(props) {
854
956
  const { task, size = 34, large = false } = props
855
- const GlyphIcon = taskGlyph(task)
856
957
  const edge = large ? 44 : size
857
958
  return h('span', {
858
959
  className: 'capital-automation-glyph',
859
960
  'data-tone': toneOf(task),
860
961
  style: { width: edge, height: edge, borderRadius: large ? 13 : 10 },
861
- }, h(GlyphIcon, { size: large ? 22 : 17 }))
962
+ }, h(TaskGlyph, { task, size: large ? 22 : 17 }))
963
+ }
964
+
965
+ function IconPickerTrigger(props) {
966
+ const { task, size = 38, large = false, onChange, disabled } = props
967
+ const [open, setOpen] = useState(false)
968
+ const selectedId = taskIconId(task)
969
+ const selected = iconDefinition(selectedId) ?? TASK_ICON_CATALOG[DEFAULT_CUSTOM_ICON]
970
+ useEffect(() => {
971
+ if (disabled === true && open) setOpen(false)
972
+ }, [disabled, open])
973
+ // The host Menu owns the portal, outside-click, Escape and button walk; the grid is only content.
974
+ return h(Menu, {
975
+ open,
976
+ anchor: h('button', {
977
+ type: 'button',
978
+ className: 'capital-automation-glyphbtn',
979
+ disabled,
980
+ 'aria-label': `${zh.iconField}:${selected.label},点击更换`,
981
+ 'aria-haspopup': 'menu',
982
+ 'aria-expanded': open ? 'true' : 'false',
983
+ title: zh.iconPickHint,
984
+ onClick: () => setOpen((current) => !current),
985
+ },
986
+ h(Glyph, { task, size, large }),
987
+ h('span', { className: 'capital-automation-glyphbtnchevron', 'aria-hidden': 'true' },
988
+ h(IconChevronDownOutlineRegular, { size: 12 }))),
989
+ items: [],
990
+ onClose: () => setOpen(false),
991
+ align: 'start',
992
+ side: 'bottom',
993
+ portal: true,
994
+ listClassName: 'capital-automation-iconmenu',
995
+ }, h('div', { className: 'capital-automation-iconmenu-grid' },
996
+ TASK_ICON_IDS.map((id) => {
997
+ const icon = TASK_ICON_CATALOG[id]
998
+ const active = id === selectedId
999
+ return h('button', {
1000
+ key: id,
1001
+ type: 'button',
1002
+ role: 'menuitemradio',
1003
+ 'aria-checked': active ? 'true' : 'false',
1004
+ 'aria-label': icon.label,
1005
+ title: icon.label,
1006
+ className: 'capital-automation-iconmenu-item',
1007
+ 'data-selected': active ? 'true' : undefined,
1008
+ onClick: () => { onChange(id); setOpen(false) },
1009
+ }, h('svg', {
1010
+ viewBox: '0 0 24 24',
1011
+ width: 18,
1012
+ height: 18,
1013
+ fill: 'none',
1014
+ stroke: 'currentColor',
1015
+ strokeWidth: 2,
1016
+ strokeLinecap: 'round',
1017
+ strokeLinejoin: 'round',
1018
+ 'aria-hidden': 'true',
1019
+ focusable: 'false',
1020
+ }, h('path', { d: icon.d })))
1021
+ })))
862
1022
  }
863
1023
 
864
1024
  function StateBadge(props) {
@@ -1119,16 +1279,28 @@ function triggerChanged(cadence, trigger) {
1119
1279
  return next
1120
1280
  }
1121
1281
 
1122
- function defaultDraft() {
1282
+ /** New-task limits start from the settings card; leaving them untouched keeps the task inherited. */
1283
+ function defaultLimits(meta) {
1284
+ const settings = meta?.execution?.settings
1285
+ return {
1286
+ runTimeoutMinutes: settings?.runTimeoutMinutes ?? DEFAULT_LIMITS.runTimeoutMinutes,
1287
+ idleTimeoutMinutes: settings?.idleTimeoutMinutes ?? DEFAULT_LIMITS.idleTimeoutMinutes,
1288
+ maxSteps: settings?.maxSteps ?? DEFAULT_LIMITS.maxSteps,
1289
+ maxToolCalls: settings?.maxToolCalls ?? DEFAULT_LIMITS.maxToolCalls,
1290
+ }
1291
+ }
1292
+
1293
+ function defaultDraft(meta) {
1123
1294
  return {
1124
1295
  title: '我的投研任务',
1125
1296
  description: '',
1126
1297
  instruction: '',
1127
1298
  category: 'research',
1299
+ icon: DEFAULT_CUSTOM_ICON,
1128
1300
  trigger: { kind: 'schedule', cadence: 'daily', time: '09:00', timezone: 'Asia/Shanghai', tradingDaysOnly: true },
1129
1301
  workspaceId: '',
1130
- limits: { ...DEFAULT_LIMITS },
1131
- notifications: { inApp: true, onFailure: true, onSuccess: false },
1302
+ limits: defaultLimits(meta),
1303
+ notifications: { inApp: meta?.execution?.settings?.inAppNotifications !== false, onFailure: true, onSuccess: false },
1132
1304
  }
1133
1305
  }
1134
1306
 
@@ -1138,6 +1310,7 @@ function draftPayload(task) {
1138
1310
  description: task.description ?? '',
1139
1311
  instruction: task.instruction ?? '',
1140
1312
  category: task.category,
1313
+ ...(task.origin === 'builtin' ? {} : { icon: task.icon ?? DEFAULT_CUSTOM_ICON }),
1141
1314
  trigger: normalizeTrigger(task.trigger),
1142
1315
  workspaceId: task.workspaceId ?? '',
1143
1316
  limits: task.limits,
@@ -1145,8 +1318,21 @@ function draftPayload(task) {
1145
1318
  }
1146
1319
  }
1147
1320
 
1321
+ function payloadKey(task) {
1322
+ return JSON.stringify(draftPayload(task))
1323
+ }
1324
+
1148
1325
  function samePayload(left, right) {
1149
- return JSON.stringify(draftPayload(left)) === JSON.stringify(draftPayload(right))
1326
+ return payloadKey(left) === payloadKey(right)
1327
+ }
1328
+
1329
+ function cloneTask(task) {
1330
+ return {
1331
+ ...task,
1332
+ trigger: normalizeTrigger(task.trigger),
1333
+ limits: { ...task.limits },
1334
+ notifications: { ...task.notifications },
1335
+ }
1150
1336
  }
1151
1337
 
1152
1338
  function groupRunCounts(runs) {
@@ -1204,11 +1390,7 @@ function notificationMessage(item) {
1204
1390
  return ''
1205
1391
  }
1206
1392
 
1207
- function draftChanged(draft, task) {
1208
- return !samePayload(draft, task)
1209
- }
1210
-
1211
- function draftIssues(task) {
1393
+ function draftIssues(task, meta) {
1212
1394
  const issues = {}
1213
1395
  if (String(task.title ?? '').trim().length === 0) issues.title = zh.titleRequired
1214
1396
  if (String(task.instruction ?? '').trim().length === 0) issues.instruction = zh.instructionRequired
@@ -1230,7 +1412,15 @@ function draftIssues(task) {
1230
1412
  }
1231
1413
  const limits = task.limits ?? {}
1232
1414
  for (const key of ['runTimeoutMinutes', 'idleTimeoutMinutes', 'maxSteps', 'maxToolCalls']) {
1233
- if (!Number.isFinite(limits[key])) issues[key] = zh.invalidLimit
1415
+ const value = limits[key]
1416
+ if (!Number.isFinite(value) || !Number.isInteger(value)) {
1417
+ issues[key] = zh.invalidLimit
1418
+ continue
1419
+ }
1420
+ const range = meta?.execution?.limits?.[key]
1421
+ if (Array.isArray(range) && range.length === 2 && (value < range[0] || value > range[1])) {
1422
+ issues[key] = zh.invalidLimitRange.replace('{min}', String(range[0])).replace('{max}', String(range[1]))
1423
+ }
1234
1424
  }
1235
1425
  return issues
1236
1426
  }
@@ -1239,9 +1429,9 @@ function draftIssues(task) {
1239
1429
  * Tracks which fields the user has left so errors appear on blur, then update live.
1240
1430
  * `resetKey` clears that state when the edited task or modal session changes.
1241
1431
  */
1242
- function useDraftIssues(task, resetKey) {
1432
+ function useDraftIssues(task, resetKey, meta) {
1243
1433
  const [touched, setTouched] = useState({})
1244
- const issues = draftIssues(task)
1434
+ const issues = draftIssues(task, meta)
1245
1435
  useEffect(() => { setTouched({}) }, [resetKey])
1246
1436
  const errorFor = (field) => (touched[field] === true ? issues[field] : undefined)
1247
1437
  const touch = (field) => setTouched((current) => (current[field] === true ? current : { ...current, [field]: true }))
@@ -1250,8 +1440,7 @@ function useDraftIssues(task, resetKey) {
1250
1440
  for (const field of Object.keys(issues)) next[field] = true
1251
1441
  return next
1252
1442
  })
1253
- const clear = () => setTouched({})
1254
- return { issues, errorFor, touch, touchAll, clear }
1443
+ return { issues, errorFor, touch, touchAll }
1255
1444
  }
1256
1445
 
1257
1446
  function limitValue(meta, key, fallback) {
@@ -1378,14 +1567,60 @@ function NotificationsButton(props) {
1378
1567
  className: 'capital-automation-btn',
1379
1568
  'data-variant': 'ghost',
1380
1569
  onClick,
1381
- title: zh.notificationsTitle,
1382
- 'aria-label': zh.notificationsTitle,
1570
+ title: zh.notificationsEntry,
1571
+ 'aria-label': zh.notificationsEntry,
1383
1572
  },
1384
1573
  h(IconAlarmClockOutlineRegular, { size: 14 }),
1385
- h('span', { className: 'capital-automation-btnlabel' }, zh.notifications),
1574
+ h('span', { className: 'capital-automation-btnlabel' }, zh.notificationsEntry),
1386
1575
  unread > 0 ? h('span', { className: 'capital-automation-count' }, String(unread > 99 ? '99+' : unread)) : null)
1387
1576
  }
1388
1577
 
1578
+ function systemStatusReason(code) {
1579
+ if (code === 'lease_held') return zh.systemReasonLeaseHeld
1580
+ if (code === 'owner_unverified') return zh.systemReasonUnverified
1581
+ return zh.systemReasonLeaseLost
1582
+ }
1583
+
1584
+ /** Profile-level scheduling state lives here, not in the task inbox. */
1585
+ function SystemStatus(props) {
1586
+ const { meta } = props
1587
+ const [open, setOpen] = useState(false)
1588
+ if (meta === null || meta === undefined) return null
1589
+ const scheduler = meta.scheduler ?? {}
1590
+ const state = scheduler.state
1591
+ const summary = state === 'active'
1592
+ ? zh.systemActive
1593
+ : state === 'standby'
1594
+ ? zh.systemStandby
1595
+ : state === 'starting'
1596
+ ? zh.systemStarting
1597
+ : state === 'stopped'
1598
+ ? zh.systemStopped
1599
+ : zh.systemUnavailable
1600
+ const calendarSummary = meta.calendar?.available === false ? zh.systemCalendarUnavailable : zh.systemCalendarOk
1601
+ const lastStandbyAt = scheduler.lastStandbyAt
1602
+ return h('section', { className: 'capital-automation-systemstatus', 'aria-label': zh.systemStatus },
1603
+ h('div', { className: 'capital-automation-systemline' },
1604
+ h('span', null, `${zh.systemStatus}:${summary} · ${calendarSummary}`),
1605
+ h('button', {
1606
+ type: 'button',
1607
+ className: 'capital-automation-systemtoggle',
1608
+ 'aria-expanded': open ? 'true' : 'false',
1609
+ onClick: () => setOpen((value) => !value),
1610
+ }, open ? zh.systemHideDetails : zh.systemDetails)),
1611
+ open
1612
+ ? h('div', { className: 'capital-automation-systemdetail' },
1613
+ lastStandbyAt
1614
+ ? h('p', null, `${zh.systemLastStandby}:${formatShortTime(lastStandbyAt)} · ${systemStatusReason(scheduler.lastStandbyCode)}`)
1615
+ : h('p', null, zh.systemNoStandby),
1616
+ lastStandbyAt && scheduler.lastStandbyReason
1617
+ ? h('details', null,
1618
+ h('summary', null, zh.systemTechnical),
1619
+ h('p', null, scheduler.lastStandbyReason))
1620
+ : null)
1621
+ : null)
1622
+ }
1623
+
1389
1624
  function Header(props) {
1390
1625
  const { tab, onTab, onNewTask, unread, onOpenInbox } = props
1391
1626
  return h('header', { className: 'capital-automation-head' },
@@ -1786,8 +2021,10 @@ function TaskDashboard(props) {
1786
2021
  market: roster.filter((task) => task.category === 'market').length,
1787
2022
  review: roster.filter((task) => task.category === 'review').length,
1788
2023
  research: roster.filter((task) => task.category === 'research').length,
2024
+ portfolio: roster.filter((task) => task.category === 'portfolio').length,
2025
+ monitor: roster.filter((task) => task.category === 'monitor').length,
1789
2026
  }
1790
- const filters = ['all', 'custom', 'news', 'market', 'review', 'research']
2027
+ const filters = ['all', 'custom', 'news', 'market', 'review', 'research', 'portfolio', 'monitor']
1791
2028
  return h('div', null,
1792
2029
  h(RunHistoryCard, { runs, onViewAll: onGoRuns }),
1793
2030
  h('section', { className: 'capital-automation-section' },
@@ -1992,12 +2229,13 @@ function MonthlySelect(props) {
1992
2229
  }
1993
2230
 
1994
2231
  function TriggerFields(props) {
1995
- const { trigger, onChange, disabled, meta, errorFor, touch } = props
2232
+ const { trigger, onChange, disabled, meta, errorFor, touch, onFlush } = props
1996
2233
  const update = (patch) => onChange(normalizeTrigger({ ...trigger, ...patch }))
1997
2234
  const setCadence = (cadence) => onChange(triggerChanged(cadence, trigger))
1998
2235
 
1999
2236
  const issue = (field) => (typeof errorFor === 'function' ? errorFor(field) : undefined)
2000
2237
  const touchField = (field) => { if (typeof touch === 'function') touch(field) }
2238
+ const commit = () => { if (typeof onFlush === 'function') onFlush() }
2001
2239
  const timeError = issue('time')
2002
2240
  const timezoneError = issue('timezone')
2003
2241
  const weekdayError = issue('weekdays')
@@ -2051,7 +2289,7 @@ function TriggerFields(props) {
2051
2289
  'aria-invalid': dateError ? 'true' : undefined,
2052
2290
  'data-invalid': dateError ? 'true' : undefined,
2053
2291
  onChange: (event) => update({ date: event.target.value }),
2054
- onBlur: () => touchField('date'),
2292
+ onBlur: () => { touchField('date'); commit() },
2055
2293
  }))
2056
2294
  : null)),
2057
2295
  h('div', { className: 'capital-automation-triggerfield' },
@@ -2065,7 +2303,7 @@ function TriggerFields(props) {
2065
2303
  'aria-invalid': timeError ? 'true' : undefined,
2066
2304
  'data-invalid': timeError ? 'true' : undefined,
2067
2305
  onChange: (event) => update({ time: event.target.value }),
2068
- onBlur: () => touchField('time'),
2306
+ onBlur: () => { touchField('time'); commit() },
2069
2307
  }))),
2070
2308
  h('div', { className: 'capital-automation-triggersettings' },
2071
2309
  h('span', { className: 'capital-automation-triggerzone' },
@@ -2393,41 +2631,31 @@ function OverviewTab(props) {
2393
2631
  }
2394
2632
 
2395
2633
  function ConfigTab(props) {
2396
- const { draft, setDraft, meta, busy, dirty, save, reset, disabled } = props
2634
+ const { draft, setDraft, meta, busy, dirty, saveState, saveError, retry, onFlush, disabled } = props
2397
2635
  const task = draft
2398
- const { issues, errorFor, touch, touchAll, clear } = useDraftIssues(task, task.id)
2399
- const formRef = useRef(null)
2636
+ const { errorFor, touch } = useDraftIssues(task, task.id, meta)
2400
2637
  const titleError = errorFor('title')
2401
2638
  const instructionError = errorFor('instruction')
2402
2639
  const set = (patch) => setDraft((current) => ({
2403
2640
  ...current,
2404
2641
  ...(typeof patch === 'function' ? patch(current) : patch),
2405
2642
  }))
2406
- const submit = () => {
2407
- if (Object.keys(issues).length > 0) {
2408
- touchAll()
2409
- setTimeout(() => {
2410
- const target = formRef.current?.querySelector('[data-invalid="true"] input, [data-invalid="true"] .capital-automation-texteditor')
2411
- target?.focus?.()
2412
- }, 0)
2413
- return
2414
- }
2415
- void save()
2416
- }
2643
+ const flush = () => { if (typeof onFlush === 'function') onFlush() }
2417
2644
  const timeoutRange = limitValue(meta, 'runTimeoutMinutes', [5, 120])
2418
2645
  const idleRange = limitValue(meta, 'idleTimeoutMinutes', [1, 30])
2419
- const stepsRange = limitValue(meta, 'maxSteps', [10, 2000])
2420
- const toolsRange = limitValue(meta, 'maxToolCalls', [10, 6000])
2646
+ const stepsRange = limitValue(meta, 'maxSteps', [100, 2000])
2647
+ const toolsRange = limitValue(meta, 'maxToolCalls', [100, 5000])
2421
2648
  const instructionEditor = h(Field, { label: zh.instructionField, error: instructionError },
2422
2649
  h(PlainTextEditor, {
2423
2650
  className: 'capital-automation-texteditor',
2424
2651
  value: task.instruction,
2652
+ maxLength: INSTRUCTION_MAX_LENGTH,
2425
2653
  disabled: busy || disabled,
2426
2654
  placeholder: zh.instructionPlaceholder,
2427
2655
  ariaLabel: zh.instructionField,
2428
2656
  invalid: instructionError ? true : undefined,
2429
2657
  onChange: (instruction) => set({ instruction }),
2430
- onBlur: () => touch('instruction'),
2658
+ onBlur: () => { touch('instruction'); flush() },
2431
2659
  }))
2432
2660
  const instructionSide = h('div', { className: 'capital-automation-instructionside' },
2433
2661
  h(SelectField, {
@@ -2440,24 +2668,43 @@ function ConfigTab(props) {
2440
2668
  meta?.workspaceError ? h('p', { className: 'capital-automation-hint', 'data-tone': 'error' }, zh.workspaceUnavailable) : null,
2441
2669
  h('div', null,
2442
2670
  h('span', { className: 'capital-automation-formlabel' }, zh.triggerField),
2443
- h(TriggerFields, { trigger: task.trigger, disabled: busy || disabled, onChange: (trigger) => set({ trigger }), meta, errorFor, touch })))
2671
+ h(TriggerFields, { trigger: task.trigger, disabled: busy || disabled, onChange: (trigger) => set({ trigger }), meta, errorFor, touch, onFlush: flush })))
2444
2672
  const advanced = h('details', { className: 'capital-automation-advanced' },
2445
2673
  h('summary', null, zh.advanced),
2446
2674
  h('div', { className: 'capital-automation-advancedbody' },
2447
2675
  h('div', { className: 'capital-automation-formgrid' },
2448
- 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 } })) }),
2449
- 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 } })) }),
2450
- 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 } })) }),
2451
- 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 } })) })),
2676
+ h(NumberField, { label: zh.runTimeout, value: task.limits.runTimeoutMinutes, min: timeoutRange[0], max: timeoutRange[1], disabled: busy || disabled, error: errorFor('runTimeoutMinutes'), onBlur: () => { touch('runTimeoutMinutes'); flush() }, onChange: (value) => set((current) => ({ limits: { ...current.limits, runTimeoutMinutes: value } })) }),
2677
+ h(NumberField, { label: zh.idleTimeout, value: task.limits.idleTimeoutMinutes, min: idleRange[0], max: idleRange[1], disabled: busy || disabled, error: errorFor('idleTimeoutMinutes'), onBlur: () => { touch('idleTimeoutMinutes'); flush() }, onChange: (value) => set((current) => ({ limits: { ...current.limits, idleTimeoutMinutes: value } })) }),
2678
+ h(NumberField, { label: zh.maxSteps, value: task.limits.maxSteps, min: stepsRange[0], max: stepsRange[1], disabled: busy || disabled, error: errorFor('maxSteps'), onBlur: () => { touch('maxSteps'); flush() }, onChange: (value) => set((current) => ({ limits: { ...current.limits, maxSteps: value } })) }),
2679
+ h(NumberField, { label: zh.maxToolCalls, value: task.limits.maxToolCalls, min: toolsRange[0], max: toolsRange[1], disabled: busy || disabled, error: errorFor('maxToolCalls'), onBlur: () => { touch('maxToolCalls'); flush() }, onChange: (value) => set((current) => ({ limits: { ...current.limits, maxToolCalls: value } })) })),
2452
2680
  h('p', { className: 'capital-automation-hint' }, zh.budgetHint)))
2453
2681
  const notificationRows = h('div', null,
2454
2682
  h('span', { className: 'capital-automation-formlabel' }, zh.notifyField),
2455
2683
  h('div', { className: 'capital-automation-switchlist' },
2456
2684
  h(SwitchRow, { label: zh.notifyInApp, checked: task.notifications.inApp, disabled: busy || disabled, onChange: (value) => set((current) => ({ notifications: { ...current.notifications, inApp: value } })) }),
2457
- h(SwitchRow, { label: zh.notifyFailure, checked: task.notifications.onFailure, disabled: busy || disabled, onChange: (value) => set((current) => ({ notifications: { ...current.notifications, onFailure: value } })) }),
2458
- h(SwitchRow, { label: zh.notifySuccess, checked: task.notifications.onSuccess, disabled: busy || disabled, onChange: (value) => set((current) => ({ notifications: { ...current.notifications, onSuccess: value } })) })),
2685
+ h(SwitchRow, { label: zh.notifyFailure, checked: task.notifications.onFailure, disabled: busy || disabled || task.notifications.inApp !== true, onChange: (value) => set((current) => ({ notifications: { ...current.notifications, onFailure: value } })) }),
2686
+ h(SwitchRow, { label: zh.notifySuccess, checked: task.notifications.onSuccess, disabled: busy || disabled || task.notifications.inApp !== true, onChange: (value) => set((current) => ({ notifications: { ...current.notifications, onSuccess: value } })) })),
2459
2687
  h('p', { className: 'capital-automation-hint' }, zh.notifyHint))
2460
- return h('div', { className: 'capital-automation-form', ref: formRef },
2688
+ const state = disabled
2689
+ ? 'disabled'
2690
+ : dirty && (saveState === 'idle' || saveState === 'saved')
2691
+ ? 'pending'
2692
+ : !dirty && saveState === 'pending'
2693
+ ? 'idle'
2694
+ : saveState
2695
+ const statusText = state === 'saving'
2696
+ ? zh.saving
2697
+ : state === 'error'
2698
+ ? `${zh.autosaveFailed}:${saveError || zh.errorGeneric}`
2699
+ : state === 'invalid'
2700
+ ? zh.autosaveInvalid
2701
+ : state === 'pending'
2702
+ ? zh.autosavePending
2703
+ : state === 'saved'
2704
+ ? zh.autosaveSaved
2705
+ : zh.noDirty
2706
+ const statusTone = state === 'error' ? 'error' : state === 'saved' ? 'ok' : undefined
2707
+ return h('div', { className: 'capital-automation-form' },
2461
2708
  h('div', { className: 'capital-automation-titlerow' },
2462
2709
  h(Field, {
2463
2710
  label: zh.titleField,
@@ -2470,7 +2717,7 @@ function ConfigTab(props) {
2470
2717
  disabled: busy || disabled,
2471
2718
  'aria-invalid': titleError ? 'true' : undefined,
2472
2719
  onChange: (event) => set({ title: event.target.value }),
2473
- onBlur: () => touch('title'),
2720
+ onBlur: () => { touch('title'); flush() },
2474
2721
  })),
2475
2722
  h(SelectField, {
2476
2723
  label: zh.categoryField,
@@ -2480,6 +2727,18 @@ function ConfigTab(props) {
2480
2727
  disabled: busy || disabled || task.origin === 'builtin',
2481
2728
  onChange: (category) => set({ category }),
2482
2729
  })),
2730
+ task.origin === 'custom'
2731
+ ? h('div', { className: 'capital-automation-iconrow' },
2732
+ h(IconPickerTrigger, {
2733
+ task,
2734
+ size: 38,
2735
+ disabled: busy || disabled,
2736
+ onChange: (icon) => set({ icon }),
2737
+ }),
2738
+ h('div', { className: 'capital-automation-iconrowbody' },
2739
+ h('span', { className: 'capital-automation-formlabel' }, zh.iconField),
2740
+ h('span', { className: 'capital-automation-hint' }, `${taskIcon(task).label} · ${zh.iconPickHint}`)))
2741
+ : h('p', { className: 'capital-automation-hint' }, zh.iconBuiltinHint),
2483
2742
  h(Field, {
2484
2743
  label: zh.descriptionField,
2485
2744
  note: zh.charCount.replace('{n}', String(Array.from(task.description ?? '').length)).replace('{max}', String(DESCRIPTION_MAX_LENGTH)),
@@ -2492,6 +2751,7 @@ function ConfigTab(props) {
2492
2751
  placeholder: zh.descriptionPlaceholder,
2493
2752
  ariaLabel: zh.descriptionField,
2494
2753
  onChange: (description) => set({ description }),
2754
+ onBlur: flush,
2495
2755
  })),
2496
2756
  h('div', { className: 'capital-automation-instructionrow' },
2497
2757
  instructionEditor,
@@ -2499,10 +2759,18 @@ function ConfigTab(props) {
2499
2759
  advanced,
2500
2760
  notificationRows,
2501
2761
  h('div', { className: 'capital-automation-formactions', 'data-sticky': 'true' },
2502
- h('span', { className: 'capital-automation-formstatus' },
2503
- disabled ? zh.storageUnavailable : dirty ? zh.dirty : zh.noDirty),
2504
- h('button', { type: 'button', className: 'capital-automation-btn', disabled: busy || !dirty, onClick: () => { clear(); reset() } }, zh.cancel),
2505
- h('button', { type: 'button', className: 'capital-automation-btn', 'data-variant': 'primary', disabled: busy || !dirty || disabled, onClick: submit }, busy === 'save' ? zh.saving : zh.save)))
2762
+ h('span', { className: 'capital-automation-formstatus' }, disabled ? zh.storageUnavailable : zh.autosaveHint),
2763
+ state === 'disabled'
2764
+ ? null
2765
+ : h('span', {
2766
+ className: 'capital-automation-autosavestate',
2767
+ role: 'status',
2768
+ 'data-tone': statusTone,
2769
+ title: state === 'error' ? saveError : undefined,
2770
+ }, statusText),
2771
+ state === 'error'
2772
+ ? h('button', { type: 'button', className: 'capital-automation-btn', 'data-size': 'sm', disabled: busy || disabled, onClick: retry }, zh.autosaveRetry)
2773
+ : null))
2506
2774
  }
2507
2775
 
2508
2776
  function NotificationsTab(props) {
@@ -2553,19 +2821,72 @@ function DetailBar(props) {
2553
2821
  function TaskDetailPage(props) {
2554
2822
  const { detail, meta, api, onChanged, openSession, tab, onTab, onRequestConfirm, onRequestCancelRun, schedulerActive, storageReady } = props
2555
2823
  const [draft, setDraft] = useState(null)
2824
+ const [savedTask, setSavedTask] = useState(null)
2556
2825
  const [busy, setBusy] = useState('')
2557
2826
  const [notice, setNotice] = useState('')
2558
2827
  const [error, setError] = useState('')
2828
+ const [saveState, setSaveState] = useState('idle')
2829
+ const [saveError, setSaveError] = useState('')
2559
2830
  const [menuOpen, setMenuOpen] = useState(false)
2560
2831
  const lastTaskIdRef = useRef(null)
2832
+ const activeTaskIdRef = useRef(null)
2833
+ const draftRef = useRef(null)
2834
+ const savedTaskRef = useRef(null)
2835
+ const ackedPayloadRef = useRef(null)
2836
+ const failedPayloadRef = useRef(null)
2837
+ const saveTimerRef = useRef(null)
2838
+ const saveInFlightRef = useRef(false)
2839
+ const saveQueuedRef = useRef(false)
2840
+ const savePromiseRef = useRef(null)
2841
+ const flushRef = useRef(null)
2842
+ const storageReadyNow = storageReady !== false
2843
+ activeTaskIdRef.current = detail.task?.id ?? null
2844
+
2845
+ const updateDraft = (patch) => {
2846
+ const current = draftRef.current
2847
+ if (current === null) return
2848
+ const next = {
2849
+ ...current,
2850
+ ...(typeof patch === 'function' ? patch(current) : patch),
2851
+ }
2852
+ draftRef.current = next
2853
+ setDraft(next)
2854
+ }
2561
2855
 
2562
2856
  useEffect(() => {
2563
- setDraft(detail.task ? {
2564
- ...detail.task,
2565
- trigger: normalizeTrigger(detail.task.trigger),
2566
- limits: { ...detail.task.limits },
2567
- notifications: { ...detail.task.notifications },
2568
- } : null)
2857
+ const server = detail.task
2858
+ if (!server) {
2859
+ savedTaskRef.current = null
2860
+ ackedPayloadRef.current = null
2861
+ setSavedTask(null)
2862
+ setDraft(null)
2863
+ draftRef.current = null
2864
+ return
2865
+ }
2866
+ const nextSaved = cloneTask(server)
2867
+ const previousAcked = ackedPayloadRef.current
2868
+ savedTaskRef.current = nextSaved
2869
+ ackedPayloadRef.current = payloadKey(nextSaved)
2870
+ setSavedTask(nextSaved)
2871
+ const current = draftRef.current
2872
+ if (current === null || current.id !== nextSaved.id) {
2873
+ draftRef.current = nextSaved
2874
+ setDraft(nextSaved)
2875
+ return
2876
+ }
2877
+ if (samePayload(current, nextSaved)) return
2878
+ // Hibernate clears overrides on the server; discard a local draft that a failed save
2879
+ // could otherwise resurrect after the task returns to its template defaults.
2880
+ if (nextSaved.state === 'dormant' && current.state !== 'dormant') {
2881
+ draftRef.current = nextSaved
2882
+ setDraft(nextSaved)
2883
+ return
2884
+ }
2885
+ // A poll or another action must not clobber edits the user has not saved yet.
2886
+ if (previousAcked !== null && payloadKey(current) === previousAcked) {
2887
+ draftRef.current = nextSaved
2888
+ setDraft(nextSaved)
2889
+ }
2569
2890
  }, [detail])
2570
2891
 
2571
2892
  useEffect(() => {
@@ -2574,20 +2895,57 @@ function TaskDetailPage(props) {
2574
2895
  lastTaskIdRef.current = id
2575
2896
  setNotice('')
2576
2897
  setError('')
2898
+ setSaveState('idle')
2899
+ setSaveError('')
2900
+ failedPayloadRef.current = null
2577
2901
  }, [detail])
2578
2902
 
2903
+ useEffect(() => {
2904
+ if (draft === null || savedTask === null || storageReadyNow === false) return undefined
2905
+ if (samePayload(draft, savedTask)) {
2906
+ setSaveState((state) => (state === 'pending' || state === 'invalid' || state === 'error' ? 'idle' : state))
2907
+ return undefined
2908
+ }
2909
+ if (busy) return undefined
2910
+ if (Object.keys(draftIssues(draft, meta)).length > 0) {
2911
+ setSaveState('invalid')
2912
+ return undefined
2913
+ }
2914
+ if (failedPayloadRef.current !== null && payloadKey(draft) === failedPayloadRef.current) {
2915
+ setSaveState('error')
2916
+ return undefined
2917
+ }
2918
+ setSaveState((state) => (state === 'saving' ? state : 'pending'))
2919
+ saveTimerRef.current = setTimeout(() => { void flushRef.current?.() }, AUTOSAVE_DELAY_MS)
2920
+ return () => {
2921
+ if (saveTimerRef.current !== null) {
2922
+ clearTimeout(saveTimerRef.current)
2923
+ saveTimerRef.current = null
2924
+ }
2925
+ }
2926
+ }, [draft, savedTask, busy, storageReadyNow, meta])
2927
+
2928
+ useEffect(() => {
2929
+ if (tab === 'config') return
2930
+ void flushRef.current?.()
2931
+ }, [tab])
2932
+
2933
+ useEffect(() => () => { void flushRef.current?.() }, [])
2934
+
2935
+ useEffect(() => {
2936
+ if (typeof document === 'undefined') return undefined
2937
+ const onHide = () => { if (document.visibilityState === 'hidden') void flushRef.current?.() }
2938
+ document.addEventListener?.('visibilitychange', onHide)
2939
+ return () => document.removeEventListener?.('visibilitychange', onHide)
2940
+ }, [])
2941
+
2579
2942
  if (draft === null) return h('p', { className: 'capital-automation-empty' }, zh.loading)
2580
2943
  const task = draft
2581
- const storageReadyNow = storageReady !== false
2582
2944
  const running = detail.runs.some((run) => isActiveRun(run))
2583
2945
  const activeRun = detail.runs.find((run) => isActiveRun(run))
2584
2946
  const unread = detail.notifications.filter((item) => item.readAt === undefined).length
2585
- const dirty = draftChanged(draft, detail.task)
2947
+ const dirty = savedTask === null ? false : !samePayload(draft, savedTask)
2586
2948
  const canRun = storageReadyNow && schedulerActive === true && !running
2587
- const set = (patch) => setDraft((current) => ({
2588
- ...current,
2589
- ...(typeof patch === 'function' ? patch(current) : patch),
2590
- }))
2591
2949
 
2592
2950
  async function act(name, action, success) {
2593
2951
  if (busy) return
@@ -2604,10 +2962,90 @@ function TaskDetailPage(props) {
2604
2962
  }
2605
2963
  }
2606
2964
 
2607
- async function save() {
2608
- await act('save', async () => {
2609
- await api.patch(detail.task.id, draftPayload(task))
2610
- }, zh.saved)
2965
+ async function flush() {
2966
+ if (saveTimerRef.current !== null) {
2967
+ clearTimeout(saveTimerRef.current)
2968
+ saveTimerRef.current = null
2969
+ }
2970
+ if (saveInFlightRef.current) {
2971
+ saveQueuedRef.current = true
2972
+ return savePromiseRef.current ?? undefined
2973
+ }
2974
+ const current = draftRef.current
2975
+ const base = savedTaskRef.current
2976
+ if (current === null || base === null || storageReadyNow === false) return
2977
+ if (samePayload(current, base)) {
2978
+ setSaveState((state) => (state === 'saving' ? state : 'idle'))
2979
+ return
2980
+ }
2981
+ if (Object.keys(draftIssues(current, meta)).length > 0) {
2982
+ setSaveState('invalid')
2983
+ return
2984
+ }
2985
+ const payload = draftPayload(current)
2986
+ const taskId = current.id
2987
+ saveInFlightRef.current = true
2988
+ saveQueuedRef.current = false
2989
+ setSaveState('saving')
2990
+ setSaveError('')
2991
+ const savePromise = (async () => {
2992
+ try {
2993
+ const result = await api.patch(taskId, payload)
2994
+ if (activeTaskIdRef.current !== taskId) {
2995
+ void onChanged?.(taskId)
2996
+ return
2997
+ }
2998
+ const serverTask = result?.task
2999
+ const nextSaved = serverTask === undefined ? cloneTask({ ...base, ...payload }) : cloneTask(serverTask)
3000
+ savedTaskRef.current = nextSaved
3001
+ ackedPayloadRef.current = payloadKey(nextSaved)
3002
+ failedPayloadRef.current = null
3003
+ setSavedTask(nextSaved)
3004
+ setSaveState('saved')
3005
+ void onChanged?.(taskId)
3006
+ } catch (failure) {
3007
+ if (activeTaskIdRef.current === taskId) {
3008
+ failedPayloadRef.current = payloadKey(current)
3009
+ setSaveState('error')
3010
+ setSaveError(failure?.message ?? zh.errorGeneric)
3011
+ }
3012
+ } finally {
3013
+ saveInFlightRef.current = false
3014
+ const latest = draftRef.current
3015
+ const baseNow = savedTaskRef.current
3016
+ const queued = saveQueuedRef.current
3017
+ saveQueuedRef.current = false
3018
+ if (queued || (latest !== null && baseNow !== null && latest.id === baseNow.id && !samePayload(latest, baseNow) && failedPayloadRef.current === null)) {
3019
+ saveTimerRef.current = setTimeout(() => { void flushRef.current?.() }, 0)
3020
+ }
3021
+ }
3022
+ })()
3023
+ savePromiseRef.current = savePromise
3024
+ try {
3025
+ await savePromise
3026
+ } finally {
3027
+ if (savePromiseRef.current === savePromise) savePromiseRef.current = null
3028
+ }
3029
+ }
3030
+ flushRef.current = flush
3031
+
3032
+ function retrySave() {
3033
+ failedPayloadRef.current = null
3034
+ setSaveError('')
3035
+ void flushRef.current?.()
3036
+ }
3037
+
3038
+ async function flushBeforeAction() {
3039
+ for (let attempt = 0; attempt < 3; attempt += 1) {
3040
+ await flushRef.current?.()
3041
+ const current = draftRef.current
3042
+ const base = savedTaskRef.current
3043
+ if (current === null || base === null) return true
3044
+ if (samePayload(current, base)) return true
3045
+ if (Object.keys(draftIssues(current, meta)).length > 0) return false
3046
+ if (failedPayloadRef.current !== null) return false
3047
+ }
3048
+ return false
2611
3049
  }
2612
3050
 
2613
3051
  async function toggleState() {
@@ -2618,6 +3056,11 @@ function TaskDetailPage(props) {
2618
3056
  }
2619
3057
 
2620
3058
  async function runNow() {
3059
+ if (!(await flushBeforeAction())) {
3060
+ const current = draftRef.current
3061
+ setError(current !== null && Object.keys(draftIssues(current, meta)).length > 0 ? zh.autosaveInvalid : zh.autosaveFailed)
3062
+ return
3063
+ }
2621
3064
  await act('run', async () => {
2622
3065
  await api.run(detail.task.id)
2623
3066
  }, zh.runSubmitted.replace('{title}', detail.task.title))
@@ -2632,6 +3075,11 @@ function TaskDetailPage(props) {
2632
3075
  }
2633
3076
 
2634
3077
  async function copyTask() {
3078
+ if (!(await flushBeforeAction())) {
3079
+ const current = draftRef.current
3080
+ setError(current !== null && Object.keys(draftIssues(current, meta)).length > 0 ? zh.autosaveInvalid : zh.autosaveFailed)
3081
+ return
3082
+ }
2635
3083
  await act('copy', async () => {
2636
3084
  await api.action(detail.task.id, 'copy')
2637
3085
  }, zh.copied.replace('{title}', detail.task.title))
@@ -2654,17 +3102,6 @@ function TaskDetailPage(props) {
2654
3102
  onClick: () => { void runNow() },
2655
3103
  }, h(IconPlayOutlineRegular, { size: 14 }), zh.runNow)
2656
3104
 
2657
- const resetDraft = () => {
2658
- setDraft({
2659
- ...detail.task,
2660
- trigger: normalizeTrigger(detail.task.trigger),
2661
- limits: { ...detail.task.limits },
2662
- notifications: { ...detail.task.notifications },
2663
- })
2664
- setError('')
2665
- setNotice('')
2666
- }
2667
-
2668
3105
  return h('div', null,
2669
3106
  h('div', { className: 'capital-automation-hero' },
2670
3107
  h(Glyph, { task, large: true }),
@@ -2696,7 +3133,18 @@ function TaskDetailPage(props) {
2696
3133
  else if (id === 'copy') void copyTask()
2697
3134
  else if (id === 'activate' || id === 'pause') void toggleState()
2698
3135
  },
2699
- onRequestConfirm: (kind, target) => { setMenuOpen(false); onRequestConfirm(kind, target) },
3136
+ onRequestConfirm: (kind, target) => {
3137
+ setMenuOpen(false)
3138
+ // A pending autosave must not land after hibernate clears the overrides.
3139
+ if (kind === 'hibernate' || kind === 'delete') {
3140
+ void (async () => {
3141
+ await flushBeforeAction()
3142
+ onRequestConfirm(kind, target)
3143
+ })()
3144
+ return
3145
+ }
3146
+ onRequestConfirm(kind, target)
3147
+ },
2700
3148
  canRun,
2701
3149
  running,
2702
3150
  busy: busy.length > 0,
@@ -2728,12 +3176,14 @@ function TaskDetailPage(props) {
2728
3176
  }) : null,
2729
3177
  tab === 'config' ? h(ConfigTab, {
2730
3178
  draft,
2731
- setDraft,
3179
+ setDraft: updateDraft,
2732
3180
  meta,
2733
3181
  busy,
2734
3182
  dirty,
2735
- save,
2736
- reset: resetDraft,
3183
+ saveState,
3184
+ saveError,
3185
+ retry: retrySave,
3186
+ onFlush: () => { void flushRef.current?.() },
2737
3187
  disabled: !storageReadyNow,
2738
3188
  }) : null,
2739
3189
  tab === 'runs' ? h('section', { className: 'capital-automation-card capital-automation-panel' },
@@ -2756,15 +3206,15 @@ function TaskDetailPage(props) {
2756
3206
 
2757
3207
  function NewTaskModal(props) {
2758
3208
  const { open, onClose, api, onCreated, meta } = props
2759
- const [draft, setDraft] = useState(defaultDraft)
3209
+ const [draft, setDraft] = useState(() => defaultDraft(meta))
2760
3210
  const [busy, setBusy] = useState(false)
2761
3211
  const [error, setError] = useState('')
2762
- const { issues, errorFor, touch, touchAll } = useDraftIssues(draft, open)
3212
+ const { issues, errorFor, touch, touchAll } = useDraftIssues(draft, open, meta)
2763
3213
  const titleError = errorFor('title')
2764
3214
  const instructionError = errorFor('instruction')
2765
3215
  useEffect(() => {
2766
3216
  if (!open) return
2767
- setDraft(defaultDraft())
3217
+ setDraft(defaultDraft(meta))
2768
3218
  setBusy(false)
2769
3219
  setError('')
2770
3220
  }, [open])
@@ -2774,8 +3224,8 @@ function NewTaskModal(props) {
2774
3224
  }))
2775
3225
  const timeoutRange = limitValue(meta, 'runTimeoutMinutes', [5, 120])
2776
3226
  const idleRange = limitValue(meta, 'idleTimeoutMinutes', [1, 30])
2777
- const stepsRange = limitValue(meta, 'maxSteps', [10, 2000])
2778
- const toolsRange = limitValue(meta, 'maxToolCalls', [10, 6000])
3227
+ const stepsRange = limitValue(meta, 'maxSteps', [100, 2000])
3228
+ const toolsRange = limitValue(meta, 'maxToolCalls', [100, 5000])
2779
3229
  async function create() {
2780
3230
  if (Object.keys(issues).length > 0) {
2781
3231
  touchAll()
@@ -2806,7 +3256,7 @@ function NewTaskModal(props) {
2806
3256
  },
2807
3257
  h('div', { className: 'capital-automation-wizard' },
2808
3258
  h('div', { className: 'capital-automation-modalheadline', 'data-invalid': titleError ? 'true' : undefined },
2809
- h(Glyph, { task: draft, size: 42 }),
3259
+ h(IconPickerTrigger, { task: draft, size: 42, onChange: (icon) => set({ icon }) }),
2810
3260
  h('input', {
2811
3261
  className: 'capital-automation-modaltitleinput',
2812
3262
  value: draft.title,
@@ -2818,7 +3268,6 @@ function NewTaskModal(props) {
2818
3268
  onBlur: () => touch('title'),
2819
3269
  })),
2820
3270
  titleError ? h('span', { className: 'capital-automation-fielderror', role: 'alert' }, titleError) : null,
2821
- h('p', { className: 'capital-automation-hint' }, zh.newTaskHint),
2822
3271
  h(Field, {
2823
3272
  label: zh.descriptionField,
2824
3273
  note: zh.charCount.replace('{n}', String(Array.from(draft.description ?? '').length)).replace('{max}', String(DESCRIPTION_MAX_LENGTH)),
@@ -2839,6 +3288,7 @@ function NewTaskModal(props) {
2839
3288
  h(PlainTextEditor, {
2840
3289
  className: 'capital-automation-texteditor',
2841
3290
  value: draft.instruction,
3291
+ maxLength: INSTRUCTION_MAX_LENGTH,
2842
3292
  placeholder: zh.instructionPlaceholder,
2843
3293
  ariaLabel: zh.instructionField,
2844
3294
  invalid: instructionError ? true : undefined,
@@ -2885,8 +3335,8 @@ function NewTaskModal(props) {
2885
3335
  h('span', { className: 'capital-automation-formlabel' }, zh.notifyField),
2886
3336
  h('div', { className: 'capital-automation-switchlist' },
2887
3337
  h(SwitchRow, { label: zh.notifyInApp, checked: draft.notifications.inApp, onChange: (value) => set((current) => ({ notifications: { ...current.notifications, inApp: value } })) }),
2888
- h(SwitchRow, { label: zh.notifyFailure, checked: draft.notifications.onFailure, onChange: (value) => set((current) => ({ notifications: { ...current.notifications, onFailure: value } })) }),
2889
- h(SwitchRow, { label: zh.notifySuccess, checked: draft.notifications.onSuccess, onChange: (value) => set((current) => ({ notifications: { ...current.notifications, onSuccess: value } })) })),
3338
+ h(SwitchRow, { label: zh.notifyFailure, checked: draft.notifications.onFailure, disabled: draft.notifications.inApp !== true, onChange: (value) => set((current) => ({ notifications: { ...current.notifications, onFailure: value } })) }),
3339
+ h(SwitchRow, { label: zh.notifySuccess, checked: draft.notifications.onSuccess, disabled: draft.notifications.inApp !== true, onChange: (value) => set((current) => ({ notifications: { ...current.notifications, onSuccess: value } })) })),
2890
3340
  h('p', { className: 'capital-automation-hint' }, zh.notifyHint)),
2891
3341
  error.length > 0 ? h('p', { className: 'capital-automation-error', role: 'alert' }, error) : null))
2892
3342
  }
@@ -3201,7 +3651,9 @@ function AutomationPage(props) {
3201
3651
 
3202
3652
  const schedulerActive = meta?.scheduler?.state === 'active'
3203
3653
  const storageReady = meta?.storage?.state !== 'unavailable'
3204
- const templates = tasks.filter((task) => task.origin === 'builtin' && task.state === 'dormant')
3654
+ const templates = tasks
3655
+ .filter((task) => task.origin === 'builtin' && task.state === 'dormant')
3656
+ .sort(compareTemplates)
3205
3657
  const selectedTask = tasks.find((task) => task.id === selectedId)
3206
3658
  const detailOpen = tab === 'tasks' && selectedId !== null
3207
3659
  // `/meta` arrives in the same batch as `/tasks`; until then the empty state would be a lie.
@@ -3270,7 +3722,7 @@ function AutomationPage(props) {
3270
3722
  onGoTasks: () => switchTab('tasks'),
3271
3723
  onNewTask: () => setWizard(true),
3272
3724
  busy: confirmBusy,
3273
- }))),
3725
+ }), detailOpen ? null : h(SystemStatus, { meta }))),
3274
3726
  h(NewTaskModal, {
3275
3727
  open: wizard,
3276
3728
  onClose: () => setWizard(false),