minecodex 2.0.26 → 2.0.28
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/README.md +2 -0
- package/features/model-slider/src/frontend/install.mjs +5 -0
- package/features/notes/src/frontend/entry.mjs +3 -0
- package/features/quick-chat/assets/history.svg +17 -0
- package/features/quick-chat/codex-feature.json +10 -0
- package/features/quick-chat/src/adapters/codex/renderer.mjs +63 -0
- package/features/quick-chat/src/domain/history.mjs +12 -0
- package/features/quick-chat/src/domain/submission.mjs +15 -0
- package/features/quick-chat/src/frontend/entry.mjs +97 -0
- package/features/quick-chat/src/injection-source.mjs +8 -0
- package/features/quick-chat/vendor/lucide-LICENSE +15 -0
- package/features/quick-chat-repair/assets/history.svg +17 -0
- package/features/quick-chat-repair/codex-feature.json +10 -0
- package/features/quick-chat-repair/src/adapters/codex/presence.mjs +74 -0
- package/features/quick-chat-repair/src/frontend/entry.mjs +12 -0
- package/features/quick-chat-repair/src/injection-source.mjs +5 -0
- package/features/quick-chat-repair/vendor/lucide-LICENSE +15 -0
- package/features/session-status/src/adapters/codex/session-log.mjs +1 -1
- package/features/session-status/src/adapters/opencodex/usage-log.mjs +4 -14
- package/features/session-status/src/domain/session-stats.mjs +40 -37
- package/features/session-status/src/session-status.mjs +12 -10
- package/package.json +11 -3
- package/packages/cli/src/config.mjs +3 -1
- package/packages/cli/src/control-server.mjs +1 -1
- package/packages/ui/src/foundation/surfaces.mjs +3 -2
- package/packages/ui/src/native/tokens.mjs +1 -0
package/README.md
CHANGED
|
@@ -18,6 +18,8 @@ MineCodex 给 Codex Desktop 增加一组轻量、本地优先的扩展功能。
|
|
|
18
18
|
| **文件级联浏览** · Cascading File Browser | 点击文件顶部路径,分列浏览文件夹并快速切换文件。 |
|
|
19
19
|
| **图片集合** · Image Collection | 集中浏览生成的图片,查看提示词并复用创作思路。 |
|
|
20
20
|
| **站点收藏** · Saved Sites | 发现项目正在运行的网页,收藏常用网址,随时打开。 |
|
|
21
|
+
| **Quick Chat 退出修复** | 修复关闭快捷聊天后透明层残留、主页面无法点击的问题;可独立开关。 |
|
|
22
|
+
| **快捷聊天增强** · Quick Chat | 发送后自动展开聊天,未发送时也能一键查看原生聊天历史。 |
|
|
21
23
|
| **侧栏聚焦** · Sidebar Focus | 一键收起侧栏中的其他分组,再次点击恢复原来的展开状态。 |
|
|
22
24
|
| **隐藏额度推广** · Hide Usage Promotions | 隐藏输入框上方的套餐升级推广,让对话更清爽。 |
|
|
23
25
|
|
|
@@ -35,6 +35,11 @@ export function installModelSlider(signal, { feature, ui, createIcon, requestRef
|
|
|
35
35
|
position: sticky; top: 0; z-index: 1; flex-shrink: 0; padding: 2px 8px 8px;
|
|
36
36
|
background: var(--mcx-color-surface-elevated-secondary);
|
|
37
37
|
}
|
|
38
|
+
/* sticky 保留菜单的 6px 内边距,补齐顶部遮挡而不改变布局高度。 */
|
|
39
|
+
[data-codex-model-slider-search-header]::before {
|
|
40
|
+
content: ""; position: absolute; top: -6px; left: -6px; right: -6px; height: 6px;
|
|
41
|
+
background: inherit; pointer-events: none;
|
|
42
|
+
}
|
|
38
43
|
[data-codex-model-slider-search] {
|
|
39
44
|
width: 100%; height: 28px; border: 0; outline: none; padding: 0;
|
|
40
45
|
background: transparent; color: var(--mcx-color-text-primary);
|
|
@@ -377,6 +377,9 @@ function sync() {
|
|
|
377
377
|
close();
|
|
378
378
|
if (!button) {
|
|
379
379
|
button = add.cloneNode(false);
|
|
380
|
+
// Add 的附件加载状态不属于只填入文字的提示词入口。
|
|
381
|
+
button.removeAttribute("disabled");
|
|
382
|
+
button.removeAttribute("aria-disabled");
|
|
380
383
|
button.removeAttribute("title");
|
|
381
384
|
button.removeAttribute("aria-describedby");
|
|
382
385
|
button.removeAttribute("id");
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
<!-- @license lucide-static v0.468.0 - ISC -->
|
|
2
|
+
<svg
|
|
3
|
+
class="lucide lucide-history"
|
|
4
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
5
|
+
width="24"
|
|
6
|
+
height="24"
|
|
7
|
+
viewBox="0 0 24 24"
|
|
8
|
+
fill="none"
|
|
9
|
+
stroke="currentColor"
|
|
10
|
+
stroke-width="2"
|
|
11
|
+
stroke-linecap="round"
|
|
12
|
+
stroke-linejoin="round"
|
|
13
|
+
>
|
|
14
|
+
<path d="M3 12a9 9 0 1 0 9-9 9.75 9.75 0 0 0-6.74 2.74L3 8" />
|
|
15
|
+
<path d="M3 3v5h5" />
|
|
16
|
+
<path d="M12 7v5l4 2" />
|
|
17
|
+
</svg>
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
{
|
|
2
|
+
"schemaVersion": 1,
|
|
3
|
+
"id": "quick-chat",
|
|
4
|
+
"version": "0.1.0",
|
|
5
|
+
"label": { "en": "Quick Chat", "zh-CN": "快捷聊天增强" },
|
|
6
|
+
"description": { "en": "Expand Quick Chat after sending and open native chat history before starting a conversation.", "zh-CN": "发送后自动展开 Quick Chat,尚未发送时也能打开原生聊天历史。" },
|
|
7
|
+
"entry": { "kind": "page-script", "script": "src/frontend/entry.mjs", "sourceFactory": "src/injection-source.mjs", "resources": { "history": "assets/history.svg" } },
|
|
8
|
+
"icon": { "resource": "assets/history.svg" },
|
|
9
|
+
"placement": { "order": 75 }
|
|
10
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
export function createQuickChatAdapter(codex) {
|
|
2
|
+
const receipts = new WeakMap();
|
|
3
|
+
let nextReceipt = 1;
|
|
4
|
+
const surfaceSelector = '[data-floating-chat-surface]';
|
|
5
|
+
function historyList(node) {
|
|
6
|
+
if (Array.isArray(node)) return node.some(historyList);
|
|
7
|
+
const props = node?.props;
|
|
8
|
+
if (!props || props.hidden || props.inert || props['aria-hidden']) return false;
|
|
9
|
+
if (Array.isArray(props.conversations) && typeof props.onConversationSelect === 'function' && typeof props.onNewChat === 'function') return true;
|
|
10
|
+
return historyList(props.children);
|
|
11
|
+
}
|
|
12
|
+
function read(surface) {
|
|
13
|
+
if (!surface?.isConnected || surface.closest('[hidden], [inert], [aria-hidden="true"]')) return null;
|
|
14
|
+
const editable = surface.querySelector('[data-chatgpt-composer] [contenteditable="true"]');
|
|
15
|
+
if (!editable) return null;
|
|
16
|
+
const composer = codex.props(editable, props => typeof props.isWorkConversation === "boolean" && props.conversationId);
|
|
17
|
+
const session = codex.props(surface, props => props.session?.conversationId)?.session;
|
|
18
|
+
const floating = codex.props(surface, props => typeof props.onPresentationChange === "function" && typeof props.renderMenu === "function");
|
|
19
|
+
// Work 的页面横条没有经典 Chat 的历史动作,不能接入此功能。
|
|
20
|
+
if (!composer || composer.isWorkConversation || composer.annotationSubmitTarget || composer.isWidgetOverlay || !session || !floating) return null;
|
|
21
|
+
const history = floating.renderMenu({})?.props?.items?.find(item => item.id === "history" && typeof item.onSelect === "function");
|
|
22
|
+
if (!history) return null;
|
|
23
|
+
const state = codex.props(editable, props => typeof props.submitDisabled === "boolean" && props.conversationId);
|
|
24
|
+
const controller = codex.props(editable, props => props.composerController?.eventEmitter)?.composerController;
|
|
25
|
+
const minimize = [...surface.querySelectorAll('button')].find(button => {
|
|
26
|
+
const props = codex.props(button, props => props.standalone === true && typeof props.onMinimize === "function" && typeof props.isCompact === "boolean", 5);
|
|
27
|
+
return Boolean(props && button.checkVisibility({visibilityProperty:true}) && !button.hasAttribute('data-mcx-quick-chat-history'));
|
|
28
|
+
});
|
|
29
|
+
let receipt = null;
|
|
30
|
+
for (let fiber = codex.currentFiber(surface), depth = 0; fiber && depth < 80; depth++, fiber = fiber.return) {
|
|
31
|
+
if (fiber.memoizedProps?.session?.conversationId !== session.conversationId) continue;
|
|
32
|
+
for (let hook = fiber.memoizedState, count = 0; hook && count < 140; hook = hook.next, count++) {
|
|
33
|
+
const value = hook.memoizedState;
|
|
34
|
+
if (value?.conversationId === session.conversationId && "owner" in value && typeof value.presentation === "string") receipt = value;
|
|
35
|
+
}
|
|
36
|
+
if (receipt) break;
|
|
37
|
+
}
|
|
38
|
+
// 发送确认回调写入新记录;拒绝发送不会写入。记录身份只映射为窗口内序号。
|
|
39
|
+
if (receipt && !receipts.has(receipt)) receipts.set(receipt, nextReceipt++);
|
|
40
|
+
const frame = surface.closest('[role="dialog"]');
|
|
41
|
+
const background = frame && [...frame.children].find(node => codex.props(node, props => props.asChild === true && typeof props.unstyled === "boolean", 8));
|
|
42
|
+
const project = [...surface.querySelectorAll('[data-composer-rail-item] button')].find(button =>
|
|
43
|
+
codex.props(button, props => props.variant === "composer" && typeof props.onProjectChange === "function"));
|
|
44
|
+
const projectRail = project?.closest('[data-composer-rail-item]') ?? null;
|
|
45
|
+
const expanded = Boolean(floating.presentation === "expanded" || floating.forceExpanded);
|
|
46
|
+
return {surface, editable, minimize, projectRail, background:!session.hasConversation && floating.forceExpanded ? background : null, sessionId:session.conversationId, hasConversation:session.hasConversation,
|
|
47
|
+
canSubmit:Boolean(state && !state.submitDisabled && !state.submitLoading), receiptId:receipt ? receipts.get(receipt) : 0, receiptPresentation:receipt?.presentation ?? null, emitter:controller?.eventEmitter,
|
|
48
|
+
expanded, showHistory:Boolean(minimize && !expanded), historyVisible:historyList(floating.renderTranscript?.()),
|
|
49
|
+
expand:() => floating.onPresentationChange("expanded"),
|
|
50
|
+
// 历史动作通过 forceExpanded 临时展开;不要覆盖最小化后的恢复形态。
|
|
51
|
+
history:() => history.onSelect()};
|
|
52
|
+
}
|
|
53
|
+
function surfaces() { return [...document.querySelectorAll(surfaceSelector)].map(read).filter(Boolean); }
|
|
54
|
+
function subscribe(snapshot, listener) {
|
|
55
|
+
const emitter = snapshot.emitter;
|
|
56
|
+
if (!emitter?.addListener || !emitter?.removeListener) return () => {};
|
|
57
|
+
// 不改变原生 emit 的 handled 结果,也不接管发送。
|
|
58
|
+
const submit = () => { listener(); return false; };
|
|
59
|
+
emitter.addListener("submit", submit);
|
|
60
|
+
return () => emitter.removeListener("submit", submit);
|
|
61
|
+
}
|
|
62
|
+
return {surfaces, read, subscribe};
|
|
63
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export function createHistoryTracker() {
|
|
2
|
+
let origin = null;
|
|
3
|
+
return {
|
|
4
|
+
open(snapshot) { if (snapshot.historyVisible) origin = snapshot.sessionId; },
|
|
5
|
+
selected(snapshot) {
|
|
6
|
+
if (origin === null || origin === snapshot.sessionId) return false;
|
|
7
|
+
origin = null;
|
|
8
|
+
return snapshot.hasConversation;
|
|
9
|
+
},
|
|
10
|
+
clear() { origin = null; },
|
|
11
|
+
};
|
|
12
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
// 发送确认会写入同会话的新呈现记录;拒绝发送不会更新该记录。
|
|
2
|
+
export function createSubmissionTracker() {
|
|
3
|
+
let pending = null;
|
|
4
|
+
return {
|
|
5
|
+
begin(snapshot) { pending = snapshot.canSubmit ? {sessionId:snapshot.sessionId, receiptId:snapshot.receiptId} : null; },
|
|
6
|
+
accepted(snapshot) {
|
|
7
|
+
if (!pending) return false;
|
|
8
|
+
if (pending.sessionId !== snapshot.sessionId) { pending = null; return false; }
|
|
9
|
+
if (!snapshot.hasConversation || !snapshot.receiptId || snapshot.receiptId === pending.receiptId) return false;
|
|
10
|
+
pending = null;
|
|
11
|
+
return snapshot.receiptPresentation !== "minimized";
|
|
12
|
+
},
|
|
13
|
+
clear() { pending = null; },
|
|
14
|
+
};
|
|
15
|
+
}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
const native = createQuickChatAdapter(config.codex);
|
|
2
|
+
const instances = new Map();
|
|
3
|
+
let scheduled = false, disposed = false;
|
|
4
|
+
const text = (en,zh) => (document.documentElement.lang || navigator.language).startsWith('zh') ? zh : en;
|
|
5
|
+
const style = document.createElement('style');
|
|
6
|
+
style.textContent = `
|
|
7
|
+
[data-mcx-quick-chat-history]{margin-inline-end:8px;flex-shrink:0}
|
|
8
|
+
[data-mcx-quick-chat-history]>svg{width:16px;height:16px}
|
|
9
|
+
[data-mcx-quick-chat-minimize]{border-radius:calc(infinity * 1px)}
|
|
10
|
+
[data-mcx-quick-chat-project-rail]{display:none!important}
|
|
11
|
+
/* 隐藏唯一的项目栏时,一并收起原生 rail 的圆角留白与负 margin。 */
|
|
12
|
+
[data-composer-rail]:has(>[data-mcx-quick-chat-project-rail]):not(:has(>:not([data-mcx-quick-chat-project-rail]):not(:empty))){display:none!important}
|
|
13
|
+
[data-floating-chat-surface]>:has(>[data-mcx-quick-chat-history]){display:flex;align-items:center}
|
|
14
|
+
`;
|
|
15
|
+
document.head.append(style);
|
|
16
|
+
function releaseBackground(instance) {
|
|
17
|
+
if (!instance.background) return;
|
|
18
|
+
instance.background.removeAttribute('data-mcx-ui-surface'); instance.background.removeAttribute('data-mcx-ui-variant');
|
|
19
|
+
instance.background = null;
|
|
20
|
+
}
|
|
21
|
+
function releaseProjectRail(instance) { instance.projectRail?.removeAttribute('data-mcx-quick-chat-project-rail'); instance.projectRail = null; }
|
|
22
|
+
function remove(instance) { releaseBackground(instance); releaseProjectRail(instance); instance.minimize?.removeAttribute('data-mcx-quick-chat-minimize'); instance.unsubscribe(); instance.button?.remove(); instance.tracker.clear(); instance.historyTracker.clear(); }
|
|
23
|
+
function sync() {
|
|
24
|
+
if (disposed) return;
|
|
25
|
+
const snapshots = native.surfaces(), active = new Set();
|
|
26
|
+
for (const snapshot of snapshots) {
|
|
27
|
+
active.add(snapshot.surface);
|
|
28
|
+
let instance = instances.get(snapshot.surface);
|
|
29
|
+
const selected = instance?.historyTracker.selected(snapshot);
|
|
30
|
+
if (!instance || instance.sessionId !== snapshot.sessionId || instance.emitter !== snapshot.emitter) {
|
|
31
|
+
if (instance) remove(instance);
|
|
32
|
+
instance = {sessionId:snapshot.sessionId, emitter:snapshot.emitter, tracker:createSubmissionTracker(), historyTracker:createHistoryTracker(), button:null};
|
|
33
|
+
const begin = () => { const current = native.read(snapshot.surface); if (current) instance.tracker.begin(current); };
|
|
34
|
+
instance.unsubscribe = native.subscribe(snapshot, begin);
|
|
35
|
+
instances.set(snapshot.surface, instance);
|
|
36
|
+
}
|
|
37
|
+
if (selected || instance.tracker.accepted(snapshot)) snapshot.expand();
|
|
38
|
+
instance.historyTracker.open(snapshot);
|
|
39
|
+
if (instance.projectRail !== snapshot.projectRail) {
|
|
40
|
+
releaseProjectRail(instance);
|
|
41
|
+
instance.projectRail = snapshot.projectRail;
|
|
42
|
+
instance.projectRail?.setAttribute('data-mcx-quick-chat-project-rail','');
|
|
43
|
+
}
|
|
44
|
+
if (instance.background !== snapshot.background) {
|
|
45
|
+
releaseBackground(instance);
|
|
46
|
+
if (snapshot.background) {
|
|
47
|
+
instance.background = snapshot.background;
|
|
48
|
+
config.ui.surface(snapshot.background,'floating-panel');
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
const minimize = snapshot.minimize;
|
|
52
|
+
if (instance.minimize !== minimize) {
|
|
53
|
+
instance.minimize?.removeAttribute('data-mcx-quick-chat-minimize'); instance.minimize = minimize;
|
|
54
|
+
minimize?.setAttribute('data-mcx-quick-chat-minimize','');
|
|
55
|
+
}
|
|
56
|
+
if (!minimize || !snapshot.showHistory) { instance.button?.remove(); instance.button = null; continue; }
|
|
57
|
+
if (!instance.button?.isConnected || instance.button.parentElement !== minimize.parentElement) {
|
|
58
|
+
instance.button?.remove();
|
|
59
|
+
const button = minimize.cloneNode(false);
|
|
60
|
+
for (const attr of ['id','disabled','aria-disabled','aria-describedby','aria-expanded','title']) button.removeAttribute(attr);
|
|
61
|
+
button.removeAttribute('data-mcx-quick-chat-minimize');
|
|
62
|
+
button.type = 'button';
|
|
63
|
+
button.setAttribute('data-mcx-quick-chat-history', '');
|
|
64
|
+
button.innerHTML = config.resources.history;
|
|
65
|
+
const icon = button.querySelector('svg');
|
|
66
|
+
if (icon) { icon.setAttribute('width','16'); icon.setAttribute('height','16'); icon.setAttribute('aria-hidden','true'); }
|
|
67
|
+
button.addEventListener('pointerdown', event => event.preventDefault(), {signal});
|
|
68
|
+
button.addEventListener('click', () => { instance.tracker.clear(); native.read(snapshot.surface)?.history(); }, {signal});
|
|
69
|
+
minimize.before(button); instance.button = button;
|
|
70
|
+
}
|
|
71
|
+
if (instance.button.className !== minimize.className) instance.button.className = minimize.className;
|
|
72
|
+
const label = text('Chat history', '聊天历史');
|
|
73
|
+
if (instance.button.getAttribute('aria-label') !== label) {
|
|
74
|
+
instance.button.setAttribute('aria-label',label); instance.button.setAttribute('data-minecodex-tooltip',label);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
for (const [surface, instance] of instances) if (!active.has(surface)) { remove(instance); instances.delete(surface); }
|
|
78
|
+
}
|
|
79
|
+
function schedule() { if (disposed || scheduled) return; scheduled = true; queueMicrotask(() => { scheduled = false; sync(); }); }
|
|
80
|
+
function begin(event) {
|
|
81
|
+
const snapshot = native.surfaces().find(item => item.surface.contains(event.target));
|
|
82
|
+
if (!snapshot) return;
|
|
83
|
+
if (event.type === 'click' && !event.target.closest('button[type="submit"]')) { instances.get(snapshot.surface)?.tracker.clear(); return; }
|
|
84
|
+
instances.get(snapshot.surface)?.tracker.begin(snapshot);
|
|
85
|
+
}
|
|
86
|
+
document.addEventListener('click',begin,{capture:true,signal});
|
|
87
|
+
document.addEventListener('submit',begin,{capture:true,signal});
|
|
88
|
+
const observer = new MutationObserver(schedule);
|
|
89
|
+
observer.observe(document.documentElement,{subtree:true,childList:true,attributes:true,attributeFilter:['aria-hidden','inert','hidden','lang']});
|
|
90
|
+
sync();
|
|
91
|
+
config.registerSelfCheck?.(() => {
|
|
92
|
+
const snapshots = native.surfaces(), anchors = snapshots.filter(item => item.showHistory), available = anchors.some(item => instances.get(item.surface)?.button?.isConnected);
|
|
93
|
+
return {state:anchors.length ? available ? 'available' : 'unavailable' : 'not-applicable',
|
|
94
|
+
reason:anchors.length ? available ? null : 'quick-chat-history-anchor-unavailable' : 'no-classic-quick-chat-floating-controls',
|
|
95
|
+
checks:[{name:'quick-chat-history',available}],scopes:snapshots.map(item=>({name:'quick-chat',element:item.surface}))};
|
|
96
|
+
});
|
|
97
|
+
return () => { disposed = true; observer.disconnect(); for (const instance of instances.values()) remove(instance); instances.clear(); style.remove(); };
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { readFile } from "node:fs/promises";
|
|
2
|
+
import { createQuickChatAdapter } from "./adapters/codex/renderer.mjs";
|
|
3
|
+
import { createSubmissionTracker } from "./domain/submission.mjs";
|
|
4
|
+
import { createHistoryTracker } from "./domain/history.mjs";
|
|
5
|
+
export async function createPageScriptSource() {
|
|
6
|
+
const source = await readFile(new URL("./frontend/entry.mjs", import.meta.url), "utf8");
|
|
7
|
+
return `const createQuickChatAdapter=${createQuickChatAdapter.toString()};\nconst createSubmissionTracker=${createSubmissionTracker.toString()};\nconst createHistoryTracker=${createHistoryTracker.toString()};\n${source}`;
|
|
8
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
ISC License
|
|
2
|
+
|
|
3
|
+
Copyright (c) for portions of Lucide are held by Cole Bemis 2013-2022 as part of Feather (MIT). All other copyright (c) for Lucide are held by Lucide Contributors 2022.
|
|
4
|
+
|
|
5
|
+
Permission to use, copy, modify, and/or distribute this software for any
|
|
6
|
+
purpose with or without fee is hereby granted, provided that the above
|
|
7
|
+
copyright notice and this permission notice appear in all copies.
|
|
8
|
+
|
|
9
|
+
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
|
10
|
+
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
|
11
|
+
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
|
12
|
+
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
|
13
|
+
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
|
14
|
+
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
|
15
|
+
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
<!-- @license lucide-static v0.468.0 - ISC -->
|
|
2
|
+
<svg
|
|
3
|
+
class="lucide lucide-history"
|
|
4
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
5
|
+
width="24"
|
|
6
|
+
height="24"
|
|
7
|
+
viewBox="0 0 24 24"
|
|
8
|
+
fill="none"
|
|
9
|
+
stroke="currentColor"
|
|
10
|
+
stroke-width="2"
|
|
11
|
+
stroke-linecap="round"
|
|
12
|
+
stroke-linejoin="round"
|
|
13
|
+
>
|
|
14
|
+
<path d="M3 12a9 9 0 1 0 9-9 9.75 9.75 0 0 0-6.74 2.74L3 8" />
|
|
15
|
+
<path d="M3 3v5h5" />
|
|
16
|
+
<path d="M12 7v5l4 2" />
|
|
17
|
+
</svg>
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
{
|
|
2
|
+
"schemaVersion": 1,
|
|
3
|
+
"id": "quick-chat-repair",
|
|
4
|
+
"version": "0.1.0",
|
|
5
|
+
"label": {"en":"Quick Chat Exit Repair", "zh-CN":"Quick Chat 退出修复"},
|
|
6
|
+
"description": {"en":"Fix the native transparent layer that blocks clicks after closing Quick Chat.", "zh-CN":"修复关闭 Quick Chat 后透明层残留、主页面无法点击的问题。"},
|
|
7
|
+
"entry": {"kind":"page-script", "script":"src/frontend/entry.mjs", "sourceFactory":"src/injection-source.mjs"},
|
|
8
|
+
"icon": {"resource":"assets/history.svg"},
|
|
9
|
+
"placement": {"order":76}
|
|
10
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
export function createQuickChatExitAdapter(codex) {
|
|
2
|
+
const completed = new WeakSet();
|
|
3
|
+
const selector = '.fixed.inset-0.zoom-adjusted-viewport.pointer-events-auto';
|
|
4
|
+
function currentLayer(layer) {
|
|
5
|
+
const attached = codex.currentFiber(layer);
|
|
6
|
+
let host = attached;
|
|
7
|
+
while (host?.return) host = host.return;
|
|
8
|
+
const current = host?.stateNode?.current;
|
|
9
|
+
if (!current) return attached;
|
|
10
|
+
// Host props 可原样复用,DOM 上的 Fiber 因此可能仍指向上一棵树。
|
|
11
|
+
const pending = [current];
|
|
12
|
+
let visited = 0;
|
|
13
|
+
while (pending.length && visited++ < 10000) {
|
|
14
|
+
const fiber = pending.pop();
|
|
15
|
+
if (fiber.stateNode === layer) return fiber;
|
|
16
|
+
for (let child = fiber.child; child; child = child.sibling) pending.push(child);
|
|
17
|
+
}
|
|
18
|
+
return null;
|
|
19
|
+
}
|
|
20
|
+
function presence(fiber) {
|
|
21
|
+
for (let dependency = fiber.dependencies?.firstContext; dependency; dependency = dependency.next) {
|
|
22
|
+
const context = dependency.memoizedValue;
|
|
23
|
+
if (context?.isPresent === false && typeof context.register === 'function' && typeof context.onExitComplete === 'function') return context;
|
|
24
|
+
}
|
|
25
|
+
return null;
|
|
26
|
+
}
|
|
27
|
+
function completion(fiber) {
|
|
28
|
+
const props = fiber.memoizedProps;
|
|
29
|
+
if (props?.propagate !== true || fiber.child || typeof fiber.type !== 'function') return null;
|
|
30
|
+
const source = Function.prototype.toString.call(fiber.type);
|
|
31
|
+
if (!source.includes('presenceAffectsLayout:') || !source.includes('propagate:') || !source.includes('.useState)')) return null;
|
|
32
|
+
const context = presence(fiber);
|
|
33
|
+
if (!context) return null;
|
|
34
|
+
for (let hook = fiber.memoizedState, count = 0; hook && count < 40; hook = hook.next, count++) {
|
|
35
|
+
const value = hook.memoizedState;
|
|
36
|
+
if (!Array.isArray(value) || typeof value[0] !== 'function') continue;
|
|
37
|
+
const deps = value[1];
|
|
38
|
+
if (!Array.isArray(deps) || deps.length !== 3 || typeof deps[0] !== 'string' || deps[1] !== context.onExitComplete || deps[2] !== true) continue;
|
|
39
|
+
// 仅补齐仍未完成的 usePresence 登记,不绕过父层或其他子动画。
|
|
40
|
+
for (let parent = fiber.return, depth = 0; parent && depth < 40; parent = parent.return, depth++) {
|
|
41
|
+
if (parent.memoizedProps?.value !== context) continue;
|
|
42
|
+
for (let owner = parent.return, level = 0; owner && level < 5; owner = owner.return, level++) {
|
|
43
|
+
for (let state = owner.memoizedState, n = 0; state && n < 20; state = state.next, n++) {
|
|
44
|
+
const map = state.memoizedState?.current;
|
|
45
|
+
if (map instanceof Map && map.get(deps[0]) === false) return value[0];
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
return null;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
return null;
|
|
52
|
+
}
|
|
53
|
+
function repair(layer) {
|
|
54
|
+
if (!layer.isConnected) return 0;
|
|
55
|
+
let root = currentLayer(layer);
|
|
56
|
+
for (let depth = 0; root && depth < 40; root = root.return, depth++) {
|
|
57
|
+
if (typeof root.memoizedProps?.isPresent === 'boolean' && typeof root.memoizedProps?.onExitComplete === 'function') break;
|
|
58
|
+
}
|
|
59
|
+
if (!root || root.memoizedProps.isPresent !== false) return 0;
|
|
60
|
+
const pending = [root];
|
|
61
|
+
const completions = [];
|
|
62
|
+
let quickChat = false, count = 0, visited = 0;
|
|
63
|
+
while (pending.length && visited++ < 1000) {
|
|
64
|
+
const fiber = pending.pop(), finish = completion(fiber);
|
|
65
|
+
if (fiber.memoizedProps?.name === 'QuickChatWindow') quickChat = true;
|
|
66
|
+
if (finish && !completed.has(finish)) completions.push(finish);
|
|
67
|
+
for (let child = fiber.child; child; child = child.sibling) pending.push(child);
|
|
68
|
+
}
|
|
69
|
+
if (!quickChat || pending.length) return 0;
|
|
70
|
+
for (const finish of completions) { completed.add(finish); finish(); count++; }
|
|
71
|
+
return count;
|
|
72
|
+
}
|
|
73
|
+
return { selector, repair, reconcile:() => [...document.querySelectorAll(selector)].reduce((count, layer) => count + repair(layer), 0) };
|
|
74
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
const native = createQuickChatExitAdapter(config.codex);
|
|
2
|
+
let scheduled = false, disposed = false;
|
|
3
|
+
function schedule() {
|
|
4
|
+
if (scheduled || disposed) return;
|
|
5
|
+
scheduled = true;
|
|
6
|
+
queueMicrotask(() => { scheduled = false; if (!disposed) native.reconcile(); });
|
|
7
|
+
}
|
|
8
|
+
const observer = new MutationObserver(schedule);
|
|
9
|
+
observer.observe(document.documentElement, {subtree:true, childList:true, attributes:true, attributeFilter:['style']});
|
|
10
|
+
schedule();
|
|
11
|
+
config.registerSelfCheck?.(() => ({state:'available', checks:[{name:'native-exit-observer', available:!disposed}], scopes:[]}));
|
|
12
|
+
return () => { disposed = true; observer.disconnect(); };
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import {readFile} from 'node:fs/promises';
|
|
2
|
+
import {createQuickChatExitAdapter} from './adapters/codex/presence.mjs';
|
|
3
|
+
export async function createPageScriptSource() {
|
|
4
|
+
return `const createQuickChatExitAdapter=${createQuickChatExitAdapter.toString()};\n${await readFile(new URL('./frontend/entry.mjs', import.meta.url), 'utf8')}`;
|
|
5
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
ISC License
|
|
2
|
+
|
|
3
|
+
Copyright (c) for portions of Lucide are held by Cole Bemis 2013-2022 as part of Feather (MIT). All other copyright (c) for Lucide are held by Lucide Contributors 2022.
|
|
4
|
+
|
|
5
|
+
Permission to use, copy, modify, and/or distribute this software for any
|
|
6
|
+
purpose with or without fee is hereby granted, provided that the above
|
|
7
|
+
copyright notice and this permission notice appear in all copies.
|
|
8
|
+
|
|
9
|
+
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
|
10
|
+
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
|
11
|
+
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
|
12
|
+
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
|
13
|
+
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
|
14
|
+
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
|
15
|
+
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
|
@@ -68,7 +68,7 @@ export function sessionFacts(record) {
|
|
|
68
68
|
if (record?.type === "event_msg") {
|
|
69
69
|
if (p.type === "context_compacted") emit("context-reset");
|
|
70
70
|
if (p.type === "task_started" && p.turn_id) emit("turn-started", { id:p.turn_id });
|
|
71
|
-
if (["task_complete", "task_interrupted", "turn_aborted"].includes(p.type)) emit("turn-ended", { id:p.turn_id });
|
|
71
|
+
if (["task_complete", "task_interrupted", "turn_aborted"].includes(p.type)) emit("turn-ended", { id:p.turn_id, completed:p.type === "task_complete" });
|
|
72
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 });
|
|
73
73
|
if (p.type === "item_completed") {
|
|
74
74
|
if (p.item?.type === "ContextCompaction") emit("compaction-finished");
|
|
@@ -10,26 +10,16 @@ function usageRecord(row) {
|
|
|
10
10
|
|| !Number.isFinite(row.timestamp) || !Number.isFinite(row.durationMs) || row.durationMs < 0) return null;
|
|
11
11
|
return { id:row.requestId, conversationId:row.conversationId, start:row.timestamp,
|
|
12
12
|
end:row.timestamp+row.durationMs, duration:row.durationMs,
|
|
13
|
-
first:row.firstOutputMs, generationStart:row.genStartMs, generationEnd:row.lastOutputMs,
|
|
14
13
|
usageStatus:row.usageStatus, input:row.usage?.inputTokens, cached:row.usage?.cachedInputTokens ?? 0,
|
|
15
14
|
output:row.usage?.outputTokens };
|
|
16
15
|
}
|
|
17
16
|
|
|
18
|
-
function
|
|
17
|
+
function requestMeasurement(record) {
|
|
19
18
|
const unavailable = reason => ({source:"ocx",output:record.output,elapsed:null,reason});
|
|
20
19
|
if (record.usageStatus === "unsupported") return unavailable("usage_unsupported");
|
|
21
20
|
if (!Number.isFinite(record.output) || record.output <= 0) return unavailable("output_missing");
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
elapsed=record.generationEnd-record.generationStart;
|
|
25
|
-
} else {
|
|
26
|
-
if (record.first === undefined) return unavailable("ttft_missing");
|
|
27
|
-
if (!Number.isFinite(record.first) || record.first < 0) return unavailable("invalid_duration");
|
|
28
|
-
elapsed=record.duration-record.first;
|
|
29
|
-
}
|
|
30
|
-
if (!Number.isFinite(elapsed) || elapsed <= 0) return unavailable("invalid_duration");
|
|
31
|
-
if (elapsed < 1000) return unavailable("decode_window_too_short");
|
|
32
|
-
return {source:"ocx",output:record.output,elapsed,reason:null};
|
|
21
|
+
if (!Number.isFinite(record.duration) || record.duration <= 0) return unavailable("invalid_duration");
|
|
22
|
+
return {source:"ocx",output:record.output,elapsed:record.duration,reason:null};
|
|
33
23
|
}
|
|
34
24
|
|
|
35
25
|
export class OpenCodexUsageReader {
|
|
@@ -85,6 +75,6 @@ export class OpenCodexUsageReader {
|
|
|
85
75
|
owners.set(record.id,(owners.get(record.id) ?? 0)+1);
|
|
86
76
|
}
|
|
87
77
|
return new Map([...candidates].filter(([,record])=>owners.get(record.id) === 1)
|
|
88
|
-
.map(([id,record])=>[id,
|
|
78
|
+
.map(([id,record])=>[id,requestMeasurement(record)]));
|
|
89
79
|
}
|
|
90
80
|
}
|
|
@@ -9,6 +9,7 @@ export function createSessionState() {
|
|
|
9
9
|
turns: new Set(),
|
|
10
10
|
currentTurnId: null,
|
|
11
11
|
runningTurns: new Set(),
|
|
12
|
+
completedTurns: new Set(),
|
|
12
13
|
responses: new Set(),
|
|
13
14
|
hasResponseUsage: false,
|
|
14
15
|
total: null,
|
|
@@ -66,8 +67,14 @@ export function applySessionStatusEvent(state, event) {
|
|
|
66
67
|
if (Number.isFinite(event.contextWindow)) state.contextWindow=event.contextWindow;
|
|
67
68
|
state.baseInstructionTokens=event.instructionTokens; break;
|
|
68
69
|
case "context-reset": state.compactionEvents++; state.last=null; reset(); break;
|
|
69
|
-
case "turn-started": reset(); state.currentTurnId=event.id; state.turns.add(event.id); state.runningTurns.add(event.id); break;
|
|
70
|
-
case "turn-ended":
|
|
70
|
+
case "turn-started": reset(); state.currentTurnId=event.id; state.turns.add(event.id); state.runningTurns.add(event.id); state.completedTurns.delete(event.id); break;
|
|
71
|
+
case "turn-ended": {
|
|
72
|
+
const id = event.id ?? state.currentTurnId;
|
|
73
|
+
if (event.completed && id) state.completedTurns.add(id);
|
|
74
|
+
reset(); state.currentTurnId=null;
|
|
75
|
+
if (id) state.runningTurns.delete(id); else state.runningTurns.clear();
|
|
76
|
+
break;
|
|
77
|
+
}
|
|
71
78
|
case "first-token-measured": state.ttftTotalMs+=event.elapsed; state.ttftSamples++; break;
|
|
72
79
|
case "compaction-finished": state.compactionCompleted++; break;
|
|
73
80
|
case "output-observed":
|
|
@@ -103,39 +110,33 @@ export function applySessionStatusEvent(state, event) {
|
|
|
103
110
|
if (state.total || state.turns.size) state.updatedAt=event.at ?? state.updatedAt;
|
|
104
111
|
}
|
|
105
112
|
|
|
113
|
+
function responseSpeed(sample, measurements) {
|
|
114
|
+
const measurement = measurements.get(sample?.id);
|
|
115
|
+
const source = measurement?.source ?? "codex";
|
|
116
|
+
const elapsed = measurement ? measurement.elapsed : sample?.start == null ? null
|
|
117
|
+
: sample.end - sample.start - boundedToolWaitMs(sample.tools, sample.start, sample.end, sample.outputs);
|
|
118
|
+
const valid = Boolean(sample?.turnId) && Number.isFinite(elapsed) && elapsed > 0
|
|
119
|
+
&& (source === "ocx" || elapsed >= 1000);
|
|
120
|
+
const output = measurement?.output ?? sample?.output;
|
|
121
|
+
const unavailableReason = measurement?.reason ?? (Number.isFinite(elapsed) && elapsed > 0 && elapsed < 1000
|
|
122
|
+
? "decode_window_too_short" : "timing_missing");
|
|
123
|
+
return { source, elapsed, valid, output, unavailableReason };
|
|
124
|
+
}
|
|
125
|
+
|
|
106
126
|
export function sessionStatusSnapshot(state, threadId, measurements = new Map()) {
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
const turnSpeeds = new Map();
|
|
111
|
-
let unavailableSource = "codex", unavailableReason = "timing_missing";
|
|
127
|
+
const { source, elapsed, valid, output, unavailableReason } = responseSpeed(state.speedMeasurements.at(-1), measurements);
|
|
128
|
+
// 窗口先按完成回合截取;缺少计时不能让更早回合补进最新十轮。
|
|
129
|
+
const recentTurns = new Map([...state.completedTurns].slice(-10).map(id => [id, { output:0, elapsed:0 }]));
|
|
112
130
|
for (const sample of state.speedMeasurements) {
|
|
113
|
-
const
|
|
114
|
-
|
|
115
|
-
const
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
unavailableReason=measurement?.reason ?? (Number.isFinite(elapsed) && elapsed > 0 && elapsed < 1000
|
|
120
|
-
? "decode_window_too_short" : "timing_missing");
|
|
121
|
-
if (!Number.isFinite(elapsed) || elapsed < 1000) continue;
|
|
122
|
-
const turn = turnSpeeds.get(sample.turnId) ?? { tokens:0, ms:0, samples:0, sources:new Set() };
|
|
123
|
-
turn.tokens += measurement?.output ?? sample.output;
|
|
124
|
-
turn.ms += elapsed;
|
|
125
|
-
turn.samples += 1;
|
|
126
|
-
turn.sources.add(source);
|
|
127
|
-
turnSpeeds.set(sample.turnId, turn);
|
|
128
|
-
}
|
|
129
|
-
// 按回合顺序取最近五个有效回合;同轮多步合并,无数据的回合不占名额。
|
|
130
|
-
const recentTurns = [...state.turns].filter(id => turnSpeeds.has(id)).slice(-5);
|
|
131
|
-
const speedSources = new Set();
|
|
132
|
-
for (const id of recentTurns) {
|
|
133
|
-
const turn = turnSpeeds.get(id);
|
|
134
|
-
decodeTokens += turn.tokens;
|
|
135
|
-
decodeMs += turn.ms;
|
|
136
|
-
speedSamples += turn.samples;
|
|
137
|
-
for (const source of turn.sources) speedSources.add(source);
|
|
131
|
+
const turn = recentTurns.get(sample.turnId);
|
|
132
|
+
if (!turn) continue;
|
|
133
|
+
const speed = responseSpeed(sample, measurements);
|
|
134
|
+
if (!speed.valid) continue;
|
|
135
|
+
turn.output += speed.output;
|
|
136
|
+
turn.elapsed += speed.elapsed;
|
|
138
137
|
}
|
|
138
|
+
const turnSpeeds = [...recentTurns.values()].filter(turn => turn.elapsed > 0)
|
|
139
|
+
.map(turn => turn.output / (turn.elapsed / 1000));
|
|
139
140
|
const total = state.total;
|
|
140
141
|
const input = total?.input ?? 0;
|
|
141
142
|
const cached = total?.cached ?? 0;
|
|
@@ -154,11 +155,13 @@ export function sessionStatusSnapshot(state, threadId, measurements = new Map())
|
|
|
154
155
|
uncachedInputTokens: total ? Math.max(0, input - cached - cacheWrite) : null,
|
|
155
156
|
outputTokens: total?.output ?? null,
|
|
156
157
|
cacheHitPercent: input > 0 ? Math.round(cached / input * 100) : null,
|
|
157
|
-
observedTps:
|
|
158
|
-
speedSamples,
|
|
159
|
-
speedSource:
|
|
160
|
-
speedUnavailableReason:
|
|
161
|
-
observedModelMs:
|
|
158
|
+
observedTps: valid ? Math.round(output / (elapsed / 1000)) : null,
|
|
159
|
+
speedSamples: valid ? 1 : 0,
|
|
160
|
+
speedSource: source,
|
|
161
|
+
speedUnavailableReason: valid ? null : unavailableReason,
|
|
162
|
+
observedModelMs: valid ? elapsed : 0,
|
|
163
|
+
averageTps: turnSpeeds.length ? Math.round(turnSpeeds.reduce((sum, speed) => sum + speed, 0) / turnSpeeds.length) : null,
|
|
164
|
+
averageTpsTurns: turnSpeeds.length,
|
|
162
165
|
toolMs: state.toolMs,
|
|
163
166
|
averageFirstTokenMs: state.ttftSamples > 0 ? Math.round(state.ttftTotalMs / state.ttftSamples) : null,
|
|
164
167
|
contextUsedTokens: contextUsed,
|
|
@@ -4,24 +4,24 @@ const STYLE_ID = "minecodex-session-status-style";
|
|
|
4
4
|
const copyByLocale = { "zh-CN": {
|
|
5
5
|
turns: "回合", steps: "步", speed: "TPS", cache: "缓存", cacheDetail: "缓存命中",
|
|
6
6
|
statistics: "会话统计", usage: "Token 用量", context: "上下文详情", total: "总量",
|
|
7
|
-
firstToken: "平均首字延迟", input: "输入", cached: "缓存输入", output: "输出",
|
|
7
|
+
firstToken: "平均首字延迟", averageSpeed: "平均 TPS", input: "输入", cached: "缓存输入", output: "输出",
|
|
8
8
|
sum: "合计", capacity: "窗口容量", contextShare: "上下文占用", compactions: "压缩次数",
|
|
9
9
|
instructions: "指令", messages: "消息", toolHistory: "工具交互", other: "未分类",
|
|
10
10
|
speedHelp: "关于 TPS", speedInfo: "根据会话日志估算生成速度,包含推理输出。",
|
|
11
|
-
speedInfoOcx: "根据 OCX
|
|
12
|
-
|
|
13
|
-
|
|
11
|
+
speedInfoOcx: "根据 OCX 记录计算响应速度,包含推理输出和请求等待时间。",
|
|
12
|
+
speedWindow: "取最近一次已完成响应的数据,仅供参考。",
|
|
13
|
+
speedWindowOcx: "取最近一次请求的记录,与 OCX 的 TPS 一致。",
|
|
14
14
|
speedUnavailable: "有效计时不足,暂无法估算。",
|
|
15
15
|
}, en: {
|
|
16
16
|
turns: "turns", steps: "steps", speed: "TPS", cache: "Cache", cacheDetail: "Cache hit",
|
|
17
17
|
statistics: "Session stats", usage: "Token usage", context: "Context", total: "Total",
|
|
18
|
-
firstToken: "Avg first token", input: "Input", cached: "Cached input", output: "Output",
|
|
18
|
+
firstToken: "Avg first token", averageSpeed: "Avg TPS", input: "Input", cached: "Cached input", output: "Output",
|
|
19
19
|
sum: "Sum", capacity: "Window size", contextShare: "Context used", compactions: "Compactions",
|
|
20
20
|
instructions: "Instructions", messages: "Messages", toolHistory: "Tool activity", other: "Unclassified",
|
|
21
21
|
speedHelp: "About TPS", speedInfo: "Generation speed estimated from session logs, including reasoning output.",
|
|
22
|
-
speedInfoOcx: "
|
|
23
|
-
|
|
24
|
-
|
|
22
|
+
speedInfoOcx: "Response speed calculated from OCX records, including reasoning output and request waiting time.",
|
|
23
|
+
speedWindow: "Uses the latest completed response. For reference only.",
|
|
24
|
+
speedWindowOcx: "Uses the latest request record, matching OCX TPS.",
|
|
25
25
|
speedUnavailable: "Not enough valid timing data to estimate speed.",
|
|
26
26
|
} };
|
|
27
27
|
function selectedCopy() {
|
|
@@ -31,8 +31,9 @@ function selectedCopy() {
|
|
|
31
31
|
|
|
32
32
|
function speedTooltip(value, copy) {
|
|
33
33
|
const source = value.speedSource === "ocx" ? copy.speedInfoOcx
|
|
34
|
-
:
|
|
35
|
-
|
|
34
|
+
: copy.speedInfo;
|
|
35
|
+
const window = value.speedSource === "ocx" ? copy.speedWindowOcx : copy.speedWindow;
|
|
36
|
+
return `${source} ${value.observedTps == null ? copy.speedUnavailable : window}`;
|
|
36
37
|
}
|
|
37
38
|
|
|
38
39
|
// 图标来自 Lucide Static 与 Phosphor Icons;许可见 vendor/。
|
|
@@ -376,6 +377,7 @@ function showPanel(kind) {
|
|
|
376
377
|
row(dl, copy.turns, number(snapshot.turns));
|
|
377
378
|
row(dl, copy.steps, number(snapshot.steps));
|
|
378
379
|
row(dl, copy.firstToken, duration(snapshot.averageFirstTokenMs));
|
|
380
|
+
row(dl, copy.averageSpeed, snapshot.averageTps == null ? "—" : `${number(snapshot.averageTps)} tok/s`);
|
|
379
381
|
const speedLabel = row(dl, copy.speed, snapshot.observedTps == null ? "—" : `${number(snapshot.observedTps)} tok/s`);
|
|
380
382
|
speedLabel.className = "tps-label";
|
|
381
383
|
tpsInfo.setAttribute("aria-label", copy.speedHelp);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "minecodex",
|
|
3
|
-
"version": "2.0.
|
|
3
|
+
"version": "2.0.28",
|
|
4
4
|
"description": "Lightweight, local-first plugins for the Codex desktop app.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -44,6 +44,10 @@
|
|
|
44
44
|
"README.md",
|
|
45
45
|
"features/usage-meter/src",
|
|
46
46
|
"features/usage-meter/codex-feature.json",
|
|
47
|
+
"features/quick-chat/src",
|
|
48
|
+
"features/quick-chat/assets",
|
|
49
|
+
"features/quick-chat/vendor/lucide-LICENSE",
|
|
50
|
+
"features/quick-chat/codex-feature.json",
|
|
47
51
|
"features/sidebar-focus/src",
|
|
48
52
|
"features/sidebar-focus/codex-feature.json",
|
|
49
53
|
"features/sidebar-focus/vendor/lucide-LICENSE",
|
|
@@ -72,14 +76,18 @@
|
|
|
72
76
|
"apps/dashboard/transport.js",
|
|
73
77
|
"packages/plugin/src",
|
|
74
78
|
"packages/plugin/assets",
|
|
75
|
-
"packages/plugin/plugin.json"
|
|
79
|
+
"packages/plugin/plugin.json",
|
|
80
|
+
"features/quick-chat-repair/src",
|
|
81
|
+
"features/quick-chat-repair/assets",
|
|
82
|
+
"features/quick-chat-repair/vendor/lucide-LICENSE",
|
|
83
|
+
"features/quick-chat-repair/codex-feature.json"
|
|
76
84
|
],
|
|
77
85
|
"engines": {
|
|
78
86
|
"node": ">=22.5.0"
|
|
79
87
|
},
|
|
80
88
|
"scripts": {
|
|
81
89
|
"start": "node packages/cli/bin/mcx.mjs serve",
|
|
82
|
-
"test": "node --test packages/cli/test/*.test.mjs packages/runtime-host/test/*.test.mjs features/model-slider/test/*.test.mjs features/notes/test/*.test.mjs features/images/test/*.test.mjs features/hide-upsell-banner/test/*.test.mjs features/session-status/test/*.test.mjs features/usage-meter/test/*.test.mjs apps/dashboard/test/*.test.mjs features/unread-inbox/test/*.test.mjs features/sidebar-focus/test/*.test.mjs features/sites/test/*.test.mjs packages/ui/test/*.test.mjs packages/codex-adapter/test/*.test.mjs features/markdown-reading/test/*.test.mjs features/file-navigation/test/*.test.mjs packages/plugin/test/*.test.mjs",
|
|
90
|
+
"test": "node --test packages/cli/test/*.test.mjs packages/runtime-host/test/*.test.mjs features/model-slider/test/*.test.mjs features/notes/test/*.test.mjs features/images/test/*.test.mjs features/hide-upsell-banner/test/*.test.mjs features/session-status/test/*.test.mjs features/usage-meter/test/*.test.mjs apps/dashboard/test/*.test.mjs features/unread-inbox/test/*.test.mjs features/quick-chat/test/*.test.mjs features/sidebar-focus/test/*.test.mjs features/sites/test/*.test.mjs packages/ui/test/*.test.mjs packages/codex-adapter/test/*.test.mjs features/markdown-reading/test/*.test.mjs features/file-navigation/test/*.test.mjs packages/plugin/test/*.test.mjs features/quick-chat-repair/test/*.test.mjs",
|
|
83
91
|
"test:cli": "node --test packages/cli/test/*.test.mjs",
|
|
84
92
|
"test:runtime": "node --test packages/runtime-host/test/*.test.mjs",
|
|
85
93
|
"check": "node scripts/check-syntax.mjs",
|
|
@@ -2,7 +2,7 @@ import { randomBytes } from "node:crypto";
|
|
|
2
2
|
import { mkdir, readFile, rename, writeFile } from "node:fs/promises";
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
|
|
5
|
-
export const FEATURE_IDS = Object.freeze(["model-slider", "notes", "images", "hide-upsell-banner", "session-status", "usage-meter", "sidebar-focus", "sites", "markdown-reading", "file-navigation"]);
|
|
5
|
+
export const FEATURE_IDS = Object.freeze(["model-slider", "notes", "images", "hide-upsell-banner", "session-status", "usage-meter", "sidebar-focus", "sites", "markdown-reading", "file-navigation", "quick-chat", "quick-chat-repair"]);
|
|
6
6
|
export const DEFAULT_CONFIG = Object.freeze({
|
|
7
7
|
schemaVersion: 1,
|
|
8
8
|
features: Object.freeze({
|
|
@@ -16,6 +16,8 @@ export const DEFAULT_CONFIG = Object.freeze({
|
|
|
16
16
|
sites: true,
|
|
17
17
|
"markdown-reading": true,
|
|
18
18
|
"file-navigation": true,
|
|
19
|
+
"quick-chat": true,
|
|
20
|
+
"quick-chat-repair": true,
|
|
19
21
|
}),
|
|
20
22
|
});
|
|
21
23
|
|
|
@@ -14,7 +14,7 @@ import { createDataTransfer, MAX_TRANSFER_BYTES } from "./data-transfer/service.
|
|
|
14
14
|
const packageRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../../..");
|
|
15
15
|
const packageMetadata = JSON.parse(await readFile(path.join(packageRoot, "package.json"), "utf8"));
|
|
16
16
|
const MAX_BODY_BYTES = 64 * 1024;
|
|
17
|
-
const DASHBOARD_FEATURE_ORDER = ["model-slider", "notes", "usage-meter", "session-status", "markdown-reading", "file-navigation", "images", "sites", "sidebar-focus", "hide-upsell-banner"];
|
|
17
|
+
const DASHBOARD_FEATURE_ORDER = ["model-slider", "notes", "usage-meter", "session-status", "markdown-reading", "file-navigation", "images", "sites", "quick-chat", "quick-chat-repair", "sidebar-focus", "hide-upsell-banner"];
|
|
18
18
|
const CONTENT_TYPES = new Map([
|
|
19
19
|
[".css", "text/css; charset=utf-8"],
|
|
20
20
|
[".html", "text/html; charset=utf-8"],
|
|
@@ -13,15 +13,16 @@ export function installSurfaces(signal, ui) {
|
|
|
13
13
|
[data-mcx-ui-surface=popover] { box-shadow:0 0 0 var(--mcx-popover-overlay-ring-width,.5px) var(--mcx-color-border-overlay),var(--mcx-shadow-popover-overlay,var(--mcx-shadow-lg)); }
|
|
14
14
|
[data-mcx-ui-surface=popover][data-mcx-ui-variant=translucent] { background:color-mix(in oklab,var(--mcx-color-surface-elevated-secondary) 90%,transparent); }
|
|
15
15
|
[data-mcx-ui-surface=composer-menu] { border:var(--mcx-suggestion-menu-border-width,1px) solid var(--mcx-color-border); border-radius:var(--mcx-radius-suggestion-menu,var(--mcx-radius-2xl)); box-shadow:var(--mcx-suggestion-menu-shadow,none); }
|
|
16
|
+
[data-mcx-ui-surface=floating-panel] { box-shadow:0 0 0 .5px var(--mcx-color-border),var(--mcx-shadow-2xl); }
|
|
16
17
|
[data-mcx-ui-surface=page] { border-radius:var(--mcx-radius-xl-base); background:var(--mcx-color-surface); box-shadow:none; }
|
|
17
18
|
[data-mcx-ui-surface=page][data-mcx-ui-variant=transparent] { background:transparent; }
|
|
18
19
|
`;
|
|
19
20
|
document.head.append(style);
|
|
20
21
|
signal.addEventListener("abort", () => style.remove(), { once:true });
|
|
21
22
|
function surface(node, kind = "popover", { variant, opaque = true } = {}) {
|
|
22
|
-
const defaults = { dialog:"opaque", dropdown:"menu", popover:opaque ? "opaque" : "translucent", "composer-menu":"floating", page:"opaque" };
|
|
23
|
+
const defaults = { dialog:"opaque", dropdown:"menu", popover:opaque ? "opaque" : "translucent", "composer-menu":"floating", page:"opaque", "floating-panel":"opaque" };
|
|
23
24
|
if (!(kind in defaults)) throw new Error(`Unknown UI surface: ${kind}`);
|
|
24
|
-
if (kind !== "dialog" && kind !== "page") ui.layer(node, "floating");
|
|
25
|
+
if (kind !== "dialog" && kind !== "page" && kind !== "floating-panel") ui.layer(node, "floating");
|
|
25
26
|
node.setAttribute("data-mcx-ui-surface", kind);
|
|
26
27
|
node.setAttribute("data-mcx-ui-variant", variant ?? defaults[kind]);
|
|
27
28
|
return node;
|