coding-agent-harness 1.0.2 → 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.
- package/CHANGELOG.md +32 -0
- package/CONTRIBUTING.md +98 -0
- package/LICENSE +661 -21
- package/LICENSE-EXCEPTION.md +37 -0
- package/README.md +244 -87
- package/README.zh-CN.md +77 -35
- package/SKILL.md +32 -24
- package/docs-release/README.md +9 -5
- package/docs-release/architecture/overview.md +17 -5
- package/docs-release/architecture/overview.zh-CN.md +9 -5
- package/docs-release/architecture/system-explainer/01-system-overview.md +217 -0
- package/docs-release/architecture/system-explainer/02-module-dependency.md +257 -0
- package/docs-release/architecture/system-explainer/03-task-lifecycle.md +304 -0
- package/docs-release/architecture/system-explainer/04-check-and-governance.md +239 -0
- package/docs-release/architecture/system-explainer/05-data-flow.md +276 -0
- package/docs-release/architecture/system-explainer/06-preset-and-migration.md +303 -0
- package/docs-release/architecture/system-explainer/README.md +67 -0
- package/docs-release/architecture/system-explainer/en-US/01-system-overview.md +226 -0
- package/docs-release/architecture/system-explainer/en-US/02-module-dependency.md +263 -0
- package/docs-release/architecture/system-explainer/en-US/03-task-lifecycle.md +319 -0
- package/docs-release/architecture/system-explainer/en-US/04-check-and-governance.md +250 -0
- package/docs-release/architecture/system-explainer/en-US/05-data-flow.md +290 -0
- package/docs-release/architecture/system-explainer/en-US/06-preset-and-migration.md +323 -0
- package/docs-release/architecture/system-explainer/en-US/README.md +70 -0
- package/docs-release/assets/dashboard-overview.png +0 -0
- package/docs-release/guides/agent-installation.en-US.md +39 -15
- package/docs-release/guides/agent-installation.md +43 -16
- package/docs-release/guides/contributing.md +100 -0
- package/docs-release/guides/contributing.zh-CN.md +99 -0
- package/docs-release/guides/document-audience-and-surfaces.en-US.md +3 -2
- package/docs-release/guides/document-audience-and-surfaces.md +3 -2
- package/docs-release/guides/full-legacy-migration-subagent-strategy.md +2 -2
- package/docs-release/guides/full-legacy-migration-subagent-strategy.zh-CN.md +2 -2
- package/docs-release/guides/legacy-migration-agent-prompt.md +0 -11
- package/docs-release/guides/legacy-migration-agent-prompt.zh-CN.md +0 -11
- package/docs-release/guides/migration-playbook.en-US.md +14 -15
- package/docs-release/guides/migration-playbook.md +14 -15
- package/docs-release/guides/parent-control-repository-pattern.en-US.md +7 -5
- package/docs-release/guides/parent-control-repository-pattern.md +7 -5
- package/docs-release/guides/preset-development.md +238 -0
- package/docs-release/guides/repository-operating-models.en-US.md +5 -4
- package/docs-release/guides/repository-operating-models.md +5 -4
- package/docs-release/guides/task-state-machine.en-US.md +224 -0
- package/docs-release/guides/task-state-machine.md +231 -0
- package/docs-release/intl/en-US.md +1 -1
- package/docs-release/intl/zh-CN.md +1 -1
- package/examples/minimal-project/docs/09-PLANNING/TASKS/demo-task/INDEX.md +60 -0
- package/examples/minimal-project/docs/09-PLANNING/TASKS/demo-task/findings.md +7 -0
- package/package.json +10 -4
- package/presets/legacy-migration/checks/preset-check.mjs +3 -0
- package/presets/legacy-migration/preset.yaml +134 -0
- package/presets/legacy-migration/scripts/plan-work-queue.mjs +4 -0
- package/presets/legacy-migration/scripts/scaffold-task-contracts.mjs +4 -0
- package/presets/legacy-migration/templates/execution_strategy.append.md +18 -0
- package/presets/legacy-migration/templates/findings.seed.md +17 -0
- package/presets/legacy-migration/templates/review.seed.md +12 -0
- package/presets/legacy-migration/templates/task_plan.append.md +9 -0
- package/presets/legacy-migration/templates/visual_map.append.md +12 -0
- package/presets/legacy-migration/workbench/dashboard-panels.yaml +2 -0
- package/presets/legacy-migration/workbench/migration-queue.schema.json +23 -0
- package/presets/lesson-sedimentation/preset.yaml +23 -0
- package/presets/lesson-sedimentation/templates/prompt.md +23 -0
- package/presets/module/preset.yaml +25 -0
- package/presets/module/templates/execution_strategy.append.md +8 -0
- package/presets/module/templates/task_plan.append.md +17 -0
- package/presets/standard-task/preset.yaml +31 -0
- package/presets/standard-task/templates/task_plan.append.md +7 -0
- package/references/adversarial-review-standard.md +2 -2
- package/references/agents-md-pattern.md +2 -2
- package/references/delivery-operating-model-standard.md +3 -3
- package/references/docs-directory-standard.md +6 -7
- package/references/harness-ledger.md +53 -96
- package/references/lessons-governance.md +88 -93
- package/references/module-parallel-standard.md +14 -14
- package/references/planning-loop.md +12 -6
- package/references/pull-request-standard.md +118 -0
- package/references/repo-governance-standard.md +11 -2
- package/references/review-routing-standard.md +7 -1
- package/references/ssot-governance.md +67 -59
- package/references/taskr-gap-analysis.md +600 -0
- package/references/walkthrough-closeout.md +7 -7
- package/scripts/check-harness.mjs +40 -301
- package/scripts/commands/dashboard-command.mjs +67 -0
- package/scripts/commands/migration-command.mjs +126 -0
- package/scripts/commands/preset-command.mjs +73 -0
- package/scripts/commands/task-command.mjs +328 -0
- package/scripts/harness.mjs +59 -260
- package/scripts/lib/capability-registry.mjs +82 -28
- package/scripts/lib/check-module-parallel.mjs +230 -0
- package/scripts/lib/check-profiles.mjs +90 -228
- package/scripts/lib/check-task-contracts.mjs +55 -0
- package/scripts/lib/core-shared.mjs +65 -2
- package/scripts/lib/dashboard-data.mjs +155 -24
- package/scripts/lib/dashboard-workbench.mjs +131 -12
- package/scripts/lib/dashboard-writer.mjs +20 -4
- package/scripts/lib/git-status-summary.mjs +46 -0
- package/scripts/lib/governance-index-generator.mjs +174 -0
- package/scripts/lib/governance-sync.mjs +611 -0
- package/scripts/lib/governance-table-boundary.mjs +175 -0
- package/scripts/lib/harness-core.mjs +6 -0
- package/scripts/lib/lesson-maintenance.mjs +36 -29
- package/scripts/lib/markdown-utils.mjs +33 -0
- package/scripts/lib/migration-planner.mjs +4 -6
- package/scripts/lib/migration-support.mjs +1 -1
- package/scripts/lib/phase-kind.mjs +50 -0
- package/scripts/lib/preset-audit-contracts.mjs +37 -0
- package/scripts/lib/preset-engine.mjs +494 -0
- package/scripts/lib/preset-registry.mjs +776 -0
- package/scripts/lib/preset-resource-contracts.mjs +83 -0
- package/scripts/lib/review-confirm-git-gate.mjs +248 -0
- package/scripts/lib/status-builder.mjs +88 -0
- package/scripts/lib/status-dashboard-renderer.mjs +105 -0
- package/scripts/lib/subagent-authorization-audit.mjs +196 -0
- package/scripts/lib/task-audit-metadata.mjs +385 -0
- package/scripts/lib/task-audit-migration.mjs +350 -0
- package/scripts/lib/task-completion-consistency.mjs +26 -0
- package/scripts/lib/task-index.mjs +93 -0
- package/scripts/lib/task-lesson-candidates.mjs +242 -0
- package/scripts/lib/task-lesson-sedimentation.mjs +326 -0
- package/scripts/lib/task-lifecycle/create-task-helpers.mjs +67 -0
- package/scripts/lib/task-lifecycle/phase-sync.mjs +88 -0
- package/scripts/lib/task-lifecycle/review-confirm.mjs +112 -0
- package/scripts/lib/task-lifecycle/review-gates.mjs +73 -0
- package/scripts/lib/task-lifecycle/review-submission.mjs +63 -0
- package/scripts/lib/task-lifecycle/scaffold-provenance.mjs +49 -0
- package/scripts/lib/task-lifecycle/template-files.mjs +53 -0
- package/scripts/lib/task-lifecycle/text-utils.mjs +24 -0
- package/scripts/lib/task-lifecycle.mjs +338 -477
- package/scripts/lib/task-metadata.mjs +118 -0
- package/scripts/lib/task-review-model.mjs +455 -0
- package/scripts/lib/task-scanner.mjs +193 -372
- package/scripts/lib/task-tombstone-commands.mjs +140 -0
- package/scripts/postinstall.mjs +14 -0
- package/skills/preset-creator/SKILL.md +179 -0
- package/skills/preset-creator/references/complex-task-skeleton/README.md +31 -0
- package/skills/preset-creator/references/complex-task-skeleton/artifacts/INDEX.md +12 -0
- package/skills/preset-creator/references/complex-task-skeleton/brief.md +43 -0
- package/skills/preset-creator/references/complex-task-skeleton/execution_strategy.md +71 -0
- package/skills/preset-creator/references/complex-task-skeleton/findings.md +24 -0
- package/skills/preset-creator/references/complex-task-skeleton/lesson_candidates.md +70 -0
- package/skills/preset-creator/references/complex-task-skeleton/long-running-task-contract.md +76 -0
- package/skills/preset-creator/references/complex-task-skeleton/progress.md +33 -0
- package/skills/preset-creator/references/complex-task-skeleton/references/INDEX.md +13 -0
- package/skills/preset-creator/references/complex-task-skeleton/review.md +107 -0
- package/skills/preset-creator/references/complex-task-skeleton/task_plan.md +111 -0
- package/skills/preset-creator/references/complex-task-skeleton/visual_map.md +50 -0
- package/skills/preset-creator/references/preset-package-skeleton.md +296 -0
- package/templates/AGENTS.md.template +24 -18
- package/templates/dashboard/assets/app-src/00-state.js +13 -0
- package/templates/dashboard/assets/app-src/10-router.js +5 -1
- package/templates/dashboard/assets/app-src/20-overview.js +18 -8
- package/templates/dashboard/assets/app-src/30-tasks.js +92 -246
- package/templates/dashboard/assets/app-src/35-task-detail.js +286 -0
- package/templates/dashboard/assets/app-src/45-review.js +241 -22
- package/templates/dashboard/assets/app-src/50-migration.js +24 -10
- package/templates/dashboard/assets/app-src/55-presets.js +375 -0
- package/templates/dashboard/assets/app-src/60-shared.js +3 -1
- package/templates/dashboard/assets/app-src/90-bindings.js +302 -29
- package/templates/dashboard/assets/app.css +1501 -376
- package/templates/dashboard/assets/app.css.manifest.json +10 -0
- package/templates/dashboard/assets/app.js +1240 -101
- package/templates/dashboard/assets/app.manifest.json +2 -0
- package/templates/dashboard/assets/css-src/00-foundation.css +346 -0
- package/templates/dashboard/assets/css-src/10-panels-flow.css +236 -0
- package/templates/dashboard/assets/css-src/20-briefs-controls.css +398 -0
- package/templates/dashboard/assets/css-src/30-task-index.css +739 -0
- package/templates/dashboard/assets/css-src/35-review-workspace.css +507 -0
- package/templates/dashboard/assets/css-src/40-detail-modules-migration.css +489 -0
- package/templates/dashboard/assets/css-src/45-presets.css +516 -0
- package/templates/dashboard/assets/css-src/50-responsive-overrides.css +551 -0
- package/templates/dashboard/assets/i18n.js +263 -23
- package/templates/ledger/Harness-Ledger.md +13 -25
- package/templates/lessons/lesson-arch-process-change.md +1 -1
- package/templates/lessons/lesson-new-doc.md +1 -1
- package/templates/lessons/lesson-ref-change.md +1 -1
- package/templates/planning/INDEX.md +87 -0
- package/templates/planning/brief.md +1 -1
- package/templates/planning/execution_strategy.md +31 -0
- package/templates/planning/lesson_candidates.md +18 -6
- package/templates/planning/module_session_prompt.md +1 -0
- package/templates/planning/optional/artifacts/INDEX.md +3 -3
- package/templates/planning/optional/references/INDEX.md +3 -3
- package/templates/planning/review.md +41 -0
- package/templates/planning/task_plan.md +5 -21
- package/templates/planning/visual_map.md +13 -9
- package/templates/planning/visual_map.simple.md +52 -0
- package/templates/reference/execution-workflow-standard.md +31 -3
- package/templates/reference/pull-request-standard.md +80 -0
- package/templates/reference/repo-governance-standard.md +7 -6
- package/templates/reference/review-routing-standard.md +6 -0
- package/templates/reference/walkthrough-standard.md +2 -1
- package/templates/verifier/verifier-output.md +1 -1
- package/templates-zh-CN/AGENTS.md.template +25 -19
- package/templates-zh-CN/ledger/Harness-Ledger.md +17 -40
- package/templates-zh-CN/planning/INDEX.md +87 -0
- package/templates-zh-CN/planning/brief.md +1 -1
- package/templates-zh-CN/planning/execution_strategy.md +30 -0
- package/templates-zh-CN/planning/lesson_candidates.md +18 -6
- package/templates-zh-CN/planning/module_session_prompt.md +1 -0
- package/templates-zh-CN/planning/review.md +41 -1
- package/templates-zh-CN/planning/task_plan.md +4 -44
- package/templates-zh-CN/planning/visual_map.md +14 -7
- package/templates-zh-CN/planning/visual_map.simple.md +48 -0
- package/templates-zh-CN/reference/adversarial-review-standard.md +1 -1
- package/templates-zh-CN/reference/docs-library-standard.md +1 -1
- package/templates-zh-CN/reference/execution-workflow-standard.md +33 -7
- package/templates-zh-CN/reference/harness-ledger-standard.md +2 -2
- package/templates-zh-CN/reference/pull-request-standard.md +106 -0
- package/templates-zh-CN/reference/repo-governance-standard.md +4 -3
- package/templates-zh-CN/reference/review-routing-standard.md +8 -1
- package/templates-zh-CN/reference/walkthrough-standard.md +3 -2
- package/templates-zh-CN/walkthrough/Closeout-SSoT.md +1 -1
- package/docs-release/assets/dashboard-overview-en.png +0 -0
- package/scripts/smoke-dashboard.mjs +0 -92
- package/scripts/test-harness.mjs +0 -1395
- package/templates/ssot/Feature-SSoT.md +0 -43
- package/templates/ssot/Lessons-SSoT.md +0 -44
- package/templates-zh-CN/ssot/Feature-SSoT.md +0 -49
- package/templates-zh-CN/ssot/Lessons-SSoT.md +0 -49
|
@@ -12,9 +12,22 @@ const state = {
|
|
|
12
12
|
taskGroupPage: 1,
|
|
13
13
|
warningFilter: "all",
|
|
14
14
|
warningPage: 1,
|
|
15
|
+
presetQuery: "",
|
|
16
|
+
presetSourceFilter: "all",
|
|
17
|
+
selectedPresetKey: "",
|
|
18
|
+
selectedPresetId: "",
|
|
19
|
+
presetActionResult: null,
|
|
20
|
+
presetInstallSource: "",
|
|
21
|
+
presetInstallScope: "project",
|
|
22
|
+
presetInstallForce: false,
|
|
23
|
+
presetSeedScope: "project",
|
|
24
|
+
presetSeedForce: false,
|
|
25
|
+
presetUninstallScope: "project",
|
|
26
|
+
presetUninstallConfirm: "",
|
|
15
27
|
renderMode: "rendered",
|
|
16
28
|
theme: localStorage.getItem("harness.theme") || "system",
|
|
17
29
|
taskLayout: localStorage.getItem("harness.taskLayout") || "list",
|
|
30
|
+
taskSortOrder: localStorage.getItem("harness.taskSortOrder") === "asc" ? "asc" : "desc",
|
|
18
31
|
runtime: { mode: "static", csrfToken: "", writableActions: [] },
|
|
19
32
|
runtimeLoaded: false,
|
|
20
33
|
runtimePoller: null,
|
|
@@ -30,6 +43,8 @@ const taskDocTabs = [
|
|
|
30
43
|
["strategy", "execution_strategy.md"],
|
|
31
44
|
["visualMap", "visual_map.md"],
|
|
32
45
|
["legacyRoadmap", "visual_roadmap.md"],
|
|
46
|
+
["lessonCandidates", "lesson_candidates.md"],
|
|
47
|
+
["longRunningContract", "long-running-task-contract.md"],
|
|
33
48
|
["progress", "progress.md"],
|
|
34
49
|
["review", "review.md"],
|
|
35
50
|
["findings", "findings.md"],
|
|
@@ -69,21 +84,34 @@ function shell() {
|
|
|
69
84
|
${routeLink("#/tasks", t("taskIndex"), "tasks")}
|
|
70
85
|
${routeLink("#/review", t("reviewQueue"), "review")}
|
|
71
86
|
${routeLink("#/modules", t("moduleView"), "modules")}
|
|
87
|
+
${routeLink("#/presets", t("presetCatalog"), "presets")}
|
|
72
88
|
<button data-language-toggle>${locale === "zh" ? "EN" : "中文"}</button>
|
|
73
89
|
<button data-theme-toggle>${themeLabel()}</button>
|
|
74
90
|
</div>
|
|
75
91
|
</header>
|
|
92
|
+
${runtimeModeBanner()}
|
|
76
93
|
${renderRoute()}
|
|
77
94
|
<div id="drawer-overlay" class="drawer-overlay"></div>
|
|
78
95
|
<div id="task-drawer" class="task-drawer"></div>
|
|
79
96
|
</div>`;
|
|
80
97
|
}
|
|
81
98
|
|
|
99
|
+
function runtimeModeBanner() {
|
|
100
|
+
if (window.__HARNESS_WORKBENCH__ === true) return "";
|
|
101
|
+
return `<section class="runtime-banner">
|
|
102
|
+
<strong>${t("staticReadOnly")}</strong>
|
|
103
|
+
<span>${t("staticReadOnlyDetail")}</span>
|
|
104
|
+
<code>harness dev</code>
|
|
105
|
+
</section>`;
|
|
106
|
+
}
|
|
107
|
+
|
|
82
108
|
function renderRoute() {
|
|
83
109
|
const route = currentRoute();
|
|
84
110
|
if (route.name === "task") return taskDetail(route);
|
|
111
|
+
if (route.name === "reviewTask") return reviewWorkspace(route);
|
|
85
112
|
if (route.name === "review") return reviewQueue();
|
|
86
113
|
if (route.name === "modules") return modulesView(route.id);
|
|
114
|
+
if (route.name === "presets") return presetsView();
|
|
87
115
|
if (route.name === "tasks") return taskIndex();
|
|
88
116
|
return overview();
|
|
89
117
|
}
|
|
@@ -92,14 +120,17 @@ function currentRoute() {
|
|
|
92
120
|
const hash = window.location.hash || "#/";
|
|
93
121
|
const parts = hash.replace(/^#\/?/, "").split("/").filter(Boolean).map(decodeURIComponent);
|
|
94
122
|
if (parts[0] === "tasks" && parts[1]) return { name: "task", id: parts[1], doc: parts[2] === "docs" ? parts[3] || "" : "" };
|
|
123
|
+
if (parts[0] === "review" && parts[1]) return { name: "reviewTask", id: parts[1] };
|
|
95
124
|
if (parts[0] === "review") return { name: "review" };
|
|
96
125
|
if (parts[0] === "modules") return { name: "modules", id: parts[1] || "" };
|
|
126
|
+
if (parts[0] === "presets") return { name: "presets" };
|
|
97
127
|
if (parts[0] === "tasks") return { name: "tasks" };
|
|
98
128
|
return { name: "overview" };
|
|
99
129
|
}
|
|
100
130
|
|
|
101
131
|
function routeLink(hash, text, routeName) {
|
|
102
|
-
const
|
|
132
|
+
const current = currentRoute().name;
|
|
133
|
+
const active = current === routeName || (routeName === "review" && current === "reviewTask");
|
|
103
134
|
return `<a class="${active ? "active" : ""}" href="${hash}">${escapeHtml(text)}</a>`;
|
|
104
135
|
}
|
|
105
136
|
|
|
@@ -121,6 +152,9 @@ function overview() {
|
|
|
121
152
|
|
|
122
153
|
function statusStrip() {
|
|
123
154
|
const status = bundle.status?.checkState?.status || "unknown";
|
|
155
|
+
const validationMode = bundle.status?.checkState?.validationMode || "validated";
|
|
156
|
+
const dataOnly = validationMode === "data-only";
|
|
157
|
+
const displayState = dataOnly ? "snapshot" : status;
|
|
124
158
|
const failures = bundle.status?.checkState?.failures || 0;
|
|
125
159
|
const warnings = bundle.status?.checkState?.warnings || 0;
|
|
126
160
|
const tasks = bundle.status?.tasks || [];
|
|
@@ -128,9 +162,9 @@ function statusStrip() {
|
|
|
128
162
|
const visual = summary.visualMapCoverage || {};
|
|
129
163
|
const withBrief = tasks.filter((task) => task.briefSource === "standalone").length;
|
|
130
164
|
return `<section class="status-card-group">
|
|
131
|
-
<div class="status-primary ${
|
|
132
|
-
<span>${t("readiness")}</span>
|
|
133
|
-
<strong>${label(status)}</strong>
|
|
165
|
+
<div class="status-primary ${displayState}">
|
|
166
|
+
<span>${dataOnly ? t("snapshotStatus") : t("readiness")}</span>
|
|
167
|
+
<strong>${dataOnly ? t("snapshot") : label(status)}</strong>
|
|
134
168
|
<p>${nextActionText()}</p>
|
|
135
169
|
</div>
|
|
136
170
|
<div class="metrics-grid">
|
|
@@ -151,6 +185,7 @@ function metric(labelText, value) {
|
|
|
151
185
|
}
|
|
152
186
|
|
|
153
187
|
function nextActionText() {
|
|
188
|
+
if ((bundle.status?.checkState?.validationMode || "validated") === "data-only") return t("snapshotNotValidated");
|
|
154
189
|
const failures = bundle.status?.checkState?.failures || 0;
|
|
155
190
|
if (failures > 0) return t("resolveBlockers");
|
|
156
191
|
const missingBriefs = (bundle.status?.tasks || []).filter((task) => task.briefSource !== "standalone").length;
|
|
@@ -270,24 +305,29 @@ function graphSummary() {
|
|
|
270
305
|
}
|
|
271
306
|
|
|
272
307
|
function activeTaskBriefs() {
|
|
273
|
-
const tasks = activeTasks()
|
|
308
|
+
const tasks = activeTasks();
|
|
274
309
|
return `<section class="task-briefs">
|
|
275
310
|
<div class="section-head">
|
|
276
311
|
<div>
|
|
277
312
|
<p class="eyebrow">${t("currentWork")}</p>
|
|
278
313
|
<h2>${t("activeBriefs")}</h2>
|
|
279
314
|
</div>
|
|
280
|
-
<
|
|
315
|
+
<div class="section-actions">
|
|
316
|
+
<span class="subtle">${t("activeBriefCount").replace("{count}", tasks.length).replace("{order}", taskSortLabel())}</span>
|
|
317
|
+
<a href="#/tasks">${t("openTaskIndex")}</a>
|
|
318
|
+
</div>
|
|
319
|
+
</div>
|
|
320
|
+
<div class="brief-scroll">
|
|
321
|
+
<div class="brief-grid">${tasks.map((task) => taskBriefCard(task, { compact: false })).join("") || emptyState(t("noActiveTasks"))}</div>
|
|
281
322
|
</div>
|
|
282
|
-
<div class="brief-grid">${tasks.map((task) => taskBriefCard(task, { compact: false })).join("") || emptyState(t("noActiveTasks"))}</div>
|
|
283
323
|
</section>`;
|
|
284
324
|
}
|
|
285
325
|
|
|
286
326
|
function activeTasks() {
|
|
287
327
|
const tasks = bundle.status?.tasks || [];
|
|
288
328
|
const active = tasks.filter((task) => isActiveTaskState(task.state) || ["planned", "not_started"].includes(task.state));
|
|
289
|
-
if (active.length > 0) return active;
|
|
290
|
-
return tasks.filter((task) => task.briefSource === "standalone")
|
|
329
|
+
if (active.length > 0) return sortTasksByTime(active);
|
|
330
|
+
return sortTasksByTime(tasks.filter((task) => task.briefSource === "standalone"));
|
|
291
331
|
}
|
|
292
332
|
|
|
293
333
|
function isActiveTaskState(state) {
|
|
@@ -310,6 +350,7 @@ function taskBriefCard(task, { compact = true } = {}) {
|
|
|
310
350
|
<p class="brief-teaser">${escapeHtml(summaryText)}</p>
|
|
311
351
|
</div>
|
|
312
352
|
<div class="card-actions">
|
|
353
|
+
${taskCopyButton(task)}
|
|
313
354
|
<button class="btn-drawer-trigger" data-open-drawer="${escapeAttr(task.id)}">${t("viewDetails")}</button>
|
|
314
355
|
</div>
|
|
315
356
|
</article>`;
|
|
@@ -349,6 +390,59 @@ function stateToColorVar(state) {
|
|
|
349
390
|
return map[state] || "--muted";
|
|
350
391
|
}
|
|
351
392
|
|
|
393
|
+
function taskSortLabel() {
|
|
394
|
+
return state.taskSortOrder === "asc" ? t("sortOldest") : t("sortNewest");
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
function taskDateKey(task) {
|
|
398
|
+
const source = `${task.shortId || ""} ${task.id || ""}`.trim();
|
|
399
|
+
const match = source.match(/(?:^|[^\d])(\d{4})-(\d{2})(?:-(\d{2}))?/);
|
|
400
|
+
if (!match) return null;
|
|
401
|
+
const year = Number(match[1]);
|
|
402
|
+
const month = Number(match[2]);
|
|
403
|
+
const day = Number(match[3] || "1");
|
|
404
|
+
if (!year || month < 1 || month > 12 || day < 1 || day > 31) return null;
|
|
405
|
+
return Date.UTC(year, month - 1, day);
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
function stableTaskLabel(task) {
|
|
409
|
+
return `${task.shortId || ""} ${task.id || ""} ${task.title || ""}`.trim();
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
function compareTasksByTime(left, right) {
|
|
413
|
+
const leftDate = taskDateKey(left);
|
|
414
|
+
const rightDate = taskDateKey(right);
|
|
415
|
+
if (leftDate !== null && rightDate !== null && leftDate !== rightDate) {
|
|
416
|
+
return state.taskSortOrder === "asc" ? leftDate - rightDate : rightDate - leftDate;
|
|
417
|
+
}
|
|
418
|
+
if (leftDate !== null && rightDate === null) return -1;
|
|
419
|
+
if (leftDate === null && rightDate !== null) return 1;
|
|
420
|
+
return stableTaskLabel(left).localeCompare(stableTaskLabel(right));
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
function sortTasksByTime(tasks) {
|
|
424
|
+
return [...tasks].sort(compareTasksByTime);
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
function taskFolderName(task) {
|
|
428
|
+
const fromPath = String(task?.path || "").split("/").filter(Boolean).pop();
|
|
429
|
+
const fromId = String(task?.id || "").split("/").filter(Boolean).pop();
|
|
430
|
+
return task?.shortId || fromPath || fromId || task?.title || "";
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
function taskCopyButton(task, extraClass = "") {
|
|
434
|
+
const folderName = taskFolderName(task);
|
|
435
|
+
return `<button type="button" class="copy-task-name ${extraClass}" data-copy-task-name="${escapeAttr(folderName)}" data-copy-task-folder="${escapeAttr(folderName)}" aria-label="${escapeAttr(t("copyTaskName"))}" title="${escapeAttr(t("copyTaskName"))}">
|
|
436
|
+
${t("copyTaskNameShort")}
|
|
437
|
+
</button>`;
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
function taskGroupTimeKey(group) {
|
|
441
|
+
const match = group.match(/^(?:month|legacy):(\d{4})-(\d{2})$/);
|
|
442
|
+
if (!match) return null;
|
|
443
|
+
return Date.UTC(Number(match[1]), Number(match[2]) - 1, 1);
|
|
444
|
+
}
|
|
445
|
+
|
|
352
446
|
function taskToolbarCard(filteredCount) {
|
|
353
447
|
return `<section class="sidebar-card">
|
|
354
448
|
<h3>${t("filterTitle")}</h3>
|
|
@@ -380,6 +474,17 @@ function taskToolbarCard(filteredCount) {
|
|
|
380
474
|
</button>
|
|
381
475
|
</div>
|
|
382
476
|
</div>
|
|
477
|
+
<div class="select-group">
|
|
478
|
+
<label>${t("sortByTime")}</label>
|
|
479
|
+
<div class="layout-toggle-group sort-toggle-group">
|
|
480
|
+
<button class="layout-btn ${state.taskSortOrder === "desc" ? "active" : ""}" data-task-sort-order="desc" aria-label="${t("sortNewest")}">
|
|
481
|
+
${t("sortNewest")}
|
|
482
|
+
</button>
|
|
483
|
+
<button class="layout-btn ${state.taskSortOrder === "asc" ? "active" : ""}" data-task-sort-order="asc" aria-label="${t("sortOldest")}">
|
|
484
|
+
${t("sortOldest")}
|
|
485
|
+
</button>
|
|
486
|
+
</div>
|
|
487
|
+
</div>
|
|
383
488
|
<div class="search-stats">
|
|
384
489
|
${t("showing")} <strong>${filteredCount}</strong> / ${(bundle.status?.tasks || []).length} ${t("tasks")}
|
|
385
490
|
</div>
|
|
@@ -483,11 +588,12 @@ function taskRow(task) {
|
|
|
483
588
|
const briefLabel = briefReady ? t("briefReady") : t("briefMissing");
|
|
484
589
|
const mapLabel = mapReady ? t("mapReady") : t("mapMissing");
|
|
485
590
|
|
|
486
|
-
return `<
|
|
591
|
+
return `<article class="task-row-card" data-open-drawer="${escapeAttr(task.id)}" style="--row-accent: var(${stateToColorVar(task.state)})">
|
|
487
592
|
<div class="row-accent-bar"></div>
|
|
488
593
|
<div class="row-main">
|
|
489
594
|
<strong>${escapeHtml(task.title)}</strong>
|
|
490
595
|
<span class="row-meta">${escapeHtml(task.id)} · ${escapeHtml(taskModuleKey(task))}</span>
|
|
596
|
+
${taskCopyButton(task, "row-copy")}
|
|
491
597
|
</div>
|
|
492
598
|
<div class="row-status">${tag(task.state)}</div>
|
|
493
599
|
<div class="row-progress">
|
|
@@ -506,7 +612,7 @@ function taskRow(task) {
|
|
|
506
612
|
${mapReady ? t("badgeMap") : t("badgeMapMissing")}
|
|
507
613
|
</span>
|
|
508
614
|
</div>
|
|
509
|
-
</
|
|
615
|
+
</article>`;
|
|
510
616
|
}
|
|
511
617
|
|
|
512
618
|
function taskIndex() {
|
|
@@ -545,7 +651,18 @@ function orderedTaskGroups(groups) {
|
|
|
545
651
|
if (group === "unknown") return 3;
|
|
546
652
|
return 4;
|
|
547
653
|
};
|
|
548
|
-
return Object.entries(groups).sort(([left], [right]) =>
|
|
654
|
+
return Object.entries(groups).sort(([left], [right]) => {
|
|
655
|
+
const rankDiff = rank(left) - rank(right);
|
|
656
|
+
if (rankDiff !== 0) return rankDiff;
|
|
657
|
+
const leftTime = taskGroupTimeKey(left);
|
|
658
|
+
const rightTime = taskGroupTimeKey(right);
|
|
659
|
+
if (leftTime !== null && rightTime !== null && leftTime !== rightTime) {
|
|
660
|
+
return state.taskSortOrder === "asc" ? leftTime - rightTime : rightTime - leftTime;
|
|
661
|
+
}
|
|
662
|
+
if (leftTime !== null && rightTime === null) return -1;
|
|
663
|
+
if (leftTime === null && rightTime !== null) return 1;
|
|
664
|
+
return left.localeCompare(right);
|
|
665
|
+
});
|
|
549
666
|
}
|
|
550
667
|
|
|
551
668
|
function taskGroups(tasks) {
|
|
@@ -570,11 +687,12 @@ function taskGroups(tasks) {
|
|
|
570
687
|
}
|
|
571
688
|
|
|
572
689
|
function taskGroup(group, tasks) {
|
|
573
|
-
const
|
|
690
|
+
const orderedTasks = sortTasksByTime(tasks);
|
|
691
|
+
const pageCount = Math.max(1, Math.ceil(orderedTasks.length / taskPageSize));
|
|
574
692
|
const page = Math.min(Math.max(1, Number(state.taskPageByGroup[group]) || 1), pageCount);
|
|
575
693
|
const start = (page - 1) * taskPageSize;
|
|
576
|
-
const visibleTasks =
|
|
577
|
-
const avgCompletion =
|
|
694
|
+
const visibleTasks = orderedTasks.slice(start, start + taskPageSize);
|
|
695
|
+
const avgCompletion = orderedTasks.length ? clampCompletion(orderedTasks.reduce((sum, task) => sum + clampCompletion(task.completion), 0) / orderedTasks.length) : 0;
|
|
578
696
|
|
|
579
697
|
const isGrid = state.taskLayout === "grid";
|
|
580
698
|
const layoutClass = isGrid ? "task-card-grid" : "task-list";
|
|
@@ -591,7 +709,7 @@ function taskGroup(group, tasks) {
|
|
|
591
709
|
<div class="section-head">
|
|
592
710
|
<div>
|
|
593
711
|
<h2>${taskGroupLabel(group)}</h2>
|
|
594
|
-
<p class="subtle">${t("showing")} ${Math.min(start + 1,
|
|
712
|
+
<p class="subtle">${t("showing")} ${Math.min(start + 1, orderedTasks.length)}-${Math.min(start + visibleTasks.length, orderedTasks.length)} / ${orderedTasks.length}</p>
|
|
595
713
|
</div>
|
|
596
714
|
<div class="group-actions">
|
|
597
715
|
<div class="group-progress" aria-label="${escapeAttr(t("groupCompletion"))}">
|
|
@@ -616,10 +734,13 @@ function taskCard(task) {
|
|
|
616
734
|
const briefLabel = briefReady ? t("briefReady") : t("briefMissing");
|
|
617
735
|
const mapLabel = mapReady ? t("mapReady") : t("mapMissing");
|
|
618
736
|
|
|
619
|
-
return `<
|
|
737
|
+
return `<article class="task-card" data-open-drawer="${escapeAttr(task.id)}" style="--row-accent: var(${stateColor})">
|
|
620
738
|
<div class="card-header">
|
|
621
739
|
<span class="card-id">${escapeHtml(task.id)}</span>
|
|
622
|
-
|
|
740
|
+
<div class="card-header-actions">
|
|
741
|
+
${taskCopyButton(task, "compact")}
|
|
742
|
+
${tag(task.state)}
|
|
743
|
+
</div>
|
|
623
744
|
</div>
|
|
624
745
|
<h4 class="card-title" title="${escapeAttr(task.title)}">${escapeHtml(task.title)}</h4>
|
|
625
746
|
<div class="card-meta">
|
|
@@ -642,7 +763,7 @@ function taskCard(task) {
|
|
|
642
763
|
${mapReady ? t("badgeMap") : t("badgeMapMissing")}
|
|
643
764
|
</span>
|
|
644
765
|
</div>
|
|
645
|
-
</
|
|
766
|
+
</article>`;
|
|
646
767
|
}
|
|
647
768
|
|
|
648
769
|
function taskGroupLabel(group) {
|
|
@@ -657,12 +778,12 @@ function taskGroupLabel(group) {
|
|
|
657
778
|
|
|
658
779
|
function filteredTasks() {
|
|
659
780
|
const query = state.query.trim().toLowerCase();
|
|
660
|
-
return (bundle.status?.tasks || []).filter((task) => {
|
|
781
|
+
return sortTasksByTime((bundle.status?.tasks || []).filter((task) => {
|
|
661
782
|
const stateMatch = state.taskState === "all" || task.state === state.taskState;
|
|
662
783
|
if (!stateMatch) return false;
|
|
663
784
|
if (!query) return true;
|
|
664
785
|
return [task.id, task.shortId, task.title, task.module, task.inferredModule, task.classificationSource, task.classificationBucket, task.state].some((value) => String(value || "").toLowerCase().includes(query));
|
|
665
|
-
});
|
|
786
|
+
}));
|
|
666
787
|
}
|
|
667
788
|
|
|
668
789
|
function taskModuleKey(task) {
|
|
@@ -680,6 +801,7 @@ function taskDetail(route) {
|
|
|
680
801
|
<p class="eyebrow">${t("taskVisibility")}</p>
|
|
681
802
|
<h2>${escapeHtml(task.title)}</h2>
|
|
682
803
|
<p>${escapeHtml(task.path)}</p>
|
|
804
|
+
${taskCopyButton(task, "detail-copy")}
|
|
683
805
|
</div>
|
|
684
806
|
<div class="detail-score">${task.completion}%</div>
|
|
685
807
|
</section>
|
|
@@ -690,7 +812,8 @@ function taskDetail(route) {
|
|
|
690
812
|
${taskDocumentLibrary(task, route.doc)}
|
|
691
813
|
</article>
|
|
692
814
|
<aside class="detail-side">
|
|
693
|
-
${reviewActionPanel(task)}
|
|
815
|
+
${reviewActionPanel(task, { mode: "summary" })}
|
|
816
|
+
${lessonCandidatePanel(task, { context: "detail" })}
|
|
694
817
|
${openFindings(task)}
|
|
695
818
|
${evidenceList(task)}
|
|
696
819
|
${documentTabs(task)}
|
|
@@ -713,25 +836,85 @@ function taskStateSummary(task) {
|
|
|
713
836
|
<span>${t("reviewStatus")}</span>
|
|
714
837
|
${tag(task.reviewStatus || "missing")}
|
|
715
838
|
</div>
|
|
839
|
+
<div>
|
|
840
|
+
<span>${t("sedimentationStatus")}</span>
|
|
841
|
+
${tag(task.lessonCandidateStatus || "missing")}
|
|
842
|
+
</div>
|
|
716
843
|
<div>
|
|
717
844
|
<span>${t("closeoutStatus")}</span>
|
|
718
845
|
${tag(task.closeoutStatus || "missing")}
|
|
719
846
|
</div>
|
|
847
|
+
<div>
|
|
848
|
+
<span>${t("lifecycleQueues")}</span>
|
|
849
|
+
${(task.taskQueues || []).map(tag).join("") || tag("active")}
|
|
850
|
+
</div>
|
|
851
|
+
${taskQueueReasonSummary(task)}
|
|
720
852
|
</section>`;
|
|
721
853
|
}
|
|
722
854
|
|
|
855
|
+
function taskQueueReasonSummary(task) {
|
|
856
|
+
const reasons = task.queueReasons || [];
|
|
857
|
+
if (!reasons.length) return "";
|
|
858
|
+
return `<div class="task-queue-reasons">
|
|
859
|
+
<span>${t("queueReasons")}</span>
|
|
860
|
+
<div class="review-reasons">
|
|
861
|
+
${reasons.slice(0, 5).map(reviewReason).join("")}
|
|
862
|
+
</div>
|
|
863
|
+
</div>`;
|
|
864
|
+
}
|
|
865
|
+
|
|
723
866
|
function phaseTimeline(task) {
|
|
867
|
+
const knownKinds = new Set(["init", "execution", "gate"]);
|
|
868
|
+
const groups = [
|
|
869
|
+
["init", "Init"],
|
|
870
|
+
["execution", "Execution"],
|
|
871
|
+
["gate", "Gate"],
|
|
872
|
+
["other", "Other / Invalid"],
|
|
873
|
+
];
|
|
874
|
+
const phases = task.phases || [];
|
|
875
|
+
const grouped = groups
|
|
876
|
+
.map(([kind, label]) => {
|
|
877
|
+
const items = kind === "other"
|
|
878
|
+
? phases.filter((phase) => !knownKinds.has(phase.kind || "execution"))
|
|
879
|
+
: phases.filter((phase) => (phase.kind || "execution") === kind);
|
|
880
|
+
if (!items.length) return "";
|
|
881
|
+
return `<div class="phase-kind-group ${escapeAttr(kind)}">
|
|
882
|
+
<h3>${escapeHtml(label)}</h3>
|
|
883
|
+
${items.map(phaseStep).join("")}
|
|
884
|
+
</div>`;
|
|
885
|
+
})
|
|
886
|
+
.join("");
|
|
724
887
|
return `<section class="phase-timeline">
|
|
725
888
|
<h2>${t("phaseTimeline")}</h2>
|
|
726
|
-
${
|
|
727
|
-
<strong>${escapeHtml(phase.id)}</strong>
|
|
728
|
-
<span>${phase.completion}%</span>
|
|
729
|
-
<p>${escapeHtml(phase.output || phase.blockingRisk || phase.state)}</p>
|
|
730
|
-
${progressBar(phase.completion)}
|
|
731
|
-
</div>`).join("") || emptyState(t("noPhaseData"))}
|
|
889
|
+
${grouped || emptyState(t("noPhaseData"))}
|
|
732
890
|
</section>`;
|
|
733
891
|
}
|
|
734
892
|
|
|
893
|
+
function phaseStep(phase) {
|
|
894
|
+
const kind = phase.kind || "execution";
|
|
895
|
+
const actor = phase.actor || "agent";
|
|
896
|
+
const knownKind = ["init", "execution", "gate"].includes(kind);
|
|
897
|
+
const kindLabel = knownKind ? escapeHtml(kind) : `<span class="tag warn">${escapeHtml(kind)}</span>`;
|
|
898
|
+
const phaseKindClass = knownKind ? kind : "other";
|
|
899
|
+
return `<div class="phase-step ${escapeAttr(phase.state)} ${escapeAttr(phaseKindClass)}">
|
|
900
|
+
<div class="phase-step-head">
|
|
901
|
+
<strong>${escapeHtml(phase.id)}</strong>
|
|
902
|
+
<span>${kindLabel} · ${phase.completion}%</span>
|
|
903
|
+
</div>
|
|
904
|
+
<p>${escapeHtml(phase.output || phase.blockingRisk || phase.state)}</p>
|
|
905
|
+
${progressBar(phase.completion)}
|
|
906
|
+
<div class="phase-meta">
|
|
907
|
+
${phaseMetaTag(actor)}
|
|
908
|
+
${tag(phase.evidenceStatus || "missing")}
|
|
909
|
+
</div>
|
|
910
|
+
${phase.exitCommand ? `<code class="phase-exit-command">${escapeHtml(phase.exitCommand)}</code>` : ""}
|
|
911
|
+
</div>`;
|
|
912
|
+
}
|
|
913
|
+
|
|
914
|
+
function phaseMetaTag(value) {
|
|
915
|
+
return `<span class="tag">${escapeHtml(String(value || "unknown").replaceAll("_", " "))}</span>`;
|
|
916
|
+
}
|
|
917
|
+
|
|
735
918
|
function taskDocSection(task, fileName, title, required) {
|
|
736
919
|
const doc = taskDocument(task, fileName);
|
|
737
920
|
if (!doc && !required) return "";
|
|
@@ -777,7 +960,7 @@ function taskDocumentPriority(task) {
|
|
|
777
960
|
const stateName = task?.state || "";
|
|
778
961
|
const lifecycle = task?.lifecycleState || "";
|
|
779
962
|
if (stateName === "review" || ["in_review", "review-blocked"].includes(lifecycle)) {
|
|
780
|
-
return ["walkthrough", "review", "findings", "visualMap", "progress", "brief", "taskPlan", "strategy", "legacyRoadmap", "references", "artifacts"];
|
|
963
|
+
return ["walkthrough", "lessonCandidates", "review", "findings", "visualMap", "progress", "brief", "taskPlan", "strategy", "longRunningContract", "legacyRoadmap", "references", "artifacts"];
|
|
781
964
|
}
|
|
782
965
|
if (stateName === "in_progress" || lifecycle === "active" || stateName === "blocked") {
|
|
783
966
|
return ["progress", "visualMap", "brief", "taskPlan", "strategy", "findings", "review", "walkthrough", "references", "artifacts", "legacyRoadmap"];
|
|
@@ -831,11 +1014,26 @@ function openFindings(task) {
|
|
|
831
1014
|
</section>`;
|
|
832
1015
|
}
|
|
833
1016
|
|
|
834
|
-
function reviewActionPanel(task) {
|
|
835
|
-
if (!
|
|
836
|
-
if (!isTaskInReviewStage(task)) return "";
|
|
1017
|
+
function reviewActionPanel(task, { mode = "summary" } = {}) {
|
|
1018
|
+
if (!isTaskInReviewQueue(task)) return "";
|
|
837
1019
|
const blocking = task.reviewStatus === "blocked-open-findings" || (task.risks || []).some((risk) => /^P[0-2]$/i.test(risk.severity || "") && (risk.open || risk.blocksRelease));
|
|
838
1020
|
const confirmed = task.reviewStatus === "confirmed";
|
|
1021
|
+
const candidateBlocked = task.budget !== "simple" && !task.lessonCandidateDecisionComplete;
|
|
1022
|
+
const candidateStatus = task.lessonCandidateStatus || "missing";
|
|
1023
|
+
if (mode !== "workspace") {
|
|
1024
|
+
return `<section class="side-panel review-actions">
|
|
1025
|
+
<h3>${t("reviewActions")}</h3>
|
|
1026
|
+
<p>${escapeHtml(confirmed ? t("reviewAlreadyConfirmed") : t("reviewOpenInWorkspace"))}</p>
|
|
1027
|
+
<p>${escapeHtml(t("lessonCandidateStatus"))}: ${tag(candidateStatus)}</p>
|
|
1028
|
+
<a href="#/review/${encodeURIComponent(task.id)}">${t("openReviewWorkspace")}</a>
|
|
1029
|
+
</section>`;
|
|
1030
|
+
}
|
|
1031
|
+
if (!canUseWorkbenchAction("review-complete")) {
|
|
1032
|
+
return `<section class="side-panel review-actions">
|
|
1033
|
+
<h3>${t("reviewActions")}</h3>
|
|
1034
|
+
<p>${escapeHtml(t("staticReadOnlyDetail"))}</p>
|
|
1035
|
+
</section>`;
|
|
1036
|
+
}
|
|
839
1037
|
if (confirmed) {
|
|
840
1038
|
return `<section class="side-panel review-actions">
|
|
841
1039
|
<h3>${t("reviewActions")}</h3>
|
|
@@ -843,26 +1041,32 @@ function reviewActionPanel(task) {
|
|
|
843
1041
|
</section>`;
|
|
844
1042
|
}
|
|
845
1043
|
const missingWalkthrough = task.budget !== "simple" && !task.walkthroughPath;
|
|
846
|
-
const
|
|
847
|
-
const
|
|
1044
|
+
const queueBlocked = !taskCanBeHumanConfirmed(task);
|
|
1045
|
+
const disabled = blocking || missingWalkthrough || candidateBlocked || queueBlocked;
|
|
1046
|
+
const message = missingWalkthrough ? t("reviewWalkthroughRequired") : blocking ? t("reviewBlocked") : candidateBlocked ? t("reviewCandidateDecisionRequired") : queueBlocked ? t("reviewQueueRequired") : t("reviewWorkbenchReady");
|
|
848
1047
|
return `<section class="side-panel review-actions">
|
|
849
1048
|
<h3>${t("reviewActions")}</h3>
|
|
850
1049
|
<p>${escapeHtml(message)}</p>
|
|
1050
|
+
<p>${escapeHtml(t("lessonCandidateStatus"))}: ${tag(candidateStatus)}</p>
|
|
851
1051
|
<label class="review-check">
|
|
852
1052
|
<input type="checkbox" data-review-confirm-check="${escapeAttr(task.id)}" ${disabled ? "disabled" : ""}>
|
|
853
1053
|
<span>${t("reviewConfirmChecklist")}</span>
|
|
854
1054
|
</label>
|
|
1055
|
+
<div class="review-confirm-copy">
|
|
1056
|
+
${taskCopyButton(task, "review-copy-task-name")}
|
|
1057
|
+
</div>
|
|
855
1058
|
<input data-review-confirm-text="${escapeAttr(task.id)}" value="" placeholder="${escapeAttr(task.shortId || task.id)}" ${disabled ? "disabled" : ""}>
|
|
856
1059
|
<button data-review-complete="${escapeAttr(task.id)}" ${disabled ? "disabled" : ""}>${t("confirmReviewComplete")}</button>
|
|
857
1060
|
<div class="review-result" data-review-result="${escapeAttr(task.id)}"></div>
|
|
858
1061
|
</section>`;
|
|
859
1062
|
}
|
|
860
1063
|
|
|
861
|
-
function
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
1064
|
+
function isTaskInReviewQueue(task) {
|
|
1065
|
+
return (task?.reviewQueueState || "not-in-queue") !== "not-in-queue";
|
|
1066
|
+
}
|
|
1067
|
+
|
|
1068
|
+
function taskCanBeHumanConfirmed(task) {
|
|
1069
|
+
return task?.reviewQueueState === "ready-to-confirm" && Array.isArray(task?.taskQueues) && task.taskQueues.includes("review");
|
|
866
1070
|
}
|
|
867
1071
|
|
|
868
1072
|
function evidenceList(task) {
|
|
@@ -933,10 +1137,16 @@ function moduleCard(module) {
|
|
|
933
1137
|
}
|
|
934
1138
|
|
|
935
1139
|
function reviewQueue() {
|
|
936
|
-
|
|
937
|
-
const
|
|
938
|
-
const
|
|
939
|
-
const
|
|
1140
|
+
ensureReviewQueueState();
|
|
1141
|
+
const tabs = reviewQueueTabs();
|
|
1142
|
+
const activeTab = tabs.find((tab) => tab.id === state.reviewQueueTab) || tabs[0];
|
|
1143
|
+
const baseTasks = reviewQueueBaseTasks(activeTab);
|
|
1144
|
+
const reasonOptions = reviewReasonOptions(baseTasks);
|
|
1145
|
+
normalizeReviewReasonFilter(reasonOptions);
|
|
1146
|
+
const tasks = reviewFilteredTasks(baseTasks);
|
|
1147
|
+
const pageCount = Math.max(1, Math.ceil(tasks.length / taskPageSize));
|
|
1148
|
+
const page = Math.min(Math.max(1, Number(state.reviewQueuePage) || 1), pageCount);
|
|
1149
|
+
const visibleTasks = tasks.slice((page - 1) * taskPageSize, page * taskPageSize);
|
|
940
1150
|
return `<div class="dashboard-grid review-queue-page">
|
|
941
1151
|
<main class="dashboard-main stack">
|
|
942
1152
|
<section class="flow-panel">
|
|
@@ -946,10 +1156,36 @@ function reviewQueue() {
|
|
|
946
1156
|
<h2>${t("reviewQueue")}</h2>
|
|
947
1157
|
<p class="subtle">${t("reviewQueueSubtitle")}</p>
|
|
948
1158
|
</div>
|
|
949
|
-
<span class="subtle">${
|
|
1159
|
+
<span class="subtle">${t("showing")} ${visibleTasks.length ? (page - 1) * taskPageSize + 1 : 0}-${Math.min(page * taskPageSize, tasks.length)} / ${tasks.length}</span>
|
|
1160
|
+
</div>
|
|
1161
|
+
<div class="review-queue-tabs" role="tablist" aria-label="${escapeAttr(t("reviewQueueTabs"))}">
|
|
1162
|
+
${tabs.map((tab) => reviewQueueTab(tab)).join("")}
|
|
1163
|
+
</div>
|
|
1164
|
+
<div class="review-queue-toolbar">
|
|
1165
|
+
<div class="input-group">
|
|
1166
|
+
<input data-search value="${escapeAttr(state.query)}" placeholder="${t("searchPlaceholder")}" aria-label="${t("searchTasks")}">
|
|
1167
|
+
</div>
|
|
1168
|
+
<div class="select-group">
|
|
1169
|
+
<label>${t("reasonFilter")}</label>
|
|
1170
|
+
<select data-review-reason-filter aria-label="${t("reasonFilter")}">
|
|
1171
|
+
<option value="all" ${state.reviewReasonFilter === "all" ? "selected" : ""}>${t("allReasons")}</option>
|
|
1172
|
+
${reasonOptions.map((code) => `<option value="${escapeAttr(code)}" ${state.reviewReasonFilter === code ? "selected" : ""}>${escapeHtml(code)}</option>`).join("")}
|
|
1173
|
+
</select>
|
|
1174
|
+
</div>
|
|
1175
|
+
<div class="select-group">
|
|
1176
|
+
<label>${t("sortBy")}</label>
|
|
1177
|
+
<select data-review-sort aria-label="${t("sortBy")}">
|
|
1178
|
+
${reviewSortOptions().map((option) => `<option value="${option.id}" ${state.reviewSort === option.id ? "selected" : ""}>${option.label}</option>`).join("")}
|
|
1179
|
+
</select>
|
|
1180
|
+
</div>
|
|
950
1181
|
</div>
|
|
951
|
-
<div class="
|
|
952
|
-
|
|
1182
|
+
<div class="review-queue-list-shell" tabindex="0" aria-label="${escapeAttr(activeTab.label)} ${escapeAttr(t("reviewQueue"))}">
|
|
1183
|
+
<div class="review-queue-list">
|
|
1184
|
+
${visibleTasks.map((task) => reviewQueueCard(task, activeTab)).join("") || emptyState(t("noQueueTasks"))}
|
|
1185
|
+
</div>
|
|
1186
|
+
</div>
|
|
1187
|
+
<div class="review-queue-pager">
|
|
1188
|
+
${pager("review", page, pageCount)}
|
|
953
1189
|
</div>
|
|
954
1190
|
</section>
|
|
955
1191
|
</main>
|
|
@@ -957,52 +1193,239 @@ function reviewQueue() {
|
|
|
957
1193
|
<section class="side-panel review-queue-summary">
|
|
958
1194
|
<h3>${t("reviewQueue")}</h3>
|
|
959
1195
|
<div class="review-queue-stats">
|
|
960
|
-
${metric(
|
|
961
|
-
${metric(t("reviewBlockedQueue"), blocked)}
|
|
962
|
-
${metric(t("reviewConfirmedQueue"), confirmed)}
|
|
1196
|
+
${tabs.map((tab) => metric(tab.label, reviewQueueBaseTasks(tab).length)).join("")}
|
|
963
1197
|
</div>
|
|
964
1198
|
</section>
|
|
965
1199
|
<section class="side-panel">
|
|
966
|
-
<h3>${
|
|
967
|
-
<p>${escapeHtml(
|
|
1200
|
+
<h3>${escapeHtml(activeTab.label)}</h3>
|
|
1201
|
+
<p>${escapeHtml(activeTab.description)}</p>
|
|
1202
|
+
<dl class="review-queue-contract">
|
|
1203
|
+
<div><dt>${t("reviewSubmitted")}</dt><dd>${reviewTruthyCount(baseTasks, "reviewSubmitted")}/${baseTasks.length}</dd></div>
|
|
1204
|
+
<div><dt>${t("materialsReady")}</dt><dd>${reviewTruthyCount(baseTasks, "materialsReady")}/${baseTasks.length}</dd></div>
|
|
1205
|
+
</dl>
|
|
968
1206
|
</section>
|
|
969
1207
|
</aside>
|
|
970
1208
|
</div>`;
|
|
971
1209
|
}
|
|
972
1210
|
|
|
973
|
-
function
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
1211
|
+
function ensureReviewQueueState() {
|
|
1212
|
+
if (!state.reviewQueueTab) state.reviewQueueTab = "review";
|
|
1213
|
+
if (!state.reviewReasonFilter) state.reviewReasonFilter = "all";
|
|
1214
|
+
if (!state.reviewSort) state.reviewSort = "queue";
|
|
1215
|
+
if (!state.reviewQueuePage) state.reviewQueuePage = 1;
|
|
1216
|
+
}
|
|
1217
|
+
|
|
1218
|
+
function reviewQueueTabs() {
|
|
1219
|
+
return [
|
|
1220
|
+
{ id: "review", queues: ["review"], label: t("queueReview"), description: t("queueReviewDesc") },
|
|
1221
|
+
{ id: "missing-materials", queues: ["missing-materials"], label: t("queueMissingMaterials"), description: t("queueMissingMaterialsDesc"), repair: true },
|
|
1222
|
+
{ id: "blocked", queues: ["blocked"], label: t("queueBlocked"), description: t("queueBlockedDesc"), repair: true },
|
|
1223
|
+
{ id: "lessons", queues: ["lessons"], label: t("queueLessons"), description: t("queueLessonsDesc") },
|
|
1224
|
+
{ id: "confirmed-finalized", queues: ["confirmed", "finalized", "confirmed-finalized", "confirmed-finalization-pending"], label: t("queueConfirmedFinalized"), description: t("queueConfirmedFinalizedDesc") },
|
|
1225
|
+
{ id: "soft-deleted-superseded", queues: ["soft-deleted-superseded"], label: t("queueSoftDeletedSuperseded"), description: t("queueSoftDeletedSupersededDesc") },
|
|
1226
|
+
];
|
|
977
1227
|
}
|
|
978
1228
|
|
|
979
|
-
function
|
|
980
|
-
const
|
|
981
|
-
|
|
1229
|
+
function reviewQueueTab(tab) {
|
|
1230
|
+
const active = tab.id === state.reviewQueueTab;
|
|
1231
|
+
const count = reviewQueueBaseTasks(tab).length;
|
|
1232
|
+
return `<button type="button" class="review-queue-tab ${active ? "active" : ""}" data-review-queue-tab="${escapeAttr(tab.id)}" role="tab" aria-selected="${active ? "true" : "false"}">
|
|
1233
|
+
<span>${escapeHtml(tab.label)}</span>
|
|
1234
|
+
<strong>${count}</strong>
|
|
1235
|
+
</button>`;
|
|
982
1236
|
}
|
|
983
1237
|
|
|
984
|
-
function
|
|
1238
|
+
function reviewSortOptions() {
|
|
1239
|
+
return [
|
|
1240
|
+
{ id: "queue", label: t("sortQueuePriority") },
|
|
1241
|
+
{ id: "newest", label: t("sortNewest") },
|
|
1242
|
+
{ id: "oldest", label: t("sortOldest") },
|
|
1243
|
+
{ id: "id", label: t("sortTaskId") },
|
|
1244
|
+
];
|
|
1245
|
+
}
|
|
1246
|
+
|
|
1247
|
+
function reviewQueueBaseTasks(tab) {
|
|
1248
|
+
return (bundle.status?.tasks || []).filter((task) => taskMatchesReviewTab(task, tab));
|
|
1249
|
+
}
|
|
1250
|
+
|
|
1251
|
+
function taskMatchesReviewTab(task, tab) {
|
|
1252
|
+
const queues = reviewTaskQueues(task);
|
|
1253
|
+
return (tab.queues || []).some((queue) => queues.includes(queue));
|
|
1254
|
+
}
|
|
1255
|
+
|
|
1256
|
+
function reviewTaskQueues(task) {
|
|
1257
|
+
return Array.isArray(task?.taskQueues) ? task.taskQueues : Array.isArray(task?.queues) ? task.queues : [];
|
|
1258
|
+
}
|
|
1259
|
+
|
|
1260
|
+
function reviewReasonOptions(tasks) {
|
|
1261
|
+
return [...new Set(tasks.flatMap((task) => (task.queueReasons || []).map((reason) => reason.code || reason.queue || "").filter(Boolean)))].sort();
|
|
1262
|
+
}
|
|
1263
|
+
|
|
1264
|
+
function normalizeReviewReasonFilter(reasonOptions) {
|
|
1265
|
+
const current = state.reviewReasonFilter || "all";
|
|
1266
|
+
if (current === "all") return;
|
|
1267
|
+
if (!reasonOptions.includes(current)) state.reviewReasonFilter = "all";
|
|
1268
|
+
}
|
|
1269
|
+
|
|
1270
|
+
function reviewFilteredTasks(tasks) {
|
|
1271
|
+
const query = state.query.trim().toLowerCase();
|
|
1272
|
+
const reasonFilter = state.reviewReasonFilter || "all";
|
|
1273
|
+
return [...tasks]
|
|
1274
|
+
.filter((task) => {
|
|
1275
|
+
if (reasonFilter !== "all" && !(task.queueReasons || []).some((reason) => (reason.code || reason.queue) === reasonFilter)) return false;
|
|
1276
|
+
if (!query) return true;
|
|
1277
|
+
return [
|
|
1278
|
+
task.id,
|
|
1279
|
+
task.shortId,
|
|
1280
|
+
task.title,
|
|
1281
|
+
task.module,
|
|
1282
|
+
task.inferredModule,
|
|
1283
|
+
task.state,
|
|
1284
|
+
task.lifecycleState,
|
|
1285
|
+
task.reviewStatus,
|
|
1286
|
+
task.closeoutStatus,
|
|
1287
|
+
...(task.taskQueues || []),
|
|
1288
|
+
...(task.queueReasons || []).flatMap((reason) => [reason.code, reason.message, reason.sourcePath]),
|
|
1289
|
+
].some((value) => String(value || "").toLowerCase().includes(query));
|
|
1290
|
+
})
|
|
1291
|
+
.sort(reviewTaskSort);
|
|
1292
|
+
}
|
|
1293
|
+
|
|
1294
|
+
function reviewTaskSort(left, right) {
|
|
1295
|
+
if (state.reviewSort === "newest") return compareTasksByTimeForOrder(left, right, "desc");
|
|
1296
|
+
if (state.reviewSort === "oldest") return compareTasksByTimeForOrder(left, right, "asc");
|
|
1297
|
+
if (state.reviewSort === "id") return stableTaskLabel(left).localeCompare(stableTaskLabel(right));
|
|
1298
|
+
return reviewPriorityRank(left) - reviewPriorityRank(right)
|
|
1299
|
+
|| compareTasksByTimeForOrder(left, right, "desc")
|
|
1300
|
+
|| stableTaskLabel(left).localeCompare(stableTaskLabel(right));
|
|
1301
|
+
}
|
|
1302
|
+
|
|
1303
|
+
function compareTasksByTimeForOrder(left, right, order) {
|
|
1304
|
+
const previous = state.taskSortOrder;
|
|
1305
|
+
state.taskSortOrder = order;
|
|
1306
|
+
const result = compareTasksByTime(left, right);
|
|
1307
|
+
state.taskSortOrder = previous;
|
|
1308
|
+
return result;
|
|
1309
|
+
}
|
|
1310
|
+
|
|
1311
|
+
function reviewPriorityRank(task) {
|
|
1312
|
+
const severityRank = { P0: 0, P1: 1, P2: 2, P3: 3 };
|
|
1313
|
+
const reasonRank = Math.min(...(task.queueReasons || []).map((reason) => severityRank[String(reason.severity || "").toUpperCase()] ?? 8), 8);
|
|
1314
|
+
const queueRank = { blocked: 0, "missing-materials": 1, review: 2, lessons: 3, confirmed: 4, finalized: 5, "soft-deleted-superseded": 6 };
|
|
1315
|
+
const queues = reviewTaskQueues(task);
|
|
1316
|
+
const taskQueueRank = Math.min(...queues.map((queue) => queueRank[queue] ?? 7), 7);
|
|
1317
|
+
return Math.min(reasonRank, taskQueueRank);
|
|
1318
|
+
}
|
|
1319
|
+
|
|
1320
|
+
function reviewTruthyCount(tasks, key) {
|
|
1321
|
+
return tasks.filter((task) => task[key] === true).length;
|
|
1322
|
+
}
|
|
1323
|
+
|
|
1324
|
+
function reviewQueueCard(task, tab) {
|
|
985
1325
|
const openMaterial = (task.risks || []).filter((risk) => /^P[0-2]$/i.test(risk.severity || "") && (risk.open || risk.blocksRelease)).length;
|
|
1326
|
+
const reasons = task.queueReasons || [];
|
|
1327
|
+
const canCopyRepairPrompt = tab?.repair && String(task.repairPrompt || "").trim();
|
|
1328
|
+
const lessonActions = tab?.id === "lessons" ? lessonCandidatePanel(task, { context: "card", limit: 2 }) : "";
|
|
1329
|
+
const displayId = task.shortId || taskFolderName(task) || task.id;
|
|
986
1330
|
return `<article class="task-card review-queue-card" style="--row-accent: var(${stateToColorVar(task.state)})">
|
|
987
1331
|
<div class="card-header">
|
|
988
|
-
<span class="card-id"
|
|
1332
|
+
<span class="card-id" title="${escapeAttr(task.id)}">${escapeHtml(displayId)}</span>
|
|
989
1333
|
${tag(task.reviewStatus || "missing")}
|
|
1334
|
+
${reviewTaskQueues(task).map(tag).join("")}
|
|
990
1335
|
</div>
|
|
991
1336
|
<h4 class="card-title" title="${escapeAttr(task.title)}">${escapeHtml(task.title)}</h4>
|
|
992
1337
|
<div class="card-meta">
|
|
993
1338
|
<span>${tag(task.lifecycleState || "unknown")}</span>
|
|
994
1339
|
<span>${tag(task.closeoutStatus || "missing")}</span>
|
|
995
1340
|
<span>${openMaterial} ${t("openFindings")}</span>
|
|
1341
|
+
<span>${t("reviewSubmitted")}: ${task.reviewSubmitted === true ? t("yes") : t("no")}</span>
|
|
1342
|
+
<span>${t("materialsReady")}: ${task.materialsReady === true ? t("yes") : t("no")}</span>
|
|
996
1343
|
</div>
|
|
997
1344
|
<p class="subtle">${escapeHtml(firstUsefulLine(task.summary || task.briefText || ""))}</p>
|
|
1345
|
+
${reasons.length ? `<div class="review-reasons">${reasons.slice(0, 4).map(reviewReason).join("")}</div>` : ""}
|
|
1346
|
+
${lessonActions}
|
|
998
1347
|
<div class="review-queue-actions">
|
|
1348
|
+
<a href="#/review/${encodeURIComponent(task.id)}">${t("openReviewWorkspace")}</a>
|
|
999
1349
|
<a href="#/tasks/${encodeURIComponent(task.id)}">${t("fullView")}</a>
|
|
1000
1350
|
<button data-open-drawer="${escapeAttr(task.id)}">${t("viewDetails")}</button>
|
|
1351
|
+
${tab?.repair ? `<button data-copy-repair-prompt="${escapeAttr(task.id)}" data-repair-prompt="${escapeAttr(task.repairPrompt || "")}" ${canCopyRepairPrompt ? "" : "disabled"}>${t("copyRepairPrompt")}</button>` : ""}
|
|
1001
1352
|
</div>
|
|
1002
|
-
${reviewActionPanel(task)}
|
|
1003
1353
|
</article>`;
|
|
1004
1354
|
}
|
|
1005
1355
|
|
|
1356
|
+
function lessonCandidatePanel(task, { context = "detail", limit = 0 } = {}) {
|
|
1357
|
+
const candidates = (task.lessonCandidateRows || []).filter((candidate) => ["ready-for-review", "needs-promotion"].includes(candidate.status));
|
|
1358
|
+
if (!candidates.length) return "";
|
|
1359
|
+
const visibleCandidates = limit > 0 ? candidates.slice(0, limit) : candidates;
|
|
1360
|
+
const hiddenCount = Math.max(0, candidates.length - visibleCandidates.length);
|
|
1361
|
+
const staticNote = canUseWorkbenchAction("lesson-sedimentation-task") ? "" : `<p class="lesson-action-note">${escapeHtml(t("lessonWorkbenchRequired"))}</p>`;
|
|
1362
|
+
return `<section class="lesson-candidate-panel ${context === "card" ? "compact" : ""}">
|
|
1363
|
+
<div class="lesson-candidate-panel-head">
|
|
1364
|
+
<div>
|
|
1365
|
+
<p class="eyebrow">${t("lessonCandidates")}</p>
|
|
1366
|
+
<h3>${t("lessonSedimentationActions")}</h3>
|
|
1367
|
+
</div>
|
|
1368
|
+
<span class="tag">${visibleCandidates.length}/${candidates.length}</span>
|
|
1369
|
+
</div>
|
|
1370
|
+
${staticNote}
|
|
1371
|
+
<div class="lesson-candidate-actions">
|
|
1372
|
+
${visibleCandidates.map((candidate) => lessonCandidateAction(task, candidate)).join("")}
|
|
1373
|
+
</div>
|
|
1374
|
+
${hiddenCount ? `<a class="lesson-candidate-more" href="#/review/${encodeURIComponent(task.id)}">${escapeHtml(t("moreLessonCandidates")).replace("{count}", String(hiddenCount))}</a>` : ""}
|
|
1375
|
+
</section>`;
|
|
1376
|
+
}
|
|
1377
|
+
|
|
1378
|
+
function lessonCandidateAction(task, candidate) {
|
|
1379
|
+
const followUp = String(candidate.followUpTask || "").trim();
|
|
1380
|
+
const hasFollowUp = followUp && !/^pending$/i.test(followUp);
|
|
1381
|
+
const prompt = lessonSedimentationPrompt(task, candidate);
|
|
1382
|
+
return `<div class="lesson-candidate-action">
|
|
1383
|
+
<div class="lesson-candidate-main">
|
|
1384
|
+
<strong>${escapeHtml(candidate.id)}</strong>
|
|
1385
|
+
<span>${escapeHtml(candidate.title || candidate.promotionTarget || t("lessonCandidates"))}</span>
|
|
1386
|
+
<small>${escapeHtml(candidate.scope || t("none"))} · ${escapeHtml(candidate.promotionTarget || t("none"))}</small>
|
|
1387
|
+
</div>
|
|
1388
|
+
<span class="review-result" data-lesson-result="${escapeAttr(task.id)}:${escapeAttr(candidate.id)}"></span>
|
|
1389
|
+
<div class="lesson-candidate-command-row">
|
|
1390
|
+
${hasFollowUp ? `<a href="#/tasks/${encodeURIComponent(followUp)}">${t("openFollowUpTask")}</a>` : ""}
|
|
1391
|
+
<button data-copy-lesson-prompt="${escapeAttr(task.id)}:${escapeAttr(candidate.id)}" data-lesson-prompt="${escapeAttr(prompt)}">${t("copyLessonPrompt")}</button>
|
|
1392
|
+
<button data-create-lesson-sedimentation="${escapeAttr(task.id)}" data-candidate-id="${escapeAttr(candidate.id)}" ${canUseWorkbenchAction("lesson-sedimentation-task") && !hasFollowUp ? "" : "disabled"}>${t("createLessonTask")}</button>
|
|
1393
|
+
</div>
|
|
1394
|
+
</div>`;
|
|
1395
|
+
}
|
|
1396
|
+
|
|
1397
|
+
function lessonSedimentationPrompt(task, candidate) {
|
|
1398
|
+
return [
|
|
1399
|
+
"You are executing a lesson sedimentation follow-up task.",
|
|
1400
|
+
"",
|
|
1401
|
+
`Source task: ${task.id}`,
|
|
1402
|
+
`Source candidate: ${candidate.id} - ${candidate.title || ""}`,
|
|
1403
|
+
`Candidate scope: ${candidate.scope || "unspecified"}`,
|
|
1404
|
+
`Candidate module key: ${candidate.moduleKey || "n/a"}`,
|
|
1405
|
+
`Detail artifact: ${candidate.detailArtifact || "not provided"}`,
|
|
1406
|
+
`Boundary reason: ${candidate.boundaryReason || "unspecified"}`,
|
|
1407
|
+
`Why it might matter: ${candidate.whyItMightMatter || "unspecified"}`,
|
|
1408
|
+
`Promotion target: ${candidate.promotionTarget || "unspecified"}`,
|
|
1409
|
+
`Conflict check: ${candidate.conflictCheck || "pending"}`,
|
|
1410
|
+
`Required standard update: ${candidate.requiredStandardUpdate || "pending"}`,
|
|
1411
|
+
"",
|
|
1412
|
+
"Instructions:",
|
|
1413
|
+
"1. Read the source task, review, findings, progress, lesson_candidates.md, and the task-local detail artifact.",
|
|
1414
|
+
"2. Use the detail artifact as the lesson body source; do not reconstruct the lesson from the brief row.",
|
|
1415
|
+
"3. Classify whether the lesson is task-local, module-local, or global, preserving the module key and source path when present.",
|
|
1416
|
+
"4. Check conflicts against existing lessons and standards.",
|
|
1417
|
+
"5. Propose the smallest diff first.",
|
|
1418
|
+
"6. Do not write a shared Lessons table; use task-local candidates and promoted detail docs.",
|
|
1419
|
+
].join("\n");
|
|
1420
|
+
}
|
|
1421
|
+
|
|
1422
|
+
function reviewReason(reason) {
|
|
1423
|
+
return `<div class="review-reason">
|
|
1424
|
+
<strong>${escapeHtml(reason.code || reason.queue || t("reason"))}</strong>
|
|
1425
|
+
<span>${escapeHtml(reason.message || reason.sourcePath || "")}</span>
|
|
1426
|
+
</div>`;
|
|
1427
|
+
}
|
|
1428
|
+
|
|
1006
1429
|
function firstUsefulLine(text) {
|
|
1007
1430
|
return String(text || "")
|
|
1008
1431
|
.split(/\n+/)
|
|
@@ -1010,6 +1433,57 @@ function firstUsefulLine(text) {
|
|
|
1010
1433
|
.filter(Boolean)[0] || "";
|
|
1011
1434
|
}
|
|
1012
1435
|
|
|
1436
|
+
function reviewWorkspace(route) {
|
|
1437
|
+
const task = (bundle.status?.tasks || []).find((item) => item.id === route.id);
|
|
1438
|
+
if (!task) return `<main>${emptyState(t("taskNotFound"))}</main>`;
|
|
1439
|
+
const walkthroughDoc = taskDocument(task, "__walkthrough__");
|
|
1440
|
+
const candidateDoc = taskDocument(task, "lesson_candidates.md");
|
|
1441
|
+
const reviewDoc = taskDocument(task, "review.md");
|
|
1442
|
+
const findingsDoc = taskDocument(task, "findings.md");
|
|
1443
|
+
return `<main class="review-workspace">
|
|
1444
|
+
<nav class="crumbs"><a href="#/review">${t("reviewQueue")}</a><span>/</span><span>${escapeHtml(task.id)}</span></nav>
|
|
1445
|
+
<section class="detail-hero review-hero">
|
|
1446
|
+
<div>
|
|
1447
|
+
<p class="eyebrow">${t("reviewWorkspace")}</p>
|
|
1448
|
+
<h2>${escapeHtml(task.title)}</h2>
|
|
1449
|
+
<p>${escapeHtml(task.path)}</p>
|
|
1450
|
+
</div>
|
|
1451
|
+
<div class="review-hero-tags">
|
|
1452
|
+
${tag(task.lifecycleState || "unknown")}
|
|
1453
|
+
${tag(task.reviewStatus || "missing")}
|
|
1454
|
+
${tag(task.lessonCandidateStatus || "missing")}
|
|
1455
|
+
</div>
|
|
1456
|
+
</section>
|
|
1457
|
+
<section class="review-workspace-grid">
|
|
1458
|
+
<article class="review-workspace-main stack">
|
|
1459
|
+
${reviewDocPanel("walkthrough", walkthroughDoc, task.walkthroughPath)}
|
|
1460
|
+
${reviewDocPanel("lessonCandidates", candidateDoc, task.lessonCandidatePath)}
|
|
1461
|
+
${reviewDocPanel("review", reviewDoc, task.reviewPath)}
|
|
1462
|
+
${reviewDocPanel("findings", findingsDoc, task.findingsPath)}
|
|
1463
|
+
</article>
|
|
1464
|
+
<aside class="review-workspace-side stack">
|
|
1465
|
+
${reviewActionPanel(task, { mode: "workspace" })}
|
|
1466
|
+
${taskStateSummary(task)}
|
|
1467
|
+
${openFindings(task)}
|
|
1468
|
+
${evidenceList(task)}
|
|
1469
|
+
</aside>
|
|
1470
|
+
</section>
|
|
1471
|
+
</main>`;
|
|
1472
|
+
}
|
|
1473
|
+
|
|
1474
|
+
function reviewDocPanel(key, doc, fallbackPath = "") {
|
|
1475
|
+
return `<section class="doc-section review-doc-panel">
|
|
1476
|
+
<div class="section-head">
|
|
1477
|
+
<div>
|
|
1478
|
+
<p class="eyebrow">${escapeHtml(fallbackPath || "")}</p>
|
|
1479
|
+
<h2>${t(key)}</h2>
|
|
1480
|
+
</div>
|
|
1481
|
+
${doc ? `<button data-render-toggle>${state.renderMode === "rendered" ? t("source") : t("rendered")}</button>` : ""}
|
|
1482
|
+
</div>
|
|
1483
|
+
<div class="review-doc-scroll"><div class="markdown">${doc ? window.HarnessMarkdown.render(doc.content, state.renderMode) : emptyState(t("documentMissing"))}</div></div>
|
|
1484
|
+
</section>`;
|
|
1485
|
+
}
|
|
1486
|
+
|
|
1013
1487
|
function migrationPanel() {
|
|
1014
1488
|
const advice = warningQueue();
|
|
1015
1489
|
const missingBriefs = advice.filter((warning) => warning.type === "missing-brief").length;
|
|
@@ -1150,25 +1624,39 @@ function pager(kind, page, pageCount, group = "") {
|
|
|
1150
1624
|
}
|
|
1151
1625
|
|
|
1152
1626
|
function lessonPanel() {
|
|
1153
|
-
const lessons = (
|
|
1154
|
-
.filter((table) => table.kind === "lessons-ssot")
|
|
1155
|
-
.flatMap((table) => table.rows);
|
|
1627
|
+
const lessons = lessonDocuments();
|
|
1156
1628
|
return `<section class="lesson-panel">
|
|
1157
1629
|
<div class="section-head"><h2>${t("lessons")}</h2><span>${lessons.length}</span></div>
|
|
1158
1630
|
<div class="lesson-list" style="padding-top: 10px;">
|
|
1159
|
-
${lessons.map((
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
return `<div class="lesson" data-open-lesson-drawer="${escapeAttr(lessonId)}">
|
|
1164
|
-
<strong>${escapeHtml(lessonId)}</strong>
|
|
1165
|
-
<p>${escapeHtml(summary)}</p>
|
|
1631
|
+
${lessons.map((lesson) => {
|
|
1632
|
+
return `<div class="lesson" data-open-lesson-drawer="${escapeAttr(lesson.id)}">
|
|
1633
|
+
<strong>${escapeHtml(lesson.id)}</strong>
|
|
1634
|
+
<p>${escapeHtml(lesson.title || lesson.path)}</p>
|
|
1166
1635
|
</div>`;
|
|
1167
1636
|
}).join("") || emptyState(t("noLessons"))}
|
|
1168
1637
|
</div>
|
|
1169
1638
|
</section>`;
|
|
1170
1639
|
}
|
|
1171
1640
|
|
|
1641
|
+
function lessonDocuments() {
|
|
1642
|
+
return (bundle.documents?.documents || [])
|
|
1643
|
+
.filter((doc) => doc.type === "lesson-detail" || /\/01-GOVERNANCE\/lessons\/[^/]+\.md$/i.test(doc.path || ""))
|
|
1644
|
+
.map((doc) => {
|
|
1645
|
+
const id = lessonIdFromDocument(doc);
|
|
1646
|
+
return { id, title: (doc.title || "").replace(new RegExp(`^${id}\\s*-\\s*`, "i"), ""), path: doc.path, doc };
|
|
1647
|
+
})
|
|
1648
|
+
.filter((lesson) => lesson.id)
|
|
1649
|
+
.sort((left, right) => String(right.id).localeCompare(String(left.id)));
|
|
1650
|
+
}
|
|
1651
|
+
|
|
1652
|
+
function lessonIdFromDocument(doc) {
|
|
1653
|
+
const content = doc?.content || "";
|
|
1654
|
+
const path = doc?.path || "";
|
|
1655
|
+
return content.match(/#\s*(L-\d{4}(?:-\d{2}-\d{2})?-\d+)/i)?.[1]
|
|
1656
|
+
|| path.match(/(L-\d{4}(?:-\d{2}-\d{2})?-\d+)/i)?.[1]
|
|
1657
|
+
|| "";
|
|
1658
|
+
}
|
|
1659
|
+
|
|
1172
1660
|
function healthPanel() {
|
|
1173
1661
|
const details = bundle.status?.checkState?.details || { failures: [], warnings: [] };
|
|
1174
1662
|
return `<section class="health-panel">
|
|
@@ -1180,6 +1668,382 @@ function healthPanel() {
|
|
|
1180
1668
|
</section>`;
|
|
1181
1669
|
}
|
|
1182
1670
|
|
|
1671
|
+
function presetsView() {
|
|
1672
|
+
ensurePresetState();
|
|
1673
|
+
const catalog = bundle.presetCatalog || { summary: {}, roots: [], presets: [] };
|
|
1674
|
+
let presets = filteredPresets();
|
|
1675
|
+
syncVisiblePresetSelection(presets);
|
|
1676
|
+
presets = filteredPresets();
|
|
1677
|
+
const selected = selectedPreset(presets);
|
|
1678
|
+
syncPresetUninstallScope(selected);
|
|
1679
|
+
return `<div class="presets-page stack">
|
|
1680
|
+
<section class="flow-panel preset-command-center">
|
|
1681
|
+
<div class="section-head">
|
|
1682
|
+
<div>
|
|
1683
|
+
<p class="eyebrow">${t("presetCatalog")}</p>
|
|
1684
|
+
<h2>${t("presetCatalog")}</h2>
|
|
1685
|
+
<p class="subtle">${t("presetCatalogSubtitle")}</p>
|
|
1686
|
+
</div>
|
|
1687
|
+
<span class="preset-count-pill">${presets.length}/${catalog.summary?.total || 0}</span>
|
|
1688
|
+
</div>
|
|
1689
|
+
<div class="preset-priority-strip" aria-label="${escapeAttr(t("presetPriorityTitle"))}">
|
|
1690
|
+
${presetPriorityStep("project", 1)}
|
|
1691
|
+
${presetPriorityStep("user", 2)}
|
|
1692
|
+
${presetPriorityStep("builtin", 3)}
|
|
1693
|
+
</div>
|
|
1694
|
+
<div class="preset-toolbar">
|
|
1695
|
+
<div class="input-group">
|
|
1696
|
+
<input data-preset-search value="${escapeAttr(state.presetQuery)}" placeholder="${escapeAttr(t("presetSearchPlaceholder"))}" aria-label="${escapeAttr(t("presetSearch"))}">
|
|
1697
|
+
</div>
|
|
1698
|
+
<div class="preset-source-tabs" role="tablist" aria-label="${escapeAttr(t("presetSourceFilter"))}">
|
|
1699
|
+
${presetSourceOptions().map((source) => presetSourceButton(source)).join("")}
|
|
1700
|
+
</div>
|
|
1701
|
+
</div>
|
|
1702
|
+
</section>
|
|
1703
|
+
<section class="preset-workspace">
|
|
1704
|
+
<div class="flow-panel preset-collection-panel">
|
|
1705
|
+
<div class="preset-panel-heading">
|
|
1706
|
+
<div>
|
|
1707
|
+
<h3>${t("presetCollection")}</h3>
|
|
1708
|
+
<p>${t("presetCollectionHint")}</p>
|
|
1709
|
+
</div>
|
|
1710
|
+
</div>
|
|
1711
|
+
<div class="preset-catalog-list">
|
|
1712
|
+
${presets.map((preset) => presetCard(preset, selected ? presetKey(selected) : "")).join("") || emptyState(t("noPresets"))}
|
|
1713
|
+
</div>
|
|
1714
|
+
</div>
|
|
1715
|
+
<div class="preset-detail-workspace stack">
|
|
1716
|
+
${presetDetailPanel(selected)}
|
|
1717
|
+
${presetLayerStackPanel(selected)}
|
|
1718
|
+
</div>
|
|
1719
|
+
<aside class="preset-context-actions stack">
|
|
1720
|
+
${presetActionPanel(selected)}
|
|
1721
|
+
${presetImportPanel()}
|
|
1722
|
+
${presetRestorePanel()}
|
|
1723
|
+
${presetSummaryPanel(catalog)}
|
|
1724
|
+
</aside>
|
|
1725
|
+
</section>
|
|
1726
|
+
</div>`;
|
|
1727
|
+
}
|
|
1728
|
+
|
|
1729
|
+
function ensurePresetState() {
|
|
1730
|
+
const presets = bundle.presetCatalog?.presets || [];
|
|
1731
|
+
if (!state.selectedPresetKey && state.selectedPresetId) {
|
|
1732
|
+
const legacySelection = presets.find((preset) => preset.id === state.selectedPresetId);
|
|
1733
|
+
if (legacySelection) state.selectedPresetKey = presetKey(legacySelection);
|
|
1734
|
+
}
|
|
1735
|
+
if (!state.selectedPresetKey && presets[0]) {
|
|
1736
|
+
state.selectedPresetKey = presetKey(presets[0]);
|
|
1737
|
+
state.presetUninstallConfirm = "";
|
|
1738
|
+
}
|
|
1739
|
+
if (state.selectedPresetKey && !presets.some((preset) => presetKey(preset) === state.selectedPresetKey) && presets[0]) {
|
|
1740
|
+
state.selectedPresetKey = presetKey(presets[0]);
|
|
1741
|
+
state.presetUninstallConfirm = "";
|
|
1742
|
+
}
|
|
1743
|
+
}
|
|
1744
|
+
|
|
1745
|
+
function presetSourceOptions() {
|
|
1746
|
+
return [
|
|
1747
|
+
["all", t("allPresets")],
|
|
1748
|
+
["project", t("presetSourceProject")],
|
|
1749
|
+
["user", t("presetSourceUser")],
|
|
1750
|
+
["builtin", t("presetSourceBuiltin")],
|
|
1751
|
+
];
|
|
1752
|
+
}
|
|
1753
|
+
|
|
1754
|
+
function presetSourceButton([source, labelText]) {
|
|
1755
|
+
const active = state.presetSourceFilter === source;
|
|
1756
|
+
const count = source === "all" ? (bundle.presetCatalog?.summary?.total || 0) : (bundle.presetCatalog?.summary?.[source] || 0);
|
|
1757
|
+
return `<button type="button" class="${active ? "active" : ""}" data-preset-source-filter="${escapeAttr(source)}" role="tab" aria-selected="${active ? "true" : "false"}">
|
|
1758
|
+
<span>${escapeHtml(labelText)}</span>
|
|
1759
|
+
<strong>${count}</strong>
|
|
1760
|
+
</button>`;
|
|
1761
|
+
}
|
|
1762
|
+
|
|
1763
|
+
function filteredPresets() {
|
|
1764
|
+
const query = String(state.presetQuery || "").trim().toLowerCase();
|
|
1765
|
+
return (bundle.presetCatalog?.presets || []).filter((preset) => {
|
|
1766
|
+
if (state.presetSourceFilter !== "all" && preset.source !== state.presetSourceFilter) return false;
|
|
1767
|
+
return presetMatchesQuery(preset, query);
|
|
1768
|
+
});
|
|
1769
|
+
}
|
|
1770
|
+
|
|
1771
|
+
function presetMatchesQuery(preset, query = state.presetQuery) {
|
|
1772
|
+
const normalizedQuery = String(query || "").trim().toLowerCase();
|
|
1773
|
+
if (!normalizedQuery) return true;
|
|
1774
|
+
return [
|
|
1775
|
+
preset.id,
|
|
1776
|
+
preset.source,
|
|
1777
|
+
preset.purpose,
|
|
1778
|
+
preset.taskKind,
|
|
1779
|
+
preset.manifestPath,
|
|
1780
|
+
preset.version,
|
|
1781
|
+
...(preset.compatibleBudgets || []),
|
|
1782
|
+
].some((value) => String(value || "").toLowerCase().includes(normalizedQuery));
|
|
1783
|
+
}
|
|
1784
|
+
|
|
1785
|
+
function syncVisiblePresetSelection(visiblePresets) {
|
|
1786
|
+
if (!visiblePresets.length) {
|
|
1787
|
+
state.selectedPresetKey = "";
|
|
1788
|
+
state.presetUninstallConfirm = "";
|
|
1789
|
+
return;
|
|
1790
|
+
}
|
|
1791
|
+
if (!visiblePresets.some((preset) => presetKey(preset) === state.selectedPresetKey)) {
|
|
1792
|
+
state.selectedPresetKey = presetKey(visiblePresets[0]);
|
|
1793
|
+
state.presetUninstallConfirm = "";
|
|
1794
|
+
}
|
|
1795
|
+
}
|
|
1796
|
+
|
|
1797
|
+
function selectedPreset(visiblePresets = filteredPresets()) {
|
|
1798
|
+
return visiblePresets.find((preset) => presetKey(preset) === state.selectedPresetKey) || visiblePresets[0] || null;
|
|
1799
|
+
}
|
|
1800
|
+
|
|
1801
|
+
function presetCard(preset, selectedId) {
|
|
1802
|
+
const key = presetKey(preset);
|
|
1803
|
+
const selected = key === selectedId;
|
|
1804
|
+
return `<article class="preset-card ${selected ? "active" : ""} ${preset.effective ? "effective" : "shadowed"}">
|
|
1805
|
+
<div class="preset-card-topline">
|
|
1806
|
+
<button type="button" class="preset-card-select" data-preset-select="${escapeAttr(key)}" aria-pressed="${selected ? "true" : "false"}">
|
|
1807
|
+
<span class="card-id">${escapeHtml(preset.id)}</span>
|
|
1808
|
+
</button>
|
|
1809
|
+
<div class="preset-card-tools">
|
|
1810
|
+
${presetSourceBadge(preset.source)}
|
|
1811
|
+
${presetStatusBadge(preset)}
|
|
1812
|
+
<button type="button" class="copy-inline" data-copy-preset-id="${escapeAttr(preset.id)}" title="${escapeAttr(t("copyPresetId"))}">${t("copyIdShort")}</button>
|
|
1813
|
+
</div>
|
|
1814
|
+
</div>
|
|
1815
|
+
<button type="button" class="preset-card-body" data-preset-select="${escapeAttr(key)}">
|
|
1816
|
+
<span>${escapeHtml(preset.purpose || t("none"))}</span>
|
|
1817
|
+
</button>
|
|
1818
|
+
<div class="preset-card-meta">
|
|
1819
|
+
<span>${t("manifestVersion")}: ${escapeHtml(formatPresetVersion(preset))}</span>
|
|
1820
|
+
<span>${t("taskKind")}: ${escapeHtml(preset.taskKind || t("none"))}</span>
|
|
1821
|
+
<span>${t("budgets")}: ${escapeHtml((preset.compatibleBudgets || []).join(", ") || t("none"))}</span>
|
|
1822
|
+
</div>
|
|
1823
|
+
<code class="preset-manifest-path">${escapeHtml(preset.manifestPath || "")}</code>
|
|
1824
|
+
</article>`;
|
|
1825
|
+
}
|
|
1826
|
+
|
|
1827
|
+
function presetKey(preset) {
|
|
1828
|
+
return preset?.key || `${preset?.source || "unknown"}:${preset?.id || ""}`;
|
|
1829
|
+
}
|
|
1830
|
+
|
|
1831
|
+
function presetSourceRank(source) {
|
|
1832
|
+
return { project: 1, user: 2, builtin: 3 }[source] || 9;
|
|
1833
|
+
}
|
|
1834
|
+
|
|
1835
|
+
function presetLayersForId(id) {
|
|
1836
|
+
return (bundle.presetCatalog?.presets || [])
|
|
1837
|
+
.filter((preset) => preset.id === id)
|
|
1838
|
+
.sort((a, b) => presetSourceRank(a.source) - presetSourceRank(b.source));
|
|
1839
|
+
}
|
|
1840
|
+
|
|
1841
|
+
function syncPresetUninstallScope(preset) {
|
|
1842
|
+
if (preset && ["project", "user"].includes(preset.source)) state.presetUninstallScope = preset.source;
|
|
1843
|
+
}
|
|
1844
|
+
|
|
1845
|
+
function presetPriorityStep(source, index) {
|
|
1846
|
+
return `<div class="preset-priority-step">
|
|
1847
|
+
<span>${index}</span>
|
|
1848
|
+
<strong>${escapeHtml(t(`presetSource_${source}`) || source)}</strong>
|
|
1849
|
+
</div>`;
|
|
1850
|
+
}
|
|
1851
|
+
|
|
1852
|
+
function presetSourceBadge(source) {
|
|
1853
|
+
const normalized = String(source || "unknown");
|
|
1854
|
+
return `<span class="tag preset-source-badge ${escapeAttr(normalized)}">${escapeHtml(t(`presetSource_${normalized}`) || normalized)}</span>`;
|
|
1855
|
+
}
|
|
1856
|
+
|
|
1857
|
+
function presetStatusBadge(preset) {
|
|
1858
|
+
return `<span class="tag ${preset.effective ? "pass" : "warn"}">${escapeHtml(preset.effective ? t("presetEffective") : t("presetShadowed"))}</span>`;
|
|
1859
|
+
}
|
|
1860
|
+
|
|
1861
|
+
function formatPresetVersion(preset) {
|
|
1862
|
+
return preset?.version ?? t("none");
|
|
1863
|
+
}
|
|
1864
|
+
|
|
1865
|
+
function presetSummaryPanel(catalog) {
|
|
1866
|
+
const roots = catalog.roots || [];
|
|
1867
|
+
return `<section class="side-panel preset-summary-panel">
|
|
1868
|
+
<h3>${t("presetSources")}</h3>
|
|
1869
|
+
<p class="preset-helper">${t("presetSourcesHint")}</p>
|
|
1870
|
+
<div class="metrics-grid compact">
|
|
1871
|
+
${metric(t("presetSourceProject"), catalog.summary?.project || 0)}
|
|
1872
|
+
${metric(t("presetSourceUser"), catalog.summary?.user || 0)}
|
|
1873
|
+
${metric(t("presetSourceBuiltin"), catalog.summary?.builtin || 0)}
|
|
1874
|
+
</div>
|
|
1875
|
+
<div class="preset-roots">
|
|
1876
|
+
${roots.map((root) => `<div><strong>${escapeHtml(t(`presetSource_${root.source}`) || root.source)}</strong><code>${escapeHtml(root.path || "")}</code></div>`).join("")}
|
|
1877
|
+
</div>
|
|
1878
|
+
</section>`;
|
|
1879
|
+
}
|
|
1880
|
+
|
|
1881
|
+
function presetDetailPanel(preset) {
|
|
1882
|
+
if (!preset) return `<section class="flow-panel preset-detail-panel">${emptyState(t("noPresets"))}</section>`;
|
|
1883
|
+
const inspectCommand = `harness preset inspect ${preset.id} --json .`;
|
|
1884
|
+
const checkCommand = `harness preset check ${preset.id} --json .`;
|
|
1885
|
+
const commandRows = preset.effective
|
|
1886
|
+
? `${presetCommandRow(inspectCommand)}${presetCommandRow(checkCommand)}`
|
|
1887
|
+
: `<div class="preset-command-warning">${escapeHtml(t("presetCommandsEffectiveOnly"))}</div>`;
|
|
1888
|
+
return `<section class="flow-panel preset-detail-panel">
|
|
1889
|
+
<div class="preset-detail-hero">
|
|
1890
|
+
<div>
|
|
1891
|
+
<div class="preset-detail-title-row">
|
|
1892
|
+
<h3>${escapeHtml(preset.id)}</h3>
|
|
1893
|
+
<button type="button" class="copy-inline" data-copy-preset-id="${escapeAttr(preset.id)}">${t("copyPresetId")}</button>
|
|
1894
|
+
</div>
|
|
1895
|
+
<p>${escapeHtml(preset.purpose || "")}</p>
|
|
1896
|
+
</div>
|
|
1897
|
+
<div class="preset-detail-badges">
|
|
1898
|
+
${presetSourceBadge(preset.source)}
|
|
1899
|
+
${presetStatusBadge(preset)}
|
|
1900
|
+
</div>
|
|
1901
|
+
</div>
|
|
1902
|
+
<dl class="preset-detail-list">
|
|
1903
|
+
${presetDetailRow(t("manifestVersion"), formatPresetVersion(preset))}
|
|
1904
|
+
${presetDetailRow(t("source"), t(`presetSource_${preset.source}`) || preset.source)}
|
|
1905
|
+
${presetDetailRow(t("status"), preset.effective ? t("presetEffective") : t("presetShadowed"))}
|
|
1906
|
+
${presetDetailRow(t("taskKind"), preset.taskKind || t("none"))}
|
|
1907
|
+
${presetDetailRow(t("budgets"), (preset.compatibleBudgets || []).join(", ") || t("none"))}
|
|
1908
|
+
${presetDetailRow(t("inputs"), preset.inputCount || 0)}
|
|
1909
|
+
${presetDetailRow(t("references"), preset.referenceCount || 0)}
|
|
1910
|
+
${presetDetailRow(t("artifacts"), preset.artifactCount || 0)}
|
|
1911
|
+
${presetDetailRow(t("writeScopes"), preset.writeScopeCount || 0)}
|
|
1912
|
+
${presetDetailRow(t("requiredReads"), preset.requiredReadCount || 0)}
|
|
1913
|
+
</dl>
|
|
1914
|
+
<div class="preset-path-block">
|
|
1915
|
+
<span>${t("manifestPath")}</span>
|
|
1916
|
+
<code class="preset-manifest-path">${escapeHtml(preset.manifestPath || "")}</code>
|
|
1917
|
+
</div>
|
|
1918
|
+
<div class="preset-command-list">
|
|
1919
|
+
${commandRows}
|
|
1920
|
+
</div>
|
|
1921
|
+
</section>`;
|
|
1922
|
+
}
|
|
1923
|
+
|
|
1924
|
+
function presetDetailRow(labelText, value) {
|
|
1925
|
+
return `<div><dt>${escapeHtml(labelText)}</dt><dd>${escapeHtml(String(value ?? ""))}</dd></div>`;
|
|
1926
|
+
}
|
|
1927
|
+
|
|
1928
|
+
function presetCommandRow(command) {
|
|
1929
|
+
return `<div class="preset-command-row">
|
|
1930
|
+
<code>${escapeHtml(command)}</code>
|
|
1931
|
+
<button type="button" class="copy-inline" data-copy-preset-command="${escapeAttr(command)}">${t("copyCommand")}</button>
|
|
1932
|
+
</div>`;
|
|
1933
|
+
}
|
|
1934
|
+
|
|
1935
|
+
function presetLayerStackPanel(preset) {
|
|
1936
|
+
if (!preset) return "";
|
|
1937
|
+
const layers = presetLayersForId(preset.id);
|
|
1938
|
+
return `<section class="flow-panel preset-layer-panel">
|
|
1939
|
+
<div class="preset-panel-heading">
|
|
1940
|
+
<div>
|
|
1941
|
+
<h3>${t("presetLayerStack")}</h3>
|
|
1942
|
+
<p>${t("presetLayerStackHint")}</p>
|
|
1943
|
+
</div>
|
|
1944
|
+
</div>
|
|
1945
|
+
<div class="preset-layer-list">
|
|
1946
|
+
${layers.map((layer) => `<button type="button" class="preset-layer-row ${presetKey(layer) === presetKey(preset) ? "active" : ""}" data-preset-select="${escapeAttr(presetKey(layer))}">
|
|
1947
|
+
<span class="preset-layer-rank">${presetSourceRank(layer.source)}</span>
|
|
1948
|
+
<span>
|
|
1949
|
+
<strong>${escapeHtml(t(`presetSource_${layer.source}`) || layer.source)}</strong>
|
|
1950
|
+
<small>${t("manifestVersion")}: ${escapeHtml(formatPresetVersion(layer))}</small>
|
|
1951
|
+
</span>
|
|
1952
|
+
${presetStatusBadge(layer)}
|
|
1953
|
+
</button>`).join("")}
|
|
1954
|
+
</div>
|
|
1955
|
+
</section>`;
|
|
1956
|
+
}
|
|
1957
|
+
|
|
1958
|
+
function presetActionPanel(preset) {
|
|
1959
|
+
const staticNote = canUseWorkbenchAction("preset-install") ? "" : `<p class="lesson-action-note">${escapeHtml(t("presetWorkbenchRequired"))}</p>`;
|
|
1960
|
+
const lockedUninstallScope = preset && ["project", "user"].includes(preset.source) ? preset.source : "";
|
|
1961
|
+
const confirmMatches = Boolean(preset && state.presetUninstallConfirm.trim() === preset.id);
|
|
1962
|
+
const canCheck = canUseWorkbenchAction("preset-check") && preset && preset.effective;
|
|
1963
|
+
const canUninstall = canUseWorkbenchAction("preset-uninstall") && preset && preset.source !== "builtin" && confirmMatches;
|
|
1964
|
+
return `<section class="side-panel preset-action-panel">
|
|
1965
|
+
<div class="preset-panel-heading">
|
|
1966
|
+
<div>
|
|
1967
|
+
<h3>${t("presetContextActions")}</h3>
|
|
1968
|
+
<p>${preset ? escapeHtml(preset.id) : t("noPresets")}</p>
|
|
1969
|
+
</div>
|
|
1970
|
+
</div>
|
|
1971
|
+
${staticNote}
|
|
1972
|
+
${presetActionResult()}
|
|
1973
|
+
<div class="preset-action-group">
|
|
1974
|
+
<h4>${t("presetCheck")}</h4>
|
|
1975
|
+
<p>${preset?.effective ? t("presetCheckHint") : t("presetShadowedActionHint")}</p>
|
|
1976
|
+
<button data-preset-check="${escapeAttr(preset?.id || "")}" ${canCheck ? "" : "disabled"}>${t("presetCheckSelected")}</button>
|
|
1977
|
+
</div>
|
|
1978
|
+
<div class="preset-action-group danger">
|
|
1979
|
+
<h4>${t("presetUninstallSelected")}</h4>
|
|
1980
|
+
<p>${preset?.source === "builtin" ? t("presetBuiltinImmutable") : t("presetUninstallHint")}</p>
|
|
1981
|
+
<label>${t("scope")}<select data-preset-uninstall-scope ${lockedUninstallScope ? "disabled" : ""}>
|
|
1982
|
+
${presetScopeOptions(lockedUninstallScope || state.presetUninstallScope)}
|
|
1983
|
+
</select></label>
|
|
1984
|
+
<div class="preset-confirm-row">
|
|
1985
|
+
<label>${t("confirmPresetId")}<input data-preset-uninstall-confirm value="${escapeAttr(state.presetUninstallConfirm)}" placeholder="${escapeAttr(preset?.id || "")}"></label>
|
|
1986
|
+
<button type="button" data-preset-fill-confirm="${escapeAttr(preset?.id || "")}" ${preset && preset.source !== "builtin" ? "" : "disabled"}>${t("useSelectedId")}</button>
|
|
1987
|
+
</div>
|
|
1988
|
+
${preset && preset.source !== "builtin" && !confirmMatches ? `<p class="preset-confirm-warning">${escapeHtml(t("presetConfirmRequired"))}</p>` : ""}
|
|
1989
|
+
<button data-preset-uninstall="${escapeAttr(preset?.id || "")}" ${canUninstall ? "" : "disabled"}>${t("presetUninstallSelected")}</button>
|
|
1990
|
+
</div>
|
|
1991
|
+
</section>`;
|
|
1992
|
+
}
|
|
1993
|
+
|
|
1994
|
+
function presetImportPanel() {
|
|
1995
|
+
return `<section class="side-panel preset-action-panel">
|
|
1996
|
+
<div class="preset-panel-heading">
|
|
1997
|
+
<div>
|
|
1998
|
+
<h3>${t("presetImportTitle")}</h3>
|
|
1999
|
+
<p>${t("presetImportHint")}</p>
|
|
2000
|
+
</div>
|
|
2001
|
+
</div>
|
|
2002
|
+
<div class="preset-action-group">
|
|
2003
|
+
<label>${t("source")}<input data-preset-install-source value="${escapeAttr(state.presetInstallSource)}" placeholder="${escapeAttr(t("presetInstallSourcePlaceholder"))}"></label>
|
|
2004
|
+
<label>${t("scope")}<select data-preset-install-scope>
|
|
2005
|
+
${presetScopeOptions(state.presetInstallScope)}
|
|
2006
|
+
</select></label>
|
|
2007
|
+
<label class="check-row"><input type="checkbox" data-preset-install-force ${state.presetInstallForce ? "checked" : ""}> ${t("forceOverwrite")}</label>
|
|
2008
|
+
<button data-preset-install ${canUseWorkbenchAction("preset-install") ? "" : "disabled"}>${t("presetInstall")}</button>
|
|
2009
|
+
</div>
|
|
2010
|
+
</section>`;
|
|
2011
|
+
}
|
|
2012
|
+
|
|
2013
|
+
function presetRestorePanel() {
|
|
2014
|
+
return `<section class="side-panel preset-action-panel">
|
|
2015
|
+
<div class="preset-panel-heading">
|
|
2016
|
+
<div>
|
|
2017
|
+
<h3>${t("presetRestoreBundled")}</h3>
|
|
2018
|
+
<p>${t("presetRestoreBundledHint")}</p>
|
|
2019
|
+
</div>
|
|
2020
|
+
</div>
|
|
2021
|
+
<div class="preset-action-group">
|
|
2022
|
+
<label>${t("scope")}<select data-preset-seed-scope>
|
|
2023
|
+
${presetScopeOptions(state.presetSeedScope)}
|
|
2024
|
+
</select></label>
|
|
2025
|
+
<label class="check-row"><input type="checkbox" data-preset-seed-force ${state.presetSeedForce ? "checked" : ""}> ${t("forceOverwrite")}</label>
|
|
2026
|
+
<button data-preset-seed ${canUseWorkbenchAction("preset-seed") ? "" : "disabled"}>${t("presetRestoreBundled")}</button>
|
|
2027
|
+
</div>
|
|
2028
|
+
</section>`;
|
|
2029
|
+
}
|
|
2030
|
+
|
|
2031
|
+
function presetScopeOptions(current) {
|
|
2032
|
+
return [["project", t("presetSourceProject")], ["user", t("presetSourceUser")]]
|
|
2033
|
+
.map(([value, labelText]) => `<option value="${value}" ${current === value ? "selected" : ""}>${escapeHtml(labelText)}</option>`)
|
|
2034
|
+
.join("");
|
|
2035
|
+
}
|
|
2036
|
+
|
|
2037
|
+
function presetActionResult() {
|
|
2038
|
+
const result = state.presetActionResult;
|
|
2039
|
+
if (!result) return "";
|
|
2040
|
+
const klass = result.ok ? "success" : "failed";
|
|
2041
|
+
return `<div class="workbench-action-result ${klass}">
|
|
2042
|
+
<strong>${escapeHtml(result.title || "")}</strong>
|
|
2043
|
+
<span>${escapeHtml(result.message || "")}</span>
|
|
2044
|
+
</div>`;
|
|
2045
|
+
}
|
|
2046
|
+
|
|
1183
2047
|
function taskDocument(task, fileName) {
|
|
1184
2048
|
if (fileName === "__walkthrough__" && task.walkthroughPath) return findDocument(task.walkthroughPath);
|
|
1185
2049
|
return findDocument(`${task.path}/${fileName}`);
|
|
@@ -1210,7 +2074,9 @@ function tag(value) {
|
|
|
1210
2074
|
}
|
|
1211
2075
|
|
|
1212
2076
|
function label(value) {
|
|
1213
|
-
|
|
2077
|
+
const key = `state_${value}`;
|
|
2078
|
+
const translated = t(key);
|
|
2079
|
+
return translated === key ? String(value || "unknown").replaceAll("_", " ") : translated;
|
|
1214
2080
|
}
|
|
1215
2081
|
|
|
1216
2082
|
function list(items = []) {
|
|
@@ -1272,6 +2138,13 @@ function bind() {
|
|
|
1272
2138
|
localStorage.setItem("harness.taskLayout", state.taskLayout);
|
|
1273
2139
|
app();
|
|
1274
2140
|
}));
|
|
2141
|
+
document.querySelectorAll("[data-task-sort-order]").forEach((btn) => btn.addEventListener("click", () => {
|
|
2142
|
+
state.taskSortOrder = btn.dataset.taskSortOrder === "asc" ? "asc" : "desc";
|
|
2143
|
+
localStorage.setItem("harness.taskSortOrder", state.taskSortOrder);
|
|
2144
|
+
state.taskPageByGroup = {};
|
|
2145
|
+
state.taskGroupPage = 1;
|
|
2146
|
+
app();
|
|
2147
|
+
}));
|
|
1275
2148
|
document.querySelectorAll("[data-render-toggle]").forEach((button) => button.addEventListener("click", () => {
|
|
1276
2149
|
state.renderMode = state.renderMode === "rendered" ? "source" : "rendered";
|
|
1277
2150
|
app();
|
|
@@ -1286,11 +2159,89 @@ function bind() {
|
|
|
1286
2159
|
state.warningPage = 1;
|
|
1287
2160
|
app();
|
|
1288
2161
|
}));
|
|
2162
|
+
document.querySelectorAll("[data-preset-search]").forEach((input) => input.addEventListener("input", () => {
|
|
2163
|
+
state.presetQuery = input.value;
|
|
2164
|
+
app();
|
|
2165
|
+
}));
|
|
2166
|
+
document.querySelectorAll("[data-preset-source-filter]").forEach((button) => button.addEventListener("click", () => {
|
|
2167
|
+
state.presetSourceFilter = button.dataset.presetSourceFilter || "all";
|
|
2168
|
+
state.selectedPresetKey = "";
|
|
2169
|
+
state.presetUninstallConfirm = "";
|
|
2170
|
+
app();
|
|
2171
|
+
}));
|
|
2172
|
+
document.querySelectorAll("[data-preset-select]").forEach((button) => button.addEventListener("click", () => {
|
|
2173
|
+
state.selectedPresetKey = button.dataset.presetSelect || "";
|
|
2174
|
+
state.selectedPresetId = "";
|
|
2175
|
+
const selectedPreset = (bundle.presetCatalog?.presets || []).find((preset) => presetKey(preset) === state.selectedPresetKey);
|
|
2176
|
+
if (selectedPreset && state.presetSourceFilter !== "all" && selectedPreset.source !== state.presetSourceFilter) {
|
|
2177
|
+
state.presetSourceFilter = selectedPreset.source;
|
|
2178
|
+
}
|
|
2179
|
+
if (selectedPreset && !presetMatchesQuery(selectedPreset)) state.presetQuery = "";
|
|
2180
|
+
if (selectedPreset && ["project", "user"].includes(selectedPreset.source)) state.presetUninstallScope = selectedPreset.source;
|
|
2181
|
+
state.presetUninstallConfirm = "";
|
|
2182
|
+
app();
|
|
2183
|
+
}));
|
|
2184
|
+
document.querySelectorAll("[data-preset-install-source]").forEach((input) => input.addEventListener("input", () => {
|
|
2185
|
+
state.presetInstallSource = input.value;
|
|
2186
|
+
}));
|
|
2187
|
+
document.querySelectorAll("[data-preset-install-scope]").forEach((select) => select.addEventListener("change", () => {
|
|
2188
|
+
state.presetInstallScope = select.value || "project";
|
|
2189
|
+
}));
|
|
2190
|
+
document.querySelectorAll("[data-preset-install-force]").forEach((input) => input.addEventListener("change", () => {
|
|
2191
|
+
state.presetInstallForce = input.checked;
|
|
2192
|
+
}));
|
|
2193
|
+
document.querySelectorAll("[data-preset-seed-scope]").forEach((select) => select.addEventListener("change", () => {
|
|
2194
|
+
state.presetSeedScope = select.value || "project";
|
|
2195
|
+
}));
|
|
2196
|
+
document.querySelectorAll("[data-preset-seed-force]").forEach((input) => input.addEventListener("change", () => {
|
|
2197
|
+
state.presetSeedForce = input.checked;
|
|
2198
|
+
}));
|
|
2199
|
+
document.querySelectorAll("[data-preset-uninstall-scope]").forEach((select) => select.addEventListener("change", () => {
|
|
2200
|
+
state.presetUninstallScope = select.value || "project";
|
|
2201
|
+
}));
|
|
2202
|
+
document.querySelectorAll("[data-preset-uninstall-confirm]").forEach((input) => input.addEventListener("input", () => {
|
|
2203
|
+
state.presetUninstallConfirm = input.value;
|
|
2204
|
+
}));
|
|
2205
|
+
document.querySelectorAll("[data-preset-fill-confirm]").forEach((button) => button.addEventListener("click", () => {
|
|
2206
|
+
state.presetUninstallConfirm = button.dataset.presetFillConfirm || "";
|
|
2207
|
+
app();
|
|
2208
|
+
}));
|
|
2209
|
+
document.querySelectorAll("[data-preset-check]").forEach((button) => button.addEventListener("click", () => runPresetAction("check", { id: button.dataset.presetCheck || "" })));
|
|
2210
|
+
document.querySelectorAll("[data-preset-install]").forEach((button) => button.addEventListener("click", () => runPresetAction("install", {
|
|
2211
|
+
source: state.presetInstallSource,
|
|
2212
|
+
scope: state.presetInstallScope,
|
|
2213
|
+
force: state.presetInstallForce,
|
|
2214
|
+
})));
|
|
2215
|
+
document.querySelectorAll("[data-preset-seed]").forEach((button) => button.addEventListener("click", () => runPresetAction("seed", {
|
|
2216
|
+
scope: state.presetSeedScope,
|
|
2217
|
+
force: state.presetSeedForce,
|
|
2218
|
+
})));
|
|
2219
|
+
document.querySelectorAll("[data-preset-uninstall]").forEach((button) => button.addEventListener("click", () => runPresetAction("uninstall", {
|
|
2220
|
+
id: button.dataset.presetUninstall || "",
|
|
2221
|
+
scope: state.presetUninstallScope,
|
|
2222
|
+
confirmText: state.presetUninstallConfirm,
|
|
2223
|
+
})));
|
|
2224
|
+
document.querySelectorAll("[data-review-queue-tab]").forEach((button) => button.addEventListener("click", () => {
|
|
2225
|
+
state.reviewQueueTab = button.dataset.reviewQueueTab || "review";
|
|
2226
|
+
state.reviewQueuePage = 1;
|
|
2227
|
+
app();
|
|
2228
|
+
}));
|
|
2229
|
+
document.querySelectorAll("[data-review-reason-filter]").forEach((select) => select.addEventListener("change", () => {
|
|
2230
|
+
state.reviewReasonFilter = select.value || "all";
|
|
2231
|
+
state.reviewQueuePage = 1;
|
|
2232
|
+
app();
|
|
2233
|
+
}));
|
|
2234
|
+
document.querySelectorAll("[data-review-sort]").forEach((select) => select.addEventListener("change", () => {
|
|
2235
|
+
state.reviewSort = select.value || "queue";
|
|
2236
|
+
state.reviewQueuePage = 1;
|
|
2237
|
+
app();
|
|
2238
|
+
}));
|
|
1289
2239
|
document.querySelectorAll("[data-page-kind]").forEach((button) => button.addEventListener("click", () => {
|
|
1290
2240
|
const page = Math.max(1, Number(button.dataset.page) || 1);
|
|
1291
2241
|
if (button.dataset.pageKind === "warning") state.warningPage = page;
|
|
1292
2242
|
if (button.dataset.pageKind === "task-groups") state.taskGroupPage = page;
|
|
1293
2243
|
if (button.dataset.pageKind === "task") state.taskPageByGroup[button.dataset.pageGroup || ""] = page;
|
|
2244
|
+
if (button.dataset.pageKind === "review") state.reviewQueuePage = page;
|
|
1294
2245
|
app();
|
|
1295
2246
|
}));
|
|
1296
2247
|
document.querySelectorAll("[data-runway-phase]").forEach((link) => link.addEventListener("click", () => {
|
|
@@ -1315,6 +2266,10 @@ function bind() {
|
|
|
1315
2266
|
const taskId = el.dataset.openDrawer;
|
|
1316
2267
|
openDrawer(taskId);
|
|
1317
2268
|
}));
|
|
2269
|
+
bindCopyTaskNameButtons(document);
|
|
2270
|
+
bindPresetCopyButtons(document);
|
|
2271
|
+
bindRepairPromptButtons(document);
|
|
2272
|
+
bindLessonSedimentationButtons(document);
|
|
1318
2273
|
document.querySelectorAll("[data-open-lesson-drawer]").forEach((el) => el.addEventListener("click", (e) => {
|
|
1319
2274
|
e.preventDefault();
|
|
1320
2275
|
const lessonId = el.dataset.openLessonDrawer;
|
|
@@ -1395,6 +2350,45 @@ async function completeReviewFromDashboard(taskId) {
|
|
|
1395
2350
|
}
|
|
1396
2351
|
}
|
|
1397
2352
|
|
|
2353
|
+
async function runPresetAction(action, body) {
|
|
2354
|
+
state.presetActionResult = { ok: true, title: t("presetActionRunning"), message: action };
|
|
2355
|
+
app();
|
|
2356
|
+
try {
|
|
2357
|
+
const response = await fetch(`/api/presets/${action}`, {
|
|
2358
|
+
method: "POST",
|
|
2359
|
+
headers: {
|
|
2360
|
+
"content-type": "application/json",
|
|
2361
|
+
"x-harness-csrf": state.runtime?.csrfToken || "",
|
|
2362
|
+
},
|
|
2363
|
+
body: JSON.stringify(body),
|
|
2364
|
+
});
|
|
2365
|
+
const payload = await response.json();
|
|
2366
|
+
if (!response.ok) throw payload;
|
|
2367
|
+
state.presetActionResult = {
|
|
2368
|
+
ok: true,
|
|
2369
|
+
title: t("presetActionSuccess"),
|
|
2370
|
+
message: presetActionMessage(action, payload),
|
|
2371
|
+
};
|
|
2372
|
+
app();
|
|
2373
|
+
if (["install", "seed", "uninstall"].includes(action)) setTimeout(() => window.location.reload(), 650);
|
|
2374
|
+
} catch (error) {
|
|
2375
|
+
state.presetActionResult = {
|
|
2376
|
+
ok: false,
|
|
2377
|
+
title: t("presetActionFailed"),
|
|
2378
|
+
message: error?.error || error?.message || String(error || action),
|
|
2379
|
+
};
|
|
2380
|
+
app();
|
|
2381
|
+
}
|
|
2382
|
+
}
|
|
2383
|
+
|
|
2384
|
+
function presetActionMessage(action, payload) {
|
|
2385
|
+
if (action === "check") return `${payload.id || ""} ${payload.status || ""}`.trim();
|
|
2386
|
+
if (action === "install") return `${payload.id || ""} -> ${payload.scope || ""}`.trim();
|
|
2387
|
+
if (action === "seed") return `${payload.created || 0} ${t("created")} · ${payload.skipped || 0} ${t("skipped")}`;
|
|
2388
|
+
if (action === "uninstall") return `${payload.id || ""} ${payload.removed ? t("removed") : t("notInstalled")}`.trim();
|
|
2389
|
+
return action;
|
|
2390
|
+
}
|
|
2391
|
+
|
|
1398
2392
|
function renderDrawerContent(taskId) {
|
|
1399
2393
|
const task = (bundle.status?.tasks || []).find((item) => item.id === taskId);
|
|
1400
2394
|
if (!task) return `<div class="empty">${t("taskNotFound")}</div>`;
|
|
@@ -1404,6 +2398,7 @@ function renderDrawerContent(taskId) {
|
|
|
1404
2398
|
<div>
|
|
1405
2399
|
<h2>${escapeHtml(task.title)}</h2>
|
|
1406
2400
|
<p style="font-family: var(--font-mono); font-size: 11px; margin: 4px 0 0; color: var(--muted);">${escapeHtml(task.id)}</p>
|
|
2401
|
+
${taskCopyButton(task, "detail-copy")}
|
|
1407
2402
|
</div>
|
|
1408
2403
|
<button class="btn-close" data-close-drawer>×</button>
|
|
1409
2404
|
</div>
|
|
@@ -1416,12 +2411,16 @@ function renderDrawerContent(taskId) {
|
|
|
1416
2411
|
|
|
1417
2412
|
const body = `
|
|
1418
2413
|
<div class="task-drawer-body stack">
|
|
1419
|
-
<div
|
|
1420
|
-
<div
|
|
1421
|
-
|
|
2414
|
+
<div class="drawer-task-summary">
|
|
2415
|
+
<div>
|
|
2416
|
+
<span>${t("statOverall")}</span>
|
|
2417
|
+
<strong>${task.completion}%</strong>
|
|
2418
|
+
</div>
|
|
2419
|
+
<a href="#/tasks/${encodeURIComponent(task.id)}" class="btn-drawer-trigger">${t("fullView")}</a>
|
|
1422
2420
|
</div>
|
|
1423
2421
|
${taskStateSummary(task)}
|
|
1424
|
-
${reviewActionPanel(task)}
|
|
2422
|
+
${reviewActionPanel(task, { mode: "summary" })}
|
|
2423
|
+
${lessonCandidatePanel(task, { context: "drawer" })}
|
|
1425
2424
|
${timeline}
|
|
1426
2425
|
${documents}
|
|
1427
2426
|
${findings}
|
|
@@ -1445,18 +2444,174 @@ function openDrawer(taskId) {
|
|
|
1445
2444
|
state.renderMode = state.renderMode === "rendered" ? "source" : "rendered";
|
|
1446
2445
|
openDrawer(taskId);
|
|
1447
2446
|
}));
|
|
2447
|
+
bindCopyTaskNameButtons(drawer);
|
|
2448
|
+
bindRepairPromptButtons(drawer);
|
|
2449
|
+
bindLessonSedimentationButtons(drawer);
|
|
1448
2450
|
drawer.querySelectorAll("[data-review-complete]").forEach((button) => button.addEventListener("click", () => completeReviewFromDashboard(button.dataset.reviewComplete)));
|
|
1449
2451
|
}
|
|
1450
2452
|
|
|
2453
|
+
function bindCopyTaskNameButtons(root) {
|
|
2454
|
+
root.querySelectorAll("[data-copy-task-name]").forEach((button) => button.addEventListener("click", async (event) => {
|
|
2455
|
+
event.preventDefault();
|
|
2456
|
+
event.stopPropagation();
|
|
2457
|
+
const taskName = button.dataset.copyTaskName || "";
|
|
2458
|
+
const defaultText = t("copyTaskNameShort");
|
|
2459
|
+
try {
|
|
2460
|
+
await copyText(taskName);
|
|
2461
|
+
button.textContent = t("copyTaskNameSuccess");
|
|
2462
|
+
} catch {
|
|
2463
|
+
button.textContent = t("copyTaskNameFailed");
|
|
2464
|
+
}
|
|
2465
|
+
window.setTimeout(() => {
|
|
2466
|
+
button.textContent = defaultText;
|
|
2467
|
+
}, 1400);
|
|
2468
|
+
}));
|
|
2469
|
+
}
|
|
2470
|
+
|
|
2471
|
+
function bindPresetCopyButtons(root) {
|
|
2472
|
+
root.querySelectorAll("[data-copy-preset-id]").forEach((button) => button.addEventListener("click", async (event) => {
|
|
2473
|
+
event.preventDefault();
|
|
2474
|
+
event.stopPropagation();
|
|
2475
|
+
const presetId = button.dataset.copyPresetId || "";
|
|
2476
|
+
const defaultText = button.textContent;
|
|
2477
|
+
try {
|
|
2478
|
+
await copyText(presetId);
|
|
2479
|
+
button.textContent = t("copyTaskNameSuccess");
|
|
2480
|
+
} catch {
|
|
2481
|
+
button.textContent = t("copyTaskNameFailed");
|
|
2482
|
+
}
|
|
2483
|
+
setTimeout(() => { button.textContent = defaultText; }, 1200);
|
|
2484
|
+
}));
|
|
2485
|
+
root.querySelectorAll("[data-copy-preset-command]").forEach((button) => button.addEventListener("click", async (event) => {
|
|
2486
|
+
event.preventDefault();
|
|
2487
|
+
event.stopPropagation();
|
|
2488
|
+
const command = button.dataset.copyPresetCommand || "";
|
|
2489
|
+
const defaultText = button.textContent;
|
|
2490
|
+
try {
|
|
2491
|
+
await copyText(command);
|
|
2492
|
+
button.textContent = t("copyTaskNameSuccess");
|
|
2493
|
+
} catch {
|
|
2494
|
+
button.textContent = t("copyTaskNameFailed");
|
|
2495
|
+
}
|
|
2496
|
+
setTimeout(() => { button.textContent = defaultText; }, 1200);
|
|
2497
|
+
}));
|
|
2498
|
+
}
|
|
2499
|
+
|
|
2500
|
+
function bindRepairPromptButtons(root) {
|
|
2501
|
+
root.querySelectorAll("[data-copy-repair-prompt]").forEach((button) => button.addEventListener("click", async (event) => {
|
|
2502
|
+
event.preventDefault();
|
|
2503
|
+
event.stopPropagation();
|
|
2504
|
+
const prompt = button.dataset.repairPrompt || "";
|
|
2505
|
+
const defaultText = t("copyRepairPrompt");
|
|
2506
|
+
try {
|
|
2507
|
+
await copyText(prompt);
|
|
2508
|
+
button.textContent = t("copyRepairPromptSuccess");
|
|
2509
|
+
} catch {
|
|
2510
|
+
button.textContent = t("copyTaskNameFailed");
|
|
2511
|
+
}
|
|
2512
|
+
window.setTimeout(() => {
|
|
2513
|
+
button.textContent = defaultText;
|
|
2514
|
+
}, 1400);
|
|
2515
|
+
}));
|
|
2516
|
+
}
|
|
2517
|
+
|
|
2518
|
+
function bindLessonSedimentationButtons(root) {
|
|
2519
|
+
root.querySelectorAll("[data-copy-lesson-prompt]").forEach((button) => button.addEventListener("click", async (event) => {
|
|
2520
|
+
event.preventDefault();
|
|
2521
|
+
event.stopPropagation();
|
|
2522
|
+
const prompt = button.dataset.lessonPrompt || "";
|
|
2523
|
+
const defaultText = t("copyLessonPrompt");
|
|
2524
|
+
try {
|
|
2525
|
+
await copyText(prompt);
|
|
2526
|
+
button.textContent = t("copyRepairPromptSuccess");
|
|
2527
|
+
} catch {
|
|
2528
|
+
button.textContent = t("copyTaskNameFailed");
|
|
2529
|
+
}
|
|
2530
|
+
window.setTimeout(() => {
|
|
2531
|
+
button.textContent = defaultText;
|
|
2532
|
+
}, 1400);
|
|
2533
|
+
}));
|
|
2534
|
+
root.querySelectorAll("[data-create-lesson-sedimentation]").forEach((button) => button.addEventListener("click", async (event) => {
|
|
2535
|
+
event.preventDefault();
|
|
2536
|
+
event.stopPropagation();
|
|
2537
|
+
await createLessonSedimentationFromDashboard(button);
|
|
2538
|
+
}));
|
|
2539
|
+
}
|
|
2540
|
+
|
|
2541
|
+
async function createLessonSedimentationFromDashboard(button) {
|
|
2542
|
+
const taskId = button.dataset.createLessonSedimentation || "";
|
|
2543
|
+
const candidateId = button.dataset.candidateId || "";
|
|
2544
|
+
const result = document.querySelector(`[data-lesson-result="${CSS.escape(`${taskId}:${candidateId}`)}"]`);
|
|
2545
|
+
if (result) result.textContent = t("lessonTaskCreating");
|
|
2546
|
+
button.disabled = true;
|
|
2547
|
+
try {
|
|
2548
|
+
const response = await fetch("/api/tasks/lesson-sedimentation", {
|
|
2549
|
+
method: "POST",
|
|
2550
|
+
headers: {
|
|
2551
|
+
"content-type": "application/json",
|
|
2552
|
+
"x-harness-csrf": state.runtime?.csrfToken || "",
|
|
2553
|
+
},
|
|
2554
|
+
body: JSON.stringify({ taskId, candidateId }),
|
|
2555
|
+
});
|
|
2556
|
+
const payload = await response.json();
|
|
2557
|
+
if (!response.ok) throw payload;
|
|
2558
|
+
if (result) {
|
|
2559
|
+
result.innerHTML = lessonSedimentationSuccess(payload);
|
|
2560
|
+
bindLessonSedimentationButtons(result);
|
|
2561
|
+
result.scrollIntoView({ block: "center", inline: "nearest" });
|
|
2562
|
+
}
|
|
2563
|
+
} catch (error) {
|
|
2564
|
+
button.disabled = false;
|
|
2565
|
+
if (result) result.innerHTML = lessonSedimentationFailure(error);
|
|
2566
|
+
}
|
|
2567
|
+
}
|
|
2568
|
+
|
|
2569
|
+
function lessonSedimentationSuccess(payload) {
|
|
2570
|
+
const followUp = payload?.followUpTask || {};
|
|
2571
|
+
const prompt = payload?.prompt || "";
|
|
2572
|
+
const taskId = followUp.id || "";
|
|
2573
|
+
const openHref = taskId ? `#/tasks/${encodeURIComponent(taskId)}` : "#/review";
|
|
2574
|
+
return `<div class="workbench-action-result success">
|
|
2575
|
+
<strong>${escapeHtml(t("lessonTaskCreated"))}</strong>
|
|
2576
|
+
${taskId ? `<a href="${openHref}">${escapeHtml(t("openFollowUpTask"))}</a>` : ""}
|
|
2577
|
+
${prompt ? `<button data-copy-lesson-prompt="${escapeAttr(taskId || "follow-up")}" data-lesson-prompt="${escapeAttr(prompt)}">${escapeHtml(t("copyLessonPrompt"))}</button>` : ""}
|
|
2578
|
+
</div>`;
|
|
2579
|
+
}
|
|
2580
|
+
|
|
2581
|
+
function lessonSedimentationFailure(error) {
|
|
2582
|
+
const message = error?.error || error?.message || t("lessonTaskCreateFailed");
|
|
2583
|
+
const recovery = Array.isArray(error?.recovery) ? error.recovery : [];
|
|
2584
|
+
const details = error?.details || {};
|
|
2585
|
+
const existingTask = details.followUpTask || details.existingTask || "";
|
|
2586
|
+
return `<div class="workbench-action-result failed">
|
|
2587
|
+
<strong>${escapeHtml(t("lessonTaskCreateFailed"))}</strong>
|
|
2588
|
+
<span>${escapeHtml(message)}</span>
|
|
2589
|
+
${existingTask ? `<a href="#/tasks/${encodeURIComponent(existingTask)}">${escapeHtml(t("openFollowUpTask"))}</a>` : ""}
|
|
2590
|
+
${recovery.length ? `<ul>${recovery.map((item) => `<li>${escapeHtml(item)}</li>`).join("")}</ul>` : ""}
|
|
2591
|
+
</div>`;
|
|
2592
|
+
}
|
|
2593
|
+
|
|
2594
|
+
async function copyText(text) {
|
|
2595
|
+
if (navigator.clipboard?.writeText) {
|
|
2596
|
+
await navigator.clipboard.writeText(text);
|
|
2597
|
+
return;
|
|
2598
|
+
}
|
|
2599
|
+
const textarea = document.createElement("textarea");
|
|
2600
|
+
textarea.value = text;
|
|
2601
|
+
textarea.setAttribute("readonly", "");
|
|
2602
|
+
textarea.style.position = "fixed";
|
|
2603
|
+
textarea.style.left = "-9999px";
|
|
2604
|
+
document.body.appendChild(textarea);
|
|
2605
|
+
textarea.select();
|
|
2606
|
+
const copied = document.execCommand("copy");
|
|
2607
|
+
textarea.remove();
|
|
2608
|
+
if (!copied) throw new Error("copy failed");
|
|
2609
|
+
}
|
|
2610
|
+
|
|
1451
2611
|
function renderLessonDrawerContent(lessonId) {
|
|
1452
|
-
const
|
|
1453
|
-
const row = (lessonTable?.rows || []).find((r) => {
|
|
1454
|
-
const cells = r.cells || {};
|
|
1455
|
-
const id = cells.ID || cells.Lesson || cells["Lesson ID"] || cells["ID"] || "";
|
|
1456
|
-
return id === lessonId;
|
|
1457
|
-
});
|
|
2612
|
+
const lesson = lessonDocuments().find((item) => item.id === lessonId);
|
|
1458
2613
|
|
|
1459
|
-
if (!
|
|
2614
|
+
if (!lesson) {
|
|
1460
2615
|
return `<div class="task-drawer-header">
|
|
1461
2616
|
<h2>${escapeHtml(lessonId)}</h2>
|
|
1462
2617
|
<button class="btn-close" data-close-drawer>×</button>
|
|
@@ -1466,23 +2621,13 @@ function renderLessonDrawerContent(lessonId) {
|
|
|
1466
2621
|
</div>`;
|
|
1467
2622
|
}
|
|
1468
2623
|
|
|
1469
|
-
const
|
|
1470
|
-
const summary = cells.Summary || cells["\u6458\u8981"] || cells.Pattern || cells.Status || "";
|
|
1471
|
-
const docPath = cells["\u8be6\u60c5\u6587\u6863"] || cells.Document || cells.document || "";
|
|
1472
|
-
|
|
1473
|
-
let doc = null;
|
|
1474
|
-
if (docPath) {
|
|
1475
|
-
doc = findDocument(docPath);
|
|
1476
|
-
}
|
|
1477
|
-
if (!doc) {
|
|
1478
|
-
doc = (bundle.documents?.documents || []).find((d) => d.path.includes(lessonId) || d.path.endsWith(`${lessonId}.md`));
|
|
1479
|
-
}
|
|
2624
|
+
const doc = lesson.doc || findDocument(lesson.path);
|
|
1480
2625
|
|
|
1481
2626
|
const header = `
|
|
1482
2627
|
<div class="task-drawer-header">
|
|
1483
2628
|
<div>
|
|
1484
2629
|
<h2>${escapeHtml(lessonId)}</h2>
|
|
1485
|
-
<p style="font-size: 12px; margin: 4px 0 0; color: var(--muted); font-weight: 600;">${escapeHtml(
|
|
2630
|
+
<p style="font-size: 12px; margin: 4px 0 0; color: var(--muted); font-weight: 600;">${escapeHtml(lesson.title || lesson.path)}</p>
|
|
1486
2631
|
</div>
|
|
1487
2632
|
<button class="btn-close" data-close-drawer>×</button>
|
|
1488
2633
|
</div>
|
|
@@ -1492,16 +2637,10 @@ function renderLessonDrawerContent(lessonId) {
|
|
|
1492
2637
|
if (doc && doc.content) {
|
|
1493
2638
|
markdownBody = `<div class="markdown">${window.HarnessMarkdown.render(doc.content, "rendered")}</div>`;
|
|
1494
2639
|
} else {
|
|
1495
|
-
const rowsHtml = Object.entries(cells)
|
|
1496
|
-
.map(([key, val]) => `<tr><th>${escapeHtml(key)}</th><td>${escapeHtml(val)}</td></tr>`)
|
|
1497
|
-
.join("");
|
|
1498
2640
|
markdownBody = `
|
|
1499
2641
|
<div style="margin-bottom: 20px; background: var(--paper-2); padding: 16px; border-radius: 8px; border: 1px dashed var(--line);">
|
|
1500
2642
|
<p style="margin: 0; font-size: 13px; color: var(--muted);">${t("lessonDocMissing")}</p>
|
|
1501
2643
|
</div>
|
|
1502
|
-
<table class="rendered-table" style="width: 100%;">
|
|
1503
|
-
<tbody>${rowsHtml}</tbody>
|
|
1504
|
-
</table>
|
|
1505
2644
|
`;
|
|
1506
2645
|
}
|
|
1507
2646
|
|