minecodex 2.0.4 → 2.0.6
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 +40 -1
- package/README.md +9 -3
- package/apps/dashboard/DESIGN.md +53 -31
- package/apps/dashboard/app.js +55 -39
- package/apps/dashboard/assets/OFL.txt +93 -0
- package/apps/dashboard/assets/info.svg +15 -0
- package/apps/dashboard/assets/moon.svg +13 -0
- package/apps/dashboard/assets/refresh-cw.svg +16 -0
- package/apps/dashboard/assets/silkscreen.ttf +0 -0
- package/apps/dashboard/assets/sun.svg +21 -0
- package/apps/dashboard/assets/terrain.js +89 -0
- package/apps/dashboard/assets/triangle-alert.svg +15 -0
- package/apps/dashboard/favicon.svg +12 -17
- package/apps/dashboard/index.html +24 -18
- package/apps/dashboard/styles.css +147 -181
- package/assets/brand/README.md +19 -0
- package/assets/brand/minecodex-icon-dark.svg +9 -0
- package/assets/brand/minecodex-icon-light.svg +9 -0
- package/assets/brand/minecodex-icon.svg +8 -0
- package/assets/brand/references/lucide-LICENSE.txt +43 -0
- package/assets/readme/hero.svg +93 -0
- package/features/hide-upsell-banner/README.md +7 -5
- package/features/hide-upsell-banner/codex-feature.json +2 -2
- package/features/hide-upsell-banner/src/hide-upsell-banner.mjs +30 -12
- package/features/images/DESIGN.md +117 -41
- package/features/images/README.md +26 -16
- package/features/images/codex-feature.json +6 -3
- package/features/images/src/adapters/codex/image-prompt.mjs +24 -2
- package/features/images/src/adapters/codex/image-sources.mjs +19 -0
- package/features/images/src/adapters/codex/library-api.mjs +2 -0
- package/features/images/src/adapters/codex/library-viewer.mjs +26 -2
- package/features/images/src/adapters/codex/native-image-modules.mjs +211 -0
- package/features/images/src/adapters/codex/native-image-source.mjs +449 -0
- package/features/images/src/backend/local-image-source.mjs +138 -0
- package/features/images/src/frontend/cloud-image-actions.mjs +41 -0
- package/features/images/src/frontend/local-image-source.mjs +195 -0
- package/features/images/src/library-prompt-page.mjs +163 -53
- package/features/images/src/library-prompt-source.mjs +9 -3
- package/features/images/src/prompt-index.mjs +14 -2
- package/features/model-slider/DESIGN.md +2 -0
- package/features/model-slider/codex-feature.json +2 -2
- package/features/model-slider/src/frontend/install.mjs +78 -11
- package/features/notes/DESIGN.md +7 -3
- package/features/notes/codex-feature.json +2 -2
- package/features/notes/src/frontend/entry.mjs +26 -9
- package/features/session-status/DESIGN.md +4 -2
- package/features/session-status/codex-feature.json +2 -2
- package/features/session-status/src/adapters/codex/session-log.mjs +2 -1
- package/features/session-status/src/domain/session-stats.mjs +17 -9
- package/features/session-status/src/session-status.mjs +47 -11
- package/features/sidebar-focus/DESIGN.md +4 -4
- package/features/sidebar-focus/codex-feature.json +3 -2
- package/features/sidebar-focus/src/adapters/codex/renderer.mjs +1 -1
- package/features/sidebar-focus/src/sidebar-focus.mjs +9 -7
- package/features/sites/DESIGN.md +25 -3
- package/features/sites/README.md +6 -2
- package/features/sites/codex-feature.json +3 -2
- package/features/sites/src/adapters/codex/renderer.mjs +25 -4
- package/features/sites/src/backend/project-sites.mjs +18 -6
- package/features/sites/src/backend/sites-service.mjs +22 -10
- package/features/sites/src/domain/discovery-candidates.mjs +53 -0
- package/features/sites/src/frontend/entry.mjs +76 -38
- package/features/sites/src/frontend/styles.css +3 -3
- package/features/unread-inbox/DESIGN.md +24 -25
- package/features/unread-inbox/codex-feature.json +3 -2
- package/features/unread-inbox/src/adapters/codex/modules.mjs +52 -0
- package/features/unread-inbox/src/adapters/codex/renderer.mjs +90 -0
- package/features/unread-inbox/src/domain/notifications.mjs +21 -0
- package/features/unread-inbox/src/injection-source.mjs +5 -2
- package/features/unread-inbox/src/unread-inbox.mjs +212 -297
- package/features/usage-meter/codex-feature.json +2 -2
- package/package.json +11 -3
- package/packages/cli/native/MineCodex.app/Contents/Info.plist +4 -2
- package/packages/cli/native/MineCodex.app/Contents/MacOS/MineCodexServiceManager +0 -0
- package/packages/cli/native/MineCodex.app/Contents/Resources/MineCodex.icns +0 -0
- package/packages/cli/native/MineCodex.app/Contents/_CodeSignature/CodeResources +11 -0
- package/packages/cli/native/README.md +31 -0
- package/packages/cli/src/browser-control-panel.mjs +48 -0
- package/packages/cli/src/commands.mjs +3 -1
- package/packages/cli/src/control-server.mjs +6 -3
- package/packages/cli/src/platform.mjs +5 -0
- package/packages/codex-adapter/README.md +2 -1
- package/packages/codex-adapter/src/renderer/environment.mjs +24 -6
- package/packages/runtime-host/README.md +10 -5
- package/packages/runtime-host/src/codex-injection.mjs +13 -9
- package/packages/runtime-host/src/codex-runtime.mjs +26 -5
- package/packages/runtime-host/src/feature-registry.mjs +1 -0
- package/packages/ui/DESIGN.md +9 -2
- package/packages/ui/assets/arrow-up-right.svg +14 -0
- package/packages/ui/assets/lucide-LICENSE.txt +43 -0
- package/packages/ui/src/components/account-menu.mjs +74 -0
- package/packages/ui/src/components/button.mjs +2 -0
- package/packages/ui/src/components/navigation-rail.mjs +18 -10
- package/packages/ui/src/components/page.mjs +9 -2
- package/packages/ui/src/foundation/layers.mjs +1 -0
- package/packages/ui/src/install.mjs +6 -1
- package/packages/ui/src/native/account-menu.mjs +21 -0
- package/packages/ui/src/native/navigation-rail.mjs +6 -1
- package/packages/ui/src/native/page.mjs +38 -0
- package/packages/ui/src/native/tokens.mjs +4 -0
|
@@ -683,6 +683,7 @@ export function installModelSlider(signal, { feature, ui, createIcon, requestRef
|
|
|
683
683
|
|
|
684
684
|
function sizeModelMenuViewport(menu, maxVisibleItems) {
|
|
685
685
|
if (!menu.isConnected) return;
|
|
686
|
+
if (modelSelectorControllerMenu?.menu === menu && modelSelectorControllerMenu.animations?.length) return;
|
|
686
687
|
const items = modelMenuItems(menu).filter((item) => item.dataset.codexModelSliderRaw);
|
|
687
688
|
if (items.length <= maxVisibleItems) {
|
|
688
689
|
menu.removeAttribute("data-codex-model-slider-overflow");
|
|
@@ -959,9 +960,38 @@ export function installModelSlider(signal, { feature, ui, createIcon, requestRef
|
|
|
959
960
|
}
|
|
960
961
|
}
|
|
961
962
|
|
|
962
|
-
function
|
|
963
|
+
function animateControllerModelMenu(record, opening, onFinish) {
|
|
964
|
+
const height = record.menu.getBoundingClientRect().height;
|
|
965
|
+
clearTimeout(record.motionTimer);
|
|
966
|
+
record.animations?.forEach((animation) => animation.cancel());
|
|
967
|
+
record.animations = [];
|
|
968
|
+
if (matchMedia("(prefers-reduced-motion: reduce)").matches) {
|
|
969
|
+
onFinish();
|
|
970
|
+
return;
|
|
971
|
+
}
|
|
972
|
+
// 原生视图切换:底边保持锚定,高度 320ms,内容 200ms 淡变并位移 10px。
|
|
973
|
+
record.menu.style.overflowY = "hidden";
|
|
974
|
+
const size = record.menu.animate([
|
|
975
|
+
{ height: `${opening ? record.sliderHeight : height}px` },
|
|
976
|
+
{ height: `${opening ? record.listHeight : record.sliderHeight}px` },
|
|
977
|
+
], { duration: 320, easing: "cubic-bezier(.19,1,.22,1)", fill: "both" });
|
|
978
|
+
const content = record.content.animate(opening
|
|
979
|
+
? [{ opacity: 0, translate: "0 10px" }, { opacity: 1, translate: "0 0" }]
|
|
980
|
+
: [{ opacity: 1, translate: "0 0" }, { opacity: 0, translate: "0 10px" }],
|
|
981
|
+
{ duration: 200, delay: opening ? 40 : 0, easing: "cubic-bezier(.23,1,.32,1)", fill: "both" });
|
|
982
|
+
record.animations.push(size, content);
|
|
983
|
+
if (!opening) {
|
|
984
|
+
record.animations.push(record.menu.animate([{ opacity: 1 }, { opacity: 0 }],
|
|
985
|
+
{ duration: 200, delay: 40, easing: "cubic-bezier(.23,1,.32,1)", fill: "both" }));
|
|
986
|
+
}
|
|
987
|
+
// 后台窗口未必派发动画完成事件,清理按同一时长结束,避免残留菜单。
|
|
988
|
+
record.motionTimer = setTimeout(onFinish, 320);
|
|
989
|
+
}
|
|
990
|
+
|
|
991
|
+
function closeControllerModelMenu({ immediate = false } = {}) {
|
|
963
992
|
const record = modelSelectorControllerMenu;
|
|
964
993
|
if (!record) return false;
|
|
994
|
+
if (record.closing && !immediate) return true;
|
|
965
995
|
const placeholder = document.querySelector(
|
|
966
996
|
'[data-codex-intelligence-trigger] [class*="_ModelPickerTriggerPlaceholder_"]',
|
|
967
997
|
);
|
|
@@ -969,10 +999,18 @@ export function installModelSlider(signal, { feature, ui, createIcon, requestRef
|
|
|
969
999
|
placeholder.firstChild.nodeValue = record.placeholderText;
|
|
970
1000
|
}
|
|
971
1001
|
record.button.setAttribute("aria-expanded", "false");
|
|
972
|
-
record.
|
|
973
|
-
record.
|
|
974
|
-
|
|
975
|
-
|
|
1002
|
+
record.closing = true;
|
|
1003
|
+
record.menu.inert = true;
|
|
1004
|
+
const finish = () => {
|
|
1005
|
+
clearTimeout(record.motionTimer);
|
|
1006
|
+
record.animations?.forEach((animation) => animation.cancel());
|
|
1007
|
+
record.parentMenu.style.position = record.position;
|
|
1008
|
+
record.parentMenu.style.overflow = record.overflow;
|
|
1009
|
+
record.menu.remove();
|
|
1010
|
+
if (modelSelectorControllerMenu === record) modelSelectorControllerMenu = null;
|
|
1011
|
+
};
|
|
1012
|
+
if (immediate || !record.parentMenu.isConnected) finish();
|
|
1013
|
+
else animateControllerModelMenu(record, false, finish);
|
|
976
1014
|
return true;
|
|
977
1015
|
}
|
|
978
1016
|
|
|
@@ -988,8 +1026,11 @@ export function installModelSlider(signal, { feature, ui, createIcon, requestRef
|
|
|
988
1026
|
|
|
989
1027
|
function openControllerModelMenu(parentMenu, modelButton, selector) {
|
|
990
1028
|
if (modelSelectorControllerMenu) {
|
|
991
|
-
|
|
992
|
-
|
|
1029
|
+
if (!modelSelectorControllerMenu.closing) {
|
|
1030
|
+
closeControllerModelMenu();
|
|
1031
|
+
return;
|
|
1032
|
+
}
|
|
1033
|
+
closeControllerModelMenu({ immediate: true });
|
|
993
1034
|
}
|
|
994
1035
|
const controller = nativeComposerModelController();
|
|
995
1036
|
if (!Array.isArray(controller?.models) || typeof controller.onSelectModel !== "function") return;
|
|
@@ -1002,7 +1043,12 @@ export function installModelSlider(signal, { feature, ui, createIcon, requestRef
|
|
|
1002
1043
|
menu.setAttribute("data-state", "open");
|
|
1003
1044
|
menu.setAttribute("data-codex-model-slider-menu", "");
|
|
1004
1045
|
menu.setAttribute("data-codex-model-slider-controller-menu", "");
|
|
1005
|
-
featureUI.surface(menu, "dropdown");
|
|
1046
|
+
featureUI.surface(menu, "dropdown", { variant: "opaque" });
|
|
1047
|
+
// 切换同一个 picker 视图,不重复播放整个 Dropdown 的入场缩放。
|
|
1048
|
+
menu.style.animation = "none";
|
|
1049
|
+
const content = document.createElement("div");
|
|
1050
|
+
content.style.flexShrink = "0";
|
|
1051
|
+
menu.append(content);
|
|
1006
1052
|
models.forEach((model, index) => {
|
|
1007
1053
|
const item = document.createElement("div");
|
|
1008
1054
|
item.className = "no-drag outline-hidden rounded-lg px-[var(--padding-row-x)] py-[var(--padding-row-y)] text-sm text-default group hover:bg-primary-ghost-hover focus:bg-primary-ghost-hover cursor-interaction flex flex-col";
|
|
@@ -1014,7 +1060,7 @@ export function installModelSlider(signal, { feature, ui, createIcon, requestRef
|
|
|
1014
1060
|
scheduleReturn: false,
|
|
1015
1061
|
owned: true,
|
|
1016
1062
|
});
|
|
1017
|
-
|
|
1063
|
+
content.append(item);
|
|
1018
1064
|
});
|
|
1019
1065
|
menu.addEventListener("click", (event) => {
|
|
1020
1066
|
const star = event.target.closest?.("[data-codex-model-slider-star]");
|
|
@@ -1038,6 +1084,7 @@ export function installModelSlider(signal, { feature, ui, createIcon, requestRef
|
|
|
1038
1084
|
menu.addEventListener("keydown", (event) => {
|
|
1039
1085
|
if (event.key === "Escape") {
|
|
1040
1086
|
event.preventDefault();
|
|
1087
|
+
event.stopPropagation();
|
|
1041
1088
|
closeControllerModelMenu();
|
|
1042
1089
|
modelButton.focus();
|
|
1043
1090
|
return;
|
|
@@ -1063,11 +1110,13 @@ export function installModelSlider(signal, { feature, ui, createIcon, requestRef
|
|
|
1063
1110
|
|
|
1064
1111
|
modelSelectorControllerMenu = {
|
|
1065
1112
|
menu,
|
|
1113
|
+
content,
|
|
1066
1114
|
parentMenu,
|
|
1067
1115
|
button: modelButton,
|
|
1068
1116
|
placeholderText,
|
|
1069
1117
|
position: parentMenu.style.position,
|
|
1070
1118
|
overflow: parentMenu.style.overflow,
|
|
1119
|
+
sliderHeight: parentMenu.getBoundingClientRect().height,
|
|
1071
1120
|
};
|
|
1072
1121
|
parentMenu.style.position = "relative";
|
|
1073
1122
|
parentMenu.style.overflow = "visible";
|
|
@@ -1076,6 +1125,15 @@ export function installModelSlider(signal, { feature, ui, createIcon, requestRef
|
|
|
1076
1125
|
modelButton.setAttribute("aria-expanded", "true");
|
|
1077
1126
|
(menu.querySelector('[data-codex-model-slider-selected]')
|
|
1078
1127
|
?? menu.querySelector('[role="menuitem"]'))?.focus();
|
|
1128
|
+
const record = modelSelectorControllerMenu;
|
|
1129
|
+
record.listHeight = menu.getBoundingClientRect().height;
|
|
1130
|
+
animateControllerModelMenu(record, true, () => {
|
|
1131
|
+
if (modelSelectorControllerMenu !== record || record.closing) return;
|
|
1132
|
+
record.animations.forEach((animation) => animation.cancel());
|
|
1133
|
+
record.animations = [];
|
|
1134
|
+
menu.style.overflowY = "auto";
|
|
1135
|
+
sizeModelMenuViewport(menu, selector.maxVisibleItems);
|
|
1136
|
+
});
|
|
1079
1137
|
}
|
|
1080
1138
|
|
|
1081
1139
|
// ========================================================================
|
|
@@ -2005,7 +2063,7 @@ export function installModelSlider(signal, { feature, ui, createIcon, requestRef
|
|
|
2005
2063
|
enhanceComposerModelTrigger(selector);
|
|
2006
2064
|
const parentMenu = nativeModelPickerMenu();
|
|
2007
2065
|
if (modelSelectorControllerMenu && modelSelectorControllerMenu.parentMenu !== parentMenu) {
|
|
2008
|
-
closeControllerModelMenu();
|
|
2066
|
+
closeControllerModelMenu({ immediate: true });
|
|
2009
2067
|
}
|
|
2010
2068
|
if (!parentMenu) {
|
|
2011
2069
|
if (
|
|
@@ -2031,7 +2089,7 @@ export function installModelSlider(signal, { feature, ui, createIcon, requestRef
|
|
|
2031
2089
|
}
|
|
2032
2090
|
|
|
2033
2091
|
function restoreModelSelectorEnhancements() {
|
|
2034
|
-
closeControllerModelMenu();
|
|
2092
|
+
closeControllerModelMenu({ immediate: true });
|
|
2035
2093
|
clearTimeout(modelSelectorReturnTimer);
|
|
2036
2094
|
clearTimeout(modelSelectorEffortTransactionTimer);
|
|
2037
2095
|
modelSelectorTriggerObserver?.disconnect();
|
|
@@ -2091,6 +2149,15 @@ export function installModelSlider(signal, { feature, ui, createIcon, requestRef
|
|
|
2091
2149
|
modelSelectorNativeToggleHandlers.clear();
|
|
2092
2150
|
}
|
|
2093
2151
|
|
|
2152
|
+
window.addEventListener("keydown", event => {
|
|
2153
|
+
const record = modelSelectorControllerMenu;
|
|
2154
|
+
if (event.key !== "Escape" || !record) return;
|
|
2155
|
+
// 先退出插件模型视图,避免原生 document 监听同时关闭整个 picker。
|
|
2156
|
+
event.preventDefault();
|
|
2157
|
+
event.stopImmediatePropagation();
|
|
2158
|
+
closeControllerModelMenu();
|
|
2159
|
+
record.button.focus();
|
|
2160
|
+
}, { capture: true, signal });
|
|
2094
2161
|
document.addEventListener("click", event => {
|
|
2095
2162
|
if (modelSelectorControllerMenu
|
|
2096
2163
|
&& !modelSelectorControllerMenu.menu.contains(event.target)
|
package/features/notes/DESIGN.md
CHANGED
|
@@ -38,7 +38,9 @@ Header 为 Prompts;右侧依次为排序图标、分类调节图标、常驻
|
|
|
38
38
|
|
|
39
39
|
点击提示词只插入当前输入框,保留原草稿且不发送。Escape 关闭并回到入口;点击外部关闭;方向键移动列表焦点。原生 Add 或权限面板打开时关闭。切换输入框时关闭,异步结果不得插入另一会话。沿用原生焦点与 hover,无额外进场动画。
|
|
40
40
|
|
|
41
|
-
|
|
41
|
+
入口通过 Codex Adapter 的 `purpose:"insertion"` 挂载到可见的原生输入框,覆盖 Chat、Work、Codex、Temporary Chat、浮动聊天,以及 side panel 全屏非聊天页面的 chat bar。多输入框并存时优先当前焦点所在输入框;插入操作显式绑定打开面板的输入框,切换后关闭旧面板,不向隐藏输入框插入。此插入契约不扩展会话指标使用的 Codex thread 契约。
|
|
42
|
+
|
|
43
|
+
原生可见的 Add 或菜单按钮展开时关闭 Popover。Session Status 的 Context 展开状态不是弹出菜单,不作为冲突条件;隐藏输入框和隐藏控件不参与锚点定位或菜单判断。
|
|
42
44
|
|
|
43
45
|
## 统计
|
|
44
46
|
成功插入后原子增加 `use_count` 并更新 `called_at`,保存于现有 SQLite。创建、编辑、复制和排序不计数。历史次数未知,从 0 开始,不从旧 `last_used_at` 推断。删除分类、切换页面、重启不清零。
|
|
@@ -50,9 +52,9 @@ Header 为 Prompts;右侧依次为排序图标、分类调节图标、常驻
|
|
|
50
52
|
|
|
51
53
|
导航栏 book-open 单独采用 20px 容器、27×27 viewBox,实际轮廓约 16.15px,描边折算 1.33px,与原生导航一致。Lucide 两个描边子路径合为一个 path,一次绘制半透明 currentColor,避免书脊交叠变深。选中态使用 Phosphor 官方 book-open-fill 的20px资源;授权见 vendor/phosphor-LICENSE。输入框入口尺寸不变。
|
|
52
54
|
|
|
53
|
-
拖拽排序使用等高 Drop Placeholder
|
|
55
|
+
拖拽排序使用等高 Drop Placeholder,拖影跟随指针并显式按源行圆角裁切,不留下矩形外边界;同组条目以 180ms ease-out 让位,落位淡入 140ms。放到范围外或取消恢复原位,不写入排序。遵循 prefers-reduced-motion。
|
|
54
56
|
|
|
55
|
-
页面选择通用 Page
|
|
57
|
+
页面选择通用 Page 的 `sidebar:"none"` 分支,自身不带常驻二级侧栏,保留原生 Home、Space、Scheduled sidebar 的悬浮叠加与导航行为。关闭页面恢复原页面位置,不改变用户的侧栏展开偏好。原生二级入口(如 Space → Images)提交导航后,通过共享 Page 的 `onNavigate` 关闭 Prompts、编辑浮层并释放底层 inert;保留用户刚选择的目标路由。仅悬浮展开侧栏不退出 Prompts。
|
|
56
58
|
|
|
57
59
|
提示词编辑沿用原生 Project 的复合标题输入:40px 图标段位于标题前,细分隔线,共用圆角边框与 focus-within 状态。图标选择入口不再单独占一行;Title 通过 aria-label 标识。
|
|
58
60
|
|
|
@@ -68,3 +70,5 @@ Category 下拉面板与触发栏等宽、左右对齐,宽度包含内边距
|
|
|
68
70
|
内置 24 条来自 dsh-prompt 0.1.13 的 MIT 提示词,正文保持原样,按思考、学习、编程、效率分类并配 Emoji。来源与许可保存在 assets/dsh-prompt-LICENSE.txt。首次启动或旧库升级时单次导入;随后允许编辑和删除,重启不还原,也不重建被删除的分类。用户个人内容只保留在本机数据库,不纳入默认资源。
|
|
69
71
|
|
|
70
72
|
简短悬浮提示调用共享 Tooltip;具体文案由本功能维护。Quick Prompts 通过通用 ComposerMenu 使用原生浮动建议列表材质和输入框主体锚点。排序、分类与图标菜单显式选择 Dropdown。
|
|
73
|
+
|
|
74
|
+
Categories 管理弹窗的 New category 是有实际功能的次级操作,使用共享 Button 的 `secondary`(outline)分支;分类编辑的 Save 保留主按钮。
|
|
@@ -7,8 +7,8 @@
|
|
|
7
7
|
"zh-CN": "提示词"
|
|
8
8
|
},
|
|
9
9
|
"description": {
|
|
10
|
-
"en": "Organize prompts
|
|
11
|
-
"zh-CN": "
|
|
10
|
+
"en": "Organize reusable prompts and insert them with one click.",
|
|
11
|
+
"zh-CN": "整理常用提示词,一键填入输入框。"
|
|
12
12
|
},
|
|
13
13
|
"surfaceUrl": "http://127.0.0.1:47832/",
|
|
14
14
|
"healthUrl": "http://127.0.0.1:47832/api/health",
|
|
@@ -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,{
|
|
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", onNavigate:() => closePage(false, false) });
|
|
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()
|
|
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,6 +8,8 @@
|
|
|
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` | 状态行文字与分隔符 |
|
|
@@ -24,7 +26,7 @@
|
|
|
24
26
|
|
|
25
27
|
界面文案跟随 Codex 的 `html[lang]`:中文语言使用简体中文,其他语言使用英文;运行中切换语言时同步更新状态行和已打开的明细浮窗。MineCodex Web 使用同样的中英文规则提供本功能开关。
|
|
26
28
|
|
|
27
|
-
TPS 根据 Codex 会话事件计算有效样本的会话平均约值,以 `N tok/s`
|
|
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 亮/暗主题原值。剩余的原生占用以浅灰表示;若估算合计超过原生占用,按比例缩放彩色段,让进度条终点始终对应原生总占用。不显示无法取得的工具定义,也不放解释性长文案。
|
|
28
30
|
|
|
29
31
|
上下文弹层按视觉归属排列:进度条下紧接三个可见历史分段,不加分段副标题;分隔线之后列原生占用合计、窗口容量和压缩次数。合计取原生最近请求用量,不是三个估算分段的求和。
|
|
30
32
|
|
|
@@ -35,7 +37,7 @@ TPS 标签后放置 12px Lucide info 图标,继承标签颜色,间距 6px。
|
|
|
35
37
|
### 折叠入口
|
|
36
38
|
- 首次安装默认展开;用户收起后在模型选择左侧显示 12px context 环,24px 热区;原生 context 环开启时隐藏原生重复入口,卸载时恢复。
|
|
37
39
|
- Tooltip 沿用原生 context 内容:上下文窗口、使用/剩余比例、已用/容量(k tokens);样式遵循通用 Tooltip。
|
|
38
|
-
- 点击环展开输入框下方详情,右侧 chevron-up
|
|
40
|
+
- 点击环展开输入框下方详情,右侧 chevron-up 收起。原生布局一次切换到目标状态,测量切换前后的位置,仅用 transform 衔接输入框与模型控件;状态行入场配合4px位移与透明度过渡;中途反向按当前位移衔接。展开220ms、收起160ms,曲线 `cubic-bezier(0.23,1,0.32,1)`。反向操作从当前可见位置和透明度继续,取消旧动画;退出内容临时脱离布局,结束后恢复并 display:none,不保留父级 gap。reduced-motion 立即切换,收起内容不参与键盘焦点。展开状态跨会话、主聊天和 side chat 共享并持久保存,展开时隐藏输入框内的 context 环;每个输入框订阅自己的会话上下文和统计。
|
|
39
41
|
- Cache 按原始 cachedInputTokens / inputTokens 计算:大于99%显示一位小数,其余取整数,状态栏和详情共用格式。
|
|
40
42
|
|
|
41
43
|
输入框实例仅绑定可见的 thread composer。全屏标签页中的隐藏聊天、浮动 Quick Chat(含 right-panel composer overlay)、inert 区域不注入;原生移动输入框或切换可见性时销毁实例并取消订阅,返回聊天后重建。
|
|
@@ -4,8 +4,8 @@
|
|
|
4
4
|
"version": "0.1.0",
|
|
5
5
|
"label": { "en": "Session Status", "zh-CN": "会话状态" },
|
|
6
6
|
"description": {
|
|
7
|
-
"en": "
|
|
8
|
-
"zh-CN": "
|
|
7
|
+
"en": "See response speed, token usage and context space.",
|
|
8
|
+
"zh-CN": "查看输出速度、Token 用量与上下文占用。"
|
|
9
9
|
},
|
|
10
10
|
"entry": { "kind": "page-script", "script": "src/session-status.mjs", "sourceFactory": "src/injection-source.mjs" },
|
|
11
11
|
"icon": {
|
|
@@ -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
|
|
34
|
-
|
|
35
|
-
|
|
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
|
|
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":
|
|
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,
|
|
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 -
|
|
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,9 +35,7 @@ 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:
|
|
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; }
|
|
@@ -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.
|
|
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
|
-
|
|
174
|
+
clearMotion();
|
|
175
|
+
collapsing = animate && !value;
|
|
163
176
|
expanded = value;
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
}
|
|
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.
|
|
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
|
|
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
|
}
|
|
@@ -5,13 +5,13 @@
|
|
|
5
5
|
保留当前任务所在分类与 Project,收起其他分支。再次点击恢复进入聚焦前的展开状态;聚焦期间切换任务跟随路径。用户手动调整分支时退出聚焦并保留手动布局,无撤销浮层。
|
|
6
6
|
|
|
7
7
|
- 入口:展开侧栏的 Activity 与 Search 之间,随原生侧栏一起隐藏。
|
|
8
|
-
-
|
|
9
|
-
- 控件:继承原生 Activity 的 Button 类与 28px
|
|
10
|
-
- 中英文标题:Focus current task / 聚焦当前任务;
|
|
8
|
+
- 图标:整理操作使用 Lucide list-chevrons-down-up;可恢复布局时切换为 list-chevrons-up-down。16px,24px viewBox,描边 1.575 units(实际 1.05px)。
|
|
9
|
+
- 控件:继承原生 Activity 的 Button 类与 28px 热区。两种状态均使用 secondary / ghost,以图标表达下一步动作,不显示蓝色模式选中态;aria-pressed 保留可恢复状态的可访问语义。
|
|
10
|
+
- 中英文标题:Focus current task / 聚焦当前任务;Restore layout / 恢复布局。
|
|
11
11
|
- 折叠通过原生回调执行;React 元数据仅用于读取稳定分类、项目和会话身份。
|
|
12
12
|
- 无可识别当前路径时禁用;Activity 列表展示期间暂停整理。
|
|
13
13
|
- 状态仅作用于当前窗口;卸载不额外改变用户布局。
|
|
14
14
|
|
|
15
|
-
|
|
15
|
+
显式使用中性 data-color 与 data-variant,直接复用原生语义 token 适配明暗模式,不写死颜色或背景色。Activity 开启时按钮原生 disabled,暂停聚焦操作,关闭后恢复可用性。
|
|
16
16
|
|
|
17
17
|
Tooltip 由共享组件库通过 `data-minecodex-tooltip` 管理;插件只提供随聚焦状态变化的文案,不维护独立材质、定位或显示事件。克隆入口时移除原控件的 `title` 与 `aria-describedby`。
|
|
@@ -7,14 +7,15 @@
|
|
|
7
7
|
"zh-CN": "侧栏聚焦"
|
|
8
8
|
},
|
|
9
9
|
"description": {
|
|
10
|
-
"en": "
|
|
11
|
-
"zh-CN": "
|
|
10
|
+
"en": "Fold unrelated branches to keep the current task in view.",
|
|
11
|
+
"zh-CN": "收起无关分支,专注当前任务路径。"
|
|
12
12
|
},
|
|
13
13
|
"entry": {
|
|
14
14
|
"kind": "page-script",
|
|
15
15
|
"script": "src/sidebar-focus.mjs",
|
|
16
16
|
"sourceFactory": "src/injection-source.mjs"
|
|
17
17
|
},
|
|
18
|
+
"icon": {"viewBox": "0 0 24 24", "markup": "<rect width=\"18\" height=\"18\" x=\"3\" y=\"3\" rx=\"2\" />\n <path d=\"M9 3v18\" />"},
|
|
18
19
|
"placement": {
|
|
19
20
|
"order": 80
|
|
20
21
|
}
|