feat: add mdtodo web-probe commands (#898)

Co-authored-by: Codex <codex@noreply.local>
This commit is contained in:
Lyon
2026-06-25 22:56:10 +08:00
committed by GitHub
parent 2da1c97e0d
commit 87b90e45b6
6 changed files with 555 additions and 38 deletions
@@ -348,10 +348,25 @@ async function processCommand(command) {
case "cancel": return withObserverSync(await cancelRunningTurn(), "cancel");
case "selectProvider": return withObserverSync(await selectProvider(String(command.provider || command.value || command.text || "")), "selectProvider");
case "clickSession": return withObserverSync(await clickSession(String(command.sessionId || command.value || "")), "clickSession");
case "gotoProjectMdtodo": return withObserverSync(await gotoProjectMdtodo(), "gotoProjectMdtodo");
case "openMdtodoSourceConfig": return openMdtodoSourceConfig(command);
case "configureMdtodoHwpodSource": return configureMdtodoHwpodSource(command);
case "probeMdtodoSource": return probeMdtodoSource(command);
case "reindexMdtodoSource": return reindexMdtodoSource(command);
case "selectProjectSource": return selectProjectSource(command);
case "selectMdtodoSource": return selectMdtodoSource(command);
case "selectMdtodoFile": return selectMdtodoFile(command);
case "selectMdtodoTask": return selectMdtodoTask(command);
case "expandMdtodoTask": return expandMdtodoTask(command);
case "editMdtodoTaskTitle": return editMdtodoTaskTitle(command);
case "editMdtodoTaskBody": return editMdtodoTaskBody(command);
case "toggleMdtodoTaskStatus": return toggleMdtodoTaskStatus(command);
case "addMdtodoRootTask": return addMdtodoRootTask(command);
case "addMdtodoSubTask": return addMdtodoSubTask(command);
case "continueMdtodoTask": return continueMdtodoTask(command);
case "deleteMdtodoTask": return deleteMdtodoTask(command);
case "launchWorkbenchFromTask": return withObserverSync(await launchWorkbenchFromTask(command), "launchWorkbenchFromTask");
case "launchWorkbenchFromMdtodo": return withObserverSync(await launchWorkbenchFromMdtodo(command), "launchWorkbenchFromMdtodo");
case "screenshot": return captureScreenshot(command.reason || "manual", command.imageType || "png");
case "mark": return { mark: truncate(command.label || command.text || "mark", 200), currentUrl: currentPageUrl(), pageId };
case "stop": stopping = true; return { stopping: true, currentUrl: currentPageUrl(), pageId };
@@ -1505,11 +1520,69 @@ function ensureProjectManagementCommand(type) {
if (!projectManagement.commandAllowlist.includes(type)) throw new Error(type + " is not in webProbe.projectManagement.commandAllowlist for the selected node/lane");
}
async function clickProjectItemByAttr({ type, attr, value, fallbackSelector }) {
async function gotoProjectMdtodo() {
ensureProjectManagementCommand("gotoProjectMdtodo");
return gotoTarget("/projects/mdtodo");
}
function commandValue(command, keys) {
for (const key of keys) {
const value = command?.[key];
if (typeof value === "string" && value.trim()) return value.trim();
}
return "";
}
async function visibleLocator(locator) {
return await locator.count().catch(() => 0) > 0 && await locator.first().isVisible().catch(() => false);
}
async function selectHtmlOptionByValueOrLabel(locator, value) {
const select = locator.first();
const targetValue = typeof value === "string" && value.trim() ? value.trim() : "";
if (targetValue) {
const byValue = await select.selectOption({ value: targetValue }).then((selected) => ({ ok: true, selected })).catch(() => ({ ok: false, selected: [] }));
if (byValue.ok && byValue.selected.length > 0) return { mode: "select-value", selectedValue: byValue.selected[0] || targetValue };
const byLabel = await select.selectOption({ label: targetValue }).then((selected) => ({ ok: true, selected })).catch(() => ({ ok: false, selected: [] }));
if (byLabel.ok && byLabel.selected.length > 0) return { mode: "select-label", selectedValue: byLabel.selected[0] || targetValue };
}
const selectedValue = await select.evaluate((element) => {
const options = Array.from(element.options || []).filter((option) => !option.disabled && option.value);
const chosen = options[0] || null;
if (!chosen) return "";
element.value = chosen.value;
element.dispatchEvent(new Event("input", { bubbles: true }));
element.dispatchEvent(new Event("change", { bubbles: true }));
return chosen.value;
});
return { mode: "select-first", selectedValue };
}
async function clickProjectItemByAttr({ type, attr, value, fallbackSelector, selectTestId }) {
ensureProjectManagementCommand(type);
const beforeUrl = currentPageUrl();
const beforeProject = await projectManagementCommandSnapshot();
const targetValue = typeof value === "string" && value.trim() ? value.trim() : null;
if (selectTestId) {
const select = page.locator('[data-testid="' + cssEscape(selectTestId) + '"]');
if (await visibleLocator(select)) {
const selected = await selectHtmlOptionByValueOrLabel(select, targetValue || "");
await page.waitForTimeout(700);
const afterProject = await projectManagementCommandSnapshot();
return {
beforeUrl,
afterUrl: currentPageUrl(),
type,
attr,
mode: selected.mode,
selected: opaqueIdSummary(selected.selectedValue || targetValue),
beforeProject,
afterProject,
pageId,
valuesRedacted: true
};
}
}
const selector = targetValue ? "[" + attr + "=\"" + cssEscape(targetValue) + "\"]" : fallbackSelector;
const locator = page.locator(selector).first();
await locator.waitFor({ state: "visible", timeout: 15000 });
@@ -1535,7 +1608,18 @@ async function selectProjectSource(command) {
type: "selectProjectSource",
attr: "data-source-id",
value: command.sourceId || command.value || command.text || "",
fallbackSelector: '[data-testid="mdtodo-source-list"] [data-source-id], [data-source-id]'
fallbackSelector: '[data-testid="mdtodo-source-list"] [data-source-id], [data-source-id]',
selectTestId: "mdtodo-source-select"
});
}
async function selectMdtodoSource(command) {
return clickProjectItemByAttr({
type: "selectMdtodoSource",
attr: "data-source-id",
value: command.sourceId || command.value || command.text || "",
fallbackSelector: '[data-testid="mdtodo-source-list"] [data-source-id], [data-source-id]',
selectTestId: "mdtodo-source-select"
});
}
@@ -1544,25 +1628,276 @@ async function selectMdtodoFile(command) {
type: "selectMdtodoFile",
attr: "data-file-ref",
value: command.fileRef || command.value || command.text || "",
fallbackSelector: '[data-testid="mdtodo-file-list"] [data-file-ref], [data-file-ref]'
fallbackSelector: '[data-testid="mdtodo-file-list"] [data-file-ref], [data-file-ref]',
selectTestId: "mdtodo-file-select"
});
}
async function mdtodoTaskLocator(command) {
const taskRef = commandValue(command, ["taskRef"]);
const taskId = commandValue(command, ["taskId", "task", "value", "text"]);
const selectors = [];
if (taskRef) selectors.push('[data-task-ref="' + cssEscape(taskRef) + '"]');
if (taskId) {
selectors.push('[data-task-id="' + cssEscape(taskId) + '"]');
selectors.push('[data-rxx-id="' + cssEscape(taskId) + '"]');
}
for (const selector of selectors) {
const locator = page.locator(selector).first();
if (await visibleLocator(locator)) return { locator, taskRef, taskId, selector };
}
if (taskId) {
const textLocator = page.locator('[data-testid="mdtodo-task-tree"] [data-task-ref], [data-task-ref]').filter({ hasText: taskId }).first();
if (await visibleLocator(textLocator)) return { locator: textLocator, taskRef, taskId, selector: "text:" + taskId };
}
const fallback = page.locator('[data-testid="mdtodo-task-tree"] [data-task-ref], [data-task-ref]').first();
return { locator: fallback, taskRef, taskId, selector: "first-visible-task" };
}
async function selectMdtodoTask(command) {
return clickProjectItemByAttr({
ensureProjectManagementCommand("selectMdtodoTask");
const beforeUrl = currentPageUrl();
const beforeProject = await projectManagementCommandSnapshot();
const target = await mdtodoTaskLocator(command);
await target.locator.waitFor({ state: "visible", timeout: 15000 });
const clicked = await target.locator.evaluate((element) => ({
taskRef: element.getAttribute("data-task-ref") || null,
taskId: element.getAttribute("data-task-id") || element.getAttribute("data-rxx-id") || null,
status: element.getAttribute("data-task-status") || null
})).catch(() => ({ taskRef: target.taskRef || null, taskId: target.taskId || null, status: null }));
await target.locator.click();
await page.waitForTimeout(700);
const afterProject = await projectManagementCommandSnapshot();
return {
beforeUrl,
afterUrl: currentPageUrl(),
type: "selectMdtodoTask",
attr: "data-task-ref",
value: command.taskRef || command.value || command.text || "",
fallbackSelector: '[data-testid="mdtodo-task-tree"] [data-task-ref], [data-task-ref]'
});
selector: target.selector,
selectedTask: opaqueIdSummary(clicked.taskRef || target.taskRef),
selectedTaskId: clicked.taskId || target.taskId || null,
selectedTaskStatus: clicked.status || null,
beforeProject,
afterProject,
pageId,
valuesRedacted: true
};
}
async function expandMdtodoTask(command) {
ensureProjectManagementCommand("expandMdtodoTask");
const beforeUrl = currentPageUrl();
const beforeProject = await projectManagementCommandSnapshot();
const target = await mdtodoTaskLocator(command);
await target.locator.waitFor({ state: "visible", timeout: 15000 });
const toggle = target.locator.locator('[data-testid="mdtodo-task-toggle"], [data-testid="mdtodo-task-expand"], [data-action="toggle-task"], button[aria-expanded]').first();
const toggleVisible = await visibleLocator(toggle);
if (toggleVisible) await toggle.click();
else await target.locator.click();
await page.waitForTimeout(700);
return {
beforeUrl,
afterUrl: currentPageUrl(),
type: "expandMdtodoTask",
selector: target.selector,
toggleVisible,
beforeProject,
afterProject: await projectManagementCommandSnapshot(),
pageId,
valuesRedacted: true
};
}
async function openMdtodoSourceConfig(command) {
ensureProjectManagementCommand("openMdtodoSourceConfig");
const beforeUrl = currentPageUrl();
const beforeProject = await projectManagementCommandSnapshot();
const button = page.locator('[data-testid="mdtodo-source-config-open"]').first();
await button.waitFor({ state: "visible", timeout: 15000 });
await button.click();
await page.locator('[data-testid="mdtodo-source-form-hwpod"], [data-testid="mdtodo-source-config-dialog"], [role="dialog"]').first().waitFor({ state: "visible", timeout: 10000 }).catch(() => null);
return {
beforeUrl,
afterUrl: currentPageUrl(),
type: "openMdtodoSourceConfig",
beforeProject,
afterProject: await projectManagementCommandSnapshot(),
pageId,
valuesRedacted: true
};
}
async function ensureMdtodoSourceConfigOpen() {
const form = page.locator('[data-testid="mdtodo-source-form-hwpod"], [data-testid="mdtodo-source-form-node"], [data-testid="mdtodo-source-form-root"]').first();
if (await visibleLocator(form)) return { opened: false };
await openMdtodoSourceConfig({ type: "openMdtodoSourceConfig" });
return { opened: true };
}
async function fillMdtodoField(testId, value) {
if (typeof value !== "string" || !value.trim()) return { testId, filled: false };
const locator = page.locator('[data-testid="' + cssEscape(testId) + '"]').first();
await locator.waitFor({ state: "visible", timeout: 10000 });
await locator.fill(value);
return { testId, filled: true, value: opaqueIdSummary(value), valuesRedacted: true };
}
async function clickProjectButtonAndMaybeWait(testId, pathPattern) {
const button = page.locator('[data-testid="' + cssEscape(testId) + '"]').first();
await button.waitFor({ state: "visible", timeout: 15000 });
const responsePromise = pathPattern ? page.waitForResponse((response) => {
try {
const pathname = new URL(response.url()).pathname;
return pathPattern.test(pathname);
} catch {
return false;
}
}, { timeout: 15000 }).then((response) => ({ observed: true, status: response.status(), path: new URL(response.url()).pathname })).catch((error) => ({ observed: false, waitError: errorSummary(error) })) : Promise.resolve(null);
const buttonState = await button.evaluate((element) => ({ disabled: Boolean(element.disabled) || element.getAttribute("aria-disabled") === "true", testId: element.getAttribute("data-testid") || null })).catch((error) => ({ disabled: null, error: errorSummary(error) }));
if (buttonState.disabled === true) {
const error = new Error(testId + " button is disabled");
error.details = { buttonState, valuesRedacted: true };
throw error;
}
await button.click();
const response = await responsePromise;
await page.waitForTimeout(900);
return { buttonState, response, valuesRedacted: true };
}
async function configureMdtodoHwpodSource(command) {
ensureProjectManagementCommand("configureMdtodoHwpodSource");
const beforeUrl = currentPageUrl();
const beforeProject = await projectManagementCommandSnapshot();
const dialog = await ensureMdtodoSourceConfigOpen();
const fields = [
await fillMdtodoField("mdtodo-source-form-hwpod", commandValue(command, ["hwpodId", "hwpod", "value"])),
await fillMdtodoField("mdtodo-source-form-node", commandValue(command, ["nodeId", "node"])),
await fillMdtodoField("mdtodo-source-form-root", commandValue(command, ["root", "path"])),
];
const save = await clickProjectButtonAndMaybeWait("mdtodo-source-save", /^\/v1\/project-management\/mdtodo\/sources/u);
return {
beforeUrl,
afterUrl: currentPageUrl(),
type: "configureMdtodoHwpodSource",
dialog,
fields,
save,
beforeProject,
afterProject: await projectManagementCommandSnapshot(),
pageId,
valuesRedacted: true
};
}
async function probeMdtodoSource(command) {
ensureProjectManagementCommand("probeMdtodoSource");
const beforeUrl = currentPageUrl();
const beforeProject = await projectManagementCommandSnapshot();
await ensureMdtodoSourceConfigOpen();
const probe = await clickProjectButtonAndMaybeWait("mdtodo-source-probe", /^\/v1\/project-management\/mdtodo\/sources/u);
return { beforeUrl, afterUrl: currentPageUrl(), type: "probeMdtodoSource", probe, beforeProject, afterProject: await projectManagementCommandSnapshot(), pageId, valuesRedacted: true };
}
async function reindexMdtodoSource(command) {
ensureProjectManagementCommand("reindexMdtodoSource");
const beforeUrl = currentPageUrl();
const beforeProject = await projectManagementCommandSnapshot();
await ensureMdtodoSourceConfigOpen();
const reindex = await clickProjectButtonAndMaybeWait("mdtodo-source-reindex", /^\/v1\/project-management\/mdtodo\/sources/u);
return { beforeUrl, afterUrl: currentPageUrl(), type: "reindexMdtodoSource", reindex, beforeProject, afterProject: await projectManagementCommandSnapshot(), pageId, valuesRedacted: true };
}
async function selectTaskIfCommandTargetsOne(command) {
if (commandValue(command, ["taskRef", "taskId", "task"]).length === 0) return null;
return selectMdtodoTask(command);
}
async function saveMdtodoTaskWithButton(command, type, testId, fields) {
ensureProjectManagementCommand(type);
const beforeUrl = currentPageUrl();
const beforeProject = await projectManagementCommandSnapshot();
const selection = await selectTaskIfCommandTargetsOne(command);
for (const field of fields) {
if (field.kind === "fill") await fillMdtodoField(field.testId, field.value);
if (field.kind === "select") {
const locator = page.locator('[data-testid="' + cssEscape(field.testId) + '"]').first();
await locator.waitFor({ state: "visible", timeout: 10000 });
await selectHtmlOptionByValueOrLabel(locator, field.value);
}
}
const save = await clickProjectButtonAndMaybeWait(testId, /^\/v1\/project-management\/mdtodo\/tasks/u);
return { beforeUrl, afterUrl: currentPageUrl(), type, selection, save, beforeProject, afterProject: await projectManagementCommandSnapshot(), pageId, valuesRedacted: true };
}
async function editMdtodoTaskTitle(command) {
const title = commandValue(command, ["title", "text", "value"]);
if (!title) throw new Error("editMdtodoTaskTitle requires --title or --text");
return saveMdtodoTaskWithButton(command, "editMdtodoTaskTitle", "mdtodo-edit-save", [{ kind: "fill", testId: "mdtodo-edit-title", value: title }]);
}
async function editMdtodoTaskBody(command) {
const body = commandValue(command, ["text", "body", "value"]);
if (!body) throw new Error("editMdtodoTaskBody requires --text or --text-stdin");
return saveMdtodoTaskWithButton(command, "editMdtodoTaskBody", "mdtodo-edit-body-save", [{ kind: "fill", testId: "mdtodo-edit-body", value: body }]);
}
async function toggleMdtodoTaskStatus(command) {
const status = commandValue(command, ["status", "value", "text"]);
if (!status) throw new Error("toggleMdtodoTaskStatus requires --status");
return saveMdtodoTaskWithButton(command, "toggleMdtodoTaskStatus", "mdtodo-edit-save", [{ kind: "select", testId: "mdtodo-edit-status", value: status }]);
}
async function fillNewTaskDraft(command) {
const title = commandValue(command, ["title", "text", "value"]);
if (!title) throw new Error(command.type + " requires --title or --text");
const fields = [await fillMdtodoField("mdtodo-new-title", title)];
const body = commandValue(command, ["body"]) || (command.title ? commandValue(command, ["text"]) : "");
if (body) fields.push(await fillMdtodoField("mdtodo-new-body", body));
return fields;
}
async function addMdtodoTaskWithButton(command, type, testId) {
ensureProjectManagementCommand(type);
const beforeUrl = currentPageUrl();
const beforeProject = await projectManagementCommandSnapshot();
const selection = type === "addMdtodoRootTask" ? null : await selectTaskIfCommandTargetsOne(command);
const fields = await fillNewTaskDraft(command);
const save = await clickProjectButtonAndMaybeWait(testId, /^\/v1\/project-management\/mdtodo\/tasks/u);
return { beforeUrl, afterUrl: currentPageUrl(), type, selection, fields, save, beforeProject, afterProject: await projectManagementCommandSnapshot(), pageId, valuesRedacted: true };
}
async function addMdtodoRootTask(command) {
return addMdtodoTaskWithButton(command, "addMdtodoRootTask", "mdtodo-add-root");
}
async function addMdtodoSubTask(command) {
return addMdtodoTaskWithButton(command, "addMdtodoSubTask", "mdtodo-add-subtask");
}
async function continueMdtodoTask(command) {
return addMdtodoTaskWithButton(command, "continueMdtodoTask", "mdtodo-continue-task");
}
async function deleteMdtodoTask(command) {
ensureProjectManagementCommand("deleteMdtodoTask");
const beforeUrl = currentPageUrl();
const beforeProject = await projectManagementCommandSnapshot();
const selection = await selectTaskIfCommandTargetsOne(command);
const firstClick = await clickProjectButtonAndMaybeWait("mdtodo-delete-task", null);
let confirmClick = null;
const confirmVisible = await visibleLocator(page.locator('[data-testid="mdtodo-delete-cancel"]').first());
if (confirmVisible) confirmClick = await clickProjectButtonAndMaybeWait("mdtodo-delete-task", /^\/v1\/project-management\/mdtodo\/tasks/u);
return { beforeUrl, afterUrl: currentPageUrl(), type: "deleteMdtodoTask", selection, firstClick, confirmClick, beforeProject, afterProject: await projectManagementCommandSnapshot(), pageId, valuesRedacted: true };
}
async function launchWorkbenchFromTask(command) {
ensureProjectManagementCommand("launchWorkbenchFromTask");
const commandType = command.type === "launchWorkbenchFromMdtodo" ? "launchWorkbenchFromMdtodo" : "launchWorkbenchFromTask";
ensureProjectManagementCommand(commandType);
const beforeUrl = currentPageUrl();
const beforeProject = await projectManagementCommandSnapshot({ includeRaw: true });
const requestedTaskRef = typeof command.taskRef === "string" && command.taskRef.trim() ? command.taskRef.trim() : null;
if (requestedTaskRef && beforeProject.selectedTaskRefRaw !== requestedTaskRef) {
const requestedTaskId = typeof command.taskId === "string" && command.taskId.trim() ? command.taskId.trim() : null;
if ((requestedTaskRef && beforeProject.selectedTaskRefRaw !== requestedTaskRef) || requestedTaskId) {
await selectMdtodoTask({ ...command, taskRef: requestedTaskRef });
}
const projectBeforeClick = await projectManagementCommandSnapshot({ includeRaw: true });
@@ -1638,6 +1973,10 @@ async function launchWorkbenchFromTask(command) {
};
}
async function launchWorkbenchFromMdtodo(command) {
return launchWorkbenchFromTask({ ...command, type: "launchWorkbenchFromMdtodo" });
}
async function projectManagementCommandSnapshot(options = {}) {
const raw = await page.evaluate(() => {
const visible = (element) => {
@@ -1650,17 +1989,27 @@ async function projectManagementCommandSnapshot(options = {}) {
const selectedTask = document.querySelector('[data-task-ref][data-selected="true"], [data-task-ref][aria-selected="true"], [data-task-ref].selected, [data-task-ref].is-selected');
const selectedSource = document.querySelector('[data-source-id][data-selected="true"], [data-source-id][aria-selected="true"], [data-source-id].selected, [data-source-id].is-selected');
const selectedFile = document.querySelector('[data-file-ref][data-selected="true"], [data-file-ref][aria-selected="true"], [data-file-ref].selected, [data-file-ref].is-selected');
const sourceSelect = document.querySelector('[data-testid="mdtodo-source-select"]');
const fileSelect = document.querySelector('[data-testid="mdtodo-file-select"]');
const sourceOptionCount = sourceSelect ? Array.from(sourceSelect.options || []).filter((option) => option.value).length : 0;
const fileOptionCount = fileSelect ? Array.from(fileSelect.options || []).filter((option) => option.value).length : 0;
const launch = document.querySelector('[data-testid="mdtodo-workbench-launch"], [data-action="launch-workbench"]');
return {
path: window.location.pathname,
pageKind: visible(document.querySelector('[data-testid="project-management-mdtodo"]')) ? "project-management-mdtodo" : visible(document.querySelector('[data-testid="project-management-root"]')) ? "project-management-root" : null,
sourceCount: Array.from(document.querySelectorAll('[data-source-id]')).filter(visible).length,
fileCount: Array.from(document.querySelectorAll('[data-file-ref]')).filter(visible).length,
sourceCount: Math.max(Array.from(document.querySelectorAll('[data-source-id]')).filter(visible).length, sourceOptionCount),
fileCount: Math.max(Array.from(document.querySelectorAll('[data-file-ref]')).filter(visible).length, fileOptionCount),
taskCount: Array.from(document.querySelectorAll('[data-task-ref]')).filter(visible).length,
selectedSourceIdRaw: selectedSource?.getAttribute("data-source-id") || null,
selectedFileRefRaw: selectedFile?.getAttribute("data-file-ref") || null,
selectedSourceIdRaw: selectedSource?.getAttribute("data-source-id") || sourceSelect?.value || null,
selectedFileRefRaw: selectedFile?.getAttribute("data-file-ref") || fileSelect?.value || null,
selectedTaskRefRaw: selectedTask?.getAttribute("data-task-ref") || null,
selectedTaskId: selectedTask?.getAttribute("data-task-id") || selectedTask?.getAttribute("data-rxx-id") || null,
selectedTaskStatus: selectedTask?.getAttribute("data-task-status") || null,
sourceSelectVisible: visible(sourceSelect),
fileSelectVisible: visible(fileSelect),
sourceConfigVisible: visible(document.querySelector('[data-testid="mdtodo-source-form-hwpod"], [data-testid="mdtodo-source-config-dialog"], [role="dialog"]')),
taskEditorVisible: visible(document.querySelector('[data-testid="mdtodo-edit-title"], [data-testid="mdtodo-edit-body"]')),
newTaskDraftVisible: visible(document.querySelector('[data-testid="mdtodo-new-title"], [data-testid="mdtodo-new-body"]')),
launchButtonVisible: visible(launch),
launchButtonEnabled: visible(launch) && !launch.disabled && launch.getAttribute("aria-disabled") !== "true",
launchButtonText: text(launch),
@@ -2077,6 +2426,10 @@ async function sampleOnePage(targetPage, { reason, groupSeq, pageRole, targetPag
if (!configuredPath && !rootVisible && !mdtodoVisible) return null;
const sourceItems = Array.from(document.querySelectorAll('[data-testid="mdtodo-source-list"] [data-source-id], [data-source-id]')).filter(visible);
const fileItems = Array.from(document.querySelectorAll('[data-testid="mdtodo-file-list"] [data-file-ref], [data-file-ref]')).filter(visible);
const sourceSelect = document.querySelector('[data-testid="mdtodo-source-select"]');
const fileSelect = document.querySelector('[data-testid="mdtodo-file-select"]');
const sourceOptionCount = sourceSelect ? Array.from(sourceSelect.options || []).filter((option) => option.value).length : 0;
const fileOptionCount = fileSelect ? Array.from(fileSelect.options || []).filter((option) => option.value).length : 0;
const taskItems = Array.from(document.querySelectorAll('[data-testid="mdtodo-task-tree"] [data-task-ref], [data-task-ref]')).filter(visible);
const taskCandidates = Array.from(document.querySelectorAll('[data-testid="mdtodo-task-tree"] li, [data-testid="mdtodo-task-tree"] [role="treeitem"], [data-testid="mdtodo-task-tree"] [role="listitem"]')).filter(visible);
const selectedSource = document.querySelector('[data-source-id][data-selected="true"], [data-source-id][aria-selected="true"], [data-source-id].selected, [data-source-id].is-selected');
@@ -2100,14 +2453,19 @@ async function sampleOnePage(targetPage, { reason, groupSeq, pageRole, targetPag
configuredPath,
rootVisible,
mdtodoVisible,
sourceCount: sourceItems.length,
fileCount: fileItems.length,
sourceCount: Math.max(sourceItems.length, sourceOptionCount),
fileCount: Math.max(fileItems.length, fileOptionCount),
taskCount: taskItems.length,
taskRefMissingCount: Math.max(0, taskCandidates.length - taskItems.length),
selectedSourceId: opaqueDomId(selectedSource?.getAttribute("data-source-id")),
selectedFileRef: opaqueDomId(selectedFile?.getAttribute("data-file-ref")),
selectedSourceId: opaqueDomId(selectedSource?.getAttribute("data-source-id") || sourceSelect?.value),
selectedFileRef: opaqueDomId(selectedFile?.getAttribute("data-file-ref") || fileSelect?.value),
selectedTaskRef: opaqueDomId(selectedTask?.getAttribute("data-task-ref")),
selectedTaskStatus: selectedTask?.getAttribute("data-task-status") || null,
sourceSelectVisible: visible(sourceSelect),
fileSelectVisible: visible(fileSelect),
sourceConfigVisible: visible(document.querySelector('[data-testid="mdtodo-source-form-hwpod"], [data-testid="mdtodo-source-config-dialog"], [role="dialog"]')),
taskEditorVisible: visible(document.querySelector('[data-testid="mdtodo-edit-title"], [data-testid="mdtodo-edit-body"]')),
newTaskDraftVisible: visible(document.querySelector('[data-testid="mdtodo-new-title"], [data-testid="mdtodo-new-body"]')),
taskStatusCounts: statusCounts,
launchButtonVisible: visible(launch),
launchButtonEnabled: visible(launch) && !launch.disabled && launch.getAttribute("aria-disabled") !== "true",
@@ -2292,6 +2650,11 @@ function digestProjectManagement(value) {
selectedFileRef: opaque(value.selectedFileRef),
selectedTaskRef: opaque(value.selectedTaskRef),
selectedTaskStatus: value.selectedTaskStatus ?? null,
sourceSelectVisible: value.sourceSelectVisible === true,
fileSelectVisible: value.fileSelectVisible === true,
sourceConfigVisible: value.sourceConfigVisible === true,
taskEditorVisible: value.taskEditorVisible === true,
newTaskDraftVisible: value.newTaskDraftVisible === true,
taskStatusCounts: value.taskStatusCounts && typeof value.taskStatusCounts === "object" ? value.taskStatusCounts : {},
launchButtonVisible: value.launchButtonVisible === true,
launchButtonEnabled: value.launchButtonEnabled === true,
@@ -2398,6 +2761,15 @@ function commandInputSummary(command) {
sourceId: opaque(command.sourceId),
fileRef: opaque(command.fileRef),
taskRef: opaque(command.taskRef),
taskId: command.taskId || null,
titleHash: command.title ? sha256Text(command.title) : null,
titleBytes: command.title ? Buffer.byteLength(command.title) : null,
bodyHash: command.body ? sha256Text(command.body) : null,
bodyBytes: command.body ? Buffer.byteLength(command.body) : null,
status: command.status || null,
hwpodId: opaque(command.hwpodId),
nodeId: opaque(command.nodeId),
root: opaque(command.root),
label: command.label ? truncate(command.label, 200) : null,
textHash: text === null ? null : sha256Text(text),
textBytes: text === null ? null : Buffer.byteLength(text),