feat: add mdtodo web-probe commands (#898)
Co-authored-by: Codex <codex@noreply.local>
This commit is contained in:
@@ -179,10 +179,25 @@ lanes:
|
||||
- /v1/project-management/
|
||||
- /v1/workbench/launches
|
||||
commandAllowlist:
|
||||
- gotoProjectMdtodo
|
||||
- openMdtodoSourceConfig
|
||||
- configureMdtodoHwpodSource
|
||||
- probeMdtodoSource
|
||||
- reindexMdtodoSource
|
||||
- selectProjectSource
|
||||
- selectMdtodoSource
|
||||
- selectMdtodoFile
|
||||
- selectMdtodoTask
|
||||
- expandMdtodoTask
|
||||
- editMdtodoTaskTitle
|
||||
- editMdtodoTaskBody
|
||||
- toggleMdtodoTaskStatus
|
||||
- addMdtodoRootTask
|
||||
- addMdtodoSubTask
|
||||
- continueMdtodoTask
|
||||
- deleteMdtodoTask
|
||||
- launchWorkbenchFromTask
|
||||
- launchWorkbenchFromMdtodo
|
||||
launchRoute: /v1/workbench/launches
|
||||
slowApiBudgetMs: 10000
|
||||
tektonDir: tekton-v03
|
||||
|
||||
@@ -92,13 +92,18 @@ export function hwlabNodeWebProbeHelp(): Record<string, unknown> {
|
||||
"bun scripts/cli.ts hwlab nodes web-probe observe command webobs-xxxx --type sendPrompt --text-stdin <<'EOF'\nlong prompt\nEOF",
|
||||
"bun scripts/cli.ts hwlab nodes web-probe observe command webobs-xxxx --type steer --text '继续观察当前 trace'",
|
||||
"bun scripts/cli.ts hwlab nodes web-probe observe command webobs-xxxx --type cancel",
|
||||
"bun scripts/cli.ts hwlab nodes web-probe observe command webobs-xxxx --type selectMdtodoTask --task-ref <opaque-task-ref>",
|
||||
"bun scripts/cli.ts hwlab nodes web-probe observe command webobs-xxxx --type launchWorkbenchFromTask",
|
||||
"bun scripts/cli.ts hwlab nodes web-probe observe command webobs-xxxx --type gotoProjectMdtodo",
|
||||
"bun scripts/cli.ts hwlab nodes web-probe observe command webobs-xxxx --type configureMdtodoHwpodSource --hwpod-id d601-f103-v2 --node-id D601 --root docs/MDTODO",
|
||||
"bun scripts/cli.ts hwlab nodes web-probe observe command webobs-xxxx --type selectMdtodoFile --file-ref docs/MDTODO/example.md",
|
||||
"bun scripts/cli.ts hwlab nodes web-probe observe command webobs-xxxx --type selectMdtodoTask --task R1.1",
|
||||
"bun scripts/cli.ts hwlab nodes web-probe observe command webobs-xxxx --type editMdtodoTaskTitle --task R1.1 --title '更新任务标题'",
|
||||
"bun scripts/cli.ts hwlab nodes web-probe observe command webobs-xxxx --type addMdtodoSubTask --task R1.1 --title '新增子任务'",
|
||||
"bun scripts/cli.ts hwlab nodes web-probe observe command webobs-xxxx --type launchWorkbenchFromMdtodo --task R1.1",
|
||||
"bun scripts/cli.ts hwlab nodes web-probe observe status webobs-xxxx",
|
||||
"bun scripts/cli.ts hwlab nodes web-probe observe stop webobs-xxxx --force",
|
||||
"bun scripts/cli.ts hwlab nodes web-probe observe collect webobs-xxxx --view turn-summary",
|
||||
"bun scripts/cli.ts hwlab nodes web-probe observe collect webobs-xxxx --view trace-frame --trace-id trc_xxx --sample-seq 42",
|
||||
"bun scripts/cli.ts hwlab nodes web-probe observe collect webobs-xxxx --view project-summary",
|
||||
"bun scripts/cli.ts hwlab nodes web-probe observe collect webobs-xxxx --view project-mdtodo-summary",
|
||||
"bun scripts/cli.ts hwlab nodes web-probe observe analyze webobs-xxxx",
|
||||
"bun scripts/cli.ts hwlab nodes web-probe sentinel plan --node D601 --lane v03 --dry-run",
|
||||
"bun scripts/cli.ts hwlab nodes web-probe sentinel status --node D601 --lane v03",
|
||||
@@ -122,9 +127,9 @@ export function hwlabNodeWebProbeHelp(): Record<string, unknown> {
|
||||
"observe sampling is passive by default: it records DOM summaries and natural page request/response/requestfailed events with observerInitiated=false; it does not actively fetch Workbench APIs, reload, switch sessions, route/intercept, or call repair helpers.",
|
||||
"observe start registers a local UniDesk-side observer id under .state/web-observe/index.json; after start, prefer observe status|command|stop|collect|analyze <id> instead of repeating --node/--lane/--state-dir.",
|
||||
"observe status reports runner liveness separately from process existence, including heartbeat age, stale threshold, command backlog, and abandoned command counts.",
|
||||
"observe command actions are explicit user/control actions and are appended to control.jsonl; use --type newSession/selectProvider/sendPrompt/steer/cancel/goto/screenshot/mark/stop. Project-management actions are selectProjectSource/selectMdtodoFile/selectMdtodoTask/launchWorkbenchFromTask and are allowed only by the selected node/lane YAML. steer/cancel reuse the Workbench composer path, and project-management launch uses public UI/API evidence plus x-hwlab-otel-trace-id capture, not private backend APIs. For long prompts use sendPrompt/steer --text-stdin; keep prompt text out of issue comments by citing textHash/textBytes.",
|
||||
"observe command actions are explicit user/control actions and are appended to control.jsonl; use --type newSession/selectProvider/sendPrompt/steer/cancel/goto/screenshot/mark/stop. Project-management actions include gotoProjectMdtodo, source config/probe/reindex, selectMdtodoSource/selectMdtodoFile/selectMdtodoTask/expandMdtodoTask, edit/toggle/add/continue/delete MDTODO task commands, and launchWorkbenchFromMdtodo. These actions are allowed only by the selected node/lane YAML. steer/cancel reuse the Workbench composer path, and project-management launch uses public UI/API evidence plus x-hwlab-otel-trace-id capture, not private backend APIs. For long prompts and task body edits use --text-stdin; keep prompt/body text out of issue comments by citing textHash/textBytes.",
|
||||
"observe stop --force first checks heartbeat/backlog health; if the runner is stale or commands are not being consumed, it kills the recorded PID from outside the command queue and marks pending/processing commands abandoned so analyze classifies them as tooling findings.",
|
||||
"observe collect --view turn-summary renders the multi-turn CLI reading layer from samples/control artifacts; --view trace-frame renders one sampled trace frame with a fixed Final Response block; --view project-summary renders project-management/mdtodo samples, Workbench launch commands, and OTel trace drill-down commands from the same artifacts. collect views do not save a second source of truth.",
|
||||
"observe collect --view turn-summary renders the multi-turn CLI reading layer from samples/control artifacts; --view trace-frame renders one sampled trace frame with a fixed Final Response block; --view project-summary is the legacy project view, and --view project-mdtodo-summary renders MDTODO samples, interactive command/mutation rows, Workbench launch commands, and OTel trace drill-down commands from the same artifacts. collect views do not save a second source of truth.",
|
||||
"observe analyze is offline-only: it reads artifact JSONL plus observer command/heartbeat artifacts and writes analysis/report.md plus analysis/report.json without accessing Workbench APIs or driving the browser. For project-management pages it reports DOM readiness, public task id coverage, Workbench launch success/failure, captured launch OTel trace headers, and YAML-budgeted project API timing.",
|
||||
"observe analyze scans every sampled DOM point, extracts Workbench timing text such as 总耗时/total and 最近 N 秒/分前, and writes a sample point vs turn timing report: each Markdown table row starts with the timestamp, followed by each turn's 总耗时(s) and 最近更新(s). Timing series are reported for post-processing/manual analysis instead of auto-judged from status tail output.",
|
||||
"observe analyze also reports visible “加载中” count, owner attribution, concurrent loading owners, and continuous visible segments; fixes must reduce real loading latency, not reveal incomplete content early to make this metric disappear.",
|
||||
|
||||
@@ -68,7 +68,38 @@ interface NodeWebProbeScriptOptions {
|
||||
}
|
||||
|
||||
type NodeWebProbeObserveAction = "start" | "status" | "command" | "stop" | "collect" | "analyze";
|
||||
type NodeWebProbeObserveCommandType = "login" | "preflight" | "goto" | "newSession" | "sendPrompt" | "steer" | "cancel" | "selectProvider" | "clickSession" | "selectProjectSource" | "selectMdtodoFile" | "selectMdtodoTask" | "launchWorkbenchFromTask" | "screenshot" | "mark" | "stop";
|
||||
type NodeWebProbeObserveCommandType =
|
||||
| "login"
|
||||
| "preflight"
|
||||
| "goto"
|
||||
| "gotoProjectMdtodo"
|
||||
| "newSession"
|
||||
| "sendPrompt"
|
||||
| "steer"
|
||||
| "cancel"
|
||||
| "selectProvider"
|
||||
| "clickSession"
|
||||
| "selectProjectSource"
|
||||
| "selectMdtodoSource"
|
||||
| "selectMdtodoFile"
|
||||
| "selectMdtodoTask"
|
||||
| "expandMdtodoTask"
|
||||
| "openMdtodoSourceConfig"
|
||||
| "configureMdtodoHwpodSource"
|
||||
| "probeMdtodoSource"
|
||||
| "reindexMdtodoSource"
|
||||
| "editMdtodoTaskTitle"
|
||||
| "editMdtodoTaskBody"
|
||||
| "toggleMdtodoTaskStatus"
|
||||
| "addMdtodoRootTask"
|
||||
| "addMdtodoSubTask"
|
||||
| "continueMdtodoTask"
|
||||
| "deleteMdtodoTask"
|
||||
| "launchWorkbenchFromTask"
|
||||
| "launchWorkbenchFromMdtodo"
|
||||
| "screenshot"
|
||||
| "mark"
|
||||
| "stop";
|
||||
|
||||
interface NodeWebProbeObserveOptions {
|
||||
action: "observe";
|
||||
@@ -111,6 +142,13 @@ interface NodeWebProbeObserveOptions {
|
||||
commandSourceId: string | null;
|
||||
commandFileRef: string | null;
|
||||
commandTaskRef: string | null;
|
||||
commandTaskId: string | null;
|
||||
commandTitle: string | null;
|
||||
commandBody: string | null;
|
||||
commandStatus: string | null;
|
||||
commandHwpodId: string | null;
|
||||
commandNodeId: string | null;
|
||||
commandRoot: string | null;
|
||||
}
|
||||
|
||||
interface NodeWebProbeSentinelOptions {
|
||||
@@ -7537,6 +7575,14 @@ function parseNodeWebProbeObserveOptions(
|
||||
"--source-id",
|
||||
"--file-ref",
|
||||
"--task-ref",
|
||||
"--task",
|
||||
"--task-id",
|
||||
"--title",
|
||||
"--body",
|
||||
"--status",
|
||||
"--hwpod-id",
|
||||
"--node-id",
|
||||
"--root",
|
||||
]), new Set(["--force", "--full", "--text-stdin"]));
|
||||
const commandTypeRaw = optionValue(args, "--type") ?? null;
|
||||
const commandType = commandTypeRaw === null ? null : parseNodeWebProbeObserveCommandType(commandTypeRaw);
|
||||
@@ -7583,7 +7629,25 @@ function parseNodeWebProbeObserveOptions(
|
||||
const commandSourceId = optionValue(args, "--source-id") ?? null;
|
||||
const commandFileRef = optionValue(args, "--file-ref") ?? null;
|
||||
const commandTaskRef = optionValue(args, "--task-ref") ?? null;
|
||||
for (const [label, value] of [["--source-id", commandSourceId], ["--file-ref", commandFileRef], ["--task-ref", commandTaskRef]] as const) {
|
||||
const commandTaskId = optionValue(args, "--task-id") ?? optionValue(args, "--task") ?? null;
|
||||
const commandTitle = optionValue(args, "--title") ?? null;
|
||||
const commandBody = optionValue(args, "--body") ?? null;
|
||||
const commandStatus = optionValue(args, "--status") ?? null;
|
||||
const commandHwpodId = optionValue(args, "--hwpod-id") ?? null;
|
||||
const commandNodeId = optionValue(args, "--node-id") ?? null;
|
||||
const commandRoot = optionValue(args, "--root") ?? null;
|
||||
for (const [label, value] of [
|
||||
["--source-id", commandSourceId],
|
||||
["--file-ref", commandFileRef],
|
||||
["--task-ref", commandTaskRef],
|
||||
["--task/--task-id", commandTaskId],
|
||||
["--title", commandTitle],
|
||||
["--body", commandBody],
|
||||
["--status", commandStatus],
|
||||
["--hwpod-id", commandHwpodId],
|
||||
["--node-id", commandNodeId],
|
||||
["--root", commandRoot],
|
||||
] as const) {
|
||||
if (value !== null && (value.includes("\0") || value.length > 500)) throw new Error(`unsafe web-probe observe ${label}: expected 1-500 non-NUL chars`);
|
||||
}
|
||||
return {
|
||||
@@ -7627,6 +7691,13 @@ function parseNodeWebProbeObserveOptions(
|
||||
commandSourceId,
|
||||
commandFileRef,
|
||||
commandTaskRef,
|
||||
commandTaskId,
|
||||
commandTitle,
|
||||
commandBody,
|
||||
commandStatus,
|
||||
commandHwpodId,
|
||||
commandNodeId,
|
||||
commandRoot,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -7641,15 +7712,30 @@ function parseNodeWebProbeObserveCommandType(value: string): NodeWebProbeObserve
|
||||
|| value === "cancel"
|
||||
|| value === "selectProvider"
|
||||
|| value === "clickSession"
|
||||
|| value === "gotoProjectMdtodo"
|
||||
|| value === "selectProjectSource"
|
||||
|| value === "selectMdtodoSource"
|
||||
|| value === "selectMdtodoFile"
|
||||
|| value === "selectMdtodoTask"
|
||||
|| value === "expandMdtodoTask"
|
||||
|| value === "openMdtodoSourceConfig"
|
||||
|| value === "configureMdtodoHwpodSource"
|
||||
|| value === "probeMdtodoSource"
|
||||
|| value === "reindexMdtodoSource"
|
||||
|| value === "editMdtodoTaskTitle"
|
||||
|| value === "editMdtodoTaskBody"
|
||||
|| value === "toggleMdtodoTaskStatus"
|
||||
|| value === "addMdtodoRootTask"
|
||||
|| value === "addMdtodoSubTask"
|
||||
|| value === "continueMdtodoTask"
|
||||
|| value === "deleteMdtodoTask"
|
||||
|| value === "launchWorkbenchFromTask"
|
||||
|| value === "launchWorkbenchFromMdtodo"
|
||||
|| value === "screenshot"
|
||||
|| value === "mark"
|
||||
|| value === "stop"
|
||||
) return value;
|
||||
throw new Error(`web-probe observe command --type must be login, preflight, goto, newSession, sendPrompt, steer, cancel, selectProvider, clickSession, selectProjectSource, selectMdtodoFile, selectMdtodoTask, launchWorkbenchFromTask, screenshot, mark, or stop; got ${value}`);
|
||||
throw new Error(`web-probe observe command --type must be login, preflight, goto, gotoProjectMdtodo, newSession, sendPrompt, steer, cancel, selectProvider, clickSession, selectProjectSource, selectMdtodoSource, selectMdtodoFile, selectMdtodoTask, expandMdtodoTask, openMdtodoSourceConfig, configureMdtodoHwpodSource, probeMdtodoSource, reindexMdtodoSource, editMdtodoTaskTitle, editMdtodoTaskBody, toggleMdtodoTaskStatus, addMdtodoRootTask, addMdtodoSubTask, continueMdtodoTask, deleteMdtodoTask, launchWorkbenchFromTask, launchWorkbenchFromMdtodo, screenshot, mark, or stop; got ${value}`);
|
||||
}
|
||||
|
||||
function parseWebProbeBrowserProxyMode(value: string | undefined): WebProbeBrowserProxyMode {
|
||||
@@ -8327,6 +8413,13 @@ function runNodeWebProbeObserveCommand(options: NodeWebProbeObserveOptions, spec
|
||||
sourceId: options.commandSourceId,
|
||||
fileRef: options.commandFileRef,
|
||||
taskRef: options.commandTaskRef,
|
||||
taskId: options.commandTaskId,
|
||||
title: options.commandTitle,
|
||||
body: options.commandBody,
|
||||
status: options.commandStatus,
|
||||
hwpodId: options.commandHwpodId,
|
||||
nodeId: options.commandNodeId,
|
||||
root: options.commandRoot,
|
||||
};
|
||||
const preStopStatus = options.force && stopCommand
|
||||
? readNodeWebProbeObserveRemoteStatus(options, spec, 1, Math.min(options.commandTimeoutSeconds, 30))
|
||||
@@ -10749,6 +10842,8 @@ function nodeWebObserveWaitCommandShell(commandId: string, waitMs: number): stri
|
||||
|
||||
function commandSummaryForOutput(payload: Record<string, unknown>): Record<string, unknown> {
|
||||
const text = typeof payload.text === "string" ? payload.text : null;
|
||||
const title = typeof payload.title === "string" ? payload.title : null;
|
||||
const body = typeof payload.body === "string" ? payload.body : null;
|
||||
const opaque = (value: unknown) => typeof value === "string" && value.length > 0
|
||||
? {
|
||||
hash: `sha256:${createHash("sha256").update(value).digest("hex")}`,
|
||||
@@ -10766,6 +10861,15 @@ function commandSummaryForOutput(payload: Record<string, unknown>): Record<strin
|
||||
sourceId: opaque(payload.sourceId),
|
||||
fileRef: opaque(payload.fileRef),
|
||||
taskRef: opaque(payload.taskRef),
|
||||
taskId: payload.taskId ?? null,
|
||||
titleHash: title === null ? null : `sha256:${createHash("sha256").update(title).digest("hex")}`,
|
||||
titleBytes: title === null ? null : Buffer.byteLength(title),
|
||||
bodyHash: body === null ? null : `sha256:${createHash("sha256").update(body).digest("hex")}`,
|
||||
bodyBytes: body === null ? null : Buffer.byteLength(body),
|
||||
status: payload.status ?? null,
|
||||
hwpodId: opaque(payload.hwpodId),
|
||||
nodeId: opaque(payload.nodeId),
|
||||
root: opaque(payload.root),
|
||||
textHash: text === null ? null : `sha256:${createHash("sha256").update(text).digest("hex")}`,
|
||||
textBytes: text === null ? null : Buffer.byteLength(text),
|
||||
valuesRedacted: true,
|
||||
|
||||
@@ -779,6 +779,11 @@ function compactProjectManagementSample(value) {
|
||||
selectedFileRef: value.selectedFileRef ?? null,
|
||||
selectedTaskRef: value.selectedTaskRef ?? null,
|
||||
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,
|
||||
@@ -999,7 +1004,7 @@ function buildProjectManagementReport(samples, control, network, pagePerformance
|
||||
const pageKindCounts = countBy(projectSamples.map((sample) => sample.projectManagement?.pageKind).filter(Boolean));
|
||||
const latestTaskStatusCounts = latestProject?.taskStatusCounts && typeof latestProject.taskStatusCounts === "object" ? latestProject.taskStatusCounts : {};
|
||||
const commandRows = projectManagementCommandRows(control, config);
|
||||
const launchCommands = commandRows.filter((item) => item.type === "launchWorkbenchFromTask");
|
||||
const launchCommands = commandRows.filter((item) => item.type === "launchWorkbenchFromTask" || item.type === "launchWorkbenchFromMdtodo");
|
||||
const launchSuccess = launchCommands.filter((item) => item.phase === "completed" && Number(item.launchStatus ?? 0) >= 200 && Number(item.launchStatus ?? 0) < 300);
|
||||
const launchFailed = launchCommands.filter((item) => item.phase === "failed" || Number(item.launchStatus ?? 200) >= 400);
|
||||
const projectApiEvents = projectManagementNetworkRows(network, config);
|
||||
@@ -1155,8 +1160,9 @@ function compactProjectManagementForOutput(report) {
|
||||
|
||||
function projectManagementCommandRows(control, config) {
|
||||
const allowed = new Set(config?.commandAllowlist || []);
|
||||
const mdtodoCommandTypes = new Set(["gotoProjectMdtodo", "openMdtodoSourceConfig", "configureMdtodoHwpodSource", "probeMdtodoSource", "reindexMdtodoSource", "expandMdtodoTask", "editMdtodoTaskTitle", "editMdtodoTaskBody", "toggleMdtodoTaskStatus", "addMdtodoRootTask", "addMdtodoSubTask", "continueMdtodoTask", "deleteMdtodoTask", "launchWorkbenchFromMdtodo"]);
|
||||
return (control || [])
|
||||
.filter((item) => allowed.has(item?.type) || String(item?.type || "").startsWith("selectMdtodo") || item?.type === "selectProjectSource" || item?.type === "launchWorkbenchFromTask")
|
||||
.filter((item) => allowed.has(item?.type) || mdtodoCommandTypes.has(item?.type) || String(item?.type || "").startsWith("selectMdtodo") || item?.type === "selectProjectSource" || item?.type === "launchWorkbenchFromTask")
|
||||
.filter((item) => item.phase === "completed" || item.phase === "failed")
|
||||
.map((item) => {
|
||||
const detail = item.detail && typeof item.detail === "object" ? item.detail : {};
|
||||
@@ -1238,7 +1244,7 @@ function buildProjectManagementFindings(report) {
|
||||
findings.push({ id: "project-management-api-slow", severity: "red", summary: "project-management API resource timing exceeded YAML projectManagement.slowApiBudgetMs", count: summary.projectApiSlowPathCount, budgetMs: summary.slowApiBudgetMs, groups: report.slowProjectApiPerformance.slice(0, 12), valuesRedacted: true });
|
||||
}
|
||||
if (Number(summary.launchFailureCount ?? 0) > 0) {
|
||||
findings.push({ id: "mdtodo-workbench-launch-failed", severity: "red", summary: "launchWorkbenchFromTask command failed or returned an HTTP error", count: summary.launchFailureCount, commands: report.launchCommands.filter((item) => item.phase === "failed" || Number(item.launchStatus ?? 200) >= 400).slice(0, 12), valuesRedacted: true });
|
||||
findings.push({ id: "mdtodo-workbench-launch-failed", severity: "red", summary: "MDTODO Workbench launch command failed or returned an HTTP error", count: summary.launchFailureCount, commands: report.launchCommands.filter((item) => item.phase === "failed" || Number(item.launchStatus ?? 200) >= 400).slice(0, 12), valuesRedacted: true });
|
||||
}
|
||||
if (Number(summary.launchSuccessCount ?? 0) > 0 && Number(summary.launchWithOtelTraceHeaderCount ?? 0) === 0) {
|
||||
findings.push({ id: "mdtodo-workbench-launch-otel-trace-missing", severity: "amber", summary: "Workbench launch succeeded but no x-hwlab-otel-trace-id header was captured for Tempo drill-down", count: summary.launchSuccessCount, commands: report.launchCommands.slice(0, 12), valuesRedacted: true });
|
||||
|
||||
@@ -2,11 +2,11 @@
|
||||
// Responsibility: Offline CLI view renderers for HWLAB web-probe observe artifacts.
|
||||
import { shellQuote } from "./ssh";
|
||||
|
||||
export type NodeWebProbeObserveCollectView = "files" | "turn-summary" | "trace-frame" | "project-summary";
|
||||
export type NodeWebProbeObserveCollectView = "files" | "turn-summary" | "trace-frame" | "project-summary" | "project-mdtodo-summary";
|
||||
|
||||
export function parseNodeWebProbeObserveCollectView(value: string): NodeWebProbeObserveCollectView {
|
||||
if (value === "files" || value === "turn-summary" || value === "trace-frame" || value === "project-summary") return value;
|
||||
throw new Error(`web-probe observe collect --view must be files, turn-summary, trace-frame, or project-summary; got ${value}`);
|
||||
if (value === "files" || value === "turn-summary" || value === "trace-frame" || value === "project-summary" || value === "project-mdtodo-summary") return value;
|
||||
throw new Error(`web-probe observe collect --view must be files, turn-summary, trace-frame, project-summary, or project-mdtodo-summary; got ${value}`);
|
||||
}
|
||||
|
||||
export function nodeWebObserveCollectViewNodeScript(input: {
|
||||
@@ -322,11 +322,20 @@ function renderTraceFrame(sample,rows){
|
||||
const rendered=['Code Agent 耗时 '+(elapsed>=0?fmtDuration(elapsed):'-')+' 最近 '+(recent>=0?String(recent)+' 秒前':'-')+' ('+status+')','=======================================================','sample seq='+(sample.seq??'-')+' ts='+(sample.ts||'-')+' traceId='+(traceId||'-')+' routeSession='+(sample.routeSessionId||'-')+' activeSession='+(sample.activeSessionId||'-'),...bodyRows,'==========================','Final Response',finalResponse.preview||'(空内容)'].join('\\n');
|
||||
return {ok:!missingRows,renderedText:rendered,blocker:missingRows?'trace-rows-missing':null,sampleSeq:sample.seq??null,traceId,finalResponse,traceDiagnostic:missingRows?{pageRole:sample.pageRole||null,pageId:sample.pageId||null,traceRows:Array.isArray(sample.traceRows)?sample.traceRows.length:0,turns:Array.isArray(sample.turns)?sample.turns.length:0,messages:Array.isArray(sample.messages)?sample.messages.length:0,sampleTraceIds:traceIdsFromSamples([sample]).slice(0,12)}:null,valuesRedacted:true};
|
||||
}
|
||||
function pathOnly(value){try{return value?new URL(String(value),'http://x').pathname:null}catch{return null}}
|
||||
function projectSummaryFromSamples(){
|
||||
const projectSamples=samples.filter((sample)=>sample?.projectManagement&&typeof sample.projectManagement==='object');
|
||||
const latest=projectSamples[projectSamples.length-1]||null;
|
||||
const latestProject=latest?.projectManagement||null;
|
||||
const launches=control.filter((item)=>item.type==='launchWorkbenchFromTask'&&(item.phase==='completed'||item.phase==='failed')).map((item)=>{
|
||||
const projectCommandTypes=new Set(['gotoProjectMdtodo','openMdtodoSourceConfig','configureMdtodoHwpodSource','probeMdtodoSource','reindexMdtodoSource','selectProjectSource','selectMdtodoSource','selectMdtodoFile','selectMdtodoTask','expandMdtodoTask','editMdtodoTaskTitle','editMdtodoTaskBody','toggleMdtodoTaskStatus','addMdtodoRootTask','addMdtodoSubTask','continueMdtodoTask','deleteMdtodoTask','launchWorkbenchFromTask','launchWorkbenchFromMdtodo']);
|
||||
const mutationTypes=new Set(['configureMdtodoHwpodSource','probeMdtodoSource','reindexMdtodoSource','editMdtodoTaskTitle','editMdtodoTaskBody','toggleMdtodoTaskStatus','addMdtodoRootTask','addMdtodoSubTask','continueMdtodoTask','deleteMdtodoTask']);
|
||||
const commandRows=control.filter((item)=>projectCommandTypes.has(item.type)&&(item.phase==='completed'||item.phase==='failed')).map((item)=>{
|
||||
const detail=item.detail&&typeof item.detail==='object'?item.detail:{};
|
||||
const error=detail.error&&typeof detail.error==='object'?detail.error:{};
|
||||
return {ts:item.ts||null,phase:item.phase||null,type:item.type||null,commandId:item.commandId||null,afterPath:pathOnly(item.afterUrl),selectedTaskHash:detail.selectedTask?.hash??detail.projectBeforeClick?.selectedTaskRef?.hash??detail.afterProject?.selectedTaskRef?.hash??null,status:detail.launchStatus??error.details?.launchStatus??null,message:error.message?short(error.message,160):null,valuesRedacted:true};
|
||||
});
|
||||
const mutations=commandRows.filter((item)=>mutationTypes.has(item.type));
|
||||
const launches=control.filter((item)=>(item.type==='launchWorkbenchFromTask'||item.type==='launchWorkbenchFromMdtodo')&&(item.phase==='completed'||item.phase==='failed')).map((item)=>{
|
||||
const detail=item.detail&&typeof item.detail==='object'?item.detail:{};
|
||||
const error=detail.error&&typeof detail.error==='object'?detail.error:{};
|
||||
return {ts:item.ts||null,phase:item.phase||null,commandId:item.commandId||null,status:detail.launchStatus??error.details?.launchStatus??null,sessionId:detail.sessionId??error.details?.sessionId??null,workbenchUrl:detail.workbenchUrl??error.details?.workbenchUrl??null,otelTraceId:detail.otelTraceId??error.details?.otelTraceId??null,taskHash:detail.selectedTask?.hash??detail.projectBeforeClick?.selectedTaskRef?.hash??null,message:error.message?short(error.message,160):null,valuesRedacted:true};
|
||||
@@ -334,8 +343,8 @@ function projectSummaryFromSamples(){
|
||||
const findings=Array.isArray(report.findings)?report.findings.filter((item)=>String(item?.id||item?.kind||'').match(/project-management|mdtodo|workbench-launch/u)).slice(0,20):[];
|
||||
const summary=report.projectManagement?.summary||{};
|
||||
const mdtodoSampleCount=projectSamples.filter((sample)=>sample.projectManagement?.pageKind==='project-management-mdtodo').length;
|
||||
const derived={enabled:summary.enabled===true||projectSamples.length>0,projectSampleCount:Math.max(Number(summary.projectSampleCount??0),projectSamples.length),mdtodoSampleCount:Math.max(Number(summary.mdtodoSampleCount??0),mdtodoSampleCount),latestPageKind:summary.latestPageKind??latestProject?.pageKind??null,latestPath:summary.latestPath??latest?.path??null,latestSeq:summary.latestSeq??latest?.seq??null,latestTs:summary.latestTs??latest?.ts??null,latestSourceCount:summary.latestSourceCount??latestProject?.sourceCount??null,latestFileCount:summary.latestFileCount??latestProject?.fileCount??null,latestTaskCount:summary.latestTaskCount??latestProject?.taskCount??null,latestSelectedTaskRefHash:summary.latestSelectedTaskRefHash??latestProject?.selectedTaskRef?.hash??null,latestSelectedTaskRefPreview:summary.latestSelectedTaskRefPreview??latestProject?.selectedTaskRef?.preview??null,launchCommandCount:summary.launchCommandCount??launches.length,launchSuccessCount:summary.launchSuccessCount??launches.filter((item)=>Number(item.status)>=200&&Number(item.status)<300).length,launchFailureCount:summary.launchFailureCount??launches.filter((item)=>item.phase==='failed'||Number(item.status)>=400).length,launchWithOtelTraceHeaderCount:summary.launchWithOtelTraceHeaderCount??launches.filter((item)=>item.otelTraceId).length,projectApiResponseCount:summary.projectApiResponseCount??null,projectApiFailureCount:summary.projectApiFailureCount??null,projectApiRequestFailedCount:summary.projectApiRequestFailedCount??null,projectApiSlowPathCount:summary.projectApiSlowPathCount??null,valuesRedacted:true};
|
||||
return {summary:derived,launches:launches.slice(-12),findings,sampleRows:projectSamples.slice(-12).map((sample)=>({seq:sample.seq??null,ts:sample.ts??null,pageRole:sample.pageRole??null,path:sample.path??null,pageKind:sample.projectManagement?.pageKind??null,sourceCount:sample.projectManagement?.sourceCount??null,fileCount:sample.projectManagement?.fileCount??null,taskCount:sample.projectManagement?.taskCount??null,selectedTaskRefHash:sample.projectManagement?.selectedTaskRef?.hash??null,selectedTaskStatus:sample.projectManagement?.selectedTaskStatus??null,launchButtonEnabled:sample.projectManagement?.launchButtonEnabled===true,workbenchLinkCount:sample.projectManagement?.workbenchLinkCount??0,valuesRedacted:true})),valuesRedacted:true};
|
||||
const derived={enabled:summary.enabled===true||projectSamples.length>0,projectSampleCount:Math.max(Number(summary.projectSampleCount??0),projectSamples.length),mdtodoSampleCount:Math.max(Number(summary.mdtodoSampleCount??0),mdtodoSampleCount),latestPageKind:summary.latestPageKind??latestProject?.pageKind??null,latestPath:summary.latestPath??latest?.path??null,latestSeq:summary.latestSeq??latest?.seq??null,latestTs:summary.latestTs??latest?.ts??null,latestSourceCount:summary.latestSourceCount??latestProject?.sourceCount??null,latestFileCount:summary.latestFileCount??latestProject?.fileCount??null,latestTaskCount:summary.latestTaskCount??latestProject?.taskCount??null,latestSelectedTaskRefHash:summary.latestSelectedTaskRefHash??latestProject?.selectedTaskRef?.hash??null,latestSelectedTaskRefPreview:summary.latestSelectedTaskRefPreview??latestProject?.selectedTaskRef?.preview??null,projectCommandCount:commandRows.length,mutationCommandCount:mutations.length,mutationFailureCount:mutations.filter((item)=>item.phase==='failed').length,launchCommandCount:summary.launchCommandCount??launches.length,launchSuccessCount:summary.launchSuccessCount??launches.filter((item)=>Number(item.status)>=200&&Number(item.status)<300).length,launchFailureCount:summary.launchFailureCount??launches.filter((item)=>item.phase==='failed'||Number(item.status)>=400).length,launchWithOtelTraceHeaderCount:summary.launchWithOtelTraceHeaderCount??launches.filter((item)=>item.otelTraceId).length,projectApiResponseCount:summary.projectApiResponseCount??null,projectApiFailureCount:summary.projectApiFailureCount??null,projectApiRequestFailedCount:summary.projectApiRequestFailedCount??null,projectApiSlowPathCount:summary.projectApiSlowPathCount??null,valuesRedacted:true};
|
||||
return {summary:derived,commands:commandRows.slice(-24),mutations:mutations.slice(-16),launches:launches.slice(-12),findings,sampleRows:projectSamples.slice(-12).map((sample)=>({seq:sample.seq??null,ts:sample.ts??null,pageRole:sample.pageRole??null,path:sample.path??null,pageKind:sample.projectManagement?.pageKind??null,sourceCount:sample.projectManagement?.sourceCount??null,fileCount:sample.projectManagement?.fileCount??null,taskCount:sample.projectManagement?.taskCount??null,selectedTaskRefHash:sample.projectManagement?.selectedTaskRef?.hash??null,selectedTaskStatus:sample.projectManagement?.selectedTaskStatus??null,launchButtonEnabled:sample.projectManagement?.launchButtonEnabled===true,workbenchLinkCount:sample.projectManagement?.workbenchLinkCount??0,valuesRedacted:true})),valuesRedacted:true};
|
||||
}
|
||||
function targetNodeFromStateDir(){
|
||||
const parts=String(dir||'').split(/[\\\\/]+/u);
|
||||
@@ -344,11 +353,17 @@ function targetNodeFromStateDir(){
|
||||
}
|
||||
function renderProjectSummary(project){
|
||||
const s=project.summary||{};
|
||||
const lines=['Project management observer '+(manifest.jobId||'-'),'=======================================================','enabled='+String(s.enabled===true)+' samples='+String(s.projectSampleCount??0)+' mdtodo='+String(s.mdtodoSampleCount??0)+' latest='+String(s.latestPageKind||'-')+' path='+String(s.latestPath||'-'),'counts source='+String(s.latestSourceCount??'-')+' file='+String(s.latestFileCount??'-')+' task='+String(s.latestTaskCount??'-')+' selectedTask='+String(s.latestSelectedTaskRefHash||'-'),'launch commands='+String(s.launchCommandCount??0)+' success='+String(s.launchSuccessCount??0)+' failure='+String(s.launchFailureCount??0)+' otelTraceHeader='+String(s.launchWithOtelTraceHeaderCount??0),'api responses='+String(s.projectApiResponseCount??'-')+' failures='+String(s.projectApiFailureCount??'-')+'/'+String(s.projectApiRequestFailedCount??'-')+' slowPaths='+String(s.projectApiSlowPathCount??'-'),'','Recent samples'];
|
||||
const lines=['Project MDTODO observer '+(manifest.jobId||'-'),'=======================================================','enabled='+String(s.enabled===true)+' samples='+String(s.projectSampleCount??0)+' mdtodo='+String(s.mdtodoSampleCount??0)+' latest='+String(s.latestPageKind||'-')+' path='+String(s.latestPath||'-'),'counts source='+String(s.latestSourceCount??'-')+' file='+String(s.latestFileCount??'-')+' task='+String(s.latestTaskCount??'-')+' selectedTask='+String(s.latestSelectedTaskRefHash||'-'),'commands='+String(s.projectCommandCount??0)+' mutations='+String(s.mutationCommandCount??0)+' mutationFailures='+String(s.mutationFailureCount??0),'launch commands='+String(s.launchCommandCount??0)+' success='+String(s.launchSuccessCount??0)+' failure='+String(s.launchFailureCount??0)+' otelTraceHeader='+String(s.launchWithOtelTraceHeaderCount??0),'api responses='+String(s.projectApiResponseCount??'-')+' failures='+String(s.projectApiFailureCount??'-')+'/'+String(s.projectApiRequestFailedCount??'-')+' slowPaths='+String(s.projectApiSlowPathCount??'-'),'','Recent samples'];
|
||||
for(const row of project.sampleRows.slice(-12)) lines.push('#'+String(row.seq??'-')+' '+String(row.ts||'-')+' '+String(row.pageRole||'-')+' '+String(row.pageKind||'-')+' src='+String(row.sourceCount??'-')+' files='+String(row.fileCount??'-')+' tasks='+String(row.taskCount??'-')+' selected='+String(row.selectedTaskRefHash||'-')+' launch='+String(row.launchButtonEnabled===true)+' links='+String(row.workbenchLinkCount??0));
|
||||
lines.push('','Launches');
|
||||
if(project.launches.length===0) lines.push('-');
|
||||
for(const item of project.launches.slice(-12)) lines.push(String(item.ts||'-')+' '+String(item.phase||'-')+' status='+String(item.status??'-')+' session='+String(item.sessionId||'-')+' otel='+String(item.otelTraceId||'-')+' task='+String(item.taskHash||'-'));
|
||||
lines.push('','MDTODO commands');
|
||||
if(project.commands.length===0) lines.push('-');
|
||||
for(const item of project.commands.slice(-24)) lines.push(String(item.ts||'-')+' '+String(item.phase||'-')+' '+String(item.type||'-')+' path='+String(item.afterPath||'-')+' task='+String(item.selectedTaskHash||'-')+' status='+String(item.status??'-')+(item.message?' message='+short(item.message,120):''));
|
||||
lines.push('','MDTODO mutations');
|
||||
if(project.mutations.length===0) lines.push('-');
|
||||
for(const item of project.mutations.slice(-16)) lines.push(String(item.ts||'-')+' '+String(item.phase||'-')+' '+String(item.type||'-')+' task='+String(item.selectedTaskHash||'-')+' status='+String(item.status??'-')+(item.message?' message='+short(item.message,120):''));
|
||||
const otelLaunches=project.launches.filter((item)=>item.otelTraceId).slice(-4);
|
||||
if(otelLaunches.length>0){
|
||||
const target=targetNodeFromStateDir()||'<target>';
|
||||
@@ -361,9 +376,9 @@ function renderProjectSummary(project){
|
||||
return lines.join('\\n');
|
||||
}
|
||||
const rows=turnSummaryRows();
|
||||
if(view==='project-summary'){
|
||||
if(view==='project-summary'||view==='project-mdtodo-summary'){
|
||||
const project=projectSummaryFromSamples();
|
||||
console.log(JSON.stringify({ok:true,command:'web-probe-observe collect',view,stateDir:dir,summary:project.summary,sampleRowCount:project.sampleRows.length,launchCount:project.launches.length,findingCount:project.findings.length,renderedText:renderProjectSummary(project),sourceFiles:['samples.jsonl','control.jsonl','analysis/report.json'],valuesRedacted:true}));
|
||||
console.log(JSON.stringify({ok:true,command:'web-probe-observe collect',view,stateDir:dir,summary:project.summary,sampleRowCount:project.sampleRows.length,commandCount:project.commands.length,mutationCount:project.mutations.length,launchCount:project.launches.length,findingCount:project.findings.length,commands:project.commands,mutations:project.mutations,launches:project.launches,renderedText:renderProjectSummary(project),sourceFiles:['samples.jsonl','control.jsonl','analysis/report.json'],valuesRedacted:true}));
|
||||
process.exit(0);
|
||||
}
|
||||
if(view==='turn-summary'){
|
||||
|
||||
@@ -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),
|
||||
|
||||
Reference in New Issue
Block a user