codex-plus-patcher 0.7.0 → 0.7.1

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.
@@ -1,5 +1,5 @@
1
1
  function messageComposerHook() {
2
- return "let CPXMC=window.CodexPlusHost.adapters.messageComposer,CPXBubbleProps=e=>CPXMC.userBubbleProps(e),CPXSurfaceProps=e=>CPXMC.composerSurfaceProps(e);";
2
+ return "var CPXMC=window.CodexPlusHost.adapters.messageComposer,CPXBubbleProps=e=>CPXMC.userBubbleProps(e),CPXSurfaceProps=e=>CPXMC.composerSurfaceProps(e);";
3
3
  }
4
4
 
5
5
  module.exports = {
@@ -2,8 +2,8 @@ function projectSelectorSearchHook() {
2
2
  return "let CPXP=window.CodexPlusHost.adapters.projectSelector;";
3
3
  }
4
4
 
5
- function projectSelectorTriggerHook() {
6
- return "function CPXPST(e,t){return CPXP.trigger(e,t,Me)}";
5
+ function projectSelectorTriggerHook(reactIdentifier = "Me") {
6
+ return `function CPXPST(e,t){return CPXP.trigger(e,t,${reactIdentifier})}`;
7
7
  }
8
8
 
9
9
  module.exports = {
@@ -1,5 +1,7 @@
1
- function reviewHook() {
2
- return "let CPXR=window.CodexPlusHost.adapters.review,CPXRM=e=>CPXR.renderBodyFromHost(e,[$,Q,s,l,ft,Or,Dr,kr,jr,y,B,of,Y,Ae,Je,yi,vi,CPXBranchPickerDropdownContent,dp,xr,Ma]);";
1
+ function reviewHook(depsExpression) {
2
+ const deps =
3
+ depsExpression || "[$,Q,s,l,ft,Or,Dr,kr,jr,y,B,of,Y,Ae,Je,yi,vi,CPXBranchPickerDropdownContent,dp,xr,Ma]";
4
+ return `let CPXR=window.CodexPlusHost.adapters.review,CPXRM=e=>CPXR.renderBodyFromHost(e,${deps});`;
3
5
  }
4
6
 
5
7
  module.exports = {
@@ -1,5 +1,6 @@
1
- function appearanceSettingsHook() {
2
- return "function CPXAppearanceRows(e){return window.CodexPlus?.ui?.settings?.appearance?.renderRows?.({deps:{React:X,jsx:Z.jsx,SettingRow:J,ColorInput:sn,Switch:q},variant:e})??[]}";
1
+ function appearanceSettingsHook(depsExpression) {
2
+ const deps = depsExpression || "{React:X,jsx:Z.jsx,SettingRow:J,ColorInput:sn,Switch:q}";
3
+ return `function CPXAppearanceRows(e){return window.CodexPlus?.ui?.settings?.appearance?.renderRows?.({deps:${deps},variant:e})??[]}`;
3
4
  }
4
5
 
5
6
  function commandMenuItemsExpression(group, jsx, menuItem, register) {
@@ -1,12 +1,7 @@
1
1
  function projectColorHook() {
2
- return "let CPXS=window.CodexPlusHost.adapters.sidebar,CPXPR=e=>CPXS.projectRowProps(e),CPXTR=e=>CPXS.threadRowProps(e);";
3
- }
4
-
5
- function sidebarMergeDataAttributes(baseExpression, extraExpression) {
6
- return `window.CodexPlusHost.adapters.sidebar.mergeThreadRowAttributes(${baseExpression},${extraExpression})`;
2
+ return "var CPXS=window.CodexPlusHost.adapters.sidebar,CPXPR=e=>CPXS.projectRowProps(e);";
7
3
  }
8
4
 
9
5
  module.exports = {
10
6
  projectColorHook,
11
- sidebarMergeDataAttributes,
12
7
  };
@@ -2,6 +2,14 @@ const { replaceOnce } = require("./replace");
2
2
  const { projectSelectorSearchHook, projectSelectorTriggerHook } = require("./hooks/project-selector");
3
3
 
4
4
  function patchLocalActiveWorkspaceRootDropdownProjectSelectorShortcut(text) {
5
+ if (text.includes("function Ti(e){let t=(0,Oi.c)(109),")) {
6
+ return replaceOnce(
7
+ text,
8
+ "H=(0,X.jsx)(`button`,{type:`button`,className:`flex min-w-0 items-center gap-1.5 rounded-lg bg-token-foreground/5 px-2 py-0.5 text-base leading-6 font-medium tracking-[-0.13px] text-token-foreground`,disabled:M,children:de})",
9
+ "H=(0,X.jsx)(`button`,{type:`button`,\"data-codex-plus-project-selector-trigger\":!0,\"data-codex-plus-project-selector-variant\":`default`,className:`flex min-w-0 items-center gap-1.5 rounded-lg bg-token-foreground/5 px-2 py-0.5 text-base leading-6 font-medium tracking-[-0.13px] text-token-foreground`,disabled:M,children:de})",
10
+ "project selector shortcut final dropdown trigger anchor",
11
+ );
12
+ }
5
13
  let patched = replaceOnce(
6
14
  text,
7
15
  "Ne=r();function Pe(e){let t=(0,Ne.c)(42),",
@@ -40,16 +48,83 @@ function patchLocalActiveWorkspaceRootDropdownProjectSelectorShortcut(text) {
40
48
  );
41
49
  }
42
50
 
51
+ function patchHomeProjectDropdownProjectSelectorShortcut(text) {
52
+ let patched = replaceOnce(
53
+ text,
54
+ "function St({activeProjectIdOverride:e,allowLocalProjects:t=!0,allowLocalProjectActions:n=t,allowRemoteProjects:r=!0,disabled:a=!1,hideLabel:o=!1,onWorkspaceRootSelected:s,variant:c=`default`,isOpen:l,onOpenChange:m,triggerButton:_}){",
55
+ `${projectSelectorSearchHook()}${projectSelectorTriggerHook("wt")}function St({activeProjectIdOverride:e,allowLocalProjects:t=!0,allowLocalProjectActions:n=t,allowRemoteProjects:r=!0,disabled:a=!1,hideLabel:o=!1,onWorkspaceRootSelected:s,variant:c=\`default\`,isOpen:l,onOpenChange:m,triggerButton:_}){`,
56
+ "home project selector shortcut helper insertion anchor",
57
+ );
58
+ patched = replaceOnce(
59
+ patched,
60
+ "let e=_.trim().toLowerCase();b=r.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))});",
61
+ "b=CPXP.fuzzyFilter(r,_);",
62
+ "home project selector fuzzy search filter anchor",
63
+ );
64
+ patched = replaceOnce(
65
+ patched,
66
+ "w=(0,X.jsx)(ie,{value:_,onChange:s,placeholder:c,className:`mb-1`})",
67
+ "w=(0,X.jsx)(ie,{value:_,onChange:s,onKeyDown:e=>CPXP.acceptFirst(e,b,o,_),placeholder:c,className:`mb-1`})",
68
+ "home project selector accept first match keydown anchor",
69
+ );
70
+ patched = replaceOnce(
71
+ patched,
72
+ "(0,X.jsx)(`span`,{className:`truncate`,children:e.label})",
73
+ "(0,X.jsx)(`span`,{className:`truncate`,children:CPXP.fuzzyHighlight(e.label,_,X.jsx)})",
74
+ "home project selector fuzzy search highlight anchor",
75
+ );
76
+ patched = replaceOnce(
77
+ patched,
78
+ "children:(0,$.jsxs)(Ne,{size:`composerSm`,color:`ghost`,className:`min-w-0`,children:",
79
+ "children:(0,$.jsxs)(Ne,{\"data-codex-plus-project-selector-trigger\":!0,\"data-codex-plus-project-selector-variant\":c,size:`composerSm`,color:`ghost`,className:`min-w-0`,children:",
80
+ "home project selector default button marker anchor",
81
+ );
82
+ patched = replaceOnce(
83
+ patched,
84
+ "children:(0,$.jsx)(gt,{categoryLabel:(0,$.jsx)(R,{id:`composer.localCwdDropdown.footerCategory`",
85
+ "children:(0,$.jsx)(gt,{\"data-codex-plus-project-selector-trigger\":!0,\"data-codex-plus-project-selector-variant\":c,categoryLabel:(0,$.jsx)(R,{id:`composer.localCwdDropdown.footerCategory`",
86
+ "home project selector footer button marker anchor",
87
+ );
88
+ patched = replaceOnce(
89
+ patched,
90
+ "Ze=()=>(0,$.jsxs)(`button`,{className:W(`heading-xl text-token-text-tertiary",
91
+ "Ze=()=>(0,$.jsxs)(`button`,{\"data-codex-plus-project-selector-trigger\":!0,\"data-codex-plus-project-selector-variant\":c,className:W(`heading-xl text-token-text-tertiary",
92
+ "home project selector hero button marker anchor",
93
+ );
94
+ patched = replaceOnce(
95
+ patched,
96
+ "triggerButton:_??J(),contentWidth:`menu`",
97
+ "triggerButton:CPXPST(_??J(),c),contentWidth:`menu`",
98
+ "home project selector empty trigger anchor",
99
+ );
100
+ return replaceOnce(
101
+ patched,
102
+ "triggerButton:_??(c===`hero`?Ze():c===`home`?J():Ke()),contentWidth:`workspace`",
103
+ "triggerButton:CPXPST(_??(c===`hero`?Ze():c===`home`?J():Ke()),c),contentWidth:`workspace`",
104
+ "home project selector workspace trigger anchor",
105
+ );
106
+ }
107
+
43
108
  function patchRunCommandProjectSelectorShortcut(text) {
109
+ const runtimeCommandEntries = "...(window.CodexPlus?.commands?.all?.()??[]).map(e=>[e.id,()=>window.CodexPlus?.commands?.run?.(e.id)])";
110
+ if (text.includes("],[`openFolder`,GTt],[`toggleSidebar`,")) {
111
+ return replaceOnce(
112
+ text,
113
+ "],[`openFolder`,GTt],[`toggleSidebar`,",
114
+ `],[\`openFolder\`,GTt],${runtimeCommandEntries},[\`toggleSidebar\`,`,
115
+ "codex plus runtime command dispatch anchor",
116
+ );
117
+ }
44
118
  return replaceOnce(
45
119
  text,
46
120
  "],[`openFolder`,()=>{r()}],[`toggleSidebar`,",
47
- "],[`codexPlus.focusProjectSelector`,()=>{window.CodexPlus?.commands?.run?.(`codexPlus.focusProjectSelector`)}],[`openFolder`,()=>{r()}],[`toggleSidebar`,",
48
- "project selector shortcut command dispatch anchor",
121
+ `],[\`openFolder\`,()=>{r()}],${runtimeCommandEntries},[\`toggleSidebar\`,`,
122
+ "codex plus runtime command dispatch anchor",
49
123
  );
50
124
  }
51
125
 
52
126
  module.exports = {
127
+ patchHomeProjectDropdownProjectSelectorShortcut,
53
128
  patchLocalActiveWorkspaceRootDropdownProjectSelectorShortcut,
54
129
  patchRunCommandProjectSelectorShortcut,
55
130
  };
@@ -62,7 +62,10 @@
62
62
  return result;
63
63
  }
64
64
 
65
+ core.plugins.list = () => Array.from(core.plugins.values());
66
+
65
67
  const CodexPlus = {
68
+ config: globalObject.__CodexPlusRuntimeConfig || {},
66
69
  definePlugin,
67
70
  registerPlugin,
68
71
  startPlugin,
@@ -55,13 +55,13 @@ const runtimeFiles = [
55
55
  ...browserRuntimeAssets,
56
56
  ];
57
57
 
58
- function browserRuntimeManifest() {
59
- return `window.__CodexPlusRuntimeFiles=${JSON.stringify(browserRuntimeFiles)};window.__CodexPlusLoadRuntimeFiles?.(window.__CodexPlusRuntimeFiles);\n`;
58
+ function browserRuntimeManifest(config = {}) {
59
+ return `window.__CodexPlusRuntimeConfig=${JSON.stringify(config)};window.__CodexPlusRuntimeFiles=${JSON.stringify(browserRuntimeFiles)};window.__CodexPlusLoadRuntimeFiles?.(window.__CodexPlusRuntimeFiles);\n`;
60
60
  }
61
61
 
62
- function codexPlusRuntimeAssets() {
62
+ function codexPlusRuntimeAssets(config = {}) {
63
63
  return runtimeFiles.map(([asarPath, localPath]) => {
64
- const content = localPath == null ? browserRuntimeManifest() : fs.readFileSync(path.join(runtimeRoot, localPath), "utf8");
64
+ const content = localPath == null ? browserRuntimeManifest(config) : fs.readFileSync(path.join(runtimeRoot, localPath), "utf8");
65
65
  return [asarPath, content];
66
66
  });
67
67
  }
@@ -4,7 +4,13 @@
4
4
  const BUTTON_CLASS = "codex-plus-mermaid-expand-button";
5
5
 
6
6
  function sourceFor(container) {
7
- return container.querySelector("pre.sr-only")?.textContent || "";
7
+ return container.querySelector("pre.sr-only")?.textContent ||
8
+ container.parentElement?.querySelector(":scope > pre.sr-only")?.textContent ||
9
+ "";
10
+ }
11
+
12
+ function hostFor(container) {
13
+ return container.closest('[data-markdown-copy="code-block"]') || container;
8
14
  }
9
15
 
10
16
  function assetUrl(assetPath) {
@@ -13,6 +19,10 @@
13
19
  return new URL(`assets/${assetPath}`, document.baseURI).href;
14
20
  }
15
21
 
22
+ function mermaidCoreAsset() {
23
+ return CodexPlus.config?.mermaidCoreAsset || "mermaid.core.js";
24
+ }
25
+
16
26
  function escapeHtml(value) {
17
27
  return String(value).replace(/[&<>"']/g, (char) => ({
18
28
  "&": "&amp;",
@@ -219,17 +229,19 @@ renderFromSource().catch((error) => {
219
229
  const isDark = document.documentElement.classList.contains("dark") || document.documentElement.classList.contains("electron-dark");
220
230
  const debug = localStorage.getItem("codexPlusMermaidDebug") === "1";
221
231
  const html = source
222
- ? viewerHtml({ source, isDark, mermaidModuleUrl: assetUrl("mermaid.core-eIokQLcr.js"), debug })
232
+ ? viewerHtml({ source, isDark, mermaidModuleUrl: assetUrl(mermaidCoreAsset()), debug })
223
233
  : `<!doctype html><meta charset="utf-8"><body>${escapeHtml("No Mermaid source was found.")}</body>`;
224
234
  CodexPlus.native.request("mermaid/openViewer", { html }).catch(() => {});
225
235
  }
226
236
 
227
237
  function decorate(container) {
228
- if (container.querySelector(`:scope > .${BUTTON_CLASS}`)) return;
229
- container.style.position ||= "relative";
238
+ const host = hostFor(container);
239
+ if (host.querySelector(`:scope > .${BUTTON_CLASS}`)) return;
240
+ host.setAttribute("data-codex-plus-mermaid-host", "");
241
+ host.style.position ||= "relative";
230
242
  const control = button("Open Mermaid diagram fullscreen");
231
- control.addEventListener("click", () => openViewer(container));
232
- container.prepend(control);
243
+ control.addEventListener("click", () => openViewer(host));
244
+ host.prepend(control);
233
245
  }
234
246
 
235
247
  function decorateAll(root = document) {
@@ -243,6 +255,7 @@ renderFromSource().catch((error) => {
243
255
  description: "Adds a separate fullscreen viewer with zoom controls to rendered Mermaid diagrams.",
244
256
  required: true,
245
257
  styles:
258
+ `[data-codex-plus-mermaid-host]{position:relative}` +
246
259
  `[data-codex-plus-mermaid-diagram]{position:relative}` +
247
260
  `.${BUTTON_CLASS}{position:absolute;left:.5rem;top:.5rem;z-index:30;display:inline-flex;width:1.75rem;height:1.75rem;align-items:center;justify-content:center;border:1px solid var(--color-token-input-border,rgba(127,127,127,.35));border-radius:.375rem;background:var(--color-background-elevated-primary,#fff);color:var(--color-token-foreground,#111);box-shadow:0 2px 8px rgba(0,0,0,.12);opacity:.82}` +
248
261
  `.${BUTTON_CLASS}::before,.${BUTTON_CLASS}::after{content:"";position:absolute;width:.42rem;height:.42rem;border-color:currentColor;border-style:solid}` +
@@ -13,6 +13,43 @@
13
13
  return JSON.stringify([hostId, conversationId, cwd]);
14
14
  }
15
15
 
16
+ function workerRequest(workerId, method, params, signal) {
17
+ const bridge = window.electronBridge;
18
+ if (typeof bridge?.sendWorkerMessageFromView !== "function" || typeof bridge?.subscribeToWorkerMessages !== "function") {
19
+ return Promise.reject(new Error("Electron worker bridge is unavailable"));
20
+ }
21
+ if (signal?.aborted) return Promise.reject(new DOMException("Aborted", "AbortError"));
22
+ const id = `codex-plus-${Date.now().toString(36)}-${Math.random().toString(36).slice(2)}`;
23
+ const request = { id, method, params };
24
+ return new Promise((resolve, reject) => {
25
+ let unsubscribe = null;
26
+ let done = false;
27
+ const cleanup = () => {
28
+ if (done) return;
29
+ done = true;
30
+ unsubscribe?.();
31
+ signal?.removeEventListener?.("abort", abort);
32
+ };
33
+ const abort = () => {
34
+ cleanup();
35
+ bridge.sendWorkerMessageFromView(workerId, { type: "worker-request-cancel", workerId, id }).catch(() => {});
36
+ reject(new DOMException("Aborted", "AbortError"));
37
+ };
38
+ unsubscribe = bridge.subscribeToWorkerMessages(workerId, (message) => {
39
+ if (message?.type !== "worker-response" || message?.workerId !== workerId || message?.response?.id !== id) return;
40
+ cleanup();
41
+ const result = message.response.result;
42
+ if (result?.type === "ok") resolve(result.value);
43
+ else reject(new Error(result?.error?.message || "Worker request failed"));
44
+ });
45
+ signal?.addEventListener?.("abort", abort, { once: true });
46
+ bridge.sendWorkerMessageFromView(workerId, { type: "worker-request", workerId, request }).catch((error) => {
47
+ cleanup();
48
+ reject(error instanceof Error ? error : new Error(String(error)));
49
+ });
50
+ });
51
+ }
52
+
16
53
  function debugText(value) {
17
54
  try {
18
55
  return JSON.stringify(value, (_key, item) => (typeof item === "bigint" ? String(item) : item), 2) ?? "";
@@ -86,6 +123,7 @@
86
123
  function RepoDiffBody({ cwd, hostConfig, conversationId, diffMode, diffText, statusText, error, isLoading }, deps) {
87
124
  const { jsx, createElement, parseDiff, DiffCard, pathValue } = deps;
88
125
  if (error != null || isLoading || diffText == null) return PlainDiff({ text: statusText }, deps);
126
+ if (typeof parseDiff !== "function" || typeof DiffCard !== "function") return PlainDiff({ text: diffText }, deps);
89
127
  let parsed;
90
128
  try {
91
129
  parsed = parseDiff(diffText);
@@ -193,6 +231,27 @@
193
231
  }, [open, query, repo.root, hostConfig.id]);
194
232
 
195
233
  const title = selected || "Unstaged";
234
+ if (!Button || !Tooltip || !Icon || !Dropdown || !DropdownMenu || !BranchPickerDropdownContent) {
235
+ const options = branches.length > 0 ? branches : searchedBranches;
236
+ return jsxs("label", {
237
+ className: "flex min-w-32 max-w-52 shrink-0 items-center gap-1 text-xs text-token-description-foreground",
238
+ children: [
239
+ jsx("span", { className: "sr-only", children: "Base branch" }),
240
+ jsx("select", {
241
+ className:
242
+ "min-w-0 flex-1 rounded-md border border-token-border bg-token-main-surface-primary px-1.5 py-1 text-xs text-token-foreground",
243
+ value: selected,
244
+ disabled: loading || error != null,
245
+ onFocus: loadBranches,
246
+ onChange: (event) => setBaseBranch(event.target.value),
247
+ children: [
248
+ jsx("option", { value: "", children: loading ? "Loading..." : "Unstaged" }, "unstaged"),
249
+ ...options.map((branch) => jsx("option", { value: branch.name ?? branch, children: branch.name ?? branch }, branch.name ?? branch)),
250
+ ],
251
+ }),
252
+ ],
253
+ });
254
+ }
196
255
  const button = jsxs(Button, {
197
256
  type: "button",
198
257
  color: selected ? "ghostActive" : "ghost",
@@ -317,14 +376,16 @@
317
376
  ],
318
377
  }),
319
378
  jsx(BranchPicker, { repo, hostConfig, baseBranch, setBaseBranch, deps }),
320
- jsx(ReviewToolbar, {
321
- conversationId,
322
- cwd: repo.cwd,
323
- hostId,
324
- codexWorktree: false,
325
- surface: "review-toolbar",
326
- reviewToolbarCompact: true,
327
- }, repo.id),
379
+ ReviewToolbar
380
+ ? jsx(ReviewToolbar, {
381
+ conversationId,
382
+ cwd: repo.cwd,
383
+ hostId,
384
+ codexWorktree: false,
385
+ surface: "review-toolbar",
386
+ reviewToolbarCompact: true,
387
+ }, repo.id)
388
+ : null,
328
389
  ],
329
390
  }),
330
391
  collapsed ? null : RepoDiffBody({ cwd: repo.cwd, hostConfig, conversationId, diffMode, diffText, statusText, error, isLoading: loading }, deps),
@@ -338,7 +399,7 @@
338
399
  const cwd = useAtom(cwdAtom);
339
400
  const hostId = useAtom(hostIdAtom);
340
401
  const hostConfig = useAtom(hostConfigAtom);
341
- const conversationAtomValue = useAtom(conversationIdAtom);
402
+ const conversationAtomValue = conversationIdAtom ? useAtom(conversationIdAtom) : null;
342
403
  const conversationId = routeStore.value.routeKind === "local-thread" ? routeStore.value.conversationId : null;
343
404
  const [targets, setTargets] = React.useState(null);
344
405
  const [collapsed, setCollapsedState] = React.useState(() => new Map());
@@ -438,8 +499,8 @@
438
499
  start(api) {
439
500
  api.ui.review.wrapBody((props, deps) => ReviewMux(props, deps));
440
501
  api.modules.registerHostModule("codex-plus:native:repository-targets", {
441
- request(params) {
442
- return api.native.request("repository-targets", params);
502
+ request(params, signal) {
503
+ return workerRequest("git", "repository-targets", params, signal);
443
504
  },
444
505
  });
445
506
  },
@@ -58,6 +58,90 @@
58
58
  return palette[fnv1a32(colorKey(project)) % palette.length];
59
59
  }
60
60
 
61
+ const projectByPath = new Map();
62
+ const projectByName = new Map();
63
+
64
+ function pathBasename(value) {
65
+ const trimmed = String(value || "").replace(/\/+$/, "");
66
+ if (trimmed === "") return "";
67
+ return trimmed.split("/").pop() || "";
68
+ }
69
+
70
+ function projectPathKeys(project) {
71
+ if (project == null || typeof project === "string") return [];
72
+ const host = project.hostId ?? project.host ?? project.remoteHostId ?? "local";
73
+ const paths = [project.path, project.cwd, project.projectPath, project.remotePath, project.root, project.workspaceRoot]
74
+ .filter((value) => value != null && String(value).trim() !== "")
75
+ .map((value) => String(value).trim());
76
+ return paths.map((path) => `${host}:${path}`);
77
+ }
78
+
79
+ function projectNameKeys(project) {
80
+ if (project == null || typeof project === "string") return [];
81
+ const repositoryRoot = project.repositoryData?.rootFolder;
82
+ const values = [
83
+ project.label,
84
+ project.name,
85
+ repositoryRoot,
86
+ pathBasename(project.projectId),
87
+ pathBasename(project.id),
88
+ ...projectPathKeys(project).map((key) => pathBasename(key)),
89
+ ];
90
+ return Array.from(new Set(values
91
+ .filter((value) => value != null && String(value).trim() !== "")
92
+ .map((value) => String(value).trim())));
93
+ }
94
+
95
+ function rememberProjectName(key, project) {
96
+ if (key === "") return;
97
+ const existing = projectByName.get(key);
98
+ if (existing === undefined) {
99
+ projectByName.set(key, project);
100
+ return;
101
+ }
102
+ if (existing != null && colorKey(existing) !== colorKey(project)) projectByName.set(key, null);
103
+ }
104
+
105
+ function rememberProject(project) {
106
+ const key = colorKey(project);
107
+ if (key.trim() === "") return project;
108
+ for (const pathKey of projectPathKeys(project)) projectByPath.set(pathKey, project);
109
+ for (const nameKey of projectNameKeys(project)) rememberProjectName(nameKey, project);
110
+ return project;
111
+ }
112
+
113
+ function resolveProject(project) {
114
+ for (const pathKey of projectPathKeys(project)) {
115
+ const knownProject = projectByPath.get(pathKey);
116
+ if (knownProject) return knownProject;
117
+ }
118
+ for (const nameKey of projectNameKeys(project)) {
119
+ const knownProject = projectByName.get(nameKey);
120
+ if (knownProject) return knownProject;
121
+ }
122
+ return null;
123
+ }
124
+
125
+ function activeSidebarStyle() {
126
+ const active = document.querySelector('[data-app-action-sidebar-thread-active="true"][data-codex-plus-project-color]');
127
+ if (!active) return undefined;
128
+ const computed = getComputedStyle(active);
129
+ const accent = computed.getPropertyValue("--codex-plus-project-accent").trim();
130
+ if (accent === "") return undefined;
131
+ return {
132
+ "--codex-plus-project-accent": accent,
133
+ "--codex-plus-project-bg-light": computed.getPropertyValue("--codex-plus-project-bg-light").trim(),
134
+ "--codex-plus-project-fg-light": computed.getPropertyValue("--codex-plus-project-fg-light").trim(),
135
+ "--codex-plus-project-soft-light": computed.getPropertyValue("--codex-plus-project-soft-light").trim(),
136
+ "--codex-plus-project-bg-dark": computed.getPropertyValue("--codex-plus-project-bg-dark").trim(),
137
+ "--codex-plus-project-fg-dark": computed.getPropertyValue("--codex-plus-project-fg-dark").trim(),
138
+ "--codex-plus-project-border-dark": computed.getPropertyValue("--codex-plus-project-border-dark").trim(),
139
+ "--codex-plus-project-separator-light": computed.getPropertyValue("--codex-plus-project-separator-light").trim(),
140
+ "--codex-plus-project-separator-dark": computed.getPropertyValue("--codex-plus-project-separator-dark").trim(),
141
+ borderLeft: `6px solid ${accent}`,
142
+ };
143
+ }
144
+
61
145
  function style(project) {
62
146
  const key = colorKey(project);
63
147
  if (!readEnabled() || key.trim() === "") return undefined;
@@ -77,7 +161,9 @@
77
161
  }
78
162
 
79
163
  function dataAttributes(project, sidebar) {
80
- const inlineStyle = style(project);
164
+ const resolvedProject = sidebar ? rememberProject(project) : resolveProject(project);
165
+ const directStyle = style(project);
166
+ const inlineStyle = resolvedProject ? style(resolvedProject) : directStyle ?? activeSidebarStyle();
81
167
  if (inlineStyle == null) return undefined;
82
168
  return {
83
169
  "data-codex-plus-project-color": "",
@@ -114,14 +200,15 @@
114
200
  description: "Provides deterministic project accent colors across sidebar, messages, and composer surfaces.",
115
201
  required: true,
116
202
  styles:
117
- ":root:not(.dark):not(.electron-dark) [data-codex-plus-project-sidebar-color]{border-radius:0;background-color:var(--codex-plus-project-soft-light);border-left-color:var(--codex-plus-project-accent)}" +
118
- ":root:not(.dark):not(.electron-dark) [data-app-action-sidebar-thread-active=\"true\"][data-codex-plus-project-sidebar-color]{background-color:var(--codex-plus-project-bg-light);box-shadow:inset 5px 0 0 var(--codex-plus-project-accent)}" +
119
- ":root.dark [data-codex-plus-project-sidebar-color],:root.electron-dark [data-codex-plus-project-sidebar-color]{border-radius:0;background-color:var(--codex-plus-project-bg-dark);border-left-color:var(--codex-plus-project-border-dark)}" +
120
- ":root.dark [data-app-action-sidebar-thread-active=\"true\"][data-codex-plus-project-sidebar-color],:root.electron-dark [data-app-action-sidebar-thread-active=\"true\"][data-codex-plus-project-sidebar-color]{background-color:color-mix(in srgb,var(--codex-plus-project-accent) 38%,transparent);border-left-color:color-mix(in srgb,var(--codex-plus-project-accent) 88%,transparent);box-shadow:inset 5px 0 0 var(--codex-plus-project-accent)}" +
203
+ ":root:not(.dark):not(.electron-dark) :is([data-app-action-sidebar-project-row],[data-app-action-sidebar-thread-row][data-codex-plus-project-sidebar-color],[data-app-action-sidebar-project-list-id][data-codex-plus-project-sidebar-color] [data-app-action-sidebar-thread-row]){border-radius:0;background-color:var(--codex-plus-project-soft-light);border-left-color:var(--codex-plus-project-accent)}" +
204
+ ":root:not(.dark):not(.electron-dark) :is([data-app-action-sidebar-thread-row][data-codex-plus-project-sidebar-color],[data-app-action-sidebar-project-list-id][data-codex-plus-project-sidebar-color] [data-app-action-sidebar-thread-row])[data-app-action-sidebar-thread-active=\"true\"]{background-color:var(--codex-plus-project-bg-light);box-shadow:inset 5px 0 0 var(--codex-plus-project-accent)}" +
205
+ ":root.dark :is([data-app-action-sidebar-project-row],[data-app-action-sidebar-thread-row][data-codex-plus-project-sidebar-color],[data-app-action-sidebar-project-list-id][data-codex-plus-project-sidebar-color] [data-app-action-sidebar-thread-row]),:root.electron-dark :is([data-app-action-sidebar-project-row],[data-app-action-sidebar-thread-row][data-codex-plus-project-sidebar-color],[data-app-action-sidebar-project-list-id][data-codex-plus-project-sidebar-color] [data-app-action-sidebar-thread-row]){border-radius:0;background-color:var(--codex-plus-project-bg-dark);border-left-color:var(--codex-plus-project-border-dark)}" +
206
+ ":root.dark :is([data-app-action-sidebar-thread-row][data-codex-plus-project-sidebar-color],[data-app-action-sidebar-project-list-id][data-codex-plus-project-sidebar-color] [data-app-action-sidebar-thread-row])[data-app-action-sidebar-thread-active=\"true\"],:root.electron-dark :is([data-app-action-sidebar-thread-row][data-codex-plus-project-sidebar-color],[data-app-action-sidebar-project-list-id][data-codex-plus-project-sidebar-color] [data-app-action-sidebar-thread-row])[data-app-action-sidebar-thread-active=\"true\"]{background-color:color-mix(in srgb,var(--codex-plus-project-accent) 38%,transparent);border-left-color:color-mix(in srgb,var(--codex-plus-project-accent) 88%,transparent);box-shadow:inset 5px 0 0 var(--codex-plus-project-accent)}" +
121
207
  ":root:not(.dark):not(.electron-dark) [data-codex-plus-project-color]{border-left-color:var(--codex-plus-project-accent)}" +
122
208
  ":root.dark [data-codex-plus-project-color],:root.electron-dark [data-codex-plus-project-color]{border-left-color:var(--codex-plus-project-border-dark)}" +
123
- ":root:not(.dark):not(.electron-dark) [data-codex-plus-project-color]:not([data-codex-plus-project-sidebar-color]){background-image:linear-gradient(to right,var(--codex-plus-project-separator-light),var(--codex-plus-project-separator-light));background-repeat:no-repeat;background-size:2px 100%;background-position:left top}" +
124
- ":root.dark [data-codex-plus-project-color]:not([data-codex-plus-project-sidebar-color]),:root.electron-dark [data-codex-plus-project-color]:not([data-codex-plus-project-sidebar-color]){background-image:linear-gradient(to right,var(--codex-plus-project-separator-dark),var(--codex-plus-project-separator-dark));background-repeat:no-repeat;background-size:2px 100%;background-position:left top}",
209
+ ":root:not(.dark):not(.electron-dark) [data-codex-plus-project-color]:not([data-codex-plus-project-sidebar-color]){box-shadow:inset 6px 0 0 var(--codex-plus-project-accent);border-left-color:var(--codex-plus-project-accent)}" +
210
+ ":root.dark [data-codex-plus-project-color]:not([data-codex-plus-project-sidebar-color]),:root.electron-dark [data-codex-plus-project-color]:not([data-codex-plus-project-sidebar-color]){box-shadow:inset 6px 0 0 var(--codex-plus-project-accent);border-left-color:var(--codex-plus-project-border-dark)}" +
211
+ "[data-codex-plus-user-entry][data-codex-plus-project-color]{box-shadow:inset 6px 0 0 var(--codex-plus-project-accent),0 0 0 .5px rgba(255,255,255,.2)!important}",
125
212
  exports: {
126
213
  colorFor,
127
214
  colorKey,
@@ -36,14 +36,71 @@
36
36
  ].map((value) => normalizeForFzf(value).text.trim()).filter(Boolean).join(" ");
37
37
  }
38
38
 
39
+ function projectSearchFields(project) {
40
+ return [
41
+ { text: project?.label, weight: 0 },
42
+ { text: project?.repositoryData?.rootFolder, weight: 10 },
43
+ { text: project?.hostDisplayName, weight: 20 },
44
+ { text: project?.path, weight: 40 },
45
+ ].map((field) => ({ ...field, text: normalizeForFzf(field.text).text.trim() })).filter((field) => field.text);
46
+ }
47
+
39
48
  function fzfConstructor() {
40
49
  return window.fzf?.Fzf;
41
50
  }
42
51
 
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));
52
+ function fallbackPositions(text, query) {
53
+ const normalizedText = normalizeForFzf(text);
54
+ const normalizedQuery = normalizeForFzf(query).text.trim().toLowerCase();
55
+ if (!normalizedText.text || !normalizedQuery) return null;
56
+
57
+ const haystack = normalizedText.text.toLowerCase();
58
+ const positions = [];
59
+ let cursor = 0;
60
+
61
+ for (const char of normalizedQuery) {
62
+ cursor = haystack.indexOf(char, cursor);
63
+ if (cursor === -1) return null;
64
+ positions.push(cursor);
65
+ cursor += 1;
66
+ }
67
+
68
+ return positions;
69
+ }
70
+
71
+ function fallbackScore(text, query) {
72
+ const positions = fallbackPositions(text, query);
73
+ if (positions == null) return null;
74
+
75
+ let score = positions[0] + (positions[positions.length - 1] - positions[0]);
76
+ for (let index = 1; index < positions.length; index += 1) {
77
+ score += positions[index] - positions[index - 1] - 1;
78
+ }
79
+ for (const position of positions) {
80
+ if (position === 0 || /\s/.test(text[position - 1] ?? "")) score -= 2;
81
+ }
82
+
83
+ return score;
84
+ }
85
+
86
+ function rankedFilter(items, query) {
87
+ return items
88
+ .map((item, index) => {
89
+ const scores = projectSearchFields(item)
90
+ .map((field) => {
91
+ const score = fallbackScore(field.text, query);
92
+ return score == null ? null : score + field.weight;
93
+ })
94
+ .filter((score) => score != null);
95
+ return { item, index, score: scores.length === 0 ? null : Math.min(...scores) };
96
+ })
97
+ .filter((entry) => entry.score != null)
98
+ .sort((left, right) =>
99
+ left.score - right.score ||
100
+ projectSearchText(left.item).length - projectSearchText(right.item).length ||
101
+ left.index - right.index,
102
+ )
103
+ .map((entry) => entry.item);
47
104
  }
48
105
 
49
106
  function fuzzyFilter(items, query) {
@@ -51,18 +108,16 @@
51
108
  const normalizedQuery = normalizeForFzf(query).text.trim();
52
109
  if (!normalizedQuery) return list;
53
110
 
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);
111
+ return rankedFilter(list, query);
61
112
  }
62
113
 
63
114
  function labelPositions(text, query) {
64
115
  const Fzf = fzfConstructor();
65
- if (typeof Fzf !== "function") return null;
116
+ if (typeof Fzf !== "function") {
117
+ const positions = fallbackPositions(text, query);
118
+ const normalizedText = normalizeForFzf(text);
119
+ return positions?.map((index) => normalizedText.map[index]).filter((index) => Number.isInteger(index)) ?? null;
120
+ }
66
121
 
67
122
  const normalizedText = normalizeForFzf(text);
68
123
  const normalizedQuery = normalizeForFzf(query).text.trim();
@@ -6,7 +6,7 @@
6
6
  name: "Sidebar Name Blur",
7
7
  description: "Registers the session-only Toggle sidebar blur command.",
8
8
  required: true,
9
- styles: ':root[data-codex-plus-sidebar-names-blurred="true"] :is([data-thread-title],[data-codex-plus-sidebar-name]){filter:blur(4px);user-select:none}',
9
+ styles: ':root[data-codex-plus-sidebar-names-blurred="true"] :is([data-thread-title],[data-codex-plus-sidebar-name],[data-app-action-sidebar-project-row]){filter:blur(4px);user-select:none}',
10
10
  commands: [
11
11
  {
12
12
  id: "codexPlusToggleSidebarNameBlur",