dsh-taskboard 0.5.5 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +22 -2
- package/lib/client.js +2604 -767
- package/lib/host/execution.js +3 -0
- package/lib/host/execution.js.map +1 -1
- package/lib/host/routes.js +31 -1
- package/lib/host/routes.js.map +1 -1
- package/lib/host/session-sync.js +210 -10
- package/lib/host/session-sync.js.map +1 -1
- package/lib/host/store.js +9 -2
- package/lib/host/store.js.map +1 -1
- package/lib/index.js +100 -1
- package/lib/index.js.map +1 -1
- package/lib/shared/api.js.map +1 -1
- package/lib/shared/protocol.js +19 -1
- package/lib/shared/protocol.js.map +1 -1
- package/package.json +75 -75
- package/src/client/api.ts +8 -0
- package/src/client/board/AlertModal.tsx +3 -1
- package/src/client/board/ImportModal.tsx +26 -24
- package/src/client/board/SettingsModal.tsx +66 -26
- package/src/client/board/SlashPromptInput.tsx +272 -0
- package/src/client/board/TaskBoard.tsx +53 -49
- package/src/client/board/TaskCard.tsx +33 -21
- package/src/client/board/TaskDetail.tsx +169 -104
- package/src/client/board/TaskFormModal.tsx +254 -202
- package/src/client/board/TemplateManager.tsx +32 -29
- package/src/client/board/labels.ts +36 -27
- package/src/client/controller.ts +62 -2
- package/src/client/i18n/en.ts +455 -0
- package/src/client/i18n/runtime.ts +155 -0
- package/src/client/i18n/zh.ts +460 -0
- package/src/client/index.ts +182 -42
- package/src/client/sidebar-entry.ts +13 -3
- package/src/client/styles.ts +131 -0
- package/src/host/execution.ts +13 -0
- package/src/host/routes.ts +49 -1
- package/src/host/session-sync.ts +334 -14
- package/src/host/store.ts +15 -1
- package/src/index.ts +115 -1
- package/src/shared/api.ts +47 -0
- package/src/shared/protocol.ts +41 -0
- 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
|
|
67
|
-
{ value: 'urgent', label: '
|
|
68
|
-
{ value: 'normal', label: '
|
|
69
|
-
{ value: 'relaxed', label: '
|
|
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
|
|
74
|
-
{ label: '
|
|
75
|
-
{ label: '
|
|
76
|
-
{ label: '
|
|
77
|
-
{ label: '
|
|
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={
|
|
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={
|
|
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=
|
|
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 }])}
|
|
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 ?
|
|
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?:
|
|
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:
|
|
226
|
+
// Model catalog: query runtime or fallback to host API
|
|
212
227
|
useEffect(() => {
|
|
213
|
-
|
|
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:
|
|
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
|
-
|
|
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 ? '
|
|
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
|
-
?
|
|
379
|
+
? t('form.hint.nextRun', { time: fmtTime(nextRun) })
|
|
365
380
|
: editing
|
|
366
|
-
?
|
|
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 ? '
|
|
376
|
-
<p>{editing ? '
|
|
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=
|
|
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
|
-
|
|
383
|
-
|
|
384
|
-
|
|
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
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
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
|
-
</
|
|
493
|
+
</div>
|
|
456
494
|
</Field>
|
|
457
|
-
)}
|
|
458
495
|
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
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={
|
|
500
|
+
key={opt.value}
|
|
511
501
|
type="button"
|
|
512
|
-
className="dsh-atb-
|
|
513
|
-
data-on={
|
|
514
|
-
onClick={() =>
|
|
502
|
+
className="dsh-atb-perm-opt"
|
|
503
|
+
data-on={permission === opt.value}
|
|
504
|
+
onClick={() => setPermission(opt.value)}
|
|
515
505
|
>
|
|
516
|
-
{
|
|
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
|
-
|
|
520
|
-
</span>
|
|
510
|
+
</div>
|
|
521
511
|
</Field>
|
|
522
|
-
)}
|
|
523
512
|
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
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
|
-
</
|
|
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
|
-
|
|
557
|
-
|
|
558
|
-
|
|
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()}
|
|
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 ? '
|
|
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>
|