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
60 changes: 58 additions & 2 deletions runtime/_test-settings.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,8 @@ function loadForkProjectHelpers(document) {
extractFunction("enabledForkSessionActions"),
extractFunction("forkedSessionPath"),
extractFunction("codexAppServerHostId"),
"return { nativeProjectTargets, forkTargetsForAction, enabledForkSessionActions, forkedSessionPath, codexAppServerHostId };",
extractFunction("codexThreadId"),
"return { nativeProjectTargets, forkTargetsForAction, enabledForkSessionActions, forkedSessionPath, sessionProjectContext, codexAppServerHostId, codexThreadId };",
].join("\n"),
)(document, document.Element);
}
Expand Down Expand Up @@ -183,6 +184,7 @@ test("disabling port forwarding stops managed tunnels", () => {

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"');
Expand All @@ -199,7 +201,13 @@ test("settings page groups options by feature area", () => {
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 = ["export", "fork"]');
expect(source).toContain('const order = ["autoRename", "export", "fork"]');
expect(source).toContain('autoRename: "Regenerate chat title"');
expect(source).toContain('bridge("/auto-rename-chat"');
expect(source).toContain('logDiagnostic("auto_rename_chat_succeeded"');
expect(source).toContain('logDiagnostic("auto_rename_chat_failed"');
expect(source).toContain("await setSidebarConversationTitleForHost(");
expect(source).toContain("autoNamingRangePayload()");
expect(source).not.toContain('move: "Move Session"');
expect(source).not.toContain('copy: "Copy Session"');
expect(source).not.toContain('const order = ["export", "copy", "move", "delete"]');
Expand All @@ -209,6 +217,15 @@ test("settings page groups options by feature area", () => {
expect(source).not.toContain('">Other</div>');
});

test("number settings validate the configured character range before saving", () => {
expect(source).toContain("if (value < 1 || value > 20)");
expect(source).toContain(
"Settings value for ${key} must be between 1 and 20",
);
expect(source).toContain('logDiagnostic("settings_update_failed", { key, message })');
expect(source).toContain('applySettings({ status: "ok", settings: featureSettings })');
});

test("account menu no longer exposes helper settings dialog entry", () => {
expect(source).toContain("Helper Settings");
expect(source).not.toContain("data-codex-helper-account-settings-entry");
Expand Down Expand Up @@ -454,6 +471,9 @@ test("session context menu extends Codex native electronBridge menu", () => {
expect(source).toContain("buildCodexSessionNativeMenuItems");
expect(source).toContain("openProjectForkMenu");
expect(source).toContain("navigateAfterFork(result, target)");
expect(source).toContain("Regenerate chat title");
expect(source).toContain("markdown_friendly_filename_succeeded");
expect(source).toContain("markdown_friendly_filename_failed");
expect(source).toContain("showHelperToast(result.warning || result.message || \"Forked\")");
expect(source).toContain("window.location.assign(path)");
expect(source).toContain("nativeProjectTargets");
Expand Down Expand Up @@ -695,6 +715,42 @@ test("fork success refreshes sidebar through Codex recent conversations manager"
expect(source).toContain('"sidebar_refresh_manager_missing"');
});

test("auto rename updates Codex sidebar manager before refreshing", () => {
expect(source).toContain("function codexThreadId(sessionId)");
expect(source).toContain(
"async function setSidebarConversationTitleForHost(hostId, sessionId, title)",
);
expect(source).not.toContain('manager.sendRequest("thread/name/set"');
expect(source).toContain("manager.applyThreadTitleUpdateAndNotify({");
expect(source).toContain('"sidebar_title_update_failed"');
expect(source).toContain("await setSidebarConversationTitleForHost(");
expect(source).toContain("await refreshSidebarConversationsForHost(context.hostId)");
});

test("auto rename preserves remote host context for sidebar title updates", () => {
const document = fakeProjectDocument([], "/srv/current");
const helpers = loadForkProjectHelpers(document);
const row = new document.Element({
"data-app-action-sidebar-thread-id": "remote:thread-1",
"data-app-action-sidebar-thread-cwd": "/srv/current",
"data-app-action-sidebar-thread-host-id": "remote-ssh-codex-managed:box",
});

expect(helpers.sessionProjectContext(row)).toEqual({
hostId: "remote-ssh-codex-managed:box",
remote: true,
path: "/srv/current",
});
expect(helpers.codexAppServerHostId("remote-ssh-codex-managed:box")).toBe(
"remote-ssh-codex-managed:box",
);
expect(helpers.codexThreadId("remote:thread-1")).toBe("thread-1");
expect(source).toContain("host_id: context.hostId");
expect(source).toContain(
"setSidebarConversationTitleForHost(\n context.hostId",
);
});

test("codex app-server helpers normalize host ids", () => {
const helpers = loadForkProjectHelpers(fakeProjectDocument([]));

Expand Down
16 changes: 16 additions & 0 deletions runtime/bootstrap.js
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,22 @@ function onHelperRuntimeKeydown(event) {
function onHelperRuntimeChange(event) {
const target = event.target;
if (!(target instanceof HTMLInputElement)) return;
if (target.hasAttribute(helperNumberAttribute)) {
event.preventDefault();
event.stopPropagation();
handleHelperNumberInput(target).catch((error) => {
target.disabled = false;
setHelperText(
"[data-codex-helper-backend]",
error?.message || String(error),
);
logDiagnostic("settings_update_failed", {
key: target.getAttribute(helperNumberAttribute),
error: error?.message || String(error),
});
});
return;
}
if (!target.hasAttribute(helperToggleAttribute)) return;
event.preventDefault();
event.stopPropagation();
Expand Down
5 changes: 5 additions & 0 deletions runtime/constants.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ 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";
const helperToastAttribute = "data-codex-helper-toast";
const helperSessionActionPrefix = "codex-helper-session-";
const helperSettingsSectionAttribute = "data-codex-helper-settings-section";
Expand Down Expand Up @@ -56,6 +57,10 @@ const suppressedPortMappings = new Set();
let featureSettings = {
markdownExportEnabled: false,
sessionMoveEnabled: false,
autoRenameMenuEnabled: false,
markdownFriendlyFilenameEnabled: true,
autoNamingMinChars: 4,
autoNamingMaxChars: 10,
portForwardingEnabled: false,
portAutoForwardWeb: true,
portSameLocalPort: true,
Expand Down
17 changes: 17 additions & 0 deletions runtime/native-settings.js
Original file line number Diff line number Diff line change
Expand Up @@ -447,6 +447,17 @@ function nativeSettingsSwitchRow(title, description, descKey, toggleKey, ariaLab
</div>`;
}

function nativeSettingsNumberRow(title, description, numberKey, ariaLabel) {
return `
<div class="flex items-center justify-between gap-4 p-3">
<div class="flex min-w-0 flex-col gap-1">
<div class="min-w-0 text-sm text-token-text-primary">${title}</div>
<div class="text-token-text-secondary min-w-0 text-sm" data-codex-helper-setting-desc="${numberKey}">${description}</div>
</div>
<input type="number" min="1" max="20" step="1" class="codex-helper-number-input" ${helperNumberAttribute}="${numberKey}" aria-label="${ariaLabel}">
</div>`;
}

function nativeSettingsActionRow(title, detail, command, buttonLabel, detailAttr = "") {
return `
<div class="flex items-center justify-between gap-4 p-3">
Expand Down Expand Up @@ -529,6 +540,12 @@ function nativeSettingsPageContent(pageId) {
${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(`
${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(`
${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")}
Expand Down
103 changes: 102 additions & 1 deletion runtime/sessions.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
// Session context menu, actions, project forks, and toast UI
function enabledSessionActions() {
const order = ["export", "fork"];
const order = ["autoRename", "export", "fork"];
return order.filter((action) => {
if (action === "autoRename") return featureSettings.autoRenameMenuEnabled;
if (action === "export") return featureSettings.markdownExportEnabled;
if (action === "fork") return featureSettings.sessionMoveEnabled;
return false;
Expand Down Expand Up @@ -85,6 +86,13 @@
return sessionRemoteHostId(hostId) || "local";
}

function codexThreadId(sessionId) {
return String(sessionId || "")
.trim()
.replace(/^local:/, "")
.replace(/^remote:/, "");
}

function sessionRowIsPinned(row) {
return row.getAttribute("data-app-action-sidebar-thread-pinned") === "true";
}
Expand Down Expand Up @@ -556,6 +564,7 @@

function sessionActionMenuLabels() {
return {
autoRename: "Regenerate chat title",
export: "Export Markdown",
forkRemoteProject: "Fork into Remote Project...",
forkLocalProject: "Fork into Local Project...",
Expand All @@ -567,6 +576,8 @@
const svgs = {
export:
'<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16"><path fill="black" d="M8 2.25v6.5M5.1 6.35 8 9.25l2.9-2.9"/><path fill="black" d="M3.25 12.75h9.5v1H3.25z"/></svg>',
autoRename:
'<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16"><path fill="black" d="M8 1.5 9.15 5l3.35 1.15-3.35 1.2L8 10.5 6.85 7.35 3.5 6.15 6.85 5z"/><path fill="black" d="M3.75 10.25h8.5v1.25h-8.5zm0 2.5h6v1.25h-6z"/></svg>',
forkRemoteProject:
'<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16"><path fill="black" d="M3 4.5h10v1.25H3zm1.25 2.5h7.5v1.25H4.25zm1.25 2.5h5v1.25H5.5zm1.25 2.5h2.5v1.25H6.75z"/><path fill="black" d="M11.5 3.25 14.25 6l-2.75 2.75V6.5H9.25V5.5h3.75z"/></svg>',
forkLocalProject:
Expand Down Expand Up @@ -750,24 +761,114 @@
}
}

async function setSidebarConversationTitleForHost(hostId, sessionId, title) {
const normalizedHostId = codexAppServerHostId(hostId);
const threadId = codexThreadId(sessionId);
const name = String(title || "").trim();
if (!threadId || !name) return false;
const manager = collectSidebarConversationManagers().find(
(candidate) => candidate.hostId === normalizedHostId,
);
if (!manager) {
logDiagnostic("sidebar_title_manager_missing", {
host_id: normalizedHostId,
session_id: threadId,
});
return false;
}
try {
const conversation =
typeof manager.getConversation === "function"
? manager.getConversation(threadId)
: null;
if (
conversation &&
typeof manager.applyThreadTitleUpdateAndNotify === "function"
) {
manager.applyThreadTitleUpdateAndNotify({
...conversation,
title: name,
});
}
return true;
} catch (error) {
logDiagnostic("sidebar_title_update_failed", {
host_id: normalizedHostId,
session_id: threadId,
message: error?.message || String(error),
});
return false;
}
}

async function refreshSidebarAfterFork(target) {
await refreshSidebarConversationsForHost(target?.hostId || "");
}

function autoNamingRangePayload() {
return {
autoNamingMinChars: featureSettings.autoNamingMinChars,
autoNamingMaxChars: featureSettings.autoNamingMaxChars,
};
}

async function handleSessionAction(action, row, ref) {
if (action === "autoRename") {
const context = sessionProjectContext(row);
const payload = {
...ref,
host_id: context.hostId,
...autoNamingRangePayload(),
};
const result = await bridge("/auto-rename-chat", payload);
if (result?.status !== "renamed") {
logDiagnostic("auto_rename_chat_failed", {
session_id: ref.session_id,
message: result?.message || "Auto rename failed",
});
throw new Error(result?.message || "Auto rename failed");
}
logDiagnostic("auto_rename_chat_succeeded", {
session_id: ref.session_id,
name: result.name || "",
source: result.source || "",
});
await setSidebarConversationTitleForHost(
context.hostId,
ref.session_id,
result.name || "",
);
await refreshSidebarConversationsForHost(context.hostId);
showHelperToast(result.message || "Regenerated chat title");
return;
}
if (action === "export") {
const context = sessionProjectContext(row);
const result = await bridge("/export-markdown", {
...ref,
host_id: context.hostId,
friendlyFilename: featureSettings.markdownFriendlyFilenameEnabled,
...autoNamingRangePayload(),
});
if (
result?.status !== "exported" ||
typeof result.markdown !== "string" ||
!result.filename
) {
if (featureSettings.markdownFriendlyFilenameEnabled) {
logDiagnostic("markdown_friendly_filename_failed", {
session_id: ref.session_id,
message: result?.message || "Export failed",
});
}
throw new Error(result?.message || "Export failed");
}
if (featureSettings.markdownFriendlyFilenameEnabled) {
logDiagnostic("markdown_friendly_filename_succeeded", {
session_id: ref.session_id,
filename: result.filename,
});
}
downloadMarkdown(result.filename, result.markdown);
showHelperToast(result.message || "Exported");
return;
Expand Down
Loading
Loading