Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
75 changes: 49 additions & 26 deletions runtime/_test-settings.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
join(import.meta.dir, "native-settings.js"),
"utf8",
);
const zedOpenSource = readFileSync(join(import.meta.dir, "zed-open.js"), "utf8");

function extractFunction(name) {
const marker = `function ${name}(`;
Expand Down Expand Up @@ -186,18 +187,20 @@
const descKeyBinding = templatePlaceholder("descKey");
const toggleKeyBinding = templatePlaceholder("toggleKey");
expect(source).toContain("Enable port forwarding");
expect(source).toContain('${helperSettingsSectionAttribute}="port-forwarding"');
expect(source).toContain('${helperSettingsSectionAttribute}="${sectionId}"');
expect(source).toContain('nativeSettingsGroupSection("Port forwarding"');
expect(source).toContain('"port-forwarding")');
expect(source).toContain("function focusHelperSettingsSection(");
expect(source).toContain(`data-codex-helper-setting-desc="${descKeyBinding}"`);
expect(source).toContain(`${helperToggleBinding}="${toggleKeyBinding}"`);
expect(source).toContain(
'switchRow("Enable port forwarding", "Detect and forward ports from agent sessions.", "portForwardingEnabled"',
'nativeSettingsSwitchRow("Enable port forwarding", "Detect and forward ports from agent sessions.", "portForwardingEnabled"',
);
expect(source).toContain(
'switchRow("Auto-forward detected web ports", "Open forwarded web URLs when a common dev port is detected.", "portAutoForwardWeb"',
'nativeSettingsSwitchRow("Auto-forward detected web ports", "Open forwarded web URLs when a common dev port is detected.", "portAutoForwardWeb"',
);
expect(source).toContain(
'switchRow("Use the same local port by default", "Bind forwarded ports to the same local port number when possible.", "portSameLocalPort"',
'nativeSettingsSwitchRow("Use the same local port by default", "Bind forwarded ports to the same local port number when possible.", "portSameLocalPort"',
);
});

Expand All @@ -217,24 +220,23 @@
});

test("settings page groups options by feature area", () => {
expect(source).toContain('codex-helper-settings-section-title text-sm font-medium text-token-text-primary">Basic</div>');
expect(source).toContain('codex-helper-settings-section-title text-sm font-medium text-token-text-primary">Auto naming</div>');
expect(source).toContain('codex-helper-settings-section-title text-sm font-medium text-token-text-primary">Sessions</div>');
expect(source).toContain('codex-helper-settings-section-title text-sm font-medium text-token-text-primary">Port forwarding</div>');
expect(source).toContain('sectionHeading("Loaded scripts"');
expect(source).toContain('sectionHeading("Log files"');
expect(source).toContain('nativeSettingsGroupSection("Integrations"');
expect(source).toContain('nativeSettingsGroupSection("Session actions"');
expect(source).toContain('nativeSettingsGroupSection("Chat titles"');
expect(source).toContain('nativeSettingsGroupSection("Port forwarding"');
expect(source).not.toContain('>Basic</div>');
expect(source).not.toContain('sectionHeading("Loaded scripts"');
expect(source).not.toContain('sectionHeading("Log files"');
expect(source).toContain("https://github.com/loocor/codex-helper");
expect(source).toContain('externalLinkRow("Project repository"');
expect(source).toContain('actionRow("Open in Zed"');
expect(source).toContain("sectionHeading");
expect(source).toContain("function nativeSettingsAboutPageContent(");
expect(source).toContain('nativeSettingsActionRow("Open in Zed"');
expect(source).toContain("open-scripts-dir");
expect(source).toContain("open-logs-dir");
expect(source).toContain("codex-helper-settings-section-link");
expect(source).not.toContain("Helper directory");
expect(source).toContain("codex-helper-settings-scroll");
expect(source).toContain('forkRemoteProject: "Fork into Remote Project..."');
expect(source).toContain('forkLocalProject: "Fork into Local Project..."');
expect(source).toContain('forkAnotherProject: "Fork into Another Project..."');
expect(source).toContain('forkRemoteProject: "Fork into remote project..."');
expect(source).toContain('forkLocalProject: "Fork into local project..."');
expect(source).toContain('forkAnotherProject: "Fork into another project..."');
expect(source).toContain('const order = ["autoRename", "export", "fork"]');
expect(source).toContain('autoRename: "Regenerate chat title"');
expect(source).toContain('bridge("/auto-rename-chat"');
Expand Down Expand Up @@ -490,19 +492,37 @@

test("standalone helper settings dialog is not bundled", () => {
expect(source).not.toContain("function showHelperSettingsDialog(");
expect(source).not.toContain("function renderHelperPage(");
expect(source).not.toContain("function clearHelperSettingsPage(");
expect(source).not.toContain("function stashHostContent(");
expect(source).not.toContain("function restoreStashedContent(");
expect(source).not.toContain("helperPageAttribute");

Check failure on line 499 in runtime/_test-settings.test.js

View workflow job for this annotation

GitHub Actions / check

error: expect(received).not.toContain(expected)

Expected to not contain: "helperPageAttribute" </span>\n <label>\n <span>Local port</span>\n <input data-codex-helper-port-dialog-local inputmode=\"numeric\" autocomplete=\"off\">\n </label>\n </div>\n <div class=\"codex-helper-port-dialog-error\" data-codex-helper-port-dialog-error></div>\n <div class=\"codex-helper-port-dialog-actions\">\n <button type=\"button\" data-codex-helper-port-dialog-cancel>Cancel</button>\n <button type=\"button\" data-codex-helper-port-dialog-submit>${options.submitLabel || \"Save\"}</button>\n </div>\n </div>\n `;\n const remoteInput = dialog.querySelector(\n \"[data-codex-helper-port-dialog-remote]\",\n );\n const localInput = dialog.querySelector(\n \"[data-codex-helper-port-dialog-local]\",\n );\n const error = dialog.querySelector(\"[data-codex-helper-port-dialog-error]\");\n if (\n !(remoteInput instanceof HTMLInputElement) ||\n !(localInput instanceof HTMLInputElement) ||\n !(error instanceof HTMLElement)\n ) {\n resolve(null);\n return;\n }\n remoteInput.value = options.remotePort ? String(options.remotePort) : \"\";\n localInput.value = options.localPort ? String(options.localPort) : \"\";\n if (options.lockRemotePort) {\n remoteInput.readOnly = true;\n remoteInput.setAttribute(\"aria-readonly\", \"true\");\n }\n const cleanup = () => {\n dialog.remove();\n if (portForwardDialogRoot === dialog) portForwardDialogRoot = null;\n };\n const submit = () => {\n const remotePort = parsePortInput(remoteInput.value);\n const localPort = parsePortInput(localInput.value);\n if (!remotePort || !localPort) {\n error.textContent = \"Enter valid ports from 1 to 65535.\";\n return;\n }\n resolvePortMappingDialog(resolve, { remotePort, localPort }, cleanup);\n };\n dialog.addEventListener(\n \"click\",\n (event) => {\n const target = event.target;\n if (\n target === dialog ||\n target?.closest?.(\"[data-codex-helper-port-dialog-cancel]\")\n ) {\n event.preventDefault();\n resolvePortMappingDialog(resolve, null, cleanup);\n return;\n }\n if (target?.closest?.(\"[data-codex-helper-port-dialog-submit]\")) {\n event.preventDefault();\n submit();\n }\n },\n true,\n );\n dialog.addEventListener(\n \"keydown\",\n (event) => {\n if (event.key === \"Escape\") {\n event.preventDefault();\n resolvePortMappingDialog(resolve, null, cleanup);\n return;\n }\n if (event.key === \"Enter\") {\n event.preventDefault();\n submit();\n }\n },\n true,\n );\n document.body.appendChild(dialog);\n portForwardDialogRoot = dialog;\n (options.lockRemotePort ? localInput : remoteInput).focus();\n });\n}\n\nasync function confirmPortMappingDelete(entry) {\n closePortForwardRowMenu();\n closePortForwardDialog();\n if (portForwardDialogRoot?.isConnected) return false;\n return new Promise((resolve) => {\n const dialog = document.createElement(\"div\");\n dialog.setAttribute(\"data-codex-helper-port-dialog\", \"true\");\n dialog.setAttribute(\"role\", \"dialog\");\n dialog.setAttribute(\"aria-modal\", \"true\");\n dialog.setAttribute(\"aria-label\", \"Delete port mapping\");\n dialog.innerHTML = `\n <div class=\"codex-helper-port-dialog-panel\">\n <div class=\"codex-helper-port-dialog-title\">Delete port mapping?</div>\n <div class=\"codex-helper-port-dialog-message\">Remote port ${entry.remotePort} will stop forwarding to localhost:${portLocalPortLabel(entry)}.</div>\n <div class=\"codex-helper-port-dialog-actions\">\n <button type=\"button\" data-codex-helper-port-dialog-cancel>Cancel</button>\n <button type=\"button\" data-codex-helper-port-dialog-delete>Delete</button>\n </div>\n </di
expect(source).not.toContain("helperEntryAttribute");
expect(source).not.toContain("helperContentHostAttribute");
expect(source).not.toContain("helperPageRoot");
expect(source).not.toContain("helperContentHost");
expect(source).not.toContain("helperContentStash");
expect(source).not.toContain("helperDialogRoot = renderHelperPage(body,");
expect(source).not.toContain("pageAttribute: helperDialogPageAttribute");
expect(source).not.toContain("helperDialogRoot = renderNativeHelperSettingsPage");
expect(source).toContain("helperNativeSettingsPageAttribute");
expect(zedOpenSource).not.toContain("data-codex-helper-settings-page");
expect(zedOpenSource).toContain("data-codex-helper-native-settings-page");
});

test("startup does not eagerly mount inline General settings page", () => {
expect(source).not.toContain("showHelperSettingsPage({ refresh: true })");
expect(source).not.toContain("showHelperSettingsPage({ refresh: false })");
});

test("session context menu extends Codex native electronBridge menu", () => {
expect(source).toContain("showExtendedSessionContextMenu");
expect(source).toContain("buildCodexSessionNativeMenuItems");
test("session context menu hooks Codex native menu model", () => {
expect(source).toContain("installSessionContextMenuBridge");
expect(source).toContain("sessionContextMenuMapRestore");
expect(source).toContain("Array.prototype.map");
expect(source).toContain("appendHelperSessionMenuItems");
expect(source).toContain("buildHelperSessionMenuModelItems");
expect(source).toContain("prepareSessionContextMenu");
expect(source).toContain("replaySessionContextMenu");
expect(source).toContain("openProjectForkMenu");
expect(source).toContain("navigateAfterFork(result, target)");
expect(source).toContain("Regenerate chat title");
Expand All @@ -512,19 +532,22 @@
expect(source).toContain("window.location.assign(path)");
expect(source).toContain("nativeProjectTargets");
expect(source).toContain("helperSessionMenuIcon");
expect(source).toContain("Fork into Another Project...");
expect(source).toContain("Fork into another project...");
expect(source).not.toContain("Move Session");
expect(source).toContain("window.electronBridge");
expect(source).toContain("open-thread-new-window");
expect(source).toContain("loadRemoteProjectMetadataOrEmpty");
expect(source).toContain('logDiagnostic("remote_project_metadata_unavailable"');
expect(source).toContain("codex-helper-session-");
expect(source).toContain("stopImmediatePropagation");
expect(source).not.toContain('id: "mark-thread-unread"');
expect(source).not.toContain('id: "fork-into-local"');
expect(source).not.toContain('id: "fork-into-worktree"');
expect(source).toContain("trackSessionContextMenu(row)");
expect(source).not.toContain("showExtendedSessionContextMenu");
expect(source).not.toContain("buildCodexSessionNativeMenuItems");
expect(source).not.toContain("forwardSessionMenuAction");
expect(source).toContain('id === "mark-thread-unread"');
expect(source).toContain('id === "fork-into-local"');
expect(source).toContain('id === "fork-into-worktree"');
expect(source).not.toContain("installSessionContextMenuItems");
expect(source).not.toContain("installElectronContextMenuHook");
expect(source).not.toContain("showContextMenuWithHelperItems");
expect(source).not.toContain("promptMoveTargetPath");
});

Expand Down
54 changes: 40 additions & 14 deletions runtime/bootstrap.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,6 @@ function installObserver() {
const observer = new MutationObserver(() => {
maintainPortsPanel();
installNativeHelperSettingsGroup();
if (helperPageRoot && !helperPageRoot.isConnected) {
clearHelperSettingsPage();
}
if (helperNativeSettingsRoot && !helperNativeSettingsRoot.isConnected) {
clearNativeHelperSettingsPage();
}
Expand Down Expand Up @@ -103,6 +100,26 @@ function onHelperRuntimeClick(event) {
});
}

function replaySessionContextMenu(event, target) {
target.dispatchEvent(
new MouseEvent("contextmenu", {
bubbles: true,
cancelable: true,
view: window,
clientX: event.clientX,
clientY: event.clientY,
screenX: event.screenX,
screenY: event.screenY,
button: event.button,
buttons: event.buttons,
ctrlKey: event.ctrlKey,
shiftKey: event.shiftKey,
altKey: event.altKey,
metaKey: event.metaKey,
}),
);
}

function onHelperRuntimeContextMenu(event) {
const target = event.target;
if (!(target instanceof Element)) return;
Expand All @@ -115,18 +132,26 @@ function onHelperRuntimeContextMenu(event) {
}
const row = sessionRowFromTarget(target);
if (!(row instanceof HTMLElement)) return;
if (!sessionContextMenuReady() && !sessionContextMenuReplayInFlight) {
event.preventDefault();
event.stopImmediatePropagation();
sessionContextMenuReplayInFlight = true;
prepareSessionContextMenu()
.catch((error) => {
logDiagnostic("session_menu_prepare_failed", {
error: error?.message || String(error),
});
})
.finally(() => {
try {
if (target.isConnected) replaySessionContextMenu(event, target);
} finally {
sessionContextMenuReplayInFlight = false;
}
});
return;
}
trackSessionContextMenu(row);
if (enabledSessionActions().length === 0) return;
event.preventDefault();
event.stopImmediatePropagation();
const ref = sessionRefFromRow(row);
if (!ref.session_id) return;
showExtendedSessionContextMenu(row, ref).catch((error) => {
showHelperToast(error?.message || String(error));
logDiagnostic("session_menu_open_failed", {
error: error?.message || String(error),
});
});
}

function onHelperRuntimeKeydown(event) {
Expand Down Expand Up @@ -202,6 +227,7 @@ window.__codexHelperRuntimeCleanup = () => {
closePortForwardRowMenu();
closePortForwardDialog();
clearNativeHelperSettingsPage();
if (sessionContextMenuMapRestore) sessionContextMenuMapRestore();
removeHelperRuntimeEventListeners();
pendingPortScan = 0;
maintainPortsPanelTimer = 0;
Expand Down
13 changes: 6 additions & 7 deletions runtime/constants.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,4 @@
// Constants and mutable runtime state
const helperEntryAttribute = "data-codex-helper-settings-entry";
const helperContentHostAttribute = "data-codex-helper-content-host";
const helperPageAttribute = "data-codex-helper-settings-page";
const helperCommandAttribute = "data-codex-helper-command";
const helperToggleAttribute = "data-codex-helper-setting-toggle";
const helperNumberAttribute = "data-codex-helper-setting-number";
Expand All @@ -28,13 +25,13 @@ const helperPanelClass =
"codex-helper-panel flex flex-col divide-y-[0.5px] divide-token-border overflow-hidden rounded-lg border border-token-border";
let observerInstalled = false;
let helperRuntimeObserver = null;
let helperPageRoot = null;
let sessionContextMenuMapRestore = null;
let sessionContextMenuReplayInFlight = false;
const SESSION_CONTEXT_MENU_MAX_AGE_MS = 2500;
let helperNativeSettingsRoot = null;
let helperNativeSettingsContentHost = null;
let helperNativeSettingsContentStash = null;
let helperNativeSettingsActivePage = "";
let helperContentHost = null;
let helperContentStash = null;
let pendingSessionMenuContext = null;
let pendingPortScan = 0;
let maintainPortsPanelTimer = 0;
Expand All @@ -57,7 +54,7 @@ const suppressedPortMappings = new Set();
let featureSettings = {
markdownExportEnabled: false,
sessionMoveEnabled: false,
autoRenameMenuEnabled: false,
autoRenameMenuEnabled: true,
markdownFriendlyFilenameEnabled: true,
autoNamingMinChars: 4,
autoNamingMaxChars: 10,
Expand All @@ -66,3 +63,5 @@ let featureSettings = {
portSameLocalPort: true,
};
let featureSettingsLoaded = false;
let cachedRemoteProjectMetadata = [];
let cachedRemoteProjectMetadataLoaded = false;
25 changes: 20 additions & 5 deletions runtime/native-settings.js
Original file line number Diff line number Diff line change
Expand Up @@ -396,6 +396,21 @@ function nativeSettingsPanel(rows, extraClass = "") {
return `<div class="${classes}" style="background-color: var(--color-background-panel, var(--color-token-bg-fog));">${rows}</div>`;
}

function nativeSettingsGroupTitle(title) {
return `<div class="codex-helper-settings-section-title text-sm font-medium text-token-text-primary">${title}</div>`;
}

function nativeSettingsGroupSection(title, rows, sectionId = "") {
const sectionAttr = sectionId
? ` ${helperSettingsSectionAttribute}="${sectionId}"`
: "";
return `
<section class="codex-helper-settings-section flex flex-col gap-1.5"${sectionAttr}>
${nativeSettingsGroupTitle(title)}
${nativeSettingsPanel(rows)}
</section>`;
}

function nativeSettingsIconSvg(name) {
if (name === "refresh") {
return nativeSettingsStandardIconSvg("refresh-cw");
Expand Down Expand Up @@ -531,26 +546,26 @@ function nativeSettingsPageContent(pageId) {
return nativeSettingsAboutPageContent();
}
return `
${nativeSettingsPanel(`
${nativeSettingsGroupSection("Integrations", `
${nativeSettingsActionRow("Backend", "Loading", "refresh", "Refresh", "data-codex-helper-backend")}
${nativeSettingsActionRow("Open in Zed", "Loading", "refresh", "Refresh", "data-codex-helper-zed-status")}
${nativeSettingsActionRow("DevTools", "Open Chrome DevTools for this Codex window.", "open-devtools", "Open")}
`)}
${nativeSettingsPanel(`
${nativeSettingsGroupSection("Session actions", `
${nativeSettingsSwitchRow("Markdown export", "Export conversations as Markdown from the session menu.", "markdownExportEnabled", "markdownExportEnabled", "Markdown export")}
${nativeSettingsSwitchRow("Fork sessions", "Fork sessions into local, remote, or another project from the sidebar context menu.", "sessionMoveEnabled", "sessionMoveEnabled", "Fork sessions")}
`)}
${nativeSettingsPanel(`
${nativeSettingsGroupSection("Chat titles", `
${nativeSettingsSwitchRow("Regenerate chat title", "Show Regenerate chat title in the session context menu.", "autoRenameMenuEnabled", "autoRenameMenuEnabled", "Regenerate chat title")}
${nativeSettingsSwitchRow("Friendly Markdown filenames", "Use Codex auto naming for exported Markdown filenames.", "markdownFriendlyFilenameEnabled", "markdownFriendlyFilenameEnabled", "Friendly Markdown filenames")}
${nativeSettingsNumberRow("Minimum characters", "Smallest expected auto name length.", "autoNamingMinChars", "Minimum auto naming characters")}
${nativeSettingsNumberRow("Maximum characters", "Largest expected auto name length; 10 works well for Chinese names.", "autoNamingMaxChars", "Maximum auto naming characters")}
`)}
${nativeSettingsPanel(`
${nativeSettingsGroupSection("Port forwarding", `
${nativeSettingsSwitchRow("Enable port forwarding", "Detect and forward ports from agent sessions.", "portForwardingEnabled", "portForwardingEnabled", "Enable port forwarding")}
${nativeSettingsSwitchRow("Auto-forward detected web ports", "Open forwarded web URLs when a common dev port is detected.", "portAutoForwardWeb", "portAutoForwardWeb", "Auto-forward detected web ports")}
${nativeSettingsSwitchRow("Use the same local port by default", "Bind forwarded ports to the same local port number when possible.", "portSameLocalPort", "portSameLocalPort", "Use the same local port by default")}
`)}
`, "port-forwarding")}
`;
}

Expand Down
Loading
Loading