minecodex 2.0.8 → 2.0.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (59) hide show
  1. package/README.md +18 -4
  2. package/apps/dashboard/app.js +59 -10
  3. package/apps/dashboard/assets/diagnostics.js +18 -0
  4. package/apps/dashboard/index.html +5 -0
  5. package/apps/dashboard/styles.css +5 -1
  6. package/assets/brand/README.md +2 -2
  7. package/features/hide-upsell-banner/src/hide-upsell-banner.mjs +2 -0
  8. package/features/images/src/adapters/codex/image-sources.mjs +8 -1
  9. package/features/images/src/adapters/codex/native-image-source.mjs +2 -2
  10. package/features/images/src/frontend/local-image-source.mjs +7 -0
  11. package/features/images/src/library-prompt-page.mjs +9 -0
  12. package/features/model-slider/src/adapters/codex/controller.mjs +14 -1
  13. package/features/model-slider/src/frontend/install.mjs +4 -1
  14. package/features/notes/src/frontend/entry.mjs +10 -0
  15. package/features/session-status/src/session-status.mjs +11 -0
  16. package/features/sidebar-focus/src/sidebar-focus.mjs +8 -0
  17. package/features/sites/src/frontend/entry.mjs +8 -0
  18. package/features/usage-meter/src/adapters/codex/renderer.mjs +2 -1
  19. package/features/usage-meter/src/usage-meter.mjs +8 -0
  20. package/package.json +1 -14
  21. package/packages/cli/src/commands.mjs +6 -1
  22. package/packages/cli/src/control-server.mjs +4 -1
  23. package/packages/cli/src/platform.mjs +10 -0
  24. package/packages/cli/src/runtime-manager.mjs +11 -3
  25. package/packages/codex-adapter/src/renderer/environment.mjs +6 -1
  26. package/packages/runtime-host/src/codex-injection.mjs +49 -16
  27. package/packages/runtime-host/src/codex-runtime.mjs +51 -0
  28. package/packages/runtime-host/src/feature-registry.mjs +2 -3
  29. package/packages/runtime-host/src/main.mjs +8 -4
  30. package/packages/runtime-host/src/renderer-self-check.mjs +56 -0
  31. package/packages/ui/src/components/composer-menu.mjs +7 -2
  32. package/packages/ui/src/native/theme.mjs +16 -1
  33. package/DESIGN.md +0 -192
  34. package/apps/dashboard/DESIGN.md +0 -67
  35. package/features/hide-upsell-banner/README.md +0 -30
  36. package/features/images/DESIGN.md +0 -151
  37. package/features/images/README.md +0 -37
  38. package/features/model-slider/DESIGN.md +0 -36
  39. package/features/model-slider/README.md +0 -45
  40. package/features/notes/DESIGN.md +0 -74
  41. package/features/notes/README.md +0 -33
  42. package/features/session-status/DESIGN.md +0 -45
  43. package/features/session-status/README.md +0 -43
  44. package/features/sidebar-focus/DESIGN.md +0 -17
  45. package/features/sites/DESIGN.md +0 -82
  46. package/features/sites/README.md +0 -31
  47. package/features/unread-inbox/DESIGN.md +0 -41
  48. package/features/unread-inbox/codex-feature.json +0 -23
  49. package/features/unread-inbox/src/adapters/codex/modules.mjs +0 -58
  50. package/features/unread-inbox/src/adapters/codex/renderer.mjs +0 -198
  51. package/features/unread-inbox/src/domain/notifications.mjs +0 -21
  52. package/features/unread-inbox/src/injection-source.mjs +0 -9
  53. package/features/unread-inbox/src/unread-inbox.mjs +0 -307
  54. package/features/usage-meter/DESIGN.md +0 -49
  55. package/features/usage-meter/README.md +0 -19
  56. package/packages/cli/native/README.md +0 -49
  57. package/packages/codex-adapter/README.md +0 -16
  58. package/packages/runtime-host/README.md +0 -213
  59. package/packages/ui/DESIGN.md +0 -38
@@ -1,198 +0,0 @@
1
- export function createThreadAdapter(codex) {
2
- function isUnread(row) {
3
- let fiber = codex.currentFiber(row);
4
- for (let depth = 0; fiber && depth < 35; depth++, fiber = fiber.return) {
5
- if (typeof fiber.memoizedProps?.isUnread === "boolean") return fiber.memoizedProps.isUnread;
6
- }
7
- return null;
8
- }
9
- function isWorking(row) {
10
- let fiber = codex.currentFiber(row);
11
- for (let depth = 0; fiber && depth < 35; depth++, fiber = fiber.return) {
12
- const status = fiber.memoizedProps?.statusState;
13
- if (status?.type) return status.type === "loading";
14
- }
15
- return null;
16
- }
17
- function projectForRow(row) {
18
- let fiber = codex.currentFiber(row);
19
- for (let depth = 0; fiber && depth < 35; depth++, fiber = fiber.return) {
20
- const group = fiber.memoizedProps?.secondaryContent?.props?.projectGroup;
21
- if (group?.projectId) return { id:group.projectId, name:group.label };
22
- }
23
- return null;
24
- }
25
- function attentionState(row) {
26
- let fiber = codex.currentFiber(row);
27
- for (let depth = 0; fiber && depth < 35; depth++, fiber = fiber.return) {
28
- const props = fiber.memoizedProps;
29
- // 复用原生状态契约,不从摘要文案推断待确认状态。
30
- if (props?.chips?.some(chip => chip.id === "awaiting-approval")) return "approval";
31
- if (props?.statusPill?.label?.props?.id === "codex.localTaskRow.needsInput") return "input";
32
- }
33
- return null;
34
- }
35
- function threadDetails(row) {
36
- let fiber = codex.currentFiber(row);
37
- let summary, hostConfig;
38
- for (let depth = 0; fiber && depth < 35; depth++, fiber = fiber.return) {
39
- summary ||= fiber.memoizedProps?.threadSummary;
40
- hostConfig ||= fiber.memoizedProps?.hoverCardHostConfig;
41
- }
42
- const hostId = summary?.hostId || row.dataset.appActionSidebarThreadHostId || "local";
43
- const nativeTime = summary?.recencyAt ?? summary?.updatedAt ?? null;
44
- return {
45
- title:row.dataset.appActionSidebarThreadTitle || row.querySelector("[data-thread-title]")?.textContent || "",
46
- summary:row.querySelector("[data-thread-secondary-title]")?.textContent || "",
47
- // 与原生 Thread hover card 相同,优先使用任务 recency 时间。
48
- timestamp:nativeTime,
49
- hostId, hostName:hostConfig?.display_name || hostId,
50
- project:projectForRow(row),
51
- working:isWorking(row),
52
- attention:attentionState(row),
53
- };
54
- }
55
- function threadRecord(row) {
56
- codex.report("thread-status",Boolean(codex.currentFiber(row)),codex.currentFiber(row) ? null : "thread-props-unavailable");
57
- let fiber = codex.currentFiber(row);
58
- let activate = null;
59
- for (let depth = 0; fiber && depth < 35; depth++, fiber = fiber.return) {
60
- const props = fiber.memoizedProps;
61
- if (props?.statusState && typeof props.onClick === "function") { activate = props.onClick; break; }
62
- }
63
- return { ...threadDetails(row), key:row.dataset.appActionSidebarThreadId, unread:isUnread(row), activate, spinner:row.querySelector?.('[class~="motion-safe:animate-spin"]')?.outerHTML ?? "" };
64
- }
65
- return {read:threadRecord};
66
- }
67
-
68
- export function notificationVisibilityStyles() {
69
- // 原生展开状态变化时同步隐藏,不等待 MutationObserver 或每秒刷新。
70
- return ':root:has([data-sidebar-destination="builtin:home"][aria-current="page"]):has([data-app-shell-sidebar-trigger][aria-expanded="true"]) [data-minecodex-unread] { display:none; }';
71
- }
72
-
73
- // 会话侧栏是可卸载视图,通知从同一原生 store 读取完整目录。
74
- export function createNotificationAdapter(codex, load, invalidate) {
75
- let bindings=null, loading=null, retryAt=0, disposed=false;
76
- let watchedScope=null, stopWatching=null;
77
- function watch(scope) {
78
- if(watchedScope===scope)return;
79
- stopWatching?.();watchedScope=scope;
80
- stopWatching=scope.watch(({get})=>{
81
- const catalog=get(bindings.catalog);
82
- get(bindings.projects,"codex");
83
- for(const key of catalog.allSidebarThreadKeys){
84
- const summary=get(bindings.thread,key)?.summary;
85
- if(!summary?.conversationId||!summary.hostId)continue;
86
- const args={conversationId:summary.conversationId,hostId:summary.hostId};
87
- get(bindings.title,{threadId:summary.conversationId,hostId:summary.hostId});
88
- get(bindings.activityTurns,summary.conversationId);
89
- get(bindings.status,args);get(bindings.attention,args);
90
- get(bindings.unread,{conversationId:summary.conversationId,fallbackRootUnread:summary.hasUnreadTurn});
91
- }
92
- invalidate();
93
- });
94
- }
95
- function activitySummary(scope,conversationId) {
96
- const turns=scope.get(bindings.activityTurns,conversationId);
97
- // 与 View Activity 相同:从最近的 agentMessage 读取原生纯文本预览。
98
- for(let i=(turns?.length??0)-1;i>=0;i--){
99
- const items=turns[i].items;
100
- for(let j=items.length-1;j>=0;j--){
101
- if(items[j].type!=="agentMessage")continue;
102
- const text=bindings.previewText(items[j].text);
103
- if(text.length)return text;
104
- }
105
- }
106
- return "";
107
- }
108
- function scopeFor(rail) {
109
- let fiber=codex.currentFiber(rail);
110
- for (let depth=0;fiber&&depth<160;depth++,fiber=fiber.return) {
111
- for (const values of fiber.updateQueue?.memoCache?.data ?? []) {
112
- const scope=values.find(value=>typeof value?.get==="function" && value.node?.store && value.query && value.scope);
113
- if (scope) return scope;
114
- }
115
- }
116
- return null;
117
- }
118
- function ensure() {
119
- if (bindings || loading || Date.now()<retryAt || disposed) return;
120
- loading=load().then(value=>{if(!disposed){bindings=value;invalidate();}}).catch(()=>{
121
- retryAt=Date.now()+30000;
122
- codex.report("task-notification-catalog",false,"native-module-contract-unavailable");
123
- }).finally(()=>{loading=null;});
124
- }
125
- function layout() {
126
- const rail=codex.rail();
127
- if (!rail) return {available:false};
128
- // 桌面端使用内存路由,window.location 不随页面跳转变化。
129
- let pageKey=null;
130
- codex.props(rail,props=>{
131
- pageKey=props.routeContext?.outlet?.props?.match?.pathname??null;
132
- return pageKey!==null;
133
- });
134
- const home=rail.querySelector('[data-sidebar-destination="builtin:home"]');
135
- const isVisible=e=>!!e && !e.closest('[inert],[aria-hidden="true"]') && e.checkVisibility() && e.getBoundingClientRect().width>0;
136
- // 展开状态先于列表完成布局;先隐藏通知,避免过渡期间与侧栏重叠。
137
- const homeExpanded=home?.getAttribute("aria-current")==="page"
138
- && !!document.querySelector('[data-app-shell-sidebar-trigger][aria-expanded="true"]');
139
- const homeSidebar=homeExpanded || [...document.querySelectorAll('[data-app-action-sidebar-scroll]')]
140
- .some(list=>isVisible(list.closest('[data-slate-sidebar-content]')) && isVisible(list));
141
- const peeking=home?.getAttribute("aria-expanded")==="true" || !!document.querySelector('[data-slate-sidebar-peeking="true"]');
142
- const titlebar=[...document.querySelectorAll('[data-app-shell-titlebar-slot="main"]')].find(isVisible);
143
- const rect=rail.getBoundingClientRect(), titleRect=titlebar?.getBoundingClientRect();
144
- return { available:true, pageKey, visible:!homeSidebar&&!peeking,
145
- left:rect.right+8, top:Math.max(rect.top,titleRect?.bottom??0)+8 };
146
- }
147
- function snapshot() {
148
- ensure();
149
- const rail=codex.rail(), scope=rail && scopeFor(rail);
150
- if (!bindings || !scope) {
151
- stopWatching?.();stopWatching=null;watchedScope=null;
152
- if (!scope) codex.report("task-notification-catalog",false,"native-scope-unavailable");
153
- return null;
154
- }
155
- try {
156
- watch(scope);
157
- const catalog=scope.get(bindings.catalog);
158
- const projectCatalog=scope.get(bindings.projects,"codex");
159
- const projects=new Map();
160
- for(const group of projectCatalog.projectGroups) for(const key of group.threadKeys) projects.set(key,{id:group.projectId,name:group.label});
161
- const rows=new Map([...document.querySelectorAll('[data-app-action-sidebar-thread-row]')].map(row=>[row.dataset.appActionSidebarThreadId,row]));
162
- const rowAdapter=createThreadAdapter(codex);
163
- const records=catalog.allSidebarThreadKeys.flatMap(key=>{
164
- const item=scope.get(bindings.thread,key), summary=item?.summary;
165
- // 目录中的待创建占位还没有可导航的正式会话。
166
- if (!summary?.conversationId || !summary.hostId) return [];
167
- const {conversationId,hostId}=summary, args={conversationId,hostId};
168
- const attention=scope.get(bindings.attention,args);
169
- const status=scope.get(bindings.status,args);
170
- const pending=scope.get(bindings.pending,{hostId,threadId:conversationId})?.pendingRequest;
171
- const resolution=scope.get(bindings.resolution,args);
172
- const countdown=attention==="response" && pending?.kind==="userInput" && resolution?.requestId===pending.userInputRequestId && resolution?.resolutionState?.status==="scheduled"
173
- ? {deadlineMs:resolution.resolutionState.deadlineMs,durationMs:bindings.countdownDuration} : null;
174
- const unread=scope.get(bindings.unread,{conversationId,fallbackRootUnread:summary.hasUnreadTurn});
175
- const row=rows.get(key), mounted=row ? rowAdapter.read(row) : null;
176
- return [{key,threadId:conversationId,hostId,title:scope.get(bindings.title,{threadId:conversationId,hostId})??summary.title??"",
177
- summary:activitySummary(scope,conversationId),project:projects.get(key)??null,
178
- timestamp:unread.unreadMessageAt??summary.recencyAt??summary.updatedAt??null,
179
- unread:unread.hasUnread,working:status==="loading"&&attention!=="response",
180
- attention:attention==="approval"?"approval":attention==="response"?"input":null,
181
- countdown,spinner:mounted?.spinner||"",activate:()=>bindings.navigate(scope,conversationId,hostId==="local"?undefined:hostId)}];
182
- });
183
- let auth=null;
184
- codex.props(rail,props=>{
185
- auth=[props.auth,props.value].find(value=>value && typeof value==="object" && "authMethod" in value && "accountId" in value && "userId" in value)??null;
186
- return Boolean(auth);
187
- });
188
- if(auth?.isLoading)return null;
189
- const currentId=codex.composerSurface?.()?.threadId??codex.currentThreadId();
190
- codex.report("task-notification-catalog",true);
191
- return {records,currentKey:records.find(item=>item.threadId===currentId)?.key??null,account:auth ? `${auth.authMethod}:${auth.accountId}:${auth.userId}` : scope.node};
192
- } catch {
193
- codex.report("task-notification-catalog",false,"native-store-contract-unavailable");
194
- return null;
195
- }
196
- }
197
- return {snapshot,layout,dispose(){disposed=true;stopWatching?.();stopWatching=null;watchedScope=null;bindings=null;}};
198
- }
@@ -1,21 +0,0 @@
1
- // 状态变化产生新通知;流式摘要刷新只更新内容,不抢占顶部。
2
- export function createNotificationQueue() {
3
- const entries = new Map();
4
- let sequence = 0;
5
- function update(records, currentKey) {
6
- const present = new Set();
7
- for (const item of records) {
8
- if (!item.key) continue;
9
- present.add(item.key);
10
- const state = item.attention || (item.unread && item.working === false ? "unread" : null);
11
- if (!state || item.key === currentKey) { entries.delete(item.key); continue; }
12
- const previous = entries.get(item.key);
13
- const event = `${state}:${state === "unread" ? item.timestamp ?? "" : ""}`;
14
- const changed = previous?.event !== event;
15
- entries.set(item.key, { item, event, order:changed ? ++sequence : previous.order, dismissed:changed ? false : previous.dismissed });
16
- }
17
- for (const key of entries.keys()) if (!present.has(key)) entries.delete(key);
18
- return [...entries.values()].filter(entry => !entry.dismissed).sort((a,b) => b.order-a.order).map(entry => entry.item);
19
- }
20
- return { update, dismiss(key) { const entry=entries.get(key); if (entry) entry.dismissed=true; }, clear() { entries.clear(); } };
21
- }
@@ -1,9 +0,0 @@
1
- import { readFile } from "node:fs/promises";
2
- import { createThreadAdapter, createNotificationAdapter, notificationVisibilityStyles } from "./adapters/codex/renderer.mjs";
3
- import { discoverThreadBindings, loadThreadBindings } from "./adapters/codex/modules.mjs";
4
- import { createNotificationQueue } from "./domain/notifications.mjs";
5
- export async function createPageScriptSource() {
6
- const source=await readFile(new URL("./unread-inbox.mjs",import.meta.url),"utf8");
7
- return [createThreadAdapter,createNotificationAdapter,notificationVisibilityStyles,discoverThreadBindings,loadThreadBindings,createNotificationQueue]
8
- .map(fn=>`const ${fn.name}=${fn.toString()};`).join("\n")+"\n"+source;
9
- }
@@ -1,307 +0,0 @@
1
- // 复用 Notes 已采用的原生 Close SVG,避免文字字形的基线偏移。
2
- const closeGlyph = "<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"16\" height=\"16\" viewBox=\"0 0 16 16\" fill=\"none\" aria-hidden=\"true\"><path d=\"M10.962 4.29539C11.1669 4.09057 11.4992 4.09076 11.7042 4.29539C11.9092 4.50039 11.9091 4.83255 11.7042 5.03758L8.74229 7.99949L11.7052 10.9624C11.9097 11.1673 11.9097 11.4996 11.7052 11.7046C11.5003 11.9095 11.168 11.9093 10.963 11.7046L8.0001 8.74168L5.03721 11.7046C4.83216 11.9093 4.49994 11.9095 4.29502 11.7046C4.09047 11.4996 4.09045 11.1673 4.29502 10.9624L7.25791 7.99949L4.296 5.03758C4.09101 4.83255 4.09099 4.5004 4.296 4.29539C4.50105 4.09079 4.8333 4.09054 5.03819 4.29539L8.0001 7.2573L10.962 4.29539Z\" fill=\"currentColor\"/></svg>";
3
- const deviceGlyphs = {"local": "<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 16 16\" width=\"16\" height=\"16\" fill=\"none\" aria-hidden=\"true\"><path fill-rule=\"evenodd\" clip-rule=\"evenodd\" d=\"M12.6667 2.47461C13.6928 2.47479 14.525 3.30694 14.5251 4.33301V10.1445C15.0248 10.184 15.3787 10.6177 15.3787 11.0928V11.9355C15.3785 12.9036 14.6274 13.7373 13.6433 13.7373H2.34644C1.36254 13.7371 0.611273 12.9035 0.611084 11.9355V11.0928C0.611084 10.6149 0.970003 10.1796 1.47437 10.1445V4.33301C1.47454 3.30702 2.3068 2.47492 3.33276 2.47461H12.6667ZM1.66187 11.9355C1.66205 12.3765 1.99397 12.6863 2.34644 12.6865H13.6433C13.9959 12.6865 14.3287 12.3767 14.3289 11.9355V11.1924H1.66187V11.9355ZM3.33276 3.52539C2.8867 3.5257 2.52532 3.88692 2.52515 4.33301V10.1416H13.4744V4.33301C13.4742 3.88684 13.1129 3.52557 12.6667 3.52539H3.33276Z\" fill=\"currentColor\"/></svg>", "remote": "<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 16 16\" width=\"16\" height=\"16\" fill=\"none\" aria-hidden=\"true\"><path fill-rule=\"evenodd\" clip-rule=\"evenodd\" d=\"M11.5005 1.1416C12.4346 1.14184 13.1919 1.89985 13.1919 2.83398V6.55859H13.3335C13.8259 6.55859 14.1917 6.9676 14.1919 7.42969V8.2959C14.1914 9.14015 13.5269 9.85938 12.6665 9.85938H3.3335C2.47306 9.85938 1.80856 9.14015 1.80811 8.2959V7.42969C1.80834 6.9677 2.17428 6.55877 2.6665 6.55859H2.80811V2.83398C2.80811 1.8997 3.56621 1.1416 4.50049 1.1416H11.5005ZM2.85889 8.2959C2.85933 8.59786 3.08989 8.80859 3.3335 8.80859H12.6665C12.9101 8.80859 13.1416 8.59786 13.1421 8.2959V7.60938H2.85889V8.2959ZM4.50049 2.19238C4.14611 2.19238 3.85889 2.4796 3.85889 2.83398V6.55859H12.1421V2.83398C12.1421 2.47975 11.8547 2.19262 11.5005 2.19238H4.50049Z\" fill=\"currentColor\"/> <path d=\"M2.8595 13.4307C3.28863 13.4308 3.63684 13.7788 3.63684 14.208C3.63677 14.6371 3.28859 14.9852 2.8595 14.9854C2.43029 14.9854 2.08222 14.6372 2.08215 14.208C2.08215 13.7788 2.43025 13.4307 2.8595 13.4307Z\" fill=\"currentColor\"/> <path d=\"M5.42981 13.4307C5.859 13.4307 6.20715 13.7788 6.20715 14.208C6.20709 14.6372 5.85896 14.9853 5.42981 14.9854C5.0006 14.9854 4.65253 14.6372 4.65247 14.208C4.65247 13.7788 5.00056 13.4307 5.42981 13.4307Z\" fill=\"currentColor\"/> <path d=\"M8.00012 13.4307C8.42937 13.4307 8.77747 13.7788 8.77747 14.208C8.7774 14.6372 8.42933 14.9854 8.00012 14.9854C7.57092 14.9854 7.22284 14.6372 7.22278 14.208C7.22278 13.7788 7.57087 13.4307 8.00012 13.4307Z\" fill=\"currentColor\"/> <path d=\"M10.5704 13.4307C10.9997 13.4307 11.3478 13.7788 11.3478 14.208C11.3477 14.6372 10.9996 14.9854 10.5704 14.9854C10.1414 14.9852 9.79413 14.6371 9.79407 14.208C9.79407 13.7789 10.1414 13.4309 10.5704 13.4307Z\" fill=\"currentColor\"/> <path d=\"M13.1417 13.4307C13.5707 13.4309 13.9181 13.7789 13.9181 14.208C13.918 14.637 13.5707 14.9851 13.1417 14.9854C12.7125 14.9854 12.3644 14.6372 12.3644 14.208C12.3644 13.7788 12.7125 13.4307 13.1417 13.4307Z\" fill=\"currentColor\"/> <path d=\"M8.00012 10.8809C8.42925 10.8809 8.77727 11.2281 8.77747 11.6572C8.77747 12.0865 8.42937 12.4346 8.00012 12.4346C7.57087 12.4346 7.22278 12.0865 7.22278 11.6572C7.22298 11.2281 7.571 10.8809 8.00012 10.8809Z\" fill=\"currentColor\"/></svg>"};
4
- const text = (en, cn) => (document.documentElement.lang || navigator.language || "en").startsWith("zh") ? cn : en;
5
- const projectIcons = new Map();
6
- const folderGlyph = "<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 20 20\" width=\"20\" height=\"20\" fill=\"none\" aria-hidden=\"true\"><path fill-rule=\"evenodd\" clip-rule=\"evenodd\" d=\"M6.94946 2.66821C7.58311 2.66821 8.20067 2.86877 8.71313 3.24146L9.77759 4.01587C10.0627 4.22319 10.4065 4.33521 10.759 4.33521H14.9583C16.6142 4.33521 17.9563 5.67732 17.9563 7.33325V14.3333C17.9563 15.9892 16.6142 17.3313 14.9583 17.3313H5.04126C3.38547 17.3311 2.04321 15.9891 2.04321 14.3333V5.66626C2.04339 4.01058 3.38558 2.66839 5.04126 2.66821H6.94946ZM3.37329 9.83228V14.3333C3.37329 15.2545 4.12001 16.001 5.04126 16.0012H14.9583C15.8796 16.0012 16.6262 15.2546 16.6262 14.3333V9.83228H3.37329ZM5.04126 3.99829C4.12012 3.99847 3.37347 4.74512 3.37329 5.66626V8.5022H16.6262V7.33325C16.6262 6.41186 15.8796 5.66528 14.9583 5.66528H10.759C10.1254 5.66528 9.50784 5.46377 8.99536 5.09106L7.93091 4.31763C7.64576 4.11024 7.30205 3.99829 6.94946 3.99829H5.04126Z\" fill=\"currentColor\"/></svg>";
7
- const marker = "data-minecodex-unread";
8
- const queue = createNotificationQueue();
9
- const native = createNotificationAdapter(config.codex, loadThreadBindings, schedule);
10
- const style = document.createElement("style");
11
- style.textContent = `
12
- [${marker}=panel] { --mcx-notification-padding:14px; width:min(320px,calc(100vw - 76px)); max-height:calc(100vh - 72px); overflow:auto; scrollbar-width:thin; scrollbar-color:var(--mcx-color-border) transparent; padding:6px; margin:-6px; box-sizing:content-box; display:flex; flex-direction:column; gap:8px; pointer-events:none; font-family:var(--mcx-font-sans-default); font-size:var(--mcx-text-sm); color:var(--mcx-color-text-primary); }
13
- [${marker}=panel][hidden],[${marker}=more][hidden] { display:none; }
14
- [${marker}=more] { pointer-events:auto; padding:5px 12px; border:1px solid var(--mcx-color-border); border-radius:999px; background:var(--mcx-color-surface-elevated-secondary); font-family:var(--mcx-font-sans-default); font-size:12px; line-height:16px; color:var(--mcx-color-text-secondary); cursor:pointer; transform:translateX(-50%); }
15
- [${marker}=more]:hover { color:var(--mcx-color-text-primary); }
16
- [${marker}=more]:focus-visible { outline:2px solid var(--mcx-color-ring); outline-offset:2px; }
17
- [${marker}=tail] { pointer-events:none; height:32px; clip-path:inset(26px -12px -20px); }
18
- [${marker}=tail][hidden] { display:none; }
19
- [${marker}=tail] > div { position:absolute; height:26px; border-radius:16px; }
20
- [${marker}=tail] > div:first-child { left:14px; right:14px; top:12px; }
21
- [${marker}=tail] > div:last-child { left:7px; right:7px; top:6px; }
22
- [${marker}=panel] .mcx-notification { position:relative; flex:none; border-radius:16px; overflow:visible; pointer-events:auto; overflow-anchor:none; }
23
- [${marker}=panel] [data-unread-row] { display:flex; flex-direction:column; gap:4px; width:100%; padding:var(--mcx-notification-padding); border:0; border-radius:16px; background:transparent; text-align:start; color:inherit; font:inherit; cursor:pointer; }
24
- [${marker}=panel] [data-unread-row]:hover { background:var(--mcx-color-background-primary-ghost-hover); }
25
- [${marker}=panel] button:focus-visible { outline:2px solid var(--mcx-color-ring); outline-offset:-3px; border-radius:12px; }
26
- [${marker}=panel] .mcx-dismiss { position:absolute; top:-4px; right:-4px; display:grid; place-items:center; width:22px; height:22px; padding:0; border:0; border-radius:50%; background:var(--mcx-color-surface-elevated-secondary); color:var(--mcx-color-text-tertiary); line-height:0; cursor:pointer; opacity:0; }
27
- [${marker}=panel] .mcx-notification:hover .mcx-dismiss,[${marker}=panel] .mcx-notification:focus-within .mcx-dismiss { opacity:1; }
28
- [${marker}=panel] .mcx-dismiss svg { display:block; width:16px; height:16px; }
29
- [${marker}=panel] .mcx-dismiss:hover { color:var(--mcx-color-text-primary); }
30
- [${marker}=panel] .mcx-unread-dot { width:6px; height:6px; border-radius:50%; flex:none; background:var(--mcx-color-chart-blue); }
31
- [${marker}=panel] .mcx-thread-header { display:flex; align-items:center; gap:8px; width:100%; min-width:0; }
32
- [${marker}=panel] .mcx-thread-title { overflow:hidden; text-overflow:ellipsis; white-space:nowrap; min-width:0; font-size:13px; line-height:18px; font-weight:500; }
33
- [${marker}=panel] .mcx-device { display:flex; flex-shrink:0; color:var(--mcx-color-text-tertiary); }
34
- [${marker}=panel] time { margin-inline-start:auto; flex-shrink:0; font-size:var(--mcx-text-xs); color:var(--mcx-color-text-tertiary); }
35
- [${marker}=panel] .mcx-summary { display:-webkit-box; -webkit-box-orient:vertical; -webkit-line-clamp:2; line-clamp:2; overflow:hidden; white-space:normal; overflow-wrap:anywhere; font-size:12px; line-height:16px; color:var(--mcx-color-text-secondary); }
36
- [${marker}=panel] .mcx-project { display:flex; align-items:center; gap:6px; padding:0; min-width:0; font-size:11px; line-height:16px; color:var(--mcx-color-text-tertiary); font-weight:400; }
37
- [${marker}=panel] .mcx-project > span:last-child { overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
38
- [${marker}=panel] .mcx-project svg, [${marker}=panel] .mcx-project-icon { width:12px; height:12px; flex:none; }
39
- [${marker}=panel] .mcx-project-icon > * { width:12px; height:12px; }
40
- [${marker}=panel] .mcx-attention { margin-inline-start:auto; flex:none; max-width:150px; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; border-radius:999px; padding:2px 10px 2px 8px; font-size:12px; line-height:16px; position:relative; isolation:isolate; color:var(--mcx-color-chart-blue); background:color-mix(in srgb,var(--mcx-color-chart-blue) 15%,transparent); }
41
- [${marker}=panel] .mcx-attention[data-attention="approval"] { color:var(--mcx-color-chart-green); background:color-mix(in srgb,var(--mcx-color-chart-green) 20%,transparent); }
42
-
43
- [${marker}=panel] .mcx-attention[data-countdown]::before { content:""; position:absolute; inset:0; z-index:-1; background:currentColor; opacity:.14; transform-origin:left; transform:scaleX(var(--countdown-progress,0)); transition:transform 1s linear; }
44
- [${marker}=panel] .mcx-countdown { display:inline-block; margin-inline-start:5px; font-variant-numeric:tabular-nums; min-width:2.5ch; }
45
- @media (prefers-reduced-motion:reduce) { [${marker}=panel] .mcx-attention::before { transition:none; } }
46
- `;
47
- style.textContent += notificationVisibilityStyles();
48
- document.head.append(style);
49
- const panel=document.createElement("section");
50
- panel.setAttribute(marker,"panel");
51
- config.ui.layer(panel,"notification");
52
- panel.setAttribute("aria-label",text("Chat notifications","会话通知"));
53
- panel.hidden=true;
54
- document.body.append(panel);
55
- // 折叠装饰在真实卡片后方,展开按钮独立于滚动遮罩。
56
- const tail=document.createElement("div");
57
- tail.setAttribute(marker,"tail");tail.setAttribute("aria-hidden","true");
58
- config.ui.layer(tail,"notification");tail.hidden=true;
59
- for(let i=0;i<2;i++){const layer=document.createElement("div");config.ui.surface(layer,"popover");tail.append(layer);}
60
- document.body.insertBefore(tail,panel);
61
- const more=document.createElement("button");more.type="button";
62
- more.setAttribute(marker,"more");
63
- config.ui.layer(more,"notification");
64
- more.hidden=true;
65
- document.body.append(more);
66
- let expanded=false,totalNotifications=0,pageKey=null;
67
- let collapseAnimation=null;
68
- more.addEventListener("click",()=>{
69
- expanded=true;more.hidden=true;tail.hidden=true;sync();
70
- // 展开后将键盘焦点交给第一条此前隐藏的通知。
71
- [...cards.values()][3]?.querySelector("[data-unread-row]")?.focus({preventScroll:true});
72
- });
73
- const cards=new Map();
74
- let disposed=false,queued=false,account=null;
75
- function relativeTime(timestamp, now = Date.now()) {
76
- if (!Number.isFinite(timestamp)) return "";
77
- const seconds = Math.max(0, Math.floor((now - timestamp) / 1000));
78
- if (seconds < 60) return text("Just now", "刚刚");
79
- const units = seconds < 3600 ? [60, "m", "分钟"] : seconds < 86400 ? [3600, "h", "小时"] : [86400, "d", "天"];
80
- const value = Math.floor(seconds / units[0]);
81
- return text(value + units[1], value + units[2] + "前");
82
- }
83
- function cacheProjectIcons() {
84
- for (const row of document.querySelectorAll('#app-shell-sidebar [data-app-action-sidebar-project-row]')) {
85
- const glyph = row.querySelector("span.relative.inline-flex");
86
- if (glyph) projectIcons.set(row.dataset.appActionSidebarProjectId, glyph.cloneNode(true));
87
- }
88
- }
89
-
90
- function createCard(item) {
91
- const shell=document.createElement("div");
92
- shell.className="mcx-notification";
93
- config.ui.surface(shell,"popover");
94
- const card=document.createElement("button");
95
- card.type="button";
96
- card.dataset.unreadRow="";
97
- card.dataset.threadKey=item.key;
98
- const header = document.createElement("span");
99
- header.className = "mcx-thread-header";
100
- const title = document.createElement("span");
101
- title.className = "mcx-thread-title";
102
- title.textContent = item.title;
103
- const device = document.createElement("span");
104
- device.className = "mcx-device";
105
- const remote = item.hostId !== "local";
106
- device.innerHTML = deviceGlyphs[remote ? "remote" : "local"];
107
- device.setAttribute("data-minecodex-tooltip", text("Remote", "远程"));
108
- device.setAttribute("aria-label", text("Remote", "远程"));
109
- header.append(title);
110
- if (remote) header.append(device);
111
- const elapsed = relativeTime(item.timestamp);
112
- if (item.attention) {
113
- const label = document.createElement("span");
114
- label.className = "mcx-attention";
115
- label.dataset.attention = item.attention;
116
- label.textContent = item.attention === "approval" ? text("Awaiting approval", "等待批准") : text("Needs input", "需要回复");
117
- header.append(label);
118
- } else if (elapsed) {
119
- const time = document.createElement("time");
120
- time.dateTime = new Date(item.timestamp).toISOString();
121
- time.textContent = elapsed;
122
- time.setAttribute("data-minecodex-tooltip", new Date(item.timestamp).toLocaleString());
123
- header.append(time);
124
- }
125
- card.append(header);
126
- if (item.summary) {
127
- const summary = document.createElement("span");
128
- summary.className = "mcx-summary";
129
- summary.textContent = item.summary;
130
- card.append(summary);
131
- }
132
- if (item.project) {
133
- const project = document.createElement("span");
134
- project.className = "mcx-project";
135
- const icon = document.createElement("span");
136
- icon.className = "mcx-project-icon";
137
- const glyph = projectIcons.get(item.project.id)?.cloneNode(true);
138
- if (glyph) icon.append(glyph);
139
- else icon.innerHTML = folderGlyph;
140
- project.append(icon);
141
- const name = document.createElement("span");
142
- name.textContent = item.project.name;
143
- project.append(name);
144
- card.append(project);
145
- }
146
-
147
- if (item.unread && !item.attention && !item.working) {
148
- const dot=document.createElement("span");
149
- dot.className="mcx-unread-dot";
150
- if (!elapsed) dot.style.marginInlineStart="auto";
151
- dot.setAttribute("aria-label",text("Unread","未读"));
152
- header.append(dot);
153
- }
154
- const dismiss=document.createElement("button");
155
- dismiss.type="button";
156
- dismiss.className="mcx-dismiss";
157
- dismiss.innerHTML=closeGlyph;
158
- dismiss.setAttribute("aria-label",text("Dismiss notification","关闭通知"));
159
- dismiss.addEventListener("click",()=>{queue.dismiss(item.key);schedule();});
160
- card.addEventListener("click",async()=>{
161
- if (card.disabled) return;
162
- card.disabled=true;
163
- try { const navigated=await shell.record.activate(); if(navigated===false){card.disabled=false;return;} queue.dismiss(item.key); schedule(); }
164
- catch { card.disabled=false; }
165
- });
166
- shell.append(card,dismiss);
167
- shell.record=item;
168
- return shell;
169
- }
170
- const motion = {duration:320,easing:"cubic-bezier(.22,1,.36,1)"};
171
- function updateCountdown(card,item) {
172
- const label=card.querySelector(".mcx-attention");
173
- if(!label)return;
174
- const value=item.countdown;
175
- let counter=label.querySelector(".mcx-countdown");
176
- if(!value){counter?.remove();delete label.dataset.countdown;label.removeAttribute("aria-label");return;}
177
- if(!counter){counter=document.createElement("span");counter.className="mcx-countdown";label.append(counter);}
178
- const remaining=Math.max(0,value.deadlineMs-Date.now());
179
- const seconds=Math.ceil(remaining/1000);
180
- counter.textContent=seconds+text("s","秒");
181
- label.dataset.countdown="";
182
- label.style.setProperty("--countdown-progress",String(Math.min(1,remaining/value.durationMs)));
183
- label.setAttribute("aria-label",text(`Needs input, ${seconds} seconds remaining`,`需要回复,剩余 ${seconds} 秒`));
184
- }
185
- function updateStackViewport() {
186
- const rows=[...cards.values()].filter(card=>card.isConnected).sort((a,b)=>a.offsetTop-b.offsetTop);
187
- const collapsedMore=!expanded&&totalNotifications>3;
188
- panel.style.maxHeight=`${Math.max(0,stackAvailableHeight-12-(collapsedMore?54:0))}px`;
189
- const bottom=panel.scrollTop+panel.clientHeight;
190
- const overflow=panel.scrollHeight>panel.clientHeight+1;
191
- const atBottom=bottom>=panel.scrollHeight-1;
192
- const edge=rows.find(card=>card.offsetTop+card.offsetHeight>=bottom)??rows.at(-1);
193
- const fade=Math.min((edge?.offsetHeight??80)/2,panel.clientHeight/3);
194
- panel.style.maskImage=expanded&&!panel.hidden&&overflow&&!atBottom ? `linear-gradient(to bottom,black calc(100% - ${fade}px),transparent 100%)` : "none";
195
- more.hidden=tail.hidden=panel.hidden||!collapsedMore;
196
- if(collapsedMore&&!panel.hidden&&!collapseAnimation){
197
- const rect=panel.getBoundingClientRect();
198
- tail.style.left=`${rect.left+6}px`;tail.style.top=`${rect.bottom-32}px`;tail.style.width=`${rect.width-12}px`;
199
- more.style.left=`${rect.left+rect.width/2}px`;more.style.top=`${rect.bottom+18}px`;
200
- const count=totalNotifications-3;
201
- const label=text(`${count} more ${count===1?"notification":"notifications"}`,`还有 ${count} 条通知`);
202
- if(more.textContent!==label)more.textContent=label;
203
- more.setAttribute("aria-expanded","false");
204
- }
205
- }
206
- let stackAvailableHeight=innerHeight;
207
- panel.addEventListener("scroll",updateStackViewport,{signal,passive:true});
208
- function renderRows(records) {
209
- const reducedMotion=window.matchMedia("(prefers-reduced-motion: reduce)").matches;
210
- const signatureFor=item=>JSON.stringify([item.title,item.summary,item.project,item.attention,item.unread,item.working,item.hostId,relativeTime(item.timestamp),document.documentElement.lang]);
211
- const ordered=[...panel.children].filter(card=>cards.get(card.record?.key)===card);
212
- if(records.length===cards.size && records.every((item,index)=>ordered[index]===cards.get(item.key)&&cards.get(item.key)?.signature===signatureFor(item))){
213
- records.forEach(item=>{const card=cards.get(item.key);card.record=item;updateCountdown(card,item);});
214
- updateStackViewport();return;
215
- }
216
- const positions=new Map([...cards].map(([key,card])=>[key,card.getBoundingClientRect().top]));
217
- // 从当前视觉位置接续,避免连续状态更新时叠加 transform 动画。
218
- for(const card of cards.values()) { card.moveAnimation?.cancel(); card.enterAnimation?.cancel(); }
219
- const keys=new Set(records.map(item=>item.key));
220
- for(const [key,card] of cards) if(!keys.has(key)){
221
- const top=card.offsetTop,width=card.offsetWidth;
222
- cards.delete(key);
223
- if(reducedMotion||panel.hidden){card.remove();continue;}
224
- card.style.cssText+=`;position:absolute;top:${top}px;width:${width}px;pointer-events:none;`;
225
- card.inert=true;
226
- card.animate([{opacity:1,transform:"translateX(0)"},{opacity:0,transform:"translateX(-10px)"}],{duration:180,easing:"ease-out"}).finished.then(()=>{card.remove();schedule();},()=>card.remove());
227
- }
228
- records.forEach((item,index)=>{
229
- const signature=signatureFor(item);
230
- let card=cards.get(item.key),fresh=!card;
231
- if(!card){card=createCard(item);cards.set(item.key,card);}
232
- else if(card.signature!==signature){
233
- // 保留容器、点击目标与键盘焦点;仅更新内容,不重播整卡入场。
234
- const next=createCard(item),button=card.querySelector("[data-unread-row]");
235
- button.replaceChildren(...next.querySelector("[data-unread-row]").childNodes);
236
- if(!reducedMotion)button.animate([{opacity:.65},{opacity:1}],{duration:180,easing:"ease-out"});
237
- }
238
- card.signature=signature;card.record=item;
239
- if(panel.children[index]!==card)panel.insertBefore(card,panel.children[index]??null);
240
- updateCountdown(card,item);
241
- if(fresh&&!reducedMotion)card.enterAnimation=card.animate([{opacity:0,transform:"translateX(-12px)"},{opacity:1,transform:"translateX(0)"}],motion);
242
- });
243
- updateStackViewport();
244
- if(!reducedMotion) for(const [key,card] of cards){
245
- const delta=positions.get(key)-card.getBoundingClientRect().top;
246
- if(Number.isFinite(delta)&&Math.abs(delta)>.5)card.moveAnimation=card.animate([{transform:`translateY(${delta}px)`},{transform:"translateY(0)"}],motion);
247
- }
248
- }
249
- function animateCollapse(before) {
250
- if(window.matchMedia("(prefers-reduced-motion: reduce)").matches)return;
251
- const after=panel.getBoundingClientRect();
252
- const distance=before.height-after.height;
253
- if(distance<=1)return;
254
- panel.style.overflow="hidden";
255
- collapseAnimation=panel.animate([{height:`${Math.max(0,before.height-12)}px`,maxHeight:`${before.height}px`},{height:`${Math.max(0,after.height-12)}px`,maxHeight:getComputedStyle(panel).maxHeight}],motion);
256
- for(const node of [tail,more]) if(!node.hidden)node.animate([
257
- {opacity:0,translate:`0 ${distance}px`},
258
- {opacity:1,translate:"0 0"}
259
- ],motion);
260
- const animation=collapseAnimation;
261
- animation.finished.then(()=>{
262
- if(collapseAnimation!==animation)return;
263
- collapseAnimation=null;panel.style.overflow="";updateStackViewport();
264
- },()=>{});
265
- }
266
- function sync() {
267
- if(disposed)return;
268
- const layout=native.layout(),snapshot=native.snapshot();
269
- const changedPage=layout.pageKey!=null&&pageKey!=null&&layout.pageKey!==pageKey;
270
- const collapseFrom=changedPage&&expanded&&!panel.hidden?panel.getBoundingClientRect():null;
271
- if(layout.pageKey!=null)pageKey=layout.pageKey;
272
- if(changedPage){
273
- expanded=false;collapseAnimation?.cancel();collapseAnimation=null;panel.style.overflow="";
274
- panel.scrollTop=0;
275
- }
276
- if(!snapshot){queue.clear();panel.replaceChildren();cards.clear();panel.hidden=true;more.hidden=true;tail.hidden=true;expanded=false;return;}
277
- if(account!==snapshot.account){queue.clear();expanded=false;account=snapshot.account;}
278
- cacheProjectIcons();
279
- const details=queue.update(snapshot.records.slice().sort((a,b)=>(a.timestamp??0)-(b.timestamp??0)),snapshot.currentKey);
280
- totalNotifications=details.length;
281
- if(!details.length)expanded=false;
282
- panel.hidden=!layout.visible||(!details.length&&!cards.size);
283
- if(!details.length){renderRows([]);return;}
284
- if(panel.hidden){more.hidden=true;tail.hidden=true;return;}
285
- const width=Math.min(320,Math.max(0,innerWidth-layout.left-12));
286
- panel.style.left=`${layout.left}px`;
287
- panel.style.top=`${layout.top}px`;
288
- panel.style.width=`${width}px`;
289
- stackAvailableHeight=Math.max(0,innerHeight-layout.top-12);
290
- panel.setAttribute("aria-label",text("Chat notifications","会话通知"));
291
- renderRows(expanded?details:details.slice(0,3));
292
- if(collapseFrom)animateCollapse(collapseFrom);
293
- }
294
- function schedule() {
295
- if(disposed||queued)return;
296
- queued=true;
297
- queueMicrotask(()=>{queued=false;sync();});
298
- }
299
- const observer=new MutationObserver(mutations=>{
300
- if(mutations.some(m=>!panel.contains(m.target)&&m.target!==style&&!more.contains(m.target)&&!tail.contains(m.target)))schedule();
301
- });
302
- observer.observe(document.documentElement,{subtree:true,childList:true,attributes:true,attributeFilter:["inert","aria-hidden","aria-expanded","aria-current","data-app-shell-sidebar-open","data-slate-sidebar-peeking","lang"]});
303
- window.addEventListener("resize",schedule,{signal});
304
- // 隐藏的原生视图不一定重绘;每秒读取 store,不发送请求或切换 Activity。
305
- const timer=setInterval(schedule,1000);
306
- sync();
307
- return ()=>{disposed=true;clearInterval(timer);observer.disconnect();native.dispose();queue.clear();panel.remove();more.remove();tail.remove();style.remove();};
@@ -1,49 +0,0 @@
1
- # Usage Meter
2
-
3
- 通用视觉、材质、按钮、Tooltip 与原生 sidebar 层级遵循根 [DESIGN.md](../../DESIGN.md)。本文件只记录功能特有组件、布局、数据语义与通用组件选型。
4
-
5
- ## 用户任务与范围
6
-
7
- 在原生导航栏 Avatar 上方查看当前账号剩余额度,点击直达 Settings → Usage。
8
- 仅向 `nav[data-app-navigation-rail="true"]` 注入一个控件,不注册通用侧栏
9
- 菜单项,不向 Settings 的分类列表插入内容。
10
-
11
- ## 数据契约
12
-
13
- 已在 2026-09-29 的运行中 Codex 确认:原生 QueryClient 的成功额度数据缓存键为
14
- `["rate-limit-status"]`,由 Codex 自行更新。
15
- 插件只订阅该缓存,校验响应的 user_id/account_id 与当前原生 auth 一致,不自行请求
16
- 接口,不持久化账号或额度数据。切换账号、退出登录、额度数据缺失时隐藏旧值。
17
-
18
- 窗口按 `limit_window_seconds` 识别:604800 为周、18000 为五小时;不依赖
19
- primary/secondary 顺序或套餐字符串。剩余百分比为 clamp(100-used_percent,0,100),
20
- 四舍五入显示整数。缺失/非法值不当成零。
21
-
22
- 单周窗口显示一个周环和周剩余数字。周与五小时同时存在时,周在外、五小时在内,
23
- 中央显示五小时剩余数字,不带百分号。没有周窗口时不显示本控件。
24
-
25
- ## 视觉 tokens
26
-
27
- | 角色 | 权威 token / 值 | 用途 |
28
- | --- | --- | --- |
29
- | 点击区域 | 克隆 Avatar 的原生按钮 class 和布局样式属性;目标 36×36px | 与导航项对齐 |
30
- | 环图 | 24×24px;viewBox 32;圆心 16;周半径 43/3、五小时半径 10.5;描边 10/3 viewBox units(渲染 2.5px) | 自定义数据图,不是替代导航图标 |
31
- | 周环 | `--color-text-tertiary`(原生中性灰) | 单圈或外圈 |
32
- | 五小时环 | `--color-text-secondary`(中性灰) | 双圈内圈 |
33
- | 轨道 | 对应环色 18% alpha | 剩余额度的底轨 |
34
- | 中央数字 | 继承环的中性灰;`--font-sans-default`;`--text-xs` × 2/3(8px),单双圈与三位数均一致;500、tabular-nums、line-height:1 | 优先五小时 |
35
-
36
- 环从顶部顺时针填充。额度 Tooltip 的特殊内容是两列对齐,列间距16px;其通用材质和文字样式遵循根规范。Tooltip 在按钮右侧 2px,纵向居中并约束在窗口内;
37
- 鼠标悬停延迟 350ms,键盘聚焦立即显示,Escape 隐藏。单圈一行 Weekly Limit;
38
- 双圈依次为 5h Limit、Weekly Limit,数值带 %。没有额外标题或说明。
39
-
40
- 原生 Avatar 点击区域目标为 36px,头像可见直径为 24px;额度外环含描边的可见直径同为 24px,
41
- 数字与环均以按钮几何中心对齐;中性灰取自原生 navbar,不使用彩色强调。
42
- 注入点为头像整行在最近原生纵向容器中的前一项,复用该容器的 8px 间距;不向头像内部横向行插入按钮,不改动原生容器布局。
43
- 单双圈使用同一实现,双圈验收图采用模拟值,不更改账号缓存。减少动画偏好不添加进度过渡。
44
-
45
- ## 跳转与生命周期
46
-
47
- 已确认原生入口指向 `/settings/usage?tab=overview`。使用当前 React Router
48
- 的 navigate,并保留当前路由 state;不模拟设置菜单内部点击。
49
- 移除功能时清除缓存订阅、DOM observer、事件、Tooltip 与按钮。
@@ -1,19 +0,0 @@
1
- # Usage Meter · 额度环
2
-
3
- 在 Codex 原生 navbar 的 Avatar 上方显示当前登录账号的**剩余额度**。
4
- 额度按钮与头像整行同属原生纵向容器,沿用其间距与居中对齐;不挤入头像内部横向行。
5
-
6
- - 只有周额度:单圈,中心为周剩余百分比整数。
7
- - 同时有周与五小时额度:外圈为周、内圈为五小时,中心显示五小时剩余数字。
8
- - 悬浮或键盘聚焦显示 Tooltip;双圈时先显示 `5h Limit`,再显示 `Weekly Limit`,标签左对齐、百分比右对齐。
9
- - 点击进入原生 Settings → Usage。
10
-
11
- 数据直接订阅当前账号的原生额度缓存,与 Usage 页同步。窗口类型按实际时长识别,
12
- 不通过套餐名称猜测。缺失数据、退出登录或切换账号时不展示旧额度。
13
- 不新增 API 请求,不持久化账号信息,不修改 Settings 分类导航。
14
-
15
- MineCodex 管理页面可单独开关此功能。设计与原生接入契约见 [DESIGN.md](DESIGN.md)。
16
-
17
- ```bash
18
- node --test features/usage-meter/test/*.test.mjs
19
- ```