codex-plus-patcher 0.4.1 → 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 +11 -0
- package/package.json +2 -1
- package/src/cli.js +97 -0
- package/src/patches/26.616.41845-4198.js +2 -0
- package/src/patches/26.616.51431-4212.js +2 -0
- package/src/patches/26.616.71553-4265.js +7 -0
- package/src/patches/26.616.81150-4306.js +8 -0
- package/src/patches/lib/common-patches.js +196 -1
- package/src/patches/lib/project-selector-shortcut-patch.js +54 -0
- package/src/runtime/assets.js +5 -0
- package/src/runtime/plugins/devTools.js +33 -0
- package/src/runtime/plugins/mermaidFullscreen.js +275 -0
- package/src/runtime/plugins/projectPathHeader.js +116 -0
- package/src/runtime/plugins/projectSelectorShortcut.js +207 -0
- package/src/runtime/runtime.js +98 -7
|
@@ -0,0 +1,275 @@
|
|
|
1
|
+
(function () {
|
|
2
|
+
const CodexPlus = window.CodexPlus;
|
|
3
|
+
const SELECTOR = "[data-codex-plus-mermaid-diagram]";
|
|
4
|
+
const BUTTON_CLASS = "codex-plus-mermaid-expand-button";
|
|
5
|
+
|
|
6
|
+
function sourceFor(container) {
|
|
7
|
+
return container.querySelector("pre.sr-only")?.textContent || "";
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
function assetUrl(assetPath) {
|
|
11
|
+
const appScript = document.querySelector('script[type="module"][src*="/assets/"],script[type="module"][src^="./assets/"]');
|
|
12
|
+
if (appScript?.src) return new URL(assetPath, new URL(".", appScript.src)).href;
|
|
13
|
+
return new URL(`assets/${assetPath}`, document.baseURI).href;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function escapeHtml(value) {
|
|
17
|
+
return String(value).replace(/[&<>"']/g, (char) => ({
|
|
18
|
+
"&": "&",
|
|
19
|
+
"<": "<",
|
|
20
|
+
">": ">",
|
|
21
|
+
'"': """,
|
|
22
|
+
"'": "'",
|
|
23
|
+
})[char]);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function mermaidLiveUrl(source) {
|
|
27
|
+
const state = JSON.stringify({
|
|
28
|
+
code: source,
|
|
29
|
+
mermaid: { theme: "default" },
|
|
30
|
+
updateEditor: false,
|
|
31
|
+
});
|
|
32
|
+
return `https://mermaid.live/edit#base64:${btoa(unescape(encodeURIComponent(state))).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "")}`;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function viewerHtml({ source, isDark, mermaidModuleUrl, debug }) {
|
|
36
|
+
return `<!doctype html>
|
|
37
|
+
<html>
|
|
38
|
+
<head>
|
|
39
|
+
<meta charset="utf-8">
|
|
40
|
+
<title>Mermaid diagram viewer</title>
|
|
41
|
+
<style>
|
|
42
|
+
:root{color-scheme:${isDark ? "dark" : "light"};--viewer-bg:${isDark ? "#0a0a0a" : "#fff"};--viewer-fg:${isDark ? "#fff" : "#111"};--viewer-toolbar-bg:${isDark ? "#252525" : "#f7f7f7"};--viewer-border:${isDark ? "rgba(255,255,255,.18)" : "rgba(0,0,0,.16)"};--viewer-button-border:${isDark ? "rgba(255,255,255,.22)" : "rgba(0,0,0,.18)"};--viewer-button-bg:${isDark ? "rgba(255,255,255,.08)" : "rgba(0,0,0,.06)"};--viewer-muted:${isDark ? "#cfcfcf" : "#333"}}
|
|
43
|
+
:root[data-theme="dark"]{color-scheme:dark;--viewer-bg:#0a0a0a;--viewer-fg:#fff;--viewer-toolbar-bg:#252525;--viewer-border:rgba(255,255,255,.18);--viewer-button-border:rgba(255,255,255,.22);--viewer-button-bg:rgba(255,255,255,.08);--viewer-muted:#cfcfcf}
|
|
44
|
+
:root[data-theme="light"]{color-scheme:light;--viewer-bg:#fff;--viewer-fg:#111;--viewer-toolbar-bg:#f7f7f7;--viewer-border:rgba(0,0,0,.16);--viewer-button-border:rgba(0,0,0,.18);--viewer-button-bg:rgba(0,0,0,.06);--viewer-muted:#333}
|
|
45
|
+
*{box-sizing:border-box}
|
|
46
|
+
html,body{height:100%;margin:0}
|
|
47
|
+
body{display:flex;flex-direction:column;background:var(--viewer-bg);color:var(--viewer-fg);font:13px -apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif}
|
|
48
|
+
.toolbar{display:flex;gap:8px;justify-content:flex-end;padding:10px;border-bottom:1px solid var(--viewer-border);background:var(--viewer-toolbar-bg)}
|
|
49
|
+
button{min-width:42px;border:1px solid var(--viewer-button-border);border-radius:6px;background:var(--viewer-button-bg);color:inherit;padding:6px 10px}
|
|
50
|
+
button:focus-visible{outline:2px solid #60a5fa;outline-offset:2px}
|
|
51
|
+
.viewport{flex:1;overflow:auto;padding:16px}
|
|
52
|
+
.stage{width:max-content;min-width:100%}
|
|
53
|
+
.stage svg{display:block;max-width:none;background:var(--viewer-bg)}
|
|
54
|
+
.render-status{position:fixed;left:12px;bottom:10px;z-index:10;color:var(--viewer-muted);font-size:12px}
|
|
55
|
+
.render-status[hidden]{display:none}
|
|
56
|
+
</style>
|
|
57
|
+
</head>
|
|
58
|
+
<body>
|
|
59
|
+
<div class="toolbar">
|
|
60
|
+
<button id="zoom-fit" type="button" aria-label="Zoom to fit">Fit</button>
|
|
61
|
+
<button id="zoom-width" type="button" aria-label="Zoom to width">Width</button>
|
|
62
|
+
<button id="zoom-height" type="button" aria-label="Zoom to height">Height</button>
|
|
63
|
+
<button id="zoom-out" type="button" aria-label="Zoom out">-</button>
|
|
64
|
+
<button id="zoom-reset" type="button" aria-label="Reset zoom">100%</button>
|
|
65
|
+
<button id="zoom-in" type="button" aria-label="Zoom in">+</button>
|
|
66
|
+
<button id="theme-toggle" type="button" aria-label="Toggle Mermaid theme">${isDark ? "Dark" : "Light"}</button>
|
|
67
|
+
<button id="open-live" type="button" aria-label="Open in Mermaid Live">Live</button>
|
|
68
|
+
<button id="close" type="button" aria-label="Close Mermaid diagram viewer">Close</button>
|
|
69
|
+
</div>
|
|
70
|
+
<div class="viewport"><div class="stage" id="stage"></div></div>
|
|
71
|
+
<div class="render-status" id="render-status" hidden>Rendering Mermaid source...</div>
|
|
72
|
+
<script type="module">
|
|
73
|
+
let scale = 1;
|
|
74
|
+
let darkTheme = ${isDark ? "true" : "false"};
|
|
75
|
+
const source = ${JSON.stringify(source)};
|
|
76
|
+
const mermaidModuleUrl = ${JSON.stringify(mermaidModuleUrl)};
|
|
77
|
+
const liveUrl = ${JSON.stringify(mermaidLiveUrl(source))};
|
|
78
|
+
const debug = ${debug ? "true" : "false"} || localStorage.getItem("codexPlusMermaidDebug") === "1";
|
|
79
|
+
const stage = document.getElementById("stage");
|
|
80
|
+
const viewport = document.querySelector(".viewport");
|
|
81
|
+
const reset = document.getElementById("zoom-reset");
|
|
82
|
+
const themeToggle = document.getElementById("theme-toggle");
|
|
83
|
+
const renderStatus = document.getElementById("render-status");
|
|
84
|
+
let fitMode = "fit";
|
|
85
|
+
let renderCount = 0;
|
|
86
|
+
let renderInFlight = false;
|
|
87
|
+
let renderQueued = false;
|
|
88
|
+
function diagram() {
|
|
89
|
+
return stage.querySelector("svg");
|
|
90
|
+
}
|
|
91
|
+
function baseSize() {
|
|
92
|
+
const svg = diagram();
|
|
93
|
+
const viewBox = svg?.viewBox?.baseVal;
|
|
94
|
+
if (viewBox && viewBox.width > 0 && viewBox.height > 0) return { width: viewBox.width, height: viewBox.height };
|
|
95
|
+
const rect = svg?.getBoundingClientRect();
|
|
96
|
+
return { width: rect?.width || 800, height: rect?.height || 600 };
|
|
97
|
+
}
|
|
98
|
+
let base = { width: 800, height: 600 };
|
|
99
|
+
function setScale(next, mode = null) {
|
|
100
|
+
fitMode = mode;
|
|
101
|
+
scale = Math.max(0.05, Math.min(8, next));
|
|
102
|
+
const svg = diagram();
|
|
103
|
+
if (svg) {
|
|
104
|
+
svg.removeAttribute("width");
|
|
105
|
+
svg.removeAttribute("height");
|
|
106
|
+
svg.style.width = Math.round(base.width * scale) + "px";
|
|
107
|
+
svg.style.height = Math.round(base.height * scale) + "px";
|
|
108
|
+
}
|
|
109
|
+
reset.textContent = Math.round(scale * 100) + "%";
|
|
110
|
+
}
|
|
111
|
+
function fitScale(mode) {
|
|
112
|
+
const width = Math.max(1, viewport.clientWidth - 32);
|
|
113
|
+
const height = Math.max(1, viewport.clientHeight - 32);
|
|
114
|
+
const byWidth = width / base.width;
|
|
115
|
+
const byHeight = height / base.height;
|
|
116
|
+
if (mode === "width") return byWidth;
|
|
117
|
+
if (mode === "height") return byHeight;
|
|
118
|
+
return Math.min(byWidth, byHeight);
|
|
119
|
+
}
|
|
120
|
+
function applyFit(mode) {
|
|
121
|
+
setScale(fitScale(mode), mode);
|
|
122
|
+
}
|
|
123
|
+
function applyThemeChrome() {
|
|
124
|
+
document.documentElement.dataset.theme = darkTheme ? "dark" : "light";
|
|
125
|
+
document.documentElement.style.colorScheme = darkTheme ? "dark" : "light";
|
|
126
|
+
themeToggle.textContent = darkTheme ? "Dark" : "Light";
|
|
127
|
+
themeToggle.setAttribute("aria-pressed", String(darkTheme));
|
|
128
|
+
}
|
|
129
|
+
function themeDirective() {
|
|
130
|
+
return "%%{init: " + JSON.stringify({ theme: darkTheme ? "dark" : "default" }) + "}%%" + String.fromCharCode(10);
|
|
131
|
+
}
|
|
132
|
+
function sourceForTheme() {
|
|
133
|
+
const trimmed = source.trimStart();
|
|
134
|
+
if (!trimmed.startsWith("%%{")) return themeDirective() + source;
|
|
135
|
+
const markerEnd = trimmed.indexOf("}%%");
|
|
136
|
+
if (markerEnd < 0) return themeDirective() + source;
|
|
137
|
+
const directive = trimmed.slice(0, markerEnd + 3).toLowerCase();
|
|
138
|
+
if (!directive.startsWith("%%{init:") && !directive.startsWith("%%{initialize:")) return themeDirective() + source;
|
|
139
|
+
let rest = trimmed.slice(markerEnd + 3);
|
|
140
|
+
while ([9, 10, 13, 32].includes(rest.charCodeAt(0))) rest = rest.slice(1);
|
|
141
|
+
return themeDirective() + rest;
|
|
142
|
+
}
|
|
143
|
+
async function renderFromSource() {
|
|
144
|
+
if (renderInFlight) {
|
|
145
|
+
renderQueued = true;
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
renderInFlight = true;
|
|
149
|
+
renderStatus.hidden = !debug;
|
|
150
|
+
renderStatus.textContent = "Rendering Mermaid source...";
|
|
151
|
+
themeToggle.disabled = true;
|
|
152
|
+
try {
|
|
153
|
+
const mermaid = (await import(mermaidModuleUrl)).default;
|
|
154
|
+
mermaid.initialize({
|
|
155
|
+
startOnLoad: false,
|
|
156
|
+
securityLevel: "strict",
|
|
157
|
+
suppressErrorRendering: true,
|
|
158
|
+
deterministicIds: true,
|
|
159
|
+
deterministicIDSeed: "codex-plus-mermaid-viewer",
|
|
160
|
+
htmlLabels: false,
|
|
161
|
+
flowchart: { htmlLabels: false },
|
|
162
|
+
darkMode: darkTheme,
|
|
163
|
+
fontFamily: '-apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif',
|
|
164
|
+
theme: darkTheme ? "dark" : "default",
|
|
165
|
+
});
|
|
166
|
+
applyThemeChrome();
|
|
167
|
+
const rendered = await mermaid.render("codex-plus-mermaid-viewer-" + String(renderCount += 1), sourceForTheme());
|
|
168
|
+
stage.innerHTML = rendered.svg;
|
|
169
|
+
renderStatus.textContent = "Rendered from Mermaid source";
|
|
170
|
+
base = baseSize();
|
|
171
|
+
applyFit(fitMode || "fit");
|
|
172
|
+
} finally {
|
|
173
|
+
renderInFlight = false;
|
|
174
|
+
themeToggle.disabled = false;
|
|
175
|
+
if (renderQueued) {
|
|
176
|
+
renderQueued = false;
|
|
177
|
+
renderFromSource();
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
document.getElementById("zoom-fit").addEventListener("click", () => applyFit("fit"));
|
|
182
|
+
document.getElementById("zoom-width").addEventListener("click", () => applyFit("width"));
|
|
183
|
+
document.getElementById("zoom-height").addEventListener("click", () => applyFit("height"));
|
|
184
|
+
document.getElementById("zoom-out").addEventListener("click", () => setScale(scale - 0.2));
|
|
185
|
+
reset.addEventListener("click", () => setScale(1));
|
|
186
|
+
document.getElementById("zoom-in").addEventListener("click", () => setScale(scale + 0.2));
|
|
187
|
+
themeToggle.addEventListener("click", () => { darkTheme = !darkTheme; applyThemeChrome(); renderQueued = true; renderFromSource(); });
|
|
188
|
+
document.getElementById("open-live").addEventListener("click", () => window.open(liveUrl, "_blank", "noopener"));
|
|
189
|
+
document.getElementById("close").addEventListener("click", () => window.close());
|
|
190
|
+
window.addEventListener("resize", () => { if (fitMode) applyFit(fitMode); });
|
|
191
|
+
document.addEventListener("keydown", (event) => {
|
|
192
|
+
if (event.key === "Escape") window.close();
|
|
193
|
+
if ((event.metaKey || event.ctrlKey) && event.key === "=") { event.preventDefault(); setScale(scale + 0.2); }
|
|
194
|
+
if ((event.metaKey || event.ctrlKey) && event.key === "-") { event.preventDefault(); setScale(scale - 0.2); }
|
|
195
|
+
if ((event.metaKey || event.ctrlKey) && event.key === "0") { event.preventDefault(); setScale(1); }
|
|
196
|
+
});
|
|
197
|
+
applyThemeChrome();
|
|
198
|
+
renderFromSource().catch((error) => {
|
|
199
|
+
renderStatus.hidden = false;
|
|
200
|
+
renderStatus.textContent = "Mermaid render failed: " + String(error?.message || error);
|
|
201
|
+
console.error("[Codex Plus] Mermaid source render failed", error);
|
|
202
|
+
});
|
|
203
|
+
</script>
|
|
204
|
+
</body>
|
|
205
|
+
</html>`;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
function button(label) {
|
|
209
|
+
const element = document.createElement("button");
|
|
210
|
+
element.type = "button";
|
|
211
|
+
element.className = BUTTON_CLASS;
|
|
212
|
+
element.setAttribute("aria-label", label);
|
|
213
|
+
element.title = label;
|
|
214
|
+
return element;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
function openViewer(container) {
|
|
218
|
+
const source = sourceFor(container);
|
|
219
|
+
const isDark = document.documentElement.classList.contains("dark") || document.documentElement.classList.contains("electron-dark");
|
|
220
|
+
const debug = localStorage.getItem("codexPlusMermaidDebug") === "1";
|
|
221
|
+
const html = source
|
|
222
|
+
? viewerHtml({ source, isDark, mermaidModuleUrl: assetUrl("mermaid.core-eIokQLcr.js"), debug })
|
|
223
|
+
: `<!doctype html><meta charset="utf-8"><body>${escapeHtml("No Mermaid source was found.")}</body>`;
|
|
224
|
+
CodexPlus.native.request("mermaid/openViewer", { html }).catch(() => {});
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
function decorate(container) {
|
|
228
|
+
if (container.querySelector(`:scope > .${BUTTON_CLASS}`)) return;
|
|
229
|
+
container.style.position ||= "relative";
|
|
230
|
+
const control = button("Open Mermaid diagram fullscreen");
|
|
231
|
+
control.addEventListener("click", () => openViewer(container));
|
|
232
|
+
container.prepend(control);
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
function decorateAll(root = document) {
|
|
236
|
+
for (const container of root.querySelectorAll(SELECTOR)) decorate(container);
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
CodexPlus.registerPlugin(
|
|
240
|
+
CodexPlus.definePlugin({
|
|
241
|
+
id: "mermaidFullscreen",
|
|
242
|
+
name: "Mermaid Fullscreen Viewer",
|
|
243
|
+
description: "Adds a separate fullscreen viewer with zoom controls to rendered Mermaid diagrams.",
|
|
244
|
+
required: true,
|
|
245
|
+
styles:
|
|
246
|
+
`[data-codex-plus-mermaid-diagram]{position:relative}` +
|
|
247
|
+
`.${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
|
+
`.${BUTTON_CLASS}::before,.${BUTTON_CLASS}::after{content:"";position:absolute;width:.42rem;height:.42rem;border-color:currentColor;border-style:solid}` +
|
|
249
|
+
`.${BUTTON_CLASS}::before{right:.42rem;top:.42rem;border-width:2px 2px 0 0}` +
|
|
250
|
+
`.${BUTTON_CLASS}::after{left:.42rem;bottom:.42rem;border-width:0 0 2px 2px}` +
|
|
251
|
+
`.${BUTTON_CLASS}:hover,.${BUTTON_CLASS}:focus-visible{opacity:1;outline:2px solid var(--color-token-focus-border,#3b82f6);outline-offset:2px}` +
|
|
252
|
+
`:root.dark .${BUTTON_CLASS},:root.electron-dark .${BUTTON_CLASS}{background:var(--color-background-elevated-primary,#111);color:var(--color-token-foreground,#fff);box-shadow:0 2px 10px rgba(0,0,0,.45)}`,
|
|
253
|
+
exports: { decorateAll, openViewer },
|
|
254
|
+
start(api) {
|
|
255
|
+
api.ui.mermaid.decorateDiagram(() => ({ "data-codex-plus-mermaid-diagram": "" }));
|
|
256
|
+
const install = () => {
|
|
257
|
+
if (!document.body) return;
|
|
258
|
+
decorateAll();
|
|
259
|
+
const observer = new MutationObserver((records) => {
|
|
260
|
+
for (const record of records) {
|
|
261
|
+
for (const node of record.addedNodes) {
|
|
262
|
+
if (node.nodeType !== Node.ELEMENT_NODE) continue;
|
|
263
|
+
if (node.matches?.(SELECTOR)) decorate(node);
|
|
264
|
+
decorateAll(node);
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
});
|
|
268
|
+
observer.observe(document.body, { childList: true, subtree: true });
|
|
269
|
+
};
|
|
270
|
+
if (document.body) install();
|
|
271
|
+
else document.addEventListener("DOMContentLoaded", install, { once: true });
|
|
272
|
+
},
|
|
273
|
+
}),
|
|
274
|
+
);
|
|
275
|
+
})();
|
|
@@ -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
|
+
})();
|