codex-plus-patcher 0.7.1 → 0.8.0

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.
@@ -40,6 +40,7 @@ function parseArgs(argv) {
40
40
  includeNativeOpenProbes: false,
41
41
  noProgress: false,
42
42
  quiet: false,
43
+ devInstanceId: "audit",
43
44
  };
44
45
  for (let index = 0; index < argv.length; index += 1) {
45
46
  const arg = argv[index];
@@ -53,6 +54,7 @@ function parseArgs(argv) {
53
54
  else if (arg === "--source-home") args.sourceHome = path.resolve(expandPath(next()));
54
55
  else if (arg === "--dev-home") args.devHome = path.resolve(expandPath(next()));
55
56
  else if (arg === "--electron-user-data") args.electronUserDataPath = path.resolve(expandPath(next()));
57
+ else if (arg === "--dev-instance-id") args.devInstanceId = next();
56
58
  else if (arg === "--remote-debugging-port" || arg === "--port") args.remoteDebuggingPort = Number(next());
57
59
  else if (arg === "--no-apply") args.apply = false;
58
60
  else if (arg === "--no-launch") args.launch = false;
@@ -168,6 +170,152 @@ async function findRendererTargetOnPort(port) {
168
170
  }
169
171
  }
170
172
 
173
+ async function waitForMermaidViewerTarget(port, beforeIds = new Set(), timeoutMs = 10000) {
174
+ const deadline = Date.now() + timeoutMs;
175
+ let lastError = null;
176
+ while (Date.now() < deadline) {
177
+ try {
178
+ const targets = await getJson(`http://127.0.0.1:${port}/json/list`);
179
+ const target = targets.find((entry) =>
180
+ !beforeIds.has(entry.id) &&
181
+ entry.url?.startsWith("file://") &&
182
+ entry.url.includes("codex-plus-mermaid-"));
183
+ if (target) return target;
184
+ } catch (error) {
185
+ lastError = error;
186
+ }
187
+ await delay(250);
188
+ }
189
+ throw new Error(`Timed out waiting for Mermaid viewer target on port ${port}${lastError ? `: ${lastError.message}` : ""}`);
190
+ }
191
+
192
+ async function verifyMermaidViewerRender(appCdp, port, { Session = CdpSession, timeoutMs = 15000 } = {}) {
193
+ const beforeTargets = await getJson(`http://127.0.0.1:${port}/json/list`).catch(() => []);
194
+ const beforeIds = new Set(beforeTargets.map((target) => target.id));
195
+ await appCdp.evaluate(`(() => {
196
+ const host = document.createElement("div");
197
+ host.setAttribute("data-markdown-copy", "code-block");
198
+ const pre = document.createElement("pre");
199
+ pre.className = "sr-only";
200
+ pre.textContent = "graph TD;A-->B";
201
+ const diagram = document.createElement("div");
202
+ diagram.setAttribute("data-codex-plus-mermaid-diagram", "");
203
+ host.append(pre, diagram);
204
+ document.body.appendChild(host);
205
+ window.CodexPlus.plugins.get("mermaidFullscreen").exports.openViewer(diagram);
206
+ setTimeout(() => host.remove(), 1000);
207
+ return true;
208
+ })()`);
209
+ const viewerTarget = await waitForMermaidViewerTarget(port, beforeIds, timeoutMs);
210
+ const viewer = new Session(viewerTarget.webSocketDebuggerUrl);
211
+ try {
212
+ await viewer.connect();
213
+ await viewer.send("Runtime.enable");
214
+ const deadline = Date.now() + timeoutMs;
215
+ let status = null;
216
+ while (Date.now() < deadline) {
217
+ status = await viewer.evaluate(`(() => {
218
+ const svg = document.querySelector("#stage svg");
219
+ const status = document.getElementById("render-status")?.textContent || "";
220
+ return {
221
+ hasSvg: Boolean(svg && svg.outerHTML.length > 1000),
222
+ status,
223
+ statusHidden: document.getElementById("render-status")?.hidden ?? null,
224
+ svgLength: svg?.outerHTML?.length || 0,
225
+ bodyText: document.body?.innerText?.slice(0, 500) || "",
226
+ };
227
+ })()`);
228
+ if (status.hasSvg && !/Mermaid render failed:/i.test(status.status)) {
229
+ return {
230
+ ok: true,
231
+ url: viewerTarget.url,
232
+ status: status.status,
233
+ svgLength: status.svgLength,
234
+ };
235
+ }
236
+ if (/Mermaid render failed:/i.test(status.status) || /Mermaid render failed:/i.test(status.bodyText)) break;
237
+ await delay(250);
238
+ }
239
+ return {
240
+ ok: false,
241
+ url: viewerTarget.url,
242
+ message: status?.status || "Mermaid viewer did not render an SVG",
243
+ status,
244
+ };
245
+ } finally {
246
+ try {
247
+ await viewer.send("Page.close");
248
+ } catch {
249
+ // The viewer may already be closed.
250
+ }
251
+ await viewer.close();
252
+ }
253
+ }
254
+
255
+ async function verifyProjectSelectorShortcutKey(cdp, { wait = delay, timeoutMs = 5000 } = {}) {
256
+ const setup = await cdp.evaluate(`new Promise((resolve) => {
257
+ document.dispatchEvent(new KeyboardEvent("keydown", { bubbles: true, cancelable: true, key: "Escape" }));
258
+ const newChatButton = Array.from(document.querySelectorAll("button")).find((button) => {
259
+ const rect = button.getBoundingClientRect();
260
+ return rect.width > 0 && rect.height > 0 && (button.innerText || "").includes("New chat");
261
+ });
262
+ newChatButton?.click?.();
263
+ let attempts = 0;
264
+ const check = () => {
265
+ const triggerCount = document.querySelectorAll("[data-codex-plus-project-selector-trigger]").length;
266
+ if (triggerCount > 0 || attempts >= 30) {
267
+ resolve({ triggerCount, clickedNewChat: Boolean(newChatButton) });
268
+ return;
269
+ }
270
+ attempts += 1;
271
+ setTimeout(check, 100);
272
+ };
273
+ check();
274
+ })`);
275
+ if (!setup?.triggerCount) {
276
+ return { ok: false, ...setup, message: "Project selector shortcut trigger marker is missing from the main composer" };
277
+ }
278
+
279
+ await cdp.send("Input.dispatchKeyEvent", {
280
+ type: "keyDown",
281
+ key: ".",
282
+ code: "Period",
283
+ windowsVirtualKeyCode: 190,
284
+ nativeVirtualKeyCode: 47,
285
+ modifiers: 4,
286
+ });
287
+ await cdp.send("Input.dispatchKeyEvent", {
288
+ type: "keyUp",
289
+ key: ".",
290
+ code: "Period",
291
+ windowsVirtualKeyCode: 190,
292
+ nativeVirtualKeyCode: 47,
293
+ modifiers: 4,
294
+ });
295
+
296
+ const deadline = Date.now() + timeoutMs;
297
+ let status = null;
298
+ while (Date.now() < deadline) {
299
+ status = await cdp.evaluate(`(() => {
300
+ const searchInput = document.querySelector("input[placeholder='Search projects']");
301
+ const menuCount = document.querySelectorAll("[data-radix-menu-content], [data-radix-popper-content-wrapper], [role='menu']").length;
302
+ return {
303
+ triggerCount: document.querySelectorAll("[data-codex-plus-project-selector-trigger]").length,
304
+ menuCount,
305
+ opened: Boolean(searchInput || menuCount > 0),
306
+ activePlaceholder: document.activeElement?.getAttribute?.("placeholder") ?? "",
307
+ };
308
+ })()`);
309
+ if (status.opened) {
310
+ await cdp.send("Input.dispatchKeyEvent", { type: "keyDown", key: "Escape", code: "Escape", windowsVirtualKeyCode: 27, nativeVirtualKeyCode: 53 });
311
+ await cdp.send("Input.dispatchKeyEvent", { type: "keyUp", key: "Escape", code: "Escape", windowsVirtualKeyCode: 27, nativeVirtualKeyCode: 53 });
312
+ return { ok: true, ...setup, ...status };
313
+ }
314
+ await wait(100);
315
+ }
316
+ return { ok: false, ...setup, ...status, message: `Cmd+. did not open the project selector: ${JSON.stringify(status)}` };
317
+ }
318
+
171
319
  function listRunningAuditApps({
172
320
  targetApp = DEFAULT_TARGET,
173
321
  electronUserDataPath = DEFAULT_ELECTRON_USER_DATA,
@@ -361,16 +509,21 @@ async function waitForAppShellMounted(cdp, timeoutMs = 90000) {
361
509
  const root = document.getElementById("root");
362
510
  const bodyText = document.body?.innerText?.trim() ?? "";
363
511
  const interactiveCount = document.querySelectorAll("button,a,nav,[role=navigation]").length;
512
+ const hasErrorBoundary = /^Oops, an error has occurred\\b/.test(bodyText);
364
513
  return {
365
514
  readyState: document.readyState,
366
515
  hasRoot: Boolean(root),
367
516
  hasStartupLoader: Boolean(document.querySelector("#root .startup-loader")),
517
+ hasErrorBoundary,
368
518
  bodyTextLength: bodyText.length,
369
519
  elementCount: document.querySelectorAll("*").length,
370
520
  interactiveCount,
371
521
  sampleText: bodyText.slice(0, 120),
372
522
  };
373
523
  })()`);
524
+ if (lastStatus.hasErrorBoundary) {
525
+ throw new Error(`Codex app shell rendered error boundary: ${JSON.stringify(lastStatus)}`);
526
+ }
374
527
  if (
375
528
  lastStatus.readyState === "complete" &&
376
529
  lastStatus.hasRoot &&
@@ -407,10 +560,12 @@ function formatAuditJson(result) {
407
560
  }
408
561
 
409
562
  function formatAuditResult(result, { quiet = false } = {}) {
563
+ const expectedWarnings = result.expectedWarnings || [];
410
564
  if (quiet) {
411
- return result.ok
412
- ? "All plugin probes passed.\n"
413
- : `Plugin audit failed: ${result.failures.length} failures\n`;
565
+ if (!result.ok) return `Plugin audit failed: ${result.failures.length} failures\n`;
566
+ return expectedWarnings.length > 0
567
+ ? "All plugin probes passed with expected warnings.\n"
568
+ : "All plugin probes passed.\n";
414
569
  }
415
570
 
416
571
  if (!result.ok) {
@@ -440,6 +595,13 @@ function formatAuditResult(result, { quiet = false } = {}) {
440
595
  }
441
596
  lines.push("");
442
597
  }
598
+ if (expectedWarnings.length > 0) {
599
+ lines.push("Expected warnings:");
600
+ for (const warning of expectedWarnings) {
601
+ lines.push(` ${warning.plugin || "audit"} ${warning.code || "warning"}: ${warning.message || "expected warning"}`);
602
+ }
603
+ lines.push("");
604
+ }
443
605
  lines.push("Re-run with --json for full probe details.");
444
606
  return `${lines.join("\n").replace(/\n{3,}/g, "\n\n")}\n`;
445
607
  }
@@ -466,11 +628,18 @@ function formatAuditResult(result, { quiet = false } = {}) {
466
628
  `Runtime ready: ${runtime.registered ?? result.registeredPlugins?.length ?? 0} registered, ${runtime.started ?? result.startedPlugins?.length ?? 0} started`,
467
629
  `App shell: ${appShell.hasStartupLoader === false ? "mounted" : "unknown"}`,
468
630
  `Probed ${probeCount} plugins`,
631
+ `Warnings: ${expectedWarnings.length} expected`,
469
632
  `Native open probes: ${result.nativeOpenProbes?.included ? "included" : "skipped"}`,
470
633
  `Cleanup: ${cleanupText}`,
471
634
  "",
472
635
  "All plugin probes passed.",
473
636
  );
637
+ if (expectedWarnings.length > 0) {
638
+ lines.push("", "Expected warnings:");
639
+ for (const warning of expectedWarnings) {
640
+ lines.push(`${warning.plugin || "audit"} ${warning.code || "warning"}: ${warning.message || "expected warning"}`);
641
+ }
642
+ }
474
643
  return `${lines.join("\n")}\n`;
475
644
  }
476
645
 
@@ -661,12 +830,16 @@ function pluginAuditExpression({ includeNativeOpenProbes = false } = {}) {
661
830
  ];
662
831
  const pluginResults = {};
663
832
  const failures = [];
833
+ const expectedWarnings = [];
664
834
  const add = (id, ok, details = {}) => {
665
835
  pluginResults[id] = { ok, ...details };
666
836
  if (!ok) failures.push({ plugin: id, message: details.message || "probe failed", details });
667
837
  };
668
838
  const fail = (id, error, details = {}) => add(id, false, { message: error?.message || String(error), ...details });
669
839
  const pass = (id, details = {}) => add(id, true, details);
840
+ const warn = (id, code, message, details = {}) => {
841
+ expectedWarnings.push({ plugin: id, code, message, details });
842
+ };
670
843
  const pluginIds = () => {
671
844
  if (typeof window.CodexPlus?.plugins?.list !== "function") {
672
845
  throw new Error("CodexPlus.plugins.list is not available");
@@ -721,6 +894,120 @@ function pluginAuditExpression({ includeNativeOpenProbes = false } = {}) {
721
894
  evidence,
722
895
  };
723
896
  };
897
+ const waitForProjectThreadRows = async (timeoutMs = 45000) => {
898
+ const startedAt = Date.now();
899
+ while (Date.now() - startedAt < timeoutMs) {
900
+ const rows = document.querySelectorAll("[data-app-action-sidebar-project-list-id] [data-app-action-sidebar-thread-row]");
901
+ if (rows.length > 0) return rows.length;
902
+ await new Promise((resolve) => setTimeout(resolve, 500));
903
+ }
904
+ return 0;
905
+ };
906
+ const isTransparentColor = (value) => value === "rgba(0, 0, 0, 0)" || value === "transparent";
907
+ const waitForMountedProjectComposer = async (expectedAccent, timeoutMs = 20000) => {
908
+ const startedAt = Date.now();
909
+ while (Date.now() - startedAt < timeoutMs) {
910
+ const editor = document.querySelector("[data-codex-composer]");
911
+ const surface = editor?.closest("[data-codex-plus-user-entry]") || editor?.closest(".composer-surface-chrome");
912
+ if (surface) {
913
+ const computed = getComputedStyle(surface);
914
+ const surfaceAccent = computed.getPropertyValue("--codex-plus-project-accent").trim();
915
+ if (
916
+ surface.hasAttribute("data-codex-plus-user-entry") &&
917
+ surface.hasAttribute("data-codex-plus-project-color") &&
918
+ surfaceAccent === expectedAccent &&
919
+ computed.boxShadow !== "none"
920
+ ) {
921
+ return {
922
+ marked: true,
923
+ projectMarked: true,
924
+ accent: surfaceAccent,
925
+ boxShadow: computed.boxShadow,
926
+ };
927
+ }
928
+ }
929
+ await new Promise((resolve) => setTimeout(resolve, 250));
930
+ }
931
+ const editor = document.querySelector("[data-codex-composer]");
932
+ const surface = editor?.closest("[data-codex-plus-user-entry]") || editor?.closest(".composer-surface-chrome");
933
+ const computed = surface ? getComputedStyle(surface) : null;
934
+ return {
935
+ marked: surface?.hasAttribute("data-codex-plus-user-entry") || false,
936
+ projectMarked: surface?.hasAttribute("data-codex-plus-project-color") || false,
937
+ accent: computed?.getPropertyValue("--codex-plus-project-accent").trim() || "",
938
+ boxShadow: computed?.boxShadow || "",
939
+ };
940
+ };
941
+ const composerPermissionPickerStatus = () => {
942
+ const editor = document.querySelector("[data-codex-composer]");
943
+ const labels = ["Full access", "Ask for approval", "Approve for me", "Custom"];
944
+ const normalize = (value) => String(value || "").replace(/\s+/g, " ").trim();
945
+ const rgb = (value) => {
946
+ const match = String(value || "").match(/rgba?\((\d+),\s*(\d+),\s*(\d+)/);
947
+ return match ? [Number(match[1]), Number(match[2]), Number(match[3])] : null;
948
+ };
949
+ const luminance = (color) => {
950
+ if (!color) return null;
951
+ const channel = (value) => {
952
+ const normalized = value / 255;
953
+ return normalized <= 0.03928 ? normalized / 12.92 : Math.pow((normalized + 0.055) / 1.055, 2.4);
954
+ };
955
+ return 0.2126 * channel(color[0]) + 0.7152 * channel(color[1]) + 0.0722 * channel(color[2]);
956
+ };
957
+ const contrast = (foreground, background) => {
958
+ const fg = luminance(rgb(foreground));
959
+ const bg = luminance(rgb(background));
960
+ if (fg == null || bg == null) return null;
961
+ const lighter = Math.max(fg, bg);
962
+ const darker = Math.min(fg, bg);
963
+ return (lighter + 0.05) / (darker + 0.05);
964
+ };
965
+ const isTransparent = (value) => {
966
+ const text = String(value || "").trim();
967
+ return text === "transparent" || text === "rgba(0, 0, 0, 0)" || /rgba\([^)]*,\s*0\)$/.test(text);
968
+ };
969
+ const trigger = Array.from(document.querySelectorAll("button")).find((button) => {
970
+ const text = normalize(button.textContent);
971
+ return labels.some((label) => text === label || text.startsWith(`${label} `));
972
+ });
973
+ const triggerStyle = trigger ? getComputedStyle(trigger) : null;
974
+ const surface = editor?.closest("[data-codex-plus-user-entry]");
975
+ const surfaceStyle = surface ? getComputedStyle(surface) : null;
976
+ let labelStyle = null;
977
+ if (trigger) {
978
+ const walker = document.createTreeWalker(trigger, NodeFilter.SHOW_TEXT);
979
+ while (walker.nextNode()) {
980
+ const node = walker.currentNode;
981
+ if (normalize(node.nodeValue) !== "") {
982
+ labelStyle = getComputedStyle(node.parentElement);
983
+ break;
984
+ }
985
+ }
986
+ }
987
+ const triggerColor = triggerStyle?.color || null;
988
+ const labelColor = labelStyle?.color || triggerColor;
989
+ const labelTextFillColor = labelStyle?.webkitTextFillColor || triggerStyle?.webkitTextFillColor || null;
990
+ const effectiveLabelColor = labelTextFillColor && !isTransparent(labelTextFillColor) ? labelTextFillColor : labelColor;
991
+ const surfaceBackground = surfaceStyle?.backgroundColor || null;
992
+ return {
993
+ editorMounted: Boolean(editor),
994
+ editorEditable: editor?.getAttribute("contenteditable") === "true",
995
+ editorText: normalize(editor?.textContent),
996
+ triggerMounted: Boolean(trigger),
997
+ triggerText: normalize(trigger?.textContent),
998
+ triggerDisabled: Boolean(trigger?.disabled),
999
+ triggerAriaDisabled: trigger?.getAttribute("aria-disabled") || null,
1000
+ triggerState: trigger?.getAttribute("data-state") || null,
1001
+ triggerOpacity: triggerStyle?.opacity || null,
1002
+ triggerColor,
1003
+ labelColor,
1004
+ labelTextFillColor,
1005
+ surfaceBackground,
1006
+ triggerContrast: contrast(effectiveLabelColor, surfaceBackground),
1007
+ labelTextFillTransparent: isTransparent(labelTextFillColor),
1008
+ triggerClassName: String(trigger?.className || ""),
1009
+ };
1010
+ };
724
1011
  const jsx = (type, props, key) => ({ type, props: props || {}, key });
725
1012
  const jsxs = jsx;
726
1013
  const reviewDeps = {
@@ -835,14 +1122,63 @@ function pluginAuditExpression({ includeNativeOpenProbes = false } = {}) {
835
1122
  props?.style?.["--codex-plus-project-accent"] === accent);
836
1123
  const liveRows = Array.from(document.querySelectorAll("[data-codex-plus-project-color]"));
837
1124
  const liveAccents = liveRows.map((row) => getComputedStyle(row).getPropertyValue("--codex-plus-project-accent").trim()).filter(Boolean);
1125
+ const projectThreadRowCount = await waitForProjectThreadRows();
1126
+ let selectedProjectAccent = "";
1127
+ let mountedComposer = null;
1128
+ const unstyledProjectThreadLists = Array.from(document.querySelectorAll("[data-app-action-sidebar-project-list-id]"))
1129
+ .map((list) => {
1130
+ const projectId = list.getAttribute("data-app-action-sidebar-project-list-id") || "";
1131
+ const projectRow = document.querySelector(`[data-app-action-sidebar-project-row][data-app-action-sidebar-project-id="${CSS.escape(projectId)}"]`);
1132
+ const threadRows = Array.from(list.querySelectorAll("[data-app-action-sidebar-thread-row]"));
1133
+ if (!projectRow || threadRows.length === 0) return null;
1134
+ const projectAccent = getComputedStyle(projectRow).getPropertyValue("--codex-plus-project-accent").trim();
1135
+ const listComputed = getComputedStyle(list);
1136
+ const listAccent = listComputed.getPropertyValue("--codex-plus-project-accent").trim();
1137
+ const listBackground = listComputed.backgroundColor;
1138
+ if (!selectedProjectAccent) {
1139
+ selectedProjectAccent = projectAccent;
1140
+ threadRows[0].click();
1141
+ }
1142
+ const unstyledRows = threadRows.filter((row) => {
1143
+ const computed = getComputedStyle(row);
1144
+ const rowAccent = computed.getPropertyValue("--codex-plus-project-accent").trim();
1145
+ return rowAccent !== projectAccent || isTransparentColor(computed.backgroundColor);
1146
+ });
1147
+ return list.hasAttribute("data-codex-plus-project-sidebar-color") &&
1148
+ listAccent === projectAccent &&
1149
+ !isTransparentColor(listBackground) &&
1150
+ unstyledRows.length === 0
1151
+ ? null
1152
+ : {
1153
+ projectId,
1154
+ projectLabel: projectRow.getAttribute("data-app-action-sidebar-project-label") || projectRow.innerText.trim(),
1155
+ projectAccent,
1156
+ listAccent,
1157
+ listBackground,
1158
+ threadRows: threadRows.length,
1159
+ unstyledRows: unstyledRows.length,
1160
+ listMarked: list.hasAttribute("data-codex-plus-project-sidebar-color"),
1161
+ };
1162
+ })
1163
+ .filter(Boolean);
1164
+ if (selectedProjectAccent) mountedComposer = await waitForMountedProjectComposer(selectedProjectAccent);
838
1165
  if (!accent) throw new Error("Project accent was not computed");
839
1166
  if (!matchingProps) throw new Error("Project, thread, bubble, and composer props do not share an accent");
1167
+ if (projectThreadRowCount === 0) throw new Error("No visible project child thread rows appeared; project sidebar styling was not proven");
1168
+ if (unstyledProjectThreadLists.length > 0) {
1169
+ throw new Error(`Project sidebar child rows or list containers are not styled like their project rows: ${JSON.stringify(unstyledProjectThreadLists.slice(0, 4))}`);
1170
+ }
1171
+ if (!mountedComposer?.marked || !mountedComposer?.projectMarked || mountedComposer?.accent !== selectedProjectAccent) {
1172
+ throw new Error(`Mounted composer does not carry the selected project accent: ${JSON.stringify(mountedComposer)}`);
1173
+ }
840
1174
  pass("projectColors", {
841
1175
  ...details,
842
1176
  accent,
843
1177
  matchingProps,
844
1178
  liveRows: liveRows.length,
845
1179
  liveAccents: Array.from(new Set(liveAccents)).slice(0, 8),
1180
+ styledProjectThreadLists: projectThreadRowCount,
1181
+ mountedComposer,
846
1182
  });
847
1183
  } catch (error) {
848
1184
  fail("projectColors", error);
@@ -860,6 +1196,33 @@ function pluginAuditExpression({ includeNativeOpenProbes = false } = {}) {
860
1196
  fail("projectPathHeader", error);
861
1197
  }
862
1198
 
1199
+ try {
1200
+ const status = composerPermissionPickerStatus();
1201
+ if (status.editorMounted && status.editorEditable && status.triggerMounted) {
1202
+ const lowOpacity = Number(status.triggerOpacity) < 0.5;
1203
+ const lowContrast = status.triggerContrast != null && status.triggerContrast < 4.5;
1204
+ if (lowOpacity || lowContrast || status.labelTextFillTransparent) {
1205
+ throw new Error(`Composer permissions picker text is unreadable: ${JSON.stringify(status)}`);
1206
+ }
1207
+ const ariaDisabled = status.triggerAriaDisabled === "true";
1208
+ const visuallyDisabled = /\bopacity-40\b/.test(status.triggerClassName);
1209
+ if (status.triggerDisabled || ariaDisabled || visuallyDisabled) {
1210
+ warn(
1211
+ "audit",
1212
+ "composer-permission-picker-disabled",
1213
+ "Composer permissions picker is disabled while the composer is editable",
1214
+ status,
1215
+ );
1216
+ }
1217
+ }
1218
+ if (!status.editorMounted || !status.triggerMounted) {
1219
+ throw new Error(`Composer permissions picker was not found: ${JSON.stringify(status)}`);
1220
+ }
1221
+ pass("audit", { composerPermissionPicker: status });
1222
+ } catch (error) {
1223
+ fail("audit", error);
1224
+ }
1225
+
863
1226
  try {
864
1227
  const details = checkCommon("sidebarNameBlur");
865
1228
  const metadata = window.CodexPlus.ui.commands.commandMetadata().some((command) => command.id === "codexPlusToggleSidebarNameBlur");
@@ -938,7 +1301,34 @@ function pluginAuditExpression({ includeNativeOpenProbes = false } = {}) {
938
1301
  if (ranked[0] !== "hassio-dev") throw new Error(`Fuzzy ranking returned ${ranked.join(", ")}`);
939
1302
  if (highlightCount === 0) throw new Error("Fuzzy match highlight did not render");
940
1303
  if (selected[0] !== "hassio-dev" || events.length !== 2) throw new Error("Enter-to-first-result adapter did not select first ranked result");
941
- pass("projectSelectorShortcut", { ...details, ranked, highlightCount, selected });
1304
+ document.dispatchEvent(new KeyboardEvent("keydown", { bubbles: true, cancelable: true, key: "Escape" }));
1305
+ const newChatButton = Array.from(document.querySelectorAll("button")).find((button) => {
1306
+ const rect = button.getBoundingClientRect();
1307
+ return rect.width > 0 && rect.height > 0 && (button.innerText || "").includes("New chat");
1308
+ });
1309
+ newChatButton?.click?.();
1310
+ let triggerCount = 0;
1311
+ for (let attempt = 0; attempt < 20; attempt += 1) {
1312
+ triggerCount = document.querySelectorAll("[data-codex-plus-project-selector-trigger]").length;
1313
+ if (triggerCount > 0) break;
1314
+ await new Promise((resolve) => setTimeout(resolve, 100));
1315
+ }
1316
+ if (triggerCount === 0) throw new Error("Project selector shortcut trigger marker is missing from the main composer");
1317
+ const syntheticShortcut = await new Promise((resolve) => {
1318
+ const event = new KeyboardEvent("keydown", { bubbles: true, cancelable: true, key: ".", metaKey: true });
1319
+ document.dispatchEvent(event);
1320
+ setTimeout(() => {
1321
+ const searchInput = document.querySelector("input[placeholder='Search projects']");
1322
+ const menu = document.querySelector("[data-radix-menu-content], [data-radix-popper-content-wrapper], [role='menu']");
1323
+ resolve({
1324
+ defaultPrevented: event.defaultPrevented,
1325
+ opened: Boolean(searchInput || menu),
1326
+ activePlaceholder: document.activeElement?.getAttribute?.("placeholder") ?? "",
1327
+ });
1328
+ }, 400);
1329
+ });
1330
+ document.dispatchEvent(new KeyboardEvent("keydown", { bubbles: true, cancelable: true, key: "Escape" }));
1331
+ pass("projectSelectorShortcut", { ...details, ranked, highlightCount, selected, triggerCount, syntheticShortcut });
942
1332
  } catch (error) {
943
1333
  fail("projectSelectorShortcut", error);
944
1334
  }
@@ -991,6 +1381,7 @@ function pluginAuditExpression({ includeNativeOpenProbes = false } = {}) {
991
1381
  ok: failures.length === 0,
992
1382
  failures,
993
1383
  pluginResults,
1384
+ expectedWarnings,
994
1385
  registeredPlugins: typeof window.CodexPlus?.plugins?.list === "function" ? pluginIds() : null,
995
1386
  startedPlugins: started(),
996
1387
  };
@@ -1009,6 +1400,8 @@ async function runAudit(args, {
1009
1400
  const Session = operations.CdpSession || CdpSession;
1010
1401
  const waitRuntime = operations.waitForLiveRuntime || waitForLiveRuntime;
1011
1402
  const waitAppShell = operations.waitForAppShellMounted || waitForAppShellMounted;
1403
+ const verifyMermaidViewer = operations.verifyMermaidViewerRender || verifyMermaidViewerRender;
1404
+ const verifyProjectSelectorShortcut = operations.verifyProjectSelectorShortcutKey || verifyProjectSelectorShortcutKey;
1012
1405
  const cleanupApp = operations.cleanupLaunchedAuditApp || cleanupLaunchedAuditApp;
1013
1406
  const checkStability = operations.checkKeepOpenAppStability || checkKeepOpenAppStability;
1014
1407
  const preflightAudit = operations.auditPreflight || auditPreflight;
@@ -1041,15 +1434,17 @@ async function runAudit(args, {
1041
1434
  }),
1042
1435
  );
1043
1436
  }
1044
- syncResult = await withAuditProgress(
1045
- progress,
1046
- "Syncing dev home",
1047
- "Synced dev home",
1048
- () => syncHome({
1049
- sourceHome: args.sourceHome,
1050
- devHome: args.devHome,
1051
- }),
1052
- );
1437
+ if (args.apply || args.launch) {
1438
+ syncResult = await withAuditProgress(
1439
+ progress,
1440
+ "Syncing dev home",
1441
+ "Synced dev home",
1442
+ () => syncHome({
1443
+ sourceHome: args.sourceHome,
1444
+ devHome: args.devHome,
1445
+ }),
1446
+ );
1447
+ }
1053
1448
  if (preflight.launch) {
1054
1449
  launchResult = await withAuditProgress(
1055
1450
  progress,
@@ -1060,6 +1455,7 @@ async function runAudit(args, {
1060
1455
  devHome: args.devHome,
1061
1456
  electronUserDataPath: args.electronUserDataPath,
1062
1457
  remoteDebuggingPort: port,
1458
+ devInstanceId: args.devInstanceId,
1063
1459
  }),
1064
1460
  );
1065
1461
  }
@@ -1090,9 +1486,53 @@ async function runAudit(args, {
1090
1486
  "Probed plugins",
1091
1487
  () => cdp.evaluate(pluginAuditExpression({ includeNativeOpenProbes: args.includeNativeOpenProbes })),
1092
1488
  );
1489
+ if (live.pluginResults?.projectSelectorShortcut?.ok) {
1490
+ const shortcut = await withAuditProgress(
1491
+ progress,
1492
+ "Verifying project selector shortcut",
1493
+ "Project selector shortcut opened",
1494
+ () => verifyProjectSelectorShortcut(cdp),
1495
+ );
1496
+ live.pluginResults.projectSelectorShortcut.shortcut = shortcut;
1497
+ if (!shortcut.ok) {
1498
+ live.ok = false;
1499
+ live.pluginResults.projectSelectorShortcut.ok = false;
1500
+ live.failures.push({
1501
+ plugin: "projectSelectorShortcut",
1502
+ message: shortcut.message || "Cmd+. did not open the project selector",
1503
+ });
1504
+ }
1505
+ }
1506
+ const shouldProbeMermaidViewer = live.pluginResults?.mermaidFullscreen?.ok;
1507
+ const mermaidViewerRender = shouldProbeMermaidViewer
1508
+ ? await withAuditProgress(
1509
+ progress,
1510
+ "Verifying Mermaid viewer render",
1511
+ "Mermaid viewer rendered",
1512
+ () => verifyMermaidViewer(cdp, port, { Session }),
1513
+ )
1514
+ : null;
1515
+ if (mermaidViewerRender != null) {
1516
+ live.pluginResults.mermaidFullscreen.viewerRenderProbe = mermaidViewerRender;
1517
+ if (!mermaidViewerRender.ok) {
1518
+ live.ok = false;
1519
+ live.pluginResults.mermaidFullscreen.ok = false;
1520
+ live.failures.push({
1521
+ plugin: "mermaidFullscreen",
1522
+ message: `Mermaid viewer render failed: ${mermaidViewerRender.message || "no SVG rendered"}`,
1523
+ });
1524
+ }
1525
+ }
1526
+ appShellStatus = await withAuditProgress(
1527
+ progress,
1528
+ "Verifying Codex app shell after probes",
1529
+ "App shell still healthy",
1530
+ () => waitAppShell(cdp),
1531
+ );
1093
1532
  result = {
1094
1533
  ok: live.ok,
1095
1534
  failures: live.failures,
1535
+ expectedWarnings: live.expectedWarnings || [],
1096
1536
  pluginResults: live.pluginResults,
1097
1537
  target: {
1098
1538
  app: path.resolve(args.target),
@@ -1115,6 +1555,8 @@ async function runAudit(args, {
1115
1555
  command: launchResult.command,
1116
1556
  args: launchResult.args,
1117
1557
  pid: launchResult.pid,
1558
+ devBundle: launchResult.devBundle,
1559
+ instanceIdentity: launchResult.instanceIdentity,
1118
1560
  },
1119
1561
  registeredPlugins: live.registeredPlugins,
1120
1562
  startedPlugins: live.startedPlugins,
@@ -1124,6 +1566,7 @@ async function runAudit(args, {
1124
1566
  nativeOpenProbes: {
1125
1567
  included: Boolean(args.includeNativeOpenProbes),
1126
1568
  },
1569
+ mermaidViewerRender,
1127
1570
  preflight,
1128
1571
  };
1129
1572
  return result;
@@ -1135,6 +1578,7 @@ async function runAudit(args, {
1135
1578
  message: error.message,
1136
1579
  details: error.details,
1137
1580
  }],
1581
+ expectedWarnings: [],
1138
1582
  pluginResults: {},
1139
1583
  target: {
1140
1584
  app: path.resolve(args.target),
@@ -1157,6 +1601,8 @@ async function runAudit(args, {
1157
1601
  command: launchResult.command,
1158
1602
  args: launchResult.args,
1159
1603
  pid: launchResult.pid,
1604
+ devBundle: launchResult.devBundle,
1605
+ instanceIdentity: launchResult.instanceIdentity,
1160
1606
  },
1161
1607
  registeredPlugins: null,
1162
1608
  startedPlugins: null,
@@ -1263,5 +1709,7 @@ module.exports = {
1263
1709
  shouldShowAuditProgress,
1264
1710
  waitForAppShellMounted,
1265
1711
  waitForLiveRuntime,
1712
+ verifyMermaidViewerRender,
1713
+ verifyProjectSelectorShortcutKey,
1266
1714
  waitForRendererTarget,
1267
1715
  };