minecodex 2.0.2 → 2.0.4
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/DESIGN.md +149 -0
- package/apps/dashboard/DESIGN.md +45 -0
- package/features/images/DESIGN.md +71 -0
- package/features/images/README.md +2 -0
- package/features/images/codex-feature.json +2 -1
- package/features/images/src/adapters/codex/cloud-import.mjs +52 -0
- package/features/images/src/adapters/codex/image-prompt.mjs +22 -0
- package/features/images/src/adapters/codex/library-api.mjs +55 -0
- package/features/images/src/adapters/codex/library-viewer.mjs +18 -0
- package/features/images/src/adapters/codex/prompt-resolver-source.mjs +8 -0
- package/features/images/src/backend/cloud-library.mjs +10 -0
- package/features/images/src/domain/library-prompt.mjs +7 -0
- package/features/images/src/library-prompt-page.mjs +7 -75
- package/features/images/src/library-prompt-source.mjs +10 -0
- package/features/images/web/app.js +9 -9
- package/features/images/web/styles.css +219 -219
- package/features/model-slider/DESIGN.md +34 -0
- package/features/model-slider/src/adapters/codex/controller.mjs +93 -0
- package/features/model-slider/src/domain/model-rules.mjs +143 -0
- package/features/model-slider/src/frontend/install.mjs +2110 -0
- package/features/model-slider/src/injection-source.mjs +10 -0
- package/features/notes/DESIGN.md +13 -15
- package/features/notes/README.md +7 -1
- package/features/notes/assets/icons/book-open-nav-fill.svg +1 -0
- package/features/notes/codex-feature.json +5 -1
- package/features/notes/src/{default-prompts.mjs → backend/default-prompts.mjs} +1 -1
- package/features/notes/src/{product.mjs → backend/product.mjs} +1 -1
- package/features/notes/src/{quick-prompts-server.mjs → backend/quick-prompts-server.mjs} +2 -2
- package/features/notes/src/{quick-prompts-page.mjs → frontend/entry.mjs} +64 -257
- package/features/notes/src/frontend/icons.json +13 -0
- package/features/notes/src/frontend/styles.css +110 -0
- package/features/notes/src/main.mjs +2 -2
- package/features/notes/vendor/phosphor-LICENSE +21 -0
- package/features/session-status/DESIGN.md +10 -8
- package/features/session-status/README.md +4 -0
- package/features/session-status/codex-feature.json +1 -1
- package/features/session-status/src/adapters/codex/context-usage.mjs +46 -0
- package/features/session-status/src/adapters/codex/session-log.mjs +81 -0
- package/features/session-status/src/adapters/codex/session-reader.mjs +106 -0
- package/features/session-status/src/domain/session-stats.mjs +141 -0
- package/features/session-status/src/injection-source.mjs +6 -0
- package/features/session-status/src/session-status.mjs +42 -102
- package/features/sidebar-focus/DESIGN.md +6 -5
- package/features/sidebar-focus/codex-feature.json +2 -1
- package/features/sidebar-focus/src/adapters/codex/renderer.mjs +28 -0
- package/features/sidebar-focus/src/injection-source.mjs +6 -0
- package/features/sidebar-focus/src/sidebar-focus.mjs +19 -66
- package/features/sites/DESIGN.md +60 -0
- package/features/sites/README.md +27 -0
- package/features/sites/codex-feature.json +28 -0
- package/features/sites/src/adapters/codex/catalog.mjs +26 -0
- package/features/sites/src/adapters/codex/renderer.mjs +83 -0
- package/features/sites/src/backend/discovery.mjs +73 -0
- package/features/sites/src/backend/project-sites.mjs +34 -0
- package/features/sites/src/backend/site-launcher.mjs +54 -0
- package/features/sites/src/backend/sites-preview.mjs +53 -0
- package/features/sites/src/backend/sites-service.mjs +162 -0
- package/features/sites/src/domain/site.mjs +40 -0
- package/features/sites/src/frontend/entry.mjs +383 -0
- package/features/sites/src/frontend/icons.json +12 -0
- package/features/sites/src/frontend/styles.css +72 -0
- package/features/sites/src/injection-source.mjs +7 -0
- package/features/sites/vendor/lucide-LICENSE +35 -0
- package/features/unread-inbox/DESIGN.md +6 -2
- package/features/unread-inbox/codex-feature.json +2 -1
- package/features/unread-inbox/src/adapters/codex/renderer.mjs +66 -0
- package/features/unread-inbox/src/injection-source.mjs +6 -0
- package/features/unread-inbox/src/unread-inbox.mjs +34 -89
- package/features/usage-meter/DESIGN.md +9 -9
- package/features/usage-meter/README.md +2 -1
- package/features/usage-meter/codex-feature.json +3 -3
- package/features/usage-meter/src/adapters/codex/renderer.mjs +61 -0
- package/features/usage-meter/src/injection-source.mjs +6 -0
- package/features/usage-meter/src/usage-meter.mjs +27 -66
- package/package.json +17 -4
- package/packages/cli/src/config.mjs +2 -1
- package/packages/codex-adapter/README.md +15 -0
- package/packages/codex-adapter/package.json +1 -0
- package/packages/codex-adapter/src/renderer/environment.mjs +53 -0
- package/packages/runtime-host/README.md +27 -0
- package/packages/runtime-host/src/codex-injection.mjs +32 -2393
- package/packages/runtime-host/src/codex-runtime.mjs +21 -3
- package/packages/runtime-host/src/feature-registry.mjs +28 -3
- package/packages/ui/DESIGN.md +31 -0
- package/packages/ui/package.json +8 -0
- package/packages/ui/src/components/button.mjs +21 -0
- package/packages/ui/src/components/composer-menu.mjs +18 -0
- package/packages/ui/src/components/dialog.mjs +55 -0
- package/packages/ui/src/components/navigation-rail.mjs +77 -0
- package/packages/ui/src/components/page.mjs +8 -0
- package/packages/ui/src/components/tooltip.mjs +96 -0
- package/packages/ui/src/foundation/layers.mjs +14 -0
- package/packages/ui/src/foundation/surfaces.mjs +34 -0
- package/packages/ui/src/install.mjs +22 -0
- package/packages/ui/src/native/navigation-rail.mjs +33 -0
- package/packages/ui/src/native/theme.mjs +35 -0
- package/packages/{runtime-host/src/codex-design-contract.mjs → ui/src/native/tokens.mjs} +67 -24
- package/packages/runtime-host/src/cloud-images.mjs +0 -123
- package/packages/runtime-host/src/feature-tooltips.mjs +0 -65
- package/packages/runtime-host/src/session-status-reader.mjs +0 -340
- /package/features/notes/src/{prompt-schema.mjs → backend/prompt-schema.mjs} +0 -0
package/DESIGN.md
ADDED
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
# MineCodex 通用设计规范
|
|
2
|
+
|
|
3
|
+
本文件是全项目通用设计规范的权威入口,由 MineCodex 组件库维护。按原生组件类型和明确分支适配;不要把一个页面中的视觉样式推断成所有组件的统一样式。组件库及插件的 DESIGN.md 只记录本级特殊组件、选型和功能差异,不复制此处的通用规则。
|
|
4
|
+
|
|
5
|
+
依据:2026-10-01,Codex **26.928.21956** 安装包源码,以及隔离窗口的原生 DOM、React 参数和计算样式。浅色 Dialog、菜单、页面与 sidebar 已实测;深色相关遮罩来自源码定义,未据此宣称深色视觉验收完成。
|
|
6
|
+
|
|
7
|
+
## 原生规则与 MineCodex 选择
|
|
8
|
+
|
|
9
|
+
原生组件分类是事实,插件采用哪个分支是产品选择,两者分别记录。例如 Prompts 和 MineCodex Sites 的管理弹窗采用用户指定的 Sites Share 分支;这不意味着原生默认透明 Dialog 被淘汰。按钮外形也不决定容器材质。
|
|
10
|
+
|
|
11
|
+
Token 名称是实现依据;括号内数值是本版本观测值。读取实际生效作用域,不仅仅读 documentElement;原生窗口、主题、局部组件均可能覆盖 token。iframe 需要宿主传递 token,不能假设自动继承 Renderer CSS。
|
|
12
|
+
|
|
13
|
+
## 基础视觉与交互
|
|
14
|
+
|
|
15
|
+
- 字体与字号引用原生 `--font-sans-default`、`--text-base`、`--text-sm`、`--text-xs`;标题按实际角色使用 `--text-heading-lg`、`--text-heading-md`。字重使用 `--font-weight-normal`、`--font-weight-medium`、`--font-weight-semibold`。功能特殊的信息密度与尺寸在本级文档记录。
|
|
16
|
+
- 标题和主要内容使用 `--color-text-primary`,说明使用 `--color-text-secondary`,辅助信息使用 `--color-text-tertiary`。旧前景名称仅由公共token适配器处理必要别名,插件不维护自己的兼容表。
|
|
17
|
+
- 交互反馈引用 `--color-background-primary-ghost-hover` 与 `--color-ring`;输入边缘使用 `--color-border-primary-outline`,普通边缘使用 `--color-border` / `--color-border-subtle`。保留所选原生控件实际的focus规则,不把所有输入强制改成同一种外圈。
|
|
18
|
+
- 图标优先复用项目已有库或原生真实SVG,随所在控件继承颜色与状态;不按视觉猜测重画,不匹配构建哈希。特殊数据图、品牌图与功能比例在插件文档说明。
|
|
19
|
+
- 保留原生主题、locale、zoom、reduced-motion与键盘反馈。用户内容不被翻译;通用控件文字、无障碍标签与功能文案随宿主语言显示。
|
|
20
|
+
- 声明功能时优先复用共享容器和控件;功能只填内容、状态与业务回调。项目本地Web界面遵循同一语义视觉契约,但通过主题桥或自身入口获取token,不假设可访问Codex原生DOM。
|
|
21
|
+
|
|
22
|
+
## Dialog 与遮罩
|
|
23
|
+
|
|
24
|
+
| 原生分支 | 容器 | 遮罩与使用依据 |
|
|
25
|
+
| --- | --- | --- |
|
|
26
|
+
| `default` | `--color-surface-elevated-secondary` 90%,`--blur-xl`(24px),0.5px `--color-border` 边缘、`--shadow-lg`,通常 `--radius-3xl`(25px) | `--color-dialog-overlay`;当前普通 Electron 浅色为 `#0002`,无模糊。Images Rename 实测 `variant=default` |
|
|
27
|
+
| `opaque` | 不透明 `--color-surface-elevated-secondary`,无容器模糊,0.5px 边缘、`--shadow-lg`,通常 `--radius-3xl` | 原生 `dimmedOverlay`:浅色 `#e3e3e380`、深色 `#00000080`,`--dialog-backdrop-fade-filter`(1px模糊)。Sites Share 实测 `variant=opaque` |
|
|
28
|
+
| `form` | 不透明 elevated-secondary,`--radius-form-dialog`,原生 `shadow-form-dialog` utility class | `dimmedOverlay`。不是默认 Dialog 的透明配方;`shadow-form-dialog` 是 class,不能虚构同名 CSS token |
|
|
29
|
+
| `confirmation` / `catalog` / `centeredText` | 不透明 elevated-secondary,专用 catalog surface、尺寸与阴影 | `dimmedOverlay`;不将默认 Dialog 的所有尺寸、阴影套入 |
|
|
30
|
+
| `beaconModal` / `expressiveBeacon` | 另有容器和主题分支 | `--dialog-backdrop-fade-background`、`--dialog-backdrop-fade-filter`;浅色 fade 当前为 `#f2f2f280`,与 dimmedOverlay 不同 |
|
|
31
|
+
|
|
32
|
+
默认容器与遮罩按原生分支成对选择。dimmedOverlay 的浅灰/黑色配方是原生 CSS 的主题分支,不是某个已命名的遮罩颜色 token;不要把 `--modal-backdrop-background` 当成它的同义名。
|
|
33
|
+
|
|
34
|
+
共有文字与结构 token:`--font-sans-default`、`--text-heading-md`(20px)、`--font-weight-semibold`、`--text-base`、`--color-text-primary`、`--color-text-secondary`、`--color-text-tertiary`、`--modal-container-inner-padding`。当前共享外壳内容 padding 为20px,标题行高28px;Close 采用原生 Sites SVG及24px热区、top/right 16px。具体内容尺寸由功能决定。
|
|
35
|
+
|
|
36
|
+
`--shadow-lg` 当前浅色为 `0 4px 8px -2px / 10%黑色`。边缘和投影是两部分;按所选原生组件保留ring与shadow组合,不再额外加一层CSS border。表单、确认等专用分支应按所选原生组件保留差异。
|
|
37
|
+
|
|
38
|
+
## 操作按钮
|
|
39
|
+
|
|
40
|
+
当前 MineCodex 管理弹窗采用 Sites Share 的 `data-size=xl`、`data-pill` 设计,语义与材质分开维护。
|
|
41
|
+
|
|
42
|
+
| 操作角色 | 填充/边缘 | 原生 token |
|
|
43
|
+
| --- | --- | --- |
|
|
44
|
+
| Cancel 等取消操作(`ghost`) | 透明、无描边;hover 才出现底色 | `--color-text-secondary`、`--color-background-primary-ghost-hover` |
|
|
45
|
+
| 有实际功能的次级操作(outline,现有API tone为`secondary`) | 透明底、内描边 | `--color-text-primary`、`--color-border-primary-outline`、`--color-background-primary-outline-hover` |
|
|
46
|
+
| 主操作(solid,现有API tone为`primary`) | 实色底与配对文字色 | `--color-background-primary-solid`、`--color-text-primary-solid`、`--color-background-primary-solid-hover` |
|
|
47
|
+
| 警戒操作(现有`danger`) | 警戒色,独立于主操作;插件负责确认业务 | `--color-background-danger-soft-alpha`、`--color-background-danger-soft-alpha-hover`、`--color-text-danger` |
|
|
48
|
+
|
|
49
|
+
尺寸:`--control-size-lg`(36px)、`--control-font-size-md`(13px)、`--font-weight-normal`(原生按钮400)、`--radius-full`。水平 gutter 为 `--control-gutter-md × --control-gutter-pill-scaling`(15.96px),图文 gap 为 `--button-gap-lg`。焦点使用 `--color-ring`;禁用保留语义并阻止 hover 反馈。
|
|
50
|
+
|
|
51
|
+
不要将所有原生按钮改为同一尺寸:Share xl 胶囊是这里选定的弹窗操作规范,页面入口、Composer 控件及其他原生组件仍按各自类型处理。
|
|
52
|
+
|
|
53
|
+
## Dropdown、Popover 与 Composer 建议列表
|
|
54
|
+
|
|
55
|
+
| 类型 | 默认材质与 token | 约束 |
|
|
56
|
+
| --- | --- | --- |
|
|
57
|
+
| Dropdown `surface=menu` | elevated-secondary 90%,`--blur-sm`(8px)、`--color-border-overlay`、`--dropdown-overlay-ring-width`、`--shadow-dropdown-overlay` | Images动作菜单实测。通常rounded-2xl;不要用Popover参数代替 |
|
|
58
|
+
| Dropdown `surface=opaque` | 不透明 elevated-secondary、dropdown专用阴影 | 保留明确的不透明分支 |
|
|
59
|
+
| Dropdown `surface=panel` | 菜单材质基础上增大padding与阴影,使用blur-lg | 只在存在对应panel需求时采用 |
|
|
60
|
+
| Popover | 原生 `opaque` 省略时为true,不透明;false改为90%填充,源码不统一添加模糊 | `--color-border-overlay`、`--popover-overlay-ring-width`、`--shadow-popover-overlay`;`variant=menu`复用opaque菜单,comment有独立样式 |
|
|
61
|
+
| Composer SuggestionMenu | 当前Add列表不透明 elevated-secondary、无模糊;边框与阴影受建议列表token控制,当前无投影 | `--radius-suggestion-menu`、`--suggestion-menu-border-width`、`--suggestion-menu-shadow`,不存在时沿用原生fallback;列表文字`--text-sm` |
|
|
62
|
+
| Composer expanded top tray | 专用背景与边缘,无blur;embedded分支透明无边框 | `--composer-top-tray-background`、`--composer-top-tray-border`;背景fallback为surface-secondary 94%。不要与独立悬浮菜单混用 |
|
|
63
|
+
|
|
64
|
+
Dropdown 的 `--shadow-dropdown-overlay` 当前指向 `--shadow-xl-spread`,该 token 自身含0.5px边缘。普通Popover的 `--shadow-popover-overlay` 当前指向 `--shadow-lg`。不要给所有浮层固定附加4px blur或同一个shadow。
|
|
65
|
+
|
|
66
|
+
Composer菜单的锚点是实际输入框主体(当前语义标记 `data-composer-body`),不是包含diff和请求队列的外层Root。挂载位置、可用高度、zoom与碰撞行为沿用对应原生组件,不为修复坐标问题盲目提高z-index。
|
|
67
|
+
|
|
68
|
+
## Page 与 sidebar
|
|
69
|
+
|
|
70
|
+
页面内容面使用 `--color-surface`,当前不透明、无模糊,`--radius-xl-base`(12px);外部PageSurface使用 `--app-shell-page-surface-shadow`,当前由 `--elevation-stroke-subtle` 与 `--shadow-card` 组成。区分内容面和窗口外壳,不重复施加阴影。
|
|
71
|
+
|
|
72
|
+
sidebar规则分状态:
|
|
73
|
+
|
|
74
|
+
| 状态 | 原生规则/token |
|
|
75
|
+
| --- | --- |
|
|
76
|
+
| 非peeking sidebar-navigation | `--color-surface` 65%(oklab混合),无CSS模糊、无浮窗阴影;可见桌面程度还取决于窗口材质 |
|
|
77
|
+
| `data-slate-sidebar-peeking=true` | `--app-shell-sidebar-popover-background`强制alpha=1;`--app-shell-sidebar-popover-radius`当前为radius-xl-base减spacing(8px);`--hairline`、`--app-shell-sidebar-popover-border`内边缘与`--shadow-card` |
|
|
78
|
+
|
|
79
|
+
保留原生的固定展开、收起与hover状态。hover sidebar应能覆盖注入页面,鼠标能进入操作,移回页面后可正常收起;不要通过页面层级、pointer-events或额外遮罩挡住它。原生overflow和contain切换负责浮出与裁切,插件不复制一套侧栏状态机。
|
|
80
|
+
|
|
81
|
+
## 左侧 Navbar
|
|
82
|
+
|
|
83
|
+
导航入口复用原生按钮容器,三种状态分别维护:
|
|
84
|
+
|
|
85
|
+
| 状态 | 图标 | 容器 |
|
|
86
|
+
| --- | --- | --- |
|
|
87
|
+
| 未选中 | outline/default 资源 | 原生未选中色与透明底 |
|
|
88
|
+
| 悬浮 | 保持 default 图标 | 原生 hover 底色;保留 sidebar peeking 行为 |
|
|
89
|
+
| 当前页面选中 | 独立 filled/selected 资源 | `data-selected` 与 `aria-current="page"`,原生选中色与底色 |
|
|
90
|
+
|
|
91
|
+
原生 `railIcons.default/selected` 根据 `isCurrentDestination` 选择资源。不能给 outline SVG 简单设置 fill 代替 selected 资源,也不能仅移除 DOM 选中属性就认为图标已还原。注入页面打开时取消原生视觉选中,关闭、导航或卸载时恢复;不更改原生路由,不破坏 Home hover sidebar。
|
|
92
|
+
|
|
93
|
+
通用入口为 `ui.navigationRail(button, { defaultIcon, selectedIcon })`,返回 `select(boolean)` 与 `restore()`。组件负责展示状态与清理,`packages/ui/src/native/navigation-rail.mjs` 负责原生资源读取。原生图标优先调用资源显式提供的 `createElement(document, className)` DOM API(包含 optical 与主题处理);单色 IconAsset 读取 default 的真实 SVG;其他组件形式使用已记录的未选中 SVG。default/selected 本就相同的自定义资源保持原图。不要调用带 Hooks 的函数或替换 React 所有的 SVG 子节点;未知资源契约需核对原生来源,不能伪造轮廓。原生重渲染更换 SVG 后重新应用展示状态。
|
|
94
|
+
|
|
95
|
+
## 层级、Portal 与生命周期
|
|
96
|
+
|
|
97
|
+
层级数字是本版本实现值,不是原生CSS设计token;共同约束应集中维护语义角色与挂载点。
|
|
98
|
+
|
|
99
|
+
- 内容页面位于原生sidebar悬浮层下方;MineCodex当前Page使用20,原生左侧栏40。
|
|
100
|
+
- 原生Dialog、Dropdown、Popover和Composer Floating父容器通常为50;同数值不保证相同层级,仍受挂载顺序与stacking context影响。
|
|
101
|
+
- 模态层挂在窗口根容器/原生Portal,遮罩覆盖整个窗口;不要嵌在Page的低层级容器内。原生`dialog.showModal()`使用top layer,不能仅用z-index推断。
|
|
102
|
+
- Tooltip使用专用角色与层级;现有MineCodex实现值不是原生标准,不传播为通用token。
|
|
103
|
+
- 组件维护通用焦点、Escape、关闭与清理契约;插件传业务回调和恢复焦点目标。原生已提供的生命周期优先复用,迁移时避免重复处理。
|
|
104
|
+
|
|
105
|
+
## Tooltip
|
|
106
|
+
|
|
107
|
+
共享Tooltip处理MineCodex标记,不接管原生Tooltip。当前声明入口为 `data-minecodex-tooltip`,删除克隆节点上过时的 `title` / `aria-describedby`,避免双重气泡。
|
|
108
|
+
|
|
109
|
+
Token:`--color-background-tooltip`、`--color-text-tooltip`、`--color-border-tooltip`、`--shadow-tooltip`、`--radius-2xl`、`--font-sans-default`、`--text-sm`、`--tooltip-compact-font-weight`、`--tracking-tooltip`。原生默认 Tooltip 使用 `rounded-2xl`,同时应用 `corner-shape: var(--codex-corner-shape)`;支持该属性的桌面版本通过 `--codex-corner-radius-scale` 放大半径并采用 superellipse,不能只复制半径而漏掉轮廓。共享实现保持此规则,padding 为 `--spacing × 1.25 / × 3`、行高18px,Portal 从触发器继承相关主题 token。
|
|
110
|
+
|
|
111
|
+
Navbar 在入口右侧居中,使用 `data-minecodex-tooltip-side="right"`;普通控件读取原生 `--side-tooltip`(默认top),也可显式声明top/bottom/left/right。默认间距2px,碰撞时翻转并保留8px窗口边距,计算考虑window zoom。共享hover延迟200ms,已打开提示之间300ms内切换免等待;键盘focus立即显示,离开/点击/Escape/窗口失焦关闭。时序依据新版自定义 Tooltip provider,不套用包内另一套 Radix 的700ms默认值。共享提示之间维护自己的handoff,不接管原生provider。支持aria关联及卸载清理。额度、上下文等特有内容通过 `tooltipSurface` 复用材质,功能维护自身内容与定位。
|
|
112
|
+
|
|
113
|
+
## 主题、窗口材质与系统玻璃
|
|
114
|
+
|
|
115
|
+
以下机制独立,不能以“玻璃效果”混称后套到所有组件:
|
|
116
|
+
|
|
117
|
+
1. CSS半透明填充与backdrop-filter:用于部分Dialog、Dropdown等组件,模糊其后的网页内容。
|
|
118
|
+
2. 窗口vibrancy:macOS部分窗口在允许透明时使用menu材质;不透明设置、焦点、窗口尺寸、屏幕scale及支持状态会影响窗口底层。Windows另有mica路径。`electron-opaque`窗口状态与Dialog的`variant=opaque`不是同一个概念。
|
|
119
|
+
3. Owl native glass:仅匹配明确选择glass的原生组件,并遵循宿主能力状态。CSS声明`-owl-native-material:glass`、`--owl-native-glass-adaptive-appearance:off`、`--owl-native-glass-variant:regular`;root `data-owl-native-glass=active`时去掉CSS模糊/阴影并使用round corner;`data-owl-native-source-underlay`分支使用16% surface填充。
|
|
120
|
+
|
|
121
|
+
本次普通隔离窗口没有Owl glass active属性。安装包中显式`surface:glass`菜单调用位于avatar-overlay-native-page;主进程显式`hasNativeGlass`条件为darwin且appearance为avatarOverlay。不得因为存在glass CSS就为所有插件浮层申请系统玻璃。
|
|
122
|
+
|
|
123
|
+
主题读取 `data-theme` 与实际token;尺寸和圆角跟随原生scale,位置考虑 `--codex-window-zoom`。沿用原生reduced-motion与焦点状态;不为适配擅自改变应用主题、窗口透明设置或系统辅助设置。
|
|
124
|
+
|
|
125
|
+
## 当前实现与规范的差异
|
|
126
|
+
|
|
127
|
+
共享组件已迁入 `packages/ui`:Dialog 支持 default/opaque,Dropdown 支持 menu/opaque,Popover 支持 opaque/translucent,ComposerMenu 使用原生浮动列表材质,Page 使用原生内容面。现有插件显式选择相应类型,不再统一套90%/4px浮层配方。未提供通用系统glass开关;没有明确原生分支与能力证据时,使用所选组件的CSS材质。form/catalog等尚无实际调用需求的分支仅记录原生事实。
|
|
128
|
+
|
|
129
|
+
现有token注册表供宿主主题桥使用,不代表本文所有组件token已注册。新增iframe消费时核对并补齐所用token;直接注入组件读取原生CSS作用域。按实际需求维护必要别名,删除已失效的兼容代码。
|
|
130
|
+
|
|
131
|
+
## 维护依据
|
|
132
|
+
|
|
133
|
+
- `app-shared-eececb2d2eb0.js`:Dialog variant、遮罩配对、Dropdown surface、Popover opaque默认值。
|
|
134
|
+
- `app-shared-342447930c78.css`:材质、glassSurface、遮罩、token及按钮规则。
|
|
135
|
+
- `app-initial-e55cd978d577.css`:Page、sidebar peeking、Composer top tray及rounded类的corner-shape规则。
|
|
136
|
+
- `app-initial-135a4ef2552c.js`:Navbar railIcons状态切换、Tooltip右侧位置;shared JS 自定义Tooltip默认200ms、skip 300ms、offset 2px。
|
|
137
|
+
- `main-BbeJ4AAR.js`:窗口opaque/vibrancy及native glass创建条件。
|
|
138
|
+
- `avatar-overlay-native-page-caa507f161db.js`:明确的glass菜单使用位置。
|
|
139
|
+
|
|
140
|
+
这些文件名仅标识本次原生证据,含构建哈希,不能成为运行时选择器。更新Codex后,优先重查语义组件和实际状态;将新的事实回写本规范及相应实现,不凭截图推断所有组件已换代。
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
## 项目 token 与原生名称的边界
|
|
144
|
+
|
|
145
|
+
上文表格记录 Codex 原生 token 名称与取证规则。实现由 `packages/ui/src/native/tokens.mjs` 的规范名称、已知别名和 `MINECODEX_THEME_BINDINGS` 映射至项目 `--mcx-*` 命名空间,插件样式消费项目变量。例如原生 `--color-text-primary` → 项目 `--mcx-color-text-primary`,`--shadow-lg` → `--mcx-shadow-lg`,`--radius-3xl` → `--mcx-radius-3xl`。
|
|
146
|
+
|
|
147
|
+
这里的规范名称是稳定的项目映射键。Codex 更新名称时补充原生别名,不能跟着重命名插件消费的项目 token。新增实际使用的语义才增加映射;不预建完整的第三方 token 系统。`native/theme.mjs` 为直接注入容器写入同一快照,Runtime Host 将它传给 iframe;卸载恢复原值。主题变化丢失某个 token 时清除旧项目值,继续使用该组件既有的明确默认值或不可用状态,不复用上一主题颜色。
|
|
148
|
+
|
|
149
|
+
这些映射不改变原生组件分类、材质分支和 hover sidebar 层级,也不重写原生 Tailwind utility class。插件复用 native Adapter 返回的原生模板;特殊样式使用项目 token,Dialog、Button、Tooltip 等通用表现继续由组件库维护。
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
# MineCodex Dashboard Design System
|
|
2
|
+
|
|
3
|
+
## Theme
|
|
4
|
+
|
|
5
|
+
The control panel is a restrained Codex-native settings surface. It follows system light/dark
|
|
6
|
+
appearance, uses one system sans family, and reserves accent color for enabled switches, focus,
|
|
7
|
+
and the primary restart action.
|
|
8
|
+
|
|
9
|
+
## Color
|
|
10
|
+
|
|
11
|
+
Use semantic OKLCH tokens rather than brand colors:
|
|
12
|
+
|
|
13
|
+
- Canvas: neutral near-white / near-black.
|
|
14
|
+
- Surface: one subtle elevation step from the canvas.
|
|
15
|
+
- Ink: high-contrast neutral foreground.
|
|
16
|
+
- Secondary and tertiary ink: still meet AA for their text size.
|
|
17
|
+
- Border: quiet neutral separator.
|
|
18
|
+
- Accent: Codex-like blue used only for action and active state.
|
|
19
|
+
- Success and warning: paired with labels or icons, never color alone.
|
|
20
|
+
|
|
21
|
+
## Typography
|
|
22
|
+
|
|
23
|
+
Use `-apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif`. Page title is 20px/600,
|
|
24
|
+
plugin names 15px/600, body text 13px/1.5, and compact metadata 12px. No display type,
|
|
25
|
+
uppercase tracking, or fluid heading sizes.
|
|
26
|
+
|
|
27
|
+
## Layout
|
|
28
|
+
|
|
29
|
+
The page uses a centered 880px content column with a compact header, one restart notice when
|
|
30
|
+
needed, a vertical plugin settings group, and a low-emphasis diagnostics footer. At narrow widths
|
|
31
|
+
the rows wrap their status and switch controls beneath the copy without horizontal scrolling.
|
|
32
|
+
|
|
33
|
+
## Components
|
|
34
|
+
|
|
35
|
+
- Plugin row: icon, name, description, actual-state label, and native checkbox switch.
|
|
36
|
+
- Status dot: always accompanied by readable state text.
|
|
37
|
+
- Restart notice: inline surface between header and plugin group, not a modal.
|
|
38
|
+
- Buttons: one primary restart button and quiet secondary actions with full hover, focus,
|
|
39
|
+
active, disabled, and loading states.
|
|
40
|
+
- Loading: skeleton rows preserve layout; errors use an inline retry surface.
|
|
41
|
+
|
|
42
|
+
## Motion
|
|
43
|
+
|
|
44
|
+
Use 160–200ms ease-out transitions only for switch position, button feedback, and the restart
|
|
45
|
+
notice appearing. Disable nonessential transitions under `prefers-reduced-motion: reduce`.
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
# Images: native Library prompt contract
|
|
2
|
+
|
|
3
|
+
通用视觉、材质、按钮、Tooltip 与原生 sidebar 层级遵循根 [DESIGN.md](../../DESIGN.md)。本文件只记录功能特有组件、布局、数据语义与通用组件选型。
|
|
4
|
+
|
|
5
|
+
## Scope
|
|
6
|
+
|
|
7
|
+
Images enhances Codex's native Library image viewer. It does not inject a sidebar
|
|
8
|
+
item after Sites, open its own gallery, import image bytes, or replace native
|
|
9
|
+
editing/download controls. The previous standalone gallery and local index code
|
|
10
|
+
remain available as legacy utilities, but the feature manifest no longer starts
|
|
11
|
+
that service or registers that surface.
|
|
12
|
+
|
|
13
|
+
## Entry and identity
|
|
14
|
+
|
|
15
|
+
The native `data-testid="viewer-header"` is the entry anchor. The current React
|
|
16
|
+
ancestor's Library image record supplies the Library ID, file ID, version and
|
|
17
|
+
originating conversation/message IDs. Non-Library viewers and non-image files
|
|
18
|
+
are excluded. No title, filename, image position or minified component name is
|
|
19
|
+
used as identity. Missing native anchors fail closed.
|
|
20
|
+
|
|
21
|
+
Resolve the prompt before adding the button. Missing/empty prompts, uploaded
|
|
22
|
+
references without generated metadata, inaccessible conversations, and unresolved
|
|
23
|
+
native API capabilities all leave the toolbar unchanged. There is no disabled
|
|
24
|
+
button, loading placeholder, or empty prompt panel.
|
|
25
|
+
|
|
26
|
+
The text button is placed immediately before the native percentage/zoom menu and
|
|
27
|
+
reuses its live base classes for capsule sizing, colors, border, hover and focus.
|
|
28
|
+
Dropdown/zoom variants are excluded: width is max-content, flex is 0 0 auto, and
|
|
29
|
+
symmetric inline padding is three native spacing units (12px in this runtime).
|
|
30
|
+
The label is Prompt / 提示词, following the native document locale.
|
|
31
|
+
|
|
32
|
+
## Prompt panel
|
|
33
|
+
|
|
34
|
+
The panel follows the native Library Version history implementation inspected in
|
|
35
|
+
the isolated Codex runtime on 2026-09-27 (`layout` asset, version-history aside):
|
|
36
|
+
|
|
37
|
+
- `absolute inset-x-2 top-toolbar bottom-2 z-30 ms-auto mt-2 max-w-75`.
|
|
38
|
+
- `rounded-2xl border border-default bg-surface shadow-lg`.
|
|
39
|
+
- Header: `flex items-center justify-between py-2 ps-4 pe-2`;
|
|
40
|
+
title: `text-base font-semibold text-default`.
|
|
41
|
+
- Body: `min-h-0 flex-1 overflow-y-auto px-3`, with one spacing unit of
|
|
42
|
+
top padding (4px). The panel reserves three spacing units below the scroll
|
|
43
|
+
viewport (12px), so scrolling text never meets the rounded bottom edge.
|
|
44
|
+
- Prompt: `text-sm leading-5 text-default whitespace-pre-wrap select-text`.
|
|
45
|
+
|
|
46
|
+
These are native tokens, not a plugin palette or fixed radius. In the inspected
|
|
47
|
+
runtime they resolve to 300px width and 20px corners. The close glyph is cloned
|
|
48
|
+
from the native viewer control. Long text scrolls inside the panel, remains
|
|
49
|
+
selectable, and is rendered with textContent. The header Copy icon button reuses the existing Images Lucide copy/check glyphs
|
|
50
|
+
at 16px, with a 32px hit area and an accessible label/tooltip. It writes only the
|
|
51
|
+
prompt body to the clipboard and briefly confirms success. No source label or
|
|
52
|
+
explanatory metadata appears in the panel. Close and Escape return focus to
|
|
53
|
+
the prompt button; Escape does not close the underlying viewer on the same press.
|
|
54
|
+
|
|
55
|
+
## Data and lifecycle
|
|
56
|
+
|
|
57
|
+
Read the source conversation through the running renderer's native authenticated
|
|
58
|
+
API; discover the API export through Library metadata's actual module imports,
|
|
59
|
+
without fixed build hashes. No credentials, signed download URLs or image bytes
|
|
60
|
+
are copied or sent to another service.
|
|
61
|
+
|
|
62
|
+
Match both the originating message and exact image asset pointer, and require
|
|
63
|
+
assistant/tool generation metadata. Prefer a recorded generator prompt. Otherwise use only the nearest user ancestor
|
|
64
|
+
of that image. Provenance stays internal; the UI simply calls this Prompt / 提示词. Unrelated branches are never used.
|
|
65
|
+
|
|
66
|
+
Changing images or leaving the viewer aborts the pending lookup and removes the
|
|
67
|
+
old UI. Late results cannot mount against a different image. Disabling the feature
|
|
68
|
+
removes the observer, keyboard handler, button and panel. Prompt data is retained
|
|
69
|
+
only for the current viewer; there is no persistent conversation cache.
|
|
70
|
+
|
|
71
|
+
The viewer and retained cloud import utility share `src/domain/library-prompt.mjs`. The source factory serializes that pure rule into the viewer script; the cloud adapter serializes the same function into its evaluator call. Whitespace prompts follow the same fallback policy. A valid generated image without prompt text can still be imported by the historical utility, while the viewer shows no prompt control.
|
|
@@ -23,3 +23,5 @@ node --test features/images/test/library-prompt-page.test.mjs
|
|
|
23
23
|
|
|
24
24
|
旧 `src/image-library.mjs`、索引、独立 `web/` 与服务脚本暂保留为历史工具,
|
|
25
25
|
不属于当前插件入口;已有本地图片和缓存不会被迁移或删除。
|
|
26
|
+
|
|
27
|
+
历史图库的交接与视觉取证已归档到 [research/images](../../docs/research/images/2026-08-07-gallery-handoff.md),不作为当前功能规范。历史服务仍可通过本插件的 `npm run service` 手动启动;它不由当前 manifest 加载。保留这些工具不意味着新增或恢复独立图库入口。
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { createLibraryApi } from "./library-api.mjs";
|
|
2
|
+
import { promptFromConversation } from "./prompt-resolver-source.mjs";
|
|
3
|
+
|
|
4
|
+
// 在原生 Renderer 内使用现有登录态;凭据和临时下载地址不跨出 Renderer。
|
|
5
|
+
export async function cloudImagesInRenderer(operation, payload, resolvePrompt = promptFromConversation, createApi = createLibraryApi) {
|
|
6
|
+
const key = Symbol.for("minecodex.cloud-images.v1");
|
|
7
|
+
const state = window[key] ??= { files: new Map(), conversations: new Map() };
|
|
8
|
+
const library = state.library ??= createApi();
|
|
9
|
+
if (operation === "list") {
|
|
10
|
+
if (!payload.cursor) { state.files.clear(); state.conversations.clear(); }
|
|
11
|
+
const result = await library.list(payload.cursor, AbortSignal.timeout(25_000));
|
|
12
|
+
const files = result.items.filter(file => ["image/png", "image/jpeg", "image/webp"].includes(file.mime_type)
|
|
13
|
+
&& file.origination_thread_id && file.origination_message_id && !file.trashed_at);
|
|
14
|
+
for (const file of files) state.files.set(file.id, file);
|
|
15
|
+
return { items: files.map(file => ({ libraryId: file.id, version: `${file.current_version_number}:${file.updated_at}`, name: file.file_name })), cursor: result.cursor ?? null };
|
|
16
|
+
}
|
|
17
|
+
const file = state.files.get(payload.libraryId);
|
|
18
|
+
if (!file) throw new Error("Cloud image must come from the current Library listing");
|
|
19
|
+
const threadId = file.origination_thread_id;
|
|
20
|
+
if (!state.conversations.has(threadId)) {
|
|
21
|
+
const request = library.conversation(threadId,AbortSignal.timeout(25_000)).catch(error=>{state.conversations.delete(threadId);throw error;});
|
|
22
|
+
state.conversations.set(threadId, request);
|
|
23
|
+
}
|
|
24
|
+
const conversation = await state.conversations.get(threadId);
|
|
25
|
+
const node = conversation.mapping?.[file.origination_message_id];
|
|
26
|
+
const resolved = resolvePrompt(file, conversation);
|
|
27
|
+
if (!resolved) return { skipped: true };
|
|
28
|
+
const prompt = resolved.text, promptKind = resolved.kind;
|
|
29
|
+
const max = 32 * 1024 * 1024;
|
|
30
|
+
if (file.file_size_bytes > max) throw new Error("Cloud image exceeds 32 MB");
|
|
31
|
+
const imageBlob = await library.download(file, AbortSignal.timeout(60_000));
|
|
32
|
+
if (!(imageBlob instanceof Blob) || imageBlob.size > max) throw new Error("Cloud image download returned an invalid image");
|
|
33
|
+
const reader = imageBlob.stream().getReader(), chunks = [];
|
|
34
|
+
let length = 0;
|
|
35
|
+
try {
|
|
36
|
+
while (true) {
|
|
37
|
+
const {done, value} = await reader.read(); if (done) break;
|
|
38
|
+
length += value.length;
|
|
39
|
+
if (length > max) throw new Error("Cloud image exceeds 32 MB");
|
|
40
|
+
chunks.push(value);
|
|
41
|
+
}
|
|
42
|
+
} finally { await reader.cancel().catch(() => {}); }
|
|
43
|
+
let binary = "";
|
|
44
|
+
for (const chunk of chunks) for (let i = 0; i < chunk.length; i += 0x8000) binary += String.fromCharCode(...chunk.subarray(i, i + 0x8000));
|
|
45
|
+
return {
|
|
46
|
+
bytesBase64: btoa(binary), mimeType: imageBlob.type.split(";")[0] || file.mime_type,
|
|
47
|
+
itemId: file.file_id, conversationId: threadId, conversationTitle: conversation.title,
|
|
48
|
+
sourceKind: "chat", prompt, promptKind,
|
|
49
|
+
cloudLibraryId: file.id, cloudVersion: `${file.current_version_number}:${file.updated_at}`,
|
|
50
|
+
createdAt: new Date((node.message.create_time || conversation.create_time) * 1000).toISOString(),
|
|
51
|
+
};
|
|
52
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
// null 表示非生成图片;有效图片无提示词时返回空字段,导入仍可保留图片。
|
|
2
|
+
export function imagePromptFacts(file, conversation) {
|
|
3
|
+
const node = conversation.mapping?.[file.origination_message_id];
|
|
4
|
+
const part = node?.message?.content?.parts?.find(part => part && typeof part === "object"
|
|
5
|
+
&& part.content_type === "image_asset_pointer"
|
|
6
|
+
&& [file.file_id, `sediment://${file.file_id}`].includes(part.asset_pointer));
|
|
7
|
+
if (!part || !["assistant", "tool"].includes(node.message.author?.role)
|
|
8
|
+
|| !(part.metadata?.generation?.gen_id || part.metadata?.dalle?.gen_id)) return null;
|
|
9
|
+
const prompt = part.metadata?.dalle?.prompt;
|
|
10
|
+
if (typeof prompt === "string" && prompt.trim()) return { generated:true, generationPrompt:prompt, requestText:null };
|
|
11
|
+
let ancestor = conversation.mapping[node.parent];
|
|
12
|
+
const visited = new Set();
|
|
13
|
+
while (ancestor && !visited.has(ancestor.id)) {
|
|
14
|
+
visited.add(ancestor.id);
|
|
15
|
+
if (ancestor.message?.author?.role === "user") {
|
|
16
|
+
const text = ancestor.message.content?.parts?.filter(part => typeof part === "string").join("\n");
|
|
17
|
+
return { generated:true, generationPrompt:null, requestText:text?.trim() ? text : null };
|
|
18
|
+
}
|
|
19
|
+
ancestor = conversation.mapping[ancestor.parent];
|
|
20
|
+
}
|
|
21
|
+
return { generated:true, generationPrompt:null, requestText:null };
|
|
22
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
// 唯一的 Library 构建产物协议;调用方只消费命名操作。
|
|
2
|
+
export function createLibraryApi({read:readSource=async url=>(await fetch(url)).text(),load:loadModule=url=>import(url)} = {}) {
|
|
3
|
+
let capability=null, downloadCapability=null;
|
|
4
|
+
async function connect() {
|
|
5
|
+
if (!capability) capability=(async()=>{
|
|
6
|
+
const initialUrl = [...document.querySelectorAll('link[rel="modulepreload"],script[src]')]
|
|
7
|
+
.map(element => element.href || element.src).find(url => /\/app-initial-[^/]+\.js/.test(url));
|
|
8
|
+
if (!initialUrl) throw new Error("Cloud Library is unavailable in this Codex version");
|
|
9
|
+
const initialSource = await readSource(initialUrl);
|
|
10
|
+
const asset = name => {
|
|
11
|
+
const file = initialSource.match(new RegExp(`${name}-[\\w-]+\\.js`))?.[0];
|
|
12
|
+
if (!file) throw new Error(`Cloud Library capability ${name} is unavailable`);
|
|
13
|
+
return new URL(file, initialUrl).href;
|
|
14
|
+
};
|
|
15
|
+
const metadataUrl = asset("library-file-metadata");
|
|
16
|
+
const metadataSource = await readSource(metadataUrl);
|
|
17
|
+
const apiLocal = metadataSource.match(/([\w$]+)\.safePost\(`\/files\/library`/)?.[1];
|
|
18
|
+
const importBlock = [...metadataSource.matchAll(/import\{([^}]+)\}from["']([^"']+)["']/g)].find(match => match[1].split(",").some(binding=>binding.trim().split(/\s+as\s+/).at(-1)===apiLocal));
|
|
19
|
+
const apiExport = importBlock?.[1].split(",").map(s => s.trim().split(/\s+as\s+/)).find(parts => parts.at(-1) === apiLocal)?.[0];
|
|
20
|
+
if (!apiExport) throw new Error("Cloud Library API contract changed");
|
|
21
|
+
const metadataInit = metadataSource.match(/function ([\w$]+)\(\)\{return\(\1=/)?.[1];
|
|
22
|
+
const metadataExports = metadataSource.match(/export\{([^}]+)\}/)?.[1].split(",").map(s => s.trim().split(/\s+as\s+/)) ?? [];
|
|
23
|
+
const initExport = metadataExports.find(parts => parts[0] === metadataInit)?.at(-1);
|
|
24
|
+
const metadata = await loadModule(metadataUrl);
|
|
25
|
+
if (typeof metadata[initExport] !== "function") throw new Error("Cloud Library initialization contract changed");
|
|
26
|
+
metadata[initExport]();
|
|
27
|
+
const api = (await loadModule(new URL(importBlock[2],metadataUrl).href))[apiExport];
|
|
28
|
+
if (!api?.safeGet || !api?.safePost) throw new Error("Cloud Library API is not ready");
|
|
29
|
+
return {api,asset};
|
|
30
|
+
})().catch(error=>{capability=null;throw error;});
|
|
31
|
+
return capability;
|
|
32
|
+
}
|
|
33
|
+
async function download(file,signal) {
|
|
34
|
+
if (!downloadCapability) downloadCapability=(async()=>{
|
|
35
|
+
const {asset}=await connect();
|
|
36
|
+
const downloadUrl = asset("library-file-download");
|
|
37
|
+
const downloadSource = await readSource(downloadUrl);
|
|
38
|
+
const exports = downloadSource.match(/export\{([^}]+)\}/)?.[1].split(",").map(s => s.trim().split(/\s+as\s+/)) ?? [];
|
|
39
|
+
const responseLocal = downloadSource.match(/async function ([\w$]+)\([^)]*\)\{return [\w$]+\(await [\w$]+\([^;]+showInline:!0/)?.[1];
|
|
40
|
+
const initLocal = downloadSource.match(/function ([\w$]+)\(\)\{return\(\1=/)?.[1];
|
|
41
|
+
const exported = local => exports.find(parts => parts[0] === local)?.at(-1);
|
|
42
|
+
const download = await loadModule(downloadUrl);
|
|
43
|
+
const initialize = download[exported(initLocal)], response = download[exported(responseLocal)];
|
|
44
|
+
if (typeof initialize !== "function" || typeof response !== "function") throw new Error("Cloud image download contract changed");
|
|
45
|
+
initialize();
|
|
46
|
+
return response;
|
|
47
|
+
})().catch(error=>{downloadCapability=null;throw error;});
|
|
48
|
+
return (await downloadCapability)(file,signal);
|
|
49
|
+
}
|
|
50
|
+
return {
|
|
51
|
+
conversation:async(id,signal)=>(await connect()).api.safeGet("/conversation/{conversation_id}",{parameters:{path:{conversation_id:id}},signal}),
|
|
52
|
+
list:async(cursor,signal)=>(await connect()).api.safePost("/files/library",{requestBody:{limit:50,cursor:cursor ?? null,include_hidden_files:false,include_saved_entities:true},signal}),
|
|
53
|
+
download,
|
|
54
|
+
};
|
|
55
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export function createLibraryViewerAdapter(codex) {
|
|
2
|
+
function libraryImage(header) {
|
|
3
|
+
let fiber = codex.currentFiber(header);
|
|
4
|
+
let selected = null;
|
|
5
|
+
for (let depth = 0; fiber && depth < 80; depth++, fiber = fiber.return) {
|
|
6
|
+
const file = fiber.memoizedProps?.file;
|
|
7
|
+
if (!file || !/^libfile_[\w-]+$/.test(file.id ?? "") || !file.mime_type?.startsWith("image/")) continue;
|
|
8
|
+
if (selected && selected.id !== file.id) break;
|
|
9
|
+
selected = file;
|
|
10
|
+
if (file.origination_thread_id && file.origination_message_id) break;
|
|
11
|
+
}
|
|
12
|
+
if (!selected) return null;
|
|
13
|
+
// 只保留关联字段,签名下载地址和图片数据不进入插件。
|
|
14
|
+
return Object.fromEntries(["id", "file_id", "current_version_number", "origination_thread_id", "origination_message_id"].map(key => [key, selected[key]]));
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
return {read:libraryImage};
|
|
18
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import selectImagePrompt from "../../domain/library-prompt.mjs";
|
|
2
|
+
import { imagePromptFacts } from "./image-prompt.mjs";
|
|
3
|
+
export function promptFromConversation(file, conversation) {
|
|
4
|
+
return selectImagePrompt(imagePromptFacts(file, conversation));
|
|
5
|
+
}
|
|
6
|
+
export function promptResolverSource() {
|
|
7
|
+
return `(function() { const normalize=${imagePromptFacts.toString()}; const select=${selectImagePrompt.toString()}; return (file, conversation) => select(normalize(file, conversation)); })()`;
|
|
8
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { cloudImagesInRenderer } from "../adapters/codex/cloud-import.mjs";
|
|
2
|
+
import { createLibraryApi } from "../adapters/codex/library-api.mjs";
|
|
3
|
+
import { promptResolverSource } from "../adapters/codex/prompt-resolver-source.mjs";
|
|
4
|
+
|
|
5
|
+
export async function cloudImagesAction(evaluate, operation, payload = {}) {
|
|
6
|
+
if (!["list", "import"].includes(operation)) throw new Error("Unknown cloud image operation");
|
|
7
|
+
if (payload.cursor != null && (typeof payload.cursor !== "string" || payload.cursor.length > 4096)) throw new Error("Invalid Library cursor");
|
|
8
|
+
if (operation === "import" && (typeof payload.libraryId !== "string" || !/^libfile_[\w-]+$/.test(payload.libraryId))) throw new Error("Invalid Library file ID");
|
|
9
|
+
return evaluate(`(${cloudImagesInRenderer.toString()})(${JSON.stringify(operation)},${JSON.stringify(payload)},${promptResolverSource()},${createLibraryApi.toString()})`, { timeoutMs: 110_000 });
|
|
10
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
// 图片生成事实的提示词优先级,与原生消息树结构无关。
|
|
2
|
+
export default function selectImagePrompt(facts) {
|
|
3
|
+
if (!facts?.generated) return null;
|
|
4
|
+
if (facts.generationPrompt?.trim()) return { text:facts.generationPrompt, kind:"generation" };
|
|
5
|
+
if (facts.requestText?.trim()) return { text:facts.requestText, kind:"request" };
|
|
6
|
+
return { text:null, kind:null };
|
|
7
|
+
}
|
|
@@ -3,75 +3,11 @@ const PANEL_ID = "minecodex-library-prompt";
|
|
|
3
3
|
const zh = () => (document.documentElement.lang || navigator.language || "en").toLowerCase().startsWith("zh");
|
|
4
4
|
const label = (en, cn) => zh() ? cn : en;
|
|
5
5
|
let mounted = null;
|
|
6
|
-
|
|
6
|
+
const libraryApi=createLibraryApi();
|
|
7
7
|
let scheduled = false;
|
|
8
8
|
let candidate = null;
|
|
9
9
|
|
|
10
|
-
|
|
11
|
-
const key = Object.keys(header).find(key => key.startsWith("__reactFiber$"));
|
|
12
|
-
let fiber = key ? header[key] : null;
|
|
13
|
-
let selected = null;
|
|
14
|
-
for (let depth = 0; fiber && depth < 80; depth++, fiber = fiber.return) {
|
|
15
|
-
const file = fiber.memoizedProps?.file;
|
|
16
|
-
if (!file || !/^libfile_[\w-]+$/.test(file.id ?? "") || !file.mime_type?.startsWith("image/")) continue;
|
|
17
|
-
if (selected && selected.id !== file.id) break;
|
|
18
|
-
selected = file;
|
|
19
|
-
if (file.origination_thread_id && file.origination_message_id) break;
|
|
20
|
-
}
|
|
21
|
-
if (!selected) return null;
|
|
22
|
-
// 只保留关联字段,签名下载地址和图片数据不进入插件。
|
|
23
|
-
return Object.fromEntries(["id", "file_id", "current_version_number", "origination_thread_id", "origination_message_id"].map(key => [key, selected[key]]));
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
async function nativeApi() {
|
|
27
|
-
if (!apiPromise) apiPromise = (async () => {
|
|
28
|
-
const initialUrl = [...document.querySelectorAll('link[rel="modulepreload"],script[src]')]
|
|
29
|
-
.map(element => element.href || element.src).find(url => /\/app-initial-[^/]+\.js/.test(url));
|
|
30
|
-
if (!initialUrl) throw new Error("Library API unavailable");
|
|
31
|
-
const initialSource = await (await fetch(initialUrl, { signal })).text();
|
|
32
|
-
const metadataAsset = initialSource.match(/library-file-metadata-[\w-]+\.js/)?.[0];
|
|
33
|
-
if (!metadataAsset) throw new Error("Library metadata unavailable");
|
|
34
|
-
const metadataUrl = new URL(metadataAsset, initialUrl).href;
|
|
35
|
-
const source = await (await fetch(metadataUrl, { signal })).text();
|
|
36
|
-
const apiLocal = source.match(/([\w$]+)\.safePost\(`\/files\/library`/)?.[1];
|
|
37
|
-
const imports = [...source.matchAll(/import\{([^}]+)\}from["']([^"']+)["']/g)]
|
|
38
|
-
.find(match => match[1].split(",").some(binding => binding.trim().split(/\s+as\s+/).at(-1) === apiLocal));
|
|
39
|
-
const apiExport = imports?.[1].split(",").map(s => s.trim().split(/\s+as\s+/)).find(parts => parts.at(-1) === apiLocal)?.[0];
|
|
40
|
-
const initLocal = source.match(/function ([\w$]+)\(\)\{return\(\1=/)?.[1];
|
|
41
|
-
const exports = source.match(/export\{([^}]+)\}/)?.[1].split(",").map(s => s.trim().split(/\s+as\s+/)) ?? [];
|
|
42
|
-
const initExport = exports.find(parts => parts[0] === initLocal)?.at(-1);
|
|
43
|
-
const metadata = await import(metadataUrl);
|
|
44
|
-
if (typeof metadata[initExport] !== "function" || !apiExport) throw new Error("Library API contract changed");
|
|
45
|
-
metadata[initExport]();
|
|
46
|
-
const api = (await import(new URL(imports[2], metadataUrl).href))[apiExport];
|
|
47
|
-
if (typeof api?.safeGet !== "function") throw new Error("Library API unavailable");
|
|
48
|
-
return api;
|
|
49
|
-
})().catch(error => { apiPromise = null; throw error; });
|
|
50
|
-
return apiPromise;
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
function promptFromConversation(file, conversation) {
|
|
54
|
-
const node = conversation.mapping?.[file.origination_message_id];
|
|
55
|
-
const part = node?.message?.content?.parts?.find(part => part && typeof part === "object"
|
|
56
|
-
&& part.content_type === "image_asset_pointer"
|
|
57
|
-
&& [file.file_id, `sediment://${file.file_id}`].includes(part.asset_pointer));
|
|
58
|
-
if (!part || !["assistant", "tool"].includes(node.message.author?.role)
|
|
59
|
-
|| !(part.metadata?.generation?.gen_id || part.metadata?.dalle?.gen_id)) return null;
|
|
60
|
-
const prompt = part.metadata?.dalle?.prompt;
|
|
61
|
-
if (typeof prompt === "string" && prompt.trim()) return { text: prompt, kind: "generation" };
|
|
62
|
-
let ancestor = conversation.mapping[node.parent];
|
|
63
|
-
const visited = new Set();
|
|
64
|
-
while (ancestor && !visited.has(ancestor.id)) {
|
|
65
|
-
visited.add(ancestor.id);
|
|
66
|
-
if (ancestor.message?.author?.role === "user") {
|
|
67
|
-
const text = ancestor.message.content?.parts?.filter(part => typeof part === "string").join("\n");
|
|
68
|
-
return text?.trim() ? { text, kind: "request" } : null;
|
|
69
|
-
}
|
|
70
|
-
ancestor = conversation.mapping[ancestor.parent];
|
|
71
|
-
}
|
|
72
|
-
return null;
|
|
73
|
-
}
|
|
74
|
-
|
|
10
|
+
const libraryImage=createLibraryViewerAdapter(config.codex).read;
|
|
75
11
|
function element(tag, className, text) {
|
|
76
12
|
const node = document.createElement(tag);
|
|
77
13
|
node.className = className;
|
|
@@ -105,12 +41,8 @@ function showPrompt(record) {
|
|
|
105
41
|
|
|
106
42
|
async function resolvePrompt(file, request) {
|
|
107
43
|
if (!file.origination_thread_id || !file.origination_message_id) return null;
|
|
108
|
-
const api = await nativeApi();
|
|
109
44
|
request.signal.throwIfAborted();
|
|
110
|
-
const conversation = await
|
|
111
|
-
parameters: { path: { conversation_id: file.origination_thread_id } },
|
|
112
|
-
signal: AbortSignal.any([signal, request.signal, AbortSignal.timeout(25_000)]),
|
|
113
|
-
});
|
|
45
|
+
const conversation = await libraryApi.conversation(file.origination_thread_id,AbortSignal.any([signal,request.signal,AbortSignal.timeout(25_000)]));
|
|
114
46
|
return promptFromConversation(file, conversation);
|
|
115
47
|
}
|
|
116
48
|
|
|
@@ -141,7 +73,7 @@ async function scan() {
|
|
|
141
73
|
try { prompt = await resolvePrompt(file, next.request); }
|
|
142
74
|
catch { return; }
|
|
143
75
|
// 异步结果只能写回当前图片;没有提示词时不展示入口。
|
|
144
|
-
if (!prompt || signal.aborted || candidate !== next || !header.isConnected) return;
|
|
76
|
+
if (!prompt?.text || signal.aborted || candidate !== next || !header.isConnected) return;
|
|
145
77
|
// 缩放项在图片详情中是百分比菜单;沿用它的胶囊类,不固定构建 hash。
|
|
146
78
|
const zoom = [...header.querySelectorAll('button[aria-haspopup="menu"]')].find(button => /^\d+(?:\.\d+)?%$/.test(button.textContent.trim()));
|
|
147
79
|
const group = zoom?.parentElement?.parentElement;
|
|
@@ -156,7 +88,7 @@ async function scan() {
|
|
|
156
88
|
button.style.width = "max-content";
|
|
157
89
|
button.style.minWidth = "0";
|
|
158
90
|
button.style.flex = "0 0 auto";
|
|
159
|
-
button.style.paddingInline = "calc(var(--spacing) * 3)";
|
|
91
|
+
button.style.paddingInline = "calc(var(--mcx-spacing) * 3)";
|
|
160
92
|
group.prepend(button);
|
|
161
93
|
// 对齐原生 Library Version history 的面板、标题和内容区 token。
|
|
162
94
|
const panel = element("aside", "absolute inset-x-2 top-toolbar bottom-2 z-30 ms-auto mt-2 flex max-w-75 flex-col overflow-hidden rounded-2xl border border-default bg-surface shadow-lg select-none");
|
|
@@ -164,7 +96,7 @@ async function scan() {
|
|
|
164
96
|
panel.setAttribute("aria-label", label("Image prompt", "图片提示词"));
|
|
165
97
|
panel.hidden = true;
|
|
166
98
|
panel.style.setProperty("-webkit-app-region", "no-drag");
|
|
167
|
-
panel.style.paddingBottom = "calc(var(--spacing) * 3)";
|
|
99
|
+
panel.style.paddingBottom = "calc(var(--mcx-spacing) * 3)";
|
|
168
100
|
const heading = element("div", "flex items-center justify-between py-2 ps-4 pe-2");
|
|
169
101
|
const close = element("button", "flex size-8 shrink-0 items-center justify-center rounded-full text-default hover:bg-primary-ghost-hover focus-visible:outline-ring");
|
|
170
102
|
close.type = "button";
|
|
@@ -186,7 +118,7 @@ async function scan() {
|
|
|
186
118
|
actions.append(copy, close);
|
|
187
119
|
heading.append(element("span", "text-base font-semibold text-default", label("Prompt", "提示词")), actions);
|
|
188
120
|
const body = element("div", "min-h-0 flex-1 overflow-y-auto px-3");
|
|
189
|
-
body.style.paddingTop = "var(--spacing)";
|
|
121
|
+
body.style.paddingTop = "var(--mcx-spacing)";
|
|
190
122
|
body.append(
|
|
191
123
|
element("p", "px-1 text-sm leading-5 text-default whitespace-pre-wrap break-words select-text", prompt.text),
|
|
192
124
|
);
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { createLibraryApi } from "./adapters/codex/library-api.mjs";
|
|
2
|
+
import { createLibraryViewerAdapter } from "./adapters/codex/library-viewer.mjs";
|
|
3
|
+
import { readFile } from "node:fs/promises";
|
|
4
|
+
import { promptResolverSource } from "./adapters/codex/prompt-resolver-source.mjs";
|
|
5
|
+
|
|
6
|
+
// 两条 Renderer 路径共享纯规则,注入后不依赖 Node 模块闭包。
|
|
7
|
+
export async function createPageScriptSource() {
|
|
8
|
+
const source = await readFile(new URL("./library-prompt-page.mjs", import.meta.url), "utf8");
|
|
9
|
+
return `const createLibraryApi=${createLibraryApi.toString()};\nconst createLibraryViewerAdapter=${createLibraryViewerAdapter.toString()};\nconst promptFromConversation = ${promptResolverSource()};\n${source}`;
|
|
10
|
+
}
|