codex-plus-patcher 0.2.1 → 0.4.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.
@@ -0,0 +1,24 @@
1
+ const { replaceAllOnce } = require("./replace");
2
+
3
+ function exactReplacement(oldText, newText, label) {
4
+ return [oldText, newText, label];
5
+ }
6
+
7
+ function prependAtAnchor(anchor, helper, label) {
8
+ return exactReplacement(anchor, `${helper}${anchor}`, label);
9
+ }
10
+
11
+ function appendImport(anchorImport, newImport, label) {
12
+ return exactReplacement(anchorImport, `${anchorImport}${newImport}`, label);
13
+ }
14
+
15
+ function applyExactReplacements(text, descriptors) {
16
+ return replaceAllOnce(text, descriptors);
17
+ }
18
+
19
+ module.exports = {
20
+ appendImport,
21
+ applyExactReplacements,
22
+ exactReplacement,
23
+ prependAtAnchor,
24
+ };
@@ -0,0 +1,14 @@
1
+ function makePatchSet({ id, codexVersion, bundleVersion, asarSha256, assetFiles, patches }) {
2
+ return {
3
+ id,
4
+ codexVersion,
5
+ bundleVersion,
6
+ asarSha256,
7
+ assetFiles,
8
+ patches,
9
+ };
10
+ }
11
+
12
+ module.exports = {
13
+ makePatchSet,
14
+ };
@@ -0,0 +1,17 @@
1
+ function replaceOnce(text, oldText, newText, label) {
2
+ const matches = text.split(oldText).length - 1;
3
+ if (matches !== 1) throw new Error(`Expected one ${label}, found ${matches}`);
4
+ return text.replace(oldText, newText);
5
+ }
6
+
7
+ function replaceAllOnce(text, replacements) {
8
+ return replacements.reduce((current, replacement) => {
9
+ const [oldText, newText, label] = replacement;
10
+ return replaceOnce(current, oldText, newText, label);
11
+ }, text);
12
+ }
13
+
14
+ module.exports = {
15
+ replaceAllOnce,
16
+ replaceOnce,
17
+ };
@@ -0,0 +1,27 @@
1
+ const fs = require("node:fs");
2
+ const path = require("node:path");
3
+
4
+ const runtimeRoot = __dirname;
5
+ const runtimeFiles = [
6
+ [".vite/build/codex-plus-aboutMetadata.js", "plugins/aboutMetadata.js"],
7
+ [".vite/build/codex-plus-worker.js", "worker.js"],
8
+ ["webview/assets/codex-plus/runtime.js", "runtime.js"],
9
+ ["webview/assets/codex-plus/plugins/aboutMetadata.js", "plugins/aboutMetadata.js"],
10
+ ["webview/assets/codex-plus/plugins/nestedRepositories.js", "plugins/nestedRepositories.js"],
11
+ ["webview/assets/codex-plus/plugins/diagnosticErrors.js", "plugins/diagnosticErrors.js"],
12
+ ["webview/assets/codex-plus/plugins/userBubbleColors.js", "plugins/userBubbleColors.js"],
13
+ ["webview/assets/codex-plus/plugins/projectColors.js", "plugins/projectColors.js"],
14
+ ["webview/assets/codex-plus/plugins/sidebarNameBlur.js", "plugins/sidebarNameBlur.js"],
15
+ ];
16
+
17
+ function codexPlusRuntimeAssets() {
18
+ return runtimeFiles.map(([asarPath, localPath]) => [
19
+ asarPath,
20
+ fs.readFileSync(path.join(runtimeRoot, localPath), "utf8"),
21
+ ]);
22
+ }
23
+
24
+ module.exports = {
25
+ codexPlusRuntimeAssets,
26
+ runtimeFiles,
27
+ };
@@ -0,0 +1,93 @@
1
+ (function (globalObject) {
2
+ const DEFAULT_REPO_URL = "https://github.com/michaelw/codex-plus-patcher";
3
+ const DISCLAIMER_HEADING = "Disclaimer of Warranty and Limitation of Liability";
4
+ const DISCLAIMER_BODY = [
5
+ 'THIS SOFTWARE IS PROVIDED "AS IS," WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED. This is a modified, binary-patched demonstrator provided strictly for experimental or demonstration purposes.',
6
+ "The upstream developers, contributors, and maintainers assume NO responsibility or liability for any errors, malfunctions, data loss, or damages—including consequential or incidental damages—arising from the installation or use of this patched version. You use, test, or distribute this patched app at your sole and absolute risk.",
7
+ "The original authors and upstream suppliers are under no obligation to provide support, updates, fixes, or assistance with any issues, mess, or conflicts caused by this modified build.",
8
+ ].join("\n\n");
9
+
10
+ function buildInfoLines(context = {}) {
11
+ const appliedPatches = Array.isArray(context.appliedPatches) ? context.appliedPatches : [];
12
+ return [
13
+ `Patcher: ${context.patcherRepoUrl || DEFAULT_REPO_URL}`,
14
+ `Patcher commit: ${context.patcherGitSha || "unknown"}`,
15
+ `Source app.asar: ${context.sourceAsarSha256 || "unknown"}`,
16
+ "",
17
+ "Applied patches:",
18
+ ...appliedPatches.map((patchId) => `- ${patchId}`),
19
+ ];
20
+ }
21
+
22
+ function aboutPayload(context = {}) {
23
+ return {
24
+ appDisplayName: "Codex Plus",
25
+ buildInfoLines: buildInfoLines(context),
26
+ disclaimerBody: DISCLAIMER_BODY,
27
+ disclaimerHeading: DISCLAIMER_HEADING,
28
+ };
29
+ }
30
+
31
+ function disclaimerStyles() {
32
+ return [
33
+ " .codex-plus-disclaimer {",
34
+ " width: 100%;",
35
+ " margin: 0 0 12px;",
36
+ " color: var(--muted-text);",
37
+ " text-align: left;",
38
+ " font-size: 9px;",
39
+ " line-height: 1.25;",
40
+ " white-space: pre-wrap;",
41
+ " overflow-wrap: anywhere;",
42
+ " }",
43
+ "",
44
+ " .codex-plus-disclaimer-heading {",
45
+ " margin-bottom: 4px;",
46
+ " font-weight: 700;",
47
+ " }",
48
+ ].join("\n");
49
+ }
50
+
51
+ function disclaimerMarkup({ escape, heading, body }) {
52
+ if (heading == null || body == null || typeof escape !== "function") return "";
53
+ return `<section class="codex-plus-disclaimer" aria-label="${escape(heading)}"><div class="codex-plus-disclaimer-heading">${escape(heading)}</div><div class="codex-plus-disclaimer-body">${escape(body)}</div></section>`;
54
+ }
55
+
56
+ function browserBuildInfo() {
57
+ return {
58
+ heading: DISCLAIMER_HEADING,
59
+ lines: ["Codex Plus runtime plugin layer active", "Plugin: aboutMetadata"],
60
+ };
61
+ }
62
+
63
+ const exportsObject = {
64
+ aboutPayload,
65
+ browserBuildInfo,
66
+ buildInfoLines,
67
+ defaultRepoUrl: DEFAULT_REPO_URL,
68
+ disclaimerBody: DISCLAIMER_BODY,
69
+ disclaimerHeading: DISCLAIMER_HEADING,
70
+ disclaimerMarkup,
71
+ disclaimerStyles,
72
+ };
73
+
74
+ if (typeof module !== "undefined" && module.exports) {
75
+ module.exports = exportsObject;
76
+ }
77
+
78
+ const CodexPlus = globalObject?.CodexPlus;
79
+ if (!CodexPlus) return;
80
+
81
+ CodexPlus.registerPlugin(
82
+ CodexPlus.definePlugin({
83
+ id: "aboutMetadata",
84
+ name: "About Metadata",
85
+ description: "Adds Codex Plus provenance and disclaimer metadata to the About dialog host surface.",
86
+ required: true,
87
+ exports: exportsObject,
88
+ start(api) {
89
+ api.ui.about.addBuildInfo(browserBuildInfo);
90
+ },
91
+ }),
92
+ );
93
+ })(typeof window !== "undefined" ? window : globalThis);
@@ -0,0 +1,43 @@
1
+ (function (globalObject) {
2
+ const DETAIL_CLASS =
3
+ "max-h-80 max-w-full overflow-auto whitespace-pre-wrap rounded-md border border-token-border bg-token-main-surface-secondary p-2 text-left font-vscode-editor text-[11px] leading-4 text-token-text-primary";
4
+
5
+ function diagnosticText(error, componentStack) {
6
+ const base = error?.stack ?? error?.message ?? String(error ?? "");
7
+ if (!base && !componentStack) return "";
8
+ return [base, componentStack].filter(Boolean).join("\n\n");
9
+ }
10
+
11
+ function renderDetails({ jsx, error, componentStack }) {
12
+ if (typeof jsx !== "function") return null;
13
+ const text = diagnosticText(error, componentStack);
14
+ if (!text) return null;
15
+ return jsx("pre", { className: DETAIL_CLASS, children: text });
16
+ }
17
+
18
+ const exportsObject = {
19
+ detailClassName: DETAIL_CLASS,
20
+ diagnosticText,
21
+ renderDetails,
22
+ };
23
+
24
+ if (typeof module !== "undefined" && module.exports) {
25
+ module.exports = exportsObject;
26
+ }
27
+
28
+ const CodexPlus = globalObject?.CodexPlus;
29
+ if (!CodexPlus) return;
30
+
31
+ CodexPlus.registerPlugin(
32
+ CodexPlus.definePlugin({
33
+ id: "diagnosticErrors",
34
+ name: "Diagnostic Errors",
35
+ description: "Adds richer diagnostic context to selected Codex app-shell errors.",
36
+ required: true,
37
+ exports: exportsObject,
38
+ start(api) {
39
+ api.ui.errors.decorateBoundary(renderDetails);
40
+ },
41
+ }),
42
+ );
43
+ })(typeof window !== "undefined" ? window : globalThis);
@@ -0,0 +1,448 @@
1
+ (function () {
2
+ const CodexPlus = window.CodexPlus;
3
+
4
+ function repoKey(repo) {
5
+ return repo?.id ?? repo?.cwd ?? repo?.path ?? "unknown";
6
+ }
7
+
8
+ function label(repo) {
9
+ return repo?.kind === "main" ? "Main" : repo?.label ?? repo?.path ?? "Repository";
10
+ }
11
+
12
+ function sessionKey(hostId, conversationId, cwd) {
13
+ return JSON.stringify([hostId, conversationId, cwd]);
14
+ }
15
+
16
+ function debugText(value) {
17
+ try {
18
+ return JSON.stringify(value, (_key, item) => (typeof item === "bigint" ? String(item) : item), 2) ?? "";
19
+ } catch (error) {
20
+ return `Unable to render debug object: ${error instanceof Error ? error.message : String(error)}`;
21
+ }
22
+ }
23
+
24
+ function Warnings({ warnings }, deps) {
25
+ const { jsx } = deps;
26
+ if (!warnings || warnings.length === 0) return null;
27
+ return jsx("div", {
28
+ className: "px-3 py-2 text-xs text-token-description-foreground",
29
+ children: warnings.map((warning, index) =>
30
+ jsx("div", { children: warning.message ?? warning.type ?? String(warning) }, `${warning.type ?? "warning"}:${warning.path ?? index}`),
31
+ ),
32
+ });
33
+ }
34
+
35
+ function Debug({ debug }, deps) {
36
+ const { jsx, jsxs } = deps;
37
+ if (debug == null) return null;
38
+ const plusToml = debug.plusToml ?? {};
39
+ const readState = plusToml.readOk === true ? "read ok" : "not read";
40
+ const parsedCount = String(plusToml.parsedRepositories ?? 0);
41
+ return jsxs("details", {
42
+ className: "mx-3 mb-2 rounded-md border border-token-border bg-token-main-surface-secondary px-2 py-1 text-xs text-token-description-foreground",
43
+ children: [
44
+ jsxs("summary", { className: "cursor-pointer select-none", children: ["plus.toml debug: ", readState, ", parsed ", parsedCount] }),
45
+ jsx("pre", {
46
+ className: "mt-2 max-h-72 overflow-auto whitespace-pre-wrap font-vscode-editor text-[11px] leading-4 text-token-foreground",
47
+ children: debugText(debug),
48
+ }),
49
+ ],
50
+ });
51
+ }
52
+
53
+ function MainGroup({ children, repo, collapsed, onToggle }, deps) {
54
+ const { jsx, jsxs } = deps;
55
+ return jsxs("section", {
56
+ className: "border-b border-token-border-default",
57
+ children: [
58
+ jsxs("button", {
59
+ type: "button",
60
+ className: "flex w-full items-center justify-between gap-2 px-3 py-2 text-left text-sm text-token-foreground hover:bg-token-list-hover-background",
61
+ onClick: onToggle,
62
+ "aria-expanded": !collapsed,
63
+ children: [
64
+ jsxs("span", {
65
+ className: "min-w-0",
66
+ children: [
67
+ jsx("span", { className: "font-medium", children: label(repo) }),
68
+ jsx("span", { className: "ml-2 text-xs text-token-description-foreground", children: repo?.path ?? "." }),
69
+ ],
70
+ }),
71
+ jsx("span", { className: "shrink-0 text-xs text-token-description-foreground", children: collapsed ? "Show" : "Hide" }),
72
+ ],
73
+ }),
74
+ collapsed ? null : children,
75
+ ],
76
+ });
77
+ }
78
+
79
+ function PlainDiff({ text }, deps) {
80
+ return deps.jsx("pre", {
81
+ className: "mx-3 mb-3 max-h-[520px] overflow-auto whitespace-pre-wrap rounded-md border border-token-border bg-token-main-surface-secondary p-3 font-vscode-editor text-xs leading-5 text-token-foreground",
82
+ children: text,
83
+ });
84
+ }
85
+
86
+ function RepoDiffBody({ cwd, hostConfig, conversationId, diffMode, diffText, statusText, error, isLoading }, deps) {
87
+ const { jsx, createElement, parseDiff, DiffCard, pathValue } = deps;
88
+ if (error != null || isLoading || diffText == null) return PlainDiff({ text: statusText }, deps);
89
+ let parsed;
90
+ try {
91
+ parsed = parseDiff(diffText);
92
+ } catch (parseError) {
93
+ const message = parseError instanceof Error ? parseError.message : String(parseError);
94
+ return PlainDiff({ text: `Unable to parse diff: ${message}\n\n${diffText}` }, deps);
95
+ }
96
+ if (parsed == null || parsed.length === 0) return PlainDiff({ text: statusText }, deps);
97
+ return jsx("div", {
98
+ className: "mx-3 mb-3 flex min-w-0 max-w-none flex-col gap-2",
99
+ children: parsed.map((diff, index) =>
100
+ createElement(DiffCard, {
101
+ key: `${diff.metadata?.newPath ?? diff.metadata?.oldPath ?? index}:${index}`,
102
+ containerClassName: "codex-review-diff-card extension:rounded-lg w-full max-w-none",
103
+ conversationId: conversationId ?? undefined,
104
+ cwd: pathValue(cwd),
105
+ defaultOpen: true,
106
+ diff,
107
+ diffViewWrap: true,
108
+ expandScope: "review",
109
+ fullContentNextFallbackToDisk: true,
110
+ headerVariant: "full-review",
111
+ hostConfig,
112
+ hunkActionsVariant: "unstaged",
113
+ hunkSeparators: diff.metadata?.additionLines ? "line-info" : "metadata",
114
+ roundedCorners: false,
115
+ showFileActions: false,
116
+ showHunkActions: false,
117
+ stickyHeader: false,
118
+ viewType: diffMode ?? "unified",
119
+ }),
120
+ ),
121
+ });
122
+ }
123
+
124
+ function BranchPicker({ repo, hostConfig, baseBranch, setBaseBranch, deps }) {
125
+ const { jsx, jsxs, React, Button, Tooltip, Icon, Dropdown, DropdownMenu, BranchPickerDropdownContent, gitRequest } = deps;
126
+ const [open, setOpen] = React.useState(false);
127
+ const [branches, setBranches] = React.useState([]);
128
+ const [loading, setLoading] = React.useState(false);
129
+ const [error, setError] = React.useState(null);
130
+ const [query, setQuery] = React.useState("");
131
+ const [searchedBranches, setSearchedBranches] = React.useState([]);
132
+ const [searchLoading, setSearchLoading] = React.useState(false);
133
+ const [searchError, setSearchError] = React.useState(null);
134
+ const selected = (baseBranch ?? "").trim();
135
+
136
+ const loadBranches = () => {
137
+ const controller = new AbortController();
138
+ setLoading(true);
139
+ setError(null);
140
+ gitRequest("git")
141
+ .request({
142
+ method: "recent-branches",
143
+ params: { root: repo.root, limit: 100, hostConfig, operationSource: "codex_plus_review" },
144
+ signal: controller.signal,
145
+ })
146
+ .then((result) => setBranches(result?.branches ?? []))
147
+ .catch((loadError) => {
148
+ if (!controller.signal.aborted) setError(loadError instanceof Error ? loadError.message : String(loadError));
149
+ })
150
+ .finally(() => {
151
+ if (!controller.signal.aborted) setLoading(false);
152
+ });
153
+ return controller;
154
+ };
155
+
156
+ React.useEffect(() => {
157
+ if (!open) return undefined;
158
+ const controller = loadBranches();
159
+ return () => controller.abort();
160
+ }, [open, repo.root, hostConfig.id]);
161
+
162
+ React.useEffect(() => {
163
+ if (!open) return undefined;
164
+ const trimmed = query.trim();
165
+ if (!trimmed) {
166
+ setSearchedBranches([]);
167
+ setSearchError(null);
168
+ setSearchLoading(false);
169
+ return undefined;
170
+ }
171
+ const controller = new AbortController();
172
+ const timer = setTimeout(() => {
173
+ setSearchLoading(true);
174
+ setSearchError(null);
175
+ gitRequest("git")
176
+ .request({
177
+ method: "search-branches",
178
+ params: { root: repo.root, query: trimmed, limit: 50, hostConfig, operationSource: "codex_plus_review" },
179
+ signal: controller.signal,
180
+ })
181
+ .then((result) => setSearchedBranches(result?.branches ?? []))
182
+ .catch((searchLoadError) => {
183
+ if (!controller.signal.aborted) setSearchError(searchLoadError instanceof Error ? searchLoadError.message : String(searchLoadError));
184
+ })
185
+ .finally(() => {
186
+ if (!controller.signal.aborted) setSearchLoading(false);
187
+ });
188
+ }, 250);
189
+ return () => {
190
+ clearTimeout(timer);
191
+ controller.abort();
192
+ };
193
+ }, [open, query, repo.root, hostConfig.id]);
194
+
195
+ const title = selected || "Unstaged";
196
+ const button = jsxs(Button, {
197
+ type: "button",
198
+ color: selected ? "ghostActive" : "ghost",
199
+ size: "toolbar",
200
+ className: "max-w-44 min-w-0 shrink-0 border-token-border px-1.5",
201
+ children: [jsx("span", { className: "min-w-0 truncate", children: title }), jsx(Icon, { className: "icon-2xs text-token-input-placeholder-foreground" })],
202
+ });
203
+ const triggerButton = jsx(Tooltip, { tooltipContent: selected ? `Base branch: ${title}` : "Working tree changes", children: button });
204
+ const dropdownContent = jsx(BranchPickerDropdownContent, {
205
+ branches,
206
+ selectedBranch: selected,
207
+ disabled: false,
208
+ isError: error != null,
209
+ isLoading: loading,
210
+ isSearchError: searchError != null,
211
+ isSearchLoading: searchLoading,
212
+ onClose: () => setOpen(false),
213
+ onRetry: loadBranches,
214
+ onRetrySearch: () => setQuery(query),
215
+ onSearchQueryChange: setQuery,
216
+ onSelectBranch: (branch) => {
217
+ setBaseBranch(branch);
218
+ setOpen(false);
219
+ },
220
+ searchedBranches,
221
+ searchQuery: query,
222
+ });
223
+ const unstaged = selected
224
+ ? jsxs(deps.Fragment, {
225
+ children: [
226
+ jsx(DropdownMenu.Separator, {}),
227
+ jsx(DropdownMenu.Item, {
228
+ onSelect: () => {
229
+ setBaseBranch("");
230
+ setOpen(false);
231
+ },
232
+ children: "Show unstaged changes",
233
+ }),
234
+ ],
235
+ })
236
+ : null;
237
+ return jsx(Dropdown, {
238
+ align: "end",
239
+ contentWidth: "menu",
240
+ open,
241
+ onOpenChange: setOpen,
242
+ triggerButton,
243
+ children: jsxs(deps.Fragment, { children: [dropdownContent, unstaged] }),
244
+ });
245
+ }
246
+
247
+ function RepoPatchGroup({ repo, hostConfig, hostId, conversationId, diffMode, baseBranch, setBaseBranch, collapsed, setCollapsed, deps }) {
248
+ const { jsx, jsxs, React, ReviewToolbar, gitRequest, pathValue } = deps;
249
+ const [diffText, setDiffText] = React.useState(null);
250
+ const [loading, setLoading] = React.useState(false);
251
+ const [error, setError] = React.useState(null);
252
+ const [currentBranch, setCurrentBranch] = React.useState(null);
253
+
254
+ React.useEffect(() => {
255
+ let cancelled = false;
256
+ const controller = new AbortController();
257
+ setDiffText(null);
258
+ setError(null);
259
+ setLoading(true);
260
+ gitRequest("git")
261
+ .request({ method: "current-branch", params: { root: repo.root, hostConfig, operationSource: "codex_plus_review" }, signal: controller.signal })
262
+ .then((result) => {
263
+ if (!cancelled) setCurrentBranch(result?.branch ?? null);
264
+ })
265
+ .catch(() => {
266
+ if (!cancelled) setCurrentBranch(null);
267
+ });
268
+
269
+ const selected = (baseBranch ?? "").trim();
270
+ gitRequest("git")
271
+ .request({
272
+ method: "review-patch",
273
+ params: {
274
+ cwd: pathValue(repo.cwd),
275
+ source: selected ? "branch" : "unstaged",
276
+ operationSource: "codex_plus_review",
277
+ hostConfig,
278
+ ...(selected ? { baseBranch: selected } : {}),
279
+ },
280
+ signal: controller.signal,
281
+ })
282
+ .then((result) => {
283
+ if (cancelled) return;
284
+ const text = result?.diff?.type === "success" ? result.diff.unifiedDiff ?? result.diff.diff ?? "" : "";
285
+ setDiffText(text.trim().length > 0 ? text : null);
286
+ })
287
+ .catch((loadError) => {
288
+ if (!cancelled) setError(loadError instanceof Error ? loadError.message : String(loadError));
289
+ })
290
+ .finally(() => {
291
+ if (!cancelled) setLoading(false);
292
+ });
293
+ return () => {
294
+ cancelled = true;
295
+ controller.abort();
296
+ };
297
+ }, [repo.cwd, repo.root, hostConfig.id, baseBranch]);
298
+
299
+ const statusText = error ?? (loading ? "Loading diff..." : diffText == null ? "No changes" : diffText);
300
+ return jsxs("section", {
301
+ className: "border-b border-token-border-default",
302
+ children: [
303
+ jsxs("div", {
304
+ className: "flex min-w-0 items-center gap-2 px-3 py-2",
305
+ children: [
306
+ jsxs("button", {
307
+ type: "button",
308
+ className: "min-w-0 flex-1 text-left hover:bg-token-list-hover-background",
309
+ onClick: () => setCollapsed(!collapsed),
310
+ "aria-expanded": !collapsed,
311
+ children: [
312
+ jsx("div", { className: "truncate text-sm font-medium text-token-foreground", children: label(repo) }),
313
+ jsx("div", {
314
+ className: "truncate text-xs text-token-description-foreground",
315
+ children: [repo.kind, repo.path ?? "", currentBranch ? ` - ${currentBranch}` : ""].filter(Boolean).join(" / "),
316
+ }),
317
+ ],
318
+ }),
319
+ 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),
328
+ ],
329
+ }),
330
+ collapsed ? null : RepoDiffBody({ cwd: repo.cwd, hostConfig, conversationId, diffMode, diffText, statusText, error, isLoading: loading }, deps),
331
+ ],
332
+ });
333
+ }
334
+
335
+ function ReviewMux(props, deps) {
336
+ const { jsx, jsxs, React, useStore, useAtom, routeAtom, cwdAtom, hostIdAtom, hostConfigAtom, conversationIdAtom, gitRequest, pathValue, DefaultReview } = deps;
337
+ const routeStore = useStore(routeAtom);
338
+ const cwd = useAtom(cwdAtom);
339
+ const hostId = useAtom(hostIdAtom);
340
+ const hostConfig = useAtom(hostConfigAtom);
341
+ const conversationAtomValue = useAtom(conversationIdAtom);
342
+ const conversationId = routeStore.value.routeKind === "local-thread" ? routeStore.value.conversationId : null;
343
+ const [targets, setTargets] = React.useState(null);
344
+ const [collapsed, setCollapsedState] = React.useState(() => new Map());
345
+ const [baseBranches, setBaseBranches] = React.useState(() => new Map());
346
+ const mainReviewContent = props.mainReviewContent;
347
+ const upstreamReview = React.useMemo(
348
+ () => mainReviewContent ?? jsx(DefaultReview, props),
349
+ [mainReviewContent, props.diffRefs, props.diffMode, props.isCappedMode, props.reviewDiffMetrics, props.showReviewGitActions],
350
+ );
351
+
352
+ React.useEffect(() => {
353
+ if (cwd == null || hostConfig == null) {
354
+ setTargets(null);
355
+ return undefined;
356
+ }
357
+ let cancelled = false;
358
+ const controller = new AbortController();
359
+ gitRequest("git")
360
+ .request({
361
+ method: "repository-targets",
362
+ params: { cwd: pathValue(cwd), hostId, hostConfig, operationSource: "codex_plus_review" },
363
+ signal: controller.signal,
364
+ })
365
+ .then((result) => {
366
+ if (!cancelled) setTargets(result);
367
+ })
368
+ .catch((error) => {
369
+ if (!cancelled) setTargets({ main: null, repositories: [], warnings: [{ type: "load-error", message: error instanceof Error ? error.message : String(error) }] });
370
+ });
371
+ return () => {
372
+ cancelled = true;
373
+ controller.abort();
374
+ };
375
+ }, [cwd, hostId, hostConfig?.id]);
376
+
377
+ const main = targets?.main ?? (cwd == null ? null : { id: `main:${cwd}`, kind: "main", path: ".", label: "Main", cwd });
378
+ const repositories = targets?.repositories ?? [];
379
+ const all = [main, ...repositories].filter(Boolean);
380
+ if (main == null || (all.length <= 1 && (!targets?.warnings || targets.warnings.length === 0) && targets?.debug == null)) return upstreamReview;
381
+
382
+ const session = sessionKey(hostId, conversationId ?? conversationAtomValue, cwd);
383
+ const keyFor = (repo) => `${session}:${repoKey(repo)}`;
384
+ const isCollapsed = (repo) => collapsed.get(keyFor(repo)) === true;
385
+ const setCollapsed = (repo, next) =>
386
+ setCollapsedState((current) => {
387
+ const copy = new Map(current);
388
+ if (next) copy.set(keyFor(repo), true);
389
+ else copy.delete(keyFor(repo));
390
+ return copy;
391
+ });
392
+ const setBaseBranch = (repo, branch) =>
393
+ setBaseBranches((current) => {
394
+ const copy = new Map(current);
395
+ copy.set(keyFor(repo), branch);
396
+ return copy;
397
+ });
398
+
399
+ return jsxs("div", {
400
+ className: "flex h-full min-h-0 w-full min-w-0 flex-1 flex-col overflow-x-hidden overflow-y-auto",
401
+ children: [
402
+ jsx("div", { className: "px-3 py-1 text-[11px] font-medium uppercase tracking-wide text-token-description-foreground", children: "Codex Plus repositories" }),
403
+ Warnings({ warnings: targets?.warnings ?? [] }, deps),
404
+ Debug({ debug: targets?.debug }, deps),
405
+ main ? MainGroup({ repo: main, collapsed: isCollapsed(main), onToggle: () => setCollapsed(main, !isCollapsed(main)), children: upstreamReview }, deps) : upstreamReview,
406
+ repositories.map((repo) =>
407
+ jsx(
408
+ RepoPatchGroup,
409
+ {
410
+ repo,
411
+ hostConfig,
412
+ hostId,
413
+ conversationId,
414
+ diffMode: props.diffMode,
415
+ baseBranch: baseBranches.get(keyFor(repo)) ?? "",
416
+ setBaseBranch: (branch) => setBaseBranch(repo, branch),
417
+ collapsed: isCollapsed(repo),
418
+ setCollapsed: (next) => setCollapsed(repo, next),
419
+ deps,
420
+ },
421
+ repoKey(repo),
422
+ ),
423
+ ),
424
+ ],
425
+ });
426
+ }
427
+
428
+ CodexPlus.registerPlugin(
429
+ CodexPlus.definePlugin({
430
+ id: "nestedRepositories",
431
+ name: "Nested Repositories",
432
+ description: "Hosts nested repository review panel behavior and worker bridge requests.",
433
+ required: true,
434
+ exports: {
435
+ ReviewMux,
436
+ repoKey,
437
+ },
438
+ start(api) {
439
+ api.ui.review.wrapBody((props, deps) => ReviewMux(props, deps));
440
+ api.modules.registerHostModule("codex-plus:native:repository-targets", {
441
+ request(params) {
442
+ return api.native.request("repository-targets", params);
443
+ },
444
+ });
445
+ },
446
+ }),
447
+ );
448
+ })();