codex-plus-patcher 0.5.0 → 0.6.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.
package/README.md CHANGED
@@ -29,7 +29,10 @@ patches plus readable runtime plugins:
29
29
  - add diagnostic detail for selected app-shell errors
30
30
  - add user-message bubble color controls in Appearance settings
31
31
  - add adaptive project colors for sidebar projects, grouped threads, pinned threads, user-message accents, and the composer
32
+ - show the active project path in the thread header with a copy action
32
33
  - add the `Toggle sidebar blur` command palette entry to blur sidebar chat and project names for the current session
34
+ - add an `Open Developer Tools` panels command for opening the current Codex window's DevTools
35
+ - add the `Focus project selector` command and `CmdOrCtrl+.` shortcut for new-chat project selection
33
36
  - add a fullscreen Mermaid diagram viewer with zoom controls
34
37
 
35
38
  The generated app includes a readable Codex Plus runtime under
@@ -112,6 +115,13 @@ Print the machine-readable result:
112
115
  codex-plus-patcher apply --dry-run --json
113
116
  ```
114
117
 
118
+ Inspect menu-related patch markers in a generated app:
119
+
120
+ ```bash
121
+ codex-plus-patcher menu-diagnostics \
122
+ --asar "~/Applications/Codex Plus.app/Contents/Resources/app.asar"
123
+ ```
124
+
115
125
  For local development of the CLI wrapper:
116
126
 
117
127
  ```bash
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "codex-plus-patcher",
3
- "version": "0.5.0",
3
+ "version": "0.6.0",
4
4
  "private": false,
5
5
  "description": "Patch queue tool for building a local Codex Plus.app from an installed Codex.app.",
6
6
  "repository": {
@@ -28,6 +28,7 @@
28
28
  },
29
29
  "license": "Apache-2.0",
30
30
  "dependencies": {
31
+ "fzf": "0.5.2",
31
32
  "ora": "^9.4.0"
32
33
  }
33
34
  }
package/src/cli.js CHANGED
@@ -62,6 +62,7 @@ function helpText() {
62
62
  return `Usage:
63
63
  codex-plus-patcher
64
64
  codex-plus-patcher apply [options]
65
+ codex-plus-patcher menu-diagnostics --asar <path> [--json]
65
66
  codex-plus-patcher asar-list --asar <path> [--contains <text>] [--json]
66
67
  codex-plus-patcher asar-cat --asar <path> --file <asar-path> [--json]
67
68
 
@@ -156,6 +157,67 @@ function readAsarFile({ asar, file }) {
156
157
  return { asar, file, size, content };
157
158
  }
158
159
 
160
+ function readPackedEntry(archive, node) {
161
+ if (node.unpacked) return null;
162
+ const size = Number(node.size || 0);
163
+ const start = archive.dataStart + Number(node.offset || 0);
164
+ return archive.buffer.subarray(start, start + size).toString("utf8");
165
+ }
166
+
167
+ function menuDiagnostics({ asar }) {
168
+ if (!asar) throw new Error("--asar is required");
169
+ const archive = readAsar(asar);
170
+ const files = walkFiles(archive.header);
171
+ const commandId = "codexPlusOpenDevTools";
172
+ const menuTitle = "Open Developer Tools";
173
+ const commandMetadataFiles = [];
174
+ const nativeBridgeFiles = [];
175
+ const runtimePluginFiles = [];
176
+ const applicationMenuFiles = [];
177
+
178
+ for (const [file, node] of files) {
179
+ if (!file.endsWith(".js")) continue;
180
+ const content = readPackedEntry(archive, node);
181
+ if (content == null) continue;
182
+ const hasDevToolsCommand = content.includes(commandId);
183
+ const hasMenuTitle = content.includes(menuTitle);
184
+ const hasToggleBottomPanel = content.includes("Toggle Bottom Panel") || content.includes("toggleBottomPanel");
185
+ const hasPanelsGroup = content.includes("commandMenuGroupKey:`panels`") || content.includes('commandMenuGroupKey:"panels"');
186
+ const hasNativeBridge = content.includes("devtools/open") || content.includes("CPXOpenDevTools");
187
+ const hasRuntimePlugin = file.endsWith("/devTools.js") || content.includes('id: "devTools"');
188
+ const hasApplicationMenu = content.includes("Menu.setApplicationMenu") || content.includes("refreshApplicationMenu");
189
+
190
+ if (hasPanelsGroup || hasDevToolsCommand || file.includes("electron-menu-shortcuts")) {
191
+ commandMetadataFiles.push({
192
+ file,
193
+ hasDevToolsCommand,
194
+ hasMenuTitle,
195
+ hasToggleBottomPanel,
196
+ hasPanelsGroup,
197
+ });
198
+ }
199
+ if (hasNativeBridge) nativeBridgeFiles.push({ file, hasDevToolsOpenRequest: content.includes("devtools/open"), hasOpenDevToolsCall: content.includes("openDevTools") });
200
+ if (hasRuntimePlugin) runtimePluginFiles.push({ file, hasDevToolsCommand, hasDevToolsOpenRequest: content.includes("devtools/open") });
201
+ if (hasApplicationMenu) applicationMenuFiles.push({ file, hasDiagnosticsHook: content.includes("CPXLogMenuDiagnostics"), hasDevToolsCommand });
202
+ }
203
+
204
+ return {
205
+ asar,
206
+ commandId,
207
+ menuTitle,
208
+ commandMetadataFiles,
209
+ nativeBridgeFiles,
210
+ runtimePluginFiles,
211
+ applicationMenuFiles,
212
+ summary: {
213
+ commandMetadataFilesWithCommand: commandMetadataFiles.filter((entry) => entry.hasDevToolsCommand).map((entry) => entry.file),
214
+ nativeBridgeFilesWithRequest: nativeBridgeFiles.filter((entry) => entry.hasDevToolsOpenRequest).map((entry) => entry.file),
215
+ runtimePluginFilesWithCommand: runtimePluginFiles.filter((entry) => entry.hasDevToolsCommand).map((entry) => entry.file),
216
+ applicationMenuFilesWithDiagnostics: applicationMenuFiles.filter((entry) => entry.hasDiagnosticsHook).map((entry) => entry.file),
217
+ },
218
+ };
219
+ }
220
+
159
221
  function formatAsarListResult(result) {
160
222
  return result.files.length > 0 ? `${result.files.join("\n")}\n` : "";
161
223
  }
@@ -164,6 +226,34 @@ function formatAsarCatResult(result) {
164
226
  return result.content;
165
227
  }
166
228
 
229
+ function formatMenuDiagnosticsResult(result) {
230
+ const lines = [
231
+ `ASAR: ${result.asar}`,
232
+ `Command: ${result.commandId}`,
233
+ "",
234
+ "Command metadata bundles:",
235
+ ...result.commandMetadataFiles.map((entry) =>
236
+ `- ${entry.file}: command=${entry.hasDevToolsCommand ? "yes" : "no"}, title=${entry.hasMenuTitle ? "yes" : "no"}, bottomPanel=${entry.hasToggleBottomPanel ? "yes" : "no"}, panels=${entry.hasPanelsGroup ? "yes" : "no"}`,
237
+ ),
238
+ "",
239
+ "Native bridge bundles:",
240
+ ...result.nativeBridgeFiles.map((entry) =>
241
+ `- ${entry.file}: request=${entry.hasDevToolsOpenRequest ? "yes" : "no"}, openDevTools=${entry.hasOpenDevToolsCall ? "yes" : "no"}`,
242
+ ),
243
+ "",
244
+ "Runtime plugin bundles:",
245
+ ...result.runtimePluginFiles.map((entry) =>
246
+ `- ${entry.file}: command=${entry.hasDevToolsCommand ? "yes" : "no"}, request=${entry.hasDevToolsOpenRequest ? "yes" : "no"}`,
247
+ ),
248
+ "",
249
+ "Application menu bundles:",
250
+ ...result.applicationMenuFiles.map((entry) =>
251
+ `- ${entry.file}: diagnosticsHook=${entry.hasDiagnosticsHook ? "yes" : "no"}, command=${entry.hasDevToolsCommand ? "yes" : "no"}`,
252
+ ),
253
+ ];
254
+ return `${lines.join("\n")}\n`;
255
+ }
256
+
167
257
  function formatError(error, { debug = false } = {}) {
168
258
  if (debug || process.env.CODEX_PLUS_PATCHER_DEBUG === "1") return error.stack || error.message || String(error);
169
259
  return `Error: ${error.message || String(error)}`;
@@ -232,6 +322,11 @@ async function main() {
232
322
  process.stdout.write(args.json ? `${JSON.stringify(result, null, 2)}\n` : formatAsarCatResult(result));
233
323
  return;
234
324
  }
325
+ if (args.command === "menu-diagnostics") {
326
+ const result = menuDiagnostics(args);
327
+ process.stdout.write(args.json ? `${JSON.stringify(result, null, 2)}\n` : formatMenuDiagnosticsResult(result));
328
+ return;
329
+ }
235
330
  if (args.command !== "apply") throw new Error(`Unknown command: ${args.command}`);
236
331
 
237
332
  const patchSets = await loadPatchSets(args);
@@ -266,10 +361,12 @@ module.exports = {
266
361
  formatAsarCatResult,
267
362
  formatAsarListResult,
268
363
  formatError,
364
+ formatMenuDiagnosticsResult,
269
365
  formatResult,
270
366
  helpText,
271
367
  listAsarFiles,
272
368
  loadPatchSets,
369
+ menuDiagnostics,
273
370
  parseArgs,
274
371
  readAsarFile,
275
372
  requirePatchSetModule,
@@ -15,6 +15,8 @@ module.exports = buildCodexPlusPatchSet({
15
15
  "threadSidePanelTabs": "webview/assets/thread-side-panel-tabs-D0dd27Zf.js",
16
16
  "userMessageAttachments": "webview/assets/user-message-attachments-CgyXEK9U.js",
17
17
  "composer": "webview/assets/composer-CCuv6v-2.js",
18
+ "localActiveWorkspaceRootDropdown": "webview/assets/local-active-workspace-root-dropdown-ymhXI2RF.js",
19
+ "runCommand": "webview/assets/run-command-D6apgII3.js",
18
20
  "localTaskRow": "webview/assets/local-task-row-vTrSC6Rc.js",
19
21
  "keyboardShortcutsSearchInput": "webview/assets/keyboard-shortcuts-search-input-DjVpifwp.js",
20
22
  "src": "src-C7fSIbpz.js",
@@ -15,6 +15,8 @@ module.exports = buildCodexPlusPatchSet({
15
15
  "threadSidePanelTabs": "webview/assets/thread-side-panel-tabs-D0dd27Zf.js",
16
16
  "userMessageAttachments": "webview/assets/user-message-attachments-CgyXEK9U.js",
17
17
  "composer": "webview/assets/composer-CCuv6v-2.js",
18
+ "localActiveWorkspaceRootDropdown": "webview/assets/local-active-workspace-root-dropdown-ymhXI2RF.js",
19
+ "runCommand": "webview/assets/run-command-D6apgII3.js",
18
20
  "localTaskRow": "webview/assets/local-task-row-vTrSC6Rc.js",
19
21
  "keyboardShortcutsSearchInput": "webview/assets/keyboard-shortcuts-search-input-DjVpifwp.js",
20
22
  "src": "src-C7fSIbpz.js",
@@ -11,10 +11,16 @@ module.exports = buildCodexPlusPatchSet({
11
11
  "appShell": "webview/assets/app-shell-0b-x_r3Z.js",
12
12
  "errorBoundary": "webview/assets/error-boundary-BOla93vo.js",
13
13
  "generalSettings": "webview/assets/general-settings-U7DFIZBC.js",
14
+ "header": "webview/assets/header-DgzE38hF.js",
15
+ "threadPageHeader": "webview/assets/thread-page-header-D_hZ50OA.js",
16
+ "localConversationPage": "webview/assets/local-conversation-page-dVDt8SxG.js",
17
+ "threadContext": "webview/assets/thread-context-B0hBrRyZ.js",
14
18
  "sidebarProjectHoverCardSourceRows": "webview/assets/sidebar-project-hover-card-source-rows-DtE7St1r.js",
15
19
  "threadSidePanelTabs": "webview/assets/thread-side-panel-tabs-CLuB2SaS.js",
16
20
  "userMessageAttachments": "webview/assets/user-message-attachments-5G1ZKim-.js",
17
21
  "composer": "webview/assets/composer-DlMDPaCL.js",
22
+ "localActiveWorkspaceRootDropdown": "webview/assets/local-active-workspace-root-dropdown-B28GluSz.js",
23
+ "runCommand": "webview/assets/run-command-B0E8hx7Q.js",
18
24
  "localTaskRow": "webview/assets/local-task-row-CoPNn6SW.js",
19
25
  "mermaidDiagramShell": "webview/assets/mermaid-diagram-shell-BO-t9BGx.js",
20
26
  "keyboardShortcutsSearchInput": "webview/assets/keyboard-shortcuts-search-input-C1dmntOi.js",
@@ -7,14 +7,21 @@ module.exports = buildCodexPlusPatchSet({
7
7
  "asarSha256": "7f45c6a6bad9c6fabe2226e8f7e0aae3792eca8c59f24305b5f2996ee4b37e40",
8
8
  "files": {
9
9
  "main": ".vite/build/main-dSxbxAhH.js",
10
+ "electronCommandSource": ".vite/build/src-DBVh5FZA.js",
10
11
  "appMain": "webview/assets/app-main-Dldh3K_n.js",
11
12
  "appShell": "webview/assets/app-shell-0b-x_r3Z.js",
12
13
  "errorBoundary": "webview/assets/error-boundary-BOla93vo.js",
13
14
  "generalSettings": "webview/assets/general-settings-U7DFIZBC.js",
15
+ "header": "webview/assets/header-DgzE38hF.js",
16
+ "threadPageHeader": "webview/assets/thread-page-header-D_hZ50OA.js",
17
+ "localConversationPage": "webview/assets/local-conversation-page-dVDt8SxG.js",
18
+ "threadContext": "webview/assets/thread-context-B0hBrRyZ.js",
14
19
  "sidebarProjectHoverCardSourceRows": "webview/assets/sidebar-project-hover-card-source-rows-DtE7St1r.js",
15
20
  "threadSidePanelTabs": "webview/assets/thread-side-panel-tabs-CLuB2SaS.js",
16
21
  "userMessageAttachments": "webview/assets/user-message-attachments-5G1ZKim-.js",
17
22
  "composer": "webview/assets/composer-DlMDPaCL.js",
23
+ "localActiveWorkspaceRootDropdown": "webview/assets/local-active-workspace-root-dropdown-B28GluSz.js",
24
+ "runCommand": "webview/assets/run-command-B0E8hx7Q.js",
18
25
  "localTaskRow": "webview/assets/local-task-row-CoPNn6SW.js",
19
26
  "mermaidDiagramShell": "webview/assets/mermaid-diagram-shell-BO-t9BGx.js",
20
27
  "keyboardShortcutsSearchInput": "webview/assets/keyboard-shortcuts-search-input-C1dmntOi.js",
@@ -1,6 +1,10 @@
1
1
  const { codexPlusRuntimeAssets } = require("../../runtime/assets");
2
2
  const { replaceOnce } = require("./replace");
3
3
  const { makePatchSet } = require("./make-patch-set");
4
+ const {
5
+ patchLocalActiveWorkspaceRootDropdownProjectSelectorShortcut,
6
+ patchRunCommandProjectSelectorShortcut,
7
+ } = require("./project-selector-shortcut-patch");
4
8
 
5
9
  function buildCodexPlusPatchSet(config) {
6
10
  const oldTitle = "<title>Codex</title>";
@@ -11,14 +15,22 @@ function buildCodexPlusPatchSet(config) {
11
15
  const files = config.files;
12
16
  const anchors = config.anchors;
13
17
  const mainFile = files.main;
18
+ const electronCommandSourceFile = files.electronCommandSource;
14
19
  const appMainFile = files.appMain;
15
20
  const appShellFile = files.appShell;
16
21
  const errorBoundaryFile = files.errorBoundary;
17
22
  const generalSettingsFile = files.generalSettings;
18
23
  const sidebarProjectHoverCardSourceRowsFile = files.sidebarProjectHoverCardSourceRows;
24
+ const headerFile = files.header;
25
+ const threadPageHeaderFile = files.threadPageHeader;
26
+ const localConversationPageFile = files.localConversationPage;
27
+ const threadContextFile = files.threadContext;
28
+ const threadContextImportFile = threadContextFile?.split("/").pop();
19
29
  const threadSidePanelTabsFile = files.threadSidePanelTabs;
20
30
  const userMessageAttachmentsFile = files.userMessageAttachments;
21
31
  const composerFile = files.composer;
32
+ const localActiveWorkspaceRootDropdownFile = files.localActiveWorkspaceRootDropdown;
33
+ const runCommandFile = files.runCommand;
22
34
  const localTaskRowFile = files.localTaskRow;
23
35
  const mermaidDiagramShellFile = files.mermaidDiagramShell;
24
36
  const electronMenuShortcutsFile = files.electronMenuShortcuts;
@@ -133,8 +145,11 @@ function CPXDiagnosticDetails(e){return window.CodexPlus?.ui?.errors?.renderDeta
133
145
  const codexPlusMermaidHelpers = `
134
146
  function CPXMermaidDiagramProps(e){return window.CodexPlus?.ui?.mermaid?.diagramProps?.(e)}`;
135
147
 
148
+ const codexPlusThreadHeaderHelpers = `
149
+ function CPXThreadHeaderAccessories(e){return window.CodexPlus?.ui?.threadHeader?.renderAccessories?.(e)??null}`;
150
+
136
151
  const codexPlusNativeMainHelpers = `
137
- function CPXOpenMermaidViewer(e){let t=e?.html;if(typeof t!==\`string\`||t.length===0)return{ok:!1};let n=(0,s.join)((0,o.tmpdir)(),\`codex-plus-mermaid-\${(0,u.randomUUID)()}.html\`);(0,l.writeFileSync)(n,t,\`utf8\`);let r=new a.BrowserWindow({height:900,resizable:!0,show:!0,title:\`Mermaid diagram viewer\`,webPreferences:{contextIsolation:!0,nodeIntegration:!1,sandbox:!0},width:1400});return r.webContents.setWindowOpenHandler(e=>{try{let t=new URL(e.url);if(t.protocol===\`https:\`&&t.hostname===\`mermaid.live\`)a.shell.openExternal(e.url)}catch{}return{action:\`deny\`}}),r.on(\`closed\`,()=>{try{(0,l.unlinkSync)(n)}catch{}}),r.loadURL((0,S.pathToFileURL)(n).toString()).catch(()=>{}),{ok:!0}}function CPXRegisterNativeRequest(e){return a.ipcMain.handle(\`codex_plus:native-request\`,async(t,n)=>{if(!e.isTrustedIpcEvent(t))return{ok:!1};switch(n?.method){case\`mermaid/openViewer\`:return CPXOpenMermaidViewer(n.params);default:return{ok:!1}}})}`;
152
+ let CPXNativeMenuItems=[],CPXRefreshApplicationMenu=null;function CPXMenuSnapshot(e){return e?.items?.map(e=>({id:e.id,label:e.label,enabled:e.enabled,visible:e.visible,accelerator:e.accelerator,submenu:CPXMenuSnapshot(e.submenu)}))}function CPXLogMenuDiagnostics(){try{let e=CPXMenuSnapshot(a.Menu.getApplicationMenu())??[],t=JSON.stringify(e),n=t.includes(\`codexPlusOpenDevTools\`)||t.includes(\`Open Developer Tools\`);if(process.env.CODEX_PLUS_MENU_DIAGNOSTICS===\`1\`||!n)console.log(\`[Codex Plus menu diagnostics] \${JSON.stringify({hasOpenDeveloperTools:n,menu:e})}\`)}catch(e){console.log(\`[Codex Plus menu diagnostics] \${JSON.stringify({error:String(e?.message??e)})}\`)}}function CPXOpenDevTools(e){try{let t=e?.sender;if(typeof t?.openDevTools!==\`function\`)return{ok:!1};return t.openDevTools(),{ok:!0}}catch{return{ok:!1}}}function CPXFocusedEvent(){let e=a.BrowserWindow.getFocusedWindow();return e&&!e.isDestroyed()?{sender:e.webContents}:null}function CPXRunNativeMenuRequest(e){switch(e?.method){case\`devtools/open\`:return CPXOpenDevTools(CPXFocusedEvent());default:return{ok:!1}}}function CPXNativeMenuTemplateItems(e){return CPXNativeMenuItems.filter(t=>t.menuId===e).map(e=>({id:e.id,label:e.label,click:()=>{CPXRunNativeMenuRequest(e.nativeRequest)}}))}function CPXRegisterNativeMenuItem(e){if(e?.id==null||e?.menuId==null||e?.label==null||e?.nativeRequest?.method==null)return{ok:!1};let t={id:String(e.id),menuId:String(e.menuId),label:String(e.label),nativeRequest:{method:String(e.nativeRequest.method),params:e.nativeRequest.params},afterId:e.afterId==null?null:String(e.afterId),afterLabel:e.afterLabel==null?null:String(e.afterLabel)};CPXNativeMenuItems=CPXNativeMenuItems.filter(e=>e.id!==t.id),CPXNativeMenuItems.push(t);try{CPXRefreshApplicationMenu?.()}catch{}return CPXLogMenuDiagnostics(),{ok:!0}}function CPXOpenMermaidViewer(e){let t=e?.html;if(typeof t!==\`string\`||t.length===0)return{ok:!1};let n=(0,s.join)((0,o.tmpdir)(),\`codex-plus-mermaid-\${(0,u.randomUUID)()}.html\`);(0,l.writeFileSync)(n,t,\`utf8\`);let r=new a.BrowserWindow({height:900,resizable:!0,show:!0,title:\`Mermaid diagram viewer\`,webPreferences:{contextIsolation:!0,nodeIntegration:!1,sandbox:!0},width:1400});return r.webContents.setWindowOpenHandler(e=>{try{let t=new URL(e.url);if(t.protocol===\`https:\`&&t.hostname===\`mermaid.live\`)a.shell.openExternal(e.url)}catch{}return{action:\`deny\`}}),r.on(\`closed\`,()=>{try{(0,l.unlinkSync)(n)}catch{}}),r.loadURL((0,S.pathToFileURL)(n).toString()).catch(()=>{}),{ok:!0}}function CPXRegisterNativeRequest(e){return a.ipcMain.handle(\`codex_plus:native-request\`,async(t,n)=>{if(!e.isTrustedIpcEvent(t))return{ok:!1};switch(n?.method){case\`native-menu/register-item\`:return CPXRegisterNativeMenuItem(n.params);case\`devtools/open\`:return CPXOpenDevTools(t);case\`mermaid/openViewer\`:return CPXOpenMermaidViewer(n.params);default:return{ok:!1}}})}`;
138
153
 
139
154
  function patchThreadSidePanelTabs(text) {
140
155
  let patched = replaceOnce(
@@ -384,6 +399,91 @@ function patchSidebarProjectHoverCardSourceRows(text) {
384
399
  );
385
400
  }
386
401
 
402
+ function patchHeader(text) {
403
+ let patched = replaceOnce(
404
+ text,
405
+ `import{Z as r,a as i,s as a}from"./app-scope-CWE-zIhQ.js";`,
406
+ `import{Z as r,a as i,a as CPX_readAtom,s as a}from"./app-scope-CWE-zIhQ.js";`,
407
+ "thread header atom reader alias import anchor",
408
+ );
409
+ patched = replaceOnce(
410
+ patched,
411
+ `import{t as ee}from"./tooltip-B-u9JAuV.js";`,
412
+ `import{t as ee,t as CPX_Tooltip}from"./tooltip-B-u9JAuV.js";`,
413
+ "thread header tooltip alias import anchor",
414
+ );
415
+ patched = replaceOnce(
416
+ patched,
417
+ `import{t as _e}from"./dock-DAmmeMut.js";`,
418
+ `import{t as _e}from"./dock-DAmmeMut.js";import{n as CPX_headerCwd,i as CPX_headerHostId}from"./${threadContextImportFile}";`,
419
+ "thread header context import anchor",
420
+ );
421
+ patched = replaceOnce(
422
+ patched,
423
+ "function lt(e){let t=(0,Z.c)(68),",
424
+ `${codexPlusThreadHeaderHelpers}function lt(e){let t=(0,Z.c)(72),`,
425
+ "thread header accessory helper insertion anchor",
426
+ );
427
+ patched = replaceOnce(
428
+ patched,
429
+ "let C;t[36]!==c||t[37]!==g||t[38]!==i?(C=(0,Q.jsx)(`div`,{className:`mr-3 line-clamp-1 flex min-w-0 flex-1 items-center gap-1 truncate`,style:{viewTransitionName:`header-title`},children:i?(0,Q.jsxs)(`div`,{className:`flex min-w-0 flex-1 items-center gap-1`,children:[(0,Q.jsx)(mt,{onClick:c}),(0,Q.jsx)(x,{color:`ghostActive`,type:`button`,onClick:u,className:`min-w-0 flex-1 truncate !px-0 !py-0 text-left text-sm text-token-foreground hover:!bg-transparent hover:opacity-80 electron:font-medium`,children:(0,Q.jsx)(`span`,{className:`truncate`,children:i})})]}):(0,Q.jsx)(`span`,{className:`text-token-description-foreground`,children:(0,Q.jsx)(pt,{mergedTasks:g,onBack:c,showBackButton:!0})})}),t[36]=c,t[37]=g,t[38]=i,t[39]=C):C=t[39];",
430
+ "let CPX_headerContext={cwd:CPX_readAtom(CPX_headerCwd),hostId:CPX_readAtom(CPX_headerHostId)},CPX_headerAccessories=CPXThreadHeaderAccessories({context:CPX_headerContext,deps:{jsx:Q.jsx,jsxs:Q.jsxs,Tooltip:CPX_Tooltip}});let C;t[36]!==c||t[37]!==g||t[38]!==i||t[68]!==CPX_headerAccessories?(C=(0,Q.jsx)(`div`,{className:`mr-3 line-clamp-1 flex min-w-0 flex-1 items-center gap-1 truncate`,style:{viewTransitionName:`header-title`},children:i?(0,Q.jsxs)(`div`,{className:`flex min-w-0 flex-1 items-center gap-1`,children:[(0,Q.jsx)(mt,{onClick:c}),(0,Q.jsx)(x,{color:`ghostActive`,type:`button`,onClick:u,className:`min-w-0 flex-1 truncate !px-0 !py-0 text-left text-sm text-token-foreground hover:!bg-transparent hover:opacity-80 electron:font-medium`,children:(0,Q.jsx)(`span`,{className:`truncate`,children:i})}),CPX_headerAccessories]}):(0,Q.jsx)(`span`,{className:`text-token-description-foreground`,children:(0,Q.jsx)(pt,{mergedTasks:g,onBack:c,showBackButton:!0})})}),t[36]=c,t[37]=g,t[38]=i,t[68]=CPX_headerAccessories,t[39]=C):C=t[39];",
431
+ "thread header accessory render anchor",
432
+ );
433
+ patched = replaceOnce(
434
+ patched,
435
+ "t[53]!==A||t[54]!==b||t[55]!==S||t[56]!==C?(M=(0,Q.jsxs)(`div`,{className:b,children:[S,C,A]}),t[53]=A,t[54]=b,t[55]=S,t[56]=C,t[57]=M):M=t[57]",
436
+ "t[53]!==A||t[54]!==b||t[55]!==S||t[56]!==C?(M=(0,Q.jsxs)(`div`,{className:b,children:[S,C,A]}),t[53]=A,t[54]=b,t[55]=S,t[56]=C,t[57]=M):M=t[57]",
437
+ "thread header accessory mount anchor",
438
+ );
439
+ return patched;
440
+ }
441
+
442
+ function patchThreadPageHeader(text) {
443
+ let patched = text;
444
+ patched = replaceOnce(
445
+ patched,
446
+ "function c(e){let t=(0,o.c)(21),",
447
+ `${codexPlusThreadHeaderHelpers}function c(e){let t=(0,o.c)(24),`,
448
+ "thread page header helper insertion anchor",
449
+ );
450
+ patched = replaceOnce(
451
+ patched,
452
+ "let t=(0,o.c)(24),{start:c,startActions:l,env:u,secondary:d,trailing:f,hostConfig:p}=e,m;",
453
+ "let t=(0,o.c)(24),{start:c,startActions:l,env:u,secondary:d,trailing:f,hostConfig:p,cwd:CPX_headerCwd}=e,CPX_headerContext={cwd:CPX_headerCwd,hostId:p?.id??null,header:{env:u,hostDisplayName:p?.display_name??null,startText:typeof c==`string`?c:null,secondaryText:typeof d==`string`?d:null,hasTrailing:f!=null}},CPX_headerAccessories=CPXThreadHeaderAccessories({context:CPX_headerContext,deps:{jsx:s.jsx,jsxs:s.jsxs}}),m;",
454
+ "thread page header accessory render anchor",
455
+ );
456
+ patched = replaceOnce(
457
+ patched,
458
+ "t[8]!==l||t[9]!==v||t[10]!==y||t[11]!==b?(x=(0,s.jsxs)(`div`,{className:`text-md flex min-w-0 items-center gap-2 truncate text-base electron:font-medium`,children:[v,y,b,l]}),t[8]=l,t[9]=v,t[10]=y,t[11]=b,t[12]=x):x=t[12]",
459
+ "t[8]!==l||t[9]!==v||t[10]!==y||t[11]!==b||t[21]!==CPX_headerAccessories?(x=(0,s.jsxs)(`div`,{className:`text-md flex min-w-0 items-center gap-2 truncate text-base electron:font-medium`,children:[v,y,b,CPX_headerAccessories,l]}),t[8]=l,t[9]=v,t[10]=y,t[11]=b,t[21]=CPX_headerAccessories,t[12]=x):x=t[12]",
460
+ "thread page header accessory mount anchor",
461
+ );
462
+ return patched;
463
+ }
464
+
465
+ function patchLocalConversationPageHeader(text) {
466
+ let patched = replaceOnce(
467
+ text,
468
+ "function Tt(e){let t=(0,Y.c)(42),",
469
+ `${codexPlusThreadHeaderHelpers}function Tt(e){let t=(0,Y.c)(45),`,
470
+ "local conversation header helper insertion anchor",
471
+ );
472
+ patched = replaceOnce(
473
+ patched,
474
+ "let t=(0,Y.c)(45),{conversationId:n,getConversationMarkdown:r,markdownParentConversationId:a,title:o,titleSuffix:s,cwd:c,canPin:l,hideProjectMetadata:d,hideForkActions:f}=e,p=l===void 0?!0:l,m=d===void 0?!1:d,h=A(),g;",
475
+ "let t=(0,Y.c)(45),{conversationId:n,getConversationMarkdown:r,markdownParentConversationId:a,title:o,titleSuffix:s,cwd:c,canPin:l,hideProjectMetadata:d,hideForkActions:f}=e,CPX_headerContext={cwd:c,hostId:u(i(O,n)).id,header:{surface:`local-conversation`,titleText:typeof o==`string`?o:null}},CPX_headerAccessories=CPXThreadHeaderAccessories({context:CPX_headerContext,deps:{jsx:Z.jsx,jsxs:Z.jsxs}}),p=l===void 0?!0:l,m=d===void 0?!1:d,h=A(),g;",
476
+ "local conversation header accessory render anchor",
477
+ );
478
+ patched = replaceOnce(
479
+ patched,
480
+ "t[38]!==F||t[39]!==I||t[40]!==L?(z=(0,Z.jsx)(`div`,{className:`draggable grid w-full min-w-0 grid-cols-[minmax(0,1fr)] items-center gap-x-4 electron:h-toolbar extension:py-row-y`,children:(0,Z.jsxs)(`div`,{className:`flex min-w-0 items-center gap-2 truncate text-base electron:font-medium`,children:[F,I,L,R]})}),t[38]=F,t[39]=I,t[40]=L,t[41]=z):z=t[41]",
481
+ "t[38]!==F||t[39]!==I||t[40]!==L||t[42]!==CPX_headerAccessories?(z=(0,Z.jsx)(`div`,{className:`draggable grid w-full min-w-0 grid-cols-[minmax(0,1fr)] items-center gap-x-4 electron:h-toolbar extension:py-row-y`,children:(0,Z.jsxs)(`div`,{className:`flex min-w-0 items-center gap-2 truncate text-base electron:font-medium`,children:[F,I,L,CPX_headerAccessories,R]})}),t[38]=F,t[39]=I,t[40]=L,t[42]=CPX_headerAccessories,t[41]=z):z=t[41]",
482
+ "local conversation header accessory mount anchor",
483
+ );
484
+ return patched;
485
+ }
486
+
387
487
  const codexPlusProjectColorHelpers = `
388
488
  function CPXHostProjectRowProps(e){return window.CodexPlus?.ui?.sidebar?.projectRowProps?.({project:e})}function CPXHostThreadRowProps(e){return window.CodexPlus?.ui?.sidebar?.threadRowProps?.({project:e})}function CPXHostUserBubbleProps(e){return window.CodexPlus?.ui?.message?.userBubbleProps?.(e)}function CPXHostComposerSurfaceProps(e){return window.CodexPlus?.ui?.composer?.surfaceProps?.(e)}`;
389
489
 
@@ -497,7 +597,7 @@ function patchElectronMenuShortcuts(text) {
497
597
  return replaceOnce(
498
598
  text,
499
599
  "{id:`toggleSidebar`,titleIntlId:`codex.command.toggleSidebar`,descriptionIntlId:`codex.commandDescription.toggleSidebar`,commandMenuGroupKey:`panels`,commandMenu:!0,electron:{menuTitle:`Toggle Sidebar`,menuTitleIntlId:`codex.commandMenuTitle.toggleSidebar`,defaultKeybindings:[{key:`CmdOrCtrl+B`}]}},{id:`toggleBottomPanel`,",
500
- "{id:`toggleSidebar`,titleIntlId:`codex.command.toggleSidebar`,descriptionIntlId:`codex.commandDescription.toggleSidebar`,commandMenuGroupKey:`panels`,commandMenu:!0,electron:{menuTitle:`Toggle Sidebar`,menuTitleIntlId:`codex.commandMenuTitle.toggleSidebar`,defaultKeybindings:[{key:`CmdOrCtrl+B`}]}},...(window.CodexPlus?.ui?.commands?.commandMetadata?.()??[]),{id:`toggleBottomPanel`,",
600
+ "{id:`toggleSidebar`,titleIntlId:`codex.command.toggleSidebar`,descriptionIntlId:`codex.commandDescription.toggleSidebar`,commandMenuGroupKey:`panels`,commandMenu:!0,electron:{menuTitle:`Toggle Sidebar`,menuTitleIntlId:`codex.commandMenuTitle.toggleSidebar`,defaultKeybindings:[{key:`CmdOrCtrl+B`}]}},{id:`codexPlus.focusProjectSelector`,title:`Focus project selector`,description:`Focus or open the new chat project selector`,commandMenuGroupKey:`workspace`,commandMenu:!0,electron:{menuTitle:`Focus project selector`,defaultKeybindings:[{key:`CmdOrCtrl+.`}]}},...(window.CodexPlus?.ui?.commands?.commandMetadata?.()?.filter?.(e=>e.id!==`codexPlus.focusProjectSelector`)??[]),{id:`toggleBottomPanel`,",
501
601
  "sidebar blur command palette metadata anchor",
502
602
  );
503
603
  }
@@ -583,6 +683,27 @@ function patchMainNativeBridge(text) {
583
683
  );
584
684
  }
585
685
 
686
+ function patchMainMenuDiagnostics(text) {
687
+ let patched = replaceOnce(
688
+ text,
689
+ "He={...b(`toggleSidePanel`),click:async()=>{let e=await y();e&&_.sendMessageToWindow(e,{type:`toggle-diff-panel`})}},Ue=",
690
+ "He={...b(`toggleSidePanel`),click:async()=>{let e=await y();e&&_.sendMessageToWindow(e,{type:`toggle-diff-panel`})}},Ue=",
691
+ "codex plus menu template helper presence anchor",
692
+ );
693
+ patched = replaceOnce(
694
+ patched,
695
+ "He,We,{type:`separator`}",
696
+ "He,We,...CPXNativeMenuTemplateItems(`view-menu`),{type:`separator`}",
697
+ "codex plus view menu template items anchor",
698
+ );
699
+ return replaceOnce(
700
+ patched,
701
+ "me.refreshApplicationMenu(),w(`application menu refreshed`,A),",
702
+ "CPXRefreshApplicationMenu=()=>me.refreshApplicationMenu(),me.refreshApplicationMenu(),CPXLogMenuDiagnostics(),w(`application menu refreshed`,A),",
703
+ "codex plus menu diagnostics refresh anchor",
704
+ );
705
+ }
706
+
586
707
  return makePatchSet({
587
708
  id: config.id,
588
709
  codexVersion: config.codexVersion,
@@ -636,6 +757,14 @@ return makePatchSet({
636
757
  [composerFile, patchComposerProjectColors],
637
758
  ],
638
759
  },
760
+ ...(headerFile ? [{
761
+ id: "project-path-header",
762
+ fileTransforms: [
763
+ [headerFile, patchHeader],
764
+ ...(threadPageHeaderFile ? [[threadPageHeaderFile, patchThreadPageHeader]] : []),
765
+ ...(localConversationPageFile ? [[localConversationPageFile, patchLocalConversationPageHeader]] : []),
766
+ ],
767
+ }] : []),
639
768
  {
640
769
  id: "sidebar-name-blur",
641
770
  fileTransforms: [
@@ -644,11 +773,19 @@ return makePatchSet({
644
773
  [keyboardShortcutsSearchInputFile, patchKeyboardShortcutsSearchInput],
645
774
  ],
646
775
  },
647
- ...(mermaidDiagramShellFile ? [{
776
+ {
777
+ id: "project-selector-shortcut",
778
+ fileTransforms: [
779
+ [localActiveWorkspaceRootDropdownFile, patchLocalActiveWorkspaceRootDropdownProjectSelectorShortcut],
780
+ [runCommandFile, patchRunCommandProjectSelectorShortcut],
781
+ ],
782
+ },
783
+ ...(mainFile ? [{
648
784
  id: "codex-plus-native-bridge",
649
785
  fileTransforms: [
650
786
  [preloadFile, patchPreloadNativeBridge],
651
787
  [mainFile, patchMainNativeBridge],
788
+ ...(electronCommandSourceFile ? [[mainFile, patchMainMenuDiagnostics]] : []),
652
789
  ],
653
790
  }] : []),
654
791
  ...(mermaidDiagramShellFile ? [{
@@ -0,0 +1,54 @@
1
+ const { replaceOnce } = require("./replace");
2
+
3
+ function patchLocalActiveWorkspaceRootDropdownProjectSelectorShortcut(text) {
4
+ let patched = replaceOnce(
5
+ text,
6
+ "Ne=r();function Pe(e){let t=(0,Ne.c)(42),",
7
+ "Ne=r();function CPXProjectSelectorFuzzyFilter(e,t){let n=String(t??``).trim().toLowerCase();return window.CodexPlus?.ui?.projectSelector?.fuzzyFilter?.(e,t)??(n?e.filter(e=>[e.label,e.repositoryData?.rootFolder??``,e.path??``,e.hostDisplayName??``].some(e=>String(e??``).toLowerCase().includes(n))):e)}function CPXProjectSelectorFuzzyHighlight(e,t){return window.CodexPlus?.ui?.projectSelector?.fuzzyHighlight?.({text:e,query:t,jsx:H.jsx})??e}function CPXProjectSelectorAcceptFirst(e,t,n,r){let i=t?.[0];if(e?.key!==`Enter`||String(r??``).trim().length===0||i==null)return;e.preventDefault?.(),e.stopPropagation?.(),n(i.projectId)}function Pe(e){let t=(0,Ne.c)(42),",
8
+ "project selector fuzzy search adapter insertion anchor",
9
+ );
10
+ patched = replaceOnce(
11
+ patched,
12
+ "let e=h.trim().toLowerCase();v=n.filter(t=>{if(!e)return!0;let n=t.repositoryData?.rootFolder??``;return[t.label,n,t.path??``,t.hostDisplayName??``].some(t=>t.toLowerCase().includes(e))});",
13
+ "v=CPXProjectSelectorFuzzyFilter(n,h);",
14
+ "project selector fuzzy search filter anchor",
15
+ );
16
+ patched = replaceOnce(
17
+ patched,
18
+ "S=(0,H.jsx)(ve,{value:h,onChange:o,placeholder:s,className:`mb-1`})",
19
+ "S=(0,H.jsx)(ve,{value:h,onChange:o,onKeyDown:e=>CPXProjectSelectorAcceptFirst(e,v,i,h),placeholder:s,className:`mb-1`})",
20
+ "project selector accept first match keydown anchor",
21
+ );
22
+ patched = replaceOnce(
23
+ patched,
24
+ "(0,H.jsx)(`span`,{className:`truncate`,children:e.label})",
25
+ "(0,H.jsx)(`span`,{className:`truncate`,children:CPXProjectSelectorFuzzyHighlight(e.label,h)})",
26
+ "project selector fuzzy search highlight anchor",
27
+ );
28
+ patched = replaceOnce(
29
+ patched,
30
+ "function Ie(e){let t=(0,Ne.c)(81),",
31
+ "function CPXProjectSelectorTrigger(e,t){return Me.isValidElement(e)?Me.cloneElement(e,{...e.props,\"data-codex-plus-project-selector-trigger\":!0,\"data-codex-plus-project-selector-variant\":t}):e}function Ie(e){let t=(0,Ne.c)(81),",
32
+ "project selector shortcut helper insertion anchor",
33
+ );
34
+ return replaceOnce(
35
+ patched,
36
+ "at=(0,H.jsx)(ye,{open:f,onOpenChange:g,onCloseAutoFocus:Y,align:tt,disabled:O,triggerButton:X,contentWidth:`workspace`,contentMaxHeight:`tall`,children:$})",
37
+ "at=(0,H.jsx)(ye,{open:f,onOpenChange:g,onCloseAutoFocus:Y,align:tt,disabled:O,triggerButton:CPXProjectSelectorTrigger(X,k),contentWidth:`workspace`,contentMaxHeight:`tall`,children:$})",
38
+ "project selector shortcut final dropdown trigger anchor",
39
+ );
40
+ }
41
+
42
+ function patchRunCommandProjectSelectorShortcut(text) {
43
+ return replaceOnce(
44
+ text,
45
+ "],[`openFolder`,()=>{r()}],[`toggleSidebar`,",
46
+ "],[`codexPlus.focusProjectSelector`,()=>{window.CodexPlus?.commands?.run?.(`codexPlus.focusProjectSelector`)}],[`openFolder`,()=>{r()}],[`toggleSidebar`,",
47
+ "project selector shortcut command dispatch anchor",
48
+ );
49
+ }
50
+
51
+ module.exports = {
52
+ patchLocalActiveWorkspaceRootDropdownProjectSelectorShortcut,
53
+ patchRunCommandProjectSelectorShortcut,
54
+ };
@@ -11,7 +11,11 @@ const runtimeFiles = [
11
11
  ["webview/assets/codex-plus/plugins/diagnosticErrors.js", "plugins/diagnosticErrors.js"],
12
12
  ["webview/assets/codex-plus/plugins/userBubbleColors.js", "plugins/userBubbleColors.js"],
13
13
  ["webview/assets/codex-plus/plugins/projectColors.js", "plugins/projectColors.js"],
14
+ ["webview/assets/codex-plus/plugins/projectPathHeader.js", "plugins/projectPathHeader.js"],
14
15
  ["webview/assets/codex-plus/plugins/sidebarNameBlur.js", "plugins/sidebarNameBlur.js"],
16
+ ["webview/assets/codex-plus/plugins/devTools.js", "plugins/devTools.js"],
17
+ ["webview/assets/codex-plus/vendor/fzf.umd.js", "../../node_modules/fzf/dist/fzf.umd.js"],
18
+ ["webview/assets/codex-plus/plugins/projectSelectorShortcut.js", "plugins/projectSelectorShortcut.js"],
15
19
  ["webview/assets/codex-plus/plugins/mermaidFullscreen.js", "plugins/mermaidFullscreen.js"],
16
20
  ];
17
21
 
@@ -0,0 +1,33 @@
1
+ (function () {
2
+ const CodexPlus = window.CodexPlus;
3
+ CodexPlus.registerPlugin(
4
+ CodexPlus.definePlugin({
5
+ id: "devTools",
6
+ name: "Developer Tools",
7
+ description: "Registers the Open Developer Tools command.",
8
+ required: true,
9
+ commands: [
10
+ {
11
+ id: "codexPlusOpenDevTools",
12
+ title: "Open Developer Tools",
13
+ description: "Open DevTools for the current Codex window",
14
+ menu: { groups: ["panels"] },
15
+ palette: { enabled: true, keywords: ["devtools", "developer", "console"] },
16
+ shortcut: { defaultKeybindings: [] },
17
+ run() {
18
+ return CodexPlus.native.request("devtools/open").catch(() => ({ ok: false }));
19
+ },
20
+ },
21
+ ],
22
+ start(api) {
23
+ api.nativeMenus.registerItem({
24
+ id: "codexPlusOpenDevTools",
25
+ menuId: "view-menu",
26
+ afterLabel: "Find",
27
+ label: "Open Developer Tools",
28
+ nativeRequest: { method: "devtools/open" },
29
+ });
30
+ },
31
+ }),
32
+ );
33
+ })();
@@ -0,0 +1,116 @@
1
+ (function (globalObject) {
2
+ function pathFromContext(context) {
3
+ const value = context?.cwd ?? context?.project?.cwd ?? context?.project?.path ?? null;
4
+ if (typeof value !== "string") return "";
5
+ return value.trim();
6
+ }
7
+
8
+ function middleTruncate(value, maxLength = 46) {
9
+ const text = String(value || "");
10
+ if (text.length <= maxLength) return text;
11
+ if (maxLength <= 5) return text.slice(0, maxLength);
12
+ const keep = maxLength - 3;
13
+ const start = Math.ceil(keep / 2);
14
+ const end = Math.floor(keep / 2);
15
+ return `${text.slice(0, start)}...${text.slice(text.length - end)}`;
16
+ }
17
+
18
+ function formatPathLabel(value, maxLength = 54, tailSegments = 3) {
19
+ const text = String(value || "");
20
+ if (text.length <= maxLength) return text;
21
+ const parts = text.split("/").filter(Boolean);
22
+ const tail = parts.slice(-tailSegments).join("/");
23
+ if (!tail) return middleTruncate(text, maxLength);
24
+ const prefix = "…";
25
+ const label = `${prefix}/${tail}`;
26
+ return label.length <= maxLength ? label : `${prefix}/${parts.slice(-2).join("/")}`;
27
+ }
28
+
29
+ function copyPath(path) {
30
+ return globalObject?.navigator?.clipboard?.writeText?.(path);
31
+ }
32
+
33
+ function diagnose(event, details) {
34
+ globalObject?.CodexPlus?.diagnostics?.log?.(`projectPathHeader.${event}`, details);
35
+ }
36
+
37
+ function ProjectPathAccessory({ context, jsx, jsxs, Tooltip }) {
38
+ const path = pathFromContext(context);
39
+ if (!path) {
40
+ diagnose("render.skip", {
41
+ reason: "missing-cwd",
42
+ contextKeys: context && typeof context === "object" ? Object.keys(context) : [],
43
+ });
44
+ return null;
45
+ }
46
+ const label = formatPathLabel(path);
47
+ diagnose("render.chip", { path, label, tooltip: typeof Tooltip === "function" });
48
+ const chip = jsxs("div", {
49
+ "data-codex-plus-project-path-header": "",
50
+ className:
51
+ "no-drag ml-1 flex min-w-0 items-center gap-1 overflow-hidden rounded border border-token-border px-1.5 py-0.5 text-xs text-token-description-foreground",
52
+ style: { flexShrink: 999, maxWidth: "min(24rem, 28vw)" },
53
+ title: path,
54
+ children: [
55
+ jsx("span", { className: "min-w-0 truncate", children: label }),
56
+ jsx("button", {
57
+ type: "button",
58
+ className:
59
+ "flex h-4 w-4 shrink-0 items-center justify-center rounded text-token-input-placeholder-foreground hover:bg-token-list-hover-background hover:text-token-foreground",
60
+ "aria-label": "Copy project path",
61
+ title: "Copy project path",
62
+ onClick(event) {
63
+ event?.preventDefault?.();
64
+ event?.stopPropagation?.();
65
+ copyPath(path);
66
+ },
67
+ children: jsx("svg", {
68
+ "aria-hidden": "true",
69
+ className: "h-3 w-3",
70
+ fill: "none",
71
+ stroke: "currentColor",
72
+ strokeLinecap: "round",
73
+ strokeLinejoin: "round",
74
+ strokeWidth: "2",
75
+ viewBox: "0 0 24 24",
76
+ children: [
77
+ jsx("rect", { x: "9", y: "9", width: "13", height: "13", rx: "2", ry: "2" }),
78
+ jsx("path", { d: "M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1" }),
79
+ ],
80
+ }),
81
+ }),
82
+ ],
83
+ });
84
+ if (typeof Tooltip === "function") return jsx(Tooltip, { tooltipContent: path, children: chip });
85
+ return chip;
86
+ }
87
+
88
+ const exportsObject = {
89
+ ProjectPathAccessory,
90
+ copyPath,
91
+ formatPathLabel,
92
+ middleTruncate,
93
+ pathFromContext,
94
+ };
95
+
96
+ if (typeof module !== "undefined" && module.exports) {
97
+ module.exports = exportsObject;
98
+ }
99
+
100
+ const CodexPlus = globalObject?.CodexPlus;
101
+ if (!CodexPlus) return;
102
+
103
+ CodexPlus.registerPlugin(
104
+ CodexPlus.definePlugin({
105
+ id: "projectPathHeader",
106
+ name: "Project Path Header",
107
+ description: "Shows the active project path in the thread header.",
108
+ required: true,
109
+ exports: exportsObject,
110
+ start(api) {
111
+ diagnose("start", { hasThreadHeader: Boolean(api.ui.threadHeader) });
112
+ api.ui.threadHeader.addAccessory(ProjectPathAccessory);
113
+ },
114
+ }),
115
+ );
116
+ })(typeof window !== "undefined" ? window : globalThis);
@@ -0,0 +1,207 @@
1
+ (function () {
2
+ const CodexPlus = window.CodexPlus;
3
+ const triggerSelector = "[data-codex-plus-project-selector-trigger]";
4
+ let keydownHandler = null;
5
+
6
+ function normalizeForFzf(value) {
7
+ const source = String(value ?? "");
8
+ const map = [];
9
+ let text = "";
10
+ let inSeparator = false;
11
+
12
+ for (let index = 0; index < source.length; index += 1) {
13
+ const char = source[index];
14
+ if (/[\s-]/.test(char)) {
15
+ if (!inSeparator) {
16
+ text += " ";
17
+ map.push(index);
18
+ inSeparator = true;
19
+ }
20
+ continue;
21
+ }
22
+ text += char;
23
+ map.push(index);
24
+ inSeparator = false;
25
+ }
26
+
27
+ return { map, text };
28
+ }
29
+
30
+ function projectSearchText(project) {
31
+ return [
32
+ project?.label,
33
+ project?.repositoryData?.rootFolder,
34
+ project?.path,
35
+ project?.hostDisplayName,
36
+ ].map((value) => normalizeForFzf(value).text.trim()).filter(Boolean).join(" ");
37
+ }
38
+
39
+ function fzfConstructor() {
40
+ return window.fzf?.Fzf;
41
+ }
42
+
43
+ function fallbackFilter(items, query) {
44
+ const needle = normalizeForFzf(query).text.trim().toLowerCase();
45
+ if (!needle) return items;
46
+ return items.filter((item) => projectSearchText(item).toLowerCase().includes(needle));
47
+ }
48
+
49
+ function fuzzyFilter(items, query) {
50
+ const list = Array.isArray(items) ? items : [];
51
+ const normalizedQuery = normalizeForFzf(query).text.trim();
52
+ if (!normalizedQuery) return list;
53
+
54
+ const Fzf = fzfConstructor();
55
+ if (typeof Fzf !== "function") return fallbackFilter(list, query);
56
+
57
+ return new Fzf(
58
+ list.map((project) => ({ project, searchText: projectSearchText(project) })),
59
+ { selector: (entry) => entry.searchText },
60
+ ).find(normalizedQuery).map((entry) => entry.item.project);
61
+ }
62
+
63
+ function labelPositions(text, query) {
64
+ const Fzf = fzfConstructor();
65
+ if (typeof Fzf !== "function") return null;
66
+
67
+ const normalizedText = normalizeForFzf(text);
68
+ const normalizedQuery = normalizeForFzf(query).text.trim();
69
+ if (!normalizedText.text || !normalizedQuery) return null;
70
+
71
+ const [entry] = new Fzf([normalizedText.text]).find(normalizedQuery);
72
+ if (!entry || typeof entry.positions?.forEach !== "function") return null;
73
+
74
+ const positions = [];
75
+ entry.positions.forEach((index) => positions.push(index));
76
+ return positions.map((index) => normalizedText.map[index]).filter((index) => Number.isInteger(index));
77
+ }
78
+
79
+ function fuzzyHighlight({ text, query, jsx }) {
80
+ if (typeof jsx !== "function") return text;
81
+
82
+ const positions = labelPositions(text, query);
83
+ if (positions == null || positions.length === 0) return text;
84
+
85
+ const matchedIndices = new Set(positions);
86
+ const parts = [];
87
+ let index = 0;
88
+ let key = 0;
89
+ const style = {
90
+ color: "var(--color-token-text-link-foreground, #2563eb)",
91
+ };
92
+
93
+ while (index < text.length) {
94
+ const isMatched = matchedIndices.has(index);
95
+ const start = index;
96
+ while (index < text.length && matchedIndices.has(index) === isMatched) index += 1;
97
+
98
+ const value = text.slice(start, index);
99
+ parts.push(
100
+ isMatched
101
+ ? jsx("strong", {
102
+ className: "font-semibold",
103
+ style,
104
+ children: value,
105
+ }, key++)
106
+ : value,
107
+ );
108
+ }
109
+
110
+ return parts;
111
+ }
112
+
113
+ function dispatchMouseEvent(target, type) {
114
+ if (typeof target.dispatchEvent !== "function") return false;
115
+ const EventConstructor = type === "pointerdown"
116
+ ? window.PointerEvent || window.MouseEvent
117
+ : window.MouseEvent;
118
+ if (typeof EventConstructor !== "function") return false;
119
+ target.dispatchEvent(new EventConstructor(type, {
120
+ bubbles: true,
121
+ button: 0,
122
+ buttons: type === "pointerdown" || type === "mousedown" ? 1 : 0,
123
+ cancelable: true,
124
+ ctrlKey: false,
125
+ view: window,
126
+ }));
127
+ return true;
128
+ }
129
+
130
+ function visibleTriggerCandidates() {
131
+ return Array.from(document.querySelectorAll(triggerSelector)).filter((trigger) => {
132
+ if (!(trigger instanceof HTMLElement)) return false;
133
+ if (trigger.disabled || trigger.getAttribute("aria-disabled") === "true") return false;
134
+ const rect = trigger.getBoundingClientRect?.();
135
+ return rect && rect.width > 0 && rect.height > 0;
136
+ });
137
+ }
138
+
139
+ function triggerPriority(trigger) {
140
+ const variant = trigger.getAttribute("data-codex-plus-project-selector-variant");
141
+ if (variant === "default") return 0;
142
+ if (variant == null || variant === "") return 1;
143
+ return 2;
144
+ }
145
+
146
+ function projectSelectorTrigger() {
147
+ const [trigger] = visibleTriggerCandidates().sort((left, right) => {
148
+ const priority = triggerPriority(left) - triggerPriority(right);
149
+ if (priority !== 0) return priority;
150
+ const leftRect = left.getBoundingClientRect();
151
+ const rightRect = right.getBoundingClientRect();
152
+ return rightRect.top - leftRect.top || rightRect.left - leftRect.left;
153
+ });
154
+ return trigger ?? null;
155
+ }
156
+
157
+ function focusProjectSelector() {
158
+ const trigger = projectSelectorTrigger();
159
+ if (trigger == null) return false;
160
+ trigger.focus?.();
161
+ const dispatched = [
162
+ dispatchMouseEvent(trigger, "pointerdown"),
163
+ dispatchMouseEvent(trigger, "mousedown"),
164
+ dispatchMouseEvent(trigger, "mouseup"),
165
+ dispatchMouseEvent(trigger, "click"),
166
+ ].some(Boolean);
167
+ if (!dispatched) trigger.click?.();
168
+ return true;
169
+ }
170
+
171
+ CodexPlus.registerPlugin(
172
+ CodexPlus.definePlugin({
173
+ id: "projectSelectorShortcut",
174
+ name: "Project Selector Shortcut",
175
+ description: "Registers the Focus project selector command.",
176
+ required: true,
177
+ commands: [
178
+ {
179
+ id: "codexPlus.focusProjectSelector",
180
+ title: "Focus project selector",
181
+ description: "Focus or open the new chat project selector",
182
+ menu: { groups: ["suggested", "workspace"] },
183
+ palette: { enabled: true, keywords: ["project", "selector", "new chat"] },
184
+ shortcut: { defaultKeybindings: [{ key: "CmdOrCtrl+." }] },
185
+ run: focusProjectSelector,
186
+ },
187
+ ],
188
+ start(api) {
189
+ api.ui.projectSelector = {
190
+ fuzzyFilter,
191
+ fuzzyHighlight,
192
+ };
193
+ keydownHandler = (event) => {
194
+ if (event.defaultPrevented || event.key !== "." || (!event.metaKey && !event.ctrlKey) || event.altKey || event.shiftKey) {
195
+ return;
196
+ }
197
+ if (api.commands.run("codexPlus.focusProjectSelector")) event.preventDefault();
198
+ };
199
+ document.addEventListener("keydown", keydownHandler, true);
200
+ },
201
+ stop() {
202
+ if (keydownHandler) document.removeEventListener("keydown", keydownHandler, true);
203
+ keydownHandler = null;
204
+ },
205
+ }),
206
+ );
207
+ })();
@@ -9,6 +9,27 @@
9
9
  const styleElements = new Map();
10
10
  const settingsListeners = new Map();
11
11
  const storagePrefix = "codex-plus:plugin:";
12
+ const diagnosticEvents = [];
13
+
14
+ function diagnosticsEnabled() {
15
+ try {
16
+ return globalObject.localStorage?.getItem("codex-plus:diagnostics") === "1";
17
+ } catch {
18
+ return false;
19
+ }
20
+ }
21
+
22
+ function diagnose(event, details = {}) {
23
+ const entry = { event, details, time: new Date().toISOString() };
24
+ diagnosticEvents.push(entry);
25
+ if (diagnosticEvents.length > 200) diagnosticEvents.shift();
26
+ if (diagnosticsEnabled()) {
27
+ try {
28
+ console.info("[Codex Plus]", event, details);
29
+ } catch {}
30
+ }
31
+ return entry;
32
+ }
12
33
 
13
34
  function safeId(id) {
14
35
  if (typeof id !== "string" || id.trim() === "") throw new Error("Codex Plus plugin ids must be non-empty strings");
@@ -258,6 +279,34 @@
258
279
  return null;
259
280
  }
260
281
 
282
+ function ThreadHeaderAccessoryHost({ accessory, context, deps }) {
283
+ const rendered = accessory?.({ context, ...deps }) ?? null;
284
+ diagnose("threadHeader.accessoryHost.render", {
285
+ accessoryName: accessory?.name || null,
286
+ cwd: typeof context?.cwd === "string" ? context.cwd : null,
287
+ rendered: rendered != null,
288
+ });
289
+ return rendered;
290
+ }
291
+
292
+ function renderThreadHeaderAccessories({ context, deps } = {}) {
293
+ const jsx = deps?.jsx;
294
+ if (typeof jsx !== "function") {
295
+ diagnose("threadHeader.render.skip", { reason: "missing-jsx" });
296
+ return null;
297
+ }
298
+ diagnose("threadHeader.render", {
299
+ accessoryCount: CodexPlus.ui.threadHeader.accessories.length,
300
+ cwd: typeof context?.cwd === "string" ? context.cwd : null,
301
+ hostId: context?.hostId ?? null,
302
+ header: context?.header ?? null,
303
+ });
304
+ const rendered = CodexPlus.ui.threadHeader.accessories.map((accessory, index) =>
305
+ jsx(ThreadHeaderAccessoryHost, { accessory, context, deps }, `thread-header-accessory:${index}`),
306
+ );
307
+ return rendered.length === 0 ? null : rendered;
308
+ }
309
+
261
310
  function registerStyle(pluginId, cssText) {
262
311
  if (typeof document === "undefined") return null;
263
312
  const id = `codex-plus-style-${safeId(pluginId)}`;
@@ -288,6 +337,7 @@
288
337
  for (const command of plugin.commands || []) registerCommand({ ...command, plugin: plugin.id });
289
338
  if (plugin.styles) registerStyle(plugin.id, plugin.styles);
290
339
  if (plugin.required || plugin.enabledByDefault) startPlugin(plugin.id);
340
+ diagnose("plugin.register", { id: plugin.id, started: startedPlugins.has(plugin.id) });
291
341
  return plugin;
292
342
  }
293
343
 
@@ -296,6 +346,7 @@
296
346
  if (!plugin || startedPlugins.has(id)) return;
297
347
  plugin.start?.(CodexPlus);
298
348
  startedPlugins.add(id);
349
+ diagnose("plugin.start", { id });
299
350
  }
300
351
 
301
352
  function stopPlugin(id) {
@@ -305,6 +356,10 @@
305
356
  startedPlugins.delete(id);
306
357
  }
307
358
 
359
+ function registerNativeMenuItem(item) {
360
+ return CodexPlus.native.request("native-menu/register-item", item).catch(() => ({ ok: false }));
361
+ }
362
+
308
363
  const CodexPlus = {
309
364
  definePlugin,
310
365
  registerPlugin,
@@ -330,6 +385,15 @@
330
385
  composer: { surfaceDecorators: [], decorateSurface(fn) { this.surfaceDecorators.push(fn); return fn; }, surfaceProps(props) { return applyDecorators(props, this.surfaceDecorators); } },
331
386
  about: { buildInfo: [], addBuildInfo(fn) { this.buildInfo.push(fn); return fn; } },
332
387
  errors: { boundaryDecorators: [], decorateBoundary(fn) { this.boundaryDecorators.push(fn); return fn; }, renderDetails: renderErrorDetails },
388
+ threadHeader: {
389
+ accessories: [],
390
+ addAccessory(fn) {
391
+ this.accessories.push(fn);
392
+ diagnose("threadHeader.addAccessory", { accessoryName: fn?.name || null, accessoryCount: this.accessories.length });
393
+ return fn;
394
+ },
395
+ renderAccessories: renderThreadHeaderAccessories,
396
+ },
333
397
  mermaid: {
334
398
  diagramDecorators: [],
335
399
  decorateDiagram(fn) { this.diagramDecorators.push(fn); return fn; },
@@ -337,12 +401,20 @@
337
401
  },
338
402
  },
339
403
  commands: { register: registerCommand, run: runCommand, all: () => Array.from(commands.values()), menuItems: (group) => Array.from(commands.values()).filter((command) => commandGroups(command).includes(group)) },
404
+ nativeMenus: { registerItem: registerNativeMenuItem },
340
405
  settings: { define: defineSettings },
341
406
  native: { async request(method, params) { return globalObject.codexPlusNative?.request?.(method, params) ?? globalObject.CodexPlusHost?.nativeRequest?.(method, params); } },
342
407
  styles: { register: registerStyle, setRootVars },
408
+ diagnostics: {
409
+ log: diagnose,
410
+ snapshot() { return diagnosticEvents.slice(); },
411
+ clear() { diagnosticEvents.splice(0, diagnosticEvents.length); },
412
+ enabled: diagnosticsEnabled,
413
+ },
343
414
  };
344
415
 
345
416
  globalObject.CodexPlus = CodexPlus;
417
+ globalObject.CodexPlusDiagnostics = CodexPlus.diagnostics;
346
418
  globalObject.CodexPlusHost ||= {};
347
419
  globalObject.CodexPlusHost.register = registerHostModule;
348
420
 
@@ -352,18 +424,31 @@
352
424
  "plugins/diagnosticErrors.js",
353
425
  "plugins/userBubbleColors.js",
354
426
  "plugins/projectColors.js",
427
+ "plugins/projectPathHeader.js",
355
428
  "plugins/sidebarNameBlur.js",
429
+ "plugins/devTools.js",
430
+ "vendor/fzf.umd.js",
431
+ "plugins/projectSelectorShortcut.js",
356
432
  "plugins/mermaidFullscreen.js",
357
433
  ];
358
434
 
359
435
  if (typeof document !== "undefined") {
360
436
  const base = new URL(".", document.currentScript?.src || globalObject.location?.href || "");
361
- for (const file of pluginFiles) {
362
- const script = document.createElement("script");
363
- script.src = new URL(file, base).href;
364
- script.async = false;
365
- script.defer = false;
366
- document.head?.appendChild(script);
437
+ if (document.readyState === "loading" && typeof document.write === "function") {
438
+ diagnose("plugins.load", { mode: "document.write", count: pluginFiles.length });
439
+ for (const file of pluginFiles) {
440
+ const src = new URL(file, base).href.replace(/"/g, "&quot;");
441
+ document.write(`<script src="${src}"><\/script>`);
442
+ }
443
+ } else {
444
+ diagnose("plugins.load", { mode: "appendChild", count: pluginFiles.length });
445
+ for (const file of pluginFiles) {
446
+ const script = document.createElement("script");
447
+ script.src = new URL(file, base).href;
448
+ script.async = false;
449
+ script.defer = false;
450
+ document.head?.appendChild(script);
451
+ }
367
452
  }
368
453
  }
369
454
  })();