iterate-plugin 2.8.3 → 2.9.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/dist/skill-prompt.js +64 -26
- package/dist/tools/checkpoint.js +16 -1
- package/dist/tools/context.js +121 -2
- package/dist/tools/decision-log.js +3 -1
- package/lib/client.js +113 -0
- package/lib/parse.js +118 -0
- package/package.json +3 -3
- package/src/client/index.ts +27 -0
- package/src/skill-prompt.ts +64 -26
- package/src/tools/checkpoint.ts +19 -1
- package/src/tools/context.ts +138 -2
- package/src/tools/decision-log.ts +3 -1
- package/src/types.ts +7 -0
package/dist/skill-prompt.js
CHANGED
|
@@ -12,14 +12,14 @@ You have the iterate plugin installed, which registers these tools:
|
|
|
12
12
|
- \`iterate_config\` — read iterate.config.yaml (dimensions, validation commands, personalization) or write a validated partial update (operation:"write", with automatic backup + rollback)
|
|
13
13
|
- \`iterate_validate\` — run a whitelisted validation command
|
|
14
14
|
- \`iterate_decision_log\` — append to the decision log, or read entries back for review
|
|
15
|
-
- \`iterate_context\` — read SKILL.md / ITERATE.md project context
|
|
15
|
+
- \`iterate_context\` — read SKILL.md / ITERATE.md project context; also relays user-attached image metadata (e.g. UI screenshots, error dialogs) so reviewers can treat them as visual evidence
|
|
16
16
|
- \`iterate_review\` — deterministic review engine: \`plan\` builds the review plan (for \`review.scope: changed-only\`, it resolves the git-diff file set against \`git.target_branch\` and auto-falls back to \`full\` when nothing changed); \`aggregate\` dedupes/merges findings, validates every finding against the findings schema when \`reviewer.output_schema_validation\` is on (dropping invalid entries and reporting them via \`schemaValidation\`), and computes convergence; \`meta-review\` audits a built report for internal consistency (counts, buckets, sorting, convergence math) and returns a final report with an \`approved\` / \`needs_revision\` verdict. Purely computational.
|
|
17
17
|
- \`iterate_triage\` — manage "known_intentional" entries in the config (list / apply, with dedupe + backup + rollback)
|
|
18
18
|
- \`iterate_fix\` — apply ONE atomic fix: backs up the file, enforces the atomic max_lines threshold, writes the new content, and records the fix (id + diff summary) in \`.iterate/fixes/registry.json\`
|
|
19
19
|
- \`iterate_diff\` — show the accumulated diff for a fixed file (vs its original backup) or a per-file summary of all fixes
|
|
20
20
|
- \`iterate_rollback\` — revert a fix by id: restore the file from its backup, remove the fix from the registry, log a \`revert\` entry. Use when a round's validation fails
|
|
21
21
|
- \`iterate_checkpoint\` — save / load / clear an iteration checkpoint (\`.iterate/checkpoint.json\`) so a long run can resume where it left off
|
|
22
|
-
- \`iterate_status\` — summarize the current run: mode, round, fixes applied, architectural remaining, decision-log size, checkpoint presence
|
|
22
|
+
- \`iterate_status\` — summarize the current run: mode, round, fixes applied, architectural remaining, decision-log size, checkpoint presence, and whether the run was interrupted (a checkpoint left on disk means the previous run was interrupted and can be resumed)
|
|
23
23
|
- \`iterate_history\` — inspect the runtime state in detail: decision-log entries and applied fixes (optionally scoped to a round or a fixed file)
|
|
24
24
|
- \`iterate_prune\` — remove stale runtime artifacts (\`.iterate/\` entries). Defaults to a read-only dry-run that reports what WOULD be removed; pass \`dryRun:false\` to actually prune.
|
|
25
25
|
|
|
@@ -30,13 +30,26 @@ When the user asks to review or iterate on the project (e.g. "review this projec
|
|
|
30
30
|
|
|
31
31
|
### Workflow script contract
|
|
32
32
|
Write a plain-JS script (top-level await, ends with \`return <json>\`). Available globals:
|
|
33
|
-
- \`agent(prompt, opts?): Promise<value>\` — spawn a subagent. \`opts.schema\` gives structured output (object-rooted JSON Schema: type/properties/required/additionalProperties/items/enum/const/oneOf only). Resolves \`null\` on child failure. Other opts: \`label\`, \`phase\`.
|
|
33
|
+
- \`agent(prompt, opts?): Promise<value>\` — spawn a subagent. \`opts.schema\` gives structured output (object-rooted JSON Schema: type/properties/required/additionalProperties/items/enum/const/oneOf only). Resolves \`null\` on child failure. Other opts: \`label\`, \`phase\`. Backend selection (optional): pass \`provider\` (e.g. \`"codex"\`, \`"claude"\`, \`"default"\`) to route the sub-agent to a specific provider backend, and/or \`model\` to pin a model id. When omitted, the sub-agent uses the same provider/model as the parent session.
|
|
34
34
|
- \`parallel(thunks): Promise<value[]>\` — run zero-arg async functions concurrently, await all.
|
|
35
35
|
- \`phase(title)\`, \`log(message)\` — progress narration.
|
|
36
36
|
- \`args\` — the args object passed to the workflow tool.
|
|
37
37
|
|
|
38
38
|
The script CANNOT call tools directly. Subagents are the ones who call tools.
|
|
39
39
|
|
|
40
|
+
### Sub-agent backend selection
|
|
41
|
+
Every \`agent()\` call may carry a backend hint via \`opts.provider\` / \`opts.model\`. Use it deliberately to balance cost, speed, and reliability:
|
|
42
|
+
- **Reviewers** (many, run in parallel, read-only, benefit from strict JSON): prefer a fast/cheap model when one is configured; otherwise omit the hint and inherit the session backend.
|
|
43
|
+
- **Fixers / aggregators** (few, must be reliable and follow tool results exactly): keep them on the parent's default backend unless a specific provider is known-good.
|
|
44
|
+
- **Never invent a provider/model name.** Pass a hint ONLY when the deployment actually registers that adapter (see \`ih config\` / the configured provider list). When in doubt, omit \`provider\`/\`model\` entirely — the sub-agent then runs on the same backend as the parent session, which is always a safe default.
|
|
45
|
+
- The optional \`args.subagentProvider\` / \`args.subagentModel\` allow the caller to override the whole run's sub-agent backend from the invocation; the canonical scripts below read them and spread the hint onto every spawned sub-agent (reviewers, fixers, validators, aggregators).
|
|
46
|
+
|
|
47
|
+
### User-attached image evidence
|
|
48
|
+
The user may attach images to the conversation (UI screenshots, error dialogs, design references, logs-as-pictures). When they do:
|
|
49
|
+
- You see those images natively in the session. Capture their metadata and pass it into the workflow via \`args.attachments\` — an array of objects, each with optional \`name\`, \`mediaType\`, \`width\`, \`height\`, and a \`note\` describing what the image shows and why it matters for this review.
|
|
50
|
+
- The canonical scripts below read \`args.attachments\` and relay them into every reviewer prompt so reviewers treat the attached visuals as evidence (e.g. "the screenshot in this message shows the broken layout the review should reproduce").
|
|
51
|
+
- If a reviewer needs the images relayed explicitly, it can call \`iterate_context\` with \`attachments\` to get the normalized image descriptions in its context. Never fabricate an attachment — only relay images the user actually attached.
|
|
52
|
+
|
|
40
53
|
### Dry-run mode workflow (pure review — the ONLY mode that never touches files)
|
|
41
54
|
This is iterate's read-only health-check: repeated review rounds until findings converge,
|
|
42
55
|
then produce an auditable report, then audit the report itself (meta-review) and give a
|
|
@@ -46,9 +59,15 @@ Canonical script — reproduce this structure exactly (adjust dims via the plan)
|
|
|
46
59
|
|
|
47
60
|
\`\`\`js
|
|
48
61
|
phase('plan')
|
|
62
|
+
// Optional per-run backend override for sub-agents (omit to inherit the session backend).
|
|
63
|
+
const subAgentProvider = (args && args.subagentProvider) || undefined
|
|
64
|
+
const subAgentModel = (args && args.subagentModel) || undefined
|
|
65
|
+
const backend = Object.assign({}, subAgentProvider ? { provider: subAgentProvider } : {}, subAgentModel ? { model: subAgentModel } : {})
|
|
66
|
+
// User-attached image evidence relayed into reviewer prompts (metadata only).
|
|
67
|
+
const attachments = (args && Array.isArray(args.attachments)) ? args.attachments : []
|
|
49
68
|
const planRes = await agent(
|
|
50
69
|
'Call iterate_review({operation:"plan", mode:"dry-run"}) and return the plan JSON.',
|
|
51
|
-
{ label: 'review:plan' }
|
|
70
|
+
Object.assign({ label: 'review:plan' }, backend)
|
|
52
71
|
)
|
|
53
72
|
const plan = (planRes && planRes.plan) ? planRes.plan : null
|
|
54
73
|
if (!plan || !Array.isArray(plan.dimensions)) throw new Error('plan failed: iterate_review did not return a valid plan')
|
|
@@ -70,16 +89,18 @@ for (let r = 1; r <= maxRounds; r++) {
|
|
|
70
89
|
? '\\nSTRICT JSON REQUIRED: your previous output failed schema validation. Return ONLY a JSON object {"findings":[...]} where EVERY finding has dimension, file, line (non-negative integer; 0 = whole-file), severity (critical|high|medium|low), summary, failure_scenario, suggested_fix, is_atomic (boolean).'
|
|
71
90
|
: ''
|
|
72
91
|
const raw = await parallel(dims.map(dim => () => agent(
|
|
73
|
-
'Review dimension "' + dim + '".
|
|
92
|
+
'Review dimension "' + dim + '".' +
|
|
93
|
+
(attachments.length > 0 ? ' User-attached images are part of the evidence (reproduce/verify against them): ' + JSON.stringify(attachments) + '.' : '') +
|
|
94
|
+
' Already-known findings (do NOT re-report): ' +
|
|
74
95
|
JSON.stringify(known) + nudge + '\\nReturn the findings JSON object.',
|
|
75
|
-
{ label: 'review:' + dim + ':r' + r, schema: plan.dimensions.find(x => x.id === dim).findingsSchema }
|
|
96
|
+
Object.assign({ label: 'review:' + dim + ':r' + r, schema: plan.dimensions.find(x => x.id === dim).findingsSchema }, backend)
|
|
76
97
|
)))
|
|
77
98
|
const thisRound = { round: r, findings: [].concat(...raw.map(x => x && x.findings ? x.findings : [])) }
|
|
78
99
|
if (rounds.length >= r) rounds[r - 1] = thisRound; else rounds.push(thisRound)
|
|
79
100
|
// Deterministic aggregate: cross-round dedupe + known_intentional filter + severity sort.
|
|
80
101
|
agg = await agent(
|
|
81
102
|
'Call iterate_review({operation:"aggregate", mode:"dry-run", rounds:' + JSON.stringify(rounds) + ', maxReviewRounds:' + maxRounds + ', knownIntentional:' + JSON.stringify(knownIntentional) + '}) and return the report JSON.',
|
|
82
|
-
{ label: 'review:aggregate:r' + r }
|
|
103
|
+
Object.assign({ label: 'review:aggregate:r' + r }, backend)
|
|
83
104
|
)
|
|
84
105
|
// reviewer.output_schema_validation (default on): aggregate returns per-round
|
|
85
106
|
// schemaValidation; retry the just-finished round (≤2 times) when invalid.
|
|
@@ -103,20 +124,20 @@ for (let r = 1; r <= maxRounds; r++) {
|
|
|
103
124
|
phase('report')
|
|
104
125
|
const finalAgg = await agent(
|
|
105
126
|
'Call iterate_review({operation:"aggregate", mode:"dry-run", rounds:' + JSON.stringify(rounds) + ', maxReviewRounds:' + maxRounds + ', knownIntentional:' + JSON.stringify(knownIntentional) + '}) and return the report JSON.',
|
|
106
|
-
{ label: 'review:aggregate:final' }
|
|
127
|
+
Object.assign({ label: 'review:aggregate:final' }, backend)
|
|
107
128
|
)
|
|
108
129
|
const report = (finalAgg && finalAgg.report) ? finalAgg.report : null
|
|
109
130
|
if (!report || !report.convergence) throw new Error('aggregate failed: no valid report was produced')
|
|
110
131
|
await agent(
|
|
111
132
|
'Call iterate_decision_log({operation:"append", type:"report", round:' + report.convergence.totalRounds + ', data:{mode:"dry-run", totalFindings:' + report.summary.totalFindings + '}})',
|
|
112
|
-
{ label: 'review:log' }
|
|
133
|
+
Object.assign({ label: 'review:log' }, backend)
|
|
113
134
|
)
|
|
114
135
|
|
|
115
136
|
phase('meta-review')
|
|
116
137
|
// Audit the report itself for internal consistency, then produce the final report.
|
|
117
138
|
const metaRes = await agent(
|
|
118
139
|
'Call iterate_review({operation:"meta-review", report:' + JSON.stringify(report) + '}) and return the finalReport JSON.',
|
|
119
|
-
{ label: 'review:meta' }
|
|
140
|
+
Object.assign({ label: 'review:meta' }, backend)
|
|
120
141
|
)
|
|
121
142
|
const finalReport = metaRes && metaRes.finalReport ? metaRes.finalReport : null
|
|
122
143
|
const metaAudit = finalReport && finalReport.metaReview ? finalReport.metaReview : null
|
|
@@ -152,26 +173,42 @@ Set \`args.mode = "normal"\`. Loop: resume → plan → parallel review ×N →
|
|
|
152
173
|
Canonical script — reproduce this structure exactly (adjust dims via the plan):
|
|
153
174
|
|
|
154
175
|
\`\`\`js
|
|
155
|
-
// args = { mode: "normal", maxRounds? }
|
|
176
|
+
// args = { mode: "normal", maxRounds?, subagentProvider?, subagentModel?, attachments? }
|
|
177
|
+
// Optional per-run backend override for sub-agents (omit to inherit the session backend).
|
|
178
|
+
const subAgentProvider = (args && args.subagentProvider) || undefined
|
|
179
|
+
const subAgentModel = (args && args.subagentModel) || undefined
|
|
180
|
+
const backend = Object.assign({}, subAgentProvider ? { provider: subAgentProvider } : {}, subAgentModel ? { model: subAgentModel } : {})
|
|
181
|
+
// User-attached image evidence relayed into reviewer prompts (metadata only).
|
|
182
|
+
const attachments = (args && Array.isArray(args.attachments)) ? args.attachments : []
|
|
156
183
|
phase('resume')
|
|
157
184
|
// If a previous run was interrupted, resume from its checkpoint instead of restarting.
|
|
158
185
|
const ckRes = await agent(
|
|
159
186
|
'Call iterate_checkpoint({ operation: "load" }) and return the checkpoint JSON.',
|
|
160
|
-
{ label: 'checkpoint:load' }
|
|
187
|
+
Object.assign({ label: 'checkpoint:load' }, backend)
|
|
161
188
|
)
|
|
162
189
|
const checkpoint = (ckRes && ckRes.checkpoint) ? ckRes.checkpoint : null
|
|
163
190
|
const startRound = (checkpoint && typeof checkpoint.round === 'number') ? checkpoint.round + 1 : 1
|
|
191
|
+
// Track how many times this checkpoint has already been resumed (interruption recovery).
|
|
192
|
+
const resumeCount = (checkpoint && typeof checkpoint.resumeCount === 'number') ? checkpoint.resumeCount : 0
|
|
193
|
+
if (checkpoint) {
|
|
194
|
+
// A previous run left a checkpoint — record the recovery so the decision log
|
|
195
|
+
// shows the resume, then continue where it left off.
|
|
196
|
+
await agent(
|
|
197
|
+
'Call iterate_decision_log({operation:"append", type:"resume", round:' + startRound + ', data:{resumedFromRound:' + checkpoint.round + ', resumeCount:' + (resumeCount + 1) + '}})',
|
|
198
|
+
Object.assign({ label: 'log:resume' }, backend)
|
|
199
|
+
)
|
|
200
|
+
}
|
|
164
201
|
|
|
165
202
|
phase('plan')
|
|
166
203
|
const configRes = await agent(
|
|
167
204
|
'Call iterate_config({ validate: true }) and return the config JSON.',
|
|
168
|
-
{ label: 'config:read' }
|
|
205
|
+
Object.assign({ label: 'config:read' }, backend)
|
|
169
206
|
)
|
|
170
207
|
const cfg = (configRes && configRes.config) ? configRes.config : null
|
|
171
208
|
const atomicMaxLines = (cfg && cfg.atomic && cfg.atomic.max_lines) ? cfg.atomic.max_lines : 20
|
|
172
209
|
const planRes = await agent(
|
|
173
210
|
'Call iterate_review({operation:"plan", mode:"normal", maxReviewRounds:' + (args.maxRounds || 3) + '}) and return the plan JSON.',
|
|
174
|
-
{ label: 'review:plan' }
|
|
211
|
+
Object.assign({ label: 'review:plan' }, backend)
|
|
175
212
|
)
|
|
176
213
|
const plan = (planRes && planRes.plan) ? planRes.plan : null
|
|
177
214
|
if (!plan || !Array.isArray(plan.dimensions)) throw new Error('plan failed: iterate_review did not return a valid plan')
|
|
@@ -198,8 +235,9 @@ for (let r = startRound; r <= maxRounds; r++) {
|
|
|
198
235
|
: ''
|
|
199
236
|
const raw = await parallel(dims.map(dim => () => agent(
|
|
200
237
|
'Review dimension "' + dim + '" on the CURRENT code state (previous atomic findings are fixed). ' +
|
|
238
|
+
(attachments.length > 0 ? ' User-attached images are part of the evidence (reproduce/verify against them): ' + JSON.stringify(attachments) + '.' : '') +
|
|
201
239
|
'Do NOT re-report already-known architectural findings: ' + JSON.stringify(architectural) + nudge + '\\nReturn the findings JSON object.',
|
|
202
|
-
{ label: 'review:' + dim + ':r' + r, schema: plan.dimensions.find(x => x.id === dim).findingsSchema }
|
|
240
|
+
Object.assign({ label: 'review:' + dim + ':r' + r, schema: plan.dimensions.find(x => x.id === dim).findingsSchema }, backend)
|
|
203
241
|
)))
|
|
204
242
|
const thisRound = { round: r, findings: [].concat(...raw.map(x => x && x.findings ? x.findings : [])) }
|
|
205
243
|
if (rounds.length >= r) rounds[r - 1] = thisRound; else rounds.push(thisRound)
|
|
@@ -209,7 +247,7 @@ for (let r = startRound; r <= maxRounds; r++) {
|
|
|
209
247
|
// show a running "fixes applied" metric for normal mode.
|
|
210
248
|
agg = await agent(
|
|
211
249
|
'Call iterate_review({operation:"aggregate", mode:"normal", rounds:' + JSON.stringify([thisRound]) + ', knownIntentional:' + JSON.stringify(knownIntentional) + ', fixedCount:' + fixedCount + '}) and return the report JSON.',
|
|
212
|
-
{ label: 'review:aggregate:r' + r }
|
|
250
|
+
Object.assign({ label: 'review:aggregate:r' + r }, backend)
|
|
213
251
|
)
|
|
214
252
|
// reviewer.output_schema_validation (default on): aggregate returns per-round
|
|
215
253
|
// schemaValidation; retry the just-finished round (≤2 times) when invalid.
|
|
@@ -239,12 +277,12 @@ for (let r = startRound; r <= maxRounds; r++) {
|
|
|
239
277
|
'iterate_fix({ file: "' + file + '", content: <full new file content>, finding: <that finding>, round: ' + r + ' }). ' +
|
|
240
278
|
'Apply the findings IN ORDER. After all fixes, call iterate_diff({ file: "' + file + '" }) to verify the accumulated diff. ' +
|
|
241
279
|
'Findings: ' + JSON.stringify(byFile[file]) + '. Return the array of {id, ok, error} per iterate_fix call.',
|
|
242
|
-
{ label: 'fix:' + file, phase: 'fix', schema: {
|
|
280
|
+
Object.assign({ label: 'fix:' + file, phase: 'fix', schema: {
|
|
243
281
|
type: 'object', additionalProperties: false,
|
|
244
282
|
properties: {
|
|
245
283
|
fixes: { type: 'array', items: { type: 'object', additionalProperties: false, properties: { id: { type: 'string' }, ok: { type: 'boolean' }, error: { type: 'string' } }, required: ['id', 'ok'] } }
|
|
246
284
|
},
|
|
247
|
-
required: ['fixes'] } }
|
|
285
|
+
required: ['fixes'] } }, backend)
|
|
248
286
|
)))
|
|
249
287
|
for (const res of fixRes) {
|
|
250
288
|
if (res && Array.isArray(res.fixes)) {
|
|
@@ -266,12 +304,12 @@ for (let r = startRound; r <= maxRounds; r++) {
|
|
|
266
304
|
const valRes = await agent(
|
|
267
305
|
'Read iterate.config.yaml validation.commands, then call iterate_validate({ command: <cmd> }) for EACH configured command ' +
|
|
268
306
|
'(one tool call per command). Return all results as {command, exitCode} entries.',
|
|
269
|
-
{ label: 'validate:r' + r, phase: 'validate', schema: {
|
|
307
|
+
Object.assign({ label: 'validate:r' + r, phase: 'validate', schema: {
|
|
270
308
|
type: 'object', additionalProperties: false,
|
|
271
309
|
properties: {
|
|
272
310
|
results: { type: 'array', items: { type: 'object', additionalProperties: false, properties: { command: { type: 'string' }, exitCode: { type: 'integer' } }, required: ['command', 'exitCode'] } }
|
|
273
311
|
},
|
|
274
|
-
required: ['results'] } }
|
|
312
|
+
required: ['results'] } }, backend)
|
|
275
313
|
)
|
|
276
314
|
failedCommands = (valRes && Array.isArray(valRes.results)) ? valRes.results.filter(v => v.exitCode !== 0).map(v => v.command) : []
|
|
277
315
|
if (failedCommands.length > 0) {
|
|
@@ -280,17 +318,17 @@ for (let r = startRound; r <= maxRounds; r++) {
|
|
|
280
318
|
if (roundFixIds.length > 0) {
|
|
281
319
|
await agent(
|
|
282
320
|
'Call iterate_rollback({ id: <id> }) for EACH of these fix ids (one call per id): ' + JSON.stringify(roundFixIds) + '. Return the array of {id, ok, error}.',
|
|
283
|
-
{ label: 'rollback:r' + r, phase: 'rollback', schema: {
|
|
321
|
+
Object.assign({ label: 'rollback:r' + r, phase: 'rollback', schema: {
|
|
284
322
|
type: 'object', additionalProperties: false,
|
|
285
323
|
properties: {
|
|
286
324
|
results: { type: 'array', items: { type: 'object', additionalProperties: false, properties: { id: { type: 'string' }, ok: { type: 'boolean' }, error: { type: 'string' } }, required: ['id', 'ok'] } }
|
|
287
325
|
},
|
|
288
|
-
required: ['results'] } }
|
|
326
|
+
required: ['results'] } }, backend)
|
|
289
327
|
)
|
|
290
328
|
}
|
|
291
329
|
await agent(
|
|
292
330
|
'Call iterate_decision_log({operation:"append", type:"round_failed", round:' + r + ', data:{failedCommands:' + JSON.stringify(failedCommands) + ', rolledBack:' + roundFixIds.length + '}})',
|
|
293
|
-
{ label: 'log:failed:r' + r }
|
|
331
|
+
Object.assign({ label: 'log:failed:r' + r }, backend)
|
|
294
332
|
)
|
|
295
333
|
break
|
|
296
334
|
}
|
|
@@ -298,13 +336,13 @@ for (let r = startRound; r <= maxRounds; r++) {
|
|
|
298
336
|
await agent(
|
|
299
337
|
'Call iterate_decision_log({operation:"append", type:"review_result", round:' + r +
|
|
300
338
|
', data:{atomic:' + atomic.length + ', architectural:' + remaining.length + ', fixedSoFar:' + fixedCount + '}})',
|
|
301
|
-
{ label: 'log:r' + r }
|
|
339
|
+
Object.assign({ label: 'log:r' + r }, backend)
|
|
302
340
|
)
|
|
303
341
|
|
|
304
342
|
// Persist progress so an interrupted run can resume from the next round.
|
|
305
343
|
await agent(
|
|
306
|
-
'Call iterate_checkpoint({ operation: "save", mode: "normal", round:' + r + ', maxRounds:' + maxRounds + ', fixedCount:' + fixedCount + ', architecturalCount:' + architectural.length + ', findings:' + JSON.stringify(architectural) + ' }) and return the checkpoint JSON.',
|
|
307
|
-
{ label: 'checkpoint:save:r' + r }
|
|
344
|
+
'Call iterate_checkpoint({ operation: "save", mode: "normal", round:' + r + ', maxRounds:' + maxRounds + ', fixedCount:' + fixedCount + ', architecturalCount:' + architectural.length + ', resumeCount:' + resumeCount + ', findings:' + JSON.stringify(architectural) + ' }) and return the checkpoint JSON.',
|
|
345
|
+
Object.assign({ label: 'checkpoint:save:r' + r }, backend)
|
|
308
346
|
)
|
|
309
347
|
|
|
310
348
|
if (atomic.length === 0 && remaining.length === 0) {
|
package/dist/tools/checkpoint.js
CHANGED
|
@@ -51,6 +51,10 @@ export function validateCheckpoint(input) {
|
|
|
51
51
|
if (typeof input.architecturalCount !== 'number' || !Number.isInteger(input.architecturalCount) || input.architecturalCount < 0) {
|
|
52
52
|
return 'architecturalCount must be a non-negative integer';
|
|
53
53
|
}
|
|
54
|
+
if (input.resumeCount !== undefined &&
|
|
55
|
+
(typeof input.resumeCount !== 'number' || !Number.isInteger(input.resumeCount) || input.resumeCount < 0)) {
|
|
56
|
+
return 'resumeCount must be a non-negative integer';
|
|
57
|
+
}
|
|
54
58
|
return null;
|
|
55
59
|
}
|
|
56
60
|
/**
|
|
@@ -88,6 +92,10 @@ export function computeStatus(input) {
|
|
|
88
92
|
findingsCount: checkpoint?.findings.length ?? 0,
|
|
89
93
|
totalDecisionLogEntries: entries.length,
|
|
90
94
|
hasCheckpoint: checkpoint !== null,
|
|
95
|
+
// A checkpoint left on disk means the previous run was interrupted before
|
|
96
|
+
// it could clear it — this is the durable "interruption" signal.
|
|
97
|
+
interrupted: checkpoint !== null,
|
|
98
|
+
resumeCount: checkpoint?.resumeCount ?? 0,
|
|
91
99
|
checkpoint,
|
|
92
100
|
lastUpdated,
|
|
93
101
|
};
|
|
@@ -114,6 +122,7 @@ export function registerCheckpointTool(ctx) {
|
|
|
114
122
|
maxRounds: { type: 'integer', description: 'Required for save: total round cap.' },
|
|
115
123
|
fixedCount: { type: 'integer', description: 'Required for save: number of fixes applied so far.' },
|
|
116
124
|
architecturalCount: { type: 'integer', description: 'Required for save: architectural findings left unfixed.' },
|
|
125
|
+
resumeCount: { type: 'integer', description: 'Optional for save: how many times this checkpoint has already been resumed after an interruption (default 0).' },
|
|
117
126
|
findings: { type: 'json', description: 'Optional for save: the current deduped findings to resume from.' },
|
|
118
127
|
path: { type: 'string', description: 'Project root directory (default: current working directory).' },
|
|
119
128
|
},
|
|
@@ -161,6 +170,7 @@ export function registerCheckpointTool(ctx) {
|
|
|
161
170
|
maxRounds: args.maxRounds,
|
|
162
171
|
fixedCount: args.fixedCount,
|
|
163
172
|
architecturalCount: args.architecturalCount,
|
|
173
|
+
resumeCount: args.resumeCount,
|
|
164
174
|
});
|
|
165
175
|
if (invalid)
|
|
166
176
|
return { operation: 'save', ok: false, error: invalid };
|
|
@@ -170,6 +180,7 @@ export function registerCheckpointTool(ctx) {
|
|
|
170
180
|
maxRounds: args.maxRounds,
|
|
171
181
|
fixedCount: args.fixedCount,
|
|
172
182
|
architecturalCount: args.architecturalCount,
|
|
183
|
+
resumeCount: (typeof args.resumeCount === 'number' ? args.resumeCount : 0),
|
|
173
184
|
findings: (Array.isArray(args.findings) ? args.findings : []),
|
|
174
185
|
startedAt: readCheckpoint(projectRoot)?.startedAt ?? new Date().toISOString(),
|
|
175
186
|
updatedAt: new Date().toISOString(),
|
|
@@ -214,6 +225,8 @@ export function registerStatusTool(ctx) {
|
|
|
214
225
|
findingsCount: { type: 'integer' },
|
|
215
226
|
totalDecisionLogEntries: { type: 'integer' },
|
|
216
227
|
hasCheckpoint: { type: 'boolean' },
|
|
228
|
+
interrupted: { type: 'boolean', description: 'True when a checkpoint exists, meaning the previous run was interrupted before finishing.' },
|
|
229
|
+
resumeCount: { type: 'integer', description: 'How many times the current checkpoint has already been resumed.' },
|
|
217
230
|
lastUpdated: { type: 'string' },
|
|
218
231
|
error: { type: 'string' },
|
|
219
232
|
},
|
|
@@ -227,7 +240,7 @@ export function registerStatusTool(ctx) {
|
|
|
227
240
|
`Fixed: ${value.fixedCount} · Architectural remaining: ${value.architecturalCount}`,
|
|
228
241
|
`Findings in checkpoint: ${value.findingsCount}`,
|
|
229
242
|
`Decision-log entries: ${value.totalDecisionLogEntries}`,
|
|
230
|
-
`Checkpoint: ${value.hasCheckpoint ? 'yes' : 'no'}`,
|
|
243
|
+
`Checkpoint: ${value.hasCheckpoint ? 'yes' : 'no'}${value.interrupted ? ' (interrupted — resumable)' : ''}${value.resumeCount ? ` · resumed ${value.resumeCount}x` : ''}`,
|
|
231
244
|
value.lastUpdated ? `Last updated: ${value.lastUpdated}` : '',
|
|
232
245
|
];
|
|
233
246
|
return [{ type: 'text', text: lines.filter(Boolean).join('\n') }];
|
|
@@ -253,6 +266,8 @@ export function registerStatusTool(ctx) {
|
|
|
253
266
|
findingsCount: status.findingsCount,
|
|
254
267
|
totalDecisionLogEntries: status.totalDecisionLogEntries,
|
|
255
268
|
hasCheckpoint: status.hasCheckpoint,
|
|
269
|
+
interrupted: status.interrupted,
|
|
270
|
+
resumeCount: status.resumeCount,
|
|
256
271
|
lastUpdated: status.lastUpdated ?? undefined,
|
|
257
272
|
};
|
|
258
273
|
},
|
package/dist/tools/context.js
CHANGED
|
@@ -5,6 +5,87 @@ import { defineTool } from '@deepseek-ai/dsh-tools';
|
|
|
5
5
|
import { resolveProjectRoot } from "../config-loader.js";
|
|
6
6
|
/** How many ancestor directories we walk up looking for a SKILL.md. */
|
|
7
7
|
const MAX_SKILL_DIR_LOOKUP_DEPTH = 12;
|
|
8
|
+
/** Maximum number of image attachments relayed into the context in one call. */
|
|
9
|
+
const MAX_ATTACHMENTS = 8;
|
|
10
|
+
/**
|
|
11
|
+
* Validate and normalize one raw image-attachment entry passed by the
|
|
12
|
+
* orchestrator. The top-level model observes user-attached images in its own
|
|
13
|
+
* context (as image blocks) and relays their metadata here so reviewers get the
|
|
14
|
+
* same visual evidence. Pure — exported for unit tests.
|
|
15
|
+
*/
|
|
16
|
+
export function normalizeAttachment(raw) {
|
|
17
|
+
if (raw === null || typeof raw !== 'object' || Array.isArray(raw)) {
|
|
18
|
+
return { ok: false, error: 'attachment must be an object' };
|
|
19
|
+
}
|
|
20
|
+
const entry = raw;
|
|
21
|
+
const out = {};
|
|
22
|
+
if (entry.name !== undefined) {
|
|
23
|
+
if (typeof entry.name !== 'string' || entry.name.length > 256) {
|
|
24
|
+
return { ok: false, error: 'attachment.name must be a string (≤ 256 chars)' };
|
|
25
|
+
}
|
|
26
|
+
out.name = entry.name;
|
|
27
|
+
}
|
|
28
|
+
if (entry.mediaType !== undefined) {
|
|
29
|
+
if (typeof entry.mediaType !== 'string' || !/^image\/(png|jpeg|webp|gif)$/.test(entry.mediaType)) {
|
|
30
|
+
return { ok: false, error: 'attachment.mediaType must be image/png, image/jpeg, image/webp, or image/gif' };
|
|
31
|
+
}
|
|
32
|
+
out.mediaType = entry.mediaType;
|
|
33
|
+
}
|
|
34
|
+
for (const dim of ['width', 'height']) {
|
|
35
|
+
if (entry[dim] !== undefined) {
|
|
36
|
+
if (typeof entry[dim] !== 'number' || !Number.isInteger(entry[dim]) || entry[dim] < 0 || entry[dim] > 16384) {
|
|
37
|
+
return { ok: false, error: `attachment.${dim} must be an integer in [0, 16384]` };
|
|
38
|
+
}
|
|
39
|
+
out[dim] = entry[dim];
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
if (entry.note !== undefined) {
|
|
43
|
+
if (typeof entry.note !== 'string' || entry.note.length > 1000) {
|
|
44
|
+
return { ok: false, error: 'attachment.note must be a string (≤ 1000 chars)' };
|
|
45
|
+
}
|
|
46
|
+
out.note = entry.note;
|
|
47
|
+
}
|
|
48
|
+
return { ok: true, value: out };
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Validate a whole attachments array, dropping invalid entries.
|
|
52
|
+
* Returns the normalized list plus the reasons for any dropped entries.
|
|
53
|
+
*/
|
|
54
|
+
export function normalizeAttachments(raw) {
|
|
55
|
+
const attachments = [];
|
|
56
|
+
const errors = [];
|
|
57
|
+
if (raw === undefined || raw === null)
|
|
58
|
+
return { attachments, errors };
|
|
59
|
+
if (!Array.isArray(raw))
|
|
60
|
+
return { attachments, errors: ['attachments must be an array'] };
|
|
61
|
+
for (let i = 0; i < raw.length; i++) {
|
|
62
|
+
if (attachments.length >= MAX_ATTACHMENTS) {
|
|
63
|
+
errors.push(`attachments capped at ${MAX_ATTACHMENTS}; entry ${i} dropped`);
|
|
64
|
+
break;
|
|
65
|
+
}
|
|
66
|
+
const result = normalizeAttachment(raw[i]);
|
|
67
|
+
if (result.ok)
|
|
68
|
+
attachments.push(result.value);
|
|
69
|
+
else
|
|
70
|
+
errors.push(`attachments[${i}]: ${result.error}`);
|
|
71
|
+
}
|
|
72
|
+
return { attachments, errors };
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Render a normalized attachment as a compact text block for the model.
|
|
76
|
+
*/
|
|
77
|
+
export function renderAttachment(a, index) {
|
|
78
|
+
const bits = [`[${index + 1}]`];
|
|
79
|
+
if (a.name)
|
|
80
|
+
bits.push(a.name);
|
|
81
|
+
if (a.mediaType)
|
|
82
|
+
bits.push(a.mediaType);
|
|
83
|
+
if (typeof a.width === 'number' && typeof a.height === 'number')
|
|
84
|
+
bits.push(`${a.width}x${a.height}`);
|
|
85
|
+
if (a.note)
|
|
86
|
+
bits.push(a.note);
|
|
87
|
+
return bits.join(' · ');
|
|
88
|
+
}
|
|
8
89
|
/**
|
|
9
90
|
* The directory this source file lives in (…/src/tools). The plugin's own
|
|
10
91
|
* package root is one level up (…/src), and the skill root is typically a few
|
|
@@ -82,7 +163,9 @@ export function registerContextTool(ctx) {
|
|
|
82
163
|
'SKILL.md contains the original iterate skill instructions; it is searched in ' +
|
|
83
164
|
'the skill directory (auto-detected), the project root, or an explicit `skillDir`. ' +
|
|
84
165
|
'ITERATE.md contains the project-specific knowledge base and onboarding information. ' +
|
|
85
|
-
'
|
|
166
|
+
'Also relays user-attached image metadata (e.g. UI screenshots, error dialogs) into ' +
|
|
167
|
+
'the review context so reviewers can treat them as visual evidence. ' +
|
|
168
|
+
'Use this to understand the skill workflow, project context, and any attached visuals.',
|
|
86
169
|
parameters: {
|
|
87
170
|
files: {
|
|
88
171
|
type: 'string',
|
|
@@ -98,6 +181,24 @@ export function registerContextTool(ctx) {
|
|
|
98
181
|
description: 'Custom directory to search for SKILL.md (highest priority). ' +
|
|
99
182
|
'When omitted, SKILL.md is auto-detected from the skill directory, then the project root.',
|
|
100
183
|
},
|
|
184
|
+
attachments: {
|
|
185
|
+
type: 'array',
|
|
186
|
+
items: {
|
|
187
|
+
type: 'object',
|
|
188
|
+
additionalProperties: false,
|
|
189
|
+
properties: {
|
|
190
|
+
name: { type: 'string', description: 'Optional display name of the attached image.' },
|
|
191
|
+
mediaType: { type: 'string', enum: ['image/png', 'image/jpeg', 'image/webp', 'image/gif'], description: 'Optional media type of the image.' },
|
|
192
|
+
width: { type: 'integer', description: 'Optional intrinsic width in pixels.' },
|
|
193
|
+
height: { type: 'integer', description: 'Optional intrinsic height in pixels.' },
|
|
194
|
+
note: { type: 'string', description: 'Optional short description of what the image shows and why it matters for this review.' },
|
|
195
|
+
},
|
|
196
|
+
},
|
|
197
|
+
description: 'Optional: image attachments observed in the session (e.g. UI screenshots, error ' +
|
|
198
|
+
'dialogs, design references) relayed into the review context. The top-level model ' +
|
|
199
|
+
'sees these images natively and passes their metadata here so reviewers get the same ' +
|
|
200
|
+
'visual evidence. Up to 8 entries; invalid entries are dropped and reported.',
|
|
201
|
+
},
|
|
101
202
|
},
|
|
102
203
|
output: {
|
|
103
204
|
schema: {
|
|
@@ -110,6 +211,8 @@ export function registerContextTool(ctx) {
|
|
|
110
211
|
skillSource: { oneOf: [{ type: 'string' }, { type: 'null' }] },
|
|
111
212
|
error: { type: 'string' },
|
|
112
213
|
searched: { type: 'array', items: { type: 'string' } },
|
|
214
|
+
attachments: { type: 'array', items: { type: 'string' }, description: 'Normalized attached-image descriptions relayed to reviewers.' },
|
|
215
|
+
attachmentErrors: { type: 'array', items: { type: 'string' }, description: 'Reasons for any attachment entries that were dropped.' },
|
|
113
216
|
},
|
|
114
217
|
},
|
|
115
218
|
render: (_args, value) => {
|
|
@@ -118,9 +221,15 @@ export function registerContextTool(ctx) {
|
|
|
118
221
|
parts.push(`--- SKILL.md (${value.skillSource ?? '?source?'}) ---\n${value.skill}`);
|
|
119
222
|
if (value.project)
|
|
120
223
|
parts.push(`--- ITERATE.md ---\n${value.project}`);
|
|
121
|
-
if (
|
|
224
|
+
if (Array.isArray(value.attachments) && value.attachments.length > 0) {
|
|
225
|
+
parts.push(`--- User-attached images (${value.attachments.length}) ---\n${value.attachments.join('\n')}`);
|
|
226
|
+
}
|
|
227
|
+
if (!value.skill && !value.project && !(Array.isArray(value.attachments) && value.attachments.length > 0)) {
|
|
122
228
|
parts.push('No files found. Searched: ' + (value.searched?.join(', ') ?? 'none'));
|
|
123
229
|
}
|
|
230
|
+
if (Array.isArray(value.attachmentErrors) && value.attachmentErrors.length > 0) {
|
|
231
|
+
parts.push('Attachment warnings: ' + value.attachmentErrors.join('; '));
|
|
232
|
+
}
|
|
124
233
|
return [{ type: 'text', text: parts.join('\n\n') }];
|
|
125
234
|
},
|
|
126
235
|
},
|
|
@@ -135,6 +244,16 @@ export function registerContextTool(ctx) {
|
|
|
135
244
|
.map((s) => s.trim().toLowerCase())
|
|
136
245
|
.filter(Boolean);
|
|
137
246
|
const result = { found: true, searched: [] };
|
|
247
|
+
// Relay user-attached image metadata into the review context. The
|
|
248
|
+
// orchestrator observes attached images in the session and passes their
|
|
249
|
+
// metadata here; invalid entries are dropped with a reported reason.
|
|
250
|
+
const attachments = normalizeAttachments(args.attachments);
|
|
251
|
+
if (attachments.attachments.length > 0) {
|
|
252
|
+
result.attachments = attachments.attachments.map(renderAttachment);
|
|
253
|
+
}
|
|
254
|
+
if (attachments.errors.length > 0) {
|
|
255
|
+
result.attachmentErrors = attachments.errors;
|
|
256
|
+
}
|
|
138
257
|
if (requested.includes('skill') || requested.includes('skill.md')) {
|
|
139
258
|
// Candidate dirs in priority order: custom path → auto-detected skill
|
|
140
259
|
// root → project root. This is how "skill 目录、项目根、自定义路径"
|
|
@@ -15,6 +15,7 @@ const VALID_ENTRY_TYPES = new Set([
|
|
|
15
15
|
'validation',
|
|
16
16
|
'decision',
|
|
17
17
|
'report',
|
|
18
|
+
'resume',
|
|
18
19
|
]);
|
|
19
20
|
/**
|
|
20
21
|
* Validate a candidate (type, round, data) triple for an append operation.
|
|
@@ -101,7 +102,7 @@ export function registerDecisionLogTool(ctx) {
|
|
|
101
102
|
type: {
|
|
102
103
|
type: 'string',
|
|
103
104
|
description: 'Entry type (required for append): round_start, review_result, atomic_fix, ' +
|
|
104
|
-
'architectural_fix, revert, round_failed, validation, decision, report.',
|
|
105
|
+
'architectural_fix, revert, round_failed, validation, decision, report, resume.',
|
|
105
106
|
enum: [
|
|
106
107
|
'round_start',
|
|
107
108
|
'review_result',
|
|
@@ -112,6 +113,7 @@ export function registerDecisionLogTool(ctx) {
|
|
|
112
113
|
'validation',
|
|
113
114
|
'decision',
|
|
114
115
|
'report',
|
|
116
|
+
'resume',
|
|
115
117
|
],
|
|
116
118
|
},
|
|
117
119
|
round: {
|
package/lib/client.js
CHANGED
|
@@ -43,6 +43,101 @@ var SEVERITY_COLOR = {
|
|
|
43
43
|
medium: "#eab308",
|
|
44
44
|
low: "#6b7280"
|
|
45
45
|
};
|
|
46
|
+
function scanSessionForResume(obj, seen, maxDepth = 20) {
|
|
47
|
+
if (maxDepth <= 0) return 0;
|
|
48
|
+
if (!obj || typeof obj !== "object") return 0;
|
|
49
|
+
const s = seen || /* @__PURE__ */ new Set();
|
|
50
|
+
if (s.has(obj)) return 0;
|
|
51
|
+
s.add(obj);
|
|
52
|
+
let best = 0;
|
|
53
|
+
const direct = (
|
|
54
|
+
/** @type {Record<string, unknown>} */
|
|
55
|
+
obj
|
|
56
|
+
);
|
|
57
|
+
if (direct.type === "resume") {
|
|
58
|
+
const data = (
|
|
59
|
+
/** @type {Record<string, unknown>} */
|
|
60
|
+
direct.data || {}
|
|
61
|
+
);
|
|
62
|
+
if (typeof data.resumeCount === "number" && data.resumeCount > best) {
|
|
63
|
+
best = data.resumeCount;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
if (direct.entry && typeof direct.entry === "object") {
|
|
67
|
+
const entry = (
|
|
68
|
+
/** @type {Record<string, unknown>} */
|
|
69
|
+
direct.entry
|
|
70
|
+
);
|
|
71
|
+
if (entry.type === "resume") {
|
|
72
|
+
const data = (
|
|
73
|
+
/** @type {Record<string, unknown>} */
|
|
74
|
+
entry.data || {}
|
|
75
|
+
);
|
|
76
|
+
if (typeof data.resumeCount === "number" && data.resumeCount > best) {
|
|
77
|
+
best = data.resumeCount;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
if (Array.isArray(obj)) {
|
|
82
|
+
for (const item of obj) {
|
|
83
|
+
const found = scanSessionForResume(item, s, maxDepth - 1);
|
|
84
|
+
if (found > best) best = found;
|
|
85
|
+
}
|
|
86
|
+
return best;
|
|
87
|
+
}
|
|
88
|
+
for (const key of Object.keys(direct)) {
|
|
89
|
+
const val = direct[key];
|
|
90
|
+
if (val && typeof val === "object") {
|
|
91
|
+
const found = scanSessionForResume(val, s, maxDepth - 1);
|
|
92
|
+
if (found > best) best = found;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
return best;
|
|
96
|
+
}
|
|
97
|
+
function countSessionImages(session) {
|
|
98
|
+
if (!session || typeof session !== "object") return 0;
|
|
99
|
+
const ids = /* @__PURE__ */ new Set();
|
|
100
|
+
let count = 0;
|
|
101
|
+
const walk = (obj, depth) => {
|
|
102
|
+
if (depth <= 0 || !obj || typeof obj !== "object") return;
|
|
103
|
+
if (seen.has(obj)) return;
|
|
104
|
+
seen.add(obj);
|
|
105
|
+
const o = (
|
|
106
|
+
/** @type {Record<string, unknown>} */
|
|
107
|
+
obj
|
|
108
|
+
);
|
|
109
|
+
let ref = null;
|
|
110
|
+
if (o.type === "image" && o.attachment && typeof o.attachment === "object") {
|
|
111
|
+
ref = /** @type {Record<string, unknown>} */
|
|
112
|
+
o.attachment;
|
|
113
|
+
}
|
|
114
|
+
if (!ref && typeof o.mediaType === "string" && String(o.mediaType).startsWith("image/")) {
|
|
115
|
+
ref = o;
|
|
116
|
+
}
|
|
117
|
+
if (ref) {
|
|
118
|
+
const id = typeof ref.attachmentId === "string" ? ref.attachmentId : null;
|
|
119
|
+
if (id) {
|
|
120
|
+
if (!ids.has(id)) {
|
|
121
|
+
ids.add(id);
|
|
122
|
+
count += 1;
|
|
123
|
+
}
|
|
124
|
+
} else {
|
|
125
|
+
count += 1;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
if (Array.isArray(obj)) {
|
|
129
|
+
for (const item of obj) walk(item, depth - 1);
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
for (const key of Object.keys(o)) {
|
|
133
|
+
const val = o[key];
|
|
134
|
+
if (val && typeof val === "object") walk(val, depth - 1);
|
|
135
|
+
}
|
|
136
|
+
};
|
|
137
|
+
const seen = /* @__PURE__ */ new Set();
|
|
138
|
+
walk(session, 12);
|
|
139
|
+
return count;
|
|
140
|
+
}
|
|
46
141
|
function isReviewReport(obj) {
|
|
47
142
|
if (!obj || typeof obj !== "object") return false;
|
|
48
143
|
const o = (
|
|
@@ -827,6 +922,10 @@ var ITERATE_CSS = `
|
|
|
827
922
|
.iterate-pill { display: inline-flex; align-items: center; gap: 7px; padding: 3px 11px; border-radius: 999px; font-size: 11px; font-weight: 600; white-space: nowrap; color: var(--dsw-alias-state-success-primary); background: color-mix(in srgb, var(--dsw-alias-state-success-primary) 12%, transparent); border: 1px solid color-mix(in srgb, var(--dsw-alias-state-success-primary) 28%, transparent); }
|
|
828
923
|
.iterate-pill-dot { width: 6px; height: 6px; border-radius: 50%; background: currentColor; }
|
|
829
924
|
|
|
925
|
+
/* Interruption / resume + attachment chips (dashboard) */
|
|
926
|
+
.iterate-chip-resume { display: inline-flex; align-items: center; gap: 6px; padding: 3px 10px; border-radius: 999px; font-size: 11px; font-weight: 600; white-space: nowrap; color: var(--dsw-alias-state-warn-primary); background: color-mix(in srgb, var(--dsw-alias-state-warn-primary) 12%, transparent); border: 1px solid color-mix(in srgb, var(--dsw-alias-state-warn-primary) 28%, transparent); }
|
|
927
|
+
.iterate-chip-images { display: inline-flex; align-items: center; gap: 6px; padding: 3px 10px; border-radius: 999px; font-size: 11px; font-weight: 600; white-space: nowrap; color: var(--dsw-alias-brand-primary); background: color-mix(in srgb, var(--dsw-alias-brand-primary) 12%, transparent); border: 1px solid color-mix(in srgb, var(--dsw-alias-brand-primary) 28%, transparent); }
|
|
928
|
+
|
|
830
929
|
/* Accessibility-switch toggle */
|
|
831
930
|
.iterate-switch { position: relative; width: 42px; height: 24px; border-radius: 999px; padding: 0; cursor: pointer; background: var(--dsw-alias-bg-layer-2); border: 1px solid var(--dsw-alias-border-l1); transition: background-color 160ms ease, border-color 160ms ease; }
|
|
832
931
|
.iterate-switch:focus-visible { outline: 2px solid var(--dsw-alias-brand-primary); outline-offset: 2px; }
|
|
@@ -1010,6 +1109,18 @@ function ConvergenceDashboard(props) {
|
|
|
1010
1109
|
const stats = severityStats(report);
|
|
1011
1110
|
const dims = groupByDimension(report);
|
|
1012
1111
|
const trend = computeTrendMetrics(report);
|
|
1112
|
+
const resumeCount = scanSessionForResume(session);
|
|
1113
|
+
const imageCount = countSessionImages(session);
|
|
1114
|
+
const resumeChip = resumeCount > 0 ? React.createElement("span", {
|
|
1115
|
+
className: "iterate-chip-resume",
|
|
1116
|
+
key: "resume",
|
|
1117
|
+
title: "\u672C\u6B21\u8FED\u4EE3\u4ECE\u4E0A\u4E00\u6B21\u4E2D\u65AD\u7684\u65AD\u70B9\u7EE7\u7EED\u6267\u884C"
|
|
1118
|
+
}, `\u5DF2\u4E2D\u65AD\u6062\u590D \xD7${String(resumeCount)}`) : null;
|
|
1119
|
+
const imageChip = imageCount > 0 ? React.createElement("span", {
|
|
1120
|
+
className: "iterate-chip-images",
|
|
1121
|
+
key: "images",
|
|
1122
|
+
title: "\u4F1A\u8BDD\u4E2D\u68C0\u6D4B\u5230\u7528\u6237\u9644\u5E26\u7684\u56FE\u7247\uFF0C\u8BC4\u5BA1\u5C06\u4F5C\u4E3A\u89C6\u89C9\u8BC1\u636E\u53C2\u8003"
|
|
1123
|
+
}, `\u9644\u4EF6\u56FE\u7247 ${String(imageCount)}`) : null;
|
|
1013
1124
|
const dimBadges = Object.keys(dims).slice(0, 6).map(
|
|
1014
1125
|
(dim) => React.createElement(
|
|
1015
1126
|
"span",
|
|
@@ -1058,6 +1169,8 @@ function ConvergenceDashboard(props) {
|
|
|
1058
1169
|
stats.medium
|
|
1059
1170
|
),
|
|
1060
1171
|
fixBadge,
|
|
1172
|
+
resumeChip,
|
|
1173
|
+
imageChip,
|
|
1061
1174
|
React.createElement(TrendChart, { points: trend.points }),
|
|
1062
1175
|
...dimBadges
|
|
1063
1176
|
);
|