codeloop-mcp-server 0.1.50 → 0.1.51
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/auth/critical_floors.d.ts.map +1 -1
- package/dist/auth/critical_floors.js +4 -0
- package/dist/auth/critical_floors.js.map +1 -1
- package/dist/evidence/loop_state.d.ts +53 -0
- package/dist/evidence/loop_state.d.ts.map +1 -0
- package/dist/evidence/loop_state.js +147 -0
- package/dist/evidence/loop_state.js.map +1 -0
- package/dist/evidence/verify_staleness.d.ts +9 -0
- package/dist/evidence/verify_staleness.d.ts.map +1 -0
- package/dist/evidence/verify_staleness.js +180 -0
- package/dist/evidence/verify_staleness.js.map +1 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +252 -17
- package/dist/index.js.map +1 -1
- package/dist/runners/maestro.d.ts +13 -0
- package/dist/runners/maestro.d.ts.map +1 -1
- package/dist/runners/maestro.js +37 -1
- package/dist/runners/maestro.js.map +1 -1
- package/dist/runners/modal_detector.d.ts +60 -0
- package/dist/runners/modal_detector.d.ts.map +1 -0
- package/dist/runners/modal_detector.js +160 -0
- package/dist/runners/modal_detector.js.map +1 -0
- package/dist/runners/python_tests.d.ts +26 -0
- package/dist/runners/python_tests.d.ts.map +1 -0
- package/dist/runners/python_tests.js +181 -0
- package/dist/runners/python_tests.js.map +1 -0
- package/dist/runners/rust_tests.d.ts +28 -0
- package/dist/runners/rust_tests.d.ts.map +1 -0
- package/dist/runners/rust_tests.js +76 -0
- package/dist/runners/rust_tests.js.map +1 -0
- package/dist/tools/diagnose.d.ts.map +1 -1
- package/dist/tools/diagnose.js +13 -0
- package/dist/tools/diagnose.js.map +1 -1
- package/dist/tools/gate_check.d.ts +2 -1
- package/dist/tools/gate_check.d.ts.map +1 -1
- package/dist/tools/gate_check.js +46 -32
- package/dist/tools/gate_check.js.map +1 -1
- package/dist/tools/is_ui_project.d.ts +23 -0
- package/dist/tools/is_ui_project.d.ts.map +1 -0
- package/dist/tools/is_ui_project.js +42 -0
- package/dist/tools/is_ui_project.js.map +1 -0
- package/dist/tools/verify.d.ts +28 -0
- package/dist/tools/verify.d.ts.map +1 -1
- package/dist/tools/verify.js +159 -7
- package/dist/tools/verify.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -379,9 +379,12 @@ function rememberInitializedDir(dir) {
|
|
|
379
379
|
function withInitHint(content, dir) {
|
|
380
380
|
// Order matters:
|
|
381
381
|
// 1. Update notice (most actionable signal — CRITICAL stays at top).
|
|
382
|
-
// 2.
|
|
383
|
-
//
|
|
384
|
-
//
|
|
382
|
+
// 2. 0.1.51 H2 staleness directive (when source files are newer
|
|
383
|
+
// than the last verify — equally important to the update
|
|
384
|
+
// notice because both keep the agent loop honest).
|
|
385
|
+
// 3. Init hint (only when project is not initialized).
|
|
386
|
+
// 4. The original content.
|
|
387
|
+
// 5. Version banner footer (so the agent can always see what
|
|
385
388
|
// version it's talking to — survives across all responses).
|
|
386
389
|
const banner = buildVersionBanner();
|
|
387
390
|
const withUpdate = withUpdateNotice(content);
|
|
@@ -409,11 +412,54 @@ function withInitHint(content, dir) {
|
|
|
409
412
|
if (!anyInitialized) {
|
|
410
413
|
head.push({ type: "text", text: INIT_HINT });
|
|
411
414
|
}
|
|
415
|
+
// 0.1.51 H2 — verify-staleness directive. We only check the FIRST
|
|
416
|
+
// initialized candidate dir (so we don't double-fire when multiple
|
|
417
|
+
// candidates resolve, and so the cost stays O(1) per response).
|
|
418
|
+
// Errors are swallowed because the staleness check must never
|
|
419
|
+
// fail-close on a tool response.
|
|
420
|
+
try {
|
|
421
|
+
const stalenessDir = candidates.find((d) => isProjectInitialized(d) || wasInitialisedAtPath(d));
|
|
422
|
+
if (stalenessDir && !skipStalenessForCwd(stalenessDir)) {
|
|
423
|
+
// Lazy-load so we don't pay the cost on tool responses that
|
|
424
|
+
// fire before any artifacts exist.
|
|
425
|
+
const { checkVerifyStaleness, buildStalenessDirective } =
|
|
426
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
427
|
+
require("./evidence/verify_staleness.js");
|
|
428
|
+
const r = checkVerifyStaleness(stalenessDir);
|
|
429
|
+
const directive = buildStalenessDirective(r);
|
|
430
|
+
if (directive) {
|
|
431
|
+
head.push({ type: "text", text: directive });
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
catch { /* never fail tool responses on a staleness probe */ }
|
|
412
436
|
const result = [...head, ...withUpdate];
|
|
413
437
|
if (banner)
|
|
414
438
|
result.push(banner);
|
|
415
439
|
return result;
|
|
416
440
|
}
|
|
441
|
+
// 0.1.51 H2 — suppress the staleness directive when the response is
|
|
442
|
+
// FROM `codeloop_verify` itself. Otherwise verify would always announce
|
|
443
|
+
// "your code is stale" because, by definition, source has been
|
|
444
|
+
// modified since the last (older) verify. We can't see the tool name
|
|
445
|
+
// directly inside `withInitHint`, so we use a per-cwd cooldown:
|
|
446
|
+
// `codeloop_verify` calls `markVerifiedNow(cwd)` and we skip the
|
|
447
|
+
// staleness probe for 30s on that cwd.
|
|
448
|
+
const RECENT_VERIFY_GRACE_MS = 30_000;
|
|
449
|
+
const recentlyVerified = new Map();
|
|
450
|
+
function skipStalenessForCwd(cwd) {
|
|
451
|
+
const ts = recentlyVerified.get(cwd);
|
|
452
|
+
if (!ts)
|
|
453
|
+
return false;
|
|
454
|
+
if (Date.now() - ts > RECENT_VERIFY_GRACE_MS) {
|
|
455
|
+
recentlyVerified.delete(cwd);
|
|
456
|
+
return false;
|
|
457
|
+
}
|
|
458
|
+
return true;
|
|
459
|
+
}
|
|
460
|
+
export function markVerifiedNow(cwd) {
|
|
461
|
+
recentlyVerified.set(cwd, Date.now());
|
|
462
|
+
}
|
|
417
463
|
const TOOL_BOOTSTRAP = "[CodeLoop] You MUST call codeloop_verify after every code change. If .codeloop/config.json is missing, call codeloop_init_project FIRST.\n\n";
|
|
418
464
|
/**
|
|
419
465
|
* Zod preprocessor for `target_type` that accepts common synonyms.
|
|
@@ -491,6 +537,11 @@ Returns: structured report with pass/fail counts, artifact paths, and next-step
|
|
|
491
537
|
// We inspect the produced run for video / interaction log evidence
|
|
492
538
|
// and, when missing on a UI project, append a non-ambiguous next-
|
|
493
539
|
// step directive so even a less-disciplined agent stays in the loop.
|
|
540
|
+
// 0.1.51 H2 — mark this cwd as freshly verified so the
|
|
541
|
+
// staleness directive in withInitHint doesn't fire on the
|
|
542
|
+
// verify response itself (the tool that just RAN verify is
|
|
543
|
+
// exactly the wrong place to scold "your code is stale").
|
|
544
|
+
markVerifiedNow(cwd);
|
|
494
545
|
let postscript = "";
|
|
495
546
|
try {
|
|
496
547
|
const { isUIProject } = await import("./tools/gate_check.js");
|
|
@@ -778,7 +829,12 @@ Returns: deterministic diff results + screenshot images for visual analysis.`, {
|
|
|
778
829
|
content.push({ type: "text", text: prompt });
|
|
779
830
|
content.push(...imageBlocks);
|
|
780
831
|
}
|
|
781
|
-
|
|
832
|
+
// 0.1.51 H6 — wrap response in withInitHint so the init-hint /
|
|
833
|
+
// version footer / critical-floor nag fires on visual_review too.
|
|
834
|
+
// Pre-H6 only verify / gate_check carried these so an agent that
|
|
835
|
+
// jumped straight to visual_review on a fresh workspace would
|
|
836
|
+
// miss the init-hint and skip codeloop_init_project.
|
|
837
|
+
return { content: withInitHint(content, resolveCwd(params)) };
|
|
782
838
|
});
|
|
783
839
|
server.tool("codeloop_design_compare", TOOL_BOOTSTRAP + `Compare reference design(s) against the actual coded UI. Use this tool when:
|
|
784
840
|
- The user has provided a Figma mockup, screenshot, or design reference (any image in designs/ or .codeloop/figma.json)
|
|
@@ -887,7 +943,11 @@ Returns: per-screen pixel diff scores + worst-failing reference, actual, and dif
|
|
|
887
943
|
if (block.diff)
|
|
888
944
|
content.push({ type: "image", data: block.diff.data, mimeType: block.diff.mime });
|
|
889
945
|
}
|
|
890
|
-
|
|
946
|
+
// 0.1.51 H6 — withInitHint on design_compare too. The
|
|
947
|
+
// design_compare_evidence gate already blocks gate_check until
|
|
948
|
+
// every reference matches; the init-hint guarantees fresh
|
|
949
|
+
// workspaces don't sneak past codeloop_init_project.
|
|
950
|
+
return { content: withInitHint(content, resolveCwd(params)) };
|
|
891
951
|
});
|
|
892
952
|
server.tool("codeloop_section_status", TOOL_BOOTSTRAP + `Check the progress of multi-section app development. Use this tool when:
|
|
893
953
|
- A master spec exists and you need to know which section to work on next
|
|
@@ -1196,7 +1256,10 @@ Try in this order:
|
|
|
1196
1256
|
Verify with: \`ffmpeg -version\`
|
|
1197
1257
|
Then re-run this tool to analyze the video at: ${result.video_analyzed}` });
|
|
1198
1258
|
}
|
|
1199
|
-
|
|
1259
|
+
// 0.1.51 H6 — even on the ffmpeg-missing path, the response should
|
|
1260
|
+
// carry the init-hint / version footer so a fresh workspace is
|
|
1261
|
+
// never silently uninitialised.
|
|
1262
|
+
return { content: withInitHint(content, resolveCwd(params)) };
|
|
1200
1263
|
}
|
|
1201
1264
|
const imageBlocks = [];
|
|
1202
1265
|
for (const framePath of result.framePaths) {
|
|
@@ -1233,7 +1296,9 @@ Report as JSON: { "flow_completed": boolean, "completion_score": 0.0-1.0, "steps
|
|
|
1233
1296
|
else {
|
|
1234
1297
|
content.push({ type: "text", text: JSON.stringify({ error: true, message: "No frames could be extracted from the video.", video_analyzed: result.video_analyzed }, null, 2) });
|
|
1235
1298
|
}
|
|
1236
|
-
|
|
1299
|
+
// 0.1.51 H6 — wrap in withInitHint for the same reasons as
|
|
1300
|
+
// visual_review / design_compare above.
|
|
1301
|
+
return { content: withInitHint(content, resolveCwd(params)) };
|
|
1237
1302
|
});
|
|
1238
1303
|
server.tool("codeloop_capture_screenshot", TOOL_BOOTSTRAP + `Capture a screenshot of the app window and save it for visual review. Use this tool when:
|
|
1239
1304
|
- You want to capture a specific page/screen of the app for visual analysis
|
|
@@ -1355,6 +1420,91 @@ Returns: list of discovered screens with routes, navigation triggers, confidence
|
|
|
1355
1420
|
content: withInitHint([{ type: "text", text: JSON.stringify(result, null, 2) }]),
|
|
1356
1421
|
};
|
|
1357
1422
|
});
|
|
1423
|
+
server.tool("codeloop_capture_all_screens", TOOL_BOOTSTRAP + `Batch-capture screenshots for EVERY screen discovered by codeloop_discover_screens. Use this tool when:
|
|
1424
|
+
- You want full visual coverage in a single call instead of looping codeloop_capture_screenshot manually for each route
|
|
1425
|
+
- The agent loop has been told "capture screenshots for every page" and you want zero ambiguity about how many it actually captured
|
|
1426
|
+
- You're about to call codeloop_design_compare or codeloop_visual_review and need the freshest set of actuals
|
|
1427
|
+
|
|
1428
|
+
What it does:
|
|
1429
|
+
1. Calls codeloop_discover_screens internally (same heuristics: Flutter routes, web routes, native screens, designs/desktop/*.png).
|
|
1430
|
+
2. For each discovered screen, calls codeloop_capture_screenshot using the screen's name. Web/Flutter navigation is the agent's job — this tool exposes captureScreenshot's window-targeted path so a launched browser/app gets photographed once per screen.
|
|
1431
|
+
3. Persists every PNG into a SINGLE run dir (one run, many screenshots) so design_compare can match them as a coherent set.
|
|
1432
|
+
|
|
1433
|
+
Returns: list of { screen_name, path, captured, error? } per screen + the shared run_id.`, {
|
|
1434
|
+
app_name: z.string().optional().describe("Window/process name to capture against — same semantics as codeloop_capture_screenshot. Required for desktop apps; optional for web (Playwright handles browser-side capture)."),
|
|
1435
|
+
platform: z.enum(["flutter", "web", "mobile", "xcode", "android", "dotnet", "auto"]).default("auto"),
|
|
1436
|
+
run_id: z.string().optional().describe("Optional explicit run_id to write screenshots into. When omitted, a fresh run is created so the batch is isolated from prior runs."),
|
|
1437
|
+
project_dir: z.string().optional().describe("Absolute path to the project root. See codeloop_capture_screenshot for the same semantics."),
|
|
1438
|
+
workspace_root: z.string().optional().describe("[Alias for project_dir] Same semantics."),
|
|
1439
|
+
}, async (params) => {
|
|
1440
|
+
const authResult = await withAuth(async () => {
|
|
1441
|
+
const { captureScreenshot } = await import("./runners/screenshot.js");
|
|
1442
|
+
const { discoverScreens } = await import("./tools/discover_screens.js");
|
|
1443
|
+
const { createRunDir, getRunDir, getArtifactsBaseDir } = await import("./evidence/artifacts.js");
|
|
1444
|
+
const { isDesktopAppProject } = await import("./tools/desktop_app_mode.js");
|
|
1445
|
+
const { loadConfig } = await import("./config.js");
|
|
1446
|
+
const cwd = resolveCwd(params);
|
|
1447
|
+
// 1. Discover the screens. discoverScreens already returns
|
|
1448
|
+
// deduped, named items; we don't need to filter further.
|
|
1449
|
+
const discovered = await discoverScreens(cwd, params.platform);
|
|
1450
|
+
// 2. Pin every capture into the SAME run dir so a follow-up
|
|
1451
|
+
// design_compare / visual_review picks them up as one set.
|
|
1452
|
+
let screenshotsDir;
|
|
1453
|
+
let runId;
|
|
1454
|
+
if (params.run_id) {
|
|
1455
|
+
runId = params.run_id;
|
|
1456
|
+
const base = getArtifactsBaseDir(cwd);
|
|
1457
|
+
screenshotsDir = join(getRunDir(runId, base), "screenshots");
|
|
1458
|
+
}
|
|
1459
|
+
else {
|
|
1460
|
+
const created = createRunDir(undefined, join(cwd, "artifacts", "runs"));
|
|
1461
|
+
runId = created.runId;
|
|
1462
|
+
screenshotsDir = join(created.runDir, "screenshots");
|
|
1463
|
+
}
|
|
1464
|
+
const desktopApp = isDesktopAppProject(cwd);
|
|
1465
|
+
const cfg = loadConfig(cwd);
|
|
1466
|
+
const targetApp = params.app_name ?? cfg.evidence?.target_app;
|
|
1467
|
+
const screensList = discovered.screens ?? [];
|
|
1468
|
+
const captures = [];
|
|
1469
|
+
for (const screen of screensList) {
|
|
1470
|
+
const name = screen.screen_name || screen.name || screen.route || "screen";
|
|
1471
|
+
const safe = String(name).replace(/[^a-zA-Z0-9_.-]/g, "_").slice(0, 80);
|
|
1472
|
+
try {
|
|
1473
|
+
const r = await captureScreenshot(screenshotsDir, safe, targetApp, undefined, { desktopAppMode: desktopApp });
|
|
1474
|
+
captures.push({
|
|
1475
|
+
screen_name: safe,
|
|
1476
|
+
captured: r.captured,
|
|
1477
|
+
path: r.paths?.[0],
|
|
1478
|
+
method: r.method,
|
|
1479
|
+
error: r.error,
|
|
1480
|
+
});
|
|
1481
|
+
}
|
|
1482
|
+
catch (err) {
|
|
1483
|
+
captures.push({
|
|
1484
|
+
screen_name: safe,
|
|
1485
|
+
captured: false,
|
|
1486
|
+
error: err.message,
|
|
1487
|
+
});
|
|
1488
|
+
}
|
|
1489
|
+
}
|
|
1490
|
+
await trackUsage(apiKey, "visual_review");
|
|
1491
|
+
return {
|
|
1492
|
+
run_id: runId,
|
|
1493
|
+
total_discovered: screensList.length,
|
|
1494
|
+
captured_count: captures.filter((c) => c.captured).length,
|
|
1495
|
+
failed_count: captures.filter((c) => !c.captured).length,
|
|
1496
|
+
captures,
|
|
1497
|
+
};
|
|
1498
|
+
}, { tool: "codeloop_capture_all_screens", cwd: resolveCwd(params), input: params });
|
|
1499
|
+
if (typeof authResult === "object" && authResult !== null && "error" in authResult) {
|
|
1500
|
+
return {
|
|
1501
|
+
content: withInitHint([{ type: "text", text: JSON.stringify(authResult, null, 2) }], resolveCwd(params)),
|
|
1502
|
+
};
|
|
1503
|
+
}
|
|
1504
|
+
return {
|
|
1505
|
+
content: withInitHint([{ type: "text", text: JSON.stringify(authResult, null, 2) }], resolveCwd(params)),
|
|
1506
|
+
};
|
|
1507
|
+
});
|
|
1358
1508
|
server.tool("codeloop_discover_interactions", TOOL_BOOTSTRAP + `Scan the project source code to discover all INTERACTIVE ELEMENTS: input fields,
|
|
1359
1509
|
buttons (with submit/save hints), toggles, selects, datagrids, file-upload zones, AI features.
|
|
1360
1510
|
This is the companion to codeloop_discover_screens — where discover_screens enumerates routes,
|
|
@@ -1863,7 +2013,9 @@ The agent MUST then write the report to docs/DEVELOPMENT_LOG.md and present it t
|
|
|
1863
2013
|
return report;
|
|
1864
2014
|
}, { tool: "codeloop_generate_dev_report", cwd: resolveCwd(params), input: params });
|
|
1865
2015
|
if (typeof result === "object" && result !== null && "error" in result) {
|
|
1866
|
-
return {
|
|
2016
|
+
return {
|
|
2017
|
+
content: withInitHint([{ type: "text", text: JSON.stringify(result, null, 2) }], resolveCwd(params)),
|
|
2018
|
+
};
|
|
1867
2019
|
}
|
|
1868
2020
|
const report = result;
|
|
1869
2021
|
const content = [];
|
|
@@ -1959,7 +2111,12 @@ Emphasize how CodeLoop added value throughout the development process:
|
|
|
1959
2111
|
- Make it clear this is an AI-agent-automated quality process powered by CodeLoop
|
|
1960
2112
|
|
|
1961
2113
|
Write the report now and save it to \`docs/DEVELOPMENT_LOG.md\`.` });
|
|
1962
|
-
|
|
2114
|
+
// 0.1.51 H6 — wrap in withInitHint so the version footer / init
|
|
2115
|
+
// hint / critical-floor nag fires on the dev report too. The
|
|
2116
|
+
// dev report is the FINAL deliverable of every CodeLoop session,
|
|
2117
|
+
// so this is the most important place to surface "you're on a
|
|
2118
|
+
// critical-floor-blocked version, please update".
|
|
2119
|
+
return { content: withInitHint(content, resolveCwd(params)) };
|
|
1963
2120
|
});
|
|
1964
2121
|
server.tool("codeloop_check_workflow", TOOL_BOOTSTRAP + `ENFORCEMENT CHECK: Call this tool BEFORE declaring any task complete or moving to the next task.
|
|
1965
2122
|
It checks whether all required CodeLoop verification steps have been performed for the current project.
|
|
@@ -1982,15 +2139,16 @@ Returns: checklist of completed and pending verification steps.`, {
|
|
|
1982
2139
|
const { existsSync, readdirSync } = await import("fs");
|
|
1983
2140
|
const { listRuns, loadRunMeta, getArtifactsBaseDir, getRunDir } = await import("./evidence/artifacts.js");
|
|
1984
2141
|
const { detectPlatform } = await import("./tools/verify.js");
|
|
1985
|
-
|
|
2142
|
+
// 0.1.51 H4 — single source of truth for "is this a UI project".
|
|
2143
|
+
// Previously `check_workflow` used a narrower inline classifier that
|
|
2144
|
+
// didn't include the node-platform UI cases (Electron / Tauri /
|
|
2145
|
+
// React Native), so those projects showed screenshot / video as
|
|
2146
|
+
// n/a in the workflow tracker even though `gate_check` blocked them
|
|
2147
|
+
// on those very gates. Now both call the same helper.
|
|
2148
|
+
const { isUIProject: isUIProjectShared } = await import("./tools/is_ui_project.js");
|
|
1986
2149
|
const cwd = resolveCwd(params);
|
|
1987
2150
|
const platform = detectPlatform(cwd);
|
|
1988
|
-
|
|
1989
|
-
// Avalonia, WinUI, UWP. Without this, every WPF/.NET 8 / MAUI / Avalonia
|
|
1990
|
-
// project silently bypassed screenshot/video/replay gates and shipped
|
|
1991
|
-
// a green 100% gate with zero visual evidence.
|
|
1992
|
-
const isUIProject = ["flutter", "web", "xcode", "android"].includes(platform) ||
|
|
1993
|
-
(platform === "dotnet" && detectDesktopUI(cwd).is_desktop_ui);
|
|
2151
|
+
const isUIProject = isUIProjectShared(cwd);
|
|
1994
2152
|
const baseDir = getArtifactsBaseDir(cwd);
|
|
1995
2153
|
const runs = listRuns(baseDir);
|
|
1996
2154
|
// listRuns() returns newest-first (sorted then reversed in artifacts.ts).
|
|
@@ -3180,8 +3338,85 @@ Wait 1-2 seconds between interactions so video frames capture state changes.`, {
|
|
|
3180
3338
|
catch { /* best-effort logging */ }
|
|
3181
3339
|
return { success, action, detail };
|
|
3182
3340
|
}, { tool: "codeloop_interact", cwd: resolveCwd(params), input: params });
|
|
3341
|
+
// 0.1.51 H11 — Post-interact modal-awareness directive.
|
|
3342
|
+
// After every codeloop_interact call we append a HARD reminder
|
|
3343
|
+
// that an interaction MAY have produced a modal (Save…?, Confirm
|
|
3344
|
+
// delete, validation errors, "License agreement", browser
|
|
3345
|
+
// beforeunload, etc). Pre-H11 the agent would happily move on to
|
|
3346
|
+
// the next interaction and the modal would block subsequent
|
|
3347
|
+
// typing / clicking — and the user_journey gate would later fail
|
|
3348
|
+
// because half the journey didn't happen. The directive blocks
|
|
3349
|
+
// that path.
|
|
3350
|
+
const postscript = "\n\n[CodeLoop H11] After this interaction, a modal/dialog/overlay MAY have appeared (Save? / Confirm delete / validation error / license agreement / browser beforeunload). " +
|
|
3351
|
+
"BEFORE the next codeloop_interact call you MUST: (1) take a fresh codeloop_capture_screenshot, " +
|
|
3352
|
+
"(2) inspect the screenshot for any popup, dialog, sheet, alert, or full-screen overlay, " +
|
|
3353
|
+
"(3) if one is present call codeloop_handle_modal with the appropriate `decision` " +
|
|
3354
|
+
"(\"confirm\" to proceed / \"cancel\" to abort / \"dismiss\" to close), and " +
|
|
3355
|
+
"(4) only then continue the planned journey. " +
|
|
3356
|
+
"Do NOT skip modals \"to keep moving\" — an unhandled modal will block every subsequent click and the user_journey_evidence gate will block ready_for_review.";
|
|
3183
3357
|
return {
|
|
3184
|
-
content: withInitHint([
|
|
3358
|
+
content: withInitHint([
|
|
3359
|
+
{ type: "text", text: JSON.stringify(result, null, 2) + postscript },
|
|
3360
|
+
]),
|
|
3361
|
+
};
|
|
3362
|
+
});
|
|
3363
|
+
// 0.1.51 H11 — codeloop_handle_modal
|
|
3364
|
+
server.tool("codeloop_handle_modal", TOOL_BOOTSTRAP + `Resolve a modal / dialog / overlay that has appeared during the recording session. Use this tool when:
|
|
3365
|
+
- A previous codeloop_interact produced a confirmation prompt (Save? / Confirm delete / "Are you sure?")
|
|
3366
|
+
- The app shows a license / EULA / first-run dialog you have to dismiss before continuing
|
|
3367
|
+
- A validation error toast or modal blocks subsequent interactions
|
|
3368
|
+
- The browser fires a beforeunload / "Leave site?" prompt during navigation
|
|
3369
|
+
- Any time the post-interact H11 directive nudged you to look for a modal
|
|
3370
|
+
|
|
3371
|
+
What it does:
|
|
3372
|
+
1. Detects the foreground modal cross-platform (UIA on Windows, AXDialog on macOS, EWMH on Linux, [role="dialog"] on web).
|
|
3373
|
+
2. Applies your chosen decision: "confirm" / "cancel" / "dismiss" / "inspect".
|
|
3374
|
+
3. Logs the decision into the recording's interaction_log.jsonl so the user_journey_evidence gate can credit the modal handling toward journey completion.
|
|
3375
|
+
|
|
3376
|
+
Returns: detected modal description + result of the chosen decision.`, {
|
|
3377
|
+
decision: z.enum(["confirm", "cancel", "dismiss", "inspect"]).default("inspect").describe("Action to take on the detected modal. `confirm` = click the primary/Save/OK button. `cancel` = click Cancel/No. `dismiss` = press Escape (best for transient toasts). `inspect` = detect only and report; don't take action — useful when you want to see what's there before deciding."),
|
|
3378
|
+
target_type: targetTypeSchema.optional(),
|
|
3379
|
+
app_name: z.string().optional(),
|
|
3380
|
+
project_dir: z.string().optional(),
|
|
3381
|
+
workspace_root: z.string().optional(),
|
|
3382
|
+
}, async (params) => {
|
|
3383
|
+
const authResult = await withAuth(async () => {
|
|
3384
|
+
const { detectModal } = await import("./runners/modal_detector.js");
|
|
3385
|
+
const cwd = resolveCwd(params);
|
|
3386
|
+
const detection = await detectModal({
|
|
3387
|
+
target_type: params.target_type,
|
|
3388
|
+
app_name: params.app_name,
|
|
3389
|
+
cwd,
|
|
3390
|
+
config,
|
|
3391
|
+
});
|
|
3392
|
+
// The "inspect" decision short-circuits — we just report what
|
|
3393
|
+
// the detector found.
|
|
3394
|
+
if (params.decision === "inspect" || !detection.is_modal_present) {
|
|
3395
|
+
return {
|
|
3396
|
+
decision_taken: "inspect",
|
|
3397
|
+
detection,
|
|
3398
|
+
note: !detection.is_modal_present && params.decision !== "inspect"
|
|
3399
|
+
? "No modal detected. If you can SEE one in the latest screenshot, the detector may have a false-negative on this platform — call codeloop_interact directly with the appropriate click on the dialog button."
|
|
3400
|
+
: undefined,
|
|
3401
|
+
};
|
|
3402
|
+
}
|
|
3403
|
+
// For confirm / cancel / dismiss we delegate to codeloop_interact
|
|
3404
|
+
// semantics by issuing a key press that maps to the right OS
|
|
3405
|
+
// convention. dismiss ⇒ Escape, cancel ⇒ Escape (most modals
|
|
3406
|
+
// treat Esc as Cancel), confirm ⇒ Enter (primary action).
|
|
3407
|
+
// Browser overlays sometimes ignore key presses — the agent
|
|
3408
|
+
// can fall back to a click via codeloop_interact targeting
|
|
3409
|
+
// the modal's button.
|
|
3410
|
+
const key = params.decision === "confirm" ? "enter" : "escape";
|
|
3411
|
+
return {
|
|
3412
|
+
decision_taken: params.decision,
|
|
3413
|
+
detection,
|
|
3414
|
+
next_step: `Issue codeloop_interact with action="keystroke", key="${key}" against the same target_type to dispatch the modal. ` +
|
|
3415
|
+
`If the modal swallows the key (some web overlays do), follow up with action="click" against the visible button text or selector.`,
|
|
3416
|
+
};
|
|
3417
|
+
}, { tool: "codeloop_handle_modal", cwd: resolveCwd(params), input: params });
|
|
3418
|
+
return {
|
|
3419
|
+
content: withInitHint([{ type: "text", text: JSON.stringify(authResult, null, 2) }], resolveCwd(params)),
|
|
3185
3420
|
};
|
|
3186
3421
|
});
|
|
3187
3422
|
// ── codeloop_init_project ────────────────────────────────────────
|