minecodex 2.0.4 → 2.0.5

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.
Files changed (38) hide show
  1. package/DESIGN.md +25 -1
  2. package/README.md +4 -2
  3. package/features/images/DESIGN.md +106 -41
  4. package/features/images/README.md +25 -16
  5. package/features/images/codex-feature.json +6 -3
  6. package/features/images/src/adapters/codex/image-prompt.mjs +24 -2
  7. package/features/images/src/adapters/codex/image-sources.mjs +19 -0
  8. package/features/images/src/adapters/codex/library-api.mjs +2 -0
  9. package/features/images/src/adapters/codex/library-viewer.mjs +26 -2
  10. package/features/images/src/adapters/codex/native-image-source.mjs +556 -0
  11. package/features/images/src/backend/local-image-source.mjs +138 -0
  12. package/features/images/src/frontend/cloud-image-actions.mjs +39 -0
  13. package/features/images/src/frontend/local-image-source.mjs +194 -0
  14. package/features/images/src/library-prompt-page.mjs +158 -53
  15. package/features/images/src/library-prompt-source.mjs +11 -3
  16. package/features/images/src/prompt-index.mjs +14 -2
  17. package/features/model-slider/DESIGN.md +2 -0
  18. package/features/model-slider/src/frontend/install.mjs +78 -11
  19. package/features/notes/DESIGN.md +7 -3
  20. package/features/notes/src/frontend/entry.mjs +26 -9
  21. package/features/session-status/DESIGN.md +4 -2
  22. package/features/session-status/src/adapters/codex/session-log.mjs +2 -1
  23. package/features/session-status/src/domain/session-stats.mjs +17 -9
  24. package/features/session-status/src/session-status.mjs +47 -11
  25. package/package.json +1 -1
  26. package/packages/codex-adapter/README.md +2 -1
  27. package/packages/codex-adapter/src/renderer/environment.mjs +24 -6
  28. package/packages/runtime-host/src/codex-injection.mjs +3 -6
  29. package/packages/runtime-host/src/codex-runtime.mjs +16 -5
  30. package/packages/runtime-host/src/feature-registry.mjs +1 -0
  31. package/packages/ui/DESIGN.md +5 -2
  32. package/packages/ui/src/components/button.mjs +2 -0
  33. package/packages/ui/src/components/navigation-rail.mjs +18 -10
  34. package/packages/ui/src/components/page.mjs +9 -2
  35. package/packages/ui/src/install.mjs +3 -1
  36. package/packages/ui/src/native/navigation-rail.mjs +6 -1
  37. package/packages/ui/src/native/page.mjs +23 -0
  38. package/packages/ui/src/native/tokens.mjs +3 -0
@@ -0,0 +1,556 @@
1
+ // 原生组件、构建产物与 React 上下文只在此适配;不复制图库或编辑器。
2
+ export function createNativeImageSource(codex, { read = async url => (await fetch(url)).text(), load = url => import(url), requestHostAction, libraryApi, createCloudImageActions } = {}) {
3
+ let connecting;
4
+ const opened = new Map();
5
+
6
+ function imported(source, local) {
7
+ for (const match of source.matchAll(/import\{([^}]+)\}from["']([^"']+)["']/g)) {
8
+ for (const binding of match[1].split(",")) {
9
+ const parts = binding.trim().split(/\s+as\s+/);
10
+ if (parts.at(-1) === local) return { name: parts[0], file: match[2] };
11
+ }
12
+ }
13
+ throw new Error(`Native Images import contract changed: ${local}`);
14
+ }
15
+
16
+ async function connect() {
17
+ if (!connecting) connecting = (async () => {
18
+ const urls = [...document.querySelectorAll('link[rel="modulepreload"],script[src]')].map(node => node.href || node.src);
19
+ const initialUrl = urls.find(url => /\/app-initial-[^/]+\.js/.test(url));
20
+ const mainUrl = urls.find(url => /\/app-main-[^/]+\.js/.test(url));
21
+ if (!initialUrl || !mainUrl) throw new Error("Native Images modules are unavailable");
22
+ const [initialSource, mainSource] = await Promise.all([read(initialUrl), read(mainUrl)]);
23
+ const asset = (name, references = initialSource) => {
24
+ const file = references.match(new RegExp(`(?:["'./])(${name}-[\\w-]+\\.js)`))?.[1];
25
+ if (!file) throw new Error(`Native Images capability is unavailable: ${name}`);
26
+ return new URL(file, initialUrl).href;
27
+ };
28
+ const module = async (name,references) => {
29
+ const url = asset(name,references);
30
+ const [source, exports] = await Promise.all([read(url), load(url)]);
31
+ // 调用产物公开的惰性初始化器,不扫描或执行任意导出函数。
32
+ for (const value of Object.values(exports)) {
33
+ if (typeof value === "function" && /^function ([\w$]+)\(\)\{return\(\1=/.test(String(value))) value();
34
+ }
35
+ return {url, source, exports};
36
+ };
37
+ const [masonry, layout, sourceImage, menu, selection, initial] = await Promise.all([
38
+ module("library-masonry"), module("library-item-layout"), module("generated-image-source"),
39
+ module("generated-image-context-menu"), module("library-selection-toolbar"), load(initialUrl),
40
+ ]);
41
+ const [moveDialog,dialogs] = await Promise.all([module("library-move-dialog",selection.source),module("library-item-dialogs",selection.source)]);
42
+ const [canvas,canvasLayout,filePreview] = await Promise.all([
43
+ module("image-side-panel"),module("preview-layout"),module("library-cloud-file-preview"),
44
+ ]);
45
+ const component = (exports, predicate, name) => {
46
+ const matches = Object.values(exports).filter(value => typeof value === "function" && predicate(String(value)));
47
+ if (matches.length !== 1) throw new Error(`Native Images contract changed: ${name}`);
48
+ return matches[0];
49
+ };
50
+ const binding = async (source, url, local) => {
51
+ if (!local) throw new Error("Native Images component contract changed");
52
+ const importedBinding = imported(source, local);
53
+ return (await load(new URL(importedBinding.file, url).href))[importedBinding.name];
54
+ };
55
+ const routerLocal = selection.source.match(/get\(([\w$]+)\)\.prepareNavigation\(/)?.[1];
56
+ const routerAtom = await binding(selection.source, selection.url, routerLocal);
57
+ const switchLocal = selection.source.match(/([\w$]+)\(e,i\);let _=t.length===1/)?.[1];
58
+ const switchMode = await binding(selection.source, selection.url, switchLocal);
59
+ const draftLocal = selection.source.match(/let u=([\w$]+)\(e,\[\]\);e.set/)?.[1];
60
+ const createChatDraft = await binding(selection.source, selection.url, draftLocal);
61
+ const rootOwner = mainSource.match(/\(0,([\w$]+)\.createRoot\)/)?.[1];
62
+ const rootInit = rootOwner && mainSource.match(new RegExp(`${rootOwner}=([\\w$]+)\\(\\)`))?.[1];
63
+ const reactOwner = masonry.source.match(/([\w$]+)\.useRef\b/)?.[1];
64
+ const reactInit = reactOwner && masonry.source.match(new RegExp(`${reactOwner}=([\\w$]+)\\(\\)`))?.[1];
65
+ const [reactFactory, rootFactory] = await Promise.all([
66
+ binding(masonry.source, masonry.url, reactInit), binding(mainSource, mainUrl, rootInit),
67
+ ]);
68
+ const React = reactFactory(), ReactDOM = rootFactory();
69
+ const portalOwner=canvas.source.match(/\(0,([\w$]+)\.createPortal\)/)?.[1];
70
+ const portalInit=portalOwner&&canvas.source.match(new RegExp(`${portalOwner}=([\\w$]+)\\(\\)`))?.[1];
71
+ const createPortal=(await binding(canvas.source,canvas.url,portalInit))().createPortal;
72
+ const dropdownLocal = selection.source.match(/\(0,[\w$]+\.jsxs?\)\(([\w$]+),\{contentWidth:`menu`/)?.[1];
73
+ const itemsLocal = selection.source.match(/\(0,[\w$]+\.jsxs?\)\(([\w$]+)\.Item,\{/)?.[1];
74
+ const buttonLocal = selection.source.match(/\(0,[\w$]+\.jsx\)\(([\w$]+),\{ref:[^,]+,color:`mediaOverlay`/)?.[1];
75
+ const iconLocal = selection.source.match(/color:`mediaOverlay`[^;]+?children:\(0,[\w$]+\.jsx\)\(([\w$]+),\{\}\)/)?.[1];
76
+ const [Dropdown, Menu, Button, MoreIcon] = await Promise.all([dropdownLocal, itemsLocal, buttonLocal, iconLocal].map(local => binding(selection.source, selection.url, local)));
77
+ const checkboxFunction = component(selection.exports,source=>/onCheckedChange:/.test(source)&&/viewMode:/.test(source),"selection checkbox");
78
+ const checkboxLocal = String(checkboxFunction).match(/\(0,[\w$]+\.jsx\)\(([\w$]+),\{className:`cursor-interaction`,variant:/)?.[1];
79
+ const Checkbox = await binding(selection.source,selection.url,checkboxLocal);
80
+ const rangeSelection = component(selection.exports,source=>/^function [\w$]+\(\{orderedIds:/.test(source)&&/extendRange:/.test(source),"range selection");
81
+ const selectionLocal = selection.source.match(/\.set\(([\w$]+),new Set\)/)?.[1];
82
+ const selectionExport = selection.source.match(/export\{([^}]+)\}/)?.[1].split(",").map(value=>value.trim().split(/\s+as\s+/)).find(parts=>parts[0]===selectionLocal)?.at(-1);
83
+ const nativeSelectedIds = selection.exports[selectionExport];
84
+ if (!nativeSelectedIds) throw new Error("Native selection state contract changed");
85
+ const clearPrefix = selection.source.slice(selection.source.indexOf("id:`libraryNext.selection.clear`"));
86
+ const clearLocal = clearPrefix.match(/\(0,[\w$]+\.jsx\)\(([\w$]+),\{\}\)/)?.[1];
87
+ const ClearIcon = await binding(selection.source,selection.url,clearLocal);
88
+ const dialogLocal = dialogs.source.match(/\(0,[\w$]+\.jsx\)\(([\w$]+),\{open:[^,]+,showDialogClose:!1/)?.[1];
89
+ const contentLocal = dialogs.source.match(/\(0,[\w$]+\.jsx\)\(([\w$]+),\{as:`form`,onSubmit:/)?.[1];
90
+ const [Dialog,DialogContent] = await Promise.all([dialogLocal,contentLocal].map(local=>binding(dialogs.source,dialogs.url,local)));
91
+ const menuCoreFile = menu.source.match(/from["']([^"']*generated-image-context-menu-[^"']+)["']/)?.[1];
92
+ const menuCoreUrl = menuCoreFile ? new URL(menuCoreFile,menu.url).href : menu.url;
93
+ const menuCoreSource = menuCoreFile ? await read(menuCoreUrl) : menu.source;
94
+ const imageActions = component(menu.exports,source=>/onOpenedInNewTab:/.test(source),"image actions");
95
+ const imageActionSource = String(imageActions);
96
+ const imageFileLocal = imageActionSource.match(/let\{file:[\w$]+\}=([\w$]+)\(/)?.[1];
97
+ const attachLocal = imageActionSource.match(/await ([\w$]+)\(e,i,\[n\],/)?.[1];
98
+ const downloadLocal = imageActionSource.match(/await ([\w$]+)\(\{downloadSrc:/)?.[1];
99
+ const [imageFile,attachChatImages,downloadImage] = await Promise.all([imageFileLocal,attachLocal,downloadLocal].map(local=>binding(menuCoreSource,menuCoreUrl,local)));
100
+ const toolbarClass = selection.source.match(/className:`(pointer-events-auto flex h-toolbar[^`]+)`/)?.[1];
101
+ const toolbarPositionClass = selection.source.match(/className:`(pointer-events-none fixed inset-x-4 bottom-6[^`]+)`/)?.[1];
102
+ if (!toolbarClass || !toolbarPositionClass) throw new Error("Native selection toolbar layout changed");
103
+ const libraryIcon = async label => {
104
+ const marker = selection.source.indexOf(`defaultMessage:\`${label}\``);
105
+ const prefix = selection.source.slice(Math.max(0, marker - 500), marker);
106
+ const match = [...prefix.matchAll(/leftIcon:\(0,[\w$]+\.jsx\)\(([\w$]+),\{icon:\{(assets:\{)?16:([\w$]+),20:([\w$]+)/g)].at(-1);
107
+ if (!match) throw new Error(`Native Library icon contract changed: ${label}`);
108
+ const [Icon, small, medium] = await Promise.all([match[1], match[3], match[4]].map(local => binding(selection.source, selection.url, local)));
109
+ const sizes = {16:small, 20:medium};
110
+ return React.createElement(Icon, {icon:match[2] ? {assets:sizes} : sizes});
111
+ };
112
+ const upload = component(initial, source => /async function/.test(source) && /libraryPersistenceMode:/.test(source) && /uploadPurpose:/.test(source), "Library upload");
113
+ const setFavorite = component(initial, source => /target_ref:/.test(source) && /showToast:/.test(source), "Library favorite");
114
+ const auth = String(setFavorite).match(/accountId:e.get\(([\w$]+)\),userId:e.get\(([\w$]+)\)/);
115
+ const [accountAtom,userAtom] = await Promise.all([auth?.[1],auth?.[2]].map(local => binding(initialSource,initialUrl,local)));
116
+ if (!accountAtom || !userAtom) throw new Error("Native Library account contract changed");
117
+ const toastLocal = selection.source.match(/get\(([\w$]+)\)\.danger\(/)?.[1];
118
+ const toastAtom = await binding(selection.source, selection.url, toastLocal);
119
+ const favoritePrefix = selection.source.slice(Math.max(0,selection.source.indexOf("id:`library.favorites.add`")-2000),selection.source.indexOf("id:`library.favorites.add`")+500);
120
+ const favoriteAsset = favoritePrefix.match(/\{16:([\w$]+),20:([\w$]+)\}:\{16:([\w$]+),20:([\w$]+)\}/);
121
+ if (!favoriteAsset) throw new Error("Native Library favorite icon contract changed");
122
+ const favoriteIconLocal = favoritePrefix.match(/icon:\{assets:/)?.index;
123
+ const favoriteIconOwner = favoritePrefix.slice(0,favoriteIconLocal).match(/\(0,[\w$]+\.jsx\)\(([\w$]+),\{$/)?.[1];
124
+ const [FavoriteIcon, savedSmall, savedMedium, emptySmall, emptyMedium] = await Promise.all([favoriteIconOwner,...favoriteAsset.slice(1)].map(local => binding(selection.source,selection.url,local)));
125
+ const favoriteIcon = saved => React.createElement(FavoriteIcon,{icon:{assets:saved?{16:savedSmall,20:savedMedium}:{16:emptySmall,20:emptyMedium}}});
126
+ const [chatIcon, downloadIcon, folderIcon, deleteIcon] = await Promise.all([
127
+ libraryIcon("Chat about this"), libraryIcon("Download"), libraryIcon("Add to folder"), libraryIcon("{kind, select, directory {Delete folder} other {Delete}}"),
128
+ ]);
129
+ // 文件画布使用原生 Home Composer 的控制器、附件 scope 和输入组件。
130
+ const composerLocals=["kr","Qn","$n","rr","ke","hn","tn","P"];
131
+ const [useComposerController,useHomeComposer,Composer,ComposerSurface,ScopeProvider,composerScope,createComposerValue,composerRouteScope] = await Promise.all(composerLocals.map(local=>binding(filePreview.source,filePreview.url,local)));
132
+ if (!React.createElement || !ReactDOM.createRoot || !Menu.Item) throw new Error("Native Images rendering contract changed");
133
+ return {
134
+ React, ReactDOM, createPortal, Dropdown, Menu, Button, MoreIcon, chatIcon, downloadIcon, folderIcon, favoriteIcon, routerAtom, switchMode, createChatDraft, upload, setFavorite, toastAtom, accountAtom, userAtom,
135
+ MoveDialog:moveDialog.exports.MoveLibraryItemsDialog,
136
+ Checkbox,rangeSelection,nativeSelectedIds,ClearIcon,Dialog,DialogContent,ConfirmationContent:dialogs.exports.LibraryItemConfirmationContent,
137
+ Canvas:canvas.exports.t,CanvasLayout:canvasLayout.exports.r,
138
+ useComposerController,useHomeComposer,Composer,ComposerSurface,ScopeProvider,composerScope,createComposerValue,composerRouteScope,
139
+ editImage:component(initial,source=>/async function/.test(source)&&/requireDispatchAcceptance:/.test(source)&&/sourceAttachment:/.test(source)&&/imageVariant/.test(source),"image edit submission"),
140
+ imageFile,attachChatImages,downloadImage,toolbarClass,toolbarPositionClass,deleteIcon,
141
+ Masonry: component(masonry.exports, source => /\{items:[^}]+animate:/.test(source), "masonry"),
142
+ Skeleton: component(masonry.exports, source => /square:/.test(source) && /Loading library/.test(source), "skeleton"),
143
+ Card: component(layout.exports, source => /actionIndicator:/.test(source) && /documentHeader:/.test(source), "card"),
144
+ useImage: component(sourceImage.exports, source => /shouldLoadFileDataUrl:/.test(source), "image resolver"),
145
+ imageMenu: component(menu.exports, source => /onOpenedInNewTab:/.test(source), "image actions"),
146
+ openImage: component(initial, source => /availableImageCount:/.test(source) && /openInEditor:/.test(source) && /entrypoint:/.test(source), "image preview"),
147
+ };
148
+ })().catch(error => { connecting = null; throw error; });
149
+ return connecting;
150
+ }
151
+
152
+ function environment(anchor) {
153
+ const providers = [];
154
+ const seen = new Set();
155
+ let scope, libraryScope, intl;
156
+ const inspect = value => {
157
+ const candidate = value?.current ?? value;
158
+ if (!libraryScope && candidate?.value?.basePath && candidate.value.accountId && typeof candidate.get === "function") libraryScope = candidate;
159
+ if (!scope && candidate?.value?.routeKind && typeof candidate.get === "function" && typeof candidate.set === "function" && typeof candidate.query?.getOrFetch === "function") scope = candidate;
160
+ };
161
+ let current, element = anchor;
162
+ while (element && !current) {
163
+ current = codex.currentFiber(element);
164
+ element = element.parentElement;
165
+ }
166
+ for (let fiber = current; fiber; fiber = fiber.return) {
167
+ if (fiber.tag === 10 && !seen.has(fiber.type)) {
168
+ seen.add(fiber.type);
169
+ const value = fiber.memoizedProps?.value;
170
+ providers.push({type:fiber.type, value});
171
+ if (!intl && typeof value?.formatMessage === "function") intl = value;
172
+ }
173
+ inspect(fiber.memoizedProps);
174
+ for (const value of Object.values(fiber.memoizedProps ?? {})) inspect(value);
175
+ for (let hook = fiber.memoizedState, count = 0; hook && count < 140; hook = hook.next, count++) inspect(hook.memoizedState);
176
+ }
177
+ if (!scope || !intl) throw new Error("Native Images context is unavailable");
178
+ return {providers, scope, libraryScope, intl};
179
+ }
180
+
181
+ function navigationContract(anchor) {
182
+ for (let fiber = codex.currentFiber(anchor); fiber; fiber = fiber.return) {
183
+ const props = fiber.memoizedProps;
184
+ if (typeof fiber.type === "function" && props?.variant === "browse"
185
+ && props.tabs?.some(item => item.key === "generated")
186
+ && props.tabs?.some(item => item.key === "uploaded")) return {type:fiber.type, props};
187
+ }
188
+ throw new Error("Native image navigation contract changed");
189
+ }
190
+
191
+ async function mountNavigation(container, anchor, onSelect) {
192
+ const native = await connect();
193
+ const context = environment(anchor);
194
+ const root = native.ReactDOM.createRoot(container);
195
+ return {
196
+ render(selectedKey) {
197
+ const contract = navigationContract(anchor);
198
+ const props = {
199
+ ...contract.props,
200
+ selectedKey,
201
+ tabs:[...contract.props.tabs, {key:"imagegen", name:"Image Gen"}],
202
+ onSelect:key => {
203
+ if (key === "imagegen") context.libraryScope?.set(native.nativeSelectedIds,new Set());
204
+ onSelect(key);
205
+ if (key !== "imagegen") navigationContract(anchor).props.onSelect(key);
206
+ },
207
+ };
208
+ let tree = native.React.createElement(contract.type, props);
209
+ for (const provider of context.providers) {
210
+ tree = native.React.createElement(provider.type, {value:provider.value}, tree);
211
+ }
212
+ root.render(tree);
213
+ },
214
+ dispose() {root.unmount();},
215
+ };
216
+ }
217
+
218
+ async function mount(container, anchor, {onOpen, onPrompt, onRetry, onHidden}) {
219
+ const native = await connect();
220
+ const context = environment(anchor);
221
+ const {React, ReactDOM} = native;
222
+ const h = React.createElement;
223
+ let failure, currentContent, folderDialog, removeDialog, canvasImage, disposed=false, batchBusy=false;
224
+ let currentImages=[],currentLoading=true,selectedIds=new Set(),selectionAnchor=null;
225
+ const scope = context.libraryScope;
226
+ if (!scope) throw new Error("Native Library context is unavailable");
227
+ const {accountId,userId} = scope.value;
228
+ const message = (id,defaultMessage) => context.intl.formatMessage({id,defaultMessage});
229
+ const toast = () => scope.get(native.toastAtom);
230
+ const assertAccount = () => {
231
+ if (disposed || scope.get(native.accountAtom) !== accountId || scope.get(native.userAtom) !== userId) throw new Error("Library account changed");
232
+ };
233
+ const reference = (image,operation,extra={}) => requestHostAction("local-image-source",{operation,id:image.id,accountId,userId,...extra});
234
+ async function favoriteStatus(id) {
235
+ const result = await libraryApi.favoriteStatus(id);
236
+ const status = result.items?.find(item => item.target_ref?.library_file_id === id);
237
+ if (status?.availability !== "available") throw new Error("Library favorite status is unavailable");
238
+ return status.favorite?.favorite_id ?? null;
239
+ }
240
+ const cloud = createCloudImageActions({
241
+ reference, assertAccount,
242
+ fileNode:id => libraryApi.fileNode(id), favoriteStatus,
243
+ setFavorite:(libraryFileId,favoriteId,isFavorited) => native.setFavorite(scope,{libraryFileId,favoriteId,isFavorited,surface:"library_grid",showToast:true}),
244
+ upload:async file => {
245
+ const bytes = Uint8Array.from(atob(file.base64),value => value.charCodeAt(0));
246
+ const pendingToast = toast().info(message("minecodex.images.saving","Saving image to Library…"),{duration:0,hasCloseButton:false});
247
+ try {
248
+ const uploaded = await native.upload(scope,new File([bytes],file.name,{type:"image/png"}),{
249
+ libraryPersistenceMode:"required",storeInLibrary:true,uploadPurpose:"library",beforeRequest:assertAccount,
250
+ });
251
+ return uploaded.libraryFileId;
252
+ } finally {pendingToast.close();}
253
+ },
254
+ });
255
+ async function folder(image) {
256
+ const images=Array.isArray(image)?image:[image];
257
+ const registered=[];
258
+ for(const item of images)registered.push(await cloud.registered(item));
259
+ const entries=registered.map(item=>item.entry),created=registered.some(item=>item.created);
260
+ let moved = false;
261
+ folderDialog = h(native.MoveDialog,{
262
+ accountId,userId,entries,initialPath:[],
263
+ onMoved:() => {moved=true;},
264
+ onClose:() => {
265
+ folderDialog=null;render(currentContent);
266
+ if (!moved) toast().info(created
267
+ ? message("minecodex.images.folderCanceledSaved","Image saved to Library, but not added to a folder.")
268
+ : message("minecodex.images.folderCanceled","No folder selected."));
269
+ },
270
+ });
271
+ render(currentContent);
272
+ }
273
+ function operationError(error) {
274
+ if (disposed) return;
275
+ toast().danger(error.code === "UPLOAD_UNCONFIRMED"
276
+ ? message("minecodex.images.uploadUnconfirmed","Image upload is pending or could not be confirmed. Check Library before uploading again.")
277
+ : message("minecodex.images.libraryActionFailed","Couldn’t complete the Library action. Please try again."));
278
+ console.error("MineCodex Library action",error);
279
+ }
280
+ const root = ReactDOM.createRoot(container, {onUncaughtError:error => {failure=error;onRetry(error);}});
281
+ function render(content) {
282
+ currentContent = content;
283
+ let tree = h(React.Fragment,null,content,folderDialog,removeDialog,selectionToolbar(),canvasImage?h(ImageCanvas,{key:canvasImage.id,image:canvasImage}):null);
284
+ for (const provider of context.providers) tree = h(provider.type, {value:provider.value}, tree);
285
+ root.render(tree);
286
+ }
287
+ function refresh() {
288
+ render(currentLoading ? h(native.Skeleton) : h("div",{className:"pt-4",children:h(native.Masonry,{items:currentImages.map(image=>({id:image.id,content:h(ImageCard,{image})})),animate:true})}));
289
+ }
290
+ function select(image,checked,extendRange=false) {
291
+ if (batchBusy) return;
292
+ const next=native.rangeSelection({orderedIds:currentImages.map(item=>item.id),selectedIds,anchor:selectionAnchor,id:image.id,selected:checked,extendRange});
293
+ selectedIds=next.selectedIds;selectionAnchor=next.anchor;refresh();
294
+ }
295
+ const selectedImages = () => currentImages.filter(image=>selectedIds.has(image.id));
296
+ function clearSelection() {selectedIds=new Set();selectionAnchor=null;removeDialog=null;refresh();}
297
+ async function performBatch(action) {
298
+ if (batchBusy) return;
299
+ batchBusy=true;refresh();
300
+ try {await action();} catch(error) {operationError(error);} finally {batchBusy=false;if(!disposed)refresh();}
301
+ }
302
+ function confirmRemove(images) {
303
+ let pending=false;
304
+ const close=()=>{if(pending)return;removeDialog=null;refresh();};
305
+ const update=()=>{
306
+ removeDialog=h(native.Dialog,{open:true,showDialogClose:false,onOpenChange:open=>{if(!open)close();}},
307
+ h(native.DialogContent,{as:"form",onSubmit:event=>{
308
+ event.preventDefault();if(pending)return;pending=true;update();refresh();
309
+ void requestHostAction("local-image-source",{operation:"hide",ids:images.map(image=>image.id)}).then(result=>{
310
+ if(disposed)return;
311
+ const hidden=new Set(result.hiddenIds);
312
+ currentImages=currentImages.filter(image=>!hidden.has(image.id));
313
+ selectedIds=new Set([...selectedIds].filter(id=>!hidden.has(id)));
314
+ pending=false;removeDialog=null;onHidden?.(result.hiddenIds);refresh();
315
+ toast().info(message("minecodex.images.hidden","Hidden from Image Gen"));
316
+ }).catch(error=>{pending=false;update();refresh();operationError(error);});
317
+ }},h(native.ConfirmationContent,{
318
+ title:context.intl.formatMessage({id:"minecodex.images.hideTitle",defaultMessage:"Delete {count, plural, one {image} other {# images}} from Image Gen?"},{count:images.length}),
319
+ description:message("minecodex.images.hideDescription","These images will only be hidden from Image Gen. Local originals and cloud copies will be kept."),
320
+ pending,confirmLabel:message("libraryNext.actions.delete","Delete"),onClose:close,
321
+ })));
322
+ };
323
+ update();refresh();
324
+ }
325
+ function selectionToolbar() {
326
+ if (!selectedIds.size) return null;
327
+ const images=selectedImages();
328
+ const button=(color,label,icon,onClick)=>h(native.Button,{color,radius:"full",size:"toolbar",disabled:batchBusy,onClick,children:[icon,label]});
329
+ const more=h(native.Dropdown,{surface:"opaque",contentWidth:"sm",align:"end",
330
+ triggerButton:h(native.Button,{color:"ghostActive",radius:"full",size:"toolbar",uniform:true,disabled:batchBusy,"aria-label":message("libraryNext.selection.more","More actions"),children:h(native.MoreIcon)}),
331
+ children:h(native.Menu.Item,{leftIcon:native.folderIcon,onSelect:()=>{void performBatch(()=>folder(images));},children:message("libraryNext.selection.move","Move")}),
332
+ });
333
+ return h("div",{className:native.toolbarPositionClass,"data-theme":"dark",children:h("div",{className:native.toolbarClass,role:"toolbar","aria-label":message("libraryNext.selection.actions","Selected item actions"),children:[
334
+ h("span",{className:"min-w-24 shrink-0 whitespace-nowrap tabular-nums select-none",children:context.intl.formatMessage({id:"libraryNext.selection.count",defaultMessage:"{count} selected"},{count:images.length})}),
335
+ button("mediaSolid",message("libraryNext.selection.chat","Start chat"),native.chatIcon,()=>{void performBatch(()=>startChat(images));}),
336
+ button("outline",message("libraryNext.actions.download","Download"),native.downloadIcon,()=>{void performBatch(async()=>{
337
+ for(const image of images){const file=await reference(image,"upload-file");if(await native.downloadImage({downloadSrc:`data:image/png;base64,${file.base64}`,suggestedFilename:file.name})){selectedIds.delete(image.id);}}
338
+ });}),
339
+ button("dangerSolid",message("libraryNext.actions.delete","Delete"),native.deleteIcon,()=>confirmRemove(images)),
340
+ more,h(native.Button,{color:"ghostActive",radius:"full",size:"toolbar",uniform:true,disabled:batchBusy,onClick:clearSelection,"aria-label":message("libraryNext.selection.clear","Clear selection"),children:h(native.ClearIcon)}),
341
+ ]})});
342
+ }
343
+ function onSelectionKey(event) {
344
+ if(event.key!=="Escape" || !selectedIds.size || batchBusy || removeDialog || folderDialog || event.defaultPrevented)return;
345
+ if(event.target instanceof Element && event.target.closest("input,textarea,[contenteditable=true],[role=dialog],[role=menu]"))return;
346
+ event.preventDefault();clearSelection();
347
+ }
348
+ document.addEventListener("keydown",onSelectionKey);
349
+ async function startChat(images) {
350
+ const conversationId=native.createChatDraft(context.scope,[]);
351
+ native.switchMode(context.scope,"chat");
352
+ const files=[];
353
+ for(const image of images){const file=await reference(image,"upload-file");files.push(native.imageFile(`data:image/png;base64,${file.base64}`,file.name).file);}
354
+ if (!await native.attachChatImages(context.scope,conversationId,files,{isTemporaryChat:false})) throw new Error("Could not attach selected images");
355
+ const navigate=context.scope.get(native.routerAtom).prepareNavigation("/",{state:{codexAppMode:"chat",initialChatGptConversationId:conversationId,focusComposerNonce:Date.now()}});
356
+ if(!navigate)throw new Error("Native Chat navigation is unavailable");
357
+ clearSelection();await navigate();
358
+ }
359
+ function localImage(image) {
360
+ return {id:image.imageGenerationItemId, src:image.sourcePath, previewSrc:image.sourcePath, alt:image.name, tabTitle:image.name, width:image.width, height:image.height};
361
+ }
362
+ function open(image, options) {
363
+ opened.set(image.sourcePath, image);
364
+ if (!options) {canvasImage=image;refresh();return;}
365
+ const {readOnly=false,scope=context.scope,chatGptConversationId}=options;
366
+ const result = native.openImage(scope, {
367
+ chatGptConversationId, alt:image.name, title:image.name, attachmentId:`image-playground:${image.imageGenerationItemId}`,
368
+ attachmentSrc:image.sourcePath, src:image.sourcePath, downloadSrc:image.sourcePath,
369
+ imageId:image.imageGenerationItemId, sourceConversationId:image.threadId,
370
+ imageSourceContext:{conversationId:image.threadId, hostId:"local"}, imageSource:"generated",
371
+ readOnly, openInEditor:true,
372
+ renderHeaderMenuActions:({renderMenu}) => renderMenu(h(native.Menu.Item, {onSelect:() => onPrompt(image), children:context.intl.formatMessage({id:"minecodex.images.viewPrompts",defaultMessage:"View prompts"})})),
373
+ }, {addToHistory:false});
374
+ if (!result) throw new Error("Native image preview could not open");
375
+ }
376
+ function ImageCanvas({image}) {
377
+ const resolved=native.useImage({src:image.sourcePath,conversationId:null,shouldLoadFileDataUrl:true});
378
+ const close=()=>{canvasImage=null;refresh();};
379
+ const viewport=container.closest(".contain-layout");
380
+ if(!viewport)throw new Error("Native Library preview viewport changed");
381
+ React.useEffect(()=>{
382
+ const page=container.closest(".overflow-y-auto");
383
+ const previous=page?.inert;
384
+ if(page)page.inert=true;
385
+ return ()=>{if(page)page.inert=previous;};
386
+ },[]);
387
+ const [composerValue]=React.useState(()=>native.createComposerValue({kind:"image",image:{filename:image.name,id:image.id,src:image.sourcePath}},`minecodex-image:${image.id}`));
388
+ const content=h(native.ScopeProvider,{scope:native.composerRouteScope.parent,value:{clientThreadId:`minecodex-image:${image.id}`,routeConversationId:null}},
389
+ h(native.ScopeProvider,{scope:native.composerRouteScope,value:{pathname:"/",routeKind:"home",routeTemplate:"/",search:"",projectContext:null}},
390
+ h(native.ScopeProvider,{scope:native.composerScope,value:composerValue},h(CanvasSurface,{image,resolved,close}))));
391
+ // 原生文件预览占据主内容 viewport,保留导航、二级侧栏和窗口栏。
392
+ return native.createPortal(h("div",{className:"absolute inset-0 z-40 overflow-hidden", "data-minecodex-image-canvas":image.id,children:content}),viewport);
393
+ }
394
+ function CanvasSurface({image,resolved,close}) {
395
+ const controller=native.useComposerController();
396
+ const home=native.useHomeComposer({homeComposerController:controller,homeComposerMode:"chat"});
397
+ const [abort]=React.useState(()=>new AbortController());
398
+ const [markup,setMarkup]=React.useState(false);
399
+ const [manualZoomPercent,setManualZoomPercent]=React.useState(null);
400
+ const [submitting,setSubmitting]=React.useState(false);
401
+ const editorRef=React.useRef(null);
402
+ const attachment=React.useRef(null);
403
+ const submissionAccepted=React.useRef(false);
404
+ React.useEffect(()=>()=>{if(!submissionAccepted.current)abort.abort();},[abort]);
405
+ const assertCurrent=()=>{abort.signal.throwIfAborted();assertAccount();};
406
+ // 查看不上传;首次发送或编辑时才准备原生临时附件,同一画布复用。
407
+ const prepare=async()=>{
408
+ assertCurrent();
409
+ if(!attachment.current)attachment.current=(async()=>{
410
+ const file=await reference(image,"upload-file");assertCurrent();
411
+ const bytes=Uint8Array.from(atob(file.base64),value=>value.charCodeAt(0));
412
+ const uploaded=await native.upload(context.scope,new File([bytes],file.name,{type:"image/png"}),{storeInLibrary:false,beforeRequest:assertCurrent,signal:abort.signal});
413
+ return {...uploaded,width:image.width,height:image.height,imageReference:true};
414
+ })().catch(error=>{attachment.current=null;throw error;});
415
+ return attachment.current;
416
+ };
417
+ if(!home)return null;
418
+ const props=home.composerProps;
419
+ const accepted=(...args)=>{submissionAccepted.current=true;close();props.onSubmitAccepted?.(...args);};
420
+ const requestClose=()=>{if(markup&&editorRef.current)editorRef.current.requestClose();else close();};
421
+ const submitEdit=async request=>{
422
+ if(submitting)return false;
423
+ const editor=markup?editorRef.current:null;
424
+ if(markup&&(!editor||editor.isReadOnly||editor.editor.getSnapshot().hasActiveGesture))return false;
425
+ editor?.commitComment();editor?.editor.commitPendingTextEdit();
426
+ const document=editor?.editor.getSnapshot().effectiveDocument;
427
+ setSubmitting(true);
428
+ try {
429
+ const file=await prepare();assertCurrent();
430
+ const sourceImage={id:image.id,src:`${file.id.startsWith("file_")?"sediment":"file-service"}://${file.id}`,status:"completed",type:"generated-image",width:image.width,height:image.height,sizeBytes:file.size};
431
+ const ok=await native.editImage(context.scope,{
432
+ beforeRequest:assertCurrent,conversationId:props.conversationId,document,image:sourceImage,
433
+ isSubmissionCurrent:()=>!abort.signal.aborted&&!disposed,isTemporaryChat:false,
434
+ request,requireDispatchAcceptance:true,signal:abort.signal,sourceAttachment:file,
435
+ });
436
+ if(ok){assertCurrent();accepted(props.conversationId);}
437
+ return ok;
438
+ }catch(error){if(!abort.signal.aborted)operationError(error);return false;}
439
+ finally {if(!abort.signal.aborted)setSubmitting(false);}
440
+ };
441
+ const composer=h(native.ComposerSurface,{placement:{kind:"home"}},h(native.Composer,{
442
+ ...props,composerController:controller,composerLayoutMode:"auto-single-line",lockedLayout:"single-line",
443
+ className:"mx-auto w-full max-w-2xl px-toolbar",errorClassName:"p-3",showFilePicker:false,signal:abort.signal,
444
+ placeholder:message("libraryFilePreview.composerPlaceholder","Ask about this file"),isPrimaryComposer:false,
445
+ disableSubmit:props.disableSubmit||submitting,
446
+ attachmentPreparation:{mimeTypes:["image/png"],prepare:async signal=>{signal?.throwIfAborted();return [await prepare()];}},
447
+ onSubmitAccepted:accepted,
448
+ }));
449
+ const dataUrl=resolved.dataUrl??resolved.previewSrc;
450
+ const canvas=h(native.Canvas,{
451
+ alt:image.name,title:image.name,src:dataUrl??image.sourcePath,downloadSrc:dataUrl??image.sourcePath,downloadFilename:image.name,
452
+ attachmentId:`image-playground:${image.imageGenerationItemId}`,attachmentSrc:image.sourcePath,
453
+ chatGptConversationId:props.conversationId,
454
+ imageSource:"generated",imageSourceContext:{conversationId:image.threadId,hostId:"local"},
455
+ readOnly:false,presentation:"dialog",standalone:true,showViewerHeader:true,onClose:requestClose,
456
+ markupMode:{active:markup,onActiveChange:setMarkup,editorRef,isSubmitting:submitting,standaloneComposer:true},
457
+ onImageEditSubmit:submitEdit,
458
+ zoom:{manualZoomPercent,onZoomPercentChange:setManualZoomPercent,onZoomToFit:()=>setManualZoomPercent(null)},
459
+ renderComposer:visible=>h(React.Activity,{mode:visible?"visible":"hidden",children:composer}),
460
+ renderHeaderMenuActions:({renderMenu})=>renderMenu(h(native.Menu.Item,{onSelect:()=>onPrompt(image),children:message("minecodex.images.viewPrompts","View prompts")})),
461
+ });
462
+ // 原生 overlay 负责底部留白、宽度、避让与 Comment 时的输入框显隐。
463
+ return h(native.CanvasLayout,{assetName:image.name,contentOwnsHeader:true,onClose:requestClose,children:canvas});
464
+ }
465
+ async function chatAbout(image) {
466
+ const conversationId = native.createChatDraft(context.scope, []);
467
+ native.switchMode(context.scope, "chat");
468
+ const navigate = context.scope.get(native.routerAtom).prepareNavigation("/", {
469
+ state:{codexAppMode:"chat", initialChatGptConversationId:conversationId, focusComposerNonce:Date.now()},
470
+ });
471
+ if (!navigate) throw new Error("Native Chat navigation is unavailable");
472
+ await navigate();
473
+ await new Promise(resolve => requestAnimationFrame(() => requestAnimationFrame(resolve)));
474
+ const composer = document.querySelector('[data-codex-composer="true"]') ?? document.querySelector('[contenteditable="true"]');
475
+ if (!composer) throw new Error("Native Chat composer is unavailable");
476
+ const chat = environment(composer);
477
+ open(image, {readOnly:true, scope:chat.scope, chatGptConversationId:conversationId});
478
+ }
479
+ function ImageCard({image}) {
480
+ const [saved,setSaved] = React.useState(false);
481
+ const [busy,setBusy] = React.useState(false);
482
+ async function perform(action) {
483
+ if (busy) return;
484
+ setBusy(true);
485
+ try {await action();} catch(error) {operationError(error);} finally {setBusy(false);}
486
+ }
487
+ async function refreshFavorite(open) {
488
+ if (!open) return;
489
+ try {
490
+ const cached = await reference(image,"reference");
491
+ setSaved(cached.libraryFileId ? !!(await favoriteStatus(cached.libraryFileId)) : false);
492
+ } catch(error) {console.error("MineCodex favorite status",error);}
493
+ }
494
+ const resolved = native.useImage({src:image.sourcePath, conversationId:image.threadId, hostId:"local", shouldLoadFileDataUrl:true});
495
+ const value = localImage(image);
496
+ const actions = native.imageMenu(context.scope, {hostId:"local",image:value,displaySrc:resolved.previewSrc ?? image.sourcePath,intl:context.intl});
497
+ // Chat about this 组合原生新 Chat 导航、附件与图片面板链路。
498
+ const byId = new Map(actions.map(item => [item.id, item]));
499
+ const action = (id, icon, message) => {
500
+ const item = byId.get(id);
501
+ return item && {...item, icon, text:context.intl.formatMessage(message ?? item.message)};
502
+ };
503
+ const promptGlyph = document.querySelector('[aria-label="Prompts"] svg');
504
+ const menuGlyph = promptGlyph?.cloneNode(true);
505
+ if (menuGlyph) {
506
+ menuGlyph.setAttribute("width","16");menuGlyph.setAttribute("height","16");
507
+ menuGlyph.style.width="16px";menuGlyph.style.height="16px";
508
+ }
509
+ const promptIcon = menuGlyph ? h("span", {className:"inline-flex size-4 items-center justify-center", "aria-hidden":true, dangerouslySetInnerHTML:{__html:menuGlyph.outerHTML}}) : null;
510
+ const menuItems = [
511
+ {id:"chat-about", icon:native.chatIcon, text:context.intl.formatMessage({id:"libraryNext.actions.chatAboutItem",defaultMessage:"Chat about this"}),onSelect:() => {void chatAbout(image).catch(onRetry);}},
512
+ {id:"favorite",icon:native.favoriteIcon(saved),text:saved
513
+ ? message("library.favorites.remove","Remove from Favorites")
514
+ : message("library.favorites.add","Add to Favorites"),onSelect:() => {void perform(async () => setSaved(await cloud.favorite(image)));}},
515
+ action("download-a-copy", native.downloadIcon, {id:"libraryNext.actions.download", defaultMessage:"Download"}),
516
+ {id:"folder",icon:native.folderIcon,text:message("libraryNext.actions.addToFolder","Add to folder"),onSelect:() => {void perform(() => folder(image));}},
517
+ {id:"delete",icon:native.deleteIcon,tone:"danger",text:message("libraryNext.actions.delete","Delete"),onSelect:()=>confirmRemove([image])},
518
+ {id:"view-prompts", icon:promptIcon, text:context.intl.formatMessage({id:"minecodex.images.viewPrompts",defaultMessage:"View prompts"}),onSelect:() => {open(image);onPrompt(image);}},
519
+ ].filter(Boolean);
520
+ const dropdown = h(native.Dropdown, {
521
+ contentWidth:"menu", align:"end", onOpenChange:refreshFavorite,
522
+ triggerButton:h(native.Button,{color:"mediaOverlay",size:"inputIcon",radius:"full","aria-label":context.intl.formatMessage({id:"minecodex.images.actions",defaultMessage:"More actions"}),children:h(native.MoreIcon)}),
523
+ children:menuItems.map(item => h(native.Menu.Item,{key:item.id,disabled:busy || batchBusy || item.disabled,tone:item.tone,leftIcon:item.icon,onSelect:item.onSelect,children:item.text})),
524
+ });
525
+ // 卡片限制比例以容纳控件;超宽或超窄原图完整缩放,不能随卡片比例裁切。
526
+ const imageRatio=image.width>0&&image.height>0?image.width/image.height:1;
527
+ const cardRatio=Math.min(20/13,Math.max(1/2,imageRatio));
528
+ const picture = resolved.previewSrc
529
+ ? h("img",{src:resolved.dataUrl ?? resolved.previewSrc,alt:"",loading:"lazy",decoding:"async",draggable:false,className:`absolute inset-0 size-full ${imageRatio===cardRatio?"object-cover":"object-contain"}`})
530
+ : h("span",{className:"absolute inset-0 bg-text/10","aria-busy":!resolved.isError,children:resolved.isError?"Image unavailable":null});
531
+ const checked=selectedIds.has(image.id);
532
+ const selection=h("span",{className:`relative flex items-center ${checked?"":"opacity-0 group-hover:opacity-100 group-has-[:focus-visible]:opacity-100"}`,onClickCapture:event=>{
533
+ if(event.shiftKey){event.preventDefault();select(image,!checked,true);}
534
+ },children:h(native.Checkbox,{className:"cursor-interaction",variant:"overlay",checked,disabled:batchBusy,"aria-label":context.intl.formatMessage({id:"libraryNext.selection.item",defaultMessage:"Select {name}"},{name:image.name}),onCheckedChange:value=>select(image,value===true)})});
535
+ return h(native.Card,{name:image.name,selection,selected:checked,selectionActive:selectedIds.size>0,onSelect:event=>{
536
+ if(selectedIds.size || event.shiftKey){select(image,!checked,event.shiftKey);return true;}return false;
537
+ },onOpen:() => onOpen(image,open),actions:dropdown,children:h("span",{className:"relative flex min-h-0 w-full flex-1 items-center justify-center",children:h("span",{className:"relative flex w-full items-center justify-center",style:{aspectRatio:cardRatio},children:picture})})});
538
+ }
539
+ return {
540
+ render({images,loading}) {
541
+ if (failure) throw failure;
542
+ currentImages=images;currentLoading=loading;
543
+ if(loading){selectedIds=new Set();selectionAnchor=null;removeDialog=null;}
544
+ refresh();
545
+ },
546
+ clearSelection,
547
+ dispose() {disposed=true;document.removeEventListener("keydown",onSelectionKey);root.unmount();},
548
+ };
549
+ }
550
+
551
+ function readViewer(header) {
552
+ const props = codex.props(header, props => typeof props?.attachmentSrc === "string" && opened.has(props.attachmentSrc));
553
+ return props ? opened.get(props.attachmentSrc) : null;
554
+ }
555
+ return {mount, mountNavigation, readViewer};
556
+ }