coding-agent-harness 1.0.4 → 1.0.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (100) hide show
  1. package/CHANGELOG.md +7 -0
  2. package/LICENSE +661 -21
  3. package/LICENSE-EXCEPTION.md +37 -0
  4. package/README.md +33 -1
  5. package/README.zh-CN.md +23 -1
  6. package/SKILL.md +9 -8
  7. package/docs-release/architecture/overview.md +1 -1
  8. package/docs-release/architecture/overview.zh-CN.md +1 -1
  9. package/docs-release/architecture/system-explainer/01-system-overview.md +217 -0
  10. package/docs-release/architecture/system-explainer/02-module-dependency.md +257 -0
  11. package/docs-release/architecture/system-explainer/03-task-lifecycle.md +304 -0
  12. package/docs-release/architecture/system-explainer/04-check-and-governance.md +239 -0
  13. package/docs-release/architecture/system-explainer/05-data-flow.md +276 -0
  14. package/docs-release/architecture/system-explainer/06-preset-and-migration.md +303 -0
  15. package/docs-release/architecture/system-explainer/README.md +67 -0
  16. package/docs-release/architecture/system-explainer/en-US/01-system-overview.md +226 -0
  17. package/docs-release/architecture/system-explainer/en-US/02-module-dependency.md +263 -0
  18. package/docs-release/architecture/system-explainer/en-US/03-task-lifecycle.md +319 -0
  19. package/docs-release/architecture/system-explainer/en-US/04-check-and-governance.md +250 -0
  20. package/docs-release/architecture/system-explainer/en-US/05-data-flow.md +290 -0
  21. package/docs-release/architecture/system-explainer/en-US/06-preset-and-migration.md +323 -0
  22. package/docs-release/architecture/system-explainer/en-US/README.md +70 -0
  23. package/docs-release/guides/agent-installation.en-US.md +8 -7
  24. package/docs-release/guides/agent-installation.md +9 -7
  25. package/docs-release/guides/preset-development.md +26 -2
  26. package/docs-release/guides/task-state-machine.en-US.md +30 -13
  27. package/docs-release/guides/task-state-machine.md +30 -13
  28. package/examples/minimal-project/docs/09-PLANNING/TASKS/demo-task/INDEX.md +60 -0
  29. package/package.json +3 -2
  30. package/references/harness-ledger.md +1 -1
  31. package/scripts/commands/migration-command.mjs +30 -0
  32. package/scripts/commands/task-command.mjs +26 -25
  33. package/scripts/harness.mjs +7 -3
  34. package/scripts/lib/capability-registry.mjs +17 -21
  35. package/scripts/lib/check-module-parallel.mjs +9 -16
  36. package/scripts/lib/check-profiles.mjs +35 -81
  37. package/scripts/lib/check-task-contracts.mjs +13 -5
  38. package/scripts/lib/core-shared.mjs +55 -2
  39. package/scripts/lib/dashboard-data.mjs +126 -18
  40. package/scripts/lib/dashboard-workbench.mjs +80 -1
  41. package/scripts/lib/dashboard-writer.mjs +6 -2
  42. package/scripts/lib/git-status-summary.mjs +1 -1
  43. package/scripts/lib/governance-sync.mjs +180 -83
  44. package/scripts/lib/harness-core.mjs +1 -0
  45. package/scripts/lib/markdown-utils.mjs +33 -0
  46. package/scripts/lib/migration-planner.mjs +4 -6
  47. package/scripts/lib/phase-kind.mjs +50 -0
  48. package/scripts/lib/preset-engine.mjs +5 -8
  49. package/scripts/lib/preset-registry.mjs +188 -39
  50. package/scripts/lib/review-confirm-git-gate.mjs +1 -1
  51. package/scripts/lib/status-builder.mjs +88 -0
  52. package/scripts/lib/status-dashboard-renderer.mjs +7 -4
  53. package/scripts/lib/task-audit-metadata.mjs +385 -0
  54. package/scripts/lib/task-audit-migration.mjs +350 -0
  55. package/scripts/lib/task-completion-consistency.mjs +11 -1
  56. package/scripts/lib/task-lifecycle/create-task-helpers.mjs +67 -0
  57. package/scripts/lib/task-lifecycle/phase-sync.mjs +88 -0
  58. package/scripts/lib/task-lifecycle/review-confirm.mjs +40 -29
  59. package/scripts/lib/task-lifecycle/review-gates.mjs +13 -10
  60. package/scripts/lib/task-lifecycle/review-submission.mjs +63 -0
  61. package/scripts/lib/task-lifecycle/scaffold-provenance.mjs +49 -0
  62. package/scripts/lib/task-lifecycle/template-files.mjs +53 -0
  63. package/scripts/lib/task-lifecycle.mjs +114 -147
  64. package/scripts/lib/task-metadata.mjs +118 -0
  65. package/scripts/lib/task-review-model.mjs +54 -68
  66. package/scripts/lib/task-scanner.mjs +70 -143
  67. package/skills/preset-creator/references/complex-task-skeleton/brief.md +11 -0
  68. package/templates/AGENTS.md.template +7 -5
  69. package/templates/dashboard/assets/app-src/00-state.js +12 -0
  70. package/templates/dashboard/assets/app-src/10-router.js +3 -0
  71. package/templates/dashboard/assets/app-src/20-overview.js +7 -3
  72. package/templates/dashboard/assets/app-src/35-task-detail.js +46 -6
  73. package/templates/dashboard/assets/app-src/55-presets.js +375 -0
  74. package/templates/dashboard/assets/app-src/60-shared.js +3 -1
  75. package/templates/dashboard/assets/app-src/90-bindings.js +131 -0
  76. package/templates/dashboard/assets/app.css +583 -0
  77. package/templates/dashboard/assets/app.css.manifest.json +1 -0
  78. package/templates/dashboard/assets/app.js +578 -10
  79. package/templates/dashboard/assets/app.manifest.json +1 -0
  80. package/templates/dashboard/assets/css-src/00-foundation.css +4 -0
  81. package/templates/dashboard/assets/css-src/40-detail-modules-migration.css +62 -0
  82. package/templates/dashboard/assets/css-src/45-presets.css +516 -0
  83. package/templates/dashboard/assets/i18n.js +140 -2
  84. package/templates/planning/INDEX.md +87 -0
  85. package/templates/planning/brief.md +1 -1
  86. package/templates/planning/module_session_prompt.md +1 -0
  87. package/templates/planning/review.md +0 -18
  88. package/templates/planning/task_plan.md +4 -43
  89. package/templates/planning/visual_map.md +13 -9
  90. package/templates/planning/visual_map.simple.md +52 -0
  91. package/templates/reference/execution-workflow-standard.md +29 -2
  92. package/templates-zh-CN/AGENTS.md.template +7 -5
  93. package/templates-zh-CN/planning/INDEX.md +87 -0
  94. package/templates-zh-CN/planning/brief.md +1 -1
  95. package/templates-zh-CN/planning/module_session_prompt.md +1 -0
  96. package/templates-zh-CN/planning/review.md +0 -18
  97. package/templates-zh-CN/planning/task_plan.md +3 -63
  98. package/templates-zh-CN/planning/visual_map.md +14 -7
  99. package/templates-zh-CN/planning/visual_map.simple.md +48 -0
  100. package/templates-zh-CN/reference/execution-workflow-standard.md +31 -6
@@ -0,0 +1,375 @@
1
+ function presetsView() {
2
+ ensurePresetState();
3
+ const catalog = bundle.presetCatalog || { summary: {}, roots: [], presets: [] };
4
+ let presets = filteredPresets();
5
+ syncVisiblePresetSelection(presets);
6
+ presets = filteredPresets();
7
+ const selected = selectedPreset(presets);
8
+ syncPresetUninstallScope(selected);
9
+ return `<div class="presets-page stack">
10
+ <section class="flow-panel preset-command-center">
11
+ <div class="section-head">
12
+ <div>
13
+ <p class="eyebrow">${t("presetCatalog")}</p>
14
+ <h2>${t("presetCatalog")}</h2>
15
+ <p class="subtle">${t("presetCatalogSubtitle")}</p>
16
+ </div>
17
+ <span class="preset-count-pill">${presets.length}/${catalog.summary?.total || 0}</span>
18
+ </div>
19
+ <div class="preset-priority-strip" aria-label="${escapeAttr(t("presetPriorityTitle"))}">
20
+ ${presetPriorityStep("project", 1)}
21
+ ${presetPriorityStep("user", 2)}
22
+ ${presetPriorityStep("builtin", 3)}
23
+ </div>
24
+ <div class="preset-toolbar">
25
+ <div class="input-group">
26
+ <input data-preset-search value="${escapeAttr(state.presetQuery)}" placeholder="${escapeAttr(t("presetSearchPlaceholder"))}" aria-label="${escapeAttr(t("presetSearch"))}">
27
+ </div>
28
+ <div class="preset-source-tabs" role="tablist" aria-label="${escapeAttr(t("presetSourceFilter"))}">
29
+ ${presetSourceOptions().map((source) => presetSourceButton(source)).join("")}
30
+ </div>
31
+ </div>
32
+ </section>
33
+ <section class="preset-workspace">
34
+ <div class="flow-panel preset-collection-panel">
35
+ <div class="preset-panel-heading">
36
+ <div>
37
+ <h3>${t("presetCollection")}</h3>
38
+ <p>${t("presetCollectionHint")}</p>
39
+ </div>
40
+ </div>
41
+ <div class="preset-catalog-list">
42
+ ${presets.map((preset) => presetCard(preset, selected ? presetKey(selected) : "")).join("") || emptyState(t("noPresets"))}
43
+ </div>
44
+ </div>
45
+ <div class="preset-detail-workspace stack">
46
+ ${presetDetailPanel(selected)}
47
+ ${presetLayerStackPanel(selected)}
48
+ </div>
49
+ <aside class="preset-context-actions stack">
50
+ ${presetActionPanel(selected)}
51
+ ${presetImportPanel()}
52
+ ${presetRestorePanel()}
53
+ ${presetSummaryPanel(catalog)}
54
+ </aside>
55
+ </section>
56
+ </div>`;
57
+ }
58
+
59
+ function ensurePresetState() {
60
+ const presets = bundle.presetCatalog?.presets || [];
61
+ if (!state.selectedPresetKey && state.selectedPresetId) {
62
+ const legacySelection = presets.find((preset) => preset.id === state.selectedPresetId);
63
+ if (legacySelection) state.selectedPresetKey = presetKey(legacySelection);
64
+ }
65
+ if (!state.selectedPresetKey && presets[0]) {
66
+ state.selectedPresetKey = presetKey(presets[0]);
67
+ state.presetUninstallConfirm = "";
68
+ }
69
+ if (state.selectedPresetKey && !presets.some((preset) => presetKey(preset) === state.selectedPresetKey) && presets[0]) {
70
+ state.selectedPresetKey = presetKey(presets[0]);
71
+ state.presetUninstallConfirm = "";
72
+ }
73
+ }
74
+
75
+ function presetSourceOptions() {
76
+ return [
77
+ ["all", t("allPresets")],
78
+ ["project", t("presetSourceProject")],
79
+ ["user", t("presetSourceUser")],
80
+ ["builtin", t("presetSourceBuiltin")],
81
+ ];
82
+ }
83
+
84
+ function presetSourceButton([source, labelText]) {
85
+ const active = state.presetSourceFilter === source;
86
+ const count = source === "all" ? (bundle.presetCatalog?.summary?.total || 0) : (bundle.presetCatalog?.summary?.[source] || 0);
87
+ return `<button type="button" class="${active ? "active" : ""}" data-preset-source-filter="${escapeAttr(source)}" role="tab" aria-selected="${active ? "true" : "false"}">
88
+ <span>${escapeHtml(labelText)}</span>
89
+ <strong>${count}</strong>
90
+ </button>`;
91
+ }
92
+
93
+ function filteredPresets() {
94
+ const query = String(state.presetQuery || "").trim().toLowerCase();
95
+ return (bundle.presetCatalog?.presets || []).filter((preset) => {
96
+ if (state.presetSourceFilter !== "all" && preset.source !== state.presetSourceFilter) return false;
97
+ return presetMatchesQuery(preset, query);
98
+ });
99
+ }
100
+
101
+ function presetMatchesQuery(preset, query = state.presetQuery) {
102
+ const normalizedQuery = String(query || "").trim().toLowerCase();
103
+ if (!normalizedQuery) return true;
104
+ return [
105
+ preset.id,
106
+ preset.source,
107
+ preset.purpose,
108
+ preset.taskKind,
109
+ preset.manifestPath,
110
+ preset.version,
111
+ ...(preset.compatibleBudgets || []),
112
+ ].some((value) => String(value || "").toLowerCase().includes(normalizedQuery));
113
+ }
114
+
115
+ function syncVisiblePresetSelection(visiblePresets) {
116
+ if (!visiblePresets.length) {
117
+ state.selectedPresetKey = "";
118
+ state.presetUninstallConfirm = "";
119
+ return;
120
+ }
121
+ if (!visiblePresets.some((preset) => presetKey(preset) === state.selectedPresetKey)) {
122
+ state.selectedPresetKey = presetKey(visiblePresets[0]);
123
+ state.presetUninstallConfirm = "";
124
+ }
125
+ }
126
+
127
+ function selectedPreset(visiblePresets = filteredPresets()) {
128
+ return visiblePresets.find((preset) => presetKey(preset) === state.selectedPresetKey) || visiblePresets[0] || null;
129
+ }
130
+
131
+ function presetCard(preset, selectedId) {
132
+ const key = presetKey(preset);
133
+ const selected = key === selectedId;
134
+ return `<article class="preset-card ${selected ? "active" : ""} ${preset.effective ? "effective" : "shadowed"}">
135
+ <div class="preset-card-topline">
136
+ <button type="button" class="preset-card-select" data-preset-select="${escapeAttr(key)}" aria-pressed="${selected ? "true" : "false"}">
137
+ <span class="card-id">${escapeHtml(preset.id)}</span>
138
+ </button>
139
+ <div class="preset-card-tools">
140
+ ${presetSourceBadge(preset.source)}
141
+ ${presetStatusBadge(preset)}
142
+ <button type="button" class="copy-inline" data-copy-preset-id="${escapeAttr(preset.id)}" title="${escapeAttr(t("copyPresetId"))}">${t("copyIdShort")}</button>
143
+ </div>
144
+ </div>
145
+ <button type="button" class="preset-card-body" data-preset-select="${escapeAttr(key)}">
146
+ <span>${escapeHtml(preset.purpose || t("none"))}</span>
147
+ </button>
148
+ <div class="preset-card-meta">
149
+ <span>${t("manifestVersion")}: ${escapeHtml(formatPresetVersion(preset))}</span>
150
+ <span>${t("taskKind")}: ${escapeHtml(preset.taskKind || t("none"))}</span>
151
+ <span>${t("budgets")}: ${escapeHtml((preset.compatibleBudgets || []).join(", ") || t("none"))}</span>
152
+ </div>
153
+ <code class="preset-manifest-path">${escapeHtml(preset.manifestPath || "")}</code>
154
+ </article>`;
155
+ }
156
+
157
+ function presetKey(preset) {
158
+ return preset?.key || `${preset?.source || "unknown"}:${preset?.id || ""}`;
159
+ }
160
+
161
+ function presetSourceRank(source) {
162
+ return { project: 1, user: 2, builtin: 3 }[source] || 9;
163
+ }
164
+
165
+ function presetLayersForId(id) {
166
+ return (bundle.presetCatalog?.presets || [])
167
+ .filter((preset) => preset.id === id)
168
+ .sort((a, b) => presetSourceRank(a.source) - presetSourceRank(b.source));
169
+ }
170
+
171
+ function syncPresetUninstallScope(preset) {
172
+ if (preset && ["project", "user"].includes(preset.source)) state.presetUninstallScope = preset.source;
173
+ }
174
+
175
+ function presetPriorityStep(source, index) {
176
+ return `<div class="preset-priority-step">
177
+ <span>${index}</span>
178
+ <strong>${escapeHtml(t(`presetSource_${source}`) || source)}</strong>
179
+ </div>`;
180
+ }
181
+
182
+ function presetSourceBadge(source) {
183
+ const normalized = String(source || "unknown");
184
+ return `<span class="tag preset-source-badge ${escapeAttr(normalized)}">${escapeHtml(t(`presetSource_${normalized}`) || normalized)}</span>`;
185
+ }
186
+
187
+ function presetStatusBadge(preset) {
188
+ return `<span class="tag ${preset.effective ? "pass" : "warn"}">${escapeHtml(preset.effective ? t("presetEffective") : t("presetShadowed"))}</span>`;
189
+ }
190
+
191
+ function formatPresetVersion(preset) {
192
+ return preset?.version ?? t("none");
193
+ }
194
+
195
+ function presetSummaryPanel(catalog) {
196
+ const roots = catalog.roots || [];
197
+ return `<section class="side-panel preset-summary-panel">
198
+ <h3>${t("presetSources")}</h3>
199
+ <p class="preset-helper">${t("presetSourcesHint")}</p>
200
+ <div class="metrics-grid compact">
201
+ ${metric(t("presetSourceProject"), catalog.summary?.project || 0)}
202
+ ${metric(t("presetSourceUser"), catalog.summary?.user || 0)}
203
+ ${metric(t("presetSourceBuiltin"), catalog.summary?.builtin || 0)}
204
+ </div>
205
+ <div class="preset-roots">
206
+ ${roots.map((root) => `<div><strong>${escapeHtml(t(`presetSource_${root.source}`) || root.source)}</strong><code>${escapeHtml(root.path || "")}</code></div>`).join("")}
207
+ </div>
208
+ </section>`;
209
+ }
210
+
211
+ function presetDetailPanel(preset) {
212
+ if (!preset) return `<section class="flow-panel preset-detail-panel">${emptyState(t("noPresets"))}</section>`;
213
+ const inspectCommand = `harness preset inspect ${preset.id} --json .`;
214
+ const checkCommand = `harness preset check ${preset.id} --json .`;
215
+ const commandRows = preset.effective
216
+ ? `${presetCommandRow(inspectCommand)}${presetCommandRow(checkCommand)}`
217
+ : `<div class="preset-command-warning">${escapeHtml(t("presetCommandsEffectiveOnly"))}</div>`;
218
+ return `<section class="flow-panel preset-detail-panel">
219
+ <div class="preset-detail-hero">
220
+ <div>
221
+ <div class="preset-detail-title-row">
222
+ <h3>${escapeHtml(preset.id)}</h3>
223
+ <button type="button" class="copy-inline" data-copy-preset-id="${escapeAttr(preset.id)}">${t("copyPresetId")}</button>
224
+ </div>
225
+ <p>${escapeHtml(preset.purpose || "")}</p>
226
+ </div>
227
+ <div class="preset-detail-badges">
228
+ ${presetSourceBadge(preset.source)}
229
+ ${presetStatusBadge(preset)}
230
+ </div>
231
+ </div>
232
+ <dl class="preset-detail-list">
233
+ ${presetDetailRow(t("manifestVersion"), formatPresetVersion(preset))}
234
+ ${presetDetailRow(t("source"), t(`presetSource_${preset.source}`) || preset.source)}
235
+ ${presetDetailRow(t("status"), preset.effective ? t("presetEffective") : t("presetShadowed"))}
236
+ ${presetDetailRow(t("taskKind"), preset.taskKind || t("none"))}
237
+ ${presetDetailRow(t("budgets"), (preset.compatibleBudgets || []).join(", ") || t("none"))}
238
+ ${presetDetailRow(t("inputs"), preset.inputCount || 0)}
239
+ ${presetDetailRow(t("references"), preset.referenceCount || 0)}
240
+ ${presetDetailRow(t("artifacts"), preset.artifactCount || 0)}
241
+ ${presetDetailRow(t("writeScopes"), preset.writeScopeCount || 0)}
242
+ ${presetDetailRow(t("requiredReads"), preset.requiredReadCount || 0)}
243
+ </dl>
244
+ <div class="preset-path-block">
245
+ <span>${t("manifestPath")}</span>
246
+ <code class="preset-manifest-path">${escapeHtml(preset.manifestPath || "")}</code>
247
+ </div>
248
+ <div class="preset-command-list">
249
+ ${commandRows}
250
+ </div>
251
+ </section>`;
252
+ }
253
+
254
+ function presetDetailRow(labelText, value) {
255
+ return `<div><dt>${escapeHtml(labelText)}</dt><dd>${escapeHtml(String(value ?? ""))}</dd></div>`;
256
+ }
257
+
258
+ function presetCommandRow(command) {
259
+ return `<div class="preset-command-row">
260
+ <code>${escapeHtml(command)}</code>
261
+ <button type="button" class="copy-inline" data-copy-preset-command="${escapeAttr(command)}">${t("copyCommand")}</button>
262
+ </div>`;
263
+ }
264
+
265
+ function presetLayerStackPanel(preset) {
266
+ if (!preset) return "";
267
+ const layers = presetLayersForId(preset.id);
268
+ return `<section class="flow-panel preset-layer-panel">
269
+ <div class="preset-panel-heading">
270
+ <div>
271
+ <h3>${t("presetLayerStack")}</h3>
272
+ <p>${t("presetLayerStackHint")}</p>
273
+ </div>
274
+ </div>
275
+ <div class="preset-layer-list">
276
+ ${layers.map((layer) => `<button type="button" class="preset-layer-row ${presetKey(layer) === presetKey(preset) ? "active" : ""}" data-preset-select="${escapeAttr(presetKey(layer))}">
277
+ <span class="preset-layer-rank">${presetSourceRank(layer.source)}</span>
278
+ <span>
279
+ <strong>${escapeHtml(t(`presetSource_${layer.source}`) || layer.source)}</strong>
280
+ <small>${t("manifestVersion")}: ${escapeHtml(formatPresetVersion(layer))}</small>
281
+ </span>
282
+ ${presetStatusBadge(layer)}
283
+ </button>`).join("")}
284
+ </div>
285
+ </section>`;
286
+ }
287
+
288
+ function presetActionPanel(preset) {
289
+ const staticNote = canUseWorkbenchAction("preset-install") ? "" : `<p class="lesson-action-note">${escapeHtml(t("presetWorkbenchRequired"))}</p>`;
290
+ const lockedUninstallScope = preset && ["project", "user"].includes(preset.source) ? preset.source : "";
291
+ const confirmMatches = Boolean(preset && state.presetUninstallConfirm.trim() === preset.id);
292
+ const canCheck = canUseWorkbenchAction("preset-check") && preset && preset.effective;
293
+ const canUninstall = canUseWorkbenchAction("preset-uninstall") && preset && preset.source !== "builtin" && confirmMatches;
294
+ return `<section class="side-panel preset-action-panel">
295
+ <div class="preset-panel-heading">
296
+ <div>
297
+ <h3>${t("presetContextActions")}</h3>
298
+ <p>${preset ? escapeHtml(preset.id) : t("noPresets")}</p>
299
+ </div>
300
+ </div>
301
+ ${staticNote}
302
+ ${presetActionResult()}
303
+ <div class="preset-action-group">
304
+ <h4>${t("presetCheck")}</h4>
305
+ <p>${preset?.effective ? t("presetCheckHint") : t("presetShadowedActionHint")}</p>
306
+ <button data-preset-check="${escapeAttr(preset?.id || "")}" ${canCheck ? "" : "disabled"}>${t("presetCheckSelected")}</button>
307
+ </div>
308
+ <div class="preset-action-group danger">
309
+ <h4>${t("presetUninstallSelected")}</h4>
310
+ <p>${preset?.source === "builtin" ? t("presetBuiltinImmutable") : t("presetUninstallHint")}</p>
311
+ <label>${t("scope")}<select data-preset-uninstall-scope ${lockedUninstallScope ? "disabled" : ""}>
312
+ ${presetScopeOptions(lockedUninstallScope || state.presetUninstallScope)}
313
+ </select></label>
314
+ <div class="preset-confirm-row">
315
+ <label>${t("confirmPresetId")}<input data-preset-uninstall-confirm value="${escapeAttr(state.presetUninstallConfirm)}" placeholder="${escapeAttr(preset?.id || "")}"></label>
316
+ <button type="button" data-preset-fill-confirm="${escapeAttr(preset?.id || "")}" ${preset && preset.source !== "builtin" ? "" : "disabled"}>${t("useSelectedId")}</button>
317
+ </div>
318
+ ${preset && preset.source !== "builtin" && !confirmMatches ? `<p class="preset-confirm-warning">${escapeHtml(t("presetConfirmRequired"))}</p>` : ""}
319
+ <button data-preset-uninstall="${escapeAttr(preset?.id || "")}" ${canUninstall ? "" : "disabled"}>${t("presetUninstallSelected")}</button>
320
+ </div>
321
+ </section>`;
322
+ }
323
+
324
+ function presetImportPanel() {
325
+ return `<section class="side-panel preset-action-panel">
326
+ <div class="preset-panel-heading">
327
+ <div>
328
+ <h3>${t("presetImportTitle")}</h3>
329
+ <p>${t("presetImportHint")}</p>
330
+ </div>
331
+ </div>
332
+ <div class="preset-action-group">
333
+ <label>${t("source")}<input data-preset-install-source value="${escapeAttr(state.presetInstallSource)}" placeholder="${escapeAttr(t("presetInstallSourcePlaceholder"))}"></label>
334
+ <label>${t("scope")}<select data-preset-install-scope>
335
+ ${presetScopeOptions(state.presetInstallScope)}
336
+ </select></label>
337
+ <label class="check-row"><input type="checkbox" data-preset-install-force ${state.presetInstallForce ? "checked" : ""}> ${t("forceOverwrite")}</label>
338
+ <button data-preset-install ${canUseWorkbenchAction("preset-install") ? "" : "disabled"}>${t("presetInstall")}</button>
339
+ </div>
340
+ </section>`;
341
+ }
342
+
343
+ function presetRestorePanel() {
344
+ return `<section class="side-panel preset-action-panel">
345
+ <div class="preset-panel-heading">
346
+ <div>
347
+ <h3>${t("presetRestoreBundled")}</h3>
348
+ <p>${t("presetRestoreBundledHint")}</p>
349
+ </div>
350
+ </div>
351
+ <div class="preset-action-group">
352
+ <label>${t("scope")}<select data-preset-seed-scope>
353
+ ${presetScopeOptions(state.presetSeedScope)}
354
+ </select></label>
355
+ <label class="check-row"><input type="checkbox" data-preset-seed-force ${state.presetSeedForce ? "checked" : ""}> ${t("forceOverwrite")}</label>
356
+ <button data-preset-seed ${canUseWorkbenchAction("preset-seed") ? "" : "disabled"}>${t("presetRestoreBundled")}</button>
357
+ </div>
358
+ </section>`;
359
+ }
360
+
361
+ function presetScopeOptions(current) {
362
+ return [["project", t("presetSourceProject")], ["user", t("presetSourceUser")]]
363
+ .map(([value, labelText]) => `<option value="${value}" ${current === value ? "selected" : ""}>${escapeHtml(labelText)}</option>`)
364
+ .join("");
365
+ }
366
+
367
+ function presetActionResult() {
368
+ const result = state.presetActionResult;
369
+ if (!result) return "";
370
+ const klass = result.ok ? "success" : "failed";
371
+ return `<div class="workbench-action-result ${klass}">
372
+ <strong>${escapeHtml(result.title || "")}</strong>
373
+ <span>${escapeHtml(result.message || "")}</span>
374
+ </div>`;
375
+ }
@@ -28,7 +28,9 @@ function tag(value) {
28
28
  }
29
29
 
30
30
  function label(value) {
31
- return t(`state_${value}`) || String(value || "unknown").replaceAll("_", " ");
31
+ const key = `state_${value}`;
32
+ const translated = t(key);
33
+ return translated === key ? String(value || "unknown").replaceAll("_", " ") : translated;
32
34
  }
33
35
 
34
36
  function list(items = []) {
@@ -49,6 +49,68 @@ function bind() {
49
49
  state.warningPage = 1;
50
50
  app();
51
51
  }));
52
+ document.querySelectorAll("[data-preset-search]").forEach((input) => input.addEventListener("input", () => {
53
+ state.presetQuery = input.value;
54
+ app();
55
+ }));
56
+ document.querySelectorAll("[data-preset-source-filter]").forEach((button) => button.addEventListener("click", () => {
57
+ state.presetSourceFilter = button.dataset.presetSourceFilter || "all";
58
+ state.selectedPresetKey = "";
59
+ state.presetUninstallConfirm = "";
60
+ app();
61
+ }));
62
+ document.querySelectorAll("[data-preset-select]").forEach((button) => button.addEventListener("click", () => {
63
+ state.selectedPresetKey = button.dataset.presetSelect || "";
64
+ state.selectedPresetId = "";
65
+ const selectedPreset = (bundle.presetCatalog?.presets || []).find((preset) => presetKey(preset) === state.selectedPresetKey);
66
+ if (selectedPreset && state.presetSourceFilter !== "all" && selectedPreset.source !== state.presetSourceFilter) {
67
+ state.presetSourceFilter = selectedPreset.source;
68
+ }
69
+ if (selectedPreset && !presetMatchesQuery(selectedPreset)) state.presetQuery = "";
70
+ if (selectedPreset && ["project", "user"].includes(selectedPreset.source)) state.presetUninstallScope = selectedPreset.source;
71
+ state.presetUninstallConfirm = "";
72
+ app();
73
+ }));
74
+ document.querySelectorAll("[data-preset-install-source]").forEach((input) => input.addEventListener("input", () => {
75
+ state.presetInstallSource = input.value;
76
+ }));
77
+ document.querySelectorAll("[data-preset-install-scope]").forEach((select) => select.addEventListener("change", () => {
78
+ state.presetInstallScope = select.value || "project";
79
+ }));
80
+ document.querySelectorAll("[data-preset-install-force]").forEach((input) => input.addEventListener("change", () => {
81
+ state.presetInstallForce = input.checked;
82
+ }));
83
+ document.querySelectorAll("[data-preset-seed-scope]").forEach((select) => select.addEventListener("change", () => {
84
+ state.presetSeedScope = select.value || "project";
85
+ }));
86
+ document.querySelectorAll("[data-preset-seed-force]").forEach((input) => input.addEventListener("change", () => {
87
+ state.presetSeedForce = input.checked;
88
+ }));
89
+ document.querySelectorAll("[data-preset-uninstall-scope]").forEach((select) => select.addEventListener("change", () => {
90
+ state.presetUninstallScope = select.value || "project";
91
+ }));
92
+ document.querySelectorAll("[data-preset-uninstall-confirm]").forEach((input) => input.addEventListener("input", () => {
93
+ state.presetUninstallConfirm = input.value;
94
+ }));
95
+ document.querySelectorAll("[data-preset-fill-confirm]").forEach((button) => button.addEventListener("click", () => {
96
+ state.presetUninstallConfirm = button.dataset.presetFillConfirm || "";
97
+ app();
98
+ }));
99
+ document.querySelectorAll("[data-preset-check]").forEach((button) => button.addEventListener("click", () => runPresetAction("check", { id: button.dataset.presetCheck || "" })));
100
+ document.querySelectorAll("[data-preset-install]").forEach((button) => button.addEventListener("click", () => runPresetAction("install", {
101
+ source: state.presetInstallSource,
102
+ scope: state.presetInstallScope,
103
+ force: state.presetInstallForce,
104
+ })));
105
+ document.querySelectorAll("[data-preset-seed]").forEach((button) => button.addEventListener("click", () => runPresetAction("seed", {
106
+ scope: state.presetSeedScope,
107
+ force: state.presetSeedForce,
108
+ })));
109
+ document.querySelectorAll("[data-preset-uninstall]").forEach((button) => button.addEventListener("click", () => runPresetAction("uninstall", {
110
+ id: button.dataset.presetUninstall || "",
111
+ scope: state.presetUninstallScope,
112
+ confirmText: state.presetUninstallConfirm,
113
+ })));
52
114
  document.querySelectorAll("[data-review-queue-tab]").forEach((button) => button.addEventListener("click", () => {
53
115
  state.reviewQueueTab = button.dataset.reviewQueueTab || "review";
54
116
  state.reviewQueuePage = 1;
@@ -95,6 +157,7 @@ function bind() {
95
157
  openDrawer(taskId);
96
158
  }));
97
159
  bindCopyTaskNameButtons(document);
160
+ bindPresetCopyButtons(document);
98
161
  bindRepairPromptButtons(document);
99
162
  bindLessonSedimentationButtons(document);
100
163
  document.querySelectorAll("[data-open-lesson-drawer]").forEach((el) => el.addEventListener("click", (e) => {
@@ -177,6 +240,45 @@ async function completeReviewFromDashboard(taskId) {
177
240
  }
178
241
  }
179
242
 
243
+ async function runPresetAction(action, body) {
244
+ state.presetActionResult = { ok: true, title: t("presetActionRunning"), message: action };
245
+ app();
246
+ try {
247
+ const response = await fetch(`/api/presets/${action}`, {
248
+ method: "POST",
249
+ headers: {
250
+ "content-type": "application/json",
251
+ "x-harness-csrf": state.runtime?.csrfToken || "",
252
+ },
253
+ body: JSON.stringify(body),
254
+ });
255
+ const payload = await response.json();
256
+ if (!response.ok) throw payload;
257
+ state.presetActionResult = {
258
+ ok: true,
259
+ title: t("presetActionSuccess"),
260
+ message: presetActionMessage(action, payload),
261
+ };
262
+ app();
263
+ if (["install", "seed", "uninstall"].includes(action)) setTimeout(() => window.location.reload(), 650);
264
+ } catch (error) {
265
+ state.presetActionResult = {
266
+ ok: false,
267
+ title: t("presetActionFailed"),
268
+ message: error?.error || error?.message || String(error || action),
269
+ };
270
+ app();
271
+ }
272
+ }
273
+
274
+ function presetActionMessage(action, payload) {
275
+ if (action === "check") return `${payload.id || ""} ${payload.status || ""}`.trim();
276
+ if (action === "install") return `${payload.id || ""} -> ${payload.scope || ""}`.trim();
277
+ if (action === "seed") return `${payload.created || 0} ${t("created")} · ${payload.skipped || 0} ${t("skipped")}`;
278
+ if (action === "uninstall") return `${payload.id || ""} ${payload.removed ? t("removed") : t("notInstalled")}`.trim();
279
+ return action;
280
+ }
281
+
180
282
  function renderDrawerContent(taskId) {
181
283
  const task = (bundle.status?.tasks || []).find((item) => item.id === taskId);
182
284
  if (!task) return `<div class="empty">${t("taskNotFound")}</div>`;
@@ -256,6 +358,35 @@ function bindCopyTaskNameButtons(root) {
256
358
  }));
257
359
  }
258
360
 
361
+ function bindPresetCopyButtons(root) {
362
+ root.querySelectorAll("[data-copy-preset-id]").forEach((button) => button.addEventListener("click", async (event) => {
363
+ event.preventDefault();
364
+ event.stopPropagation();
365
+ const presetId = button.dataset.copyPresetId || "";
366
+ const defaultText = button.textContent;
367
+ try {
368
+ await copyText(presetId);
369
+ button.textContent = t("copyTaskNameSuccess");
370
+ } catch {
371
+ button.textContent = t("copyTaskNameFailed");
372
+ }
373
+ setTimeout(() => { button.textContent = defaultText; }, 1200);
374
+ }));
375
+ root.querySelectorAll("[data-copy-preset-command]").forEach((button) => button.addEventListener("click", async (event) => {
376
+ event.preventDefault();
377
+ event.stopPropagation();
378
+ const command = button.dataset.copyPresetCommand || "";
379
+ const defaultText = button.textContent;
380
+ try {
381
+ await copyText(command);
382
+ button.textContent = t("copyTaskNameSuccess");
383
+ } catch {
384
+ button.textContent = t("copyTaskNameFailed");
385
+ }
386
+ setTimeout(() => { button.textContent = defaultText; }, 1200);
387
+ }));
388
+ }
389
+
259
390
  function bindRepairPromptButtons(root) {
260
391
  root.querySelectorAll("[data-copy-repair-prompt]").forEach((button) => button.addEventListener("click", async (event) => {
261
392
  event.preventDefault();