minecodex 2.0.3 → 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 (39) 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 +7 -3
  22. package/features/session-status/src/adapters/codex/context-usage.mjs +11 -4
  23. package/features/session-status/src/adapters/codex/session-log.mjs +2 -1
  24. package/features/session-status/src/domain/session-stats.mjs +17 -9
  25. package/features/session-status/src/session-status.mjs +48 -12
  26. package/package.json +1 -1
  27. package/packages/codex-adapter/README.md +2 -1
  28. package/packages/codex-adapter/src/renderer/environment.mjs +24 -6
  29. package/packages/runtime-host/src/codex-injection.mjs +3 -6
  30. package/packages/runtime-host/src/codex-runtime.mjs +16 -5
  31. package/packages/runtime-host/src/feature-registry.mjs +1 -0
  32. package/packages/ui/DESIGN.md +5 -2
  33. package/packages/ui/src/components/button.mjs +2 -0
  34. package/packages/ui/src/components/navigation-rail.mjs +18 -10
  35. package/packages/ui/src/components/page.mjs +9 -2
  36. package/packages/ui/src/install.mjs +3 -1
  37. package/packages/ui/src/native/navigation-rail.mjs +6 -1
  38. package/packages/ui/src/native/page.mjs +23 -0
  39. package/packages/ui/src/native/tokens.mjs +3 -0
@@ -9,6 +9,7 @@ let button = null;
9
9
  let addButton = null;
10
10
  let composerRoot = null;
11
11
  let composerThread = null;
12
+ let composerEditable = null;
12
13
  let panel = null;
13
14
  let items = [];
14
15
  let generation = 0;
@@ -169,7 +170,7 @@ function renderList() {
169
170
  async () => {
170
171
  const current = generation;
171
172
  try {
172
- window.__codexPersonalRuntime.insertText(item.body);
173
+ window.__codexPersonalRuntime.insertText(item.body, composerEditable);
173
174
  // 插入同步成功后再记一次调用;失败和单纯浏览不计数。
174
175
  close();
175
176
  await api(`/api/prompts/${encodeURIComponent(item.id)}/use`, {
@@ -224,7 +225,7 @@ function isOpenComposerMenu(node) {
224
225
  function sync() {
225
226
  if (disposed || signal.aborted) return;
226
227
  syncNavigation();
227
- const nativeComposer = config.codex.composer(null,{threadOnly:false});
228
+ const nativeComposer = config.codex.composer(null,{purpose:"insertion"});
228
229
  const add = nativeComposer?.add;
229
230
  const card = nativeComposer?.root;
230
231
  const thread = nativeComposer?.threadId ?? null;
@@ -232,13 +233,14 @@ function sync() {
232
233
  close();
233
234
  composerThread = thread;
234
235
  }
235
- if (add !== addButton || card !== composerRoot) {
236
+ if (add !== addButton || card !== composerRoot || nativeComposer?.editable !== composerEditable) {
236
237
  close();
237
238
  button?.remove();
238
239
  button = null;
239
240
  addButton = add;
240
241
  composerRoot = card;
241
242
  }
243
+ composerEditable = nativeComposer?.editable ?? null;
242
244
  if (!add) return;
243
245
  if (
244
246
  panel &&
@@ -325,11 +327,12 @@ function syncNavigationSelection() {
325
327
  function restoreNavigationSelection() {
326
328
  navigationRail?.select(false);
327
329
  }
328
- function closePage(restoreFocus = false) {
330
+ function closePage(restoreFocus = false, restoreNavigation = true) {
329
331
  dragState?.cancel();
330
332
  pageGeneration++;
331
333
  closePagePopup();
332
334
  closeEditor(false);
335
+ if (page) config.ui.releasePage(page, { restoreNavigation });
333
336
  page?.remove();
334
337
  page = null;
335
338
  restoreNavigationSelection();
@@ -362,7 +365,7 @@ function openPage() {
362
365
  pageFocus = document.activeElement;
363
366
  page = element("section");
364
367
  page.setAttribute(MARKER, "page");
365
- config.ui.page(page);
368
+ config.ui.page(page, { sidebar:"none" });
366
369
  page.setAttribute("aria-label", text("Prompts", "提示词"));
367
370
  document.body.append(page);
368
371
  // 只隔离主内容的焦点,保留导航栏与原生窗口控件。
@@ -726,6 +729,18 @@ function orderControls(item, ordered, isCategory, row) {
726
729
  const parent = row.parentElement;
727
730
  const siblings = [...parent.children].filter(node => node.dataset.orderGroup === group);
728
731
  const originalNext = row.nextSibling;
732
+ const rect = row.getBoundingClientRect();
733
+ const radius = getComputedStyle(row).borderRadius;
734
+ const dragImage = row.cloneNode(true);
735
+ dragImage.removeAttribute("data-order-group");
736
+ dragImage.removeAttribute("data-order-id");
737
+ dragImage.setAttribute("aria-hidden", "true");
738
+ Object.assign(dragImage.style, { position:"fixed", left:"-10000px", top:"0", width:`${rect.width}px`,
739
+ height:`${rect.height}px`, margin:"0", boxSizing:"border-box", pointerEvents:"none",
740
+ background:"var(--mcx-color-surface)", borderRadius:radius, overflow:"hidden", clipPath:`inset(0 round ${radius})` });
741
+ // 显式裁切拖影,避免浏览器自动截图留下矩形外边界。
742
+ parent.append(dragImage);
743
+ event.dataTransfer.setDragImage(dragImage, event.clientX - rect.left, event.clientY - rect.top);
729
744
  const placeholder = element("div", "pm-drop-placeholder");
730
745
  placeholder.style.height = `${row.getBoundingClientRect().height}px`;
731
746
  placeholder.setAttribute("aria-hidden", "true");
@@ -752,6 +767,7 @@ function orderControls(item, ordered, isCategory, row) {
752
767
  if (finished) return;
753
768
  finished = true;
754
769
  lifetime.abort();
770
+ dragImage.remove();
755
771
  dragState = null;
756
772
  row.draggable = false;
757
773
  if (!started) return;
@@ -768,8 +784,9 @@ function orderControls(item, ordered, isCategory, row) {
768
784
  dragState = { cancel: () => finish(false) };
769
785
  event.dataTransfer.effectAllowed = "move";
770
786
  event.dataTransfer.setData("text/plain", item.id);
771
- // 等浏览器截取拖影后再隐藏原行,避免拖影变成空白。
787
+ // 等浏览器截取拖影后再清理替身、隐藏原行。
772
788
  requestAnimationFrame(() => {
789
+ dragImage.remove();
773
790
  if (finished || !row.isConnected) return;
774
791
  row.before(placeholder);
775
792
  row.classList.add("pm-dragging");
@@ -1075,7 +1092,7 @@ function manageCategories() {
1075
1092
  const target = editorShell(text("Categories", "分类"));
1076
1093
  const footer = config.ui.footer(element("div", "pm-footer"));
1077
1094
  footer.append(
1078
- pageControl(text("New category", "新建分类"), () => editCategory(), true),
1095
+ pageControl(text("New category", "新建分类"), () => editCategory()),
1079
1096
  );
1080
1097
  for (const category of categories) {
1081
1098
  const row = element("div", "pm-row");
@@ -1291,11 +1308,11 @@ document.addEventListener(
1291
1308
  event.target.closest("[data-app-navigation-rail] button") &&
1292
1309
  !navButton?.contains(event.target)
1293
1310
  )
1294
- closePage();
1311
+ closePage(false, false);
1295
1312
  },
1296
1313
  { signal, capture: true },
1297
1314
  );
1298
- window.addEventListener("popstate", () => closePage(), { signal });
1315
+ window.addEventListener("popstate", () => closePage(false, false), { signal });
1299
1316
  window.addEventListener("resize", positionPage, { signal });
1300
1317
 
1301
1318
  const observer = new MutationObserver(() => {
@@ -8,13 +8,17 @@
8
8
 
9
9
  ## Token 与布局
10
10
 
11
+ 状态栏实例跟随可见正式会话的原生 Composer root,不跟随 contenteditable 编辑节点。发送时编辑区暂时不可交互或重建,同一外壳、同一会话的状态栏继续显示并保留展开选择和统计;不以延时隐藏掩盖变化。真正切换会话时清除旧统计,进入 home、隐藏外壳或迁入浮动输入框时移除实例。宿主读取身份与 Renderer 使用同一个 `composerSurface` 契约。
12
+
11
13
  | 角色 | Codex token | 用途 |
12
14
  | --- | --- | --- |
13
15
  | 次要文字 | `--color-text-tertiary` | 状态行文字与分隔符 |
14
16
  | 主要文字 | `--color-text-primary` | 弹层标题和数值 |
15
17
  | 分类进度 | Codex `--app-block-accent-purple`、`--app-block-accent-blue`、`--app-block-accent-green`,以及 `--color-token-border-default` | 各弹层的分类比例与未分类段 |
16
18
 
17
- 状态行和输入框共用内容宽度,单行高度为 24px,顶部留 2px 内边距;每个状态按钮高 22px,内边距为上下 3px、左右 6px。按钮之间 gap 为 4px,两组可见内容间距为 16px(6 + 4 + 6);按钮内图标与文字 gap 为 6px。状态行展开且可见时,将包含它的原生 `.bottom-0.pb-4` 容器底部 padding 从 16px 覆盖为 4px;收起、隐藏或卸载后自动恢复原生留白,展开的状态行使用 -4px 顶部外边距,将输入框下方原生 8px gap 的实际距离收为4px,收起时恢复。宽度不足时自然换行,不覆盖输入框或悬浮控件。统计图标采用 Lucide Gauge,用量图标采用 Phosphor Database 单路径版本,两者以 14px 绘制;上下文使用与 Codex 原生相同参数的 12px 百分环。弹层由按钮锚定,键盘可聚焦,Escape 和点击外部关闭。遵守系统减少动态效果设置,不添加持续动画。
19
+ 状态行和输入框共用内容宽度,单行高度为 24px,顶部留 2px 内边距;每个状态按钮高 22px,内边距为上下 3px、左右 6px。按钮之间 gap 为 4px,两组可见内容间距为 16px(6 + 4 + 6);按钮内图标与文字 gap 为 6px。状态行展开且可见时,在包含它的原生 `[data-thread-scroll-footer="true"]` 容器上将 `--thread-composer-bottom-inset` 设为 4px;默认主聊天底部 padding 从 16px 收为 4px,收起、隐藏或卸载后自动恢复。原生 panel 的 `pb-3` 与 compact 分支不消费该变量,保留其独立留白。展开的状态行使用 -4px 顶部外边距,将输入框下方原生 8px gap 的实际距离收为4px,收起时恢复。宽度不足时自然换行,不覆盖输入框或悬浮控件。统计图标采用 Lucide Gauge,用量图标采用 Phosphor Database 单路径版本,两者以 14px 绘制;上下文使用与 Codex 原生相同参数的 12px 百分环。弹层由按钮锚定,键盘可聚焦,Escape 和点击外部关闭。遵守系统减少动态效果设置,不添加持续动画。
20
+
21
+ 挂载依据:2026-10-02,Codex 26.930.21537 的主聊天实测 DOM 与 `thread-scroll-layout` / `app-primary` 源码。状态行仍作为 `[data-codex-composer-root]` 的紧邻后续节点;中间的 `display:contents` 包装不改变它与输入框在外层 flex 中的顺序和宽度。折叠入口放在 reasoning 控件最接近 `items-center` 控制组的包装节点之前;原生 Context 环仍为带 `aria-label`、`role=img` 的 span,包含 SVG circle。新版默认 footer 使用 `pb-(--thread-composer-bottom-inset)`,不再匹配旧 `.pb-4` 规则。panel/compact 分支来自源码,未据此宣称侧边聊天的手动验收已完成。
18
22
 
19
23
  状态行以当前输入框所在的会话为准,不依赖侧栏中可能因项目折叠而卸载的 Thread 行。新聊天没有会话 ID 时隐藏旧数据;首条消息建立会话后默认展开详细状态行;若用户曾收起,则沿用其选择。尚未取得的指标用 `—` 占位,不等待日志或 Token 用量;切换会话时立即清空旧数值,详情展开状态保持不变。
20
24
 
@@ -22,7 +26,7 @@
22
26
 
23
27
  界面文案跟随 Codex 的 `html[lang]`:中文语言使用简体中文,其他语言使用英文;运行中切换语言时同步更新状态行和已打开的明细浮窗。MineCodex Web 使用同样的中英文规则提供本功能开关。
24
28
 
25
- TPS 根据 Codex 会话事件计算有效样本的会话平均约值,以 `N tok/s` 呈现;分母扣除工具执行区间(含迟到事件,重叠区间仅扣一次),不将其视为实时解码速度;缺少有效计时样本时显示 `—`。累计 token 与缓存比例来自 Codex 用量记录。上下文使用原生 Codex 桌面版的口径:最近请求 `last.totalTokens / modelContextWindow`,直接订阅原生会话的 `latestTokenUsageInfo`(`thread/tokenUsage/updated` 更新),压缩后与原生同步刷新,不依赖日志轮询;原生数据不可用时显示占位;状态栏显示百分环和数字,弹层显示用量、窗口容量和压缩次数。二级浮窗的 token 数按十进制换算为 K/M,省略 `tok` 后缀;TPS 仍保留 `tok/s`。日志可见历史只列有数据的指令、消息、工具交互三个短标签,估算值以 `~` 开头。6px 进度条使用 Codex 图标选择器的紫、蓝、绿配色,标签圆点保持同色;当前页面未暴露对应变量时使用 Codex 亮/暗主题原值。剩余的原生占用以浅灰表示;若估算合计超过原生占用,按比例缩放彩色段,让进度条终点始终对应原生总占用。不显示无法取得的工具定义,也不放解释性长文案。
29
+ TPS 根据 Codex 会话事件计算有效样本的会话平均约值,以 `N tok/s` 呈现;分母仅扣除完整落在同一响应计时范围内、且不与已观测模型输出重叠的工具区间(含迟到事件,重叠区间仅扣一次);跨响应运行或与生成并行的后台工具不扣时,模型输出缺少结束时间时保守保留该样本的观测时长,不将其视为实时解码速度;缺少有效计时样本时显示 `—`。累计 token 与缓存比例来自 Codex 用量记录。上下文使用原生 Codex 桌面版的口径:最近请求 `last.totalTokens / modelContextWindow`,直接订阅原生会话的 `latestTokenUsageInfo`(`thread/tokenUsage/updated` 更新),压缩后与原生同步刷新,不依赖日志轮询;原生数据不可用时显示占位;状态栏显示百分环和数字,弹层显示用量、窗口容量和压缩次数。二级浮窗的 token 数按十进制换算为 K/M,省略 `tok` 后缀;TPS 仍保留 `tok/s`。日志可见历史只列有数据的指令、消息、工具交互三个短标签,估算值以 `~` 开头;工具结果同时接受字符串与内容数组,压缩后的 replacement_history 采用同一文本解析。6px 进度条使用 Codex 图标选择器的紫、蓝、绿配色,标签圆点保持同色;当前页面未暴露对应变量时使用 Codex 亮/暗主题原值。剩余的原生占用以浅灰表示;若估算合计超过原生占用,按比例缩放彩色段,让进度条终点始终对应原生总占用。不显示无法取得的工具定义,也不放解释性长文案。
26
30
 
27
31
  上下文弹层按视觉归属排列:进度条下紧接三个可见历史分段,不加分段副标题;分隔线之后列原生占用合计、窗口容量和压缩次数。合计取原生最近请求用量,不是三个估算分段的求和。
28
32
 
@@ -33,7 +37,7 @@ TPS 标签后放置 12px Lucide info 图标,继承标签颜色,间距 6px。
33
37
  ### 折叠入口
34
38
  - 首次安装默认展开;用户收起后在模型选择左侧显示 12px context 环,24px 热区;原生 context 环开启时隐藏原生重复入口,卸载时恢复。
35
39
  - Tooltip 沿用原生 context 内容:上下文窗口、使用/剩余比例、已用/容量(k tokens);样式遵循通用 Tooltip。
36
- - 点击环展开输入框下方详情,右侧 chevron-up 收起;200ms grid 高度与160ms透明度过渡,尊重 reduced-motion。展开状态跨会话、主聊天和 side chat 共享并持久保存。展开时隐藏输入框内的 context 环;收起动效完成后容器 display:none,不保留父级 gap,收起内容不参与键盘焦点。每个输入框订阅自己的会话上下文和统计。
40
+ - 点击环展开输入框下方详情,右侧 chevron-up 收起。原生布局一次切换到目标状态,测量切换前后的位置,仅用 transform 衔接输入框与模型控件;状态行入场配合4px位移与透明度过渡;中途反向按当前位移衔接。展开220ms、收起160ms,曲线 `cubic-bezier(0.23,1,0.32,1)`。反向操作从当前可见位置和透明度继续,取消旧动画;退出内容临时脱离布局,结束后恢复并 display:none,不保留父级 gap。reduced-motion 立即切换,收起内容不参与键盘焦点。展开状态跨会话、主聊天和 side chat 共享并持久保存,展开时隐藏输入框内的 context 环;每个输入框订阅自己的会话上下文和统计。
37
41
  - Cache 按原始 cachedInputTokens / inputTokens 计算:大于99%显示一位小数,其余取整数,状态栏和详情共用格式。
38
42
 
39
43
  输入框实例仅绑定可见的 thread composer。全屏标签页中的隐藏聊天、浮动 Quick Chat(含 right-panel composer overlay)、inert 区域不注入;原生移动输入框或切换可见性时销毁实例并取消订阅,返回聊天后重建。
@@ -2,10 +2,17 @@
2
2
  export function createContextUsageAdapter(signal, codex, onChange, loadFamily = null) {
3
3
  let family=null, thread=null, composer=null, unsubscribe=null;
4
4
  const empty = () => ({ contextUsedTokens:null, contextWindow:null, contextPercent:null });
5
+ let published = null;
6
+ function publish(value) {
7
+ // 未就绪时重复连接不能重绘,否则 DOM 监听会在微任务中自循环。
8
+ if (published && Object.keys(value).every(key => published[key] === value[key])) return;
9
+ published = value;
10
+ onChange(value);
11
+ }
5
12
  function disconnect() { unsubscribe?.(); unsubscribe=null; }
6
13
  function connect(id, element) {
7
- if (thread === id && unsubscribe) return;
8
- disconnect(); thread=id; composer=element; onChange(empty());
14
+ if (thread === id && composer === element && unsubscribe) return;
15
+ disconnect(); thread=id; composer=element; publish(empty());
9
16
  if (!id || !family || !element || signal.aborted) return;
10
17
  const props=codex.props(element, p => p.value instanceof Map && [...p.value.values()].some(node => node?.token?.__scopeBrand === 'AppScope'));
11
18
  const chain=props?.value, node=chain && [...chain.values()].find(node => node?.token?.__scopeBrand === 'AppScope');
@@ -13,11 +20,11 @@ export function createContextUsageAdapter(signal, codex, onChange, loadFamily =
13
20
  try {
14
21
  const atom=family.resolve(node,chain,id).resolve(node,chain);
15
22
  const update=()=>{
16
- if (signal.aborted || thread !== id) return;
23
+ if (signal.aborted || thread !== id || composer !== element) return;
17
24
  const usage=node.store.get(atom), window=usage?.modelContextWindow, total=usage?.last?.totalTokens;
18
25
  const valid=Number.isFinite(window) && window > 0 && Number.isFinite(total) && total >= 0;
19
26
  const used=valid ? Math.min(total,window) : null;
20
- onChange(valid ? {contextUsedTokens:used,contextWindow:window,contextPercent:Math.round(used/window*100)} : empty());
27
+ publish(valid ? {contextUsedTokens:used,contextWindow:window,contextPercent:Math.round(used/window*100)} : empty());
21
28
  };
22
29
  unsubscribe=node.store.sub(atom,update); update(); codex.report("context-usage",true);
23
30
  } catch { codex.report("context-usage",false,"atom-contract-unavailable"); }
@@ -7,6 +7,7 @@ function estimatedTextTokens(value) {
7
7
  }
8
8
 
9
9
  function estimatedContentTokens(content) {
10
+ if (typeof content === "string") return estimatedTextTokens(content);
10
11
  if (!Array.isArray(content)) return 0;
11
12
  return content.reduce((total, item) => {
12
13
  if (item?.type === "input_image" || item?.type === "output_image") return total + IMAGE_TOKENS_ESTIMATE;
@@ -71,7 +72,7 @@ export function sessionFacts(record) {
71
72
  if (p.type === "task_complete" && Number.isFinite(p.time_to_first_token_ms)) emit("first-token-measured", { elapsed:p.time_to_first_token_ms });
72
73
  if (p.type === "item_completed") {
73
74
  if (p.item?.type === "ContextCompaction") emit("compaction-finished");
74
- if (MODEL_OUTPUT_ITEMS.has(p.item?.type) && Number.isFinite(p.started_at_ms)) emit("output-observed", { start:p.started_at_ms });
75
+ if (MODEL_OUTPUT_ITEMS.has(p.item?.type) && Number.isFinite(p.started_at_ms)) emit("output-observed", { start:p.started_at_ms, end:p.completed_at_ms });
75
76
  if (TOOL_ITEMS.has(p.item?.type) && Number.isFinite(p.started_at_ms) && Number.isFinite(p.completed_at_ms)) emit("tool-measured", { start:p.started_at_ms, end:p.completed_at_ms });
76
77
  }
77
78
  if (p.type === "token_count") emit("usage-observed", { total:usage(p.info?.total_token_usage), last:usage(p.info?.last_token_usage), contextWindow:p.info?.model_context_window });
@@ -14,6 +14,7 @@ export function createSessionState() {
14
14
  last: null,
15
15
  contextWindow: null,
16
16
  firstOutputAt: null,
17
+ pendingOutputIntervals: [],
17
18
  pendingToolIntervals: [],
18
19
  speedMeasurements: [],
19
20
  ttftTotalMs: 0,
@@ -30,14 +31,17 @@ export function createSessionState() {
30
31
  };
31
32
  }
32
33
 
33
- function overlappingToolMs(intervals, start, end) {
34
- const clipped = intervals
35
- .map(([toolStart, toolEnd]) => [Math.max(start, toolStart), Math.min(end, toolEnd)])
34
+ function boundedToolWaitMs(intervals, start, end, outputs) {
35
+ // 仅扣同一响应内、与已观测生成不重叠的工具区间;后台进程存活时间不是等待时间。
36
+ if (outputs.some(([, outputEnd]) => !Number.isFinite(outputEnd))) return 0;
37
+ const waits = intervals
38
+ .filter(([toolStart, toolEnd]) => toolStart >= start && toolEnd <= end
39
+ && !outputs.some(([outputStart, outputEnd]) => toolStart < outputEnd && toolEnd > outputStart))
36
40
  .filter(([left, right]) => right > left)
37
41
  .sort((left, right) => left[0] - right[0]);
38
42
  let elapsed = 0;
39
43
  let rightEdge = start;
40
- for (const [left, right] of clipped) {
44
+ for (const [left, right] of waits) {
41
45
  elapsed += Math.max(0, right - Math.max(left, rightEdge));
42
46
  rightEdge = Math.max(rightEdge, right);
43
47
  }
@@ -45,7 +49,7 @@ function overlappingToolMs(intervals, start, end) {
45
49
  }
46
50
 
47
51
  export function applySessionStatusEvent(state, event) {
48
- const reset = () => { state.firstOutputAt=null; state.pendingToolIntervals=[]; };
52
+ const reset = () => { state.firstOutputAt=null; state.pendingOutputIntervals=[]; state.pendingToolIntervals=[]; };
49
53
  switch (event.kind) {
50
54
  case "history-replaced":
51
55
  state.compactionRecords++; state.last=null; reset();
@@ -63,11 +67,14 @@ export function applySessionStatusEvent(state, event) {
63
67
  case "turn-ended": reset(); if (event.id) state.runningTurns.delete(event.id); else state.runningTurns.clear(); break;
64
68
  case "first-token-measured": state.ttftTotalMs+=event.elapsed; state.ttftSamples++; break;
65
69
  case "compaction-finished": state.compactionCompleted++; break;
66
- case "output-observed": state.firstOutputAt=Math.min(state.firstOutputAt ?? event.start,event.start); break;
70
+ case "output-observed":
71
+ state.firstOutputAt=Math.min(state.firstOutputAt ?? event.start,event.start);
72
+ state.pendingOutputIntervals.push([event.start,event.end]);
73
+ break;
67
74
  case "tool-measured":
68
75
  state.toolMs+=Math.max(0,event.end-event.start);
69
76
  state.pendingToolIntervals.push([event.start,event.end]);
70
- // 工具记录可能晚到,补扣与既有响应样本重叠的时间。
77
+ // 工具记录可能晚到;是否属于可扣除等待区间,由样本的生成边界判断。
71
78
  for (const sample of state.speedMeasurements) if (event.start < sample.end && event.end > sample.start) sample.tools.push([event.start,event.end]);
72
79
  break;
73
80
  case "usage-observed":
@@ -81,7 +88,8 @@ export function applySessionStatusEvent(state, event) {
81
88
  state.responses.add(event.id);
82
89
  const output=event.last?.output;
83
90
  if (Number.isFinite(output) && output > 0 && state.firstOutputAt != null && Number.isFinite(event.end) && event.end > state.firstOutputAt)
84
- state.speedMeasurements.push({ start:state.firstOutputAt,end:event.end,output,tools:state.pendingToolIntervals.slice() });
91
+ state.speedMeasurements.push({ start:state.firstOutputAt,end:event.end,output,
92
+ outputs:state.pendingOutputIntervals.slice(),tools:state.pendingToolIntervals.slice() });
85
93
  }
86
94
  reset();
87
95
  if (event.total) { state.total=event.total; state.hasResponseUsage=true; }
@@ -96,7 +104,7 @@ export function sessionStatusSnapshot(state, threadId) {
96
104
  let decodeMs = 0;
97
105
  let speedSamples = 0;
98
106
  for (const sample of state.speedMeasurements) {
99
- const elapsed = sample.end - sample.start - overlappingToolMs(sample.tools, sample.start, sample.end);
107
+ const elapsed = sample.end - sample.start - boundedToolWaitMs(sample.tools, sample.start, sample.end, sample.outputs);
100
108
  if (elapsed <= 0 || elapsed >= 600_000) continue;
101
109
  decodeTokens += sample.output;
102
110
  decodeMs += elapsed;
@@ -35,15 +35,13 @@ style.id = STYLE_ID;
35
35
  style.textContent = `
36
36
  [${MARKER}] { box-sizing:border-box; display:flex; flex-wrap:wrap; justify-content:center; align-items:center; gap:0 4px; margin:0 auto; padding-top:2px; max-width:100%; min-height:24px; min-width:0; font-family:var(--mcx-font-sans-default,inherit); font-size:var(--mcx-text-xs,12px); line-height:16px; font-variant-numeric:tabular-nums; color:var(--mcx-color-text-tertiary); }
37
37
  [${MARKER}][hidden] { display:none; }
38
- [data-minecodex-session-reveal] { display:grid; grid-template-rows:0fr; opacity:0; transition:grid-template-rows 200ms ease,opacity 160ms ease; }
39
- [data-minecodex-session-reveal][data-expanded] { grid-template-rows:1fr; opacity:1; }
40
- @starting-style { [data-minecodex-session-reveal][data-expanded] { grid-template-rows:0fr; opacity:0; } }
38
+ [data-minecodex-session-reveal] { display:grid; grid-template-rows:1fr; }
41
39
  [data-minecodex-session-reveal][data-expanded]:not([hidden]) { margin-top:-4px; }
42
40
  [data-minecodex-session-reveal] > div { min-height:0; overflow:hidden; }
43
41
  [data-minecodex-session-reveal]:not([data-expanded]) { pointer-events:none; }
44
42
  [data-minecodex-session-reveal][hidden] { display:none; }
45
43
  /* 仅展开状态行时缩减原生底部留白;收起或卸载后自动恢复。 */
46
- .bottom-0.pb-4:has([data-minecodex-session-reveal][data-expanded]:not([hidden])) { padding-bottom:4px; }
44
+ [data-thread-scroll-footer="true"]:has([data-minecodex-session-reveal][data-expanded]:not([hidden])) { --thread-composer-bottom-inset:4px; }
47
45
  [data-minecodex-context-trigger] { display:inline-flex; align-items:center; justify-content:center; flex:none; width:24px; height:var(--height-token-button-composer,28px); padding:0; border:0; border-radius:var(--mcx-radius-full); background:transparent; color:var(--mcx-color-text-tertiary); cursor:pointer; }
48
46
  [data-minecodex-context-trigger][hidden] { display:none; }
49
47
  [data-context-progress] { transition:stroke-dashoffset 120ms ease-out; }
@@ -102,7 +100,7 @@ window.addEventListener("storage", (event) => {
102
100
  }, { signal });
103
101
  // Codex 会把离屏输入框搬到 Quick Chat 浮层;节点仍连接不代表当前页面是聊天。
104
102
  function isActiveComposer(composer) {
105
- return Boolean(config.codex.composer(composer));
103
+ return Boolean(config.codex.composerSurface(composer));
106
104
  }
107
105
  function createInstance(composerElement) {
108
106
  let copy = selectedCopy();
@@ -156,15 +154,30 @@ document.body.append(tooltip);
156
154
  let expanded = globalExpanded;
157
155
  let collapseTimer = null;
158
156
  let collapsing = false;
157
+ const motion = new Set();
158
+ function clearMotion() {
159
+ for (const animation of motion) animation.cancel();
160
+ motion.clear();
161
+ for (const key of ["position", "left", "top", "width"]) reveal.style.removeProperty(key);
162
+ }
159
163
  const nativeIndicators = new Set();
160
164
  function setExpanded(value) {
165
+ const card = reveal.previousElementSibling;
166
+ const model = card?.querySelector('[data-composer-navigation-target="reasoning"]');
167
+ const controls = model?.parentElement;
168
+ const before = card?.getBoundingClientRect();
169
+ const controlsBefore = controls?.getBoundingClientRect();
170
+ const revealBefore = reveal.getBoundingClientRect();
171
+ const opacity = reveal.hidden ? 0 : Number(getComputedStyle(reveal).opacity);
172
+ const animate = value !== expanded && !root.hidden && !matchMedia("(prefers-reduced-motion: reduce)").matches;
161
173
  clearTimeout(collapseTimer);
162
- collapsing = expanded && !value && !matchMedia("(prefers-reduced-motion: reduce)").matches;
174
+ clearMotion();
175
+ collapsing = animate && !value;
163
176
  expanded = value;
164
- if (collapsing) collapseTimer = setTimeout(() => {
165
- collapsing = false;
166
- syncVisibility(findComposer());
167
- }, 200);
177
+ // 退出内容暂时脱离布局,原生输入框一次到达目标位置,再以位移衔接。
178
+ if (collapsing) Object.assign(reveal.style, {
179
+ position:"fixed", left:`${revealBefore.left}px`, top:`${revealBefore.top}px`, width:`${revealBefore.width}px`,
180
+ });
168
181
  reveal.toggleAttribute("data-expanded", expanded);
169
182
  clip.inert = !expanded;
170
183
  trigger.setAttribute("aria-expanded", String(expanded));
@@ -172,6 +185,25 @@ function setExpanded(value) {
172
185
  if (expanded) reveal.hidden = false;
173
186
  syncVisibility(findComposer());
174
187
  if (!expanded) closePanel();
188
+ if (!animate) return;
189
+ const duration = expanded ? 220 : 160;
190
+ const options = { duration, easing:"cubic-bezier(0.23,1,0.32,1)", fill:"both" };
191
+ const play = (node, frames) => { if (node) motion.add(node.animate(frames, options)); };
192
+ const after = card.getBoundingClientRect();
193
+ play(card, [{transform:`translateY(${before.top-after.top}px)`},{transform:"translateY(0)"}]);
194
+ if (controlsBefore) {
195
+ const after = controls.getBoundingClientRect();
196
+ play(controls, [{transform:`translateX(${controlsBefore.left-after.left}px)`},{transform:"translateX(0)"}]);
197
+ }
198
+ const offset = expanded && revealBefore.height ? revealBefore.top-reveal.getBoundingClientRect().top : -4;
199
+ play(reveal, [{opacity,transform:`translateY(${expanded ? offset : 0}px)`},
200
+ {opacity:expanded ? 1 : 0,transform:`translateY(${expanded ? 0 : -4}px)`}]);
201
+ if (!expanded) play(trigger, [{opacity:0},{opacity:1}]);
202
+ collapseTimer = setTimeout(() => {
203
+ collapsing = false;
204
+ clearMotion();
205
+ syncVisibility(findComposer());
206
+ }, duration);
175
207
  }
176
208
  trigger.addEventListener("click", () => setGlobalExpanded(!expanded), { signal });
177
209
  function hideTooltip() { tooltip.hidden = true; trigger.removeAttribute("aria-describedby"); }
@@ -401,7 +433,7 @@ function findComposer() {
401
433
  return isActiveComposer(composerElement) ? composerElement : null;
402
434
  }
403
435
  function currentThreadId(composer) {
404
- return config.codex.currentThreadId(composer);
436
+ return composer ? config.codex.composerSurface(composer)?.threadId ?? null : null;
405
437
  }
406
438
  // 与原生 Context 指示器订阅同一会话状态;不以日志百分比覆盖它。
407
439
  const emptyNativeContext = () => ({ contextUsedTokens:null,contextWindow:null,contextPercent:null });
@@ -445,6 +477,9 @@ function ensurePosition() {
445
477
  const composer = findComposer();
446
478
  const threadId = currentThreadId(composer);
447
479
  if (threadId !== activeThreadId) {
480
+ clearTimeout(collapseTimer);
481
+ clearMotion();
482
+ collapsing = false;
448
483
  activeThreadId = threadId;
449
484
  nativeContext = emptyNativeContext();
450
485
  // 会话建立后先展示占位,不让日志落盘速度决定状态栏是否出现。
@@ -547,6 +582,7 @@ return { update: receiver.update, setExpanded, dispose() {
547
582
  nativeUsage.dispose();
548
583
  clearTimeout(refreshTimer);
549
584
  clearTimeout(collapseTimer);
585
+ clearMotion();
550
586
  observer.disconnect();
551
587
  localeObserver.disconnect();
552
588
  closePanel();
@@ -560,7 +596,7 @@ function syncInstances() {
560
596
  for (const [composer, instance] of instances) {
561
597
  if (!isActiveComposer(composer)) { instance.dispose(); instances.delete(composer); }
562
598
  }
563
- for (const composer of document.querySelectorAll('[data-codex-composer="true"][contenteditable="true"]')) {
599
+ for (const composer of document.querySelectorAll('[data-codex-composer-root]')) {
564
600
  if (isActiveComposer(composer) && !instances.has(composer)) instances.set(composer, createInstance(composer));
565
601
  }
566
602
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "minecodex",
3
- "version": "2.0.3",
3
+ "version": "2.0.5",
4
4
  "description": "Lightweight, local-first plugins for the Codex desktop app.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -6,8 +6,9 @@
6
6
 
7
7
  | 接口 | 结果与约束 |
8
8
  | --- | --- |
9
- | `composer(element?, {threadOnly}?)` | 默认仅可见的 thread 输入框;Prompts 显式传 false 支持首页输入框。返回 editable/root/body/add/threadId;非thread不提供身份,隐藏祖先返回 null |
9
+ | `composer(element?, {threadOnly, purpose}?)` | 默认仅可见的 Codex thread 输入框;Prompts 使用 `purpose:"insertion"` 支持 Chat、Work、Codex、Temporary Chat 与面板 chat bar。优先当前焦点所在输入框,并可显式绑定插入目标。返回 editable/root/body/add/threadId;非thread不提供身份,隐藏祖先返回 null |
10
10
  | `currentThreadId(element?)` | 从合格输入框取得 UUID;不从残留 sidebar 选中项猜当前线程 |
11
+ | `composerSurface(element?)` | 返回可见正式会话外壳 root/threadId,独立于编辑区是否存在或暂时 inert;供会话状态呈现与宿主统计订阅使用。真正隐藏、移入浮层、切换 home 或卸载时返回 null;不能作为文字插入许可 |
11
12
  | `rail()` / `railSlot(ownGroup)` | 可见原生 rail 与模板/滚动区/固定组句柄;不包含插件菜单策略 |
12
13
  | `currentFiber(element)` / `props(element,predicate,limit?)` | 仅供 native Adapter 使用;优先匹配当前 committed props,支持最近祖先的 scope |
13
14
  | `report(name,available,reason?)` / `diagnostics()` | 最新能力状态的副本;不持有凭据、原始账户数据或用户内容 |
@@ -18,12 +18,17 @@ export function createCodexEnvironment() {
18
18
  }
19
19
  return null;
20
20
  }
21
- function composer(element = null, {threadOnly=true} = {}) {
22
- const candidates = element ? [element] : document.querySelectorAll('[data-codex-composer="true"][contenteditable="true"]');
21
+ function composer(element = null, {threadOnly=true, purpose="thread"} = {}) {
22
+ const insertion = purpose === "insertion";
23
+ const rootSelector = insertion ? '[data-codex-composer-root], [data-chatgpt-composer]' : '[data-codex-composer-root]';
24
+ const candidates = element ? [element] : [...document.querySelectorAll(insertion
25
+ ? '[data-codex-composer="true"][contenteditable="true"], [data-chatgpt-composer] [data-composer-markdown][contenteditable="true"]'
26
+ : '[data-codex-composer="true"][contenteditable="true"]')];
27
+ if (!element && insertion) candidates.sort((a,b) => Number(Boolean(b.closest(rootSelector)?.contains(document.activeElement))) - Number(Boolean(a.closest(rootSelector)?.contains(document.activeElement))));
23
28
  for (const editable of candidates) {
24
- const root = editable.closest('[data-codex-composer-root]');
25
- if (!editable.isConnected || !root || (threadOnly && root.getAttribute('data-composer-placement') !== 'thread')
26
- || editable.closest('[hidden], [inert], [aria-hidden="true"], [data-floating-chat-surface], [data-right-panel-composer-overlay-content]')
29
+ const root = editable.closest(rootSelector);
30
+ if (!editable.isConnected || !root || (!insertion && threadOnly && root.getAttribute('data-composer-placement') !== 'thread')
31
+ || editable.closest(insertion ? '[hidden], [inert], [aria-hidden="true"]' : '[hidden], [inert], [aria-hidden="true"], [data-floating-chat-surface], [data-right-panel-composer-overlay-content]')
27
32
  || !editable.checkVisibility({ visibilityProperty:true })) continue;
28
33
  const value = root.getAttribute('data-composer-placement') === 'thread' ? root.querySelector('[data-above-composer-portal]')?.getAttribute('data-above-composer-conversation-id') : null;
29
34
  const threadId = value?.match(/[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/i)?.[0] ?? null;
@@ -34,6 +39,19 @@ export function createCodexEnvironment() {
34
39
  report("composer", false, "active-thread-composer-unavailable");
35
40
  return null;
36
41
  }
42
+ // 状态属于会话外壳;发送时编辑区可能 inert 或重建,不代表离开会话。
43
+ function composerSurface(element = null) {
44
+ const roots = element ? [element.closest('[data-codex-composer-root]')] : document.querySelectorAll('[data-codex-composer-root]');
45
+ for (const root of roots) {
46
+ if (!root?.isConnected || root.getAttribute('data-composer-placement') !== 'thread'
47
+ || root.closest('[hidden], [aria-hidden="true"], [data-floating-chat-surface], [data-right-panel-composer-overlay-content]')
48
+ || !root.checkVisibility({visibilityProperty:true})) continue;
49
+ const value = root.querySelector('[data-above-composer-portal]')?.getAttribute('data-above-composer-conversation-id');
50
+ const threadId = value?.match(/[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/i)?.[0] ?? null;
51
+ return {root,threadId};
52
+ }
53
+ return null;
54
+ }
37
55
  function rail() {
38
56
  const node = [...document.querySelectorAll('[data-app-navigation-rail]')].find(node => node.getBoundingClientRect().width > 0) ?? null;
39
57
  report("navigation-rail", Boolean(node), node ? null : "rail-unavailable");
@@ -47,7 +65,7 @@ export function createCodexEnvironment() {
47
65
  const group = [...(scroll?.children ?? [])].find(child => child !== ownGroup && child.classList.contains('flex-col') && child.classList.contains('shrink-0'));
48
66
  return node && template && scroll ? { rail:node, template, scroll, group } : null;
49
67
  }
50
- return { composer, rail, railSlot, currentThreadId:element => composer(element)?.threadId ?? null,
68
+ return { composer, composerSurface, rail, railSlot, currentThreadId:element => composer(element)?.threadId ?? null,
51
69
  // 仅供 UI/插件 native Adapter 使用,交互层不读取 React props。
52
70
  currentFiber, props, report, diagnostics:() => [...capabilities.values()].map(item => ({...item})) };
53
71
  }
@@ -812,10 +812,7 @@ export function createInjectionSource(features, {
812
812
  }
813
813
 
814
814
  function currentComposer() {
815
- const marked = document.querySelector('[data-codex-composer="true"][contenteditable="true"]');
816
- if (marked) return marked;
817
- return Array.from(document.querySelectorAll('[contenteditable="true"][role="textbox"]'))
818
- .find((element) => isVisibleToolbarControl(element)) ?? null;
815
+ return codex.composer(null, { purpose: "insertion" })?.editable ?? null;
819
816
  }
820
817
 
821
818
  function rememberComposerSelection() {
@@ -828,9 +825,9 @@ export function createInjectionSource(features, {
828
825
  savedComposerThread = threadIdentity();
829
826
  }
830
827
 
831
- function insertComposerText(text) {
828
+ function insertComposerText(text, target = null) {
832
829
  if (typeof text !== "string" || !text) throw new Error("Text is required");
833
- const composer = currentComposer();
830
+ const composer = target ? codex.composer(target, { purpose: "insertion" })?.editable : currentComposer();
834
831
  if (!composer) throw new Error("The active Composer was not found");
835
832
  const selection = document.getSelection();
836
833
  let fallback = true;
@@ -1,3 +1,4 @@
1
+ import { LocalImageSource } from "../../../features/images/src/backend/local-image-source.mjs";
1
2
  import { cloudImagesAction } from "../../../features/images/src/backend/cloud-library.mjs";
2
3
  // CodexRuntime:Codex Renderer 生命周期与 host action 执行。
3
4
  // 注入源(createInjectionSource)与 CDP 客户端分别在 codex-injection.mjs / codex-cdp.mjs。
@@ -34,6 +35,7 @@ import {
34
35
  } from "./host-actions.mjs";
35
36
  import { SitesService } from "../../../features/sites/src/backend/sites-service.mjs";
36
37
  import { SessionStatusReader, threadIdFromLocation } from "../../../features/session-status/src/adapters/codex/session-reader.mjs";
38
+ import { createCodexEnvironment } from "../../codex-adapter/src/renderer/environment.mjs";
37
39
 
38
40
 
39
41
  // 保持公共入口兼容:历史 import 从本文件继续可用。
@@ -48,11 +50,9 @@ export {
48
50
  } from "./codex-injection.mjs";
49
51
 
50
52
  export const SESSION_STATUS_THREAD_ID_EXPRESSION = `(() => {
51
- return [...new Set([...document.querySelectorAll('[data-codex-composer="true"][contenteditable="true"]')].map(composer => {
52
- const card = composer.closest('[data-codex-composer-root]');
53
- if (card?.getAttribute('data-composer-placement') !== 'thread') return null;
54
- return card.querySelector('[data-above-composer-portal]')?.getAttribute('data-above-composer-conversation-id');
55
- }).filter(Boolean))];
53
+ const codex = (${createCodexEnvironment.toString()})();
54
+ return [...new Set([...document.querySelectorAll('[data-codex-composer-root]')]
55
+ .map(root => codex.composerSurface(root)?.threadId).filter(Boolean))];
56
56
  })()`;
57
57
 
58
58
  export class CodexRuntime {
@@ -79,6 +79,7 @@ export class CodexRuntime {
79
79
  }) {
80
80
  this.features = features;
81
81
  this.sitesService = sitesService ?? (features.some(feature => feature.id === "sites") ? new SitesService() : null);
82
+ this.localImageSource = features.some(feature => feature.id === "images") ? new LocalImageSource({dataDir:path.join(profileDir,"minecodex-image-gen")}) : null;
82
83
  this.profileDir = profileDir;
83
84
  this.appPath = appPath;
84
85
  this.cdpPort = cdpPort;
@@ -549,6 +550,15 @@ export class CodexRuntime {
549
550
  });
550
551
  return;
551
552
  }
553
+ if (feature.id === "images" && request.action === "local-image-source") {
554
+ try {
555
+ const result = await this.localImageSource.action(request.payload);
556
+ await this.resolveHostAction(client, request.requestId, {ok:true,result});
557
+ } catch (error) {
558
+ await this.resolveHostAction(client, request.requestId, {ok:false,error:{code:"LOCAL_IMAGE_SOURCE_FAILED",message:error.message}});
559
+ }
560
+ return;
561
+ }
552
562
  if (feature.id === "sites" && request.action === "sites") {
553
563
  try {
554
564
  const result = await this.sitesService.action(request.payload);
@@ -812,6 +822,7 @@ export class CodexRuntime {
812
822
  if (this.stopPromise) return this.stopPromise;
813
823
  this.stopPromise = (async () => {
814
824
  this.stopping = true;
825
+ await this.localImageSource?.close();
815
826
  this.targetRefreshQueued = false;
816
827
  this.monitorWake?.();
817
828
  await this.monitorPromise?.catch(() => {});
@@ -17,6 +17,7 @@ const HOST_ACTIONS = new Set([
17
17
  "resolve-file-paths",
18
18
  "import-generated-image",
19
19
  "list-cloud-images",
20
+ "local-image-source",
20
21
  "import-cloud-image",
21
22
  "create-image-edit-thread",
22
23
  ]);
@@ -13,15 +13,18 @@
13
13
  - 原生适配层:解析实际token、主题、zoom和能力状态;共享锚点仅包含真实跨插件需求。原生事实遵循根规范,不在每个组件重新猜测。
14
14
  - 材质与层级基础:按Dialog、Dropdown、Popover、ComposerMenu、Page的原生分支提供有限选项,将遮罩与Portal规则同所选组件匹配。
15
15
  - Dialog:提供容器、标题、Close及操作区的结构;插件填表单、列表或确认内容。
16
- - Button:表达操作角色和状态,保留Cancel ghost、实际次级outline与主操作solid的语义区分;业务动作由插件传入。
17
- - Navbar:使用成对 default/selected 图标管理展示状态;原生图标与选中状态适配集中在 native 层,保留原生路由与 sidebar hover。
16
+ - Button:通过 `action(node, "danger-solid")` 提供原生实色警戒分支,保留既有 `danger` soft 分支;通用 token 与适用规则见根 DESIGN.md。表达操作角色和状态,保留Cancel ghost、实际次级outline与主操作solid的语义区分;业务动作由插件传入。
17
+ - Navbar:使用成对 default/selected 图标管理展示状态;原生图标与选中状态适配集中在 native 层,保留原生路由与 sidebar hover。退出注入页面时按当前 committed 原生状态恢复选中属性,不回放进入时的旧状态;原生目的地变化时及时移除替身图标,原节点复用或重渲染后仍保证只显示一个图标。
18
18
  - Tooltip:处理声明式标记、hover/focus、aria关联、关闭与卸载,不接管原生Tooltip。
19
+ - Page 的 `sidebar:"none"` 分支通过 native/page Adapter 取得原生无上下文侧栏页面壳;`releasePage` 释放时区分返回原页面和用户主动导航,禁用时清理。
19
20
  - 页面、菜单与浮层:提供对应类型的外壳、挂载和通用交互,不读取提示词、会话或站点业务数据。
20
21
 
21
22
  ## 接入约束
22
23
 
23
24
  当前page-script通过 `config.ui` 使用公共能力;现有API包括 `heading`、`footer`、`action`、`overlay`、`surface`。`surface(node, kind, options)` 显式区分原生材质;`page`、`mount`、`trapFocus`、`composerMenu`、`tooltipSurface`、`navigationRail` 维护实际复用的层级、焦点与定位。`entry.resources` 通过 `config.resources` 提供插件自身 CSS 与图标,通用视觉仍由组件库维护。
24
25
 
26
+ 独立全页面且无专属二级导航的功能使用 `config.ui.page(node, { sidebar:"none" })`。移除页面前调用 `config.ui.releasePage(node)`;若用户正在主动选择原生导航,则传 `{ restoreNavigation:false }`,避免恢复动作覆盖用户选择。默认 `sidebar:"inherit"` 保留现有接入行为,不自动迁移其他功能。页面壳的产品规则与验收标准见根 DESIGN.md 的「全页面功能的页面壳选择」。
27
+
25
28
  Renderer安装函数经序列化注入,必须自包含。组件代码与Node/CDP隔离,宿主负责组装注入源码、生命周期和iframe主题桥;不为整理目录引入不必要的打包系统。
26
29
 
27
30
  通用生命周期契约由组件库维护。原生已经提供的焦点圈定、Escape和关闭机制优先复用;插件继续拥有业务状态、内容更新和具体回调。迁移时同步触发点焦点恢复、异步卸载及原生节点归属验证。
@@ -11,6 +11,8 @@ export function installButton(signal, ui) {
11
11
  [data-mcx-ui-surface=dialog] [data-mcx-ui-footer]>[data-mcx-ui-action=primary]:hover:not(:disabled):not([aria-disabled=true]) { background:var(--mcx-color-background-primary-solid-hover); }
12
12
  [data-mcx-ui-surface=dialog] [data-mcx-ui-footer]>[data-mcx-ui-action=danger] { box-shadow:none; background:var(--mcx-color-background-danger-soft-alpha); color:var(--mcx-color-text-danger); }
13
13
  [data-mcx-ui-surface=dialog] [data-mcx-ui-footer]>[data-mcx-ui-action=danger]:hover:not(:disabled):not([aria-disabled=true]) { background:var(--mcx-color-background-danger-soft-alpha-hover); }
14
+ [data-mcx-ui-action=danger-solid], [data-mcx-ui-surface=dialog] [data-mcx-ui-footer]>[data-mcx-ui-action=danger-solid] { box-shadow:none; border-color:transparent; background:var(--mcx-color-background-danger-solid); color:var(--mcx-color-text-danger-solid); }
15
+ [data-mcx-ui-action=danger-solid]:hover:not(:disabled):not([aria-disabled=true]), [data-mcx-ui-surface=dialog] [data-mcx-ui-footer]>[data-mcx-ui-action=danger-solid]:hover:not(:disabled):not([aria-disabled=true]) { background:color-mix(in oklab,var(--mcx-color-background-danger-solid) 90%,transparent); }
14
16
  [data-mcx-ui-surface=dialog] button:disabled { opacity:.4; cursor:default; }
15
17
  [data-mcx-ui-surface=dialog] [data-mcx-ui-close]:focus-visible, [data-mcx-ui-surface=dialog] [data-mcx-ui-footer]>button:focus-visible { outline:2px solid var(--mcx-color-ring); outline-offset:0; }
16
18
  `;