@linxin666/dsh-client-ui-task-board 0.4.5 → 0.4.7
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.i18n.yaml +2 -2
- package/README.md +34 -18
- package/README.zh.md +34 -18
- package/lib/client.js +2804 -1024
- package/lib/client.js.map +1 -1
- package/lib/index.js +2576 -416
- package/lib/types/client/CardDefaultPresetField.d.ts +35 -0
- package/lib/types/client/TaskBoardSettingsCard.d.ts +61 -0
- package/lib/types/client/board/ReasoningEffortField.d.ts +25 -0
- package/lib/types/client/board/model-options.d.ts +67 -0
- package/lib/types/client/board/preset-label.d.ts +21 -0
- package/lib/types/client/board/preset-roster.d.ts +45 -0
- package/lib/types/client/board/team-run-promotion.d.ts +26 -0
- package/lib/types/client/index.d.ts +4 -5
- package/lib/types/client/locales.d.ts +81 -0
- package/lib/types/client/plugin-card-seat.d.ts +5 -0
- package/lib/types/client/schedule-zone.d.ts +50 -0
- package/lib/types/core/card-defaults.d.ts +78 -0
- package/lib/types/core/controller.d.ts +41 -17
- package/lib/types/core/launch-width.d.ts +31 -0
- package/lib/types/core/poll-cadence.d.ts +31 -0
- package/lib/types/core/reasoning-effort.d.ts +57 -0
- package/lib/types/core/subtask.d.ts +30 -8
- package/lib/types/core/tasks.d.ts +163 -14
- package/lib/types/core/use-cases/task-create.d.ts +10 -5
- package/lib/types/core/use-cases/task-schedule.d.ts +68 -10
- package/lib/types/core/use-cases/task-update.d.ts +1 -1
- package/lib/types/core/verification-budget.d.ts +52 -0
- package/lib/types/core/verification.d.ts +376 -9
- package/lib/types/host/launch-admission.d.ts +72 -0
- package/lib/types/host/verification-gate.d.ts +24 -0
- package/lib/types/host/verification-runner.d.ts +38 -7
- package/lib/types/host-ledger.d.ts +91 -11
- package/lib/types/host-runner.d.ts +111 -13
- package/lib/types/host-service.d.ts +199 -8
- package/lib/types/index.d.ts +141 -0
- package/lib/types/protocol.d.ts +63 -12
- package/package.json +29 -33
- package/src/client/CardDefaultPresetField.tsx +101 -0
- package/src/client/TaskBoardSettingsCard.tsx +288 -17
- package/src/client/board/LinkSubtaskModal.tsx +8 -0
- package/src/client/board/NewTaskModal.tsx +237 -60
- package/src/client/board/ReasoningEffortField.tsx +50 -0
- package/src/client/board/TaskCard.tsx +3 -3
- package/src/client/board/TaskDetail.tsx +261 -24
- package/src/client/board/VerificationReport.tsx +132 -13
- package/src/client/board/model-options.ts +178 -0
- package/src/client/board/preset-label.ts +32 -0
- package/src/client/board/preset-roster.ts +58 -0
- package/src/client/board/status-key.ts +3 -0
- package/src/client/board/team-run-promotion.ts +39 -0
- package/src/client/index.ts +105 -108
- package/src/client/locales.ts +171 -6
- package/src/client/plugin-card-seat.ts +7 -0
- package/src/client/schedule-zone.ts +100 -2
- package/src/client/settings-entry-form.ts +0 -1
- package/src/core/card-defaults.ts +132 -0
- package/src/core/controller.ts +55 -16
- package/src/core/launch-width.ts +38 -0
- package/src/core/poll-cadence.ts +41 -0
- package/src/core/reasoning-effort.ts +92 -0
- package/src/core/store.ts +69 -15
- package/src/core/subtask.ts +48 -13
- package/src/core/tasks.ts +213 -39
- package/src/core/use-cases/task-create.ts +57 -25
- package/src/core/use-cases/task-schedule.ts +190 -25
- package/src/core/use-cases/task-update.ts +9 -4
- package/src/core/verification-budget.ts +115 -0
- package/src/core/verification.ts +785 -17
- package/src/host/agent-tools.ts +127 -29
- package/src/host/launch-admission.ts +140 -0
- package/src/host/verification-gate.ts +172 -16
- package/src/host/verification-runner.ts +207 -40
- package/src/host-ledger.ts +346 -48
- package/src/host-runner.ts +234 -35
- package/src/host-service.ts +539 -52
- package/src/index.ts +250 -7
- package/src/protocol.ts +160 -32
- package/lib/invariant.js +0 -6
- package/lib/types/client/board-card-seat.d.ts +0 -34
- package/lib/types/invariant.d.ts +0 -3
- package/src/client/board-card-seat.ts +0 -102
- package/src/invariant.ts +0 -4
|
@@ -13,13 +13,18 @@
|
|
|
13
13
|
* execution row's existing markup instead of extending that enum.
|
|
14
14
|
*/
|
|
15
15
|
import {
|
|
16
|
+
MAX_INVALID_ATTEMPTS,
|
|
16
17
|
MAX_QUALITY_ATTEMPTS,
|
|
17
18
|
exceptionAttempts,
|
|
19
|
+
invalidAttempts,
|
|
18
20
|
qualityAttempts,
|
|
19
21
|
verificationPhase,
|
|
20
22
|
verificationTotals,
|
|
23
|
+
type AcceptanceAuditRecord,
|
|
24
|
+
type AcceptanceCleanupRecord,
|
|
21
25
|
type ExecutionVerification,
|
|
22
26
|
type VerificationAttempt,
|
|
27
|
+
type VerificationCriterionFinding,
|
|
23
28
|
} from '../../core/verification.ts'
|
|
24
29
|
import { t } from '../locales.ts'
|
|
25
30
|
import type { TaskBoardKey } from '../locales.ts'
|
|
@@ -46,24 +51,67 @@ const PHASE_STATUS_KEY: Record<ReturnType<typeof verificationPhase>, TaskBoardKe
|
|
|
46
51
|
repairing: 'verify.status.failed',
|
|
47
52
|
passed: 'verify.status.passed',
|
|
48
53
|
failed: 'verify.status.failed',
|
|
54
|
+
invalid: 'verify.status.invalidHeld',
|
|
49
55
|
}
|
|
50
56
|
|
|
51
|
-
/**
|
|
52
|
-
|
|
53
|
-
|
|
57
|
+
/** Evidence location → locale key, for one structured finding's citation. */
|
|
58
|
+
const LOCATION_KEY: Record<VerificationCriterionFinding['location'], TaskBoardKey> = {
|
|
59
|
+
task: 'verify.location.task',
|
|
60
|
+
trajectory: 'verify.location.trajectory',
|
|
61
|
+
workspace: 'verify.location.workspace',
|
|
62
|
+
baseline: 'verify.location.baseline',
|
|
63
|
+
unknown: 'verify.location.unknown',
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** Invalid reason → locale key. */
|
|
67
|
+
function invalidReasonKey(attempt: VerificationAttempt): TaskBoardKey {
|
|
68
|
+
switch (attempt.invalidReason) {
|
|
69
|
+
case 'missing-finding': return 'verify.invalidReason.missing-finding'
|
|
70
|
+
case 'unlocatable-quote': return 'verify.invalidReason.unlocatable-quote'
|
|
71
|
+
case 'baseline-finding': return 'verify.invalidReason.baseline-finding'
|
|
72
|
+
case 'vacuous-finding': return 'verify.invalidReason.vacuous-finding'
|
|
73
|
+
case 'insufficient-evidence': return 'verify.invalidReason.insufficient-evidence'
|
|
74
|
+
default: return 'verify.invalidReason.unknown'
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** Audit stage → locale key (the lightweight credential after cleanup). */
|
|
79
|
+
const AUDIT_STAGE_KEY: Record<AcceptanceAuditRecord['stage'], TaskBoardKey> = {
|
|
80
|
+
quality: 'verify.cleanup.stage.quality',
|
|
81
|
+
invalid: 'verify.cleanup.stage.invalid',
|
|
82
|
+
exception: 'verify.cleanup.stage.exception',
|
|
83
|
+
budget: 'verify.cleanup.stage.budget',
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/** One recorded attempt: a quality verdict, an invalid veto, an anomaly, or a budget stop. */
|
|
87
|
+
function AttemptRow({ attempt, detailRemoved }: { attempt: VerificationAttempt, detailRemoved: boolean }) {
|
|
88
|
+
// Everything that is not a quality verdict renders as an anomaly row: a
|
|
89
|
+
// budget stop carries no score either, and an invalid acceptance carries a
|
|
90
|
+
// score the board REFUSED to book, so showing either as a scored failure
|
|
91
|
+
// would read as a judgement the board never made. The invalid row gets its
|
|
92
|
+
// own badge and reason line, because it is a distinct outcome.
|
|
93
|
+
const invalid = attempt.stage === 'invalid'
|
|
94
|
+
const exception = attempt.stage !== 'quality' && !invalid
|
|
54
95
|
const result = exception ? 'failed' : attempt.passed ? 'succeeded' : 'failed'
|
|
55
96
|
return (
|
|
56
97
|
<li className={css.executionRow} data-result={result}>
|
|
57
98
|
<span className={css.executionBadge} data-result={result}>
|
|
58
|
-
{exception ? t('verify.status.exception') : attempt.passed ? t('verify.status.passed') : t('verify.status.failed')}
|
|
99
|
+
{invalid ? t('verify.status.invalid') : exception ? t('verify.status.exception') : attempt.passed ? t('verify.status.passed') : t('verify.status.failed')}
|
|
59
100
|
</span>
|
|
60
101
|
<span className={css.executionTimes}>
|
|
61
|
-
{
|
|
62
|
-
? t('verify.
|
|
63
|
-
:
|
|
102
|
+
{invalid
|
|
103
|
+
? t('verify.attemptInvalid', { index: String(attempt.index) })
|
|
104
|
+
: exception
|
|
105
|
+
? t('verify.attemptException', { index: String(attempt.index) })
|
|
106
|
+
: t('verify.attempt', { index: String(attempt.index) })}
|
|
64
107
|
{' · '}{routeLabel(attempt)}
|
|
65
|
-
{!exception && ' · ' + t('verify.rounds', { rounds: String(attempt.rounds) })}
|
|
108
|
+
{!exception && !invalid && ' · ' + t('verify.rounds', { rounds: String(attempt.rounds) })}
|
|
66
109
|
</span>
|
|
110
|
+
{invalid && (
|
|
111
|
+
<span className={css.executionTimes}>
|
|
112
|
+
{t('verify.invalidDetail', { reason: t(invalidReasonKey(attempt)) })}
|
|
113
|
+
</span>
|
|
114
|
+
)}
|
|
67
115
|
{attempt.error !== undefined && <span className={css.executionTimes}>{attempt.error}</span>}
|
|
68
116
|
{!exception && (
|
|
69
117
|
<>
|
|
@@ -88,6 +136,25 @@ function AttemptRow({ attempt }: { attempt: VerificationAttempt }) {
|
|
|
88
136
|
</ul>
|
|
89
137
|
</>
|
|
90
138
|
)}
|
|
139
|
+
{(attempt.criterionFindings ?? []).length > 0 && (
|
|
140
|
+
<>
|
|
141
|
+
<span className={css.executionTimes}>{t('verify.criterionFindings')}</span>
|
|
142
|
+
<ul className={css.executionList}>
|
|
143
|
+
{(attempt.criterionFindings ?? []).map((finding, index) => (
|
|
144
|
+
<li key={findingKey(attempt, index)} className={css.executionTimes}>
|
|
145
|
+
{t('verify.criterionFindingFull', {
|
|
146
|
+
criterion: finding.criterionId,
|
|
147
|
+
location: t(LOCATION_KEY[finding.location]),
|
|
148
|
+
quote: finding.quote,
|
|
149
|
+
requirement: finding.requirement,
|
|
150
|
+
observation: finding.observation,
|
|
151
|
+
gap: finding.gap,
|
|
152
|
+
})}
|
|
153
|
+
</li>
|
|
154
|
+
))}
|
|
155
|
+
</ul>
|
|
156
|
+
</>
|
|
157
|
+
)}
|
|
91
158
|
{attempt.findings.length > 0 && (
|
|
92
159
|
<>
|
|
93
160
|
<span className={css.executionTimes}>{t('verify.findings')}</span>
|
|
@@ -98,8 +165,12 @@ function AttemptRow({ attempt }: { attempt: VerificationAttempt }) {
|
|
|
98
165
|
</ul>
|
|
99
166
|
</>
|
|
100
167
|
)}
|
|
101
|
-
{!exception && attempt.findings.length === 0 && (
|
|
102
|
-
|
|
168
|
+
{!exception && !invalid && attempt.findings.length === 0 && (
|
|
169
|
+
// A cleaned attempt has no findings because the acceptance removed them,
|
|
170
|
+
// not because the judge gave none: saying "the judge reported no
|
|
171
|
+
// finding" here would invite exactly the misreading the cleanup rule
|
|
172
|
+
// exists to avoid.
|
|
173
|
+
<span className={css.executionTimes}>{t(detailRemoved ? 'verify.detailRemoved' : 'verify.noFindings')}</span>
|
|
103
174
|
)}
|
|
104
175
|
<span className={css.executionTimes}>
|
|
105
176
|
{t('verify.evidence', {
|
|
@@ -145,16 +216,20 @@ export function VerificationReport({ verification }: { verification: ExecutionVe
|
|
|
145
216
|
? 'verify.applicability.skipped'
|
|
146
217
|
: verification.applicability === 'goal-unavailable'
|
|
147
218
|
? 'verify.applicability.goalUnavailable'
|
|
148
|
-
: verification.applicability === '
|
|
219
|
+
: verification.applicability === 'goal-disabled'
|
|
220
|
+
? 'verify.applicability.goalDisabled'
|
|
221
|
+
: verification.applicability === 'team-member' ? 'verify.applicability.teamMember' : undefined
|
|
149
222
|
return (
|
|
150
223
|
<div className={css.executionTimes}>
|
|
151
224
|
<span className={css.executionBadge} data-result={phase === 'passed' ? 'succeeded' : phase === 'failed' ? 'failed' : undefined}>
|
|
152
225
|
{t('verify.title')} · {t(PHASE_STATUS_KEY[phase])}
|
|
153
226
|
</span>
|
|
154
227
|
<span className={css.executionTimes}>
|
|
155
|
-
{t('verify.
|
|
228
|
+
{t('verify.invalidCounts', {
|
|
156
229
|
quality: String(qualityAttempts(verification).length),
|
|
157
230
|
max: String(MAX_QUALITY_ATTEMPTS),
|
|
231
|
+
invalid: String(invalidAttempts(verification).length),
|
|
232
|
+
maxInvalid: String(MAX_INVALID_ATTEMPTS),
|
|
158
233
|
exceptions: String(exceptionAttempts(verification).length),
|
|
159
234
|
})}
|
|
160
235
|
</span>
|
|
@@ -194,10 +269,54 @@ export function VerificationReport({ verification }: { verification: ExecutionVe
|
|
|
194
269
|
{verification.attempts.length > 0 && (
|
|
195
270
|
<ul className={css.executionList}>
|
|
196
271
|
{[...verification.attempts].reverse().map(attempt => (
|
|
197
|
-
<AttemptRow
|
|
272
|
+
<AttemptRow
|
|
273
|
+
key={attempt.at.toString(36) + '-' + attempt.stage + '-' + String(attempt.index)}
|
|
274
|
+
attempt={attempt}
|
|
275
|
+
detailRemoved={verification.cleanup?.state === 'cleaned'}
|
|
276
|
+
/>
|
|
198
277
|
))}
|
|
199
278
|
</ul>
|
|
200
279
|
)}
|
|
280
|
+
{verification.cleanup !== undefined && <CleanupNotice cleanup={verification.cleanup} />}
|
|
201
281
|
</div>
|
|
202
282
|
)
|
|
203
283
|
}
|
|
284
|
+
|
|
285
|
+
/**
|
|
286
|
+
* Retention notice of one execution's acceptance detail.
|
|
287
|
+
*
|
|
288
|
+
* It exists so a reader never mistakes a cleaned-up report for a judge that
|
|
289
|
+
* produced no evidence: the notice says the detail WAS produced and was removed
|
|
290
|
+
* by the automatic cleanup, and lists the lightweight credential that survives.
|
|
291
|
+
* A pending or failed cleanup is reported the same way — never as a missing
|
|
292
|
+
* verdict and never as a revoked pass.
|
|
293
|
+
*/
|
|
294
|
+
function CleanupNotice({ cleanup }: { cleanup: AcceptanceCleanupRecord }) {
|
|
295
|
+
const key: TaskBoardKey = cleanup.state === 'cleaned'
|
|
296
|
+
? 'verify.cleanup.cleaned'
|
|
297
|
+
: cleanup.state === 'failed' ? 'verify.cleanup.failed' : 'verify.cleanup.pending'
|
|
298
|
+
return (
|
|
299
|
+
<>
|
|
300
|
+
<span className={css.executionTimes}>
|
|
301
|
+
{cleanup.state === 'failed'
|
|
302
|
+
? t(key, { attempts: String(cleanup.attempts), error: cleanup.lastError ?? '' })
|
|
303
|
+
: t(key)}
|
|
304
|
+
</span>
|
|
305
|
+
{cleanup.audit.length > 0 && (
|
|
306
|
+
<ul className={css.executionList}>
|
|
307
|
+
{cleanup.audit.map((record, index) => (
|
|
308
|
+
<li key={record.at.toString(36) + '-' + record.stage + '-' + String(index)} className={css.executionTimes}>
|
|
309
|
+
{t('verify.cleanup.audit', {
|
|
310
|
+
index: String(index + 1),
|
|
311
|
+
stage: t(AUDIT_STAGE_KEY[record.stage]),
|
|
312
|
+
score: percent(record.score),
|
|
313
|
+
hash: record.evidenceHash.slice(0, 12),
|
|
314
|
+
})}
|
|
315
|
+
{record.findingSummary === undefined ? '' : ' · ' + record.findingSummary}
|
|
316
|
+
</li>
|
|
317
|
+
))}
|
|
318
|
+
</ul>
|
|
319
|
+
)}
|
|
320
|
+
</>
|
|
321
|
+
)
|
|
322
|
+
}
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Execution model options: the picker rows built from the official
|
|
3
|
+
* `session/modelCatalog` reply, and the reasoning-effort derivation both
|
|
4
|
+
* execution-target editors read off them (the create dialog and the detail
|
|
5
|
+
* page share one contract here, so they cannot promise different levels).
|
|
6
|
+
*
|
|
7
|
+
* The pickers must offer each model's real reasoning levels, which only the
|
|
8
|
+
* catalog declares — so the reply is normalized once, here, instead of every
|
|
9
|
+
* picker re-reading the wire shape. The fallback discovery path carries no
|
|
10
|
+
* capability metadata at all; its rows stay silent about reasoning rather than
|
|
11
|
+
* claiming a model supports none.
|
|
12
|
+
*/
|
|
13
|
+
import type { ExecutionModelOption } from '../../core/controller.ts'
|
|
14
|
+
import type { TaskBoardKey } from '../locales.ts'
|
|
15
|
+
|
|
16
|
+
/** One model's declared reasoning capability, as a picker row carries it. */
|
|
17
|
+
type ModelReasoning = NonNullable<ExecutionModelOption['reasoning']>
|
|
18
|
+
|
|
19
|
+
/** A plain JSON object, or undefined for anything else (including arrays). */
|
|
20
|
+
function record(value: unknown): Record<string, unknown> | undefined {
|
|
21
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value) ? value as Record<string, unknown> : undefined
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** A non-empty string, or undefined. */
|
|
25
|
+
function text(value: unknown): string | undefined {
|
|
26
|
+
return typeof value === 'string' && value !== '' ? value : undefined
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** The effort ids an adapter published for one model, in catalog order. */
|
|
30
|
+
function effortIdsOf(reasoning: Record<string, unknown>): string[] {
|
|
31
|
+
const efforts: string[] = []
|
|
32
|
+
for (const entry of Array.isArray(reasoning.efforts) ? reasoning.efforts : []) {
|
|
33
|
+
const id = text(record(entry)?.id)
|
|
34
|
+
if (id !== undefined && !efforts.includes(id)) efforts.push(id)
|
|
35
|
+
}
|
|
36
|
+
return efforts
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* What the catalog declares about one model's reasoning: a published
|
|
41
|
+
* `reasoning` block is what "this route reasons" means, and a model published
|
|
42
|
+
* without one is declared to have none. A model the reply never described at
|
|
43
|
+
* all is a different state, expressed by the row carrying no capability.
|
|
44
|
+
*/
|
|
45
|
+
function reasoningOf(model: Record<string, unknown>): ModelReasoning {
|
|
46
|
+
const reasoning = record(model.reasoning)
|
|
47
|
+
if (reasoning === undefined) return { supported: false }
|
|
48
|
+
const efforts = effortIdsOf(reasoning)
|
|
49
|
+
return { supported: true, ...(efforts.length === 0 ? {} : { efforts }) }
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* One picker row, or undefined when the entry carries no usable id.
|
|
54
|
+
* @param withCapability - whether this reply declares reasoning metadata at
|
|
55
|
+
* all; the flat discovery list does not, so its rows stay silent about it.
|
|
56
|
+
*/
|
|
57
|
+
function rowOf(model: Record<string, unknown>, provider: string | undefined, withCapability: boolean): ExecutionModelOption | undefined {
|
|
58
|
+
const id = text(model.id)
|
|
59
|
+
if (id === undefined) return undefined
|
|
60
|
+
const reasoning = withCapability ? reasoningOf(model) : undefined
|
|
61
|
+
return {
|
|
62
|
+
id: provider === undefined ? id : `${provider}/${id}`,
|
|
63
|
+
name: text(model.name) ?? id,
|
|
64
|
+
...(provider === undefined ? {} : { provider }),
|
|
65
|
+
...(reasoning === undefined ? {} : { reasoning }),
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Normalize a `session/modelCatalog` reply into picker rows: one row per model
|
|
71
|
+
* of every provider group, keyed by the qualified route an execution pins.
|
|
72
|
+
* @param value - the raw catalog reply.
|
|
73
|
+
* @returns the rows, empty when the reply is unreadable.
|
|
74
|
+
*/
|
|
75
|
+
export function modelOptionsFromCatalog(value: unknown): ExecutionModelOption[] {
|
|
76
|
+
const options: ExecutionModelOption[] = []
|
|
77
|
+
const groups = record(value)?.groups
|
|
78
|
+
for (const entry of Array.isArray(groups) ? groups : []) {
|
|
79
|
+
const group = record(entry)
|
|
80
|
+
if (group === undefined) continue
|
|
81
|
+
const provider = text(group.id) ?? text(group.provider)
|
|
82
|
+
for (const item of Array.isArray(group.models) ? group.models : []) {
|
|
83
|
+
const model = record(item)
|
|
84
|
+
if (model === undefined) continue
|
|
85
|
+
const row = rowOf(model, provider, true)
|
|
86
|
+
if (row !== undefined) options.push(row)
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
return options
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Normalize the flat `llm/discoverModels` list into picker rows. That reply
|
|
94
|
+
* names models only, so every row leaves the reasoning capability unknown.
|
|
95
|
+
* @param value - the raw discovery list.
|
|
96
|
+
* @returns the rows, empty when the list is unreadable.
|
|
97
|
+
*/
|
|
98
|
+
export function modelOptionsFromDiscoveredModels(value: unknown): ExecutionModelOption[] {
|
|
99
|
+
const options: ExecutionModelOption[] = []
|
|
100
|
+
for (const item of Array.isArray(value) ? value : []) {
|
|
101
|
+
const model = record(item)
|
|
102
|
+
if (model === undefined) continue
|
|
103
|
+
const row = rowOf(model, undefined, false)
|
|
104
|
+
if (row !== undefined) options.push(row)
|
|
105
|
+
}
|
|
106
|
+
return options
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* The picker rows of a model roster: the qualified `provider/model` route as
|
|
111
|
+
* the value a pin stores, and the provider beside the model name so two
|
|
112
|
+
* providers offering one model name stay distinguishable.
|
|
113
|
+
* @param models - the roster rows.
|
|
114
|
+
* @returns the choice rows, in roster order.
|
|
115
|
+
*/
|
|
116
|
+
export function modelChoiceRows(models: readonly ExecutionModelOption[]): Array<{ value: string; label: string }> {
|
|
117
|
+
return models.map(model => ({
|
|
118
|
+
value: model.id,
|
|
119
|
+
label: (model.provider === undefined ? '' : model.provider + ' · ') + (model.name ?? model.id),
|
|
120
|
+
}))
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* The capability row the roster publishes for one model route.
|
|
125
|
+
* @returns undefined when no model is pinned, or the roster does not describe
|
|
126
|
+
* the pinned one — an unknown model is not the same as a model without
|
|
127
|
+
* reasoning.
|
|
128
|
+
*/
|
|
129
|
+
export function reasoningOfModel(
|
|
130
|
+
models: readonly ExecutionModelOption[],
|
|
131
|
+
modelId: string,
|
|
132
|
+
): ExecutionModelOption['reasoning'] | undefined {
|
|
133
|
+
if (modelId === '') return undefined
|
|
134
|
+
return models.find(item => item.id === modelId)?.reasoning
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* The effort ids the picker offers for a model, in catalog order.
|
|
139
|
+
* @returns an empty list when the model is unpinned, unknown, declares no
|
|
140
|
+
* reasoning, or publishes no selectable level.
|
|
141
|
+
*/
|
|
142
|
+
export function reasoningEffortsFor(models: readonly ExecutionModelOption[], modelId: string): string[] {
|
|
143
|
+
const reasoning = reasoningOfModel(models, modelId)
|
|
144
|
+
return reasoning?.supported === true ? [...reasoning.efforts ?? []] : []
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/** Whether the effort picker may be operated: a pinned model with declared, selectable levels. */
|
|
148
|
+
export function reasoningPickerEnabled(models: readonly ExecutionModelOption[], modelId: string): boolean {
|
|
149
|
+
return reasoningEffortsFor(models, modelId).length > 0
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* The effort to keep when the picked model changes. Only a model that declares
|
|
154
|
+
* its levels can invalidate one: a model the roster cannot describe keeps the
|
|
155
|
+
* stored value, because the roster has no grounds to call it invalid.
|
|
156
|
+
* @param effort - the effort currently stored/selected ('' = follow the default).
|
|
157
|
+
* @returns the effort to keep.
|
|
158
|
+
*/
|
|
159
|
+
export function reconcileReasoningEffort(
|
|
160
|
+
models: readonly ExecutionModelOption[],
|
|
161
|
+
modelId: string,
|
|
162
|
+
effort: string,
|
|
163
|
+
): string {
|
|
164
|
+
if (effort === '') return ''
|
|
165
|
+
const reasoning = reasoningOfModel(models, modelId)
|
|
166
|
+
if (reasoning === undefined) return modelId === '' ? '' : effort
|
|
167
|
+
return reasoning.efforts?.includes(effort) === true ? effort : ''
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* The copy explaining why the picker is in its current state. One lookup for
|
|
172
|
+
* both editors, so the create dialog and the detail page never drift apart.
|
|
173
|
+
*/
|
|
174
|
+
export function reasoningHintKey(models: readonly ExecutionModelOption[], modelId: string): TaskBoardKey {
|
|
175
|
+
if (modelId === '') return 'exec.reasoning.hint.pickModel'
|
|
176
|
+
if (reasoningOfModel(models, modelId) === undefined) return 'exec.reasoning.hint.unknownModel'
|
|
177
|
+
return reasoningPickerEnabled(models, modelId) ? 'exec.reasoning.hint.inherit' : 'exec.reasoning.hint.noEfforts'
|
|
178
|
+
}
|
|
@@ -42,3 +42,35 @@ export function inheritPresetLabel(presets: readonly ExecutionPresetOption[]): s
|
|
|
42
42
|
if (fallback === undefined) return t('exec.mode.inherit')
|
|
43
43
|
return t('exec.mode.inheritWith', { preset: presetLabel(fallback) })
|
|
44
44
|
}
|
|
45
|
+
|
|
46
|
+
/** One preset row a picker renders: the stored id, its label, and whether it may be picked. */
|
|
47
|
+
export interface PresetChoiceRow {
|
|
48
|
+
/** The value a pin stores. */
|
|
49
|
+
id: string
|
|
50
|
+
/** Display label, already carrying the default and unavailable suffixes. */
|
|
51
|
+
label: string
|
|
52
|
+
/** True when the preset cannot compose a session: the row is shown but not selectable. */
|
|
53
|
+
disabled: boolean
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* The picker rows of one roster, split into the built-in and the user presets.
|
|
58
|
+
* Both pickers render these rows — the create dialog's mode select and the
|
|
59
|
+
* settings card's default-preset field — so their wording and their
|
|
60
|
+
* unavailable rows cannot drift apart.
|
|
61
|
+
* @param presets - the roster options.
|
|
62
|
+
* @returns one row list per group, in roster order.
|
|
63
|
+
*/
|
|
64
|
+
export function presetChoiceRows(presets: readonly ExecutionPresetOption[]): { builtin: PresetChoiceRow[]; custom: PresetChoiceRow[] } {
|
|
65
|
+
const row = (preset: ExecutionPresetOption): PresetChoiceRow => ({
|
|
66
|
+
id: preset.id,
|
|
67
|
+
label: presetLabel(preset)
|
|
68
|
+
+ (preset.isDefault ? t('exec.mode.defaultSuffix') : '')
|
|
69
|
+
+ (preset.broken !== undefined ? t('exec.mode.brokenSuffix') : ''),
|
|
70
|
+
disabled: preset.broken !== undefined,
|
|
71
|
+
})
|
|
72
|
+
return {
|
|
73
|
+
builtin: presets.filter(preset => isBuiltinPreset(preset.id)).map(row),
|
|
74
|
+
custom: presets.filter(preset => !isBuiltinPreset(preset.id)).map(row),
|
|
75
|
+
}
|
|
76
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The agent-preset roster the execution-target pickers read: one read of the
|
|
3
|
+
* generated api-remotes face, and its normalization into the option rows the
|
|
4
|
+
* board controller and the settings card's new-card-defaults topic both
|
|
5
|
+
* consume.
|
|
6
|
+
*
|
|
7
|
+
* The roster is a runtime fact of the deployment, not ledger state. The
|
|
8
|
+
* namespace mounts with the official api-remotes assembly, so the read itself
|
|
9
|
+
* is wired through a scoped inject at the call site (see src/client/index.ts);
|
|
10
|
+
* this module owns only the shape both consumers agree on, so neither has to
|
|
11
|
+
* know how the wire face answers.
|
|
12
|
+
*/
|
|
13
|
+
import type { ClientRemote } from '@deepseek-ai/dsh-api-remotes/client'
|
|
14
|
+
import type { ExecutionPresetOption } from '../../core/controller.ts'
|
|
15
|
+
|
|
16
|
+
/** The generated Remote namespace the mode pickers read their roster from. */
|
|
17
|
+
export type PresetRemote = ClientRemote['agentPresets']
|
|
18
|
+
|
|
19
|
+
/** One agent-preset row the wire face answers with. */
|
|
20
|
+
export interface PresetRosterRow {
|
|
21
|
+
id: string
|
|
22
|
+
name?: string
|
|
23
|
+
description?: string
|
|
24
|
+
/** Why this preset cannot compose a session; absent when it can. */
|
|
25
|
+
broken?: string
|
|
26
|
+
isDefault: boolean
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Read the agent-preset roster of one deployment. The caller passes the
|
|
31
|
+
* namespace resolved inside its scoped inject, so a host that serves no such
|
|
32
|
+
* namespace never reaches here; a host that serves it answers `ok: false`
|
|
33
|
+
* instead, which leaves the caller's options untouched.
|
|
34
|
+
* @param remote - the `remote.agentPresets` namespace.
|
|
35
|
+
* @returns the roster, or a failed read the caller ignores.
|
|
36
|
+
*/
|
|
37
|
+
export async function readPresetRoster(remote: PresetRemote): Promise<{ ok: boolean; presets: readonly PresetRosterRow[] }> {
|
|
38
|
+
const response = await remote.list()
|
|
39
|
+
if (!response.ok) return { ok: false, presets: [] }
|
|
40
|
+
return { ok: true, presets: response.value.presets }
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Normalize one roster into the picker rows the UI consumes. A preset that
|
|
45
|
+
* cannot compose a session keeps its `broken` reason: the pickers show it and
|
|
46
|
+
* disable it, so a missing preset never looks like a missing roster.
|
|
47
|
+
* @param presets - the roster rows.
|
|
48
|
+
* @returns the picker options.
|
|
49
|
+
*/
|
|
50
|
+
export function presetOptionsFromRoster(presets: readonly PresetRosterRow[]): ExecutionPresetOption[] {
|
|
51
|
+
return presets.map(preset => ({
|
|
52
|
+
id: preset.id,
|
|
53
|
+
name: preset.name,
|
|
54
|
+
description: preset.description,
|
|
55
|
+
broken: preset.broken,
|
|
56
|
+
isDefault: preset.isDefault,
|
|
57
|
+
}))
|
|
58
|
+
}
|
|
@@ -23,6 +23,9 @@ export const VERIFICATION_PHASE_KEY: Record<VerificationPhase, TaskBoardKey> = {
|
|
|
23
23
|
repairing: 'running.repairing',
|
|
24
24
|
passed: 'running.verificationPassed',
|
|
25
25
|
failed: 'running.verificationFailed',
|
|
26
|
+
// Held for a human: the judge produced no usable veto, so this is NOT the
|
|
27
|
+
// quality-failure label.
|
|
28
|
+
invalid: 'running.verificationInvalid',
|
|
26
29
|
}
|
|
27
30
|
|
|
28
31
|
/**
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Team-run promotion for a parent card that just gained a subtask. A card with
|
|
3
|
+
* subtasks is exactly what the Agent Team mode is for, so the two join actions
|
|
4
|
+
* (create a subtask, link an existing task) opt the parent in — but only after
|
|
5
|
+
* the Host confirmed the join, and only where the deployment serves Agent
|
|
6
|
+
* Teams. A refused join changes nothing, and a parent that already opted in is
|
|
7
|
+
* left alone rather than rewritten.
|
|
8
|
+
*
|
|
9
|
+
* Nothing here runs on mount or from a render effect: an existing card is only
|
|
10
|
+
* ever touched by the user's own join action, so switching the option off by
|
|
11
|
+
* hand stays off.
|
|
12
|
+
*/
|
|
13
|
+
import type { BoardController } from '../../core/controller.ts'
|
|
14
|
+
import type { TaskRecord } from '../../core/tasks.ts'
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Whether a join would promote this parent: the deployment serves Agent Teams
|
|
18
|
+
* and the parent has not opted in yet.
|
|
19
|
+
* @param controller - the board controller (the snapshot carries both facts).
|
|
20
|
+
* @param parent - the task that gained a subtask, if there is one.
|
|
21
|
+
*/
|
|
22
|
+
export function parentAwaitsTeamRunPromotion(controller: BoardController, parent: TaskRecord | undefined): boolean {
|
|
23
|
+
if (parent === undefined) return false
|
|
24
|
+
const snapshot = controller.getSnapshot()
|
|
25
|
+
if (snapshot.host?.teamRunAvailable !== true) return false
|
|
26
|
+
// The freshest record wins: the card may have been edited since this modal
|
|
27
|
+
// opened with its own copy.
|
|
28
|
+
return (snapshot.tasks.find(task => task.id === parent.id) ?? parent).teamRun !== true
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Opt the parent in after the Host confirmed the join.
|
|
33
|
+
* @returns true when the opt-in was written; false when it did not apply.
|
|
34
|
+
*/
|
|
35
|
+
export async function promoteParentForTeamRun(controller: BoardController, parent: TaskRecord | undefined): Promise<boolean> {
|
|
36
|
+
if (parent === undefined) return false
|
|
37
|
+
if (!parentAwaitsTeamRunPromotion(controller, parent)) return false
|
|
38
|
+
return await controller.updateTask(parent.id, { teamRun: true })
|
|
39
|
+
}
|