minecodex 2.0.45 → 2.0.46
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/apps/dashboard/assets/preview-content.js +3 -0
- package/features/files-auto-refresh/assets/folder-sync.svg +7 -0
- package/features/files-auto-refresh/codex-feature.json +17 -0
- package/features/files-auto-refresh/src/adapters/codex/directories.mjs +75 -0
- package/features/files-auto-refresh/src/entry.mjs +58 -0
- package/features/files-auto-refresh/src/injection-source.mjs +8 -0
- package/features/files-auto-refresh/src/refresh.mjs +32 -0
- package/features/files-auto-refresh/vendor/lucide-LICENSE +43 -0
- package/features/quick-chat/assets/close.svg +15 -0
- package/features/quick-chat/codex-feature.json +2 -1
- package/features/quick-chat/src/adapters/codex/lifecycle.mjs +27 -0
- package/features/quick-chat/src/adapters/codex/renderer.mjs +8 -5
- package/features/quick-chat/src/adapters/codex/shortcut.mjs +38 -14
- package/features/quick-chat/src/frontend/entry.mjs +33 -18
- package/features/quick-chat/src/injection-source.mjs +2 -1
- package/features/usage-meter/src/adapters/codex/renderer.mjs +14 -4
- package/features/usage-meter/src/usage-meter.mjs +24 -19
- package/package.json +6 -2
- package/packages/cli/src/config.mjs +2 -1
- package/packages/cli/src/control-server.mjs +1 -1
- package/packages/ui/src/native/tokens.mjs +2 -0
|
@@ -21,6 +21,9 @@ const content = {
|
|
|
21
21
|
'file-navigation': [
|
|
22
22
|
['Codex 文件预览上方的路径区域。', '浏览相邻目录和文件需要额外跳转。', '点击路径展开分列目录,逐级浏览并打开另一个文件。'],
|
|
23
23
|
['The path above a file preview in Codex.', 'Browsing nearby files and folders takes extra navigation.', 'Open the path to browse cascading folder columns and choose another file.']],
|
|
24
|
+
'files-auto-refresh': [
|
|
25
|
+
['Codex Side Panel 的 Files 目录列表。', 'Agent 改变项目文件后,需要反复收起、展开才能看到新列表。', '保持 Files 打开,自动同步根目录和已展开目录;保留当前文件与浏览位置。'],
|
|
26
|
+
['The Files tree in the Codex side panel.', 'File changes can require collapsing and reopening the tree.', 'Keep Files open to refresh the root and expanded folders while preserving the current file and browsing position.']],
|
|
24
27
|
images: [
|
|
25
28
|
['Codex Library 的图片集合与图片查看器。', '查找图片及其提示词需要来回切换。', '浏览图片集合,打开图片查看大图及可追溯的提示词信息。'],
|
|
26
29
|
['Image collections in Codex Library and the image viewer.', 'Finding images and their prompts requires switching views.', 'Browse collections, open an image, and inspect available prompt provenance.']],
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
|
2
|
+
<path d="M9 20H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h3.9a2 2 0 0 1 1.69.9l.81 1.2a2 2 0 0 0 1.67.9H20a2 2 0 0 1 2 2v.5" />
|
|
3
|
+
<path d="M12 10v4h4" />
|
|
4
|
+
<path d="m12 14 1.535-1.605a5 5 0 0 1 8 1.5" />
|
|
5
|
+
<path d="M22 22v-4h-4" />
|
|
6
|
+
<path d="m22 18-1.535 1.605a5 5 0 0 1-8-1.5" />
|
|
7
|
+
</svg>
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
{
|
|
2
|
+
"schemaVersion": 1,
|
|
3
|
+
"id": "files-auto-refresh",
|
|
4
|
+
"version": "0.1.0",
|
|
5
|
+
"label": { "en": "Files Auto Refresh", "zh-CN": "Files 自动刷新" },
|
|
6
|
+
"description": {
|
|
7
|
+
"en": "Keep the open Files tree up to date as files and folders are created, removed or renamed.",
|
|
8
|
+
"zh-CN": "文件或文件夹新增、删除、重命名后,自动更新打开的 Files 目录列表。"
|
|
9
|
+
},
|
|
10
|
+
"entry": {
|
|
11
|
+
"kind": "page-script",
|
|
12
|
+
"script": "src/entry.mjs",
|
|
13
|
+
"sourceFactory": "src/injection-source.mjs"
|
|
14
|
+
},
|
|
15
|
+
"icon": { "resource": "assets/folder-sync.svg" },
|
|
16
|
+
"placement": { "order": 92 }
|
|
17
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
export function createDirectoryRefreshAdapter(codex, signal = new AbortController().signal) {
|
|
2
|
+
const visible = element => element.isConnected && !element.closest('[hidden],[inert],[aria-hidden="true"]')
|
|
3
|
+
&& element.checkVisibility({ visibilityProperty: true }) && element.getClientRects().length > 0;
|
|
4
|
+
|
|
5
|
+
function context(anchor) {
|
|
6
|
+
let directory = null, client = null, expanded = [];
|
|
7
|
+
codex.props(anchor, props => {
|
|
8
|
+
if (Array.isArray(props.initialExpandedPaths) && 'files' in props && 'renderTree' in props)
|
|
9
|
+
expanded = props.initialExpandedPaths;
|
|
10
|
+
if (!directory && typeof props.root === 'string' && typeof props.hostId === 'string'
|
|
11
|
+
&& 'includeHidden' in props && 'renderTree' in props) directory = props;
|
|
12
|
+
for (const value of [props.value, props.client, props.queryClient, props.value?.queryClient])
|
|
13
|
+
if (!client && typeof value?.getQueryCache === 'function' && typeof value.setQueryData === 'function') client = value;
|
|
14
|
+
return Boolean(directory && client);
|
|
15
|
+
}, 360);
|
|
16
|
+
return { directory, client, expanded };
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function directoryQueries(client, directory, expanded) {
|
|
20
|
+
const directories = new Set([null, ...expanded]);
|
|
21
|
+
return client.getQueryCache().getAll().filter(query => {
|
|
22
|
+
const key = query.queryKey;
|
|
23
|
+
if (key?.[0] !== 'vscode' || key[1] !== 'workspace-directory-entries' || !query.isActive()
|
|
24
|
+
|| typeof query.options?.queryFn !== 'function') return false;
|
|
25
|
+
let params;
|
|
26
|
+
try { params = JSON.parse(key.at(-1)); } catch { return false; }
|
|
27
|
+
return params?.hostId === directory.hostId && params.workspaceRoot === directory.root
|
|
28
|
+
&& params.includeHidden === directory.includeHidden && directories.has(params.directoryPath);
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
return {
|
|
33
|
+
scan() {
|
|
34
|
+
const views = [], roots = new Set();
|
|
35
|
+
// 搜索框在空目录时也存在,不能只依赖文件树节点。
|
|
36
|
+
const anchors = [...document.querySelectorAll('file-tree-container'), ...document.querySelectorAll('[id="workspace-directory-tree-search"]')];
|
|
37
|
+
for (const anchor of anchors) {
|
|
38
|
+
if (!visible(anchor)) continue;
|
|
39
|
+
const { directory, client, expanded } = context(anchor);
|
|
40
|
+
if (!directory) continue;
|
|
41
|
+
const key = JSON.stringify([directory.hostId, directory.environmentId, directory.root]);
|
|
42
|
+
if (roots.has(key)) continue;
|
|
43
|
+
roots.add(key);
|
|
44
|
+
const available = Boolean(client && directory.environmentId == null);
|
|
45
|
+
const current = () => {
|
|
46
|
+
if (signal.aborted || document.visibilityState === 'hidden' || !visible(anchor)) return false;
|
|
47
|
+
const next = context(anchor);
|
|
48
|
+
return next.client === client && next.directory?.hostId === directory.hostId
|
|
49
|
+
&& next.directory?.root === directory.root && next.directory?.environmentId === directory.environmentId
|
|
50
|
+
&& next.directory?.includeHidden === directory.includeHidden;
|
|
51
|
+
};
|
|
52
|
+
views.push({
|
|
53
|
+
key, anchor, available, current, boundary: anchor.closest('[data-app-shell-pane-content-viewport]'),
|
|
54
|
+
// 每轮重新取得 active queries,已收起的分支不会继续请求。
|
|
55
|
+
queries: available ? directoryQueries(client, directory, expanded).map(query => ({
|
|
56
|
+
async refresh() {
|
|
57
|
+
if (!current() || !query.isActive() || query.state.fetchStatus === 'fetching') return;
|
|
58
|
+
const path = JSON.parse(query.queryKey.at(-1)).directoryPath;
|
|
59
|
+
if (path !== null && !context(anchor).expanded.includes(path)) return;
|
|
60
|
+
const before = query.state.data, updatedAt = query.state.dataUpdatedAt;
|
|
61
|
+
// 失败不进入原生 query 的 error 状态,避免原生树用错误页替换已有列表。
|
|
62
|
+
const data = await query.options.queryFn({ client, queryKey: query.queryKey, meta: query.options.meta, signal });
|
|
63
|
+
if (!Array.isArray(data?.entries)) throw new Error('directory-refresh-response-unavailable');
|
|
64
|
+
if (!current() || !query.isActive() || query.state.fetchStatus === 'fetching'
|
|
65
|
+
|| query.state.data !== before || query.state.dataUpdatedAt !== updatedAt) return;
|
|
66
|
+
if (path !== null && !context(anchor).expanded.includes(path)) return;
|
|
67
|
+
client.setQueryData(query.queryKey, data);
|
|
68
|
+
},
|
|
69
|
+
})) : [],
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
return views;
|
|
73
|
+
},
|
|
74
|
+
};
|
|
75
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
const reads = new AbortController();
|
|
2
|
+
const adapter = createDirectoryRefreshAdapter(config.codex, reads.signal);
|
|
3
|
+
const failures = new Map();
|
|
4
|
+
let views = [], frame = null;
|
|
5
|
+
const isVisible = () => !signal.aborted && document.visibilityState === 'visible';
|
|
6
|
+
const loop = createDirectoryRefreshLoop({
|
|
7
|
+
scan() {
|
|
8
|
+
views = adapter.scan();
|
|
9
|
+
for (const [key, item] of failures) if (!views.some(view => view.key === key && view.anchor === item.anchor)) {
|
|
10
|
+
item.button.remove(); failures.delete(key);
|
|
11
|
+
}
|
|
12
|
+
return views;
|
|
13
|
+
},
|
|
14
|
+
isVisible,
|
|
15
|
+
onResult(view, failed) {
|
|
16
|
+
if (!failed) { failures.get(view.key)?.button.remove(); failures.delete(view.key); return; }
|
|
17
|
+
if (!view.boundary) return;
|
|
18
|
+
let button = failures.get(view.key)?.button;
|
|
19
|
+
if (!button) {
|
|
20
|
+
button = document.createElement('button');
|
|
21
|
+
button.type = 'button'; button.setAttribute('data-mcx-directory-refresh-retry', '');
|
|
22
|
+
button.className = 'cursor-interaction rounded-md border border-default bg-surface px-2 py-1 text-sm text-secondary';
|
|
23
|
+
button.textContent = config.locale === 'zh-CN' ? '目录尚未同步 · 重试' : 'Directory not synced · Retry';
|
|
24
|
+
button.addEventListener('click', () => { loop.pause(); loop.request(); });
|
|
25
|
+
button.style.position = 'fixed'; button.style.zIndex = '1';
|
|
26
|
+
view.boundary.append(button); failures.set(view.key, { anchor: view.anchor, button });
|
|
27
|
+
}
|
|
28
|
+
// 提示放在列表底部,不把全高目录向下挤,也不改变原生容器的定位规则。
|
|
29
|
+
const bounds = view.boundary.getBoundingClientRect();
|
|
30
|
+
button.style.left = `${bounds.left + 8}px`; button.style.maxWidth = `${Math.max(0, bounds.width - 16)}px`;
|
|
31
|
+
button.style.top = `${Math.max(bounds.top + 8, bounds.bottom - button.offsetHeight - 8)}px`;
|
|
32
|
+
},
|
|
33
|
+
});
|
|
34
|
+
function reconcile() {
|
|
35
|
+
frame = null;
|
|
36
|
+
if (isVisible()) loop.request(); else loop.pause();
|
|
37
|
+
}
|
|
38
|
+
const observer = new MutationObserver(records => {
|
|
39
|
+
if (records.every(record => record.target.closest?.('[data-mcx-directory-refresh-retry],.cm-content'))) return;
|
|
40
|
+
if (frame === null) frame = requestAnimationFrame(reconcile);
|
|
41
|
+
});
|
|
42
|
+
observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['hidden', 'inert', 'aria-hidden', 'class', 'style'] });
|
|
43
|
+
document.addEventListener('visibilitychange', reconcile);
|
|
44
|
+
config.registerSelfCheck?.(() => {
|
|
45
|
+
const current = adapter.scan();
|
|
46
|
+
return {
|
|
47
|
+
state: !current.length ? 'not-applicable' : current.every(view => view.available && view.queries.length && !failures.has(view.key)) ? 'available' : 'unavailable',
|
|
48
|
+
checks: current.map(view => ({ name: 'native-directory-query', available: view.available && view.queries.length > 0 && !failures.has(view.key) })),
|
|
49
|
+
};
|
|
50
|
+
});
|
|
51
|
+
function dispose() {
|
|
52
|
+
reads.abort(); loop.dispose(); observer.disconnect(); document.removeEventListener('visibilitychange', reconcile);
|
|
53
|
+
if (frame !== null) cancelAnimationFrame(frame);
|
|
54
|
+
for (const item of failures.values()) item.button.remove(); failures.clear(); views = [];
|
|
55
|
+
}
|
|
56
|
+
signal.addEventListener('abort', dispose, { once: true });
|
|
57
|
+
reconcile();
|
|
58
|
+
return dispose;
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { readFile } from 'node:fs/promises';
|
|
2
|
+
import { createDirectoryRefreshAdapter } from './adapters/codex/directories.mjs';
|
|
3
|
+
import { createDirectoryRefreshLoop } from './refresh.mjs';
|
|
4
|
+
|
|
5
|
+
export async function createPageScriptSource() {
|
|
6
|
+
return [createDirectoryRefreshAdapter, createDirectoryRefreshLoop].map(fn => `const ${fn.name} = ${fn.toString()};`).join('\n')
|
|
7
|
+
+ '\n' + await readFile(new URL('./entry.mjs', import.meta.url), 'utf8');
|
|
8
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
export function createDirectoryRefreshLoop({ scan, onResult, isVisible, schedule = setTimeout, cancel = clearTimeout, interval = 1000 }) {
|
|
2
|
+
let timer = null, running = false, disposed = false;
|
|
3
|
+
async function tick() {
|
|
4
|
+
timer = null;
|
|
5
|
+
if (disposed || !isVisible()) return;
|
|
6
|
+
running = true;
|
|
7
|
+
let views = [];
|
|
8
|
+
try {
|
|
9
|
+
views = scan();
|
|
10
|
+
for (const view of views) {
|
|
11
|
+
let failed = false;
|
|
12
|
+
// 限制并发;刷新只触达原生已订阅目录,不扫描未知子目录。
|
|
13
|
+
for (let index = 0; index < view.queries.length; index += 4) {
|
|
14
|
+
if (disposed || !isVisible() || !view.current()) break;
|
|
15
|
+
const results = await Promise.allSettled(view.queries.slice(index, index + 4).map(query => query.refresh()));
|
|
16
|
+
failed ||= results.some(result => result.status === 'rejected');
|
|
17
|
+
}
|
|
18
|
+
if (!disposed && isVisible() && view.current()) onResult(view, failed);
|
|
19
|
+
}
|
|
20
|
+
} finally {
|
|
21
|
+
running = false;
|
|
22
|
+
if (!disposed && isVisible() && views.length) timer = schedule(tick, interval);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
return {
|
|
26
|
+
request() {
|
|
27
|
+
if (!disposed && !running && timer === null && isVisible()) timer = schedule(tick, 0);
|
|
28
|
+
},
|
|
29
|
+
pause() { if (timer !== null) cancel(timer); timer = null; },
|
|
30
|
+
dispose() { disposed = true; if (timer !== null) cancel(timer); timer = null; },
|
|
31
|
+
};
|
|
32
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
ISC License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Lucide Icons and Contributors
|
|
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.
|
|
16
|
+
|
|
17
|
+
---
|
|
18
|
+
|
|
19
|
+
The following Lucide icons are derived from the Feather project:
|
|
20
|
+
|
|
21
|
+
airplay, alert-circle, alert-octagon, alert-triangle, aperture, arrow-down-circle, arrow-down-left, arrow-down-right, arrow-down, arrow-left-circle, arrow-left, arrow-right-circle, arrow-right, arrow-up-circle, arrow-up-left, arrow-up-right, arrow-up, at-sign, calendar, cast, check, chevron-down, chevron-left, chevron-right, chevron-up, chevrons-down, chevrons-left, chevrons-right, chevrons-up, circle, clipboard, clock, code, columns, command, compass, corner-down-left, corner-down-right, corner-left-down, corner-left-up, corner-right-down, corner-right-up, corner-up-left, corner-up-right, crosshair, database, divide-circle, divide-square, dollar-sign, download, external-link, feather, frown, hash, headphones, help-circle, info, italic, key, layout, life-buoy, link-2, link, loader, lock, log-in, log-out, maximize, meh, minimize, minimize-2, minus-circle, minus-square, minus, monitor, moon, more-horizontal, more-vertical, move, music, navigation-2, navigation, octagon, pause-circle, percent, plus-circle, plus-square, plus, power, radio, rss, search, server, share, shopping-bag, sidebar, smartphone, smile, square, table-2, tablet, target, terminal, trash-2, trash, triangle, tv, type, upload, x-circle, x-octagon, x-square, x, zoom-in, zoom-out
|
|
22
|
+
|
|
23
|
+
The MIT License (MIT) (for the icons listed above)
|
|
24
|
+
|
|
25
|
+
Copyright (c) 2013-present Cole Bemis
|
|
26
|
+
|
|
27
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
28
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
29
|
+
in the Software without restriction, including without limitation the rights
|
|
30
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
31
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
32
|
+
furnished to do so, subject to the following conditions:
|
|
33
|
+
|
|
34
|
+
The above copyright notice and this permission notice shall be included in all
|
|
35
|
+
copies or substantial portions of the Software.
|
|
36
|
+
|
|
37
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
38
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
39
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
40
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
41
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
42
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
43
|
+
SOFTWARE.
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
<!-- @license lucide-static v0.468.0 - ISC; source: lucide-icons/lucide icons/x.svg -->
|
|
2
|
+
<svg
|
|
3
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
4
|
+
width="24"
|
|
5
|
+
height="24"
|
|
6
|
+
viewBox="0 0 24 24"
|
|
7
|
+
fill="none"
|
|
8
|
+
stroke="currentColor"
|
|
9
|
+
stroke-width="2"
|
|
10
|
+
stroke-linecap="round"
|
|
11
|
+
stroke-linejoin="round"
|
|
12
|
+
>
|
|
13
|
+
<path d="M18 6 6 18" />
|
|
14
|
+
<path d="m6 6 12 12" />
|
|
15
|
+
</svg>
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
// 从原生“丢弃 Quick Chat”动作中仅取关闭和草稿清理;不调用会话销毁,保留后台生成。
|
|
2
|
+
export function quickChatLifecycleActions(source) {
|
|
3
|
+
const match = source.match(/let ([\w$]+)=([\w$]+)\.get\(([\w$]+)\);([\w$]+)\(\2\),\2\.set\(\3,null\),\1!=null&&\(([\w$]+)\(\2,\{draftKey:\1\.conversationId,content:null\}\),([\w$]+)\(\2,\1\.conversationId,[`"'][`"']\),\2\.set\(([\w$]+),\1\.conversationId,\[\]\)/);
|
|
4
|
+
if (!match) throw new Error('quick-chat-close-contract-unavailable');
|
|
5
|
+
const exports = source.slice(source.lastIndexOf('export{'));
|
|
6
|
+
const exported = local => {
|
|
7
|
+
const escaped = local.replace(/[.*+?^${}()|[\]\\]/g,'\\$&');
|
|
8
|
+
const name = exports.match(new RegExp(`(?:\\{|,)${escaped} as ([\\w$]+)(?:,|\\})`))?.[1];
|
|
9
|
+
if (!name) throw new Error('quick-chat-close-export-unavailable');
|
|
10
|
+
return name;
|
|
11
|
+
};
|
|
12
|
+
const roles = {session:exported(match[3]),close:exported(match[4]),draft:exported(match[5]),text:exported(match[6]),attachments:exported(match[7])};
|
|
13
|
+
const mode = source.match(/([\w$]+)=[\w$]+\([\w$]+,\(\{get:[\w$]+\}\)=>\{[\s\S]{0,700}?conversationDetailMode[\s\S]{0,700}?[`"']STEPS_PROSE[`"'][^{}]*\?[`"']work[`"']:[`"']codex[`"']/)?.[1];
|
|
14
|
+
if (mode) roles.mode=exported(mode);
|
|
15
|
+
return roles;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function quickChatScope(codex, element) {
|
|
19
|
+
for (let fiber=codex.currentFiber(element),depth=0;fiber && depth++<200;fiber=fiber.return) {
|
|
20
|
+
const values=[];
|
|
21
|
+
for (let hook=fiber.memoizedState,count=0;hook && count++<150;hook=hook.next) values.push(hook.memoizedState?.current);
|
|
22
|
+
for (const row of fiber.updateQueue?.memoCache?.data ?? []) values.push(...row);
|
|
23
|
+
const scope=values.find(value=>typeof value?.get==='function' && typeof value.set==='function' && value.scope?.__scopeBrand==='AppScope');
|
|
24
|
+
if (scope) return scope;
|
|
25
|
+
}
|
|
26
|
+
return null;
|
|
27
|
+
}
|
|
@@ -18,10 +18,14 @@ export function createQuickChatAdapter(codex) {
|
|
|
18
18
|
if (!history) return null;
|
|
19
19
|
const state = codex.props(editable, props => typeof props.submitDisabled === "boolean" && props.conversationId);
|
|
20
20
|
const controller = codex.props(editable, props => props.composerController?.eventEmitter)?.composerController;
|
|
21
|
+
const expanded = Boolean(floating.presentation === "expanded" || floating.forceExpanded);
|
|
21
22
|
const minimize = [...surface.querySelectorAll('button')].find(button => {
|
|
22
|
-
const props = codex.props(button, props => props.standalone === true && typeof props.onMinimize === "function" && typeof props.isCompact === "boolean"
|
|
23
|
-
|
|
23
|
+
const props = codex.props(button, props => (props.standalone === true || expanded) && typeof props.onMinimize === "function" && typeof props.isCompact === "boolean"
|
|
24
|
+
&& Object.keys(props).every(key => ["standalone","isCompact","onMinimize"].includes(key)), 5);
|
|
25
|
+
return Boolean(props && button.checkVisibility({visibilityProperty:true}) && !button.hasAttribute('data-mcx-quick-chat-history') && !button.hasAttribute('data-mcx-quick-chat-close'));
|
|
24
26
|
});
|
|
27
|
+
const drag = expanded && [...surface.querySelectorAll('header button[data-quick-chat-drag-handle]')]
|
|
28
|
+
.find(button => button.checkVisibility({visibilityProperty:true}));
|
|
25
29
|
let receipt = null;
|
|
26
30
|
for (let fiber = codex.currentFiber(surface), depth = 0; fiber && depth < 80; depth++, fiber = fiber.return) {
|
|
27
31
|
if (fiber.memoizedProps?.session?.conversationId !== session.conversationId) continue;
|
|
@@ -41,13 +45,12 @@ export function createQuickChatAdapter(codex) {
|
|
|
41
45
|
const project = [...surface.querySelectorAll('[data-composer-rail-item] button')].find(button =>
|
|
42
46
|
codex.props(button, props => props.variant === "composer" && typeof props.onProjectChange === "function"));
|
|
43
47
|
const projectRail = project?.closest('[data-composer-rail-item]') ?? null;
|
|
44
|
-
const expanded = Boolean(floating.presentation === "expanded" || floating.forceExpanded);
|
|
45
48
|
const emitter = controller?.eventEmitter;
|
|
46
49
|
const subscribable = typeof emitter?.addListener === "function" && typeof emitter?.removeListener === "function";
|
|
47
50
|
if (subscribable && !subscriptions.has(emitter)) subscriptions.set(emitter, nextSubscription++);
|
|
48
|
-
const snapshot = {surface, editable, minimize, projectRail, background:!session.hasConversation && floating.forceExpanded ? background : null, sessionId:session.conversationId, hasConversation:session.hasConversation,
|
|
51
|
+
const snapshot = {surface, editable, minimize, closeAnchor:drag || minimize, projectRail, background:!session.hasConversation && floating.forceExpanded ? background : null, sessionId:session.conversationId, hasConversation:session.hasConversation,
|
|
49
52
|
canSubmit:Boolean(state && !state.submitDisabled && !state.submitLoading), receiptId:receipt ? receipts.get(receipt) : 0, receiptPresentation:receipt?.presentation ?? null, subscriptionId:subscribable ? subscriptions.get(emitter) : 0,
|
|
50
|
-
expanded, showHistory:Boolean(minimize && !expanded), historyVisible:quickChatHistoryVisible(floating.renderTranscript?.()),
|
|
53
|
+
expanded, standalone:Boolean(minimize && codex.props(minimize,props=>props.standalone===true,5)), showHistory:Boolean(minimize && !expanded), showClose:Boolean(minimize && expanded), historyVisible:quickChatHistoryVisible(floating.renderTranscript?.()),
|
|
51
54
|
expand:() => floating.onPresentationChange("expanded"),
|
|
52
55
|
// 历史动作通过 forceExpanded 临时展开;不要覆盖最小化后的恢复形态。
|
|
53
56
|
history:() => history.onSelect()};
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import {quickChatLifecycleActions, quickChatScope} from "./lifecycle.mjs";
|
|
1
2
|
// 通过原生能力检查与已有会话分支定位打开动作,不依赖构建哈希或压缩名称。
|
|
2
3
|
export function quickChatOpenExport(source) {
|
|
3
4
|
const match = source.match(/function ([\w$]+)\(([\w$]+),[\w$]+=\{\}\)\{if\(!\2\.get\([\w$]+,\{name:[`"']chatgpt\.quick-chat[`"']\}\)\.isCapable\)return;let [\w$]+=\2\.get\([\w$]+\),[\w$]+=\2\.get\([\w$]+\);if\(/);
|
|
@@ -83,13 +84,13 @@ export function installQuickChatShortcut(signal, codex, discover = null) {
|
|
|
83
84
|
const picker = codex.props(button, p => typeof p.onModeSelect === 'function' && typeof p.mode === 'string', 80);
|
|
84
85
|
// 原生 ChatGPT 也使用 work 路由,具体产品由 workModeAccess 区分。
|
|
85
86
|
if (picker?.mode !== 'codex' && !(picker?.mode === 'work' && picker.workModeAccess === 'chatgpt')) continue;
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
87
|
+
const scope=quickChatScope(codex,button);
|
|
88
|
+
if (scope) return scope;
|
|
89
|
+
}
|
|
90
|
+
// 侧栏可能卸载;从仍挂载的窗口内容取当前 scope,模式由原生状态判定。
|
|
91
|
+
if (bindings?.mode) for (const anchor of document.querySelectorAll('[data-app-navigation-rail], [data-codex-composer-root], [data-chatgpt-composer], main, #root > *, #app > *')) {
|
|
92
|
+
const scope=quickChatScope(codex,anchor);
|
|
93
|
+
if (scope && bindings.mode(scope)==='codex') return scope;
|
|
93
94
|
}
|
|
94
95
|
return null;
|
|
95
96
|
}
|
|
@@ -120,12 +121,29 @@ export function installQuickChatShortcut(signal, codex, discover = null) {
|
|
|
120
121
|
if (!response.ok) throw error;
|
|
121
122
|
native=quickChatNativeCommand(await response.text());
|
|
122
123
|
}
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
124
|
+
const bindings={native};
|
|
125
|
+
if (!native) {
|
|
126
|
+
const roles = quickChatLifecycleExports(source);
|
|
127
|
+
if (!module[roles.openState] && typeof module[roles.initialize] === 'function') module[roles.initialize]();
|
|
128
|
+
const open = module[roles.open], create = module[roles.create], openState = module[roles.openState];
|
|
129
|
+
if (typeof open !== 'function' || typeof create !== 'function' || !openState) throw new Error('quick-chat-open-action-unavailable');
|
|
130
|
+
Object.assign(bindings,{open,create,isOpen:scope=>scope.get(openState)});
|
|
131
|
+
}
|
|
132
|
+
try {
|
|
133
|
+
const actions=quickChatLifecycleActions(source);
|
|
134
|
+
if (actions.mode && module[actions.mode]) bindings.mode=scope=>scope.get(module[actions.mode]);
|
|
135
|
+
if (['close','draft','text'].every(key=>typeof module[actions[key]]==='function') && module[actions.session] && module[actions.attachments]) {
|
|
136
|
+
bindings.close=(scope,id)=>{
|
|
137
|
+
if (scope.get(module[actions.session])?.conversationId!==id) return false;
|
|
138
|
+
module[actions.draft](scope,{draftKey:id,content:null});
|
|
139
|
+
module[actions.text](scope,id,'');
|
|
140
|
+
scope.set(module[actions.attachments],id,[]);
|
|
141
|
+
module[actions.close](scope);
|
|
142
|
+
return true;
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
} catch { codex.report('quick-chat-close',false,'quick-chat-close-contract-unavailable'); }
|
|
146
|
+
return bindings;
|
|
129
147
|
}
|
|
130
148
|
const ready = (discover ?? load)().then(value => {
|
|
131
149
|
if (disposed || signal.aborted) return;
|
|
@@ -146,6 +164,12 @@ export function installQuickChatShortcut(signal, codex, discover = null) {
|
|
|
146
164
|
}
|
|
147
165
|
return true;
|
|
148
166
|
}
|
|
167
|
+
function close(surface,id) {
|
|
168
|
+
const scope=quickChatScope(codex,surface);
|
|
169
|
+
if (!scope || !bindings?.close || disposed || signal.aborted) return false;
|
|
170
|
+
pendingFocus=null;
|
|
171
|
+
return bindings.close(scope,id);
|
|
172
|
+
}
|
|
149
173
|
function keydown(event) {
|
|
150
174
|
const primaryModifier = windows ? event.ctrlKey && !event.metaKey : event.metaKey && !event.ctrlKey;
|
|
151
175
|
if (event.defaultPrevented || event.repeat || event.isComposing
|
|
@@ -156,5 +180,5 @@ export function installQuickChatShortcut(signal, codex, discover = null) {
|
|
|
156
180
|
document.addEventListener('keydown', keydown, {signal});
|
|
157
181
|
function dispose() { disposed = true; stopWaiting?.(); document.removeEventListener('keydown', keydown); bindings = null; pendingFocus=null; }
|
|
158
182
|
signal.addEventListener('abort', dispose, {once:true});
|
|
159
|
-
return {ready, dispose, sync:focusNew, activate, available:() => Boolean(bindings && !bindings.native && !disposed && !signal.aborted)};
|
|
183
|
+
return {ready, dispose, sync:focusNew, activate, close, canClose:() => Boolean(bindings?.close && !disposed && !signal.aborted), available:() => Boolean(bindings && !bindings.native && !disposed && !signal.aborted)};
|
|
160
184
|
}
|
|
@@ -6,13 +6,17 @@ let scheduled = false, disposed = false;
|
|
|
6
6
|
const text = (en,zh) => (document.documentElement.lang || navigator.language).startsWith('zh') ? zh : en;
|
|
7
7
|
const style = document.createElement('style');
|
|
8
8
|
style.textContent = `
|
|
9
|
-
[data-mcx-quick-chat-history]{margin-inline-end:8px;flex-shrink:0}
|
|
10
|
-
[data-mcx-quick-chat-history]>svg{width:16px;height:16px}
|
|
9
|
+
[data-mcx-quick-chat-history],[data-mcx-quick-chat-close]{margin-inline-end:8px;flex-shrink:0}
|
|
10
|
+
[data-mcx-quick-chat-history]>svg,[data-mcx-quick-chat-close]>svg{width:16px;height:16px}
|
|
11
|
+
[data-mcx-quick-chat-close]{margin-inline-end:0}
|
|
11
12
|
[data-mcx-quick-chat-minimize]{border-radius:calc(infinity * 1px)}
|
|
13
|
+
/* 单行输入沿用控件中线,消除原生编辑区换行布局留下的底部间距。 */
|
|
14
|
+
[data-mcx-quick-chat-compact] [data-composer-input]:has(>[data-composer-input-layout="single-line"]){margin-block:0;align-self:center}
|
|
15
|
+
[data-mcx-quick-chat-compact] [data-composer-input-layout="single-line"]{margin-block:0;align-self:center}
|
|
12
16
|
[data-mcx-quick-chat-project-rail]{display:none!important}
|
|
13
17
|
/* 隐藏唯一的项目栏时,一并收起原生 rail 的圆角留白与负 margin。 */
|
|
14
18
|
[data-composer-rail]:has(>[data-mcx-quick-chat-project-rail]):not(:has(>:not([data-mcx-quick-chat-project-rail]):not(:empty))){display:none!important}
|
|
15
|
-
[data-floating-chat-surface]>:has(>[data-mcx-quick-chat-history]){display:flex;align-items:center}
|
|
19
|
+
[data-floating-chat-surface]>:has(>[data-mcx-quick-chat-history],>[data-mcx-quick-chat-close]){display:flex;align-items:center}
|
|
16
20
|
`;
|
|
17
21
|
document.head.append(style);
|
|
18
22
|
function releaseBackground(instance) {
|
|
@@ -21,7 +25,7 @@ function releaseBackground(instance) {
|
|
|
21
25
|
instance.background = null;
|
|
22
26
|
}
|
|
23
27
|
function releaseProjectRail(instance) { instance.projectRail?.removeAttribute('data-mcx-quick-chat-project-rail'); instance.projectRail = null; }
|
|
24
|
-
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(); }
|
|
28
|
+
function remove(instance) { instance.surface?.removeAttribute('data-mcx-quick-chat-compact'); releaseBackground(instance); releaseProjectRail(instance); instance.minimize?.removeAttribute('data-mcx-quick-chat-minimize'); instance.unsubscribe(); instance.button?.remove(); instance.tracker.clear(); instance.historyTracker.clear(); }
|
|
25
29
|
function sync() {
|
|
26
30
|
if (disposed) return;
|
|
27
31
|
shortcut.sync();
|
|
@@ -32,13 +36,14 @@ function sync() {
|
|
|
32
36
|
const selected = instance?.historyTracker.selected(snapshot);
|
|
33
37
|
if (!instance || instance.sessionId !== snapshot.sessionId || instance.subscriptionId !== snapshot.subscriptionId) {
|
|
34
38
|
if (instance) remove(instance);
|
|
35
|
-
instance = {sessionId:snapshot.sessionId, subscriptionId:snapshot.subscriptionId, tracker:createSubmissionTracker(), historyTracker:createHistoryTracker(), button:null};
|
|
39
|
+
instance = {surface:snapshot.surface, sessionId:snapshot.sessionId, subscriptionId:snapshot.subscriptionId, tracker:createSubmissionTracker(), historyTracker:createHistoryTracker(), button:null};
|
|
36
40
|
const begin = () => { const current = native.read(snapshot.surface); if (current) instance.tracker.begin(current); };
|
|
37
41
|
instance.unsubscribe = native.subscribe(snapshot, begin);
|
|
38
42
|
instances.set(snapshot.surface, instance);
|
|
39
43
|
}
|
|
40
44
|
if (selected || instance.tracker.accepted(snapshot)) snapshot.expand();
|
|
41
45
|
instance.historyTracker.open(snapshot);
|
|
46
|
+
snapshot.surface.toggleAttribute('data-mcx-quick-chat-compact', !snapshot.expanded);
|
|
42
47
|
if (instance.projectRail !== snapshot.projectRail) {
|
|
43
48
|
releaseProjectRail(instance);
|
|
44
49
|
instance.projectRail = snapshot.projectRail;
|
|
@@ -54,25 +59,34 @@ function sync() {
|
|
|
54
59
|
const minimize = snapshot.minimize;
|
|
55
60
|
if (instance.minimize !== minimize) {
|
|
56
61
|
instance.minimize?.removeAttribute('data-mcx-quick-chat-minimize'); instance.minimize = minimize;
|
|
57
|
-
minimize?.setAttribute('data-mcx-quick-chat-minimize','');
|
|
62
|
+
if (snapshot.standalone) minimize?.setAttribute('data-mcx-quick-chat-minimize','');
|
|
58
63
|
}
|
|
59
|
-
|
|
60
|
-
|
|
64
|
+
const kind=snapshot.showHistory ? 'history' : snapshot.showClose && shortcut.canClose() ? 'close' : null;
|
|
65
|
+
const anchor=kind==='close' ? snapshot.closeAnchor : minimize;
|
|
66
|
+
if (!minimize || !anchor || !kind) { instance.button?.remove(); instance.button = null; continue; }
|
|
67
|
+
if (!instance.button?.isConnected || instance.button.parentElement !== anchor.parentElement || instance.button.nextElementSibling!==anchor || instance.buttonKind!==kind) {
|
|
61
68
|
instance.button?.remove();
|
|
62
69
|
const button = minimize.cloneNode(false);
|
|
63
70
|
for (const attr of ['id','disabled','aria-disabled','aria-describedby','aria-expanded','title']) button.removeAttribute(attr);
|
|
64
71
|
button.removeAttribute('data-mcx-quick-chat-minimize');
|
|
65
72
|
button.type = 'button';
|
|
66
|
-
button.setAttribute(
|
|
67
|
-
button.innerHTML = config.resources
|
|
73
|
+
button.setAttribute(`data-mcx-quick-chat-${kind}`, '');
|
|
74
|
+
button.innerHTML = config.resources[kind];
|
|
68
75
|
const icon = button.querySelector('svg');
|
|
69
76
|
if (icon) { icon.setAttribute('width','16'); icon.setAttribute('height','16'); icon.setAttribute('aria-hidden','true'); }
|
|
70
|
-
button.addEventListener('pointerdown', event => event.preventDefault(), {signal});
|
|
71
|
-
button.addEventListener('click',
|
|
72
|
-
|
|
77
|
+
button.addEventListener('pointerdown', event => {event.preventDefault();event.stopPropagation();}, {signal});
|
|
78
|
+
button.addEventListener('click', event => {
|
|
79
|
+
event.preventDefault(); event.stopPropagation();
|
|
80
|
+
instance.tracker.clear(); instance.historyTracker.clear();
|
|
81
|
+
const current=native.read(snapshot.surface);
|
|
82
|
+
if (!current || current.sessionId!==instance.sessionId) return;
|
|
83
|
+
if (kind==='history') current.history();
|
|
84
|
+
else if (!shortcut.close(snapshot.surface,current.sessionId)) config.codex.report('quick-chat-close',false,'quick-chat-close-action-unavailable');
|
|
85
|
+
}, {signal});
|
|
86
|
+
anchor.before(button); instance.button = button; instance.buttonKind=kind;
|
|
73
87
|
}
|
|
74
88
|
if (instance.button.className !== minimize.className) instance.button.className = minimize.className;
|
|
75
|
-
const label = text('Chat history', '聊天历史');
|
|
89
|
+
const label = kind==='history' ? text('Chat history', '聊天历史') : text('Close Quick Chat', '关闭快捷聊天');
|
|
76
90
|
if (instance.button.getAttribute('aria-label') !== label) {
|
|
77
91
|
instance.button.setAttribute('aria-label',label); instance.button.setAttribute('data-minecodex-tooltip',label);
|
|
78
92
|
}
|
|
@@ -91,14 +105,15 @@ document.addEventListener('submit',begin,{capture:true,signal});
|
|
|
91
105
|
const observer = new MutationObserver(schedule);
|
|
92
106
|
observer.observe(document.documentElement,{subtree:true,childList:true,attributes:true,attributeFilter:['aria-hidden','inert','hidden','lang']});
|
|
93
107
|
sync();
|
|
108
|
+
void shortcut.ready.then(schedule);
|
|
94
109
|
config.registerSelfCheck?.(() => {
|
|
95
|
-
const snapshots = native.surfaces(), anchors = snapshots.filter(item => item.showHistory);
|
|
96
|
-
const
|
|
110
|
+
const snapshots = native.surfaces(), anchors = snapshots.filter(item => item.showHistory || item.showClose);
|
|
111
|
+
const controls = anchors.every(item => instances.get(item.surface)?.button?.isConnected), entry = sidebar.status();
|
|
97
112
|
const applicable = anchors.length > 0 || entry.applicable;
|
|
98
|
-
const available = (!anchors.length ||
|
|
113
|
+
const available = (!anchors.length || controls) && (!entry.applicable || entry.available);
|
|
99
114
|
return {state:applicable ? available ? 'available' : 'unavailable' : 'not-applicable',
|
|
100
115
|
reason:applicable ? available ? null : 'quick-chat-controls-unavailable' : 'no-classic-quick-chat-floating-controls',
|
|
101
|
-
checks:[...(anchors.length ? [{name:'quick-chat-
|
|
116
|
+
checks:[...(anchors.length ? [{name:'quick-chat-controls',available:controls}] : []), ...(entry.applicable ? [{name:'quick-chat-sidebar-entry',available:entry.available}] : [])],
|
|
102
117
|
scopes:snapshots.map(item=>({name:'quick-chat',element:item.surface}))};
|
|
103
118
|
});
|
|
104
119
|
return () => { disposed = true; shortcut.dispose(); sidebar.dispose(); observer.disconnect(); for (const instance of instances.values()) remove(instance); instances.clear(); style.remove(); };
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import {quickChatLifecycleActions, quickChatScope} from "./adapters/codex/lifecycle.mjs";
|
|
1
2
|
import {quickChatHistoryVisible} from "./adapters/codex/history.mjs";
|
|
2
3
|
import { readFile } from "node:fs/promises";
|
|
3
4
|
import { createQuickChatAdapter } from "./adapters/codex/renderer.mjs";
|
|
@@ -9,7 +10,7 @@ import { createHistoryTracker } from "./domain/history.mjs";
|
|
|
9
10
|
export async function createPageScriptSource() {
|
|
10
11
|
const source = await readFile(new URL("./frontend/entry.mjs", import.meta.url), "utf8");
|
|
11
12
|
const functions = [quickChatHistoryVisible, loadQuickChatSidebarIcon, createQuickChatSidebarAdapter, installQuickChatSidebar,
|
|
12
|
-
quickChatOpenExport, quickChatLifecycleExports, quickChatNativeCommand, installQuickChatShortcut, createQuickChatAdapter,
|
|
13
|
+
quickChatLifecycleActions, quickChatScope, quickChatOpenExport, quickChatLifecycleExports, quickChatNativeCommand, installQuickChatShortcut, createQuickChatAdapter,
|
|
13
14
|
createSubmissionTracker, createHistoryTracker];
|
|
14
15
|
return functions.map(fn => `const ${fn.name}=${fn.toString()};`).join('\n') + '\n' + source;
|
|
15
16
|
}
|
|
@@ -44,6 +44,13 @@ function findAvatar(rail) {
|
|
|
44
44
|
?? buttons.findLast(button => button.querySelector('img[alt], img[src], [data-avatar]'))
|
|
45
45
|
?? null;
|
|
46
46
|
}
|
|
47
|
+
function findUsage(rail) {
|
|
48
|
+
// 原生图表的轨道 token 是语义锚点,避免依赖构建生成的 class 或界面语言。
|
|
49
|
+
return [...(rail?.querySelectorAll("button") ?? [])].find(button =>
|
|
50
|
+
!button.hasAttribute?.("data-minecodex-usage-meter") &&
|
|
51
|
+
(button.querySelector?.('[stroke="var(--color-usage-meter-track)"]') ||
|
|
52
|
+
/^(Usage|用量|使用量|使用情况|使用情況)$/.test(button.getAttribute("aria-label") ?? ""))) ?? null;
|
|
53
|
+
}
|
|
47
54
|
function avatarAnchor(avatar, rail) {
|
|
48
55
|
// 头像内部是横向行;在最近的原生纵向容器中插入独立一项。
|
|
49
56
|
let anchor = avatar;
|
|
@@ -58,13 +65,16 @@ function avatarAnchor(avatar, rail) {
|
|
|
58
65
|
unsubscribe?.(); client=context.client;
|
|
59
66
|
unsubscribe=client?.getQueryCache().subscribe(event=>{ if (event.query?.queryKey?.[0] === "rate-limit-status") invalidate(); });
|
|
60
67
|
}
|
|
61
|
-
const windows=currentWindows(context), avatar=findAvatar(rail);
|
|
62
|
-
const available=Boolean(avatar && context.router && windows);
|
|
68
|
+
const windows=currentWindows(context), avatar=findAvatar(rail), usage=findUsage(rail);
|
|
69
|
+
const available=Boolean((usage || avatar) && context.router && windows);
|
|
63
70
|
codex.report("usage-meter",available,available ? null : "account-quota-or-anchor-unavailable");
|
|
64
|
-
return { rail,avatar,windows,available,anchorAvailable:Boolean(avatar && context.router),
|
|
71
|
+
return { rail,avatar,usage,windows,available,anchorAvailable:Boolean((usage || avatar) && context.router),
|
|
65
72
|
applicable:context.auth?.isLoading ? null : context.auth ? ["chatgpt","chatgptAuthTokens"].includes(context.auth.authMethod) : null };
|
|
66
73
|
}
|
|
67
74
|
function open(rail) { const {router}=nativeContext(rail); return router?.navigate("/settings/usage?tab=overview",{state:router.state?.location?.state}); }
|
|
68
75
|
signal.addEventListener("abort",()=>unsubscribe?.(),{once:true});
|
|
69
|
-
return { read,open,anchor:avatarAnchor
|
|
76
|
+
return { read,open,anchor:avatarAnchor,
|
|
77
|
+
conceal: usage => usage?.setAttribute("data-minecodex-native-usage", ""),
|
|
78
|
+
reveal: usage => usage?.removeAttribute("data-minecodex-native-usage") };
|
|
79
|
+
|
|
70
80
|
}
|
|
@@ -17,9 +17,10 @@ let hoverTimer = null;
|
|
|
17
17
|
const style = document.createElement("style");
|
|
18
18
|
style.id = STYLE_ID;
|
|
19
19
|
style.textContent = `
|
|
20
|
-
[${MARKER}] { position:relative; display:flex; align-items:center; justify-content:center; flex:none; color:var(--mcx-color-text-tertiary); -webkit-app-region:no-drag; }
|
|
20
|
+
[${MARKER}] { position:relative; display:flex; align-items:center; justify-content:center; flex:none; color:var(--mcx-color-usage-meter-fill,var(--mcx-color-text-tertiary)); -webkit-app-region:no-drag; }
|
|
21
|
+
[data-minecodex-native-usage] { display:none !important; }
|
|
21
22
|
[${MARKER}] .mcx-usage-ring { display:block; width:24px; height:24px; overflow:visible; }
|
|
22
|
-
[${MARKER}] .mcx-usage-number { position:absolute; inset:0; display:flex; align-items:center; justify-content:center; color:
|
|
23
|
+
[${MARKER}] .mcx-usage-number { position:absolute; inset:0; display:flex; align-items:center; justify-content:center; color:var(--mcx-color-usage-meter-fill,var(--mcx-color-text-tertiary)); font-family:var(--mcx-font-sans-default); font-size:calc(var(--mcx-text-xs) * 2 / 3); line-height:1; font-weight:var(--font-weight-medium,500); letter-spacing:0; font-variant-numeric:tabular-nums; pointer-events:none; }
|
|
23
24
|
#${TIP_ID} { width:max-content; max-width:calc(100vw - 16px); }
|
|
24
25
|
#${TIP_ID}[hidden] { display:none; }
|
|
25
26
|
#${TIP_ID} dl { display:grid; grid-template-columns:auto auto; gap:2px 16px; margin:0; }
|
|
@@ -31,13 +32,14 @@ style.textContent = `
|
|
|
31
32
|
document.head.append(style);
|
|
32
33
|
|
|
33
34
|
function svgNode(name, attributes) {
|
|
35
|
+
if (name === "path" || name === "circle") attributes = { ...attributes, "vector-effect": "non-scaling-stroke" };
|
|
34
36
|
const node = document.createElementNS("http://www.w3.org/2000/svg", name);
|
|
35
37
|
for (const [key, value] of Object.entries(attributes)) node.setAttribute(key, String(value));
|
|
36
38
|
return node;
|
|
37
39
|
}
|
|
38
40
|
|
|
39
41
|
function quotaArc(start, end, clockwise) {
|
|
40
|
-
const radius =
|
|
42
|
+
const radius = 44 / 3;
|
|
41
43
|
const point = angle => {
|
|
42
44
|
const radians = (angle - 90) * Math.PI / 180;
|
|
43
45
|
return [16 + radius * Math.cos(radians), 16 + radius * Math.sin(radians)];
|
|
@@ -52,8 +54,8 @@ function draw(windows) {
|
|
|
52
54
|
if (windows.fiveHour != null) {
|
|
53
55
|
// 左下、右下各留 24° 圆头中心间距;两段都从左下切口扣减。
|
|
54
56
|
for (const [key, start, end, clockwise] of [["fiveHour", 237, 483, true], ["weekly", 213, 147, false]]) {
|
|
55
|
-
const group = svgNode("g", { "data-window": key, fill: "none", stroke: "currentColor", "stroke-width":
|
|
56
|
-
group.append(svgNode("path", { d: quotaArc(start, end, clockwise),
|
|
57
|
+
const group = svgNode("g", { "data-window": key, fill: "none", stroke: "currentColor", "stroke-width": 2, "stroke-linecap": "round" });
|
|
58
|
+
group.append(svgNode("path", { d: quotaArc(start, end, clockwise), stroke: "var(--mcx-color-usage-meter-track,color-mix(in srgb,currentColor 18%,transparent))" }));
|
|
57
59
|
if (windows[key] > 0) {
|
|
58
60
|
const remainingStart = start + (end - start) * (1 - windows[key] / 100);
|
|
59
61
|
group.append(svgNode("path", { d: quotaArc(remainingStart, end, clockwise) }));
|
|
@@ -61,9 +63,9 @@ function draw(windows) {
|
|
|
61
63
|
svg.append(group);
|
|
62
64
|
}
|
|
63
65
|
} else {
|
|
64
|
-
const key = "weekly", radius =
|
|
65
|
-
const group = svgNode("g", { "data-window": key, fill: "none", stroke: "currentColor", "stroke-width":
|
|
66
|
-
group.append(svgNode("circle", { cx: 16, cy: 16, r: radius,
|
|
66
|
+
const key = "weekly", radius = 44 / 3;
|
|
67
|
+
const group = svgNode("g", { "data-window": key, fill: "none", stroke: "currentColor", "stroke-width": 2 });
|
|
68
|
+
group.append(svgNode("circle", { cx: 16, cy: 16, r: radius, stroke: "var(--mcx-color-usage-meter-track,color-mix(in srgb,currentColor 18%,transparent))" }));
|
|
67
69
|
if (windows[key] > 0) group.append(svgNode("circle", {
|
|
68
70
|
cx: 16, cy: 16, r: radius, pathLength: 100,
|
|
69
71
|
"stroke-dasharray": `${windows[key]} 100`, "stroke-linecap": "round", transform: "rotate(-90 16 16)",
|
|
@@ -127,6 +129,7 @@ function hideTooltip() {
|
|
|
127
129
|
}
|
|
128
130
|
function removeControl() {
|
|
129
131
|
hideTooltip();
|
|
132
|
+
nativeUsage.reveal(record?.usage);
|
|
130
133
|
record?.button.remove();
|
|
131
134
|
record?.tooltip.remove();
|
|
132
135
|
record = null;
|
|
@@ -136,21 +139,23 @@ function removeControl() {
|
|
|
136
139
|
|
|
137
140
|
|
|
138
141
|
|
|
139
|
-
function mount(avatar, rail) {
|
|
140
|
-
//
|
|
141
|
-
const button = avatar.cloneNode(false);
|
|
142
|
+
function mount(avatar, rail, usage) {
|
|
143
|
+
// 复用原生 Usage 热区;旧版宿主才使用 Avatar 模板。
|
|
144
|
+
const button = (usage || avatar).cloneNode(false);
|
|
142
145
|
for (const attribute of [...button.attributes]) {
|
|
143
|
-
if (attribute.name.startsWith("aria-") || ["id", "title", "data-state", "data-sidebar-destination", "data-testid"].includes(attribute.name)) button.removeAttribute(attribute.name);
|
|
146
|
+
if (attribute.name.startsWith("aria-") || ["id", "title", "data-state", "data-sidebar-destination", "data-testid", "data-minecodex-native-usage"].includes(attribute.name)) button.removeAttribute(attribute.name);
|
|
144
147
|
}
|
|
145
148
|
button.setAttribute(MARKER, "");
|
|
149
|
+
config.ui.theme.bindScope(button, ["--color-usage-meter-fill", "--color-usage-meter-track"]);
|
|
146
150
|
const tooltip = document.createElement("div");
|
|
147
151
|
tooltip.id = TIP_ID;
|
|
148
152
|
config.ui.tooltipSurface(tooltip);
|
|
149
153
|
tooltip.setAttribute("role", "tooltip");
|
|
150
154
|
tooltip.hidden = true;
|
|
151
|
-
nativeUsage.anchor(avatar, rail).before(button);
|
|
155
|
+
(usage || nativeUsage.anchor(avatar, rail)).before(button);
|
|
156
|
+
nativeUsage.conceal(usage);
|
|
152
157
|
document.body.append(tooltip);
|
|
153
|
-
record = { button, tooltip, rail, avatar, signature: null };
|
|
158
|
+
record = { button, tooltip, rail, avatar, usage, signature: null };
|
|
154
159
|
button.addEventListener("pointerenter", () => { hoverTimer = setTimeout(showTooltip, 350); });
|
|
155
160
|
button.addEventListener("pointerleave", hideTooltip);
|
|
156
161
|
button.addEventListener("focus", showTooltip);
|
|
@@ -165,10 +170,10 @@ function mount(avatar, rail) {
|
|
|
165
170
|
function sync() {
|
|
166
171
|
queued = false;
|
|
167
172
|
if (signal.aborted) return;
|
|
168
|
-
const { rail,avatar,windows,available } = nativeUsage.read();
|
|
173
|
+
const { rail,avatar,usage,windows,available } = nativeUsage.read();
|
|
169
174
|
if (!available) { removeControl(); return; }
|
|
170
|
-
if (record?.avatar !== avatar || !record?.button.isConnected) { removeControl(); mount(avatar, rail); }
|
|
171
|
-
else if (record.button.nextElementSibling !== nativeUsage.anchor(avatar, rail)) nativeUsage.anchor(avatar, rail).before(record.button);
|
|
175
|
+
if (record?.avatar !== avatar || record?.usage !== usage || !record?.button.isConnected) { removeControl(); mount(avatar, rail, usage); }
|
|
176
|
+
else if (record.button.nextElementSibling !== (usage || nativeUsage.anchor(avatar, rail))) (usage || nativeUsage.anchor(avatar, rail)).before(record.button);
|
|
172
177
|
const signature = JSON.stringify(windows);
|
|
173
178
|
if (record.signature !== signature) { record.signature = signature; draw(windows); }
|
|
174
179
|
}
|
|
@@ -196,8 +201,8 @@ config.registerSelfCheck?.(() => {
|
|
|
196
201
|
const snapshot=nativeUsage.read();
|
|
197
202
|
const available=Boolean(snapshot.available && record?.button.isConnected);
|
|
198
203
|
return {state:available ? "available" : snapshot.applicable === false ? "not-applicable" : !snapshot.anchorAvailable && snapshot.rail ? "unavailable" : "unknown",reason:available ? null : "quota-data-or-anchor-unavailable",
|
|
199
|
-
checks:[{name:"
|
|
200
|
-
scopes:[{name:"account",element:snapshot.avatar}]};
|
|
204
|
+
checks:[{name:"usage-anchor",available:Boolean(snapshot.usage || snapshot.avatar)},{name:"account-quota",available:Boolean(snapshot.windows)},{name:"usage-meter-mount",available:Boolean(record?.button.isConnected)}],
|
|
205
|
+
scopes:[{name:"account",element:snapshot.usage || snapshot.avatar}]};
|
|
201
206
|
});
|
|
202
207
|
|
|
203
208
|
return cleanup;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "minecodex",
|
|
3
|
-
"version": "2.0.
|
|
3
|
+
"version": "2.0.46",
|
|
4
4
|
"description": "Lightweight, local-first plugins for the Codex desktop app.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -71,6 +71,10 @@
|
|
|
71
71
|
"features/markdown-reading/codex-feature.json",
|
|
72
72
|
"features/markdown-reading/vendor/lucide-LICENSE",
|
|
73
73
|
"features/markdown-reading/assets",
|
|
74
|
+
"features/files-auto-refresh/src",
|
|
75
|
+
"features/files-auto-refresh/codex-feature.json",
|
|
76
|
+
"features/files-auto-refresh/assets",
|
|
77
|
+
"features/files-auto-refresh/vendor/lucide-LICENSE",
|
|
74
78
|
"features/file-navigation/src",
|
|
75
79
|
"features/file-navigation/codex-feature.json",
|
|
76
80
|
"features/file-navigation/assets",
|
|
@@ -92,7 +96,7 @@
|
|
|
92
96
|
},
|
|
93
97
|
"scripts": {
|
|
94
98
|
"start": "node packages/cli/bin/mcx.mjs serve",
|
|
95
|
-
"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/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/composer-expand/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/mermaid-diagrams/test/*.test.mjs",
|
|
99
|
+
"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/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/composer-expand/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 features/files-auto-refresh/test/*.test.mjs packages/plugin/test/*.test.mjs features/mermaid-diagrams/test/*.test.mjs",
|
|
96
100
|
"test:cli": "node --test packages/cli/test/*.test.mjs",
|
|
97
101
|
"test:runtime": "node --test packages/runtime-host/test/*.test.mjs",
|
|
98
102
|
"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", "session-status", "usage-meter", "sidebar-focus", "sites", "markdown-reading", "mermaid-diagrams", "file-navigation", "quick-chat", "composer-expand"]);
|
|
5
|
+
export const FEATURE_IDS = Object.freeze(["model-slider", "notes", "images", "session-status", "usage-meter", "sidebar-focus", "sites", "markdown-reading", "mermaid-diagrams", "file-navigation", "files-auto-refresh", "quick-chat", "composer-expand"]);
|
|
6
6
|
export const DEFAULT_CONFIG = Object.freeze({
|
|
7
7
|
schemaVersion: 1,
|
|
8
8
|
features: Object.freeze({
|
|
@@ -16,6 +16,7 @@ export const DEFAULT_CONFIG = Object.freeze({
|
|
|
16
16
|
"markdown-reading": true,
|
|
17
17
|
"mermaid-diagrams": false,
|
|
18
18
|
"file-navigation": true,
|
|
19
|
+
"files-auto-refresh": true,
|
|
19
20
|
"quick-chat": true,
|
|
20
21
|
"composer-expand": true,
|
|
21
22
|
}),
|
|
@@ -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", "mermaid-diagrams", "file-navigation", "images", "sites", "quick-chat", "composer-expand", "sidebar-focus"];
|
|
17
|
+
const DASHBOARD_FEATURE_ORDER = ["model-slider", "notes", "usage-meter", "session-status", "markdown-reading", "mermaid-diagrams", "file-navigation", "files-auto-refresh", "images", "sites", "quick-chat", "composer-expand", "sidebar-focus"];
|
|
18
18
|
const CONTENT_TYPES = new Map([
|
|
19
19
|
[".css", "text/css; charset=utf-8"],
|
|
20
20
|
[".html", "text/html; charset=utf-8"],
|
|
@@ -102,6 +102,8 @@ export const CODEX_THEME_TOKENS = Object.freeze([
|
|
|
102
102
|
"--color-text-primary",
|
|
103
103
|
"--color-text-secondary",
|
|
104
104
|
"--color-text-tertiary",
|
|
105
|
+
"--color-usage-meter-fill",
|
|
106
|
+
"--color-usage-meter-track",
|
|
105
107
|
"--color-border-overlay",
|
|
106
108
|
"--color-ring",
|
|
107
109
|
"--color-dialog-overlay",
|