dsh-taskboard 0.5.5 → 0.6.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 (42) hide show
  1. package/README.md +26 -2
  2. package/lib/client.js +2663 -767
  3. package/lib/host/execution.js +3 -0
  4. package/lib/host/execution.js.map +1 -1
  5. package/lib/host/routes.js +31 -1
  6. package/lib/host/routes.js.map +1 -1
  7. package/lib/host/session-sync.js +210 -10
  8. package/lib/host/session-sync.js.map +1 -1
  9. package/lib/host/store.js +9 -2
  10. package/lib/host/store.js.map +1 -1
  11. package/lib/index.js +100 -1
  12. package/lib/index.js.map +1 -1
  13. package/lib/shared/api.js.map +1 -1
  14. package/lib/shared/protocol.js +19 -1
  15. package/lib/shared/protocol.js.map +1 -1
  16. package/package.json +75 -75
  17. package/src/client/api.ts +8 -0
  18. package/src/client/board/AlertModal.tsx +3 -1
  19. package/src/client/board/ImportModal.tsx +26 -24
  20. package/src/client/board/SettingsModal.tsx +66 -26
  21. package/src/client/board/SlashPromptInput.tsx +340 -0
  22. package/src/client/board/TaskBoard.tsx +53 -49
  23. package/src/client/board/TaskCard.tsx +33 -21
  24. package/src/client/board/TaskDetail.tsx +169 -104
  25. package/src/client/board/TaskFormModal.tsx +254 -202
  26. package/src/client/board/TemplateManager.tsx +32 -29
  27. package/src/client/board/labels.ts +36 -27
  28. package/src/client/controller.ts +62 -2
  29. package/src/client/i18n/en.ts +455 -0
  30. package/src/client/i18n/runtime.ts +155 -0
  31. package/src/client/i18n/zh.ts +460 -0
  32. package/src/client/index.ts +182 -42
  33. package/src/client/sidebar-entry.ts +13 -3
  34. package/src/client/styles.ts +133 -0
  35. package/src/host/execution.ts +13 -0
  36. package/src/host/routes.ts +49 -1
  37. package/src/host/session-sync.ts +334 -14
  38. package/src/host/store.ts +15 -1
  39. package/src/index.ts +115 -1
  40. package/src/shared/api.ts +47 -0
  41. package/src/shared/protocol.ts +41 -0
  42. package/src/shared/version.ts +1 -1
@@ -11,9 +11,11 @@
11
11
  import { useEffect, useRef, useState, type ReactNode } from 'react'
12
12
  import type { BoardController } from '../controller.ts'
13
13
  import type { TaskTemplateSpec } from '../../shared/api.ts'
14
- import type { ChecklistItem, IsolationMode, Urgency } from '../../shared/protocol.ts'
15
- import { MAX_CHECKLIST_ITEMS, defaultIsolationOf, nextCronTime, parseCron } from '../../shared/protocol.ts'
14
+ import type { ChecklistItem, IsolationMode, PermissionMode, TaskRecord, Urgency } from '../../shared/protocol.ts'
15
+ import { MAX_CHECKLIST_ITEMS, asPermission, defaultIsolationOf, defaultPermissionOf, nextCronTime, parseCron } from '../../shared/protocol.ts'
16
16
  import { fmtTime } from './format.ts'
17
+ import { useT, type Translate } from '../i18n/runtime.ts'
18
+ import { SlashPromptInput } from './SlashPromptInput.tsx'
17
19
 
18
20
  /** One row of the configured model catalog (from llm.models). */
19
21
  export interface CatalogModel {
@@ -62,19 +64,26 @@ export function saveLastModel(model?: { provider: string; model: string; reasoni
62
64
  } catch { /* storage unavailable */ }
63
65
  }
64
66
 
65
- /** Urgency segmented options with a one-line hint each. */
66
- const URGENCY_OPTIONS: ReadonlyArray<{ value: Urgency; label: string; hint: string }> = [
67
- { value: 'urgent', label: '紧急', hint: '优先处理' },
68
- { value: 'normal', label: '一般', hint: '正常排期' },
69
- { value: 'relaxed', label: '不急', hint: '有空再做' },
67
+ /** Urgency segmented options with a one-line hint each (translated per render). */
68
+ const urgencyOptions = (t: Translate): ReadonlyArray<{ value: Urgency; label: string; hint: string }> => [
69
+ { value: 'urgent', label: t('form.urgency.urgent'), hint: t('form.urgency.urgentHint') },
70
+ { value: 'normal', label: t('form.urgency.normal'), hint: t('form.urgency.normalHint') },
71
+ { value: 'relaxed', label: t('form.urgency.relaxed'), hint: t('form.urgency.relaxedHint') },
70
72
  ]
71
73
 
72
- /** Cron presets offered in the scheduled mode. */
73
- const CRON_PRESETS: ReadonlyArray<{ label: string; cron: string }> = [
74
- { label: '每天 09:00', cron: '0 9 * * *' },
75
- { label: '每小时', cron: '0 * * * *' },
76
- { label: '每 10 分钟', cron: '*/10 * * * *' },
77
- { label: '每周一 09:00', cron: '0 9 * * 1' },
74
+ /** Cron presets offered in the scheduled mode (translated per render). */
75
+ const cronPresets = (t: Translate): ReadonlyArray<{ label: string; cron: string }> => [
76
+ { label: t('form.cron.daily'), cron: '0 9 * * *' },
77
+ { label: t('form.cron.hourly'), cron: '0 * * * *' },
78
+ { label: t('form.cron.every10min'), cron: '*/10 * * * *' },
79
+ { label: t('form.cron.weekly'), cron: '0 9 * * 1' },
80
+ ]
81
+
82
+ /** Permission presets aligned with DSH (translated per render). */
83
+ const permissionOptions = (t: Translate): ReadonlyArray<{ value: PermissionMode; label: string; hint: string; icon: string }> => [
84
+ { value: 'workspace-write', label: t('form.perm.write'), hint: t('form.perm.writeHint'), icon: '📁' },
85
+ { value: 'read-only', label: t('form.perm.readOnly'), hint: t('form.perm.readOnlyHint'), icon: '🔒' },
86
+ { value: 'danger-full-access', label: t('form.perm.fullAccess'), hint: t('form.perm.fullAccessHint'), icon: '⚡' },
78
87
  ]
79
88
 
80
89
  /** Field shell: label + control, optionally spanning the full grid row. */
@@ -111,6 +120,7 @@ interface CheckRow {
111
120
  * replaces the whole list on save).
112
121
  */
113
122
  function ChecklistEditor({ rows, onChange, editing }: { rows: CheckRow[]; onChange: (rows: CheckRow[]) => void; editing: boolean }) {
123
+ const t = useT()
114
124
  const setRow = (index: number, patch: Partial<CheckRow>): void => {
115
125
  const next = rows.map((row, i) => i === index ? { ...row, ...patch } : row)
116
126
  onChange(next)
@@ -125,7 +135,7 @@ function ChecklistEditor({ rows, onChange, editing }: { rows: CheckRow[]; onChan
125
135
  type="checkbox"
126
136
  className="dsh-atb-cke-box"
127
137
  checked={row.checked}
128
- title={`勾选状态随保存保留(当前勾选人:${row.checkedBy ?? '未勾选'})`}
138
+ title={t('form.check.checkedTitle', { who: row.checkedBy ?? t('form.check.notCheckedYet') })}
129
139
  onChange={e => setRow(index, { checked: e.target.checked })}
130
140
  />
131
141
  )}
@@ -133,18 +143,18 @@ function ChecklistEditor({ rows, onChange, editing }: { rows: CheckRow[]; onChan
133
143
  className="dsh-atb-cke-text"
134
144
  value={row.text}
135
145
  maxLength={200}
136
- placeholder={`验收项 ${index + 1}(完成标准)`}
146
+ placeholder={t('form.check.itemPlaceholder', { n: index + 1 })}
137
147
  spellCheck={false}
138
148
  onChange={e => setRow(index, { text: e.target.value })}
139
149
  />
140
- <button type="button" className="dsh-atb-cke-del" title="删除该验收项" onClick={() => onChange(rows.filter((_, i) => i !== index))}>✕</button>
150
+ <button type="button" className="dsh-atb-cke-del" title={t('form.check.removeTitle')} onClick={() => onChange(rows.filter((_, i) => i !== index))}>✕</button>
141
151
  </div>
142
152
  ))}
143
153
  {rows.length < MAX_CHECKLIST_ITEMS && (
144
- <button type="button" className="dsh-atb-cke-add" onClick={() => onChange([...rows, { text: '', checked: false }])}>+ 添加验收项</button>
154
+ <button type="button" className="dsh-atb-cke-add" onClick={() => onChange([...rows, { text: '', checked: false }])}>{t('form.check.add')}</button>
145
155
  )}
146
156
  {rows.length > 0 && (
147
- <span className="dsh-atb-cke-hint">{editing ? `已勾选 ${checked}/${rows.length}(保存将整体覆盖清单,勾选状态保留)` : `共 ${rows.length} 项,执行会话按清单干活并逐项勾选,未完成项验收时高亮`}</span>
157
+ <span className="dsh-atb-cke-hint">{editing ? t('form.check.hintEdit', { checked, total: rows.length }) : t('form.check.hintCreate', { n: rows.length })}</span>
148
158
  )}
149
159
  </div>
150
160
  )
@@ -158,7 +168,8 @@ function ChecklistEditor({ rows, onChange, editing }: { rows: CheckRow[]; onChan
158
168
  * @param controller - the controller.
159
169
  * @param task - the task being edited (create mode when absent).
160
170
  */
161
- export function TaskFormModal({ controller, task }: { controller: BoardController; task?: TaskRecordLike }) {
171
+ export function TaskFormModal({ controller, task }: { controller: BoardController; task?: TaskRecord }) {
172
+ const t = useT()
162
173
  const state = controller.getSnapshot()
163
174
  const prefill: TaskTemplateSpec | undefined = state.templatePrefill
164
175
  const editing = task !== undefined
@@ -182,6 +193,10 @@ export function TaskFormModal({ controller, task }: { controller: BoardControlle
182
193
  const [presetId, setPresetId] = useState(initialPreset)
183
194
  const [presets, setPresets] = useState<Array<{ id: string; name?: string }>>([])
184
195
  const [presetDefault, setPresetDefault] = useState<string | undefined>(undefined)
196
+ // Permission preset (0.5.5): 'workspace-write' (default) | 'read-only' | 'danger-full-access'
197
+ const [permission, setPermission] = useState<PermissionMode>(
198
+ task?.permission ?? (prefill?.permission ? asPermission(prefill.permission) : defaultPermissionOf(state.ledger.settings)),
199
+ )
185
200
  // Isolation toggle: create mode starts from the board setting (0.5.0
186
201
  // 看板设置 → 默认执行隔离) or the template's choice; edit mode starts from
187
202
  // the task and locks once execution began.
@@ -208,20 +223,16 @@ export function TaskFormModal({ controller, task }: { controller: BoardControlle
208
223
  return () => document.removeEventListener('keydown', onKey)
209
224
  }, [controller])
210
225
 
211
- // Model catalog: the controller exposes the installed face when the runtime is up.
226
+ // Model catalog: query runtime or fallback to host API
212
227
  useEffect(() => {
213
- const face = controller.modelCatalog
214
- if (face === undefined) return
215
- void face().then(setCatalog).catch(() => setCatalog([]))
228
+ void controller.fetchModelCatalog().then(setCatalog).catch(() => setCatalog([]))
216
229
  }, [controller])
217
230
 
218
- // Preset roster: same lazy face; pre-select the deployment default in
231
+ // Preset roster: query runtime or fallback to host API; pre-select the deployment default in
219
232
  // create mode (unless a template pinned one) so executions run with a
220
233
  // real tool set out of the box.
221
234
  useEffect(() => {
222
- const face = controller.presetCatalog
223
- if (face === undefined) return
224
- void face().then(roster => {
235
+ void controller.fetchPresetCatalog().then(roster => {
225
236
  setPresets(roster.presets)
226
237
  setPresetDefault(roster.defaultId)
227
238
  // CREATE mode only (review P1): pre-selecting in edit mode would
@@ -298,6 +309,7 @@ export function TaskFormModal({ controller, task }: { controller: BoardControlle
298
309
  model: picked ?? null,
299
310
  ...(isolationOut !== undefined && !isolationLocked ? { isolation: isolationOut } : {}),
300
311
  presetId: presetOut ?? null,
312
+ permission,
301
313
  // [] clears the checklist (host deletes the field on empty).
302
314
  checklist: rows.length > 0 ? rows : null,
303
315
  })
@@ -311,6 +323,7 @@ export function TaskFormModal({ controller, task }: { controller: BoardControlle
311
323
  model: picked,
312
324
  ...(isolationOut !== undefined ? { isolation: isolationOut } : {}),
313
325
  ...(presetOut !== undefined ? { presetId: presetOut } : {}),
326
+ permission,
314
327
  ...(rows.length > 0 ? { checklist: rows.map(r => r.text) } : {}),
315
328
  })
316
329
  void action.catch(() => undefined).finally(() => setBusy(false))
@@ -337,6 +350,7 @@ export function TaskFormModal({ controller, task }: { controller: BoardControlle
337
350
  model: picked ?? null,
338
351
  ...(isolationOut !== undefined && !isolationLocked ? { isolation: isolationOut } : {}),
339
352
  presetId: presetOut ?? null,
353
+ permission,
340
354
  checklist: rows.length > 0 ? rows : null,
341
355
  })
342
356
  if (saved) await controller.run(task.id)
@@ -351,6 +365,7 @@ export function TaskFormModal({ controller, task }: { controller: BoardControlle
351
365
  model: picked,
352
366
  ...(isolationOut !== undefined ? { isolation: isolationOut } : {}),
353
367
  ...(presetOut !== undefined ? { presetId: presetOut } : {}),
368
+ permission,
354
369
  ...(rows.length > 0 ? { checklist: rows.map(r => r.text) } : {}),
355
370
  })
356
371
  if (id !== undefined) await controller.run(id)
@@ -359,220 +374,257 @@ export function TaskFormModal({ controller, task }: { controller: BoardControlle
359
374
  }
360
375
 
361
376
  const hint = !valid
362
- ? (title.trim().length === 0 ? '请填写标题' : workspaceId === '' ? '请选择项目' : 'Cron 表达式无效(分 时 日 月 周)')
377
+ ? (title.trim().length === 0 ? t('form.hint.needTitle') : workspaceId === '' ? t('form.hint.needProject') : t('form.hint.cronBad'))
363
378
  : mode === 'scheduled' && nextRun !== null
364
- ? `下次运行 ${fmtTime(nextRun)}`
379
+ ? t('form.hint.nextRun', { time: fmtTime(nextRun) })
365
380
  : editing
366
- ? `保存后版本 v${task.version} v${task.version + 1}`
367
- : '创建后项目内会话可认领执行'
381
+ ? t('form.hint.saveVersion', { v: task.version, next: task.version + 1 })
382
+ : t('form.hint.createClaim')
368
383
 
369
384
  return (
370
385
  <div className="dsh-atb-modal-backdrop" onClick={e => { if (e.target === e.currentTarget) controller.closeForm() }}>
371
- <div className="dsh-atb-modal" data-mode={editing ? 'edit' : 'create'} role="dialog" aria-modal="true" aria-label={editing ? '编辑任务' : '新建任务'}>
386
+ <div className="dsh-atb-modal dsh-atb-taskform-modal" data-mode={editing ? 'edit' : 'create'} role="dialog" aria-modal="true" aria-label={editing ? t('form.title.edit') : t('form.title.create')}>
372
387
  <div className="dsh-atb-modal-head">
373
388
  <span className="dsh-atb-modal-headicon">{editing ? '✎' : '✚'}</span>
374
389
  <div className="dsh-atb-modal-headtext">
375
- <h3>{editing ? '编辑任务' : '新建任务'}</h3>
376
- <p>{editing ? '调整任务内容与执行配置' : '推入看板,项目内会话可认领执行'}</p>
390
+ <h3>{editing ? t('form.title.edit') : t('form.title.create')}</h3>
391
+ <p>{editing ? t('form.subtitle.edit') : t('form.subtitle.create')}</p>
377
392
  </div>
378
- <button type="button" className="dsh-atb-modal-close" aria-label="关闭" onClick={() => controller.closeForm()}>✕</button>
393
+ <button type="button" className="dsh-atb-modal-close" aria-label={t('shared.close')} onClick={() => controller.closeForm()}>✕</button>
379
394
  </div>
380
395
 
381
- <div className="dsh-atb-modal-body">
382
- <Field label="标题" required full>
383
- <input ref={titleRef} value={title} onChange={e => setTitle(e.target.value)} placeholder="一句话说清要做什么" maxLength={200} />
384
- </Field>
385
-
386
- <Field label="项目" required>
387
- <select value={workspaceId} onChange={e => setWorkspaceId(e.target.value)}>
388
- {state.workspaces.map(ws => <option key={ws.id} value={ws.id}>{ws.title || ws.path}</option>)}
389
- </select>
390
- </Field>
391
-
392
- <Field label="模型(默认 = 会话默认模型)">
393
- <select
394
- value={model}
395
- onChange={e => {
396
- const val = e.target.value
397
- setModel(val)
398
- if (val === '') {
399
- setReasoningEffort('')
400
- } else {
401
- const pm = JSON.parse(val) as { provider: string; model: string }
402
- const cm = catalog.find(m => m.provider === pm.provider && m.model === pm.model)
403
- if (cm?.reasoning?.defaultEffort !== undefined) {
404
- setReasoningEffort(cm.reasoning.defaultEffort)
405
- } else {
406
- setReasoningEffort('')
407
- }
408
- }
409
- }}
410
- >
411
- <option value="">默认模型</option>
412
- {catalog.map(m => (
413
- <option key={`${m.provider}/${m.model}`} value={JSON.stringify({ provider: m.provider, model: m.model })}>
414
- {m.name ?? m.model}({m.provider})
415
- </option>
416
- ))}
417
- </select>
418
- </Field>
419
-
420
- {parsedModel !== undefined && (
421
- <Field label="思考强度(Reasoning Effort)">
422
- <select
423
- value={reasoningEffort}
424
- onChange={e => setReasoningEffort(e.target.value)}
425
- title="设置模型的思考强度(如 low/medium/high);默认 = 跟随模型/提供商默认"
426
- >
427
- <option value="">跟随模型默认{modelReasoning?.defaultEffort !== undefined ? `(当前:${modelReasoning.efforts.find(ef => ef.id === modelReasoning.defaultEffort)?.name ?? modelReasoning.defaultEffort})` : ''}</option>
428
- {modelReasoning !== undefined && modelReasoning.efforts.length > 0 ? (
429
- modelReasoning.efforts.map(eff => (
430
- <option key={eff.id} value={eff.id}>
431
- {eff.name}{eff.description ? ` (${eff.description})` : ''}
432
- </option>
433
- ))
434
- ) : (
435
- <>
436
- <option value="low">低 (low)</option>
437
- <option value="medium">中 (medium)</option>
438
- <option value="high">高 (high)</option>
439
- <option value="none">关闭思考 (none)</option>
440
- </>
441
- )}
442
- </select>
396
+ <div className="dsh-atb-modal-body dsh-atb-taskform-body">
397
+ {/* Left Column: Core Fields & Execution Configurations */}
398
+ <div className="dsh-atb-form-col dsh-atb-form-left">
399
+ <Field label={t('form.field.title')} required full>
400
+ <input ref={titleRef} value={title} onChange={e => setTitle(e.target.value)} placeholder={t('form.field.titlePlaceholder')} maxLength={200} />
443
401
  </Field>
444
- )}
445
402
 
446
- {presets.length > 0 && (
447
- <Field label="执行模式(preset)">
448
- <select value={presetId} onChange={e => setPresetId(e.target.value)} title="执行会话按该 preset 组合(决定工具集与人设);默认 = 部署默认 preset">
449
- <option value="">跟随部署默认{presetDefault !== undefined ? `(当前:${presets.find(p => p.id === presetDefault)?.name ?? presetDefault})` : ''}</option>
450
- {presets.map(p => (
451
- <option key={p.id} value={p.id}>
452
- {p.name ?? p.id}{p.id === presetDefault ? '(部署默认)' : ''}
453
- </option>
403
+ <div className="dsh-atb-form-subgrid">
404
+ <Field label={t('form.field.project')} required>
405
+ <select value={workspaceId} onChange={e => setWorkspaceId(e.target.value)}>
406
+ {state.workspaces.map(ws => <option key={ws.id} value={ws.id}>{ws.title || ws.path}</option>)}
407
+ </select>
408
+ </Field>
409
+
410
+ <Field label={t('form.field.model')}>
411
+ <select
412
+ value={model}
413
+ onChange={e => {
414
+ const val = e.target.value
415
+ setModel(val)
416
+ if (val === '') {
417
+ setReasoningEffort('')
418
+ } else {
419
+ const pm = JSON.parse(val) as { provider: string; model: string }
420
+ const cm = catalog.find(m => m.provider === pm.provider && m.model === pm.model)
421
+ if (cm?.reasoning?.defaultEffort !== undefined) {
422
+ setReasoningEffort(cm.reasoning.defaultEffort)
423
+ } else {
424
+ setReasoningEffort('')
425
+ }
426
+ }
427
+ }}
428
+ >
429
+ <option value="">{t('form.field.modelDefault')}</option>
430
+ {catalog.map(m => (
431
+ <option key={`${m.provider}/${m.model}`} value={JSON.stringify({ provider: m.provider, model: m.model })}>
432
+ {t('form.model.option', { name: m.name ?? m.model, provider: m.provider })}
433
+ </option>
434
+ ))}
435
+ </select>
436
+ </Field>
437
+
438
+ {parsedModel !== undefined && (
439
+ <Field label={t('form.field.effort')}>
440
+ <select
441
+ value={reasoningEffort}
442
+ onChange={e => setReasoningEffort(e.target.value)}
443
+ title={t('form.field.effortTitle')}
444
+ >
445
+ <option value="">{t('form.effort.follow')}{modelReasoning?.defaultEffort !== undefined ? t('shared.current', { name: modelReasoning.efforts.find(ef => ef.id === modelReasoning.defaultEffort)?.name ?? modelReasoning.defaultEffort }) : ''}</option>
446
+ {modelReasoning !== undefined && modelReasoning.efforts.length > 0 ? (
447
+ modelReasoning.efforts.map(eff => (
448
+ <option key={eff.id} value={eff.id}>
449
+ {eff.name}{eff.description ? ` (${eff.description})` : ''}
450
+ </option>
451
+ ))
452
+ ) : (
453
+ <>
454
+ <option value="low">{t('form.effort.low')}</option>
455
+ <option value="medium">{t('form.effort.medium')}</option>
456
+ <option value="high">{t('form.effort.high')}</option>
457
+ <option value="none">{t('form.effort.none')}</option>
458
+ </>
459
+ )}
460
+ </select>
461
+ </Field>
462
+ )}
463
+
464
+ {presets.length > 0 && (
465
+ <Field label={t('form.field.preset')}>
466
+ <select value={presetId} onChange={e => setPresetId(e.target.value)} title={t('form.field.presetTitle')}>
467
+ <option value="">{t('form.preset.follow')}{presetDefault !== undefined ? t('shared.current', { name: presets.find(p => p.id === presetDefault)?.name ?? presetDefault }) : ''}</option>
468
+ {presets.map(p => (
469
+ <option key={p.id} value={p.id}>
470
+ {p.name ?? p.id}{p.id === presetDefault ? t('form.preset.defaultTag') : ''}
471
+ </option>
472
+ ))}
473
+ </select>
474
+ </Field>
475
+ )}
476
+ </div>
477
+
478
+ <Field label={t('form.field.urgency')} full>
479
+ <div className="dsh-atb-urgency-picker">
480
+ {urgencyOptions(t).map(o => (
481
+ <button
482
+ key={o.value}
483
+ type="button"
484
+ className="dsh-atb-urgency-opt"
485
+ data-urgency={o.value}
486
+ data-on={urgency === o.value}
487
+ onClick={() => setUrgency(o.value)}
488
+ >
489
+ <span className="dsh-atb-urgency-name"><span className="dsh-atb-dot" data-urgency={o.value} />{o.label}</span>
490
+ <span className="dsh-atb-urgency-hint">{o.hint}</span>
491
+ </button>
454
492
  ))}
455
- </select>
493
+ </div>
456
494
  </Field>
457
- )}
458
495
 
459
- <Field label="紧急度" full>
460
- <div className="dsh-atb-urgency-picker">
461
- {URGENCY_OPTIONS.map(o => (
462
- <button
463
- key={o.value}
464
- type="button"
465
- className="dsh-atb-urgency-opt"
466
- data-urgency={o.value}
467
- data-on={urgency === o.value}
468
- onClick={() => setUrgency(o.value)}
469
- >
470
- <span className="dsh-atb-urgency-name"><span className="dsh-atb-dot" data-urgency={o.value} />{o.label}</span>
471
- <span className="dsh-atb-urgency-hint">{o.hint}</span>
472
- </button>
473
- ))}
474
- </div>
475
- </Field>
476
-
477
- <Field label={editing ? '描述' : '描述(可选)'} full>
478
- <textarea value={description} onChange={e => setDescription(e.target.value)} placeholder="需求细节、验收标准…" />
479
- </Field>
480
-
481
- <Field label={editing ? '执行 Prompt(实际 Prompt = 标题+任务描述+Prompt)' : '执行 Prompt(可选;实际 Prompt = 标题+任务描述+Prompt)'} full>
482
- <textarea value={prompt} onChange={e => setPrompt(e.target.value)} placeholder={'追加在「标题+任务描述」之后发给执行会话的补充指令。支持模板变量:{{lastExecution}}(上次执行结果)、{{lastComments}}(最近 3 条评论)'} />
483
- </Field>
484
-
485
- <Field label="执行方式" full>
486
- <div className="dsh-atb-mode-picker">
487
- <button type="button" className="dsh-atb-mode-opt" data-on={mode === 'claim'} onClick={() => setMode('claim')}>
488
- <span className="dsh-atb-mode-name">🤝 认领制</span>
489
- <span className="dsh-atb-mode-hint">项目内会话认领</span>
490
- </button>
491
- <button type="button" className="dsh-atb-mode-opt" data-on={mode === 'scheduled'} onClick={() => setMode('scheduled')}>
492
- <span className="dsh-atb-mode-name">⏰ 定时执行</span>
493
- <span className="dsh-atb-mode-hint">到点自动开跑</span>
494
- </button>
495
- </div>
496
- </Field>
497
-
498
- {mode === 'scheduled' && (
499
- <Field label="Cron 表达式" required full>
500
- <input
501
- className={cronBad ? 'dsh-atb-input-bad' : undefined}
502
- value={cron}
503
- onChange={e => setCron(e.target.value)}
504
- placeholder="分 时 日 月 周"
505
- spellCheck={false}
506
- />
507
- <span className="dsh-atb-cron-presets">
508
- {CRON_PRESETS.map(p => (
496
+ <Field label={t('form.field.permission')} full>
497
+ <div className="dsh-atb-perm-picker">
498
+ {permissionOptions(t).map(opt => (
509
499
  <button
510
- key={p.cron}
500
+ key={opt.value}
511
501
  type="button"
512
- className="dsh-atb-cron-preset"
513
- data-on={cron.trim() === p.cron}
514
- onClick={() => setCron(p.cron)}
502
+ className="dsh-atb-perm-opt"
503
+ data-on={permission === opt.value}
504
+ onClick={() => setPermission(opt.value)}
515
505
  >
516
- {p.label}
506
+ <span className="dsh-atb-perm-name">{opt.icon} {opt.label}{opt.value === 'workspace-write' ? t('form.perm.defaultTag') : ''}</span>
507
+ <span className="dsh-atb-perm-hint">{opt.hint}</span>
517
508
  </button>
518
509
  ))}
519
- {!cronBad && nextRun !== null && <span className="dsh-atb-cron-next">下次 {fmtTime(nextRun)}</span>}
520
- </span>
510
+ </div>
521
511
  </Field>
522
- )}
523
512
 
524
- <Field label="执行隔离" full>
525
- <div className="dsh-atb-mode-picker" data-disabled={isolationDisabled ? 'true' : undefined}>
526
- <button
527
- type="button"
528
- className="dsh-atb-mode-opt"
529
- data-on={isolation === 'worktree'}
530
- disabled={isolationDisabled}
531
- title={isolationLocked ? '任务已有执行记录,隔离方式已锁定' : !gitOk ? '当前项目非 git 仓库' : '每次执行在独立 worktree 分支上进行'}
532
- onClick={() => setIsolation('worktree')}
533
- >
534
- <span className="dsh-atb-mode-name">🌿 Worktree 隔离</span>
535
- <span className="dsh-atb-mode-hint">
536
- {isolationLocked ? '已锁定(执行开始后不可更改)' : !gitOk ? '当前项目非 git 仓库' : '独立分支 task/标题+ID,互不污染'}
513
+ <Field label={t('form.field.mode')} full>
514
+ <div className="dsh-atb-mode-picker">
515
+ <button type="button" className="dsh-atb-mode-opt" data-on={mode === 'claim'} onClick={() => setMode('claim')}>
516
+ <span className="dsh-atb-mode-name">{t('form.mode.claim')}</span>
517
+ <span className="dsh-atb-mode-hint">{t('form.mode.claimHint')}</span>
518
+ </button>
519
+ <button type="button" className="dsh-atb-mode-opt" data-on={mode === 'scheduled'} onClick={() => setMode('scheduled')}>
520
+ <span className="dsh-atb-mode-name">{t('form.mode.scheduled')}</span>
521
+ <span className="dsh-atb-mode-hint">{t('form.mode.scheduledHint')}</span>
522
+ </button>
523
+ </div>
524
+ </Field>
525
+
526
+ {mode === 'scheduled' && (
527
+ <Field label={t('form.field.cron')} required full>
528
+ <input
529
+ className={cronBad ? 'dsh-atb-input-bad' : undefined}
530
+ value={cron}
531
+ onChange={e => setCron(e.target.value)}
532
+ placeholder={t('form.cron.placeholder')}
533
+ spellCheck={false}
534
+ />
535
+ <span className="dsh-atb-cron-presets">
536
+ {cronPresets(t).map(p => (
537
+ <button
538
+ key={p.cron}
539
+ type="button"
540
+ className="dsh-atb-cron-preset"
541
+ data-on={cron.trim() === p.cron}
542
+ onClick={() => setCron(p.cron)}
543
+ >
544
+ {p.label}
545
+ </button>
546
+ ))}
547
+ {!cronBad && nextRun !== null && <span className="dsh-atb-cron-next">{t('form.cron.next', { time: fmtTime(nextRun) })}</span>}
537
548
  </span>
538
- </button>
539
- <button
540
- type="button"
541
- className="dsh-atb-mode-opt"
542
- data-on={isolation === 'none'}
543
- disabled={isolationDisabled}
544
- title={isolationLocked ? '任务已有执行记录,隔离方式已锁定' : '直接在项目目录执行(不使用 git)'}
545
- onClick={() => setIsolation('none')}
546
- >
547
- <span className="dsh-atb-mode-name">📁 原目录执行</span>
548
- <span className="dsh-atb-mode-hint">{isolationLocked ? '已锁定(执行开始后不可更改)' : !gitOk ? '当前项目非 git 仓库,将在原目录执行' : '不使用 git,直接在项目目录工作'}</span>
549
- </button>
550
- </div>
551
- {!gitOk && !isolationLocked && (
552
- <span className="dsh-atb-isolation-note">当前项目非 git 仓库,将在原目录执行(任务仍按默认配置创建,运行时自动降级)</span>
549
+ </Field>
553
550
  )}
554
- </Field>
555
551
 
556
- <Field label={editing ? '验收清单(DoD)' : '验收清单(DoD,可选)'} full>
557
- <ChecklistEditor rows={checkRows} onChange={setCheckRows} editing={editing} />
558
- </Field>
552
+ <Field label={t('form.field.isolation')} full>
553
+ <div className="dsh-atb-mode-picker" data-disabled={isolationDisabled ? 'true' : undefined}>
554
+ <button
555
+ type="button"
556
+ className="dsh-atb-mode-opt"
557
+ data-on={isolation === 'worktree'}
558
+ disabled={isolationDisabled}
559
+ title={isolationLocked ? t('form.iso.locked') : !gitOk ? t('form.iso.nonGit') : t('form.iso.worktreeTitle')}
560
+ onClick={() => setIsolation('worktree')}
561
+ >
562
+ <span className="dsh-atb-mode-name">{t('form.iso.worktree')}</span>
563
+ <span className="dsh-atb-mode-hint">
564
+ {isolationLocked ? t('form.iso.lockedShort') : !gitOk ? t('form.iso.nonGit') : t('form.iso.worktreeHint')}
565
+ </span>
566
+ </button>
567
+ <button
568
+ type="button"
569
+ className="dsh-atb-mode-opt"
570
+ data-on={isolation === 'none'}
571
+ disabled={isolationDisabled}
572
+ title={isolationLocked ? t('form.iso.locked') : t('form.iso.noneTitle')}
573
+ onClick={() => setIsolation('none')}
574
+ >
575
+ <span className="dsh-atb-mode-name">{t('form.iso.none')}</span>
576
+ <span className="dsh-atb-mode-hint">{isolationLocked ? t('form.iso.lockedShort') : !gitOk ? t('form.iso.noneHintNonGit') : t('form.iso.noneHint')}</span>
577
+ </button>
578
+ </div>
579
+ {!gitOk && !isolationLocked && (
580
+ <span className="dsh-atb-isolation-note">{t('form.iso.nonGitNote')}</span>
581
+ )}
582
+ </Field>
583
+
584
+ <Field label={editing ? t('form.field.checklist') : t('form.field.checklistOptional')} full>
585
+ <ChecklistEditor rows={checkRows} onChange={setCheckRows} editing={editing} />
586
+ </Field>
587
+ </div>
588
+
589
+ {/* Right Column: Description & Execution Prompt */}
590
+ <div className="dsh-atb-form-col dsh-atb-form-right">
591
+ <Field label={editing ? t('form.field.description') : t('form.field.descriptionOptional')} full>
592
+ <SlashPromptInput
593
+ value={description}
594
+ onChange={setDescription}
595
+ controller={controller}
596
+ rows={7}
597
+ placeholder={t('form.desc.placeholder')}
598
+ />
599
+ </Field>
600
+
601
+ <Field label={editing ? t('form.field.prompt') : t('form.field.promptOptional')} full>
602
+ <SlashPromptInput
603
+ value={prompt}
604
+ onChange={setPrompt}
605
+ controller={controller}
606
+ rows={7}
607
+ placeholder={t('form.prompt.placeholder')}
608
+ />
609
+ </Field>
610
+ </div>
559
611
  </div>
560
612
 
561
613
  <div className="dsh-atb-modal-foot">
562
614
  <span className="dsh-atb-modal-hint" data-tone={valid ? undefined : 'bad'}>{hint}</span>
563
615
  <span className="dsh-atb-modal-footbtns">
564
- <button type="button" className="dsh-atb-btn" onClick={() => controller.closeForm()}>取消</button>
616
+ <button type="button" className="dsh-atb-btn" onClick={() => controller.closeForm()}>{t('shared.cancel')}</button>
565
617
  <button
566
618
  type="button"
567
619
  className="dsh-atb-btn"
568
620
  disabled={!valid || runBlocked || busy}
569
- title={runBlocked ? '任务正在执行中,不能重复发起' : busy ? '正在提交…' : '保存后立即发起执行(新会话)'}
621
+ title={runBlocked ? t('form.action.runBlockedTitle') : busy ? t('form.action.runBusyTitle') : t('form.action.runTitle')}
570
622
  onClick={submitAndRun}
571
623
  >
572
- ⚡ 立即执行
624
+ {t('form.action.run')}
573
625
  </button>
574
626
  <button type="button" className="dsh-atb-btn" data-primary="true" disabled={!valid || busy} onClick={submit}>
575
- {editing ? '保存修改' : '创建任务'}
627
+ {editing ? t('form.action.save') : t('form.action.create')}
576
628
  </button>
577
629
  </span>
578
630
  </div>