minecodex 2.0.5 → 2.0.7

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 (85) hide show
  1. package/DESIGN.md +19 -0
  2. package/README.md +5 -1
  3. package/apps/dashboard/DESIGN.md +53 -31
  4. package/apps/dashboard/app.js +55 -39
  5. package/apps/dashboard/assets/OFL.txt +93 -0
  6. package/apps/dashboard/assets/info.svg +15 -0
  7. package/apps/dashboard/assets/moon.svg +13 -0
  8. package/apps/dashboard/assets/refresh-cw.svg +16 -0
  9. package/apps/dashboard/assets/silkscreen.ttf +0 -0
  10. package/apps/dashboard/assets/sun.svg +21 -0
  11. package/apps/dashboard/assets/terrain.js +89 -0
  12. package/apps/dashboard/assets/triangle-alert.svg +15 -0
  13. package/apps/dashboard/favicon.svg +12 -17
  14. package/apps/dashboard/index.html +24 -18
  15. package/apps/dashboard/styles.css +147 -181
  16. package/assets/brand/README.md +19 -0
  17. package/assets/brand/minecodex-icon-dark.svg +9 -0
  18. package/assets/brand/minecodex-icon-light.svg +9 -0
  19. package/assets/brand/minecodex-icon.svg +8 -0
  20. package/assets/brand/references/lucide-LICENSE.txt +43 -0
  21. package/assets/readme/hero.svg +93 -0
  22. package/features/hide-upsell-banner/README.md +7 -5
  23. package/features/hide-upsell-banner/codex-feature.json +2 -2
  24. package/features/hide-upsell-banner/src/hide-upsell-banner.mjs +30 -12
  25. package/features/images/DESIGN.md +18 -3
  26. package/features/images/README.md +4 -3
  27. package/features/images/codex-feature.json +2 -2
  28. package/features/images/src/adapters/codex/native-image-modules.mjs +220 -0
  29. package/features/images/src/adapters/codex/native-image-source.mjs +113 -198
  30. package/features/images/src/assets/prompts-menu.svg +15 -0
  31. package/features/images/src/frontend/cloud-image-actions.mjs +8 -6
  32. package/features/images/src/frontend/local-image-source.mjs +2 -1
  33. package/features/images/src/library-prompt-page.mjs +10 -4
  34. package/features/images/src/library-prompt-source.mjs +4 -5
  35. package/features/model-slider/codex-feature.json +2 -2
  36. package/features/notes/DESIGN.md +1 -1
  37. package/features/notes/codex-feature.json +2 -2
  38. package/features/notes/src/frontend/entry.mjs +1 -1
  39. package/features/session-status/codex-feature.json +2 -2
  40. package/features/sidebar-focus/DESIGN.md +4 -4
  41. package/features/sidebar-focus/codex-feature.json +3 -2
  42. package/features/sidebar-focus/src/adapters/codex/renderer.mjs +1 -1
  43. package/features/sidebar-focus/src/sidebar-focus.mjs +9 -7
  44. package/features/sites/DESIGN.md +25 -3
  45. package/features/sites/README.md +6 -2
  46. package/features/sites/codex-feature.json +3 -2
  47. package/features/sites/src/adapters/codex/renderer.mjs +25 -4
  48. package/features/sites/src/backend/project-sites.mjs +18 -6
  49. package/features/sites/src/backend/sites-service.mjs +22 -10
  50. package/features/sites/src/domain/discovery-candidates.mjs +53 -0
  51. package/features/sites/src/frontend/entry.mjs +76 -38
  52. package/features/sites/src/frontend/styles.css +3 -3
  53. package/features/unread-inbox/DESIGN.md +24 -25
  54. package/features/unread-inbox/codex-feature.json +3 -2
  55. package/features/unread-inbox/src/adapters/codex/modules.mjs +52 -0
  56. package/features/unread-inbox/src/adapters/codex/renderer.mjs +90 -0
  57. package/features/unread-inbox/src/domain/notifications.mjs +21 -0
  58. package/features/unread-inbox/src/injection-source.mjs +5 -2
  59. package/features/unread-inbox/src/unread-inbox.mjs +212 -297
  60. package/features/usage-meter/codex-feature.json +2 -2
  61. package/package.json +11 -3
  62. package/packages/cli/native/MineCodex.app/Contents/Info.plist +4 -2
  63. package/packages/cli/native/MineCodex.app/Contents/MacOS/MineCodexServiceManager +0 -0
  64. package/packages/cli/native/MineCodex.app/Contents/Resources/MineCodex.icns +0 -0
  65. package/packages/cli/native/MineCodex.app/Contents/_CodeSignature/CodeResources +11 -0
  66. package/packages/cli/native/README.md +31 -0
  67. package/packages/cli/src/browser-control-panel.mjs +48 -0
  68. package/packages/cli/src/commands.mjs +3 -1
  69. package/packages/cli/src/control-server.mjs +6 -3
  70. package/packages/cli/src/platform.mjs +5 -0
  71. package/packages/runtime-host/README.md +10 -5
  72. package/packages/runtime-host/src/codex-injection.mjs +12 -3
  73. package/packages/runtime-host/src/codex-runtime.mjs +10 -0
  74. package/packages/ui/DESIGN.md +5 -1
  75. package/packages/ui/assets/arrow-up-right.svg +13 -0
  76. package/packages/ui/assets/lucide-LICENSE.txt +43 -0
  77. package/packages/ui/assets/minecodex-menu-16.svg +7 -0
  78. package/packages/ui/assets/minecodex-menu-20.svg +7 -0
  79. package/packages/ui/src/components/account-menu.mjs +88 -0
  80. package/packages/ui/src/components/page.mjs +2 -2
  81. package/packages/ui/src/foundation/layers.mjs +1 -0
  82. package/packages/ui/src/install.mjs +3 -0
  83. package/packages/ui/src/native/account-menu.mjs +21 -0
  84. package/packages/ui/src/native/page.mjs +17 -2
  85. package/packages/ui/src/native/tokens.mjs +1 -0
@@ -1,112 +1,76 @@
1
- const nativeThreads = createThreadAdapter(config.codex);
2
- function threadRecord(row) {
3
- const item=nativeThreads.read(row), previous=messageUpdates.get(item.key);
4
- if (item.working && (!previous || previous.message !== item.summary || !previous.working))
5
- messageUpdates.set(item.key,{message:item.summary,working:true,time:Math.max(item.timestamp || 0,Date.now())});
6
- else if (previous) previous.working=item.working;
7
- return {...item,timestamp:Math.max(item.timestamp || 0,messageUpdates.get(item.key)?.time || 0) || null};
8
- }
9
- // 原生完整侧栏图标,取自当前 Codex 16px/20px 资源。
10
- const sidebarGlyphs = {"16": "<svg aria-hidden=\"true\" class=\"_Icon_uttcz_1\" focusable=\"false\" height=\"16\" viewBox=\"0 0 16 16\" width=\"16\"><path d=\"M4.66699 4.80859C4.95683 4.80873 5.19238 5.04412 5.19238 5.33398V10.667C5.19238 10.9569 4.95683 11.1923 4.66699 11.1924C4.37704 11.1924 4.1416 10.9569 4.1416 10.667V5.33398C4.1416 5.04403 4.37704 4.80859 4.66699 4.80859Z\" fill=\"currentColor\"></path> <path fill-rule=\"evenodd\" clip-rule=\"evenodd\" d=\"M11.5 2.30762C13.1707 2.30762 14.5254 3.66235 14.5254 5.33301V10.666C14.5254 12.3367 13.1707 13.6914 11.5 13.6914H4.5C2.82934 13.6914 1.47461 12.3367 1.47461 10.666V5.33301C1.47461 3.66235 2.82934 2.30762 4.5 2.30762H11.5ZM4.5 3.3584C3.40924 3.3584 2.52539 4.24225 2.52539 5.33301V10.666C2.52539 11.7568 3.40924 12.6416 4.5 12.6416H11.5C12.5908 12.6416 13.4746 11.7568 13.4746 10.666V5.33301C13.4746 4.24225 12.5908 3.3584 11.5 3.3584H4.5Z\" fill=\"currentColor\"></path></svg>", "20": "<svg aria-hidden=\"true\" class=\"_Icon_uttcz_1\" focusable=\"false\" height=\"20\" viewBox=\"0 0 20 20\" width=\"20\"><path d=\"M5.83301 6.00098C6.20028 6.00098 6.49805 6.29875 6.49805 6.66602V13.333C6.49787 13.7001 6.20017 13.998 5.83301 13.998C5.46596 13.9979 5.16814 13.7 5.16797 13.333V6.66602C5.16797 6.29883 5.46585 6.00111 5.83301 6.00098Z\" fill=\"currentColor\"></path> <path fill-rule=\"evenodd\" clip-rule=\"evenodd\" d=\"M14.5 2.87695C16.5241 2.87695 18.165 4.51787 18.165 6.54199V13.459C18.1649 15.483 16.524 17.124 14.5 17.124H5.5C3.47598 17.124 1.83514 15.483 1.83496 13.459V6.54199C1.83496 4.51787 3.47588 2.87695 5.5 2.87695H14.5ZM5.5 4.20703C4.21042 4.20703 3.16504 5.25241 3.16504 6.54199V13.459C3.16521 14.7484 4.21052 15.7939 5.5 15.7939H14.5C15.7895 15.7939 16.8348 14.7484 16.835 13.459V6.54199C16.835 5.25241 15.7896 4.20703 14.5 4.20703H5.5Z\" fill=\"currentColor\"></path></svg>"};
11
- const taskGlyph = "<svg width=\"16\" height=\"16\" viewBox=\"0 0 16 16\" fill=\"none\" aria-hidden=\"true\"><path d=\"M13.3336 11.4749C13.6234 11.475 13.858 11.7104 13.858 12.0002C13.8579 12.2899 13.6233 12.5255 13.3336 12.5256H6.66663C6.37678 12.5256 6.1414 12.2901 6.14124 12.0002C6.14124 11.7103 6.37668 11.4749 6.66663 11.4749H13.3336Z\" fill=\"currentColor\"/> <path d=\"M3.07874 10.9827C3.50002 10.9827 3.84226 11.3241 3.84241 11.7454C3.84241 12.1667 3.50011 12.509 3.07874 12.509C2.65748 12.5089 2.31604 12.1666 2.31604 11.7454C2.31619 11.3242 2.65758 10.9828 3.07874 10.9827Z\" fill=\"currentColor\"/> <path d=\"M13.3336 7.47485C13.6234 7.47503 13.858 7.7104 13.858 8.00024C13.8579 8.28995 13.6233 8.52546 13.3336 8.52563H6.66663C6.37678 8.52563 6.1414 8.29005 6.14124 8.00024C6.14124 7.71029 6.37668 7.47485 6.66663 7.47485H13.3336Z\" fill=\"currentColor\"/> <path d=\"M3.07874 6.98267C3.50002 6.98267 3.84226 7.32411 3.84241 7.74536C3.84241 8.16674 3.50011 8.50903 3.07874 8.50903C2.65748 8.50888 2.31604 8.16665 2.31604 7.74536C2.31619 7.3242 2.65758 6.98281 3.07874 6.98267Z\" fill=\"currentColor\"/> <path d=\"M13.3336 3.47485C13.6234 3.47503 13.858 3.7104 13.858 4.00024C13.8579 4.28995 13.6233 4.52546 13.3336 4.52563H6.66663C6.37678 4.52563 6.1414 4.29005 6.14124 4.00024C6.14124 3.71029 6.37668 3.47485 6.66663 3.47485H13.3336Z\" fill=\"currentColor\"/> <path d=\"M3.07874 2.98267C3.50002 2.98267 3.84226 3.32411 3.84241 3.74536C3.84241 4.16674 3.50011 4.50903 3.07874 4.50903C2.65748 4.50888 2.31604 4.16665 2.31604 3.74536C2.31619 3.3242 2.65758 2.98281 3.07874 2.98267Z\" fill=\"currentColor\"/></svg>";
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>";
12
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>"};
13
4
  const text = (en, cn) => (document.documentElement.lang || navigator.language || "en").startsWith("zh") ? cn : en;
14
5
  const projectIcons = new Map();
15
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>";
16
7
  const marker = "data-minecodex-unread";
8
+ const queue = createNotificationQueue();
9
+ const native = createNotificationAdapter(config.codex, loadThreadBindings, schedule);
17
10
  const style = document.createElement("style");
18
11
  style.textContent = `
19
- [data-minecodex-task-title] { pointer-events:auto; -webkit-app-region:no-drag; cursor:pointer; border-radius:var(--mcx-radius-lg); min-height:32px; padding-inline:6px; }
20
- [data-minecodex-task-title]:hover { background:var(--mcx-color-background-primary-ghost-hover); }
21
- [data-minecodex-task-title]:focus-visible { outline:2px solid var(--mcx-color-ring); outline-offset:-2px; }
22
- [data-minecodex-task-badge] { display:inline-flex; flex-shrink:0; align-items:center; gap:4px; color:var(--mcx-color-text-secondary); font-family:var(--mcx-font-sans-default); font-size:var(--mcx-text-xs); font-variant-numeric:tabular-nums; position:relative; }
23
- [data-minecodex-task-badge][data-notification]::after { content:""; position:absolute; width:6px; height:6px; right:-2px; top:-1px; border-radius:50%; background:var(--blue-400,#3A83F7); box-shadow:0 0 0 1.5px var(--mcx-color-token-dropdown-background); }
24
- [data-minecodex-task-badge][data-notification=red]::after { background:#FF3B30; }
25
- [data-minecodex-sidebar-original] { display:none !important; }
26
- [data-minecodex-unread=trigger] { -webkit-app-region:no-drag; }
27
- [${marker}=panel] { position:fixed; box-sizing:border-box; width:min(320px,calc(100vw - 16px)); max-height:min(440px,calc(100vh - 64px)); display:flex; flex-direction:column; overflow:hidden; padding:6px 0; border-radius:16px; color:var(--mcx-color-text-primary); font-family:var(--mcx-font-sans-default); font-size:var(--mcx-text-sm); }
28
- [${marker}=panel][hidden] { display:none; }
29
- [${marker}=panel] .mcx-thread-list { min-height:0; overflow-y:auto; overscroll-behavior:contain; padding:0 6px; scrollbar-width:thin; scrollbar-color:var(--mcx-color-border) transparent; }
30
- [${marker}=panel] .mcx-thread-list:hover, [${marker}=panel] .mcx-thread-list:active { scrollbar-color:var(--color-border-strong) transparent; }
31
- [${marker}=panel] h2 { flex-shrink:0; margin:8px 16px; font-size:var(--mcx-text-sm); font-weight:500; color:var(--mcx-color-text-tertiary); }
32
- [${marker}=panel] [data-unread-row] { display:flex; flex-direction:column; gap:4px; width:100%; padding:10px 12px; box-sizing:border-box; border:0; border-radius:10px; background:transparent; text-align:start; color:var(--mcx-color-text-primary); font:inherit; cursor:pointer; }
33
- [${marker}=panel] [data-unread-row][aria-current=page] .mcx-thread-title::before { content:""; display:inline-block; width:5px; height:5px; margin-inline-end:6px; border-radius:50%; background:currentColor; vertical-align:middle; }
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; }
34
24
  [${marker}=panel] [data-unread-row]:hover { background:var(--mcx-color-background-primary-ghost-hover); }
35
- [${marker}=panel] [data-unread-row]:focus-visible { outline:2px solid var(--mcx-color-ring); outline-offset:-2px; }
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); }
36
31
  [${marker}=panel] .mcx-thread-header { display:flex; align-items:center; gap:8px; width:100%; min-width:0; }
37
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; }
38
33
  [${marker}=panel] .mcx-device { display:flex; flex-shrink:0; color:var(--mcx-color-text-tertiary); }
39
- [${marker}=panel] .mcx-loading { margin-inline-start:auto; flex-shrink:0; display:flex; align-items:center; color:var(--mcx-color-text-tertiary); }
40
34
  [${marker}=panel] time { margin-inline-start:auto; flex-shrink:0; font-size:var(--mcx-text-xs); color:var(--mcx-color-text-tertiary); }
41
- [${marker}=panel] .mcx-summary { display:block; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; font-size:12px; line-height:16px; 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); }
42
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; }
43
37
  [${marker}=panel] .mcx-project > span:last-child { overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
44
38
  [${marker}=panel] .mcx-project svg, [${marker}=panel] .mcx-project-icon { width:12px; height:12px; flex:none; }
45
39
  [${marker}=panel] .mcx-project-icon > * { width:12px; height:12px; }
46
- [${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; color:var(--mcx-color-chart-blue); background:color-mix(in srgb,var(--mcx-color-chart-blue) 15%,transparent); }
47
- [${marker}=panel] .mcx-attention[data-attention="approval"] { color:var(--color-chart-green); background:color-mix(in srgb,var(--color-chart-green) 20%,transparent); }
48
- [${marker}=panel] .mcx-empty { margin:20px 12px; color:var(--mcx-color-text-tertiary); }
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; } }
49
46
  `;
50
47
  document.head.append(style);
51
- const panel = document.createElement("div");
52
- panel.setAttribute(marker, "panel");
53
- config.ui.surface(panel, "popover");
54
- panel.id = "minecodex-unread-panel";
55
- panel.setAttribute("role", "dialog");
56
- panel.hidden = true;
48
+ const panel=document.createElement("section");
49
+ panel.setAttribute(marker,"panel");
50
+ config.ui.layer(panel,"notification");
51
+ panel.setAttribute("aria-label",text("Chat notifications","会话通知"));
52
+ panel.hidden=true;
57
53
  document.body.append(panel);
58
- let trigger = null, badge = null, binding = null, standalone = false;
59
- let activityFrame = null, activityTimer = null;
60
- let hoverOpenTimer = null, hoverCloseTimer = null;
61
- function cancelHoverTimers() {
62
- clearTimeout(hoverOpenTimer); clearTimeout(hoverCloseTimer);
63
- }
64
- function scheduleHoverOpen() {
65
- cancelHoverTimers();
66
- if (panel.hidden) hoverOpenTimer = setTimeout(open, 150);
67
- }
68
- function scheduleHoverClose() {
69
- cancelHoverTimers();
70
- // 留出跨越入口与浮层间隙的时间。
71
- hoverCloseTimer = setTimeout(() => close(), 250);
72
- }
73
- panel.addEventListener("pointerenter", cancelHoverTimers, { signal });
74
- panel.addEventListener("pointerleave", scheduleHoverClose, { signal });
75
- const recentRows = new Map();
76
- const openedAt = new Map();
77
- const messageUpdates = new Map();
78
- let selectedKey = null;
79
- let restoreActivity = false, signature = "", queued = false, disposed = false;
80
- function activityButton() {
81
- return [...document.querySelectorAll('#app-shell-sidebar button[aria-pressed]')].find(e => /activity|活动|动态/.test(e.getAttribute("aria-label") || ""));
82
- }
83
-
84
-
85
- // 缓存数据与原生导航动作,不保留会被 React 卸载的会话 DOM。
86
-
87
- function notificationColor(records) {
88
- return records.some(item => item.attention) ? "red" : records.some(item => item.unread) ? "blue" : null;
89
- }
90
- function selectThreads(records, recent, now = Date.now(), currentKey = null) {
91
- const rank = item => item.attention === "input" ? 0 : item.attention === "approval" ? 1 : item.unread ? 2 : item.working ? 3 : 4;
92
- return [...new Map(records.map(item => [item.key, item])).values()]
93
- .filter(item => item.key !== currentKey && (item.attention || item.working || item.unread || now - (recent.get(item.key) ?? -Infinity) < 3600000))
94
- .sort((a, b) => rank(a) - rank(b) || (b.timestamp ?? 0) - (a.timestamp ?? 0));
95
- }
96
- function captureCurrentRow() {
97
- const selected = document.querySelector('#app-shell-sidebar [data-app-action-sidebar-thread-selected="true"]');
98
- const id = config.codex.currentThreadId();
99
- selectedKey = selected?.dataset.appActionSidebarThreadId
100
- ?? [...recentRows.keys()].find(key => id && key.endsWith(":" + id)) ?? null;
101
- if (selected) recentRows.set(selectedKey, threadRecord(selected));
102
- if (selectedKey) openedAt.set(selectedKey, Date.now());
103
- for (const [key, time] of openedAt) if (Date.now() - time >= 3600000) {
104
- openedAt.delete(key); recentRows.delete(key); messageUpdates.delete(key);
105
- }
106
- }
107
-
108
-
109
-
54
+ // 折叠装饰在真实卡片后方,展开按钮独立于滚动遮罩。
55
+ const tail=document.createElement("div");
56
+ tail.setAttribute(marker,"tail");tail.setAttribute("aria-hidden","true");
57
+ config.ui.layer(tail,"notification");tail.hidden=true;
58
+ for(let i=0;i<2;i++){const layer=document.createElement("div");config.ui.surface(layer,"popover");tail.append(layer);}
59
+ document.body.insertBefore(tail,panel);
60
+ const more=document.createElement("button");more.type="button";
61
+ more.setAttribute(marker,"more");
62
+ config.ui.layer(more,"notification");
63
+ more.hidden=true;
64
+ document.body.append(more);
65
+ let expanded=false,totalNotifications=0,pageKey=null;
66
+ let collapseAnimation=null;
67
+ more.addEventListener("click",()=>{
68
+ expanded=true;more.hidden=true;tail.hidden=true;sync();
69
+ // 展开后将键盘焦点交给第一条此前隐藏的通知。
70
+ [...cards.values()][3]?.querySelector("[data-unread-row]")?.focus({preventScroll:true});
71
+ });
72
+ const cards=new Map();
73
+ let disposed=false,queued=false,account=null;
110
74
  function relativeTime(timestamp, now = Date.now()) {
111
75
  if (!Number.isFinite(timestamp)) return "";
112
76
  const seconds = Math.max(0, Math.floor((now - timestamp) / 1000));
@@ -121,63 +85,15 @@ function cacheProjectIcons() {
121
85
  if (glyph) projectIcons.set(row.dataset.appActionSidebarProjectId, glyph.cloneNode(true));
122
86
  }
123
87
  }
124
- function close(restoreFocus = false) {
125
- cancelHoverTimers();
126
- cancelAnimationFrame(activityFrame); clearTimeout(activityTimer);
127
- activityFrame = null; activityTimer = null;
128
- panel.hidden = true;
129
- if (trigger?.getAttribute("aria-expanded") !== "false") trigger?.setAttribute("aria-expanded", "false");
130
- if (restoreActivity) {
131
- restoreActivity = false;
132
- const native = activityButton();
133
- if (native?.getAttribute("aria-pressed") === "true") native.click();
134
- }
135
- if (restoreFocus) trigger?.focus();
136
- }
137
- function position() {
138
- if (!trigger) return;
139
- const rect = trigger.getBoundingClientRect();
140
- const width = Math.min(320, innerWidth - 16);
141
- panel.style.left = `${Math.max(8, Math.min(rect.left, innerWidth - width - 8))}px`;
142
- panel.style.top = `${rect.bottom + 8}px`;
143
- panel.style.maxWidth = `${width}px`;
144
- panel.style.maxHeight = `${Math.max(0, Math.min(440, innerHeight - rect.bottom - 24))}px`;
145
- }
146
- function activityRecords() {
147
- captureCurrentRow();
148
- const records = [...document.querySelectorAll('#app-shell-sidebar [data-app-action-sidebar-thread-row]')].map(threadRecord);
149
- const mounted = new Set(records.map(item => item.key));
150
- for (const item of records) if (openedAt.has(item.key)) recentRows.set(item.key, item);
151
- for (const [key, item] of recentRows) if (!mounted.has(key)) records.push({ ...item, working:false, unread:false, attention:null });
152
- return selectThreads(records, openedAt, Date.now(), selectedKey);
153
- }
154
- function syncNotification(details) {
155
- const color = notificationColor(details);
156
- if (color) { if (badge?.dataset.notification !== color) badge?.setAttribute("data-notification", color); }
157
- else badge?.removeAttribute("data-notification");
158
- }
159
- function renderRows(details = activityRecords()) {
160
- syncNotification(details);
161
- if (!details.length) { close(); return; }
162
- const next = JSON.stringify([text("Recent activity", "最近活动"), details, Math.floor(Date.now() / 60000)]);
163
- if (signature === next) return;
164
- signature = next;
165
- const scrollTop = panel.querySelector(".mcx-thread-list")?.scrollTop ?? 0;
166
- const focusedKey = panel.contains(document.activeElement) ? document.activeElement?.dataset.threadKey : null;
167
- panel.replaceChildren();
168
- panel.setAttribute("aria-label", text("Recent activity", "最近活动"));
169
- const heading = document.createElement("h2");
170
- heading.textContent = text("Recent activity", "最近活动");
171
- panel.append(heading);
172
- const list = document.createElement("div");
173
- list.className = "mcx-thread-list";
174
- panel.append(list);
175
- details.forEach(item => {
176
- const card = document.createElement("button");
177
- card.type = "button";
178
- card.dataset.unreadRow = "";
179
- card.dataset.threadKey = item.key;
180
- if (item.current) card.setAttribute("aria-current", "page");
88
+
89
+ function createCard(item) {
90
+ const shell=document.createElement("div");
91
+ shell.className="mcx-notification";
92
+ config.ui.surface(shell,"popover");
93
+ const card=document.createElement("button");
94
+ card.type="button";
95
+ card.dataset.unreadRow="";
96
+ card.dataset.threadKey=item.key;
181
97
  const header = document.createElement("span");
182
98
  header.className = "mcx-thread-header";
183
99
  const title = document.createElement("span");
@@ -188,7 +104,7 @@ function renderRows(details = activityRecords()) {
188
104
  const remote = item.hostId !== "local";
189
105
  device.innerHTML = deviceGlyphs[remote ? "remote" : "local"];
190
106
  device.setAttribute("data-minecodex-tooltip", text("Remote", "远程"));
191
- device.setAttribute("aria-label", device.title);
107
+ device.setAttribute("aria-label", text("Remote", "远程"));
192
108
  header.append(title);
193
109
  if (remote) header.append(device);
194
110
  const elapsed = relativeTime(item.timestamp);
@@ -198,14 +114,6 @@ function renderRows(details = activityRecords()) {
198
114
  label.dataset.attention = item.attention;
199
115
  label.textContent = item.attention === "approval" ? text("Awaiting approval", "等待批准") : text("Needs input", "需要回复");
200
116
  header.append(label);
201
- } else if (item.working) {
202
- const loading = document.createElement("span");
203
- loading.className = "mcx-loading";
204
- loading.setAttribute("role", "status");
205
- loading.setAttribute("aria-label", text("Working", "正在工作"));
206
- // 直接复用当前会话原生 loading,保持动画与 reduced-motion 行为一致。
207
- loading.innerHTML = item.spinner;
208
- header.append(loading);
209
117
  } else if (elapsed) {
210
118
  const time = document.createElement("time");
211
119
  time.dateTime = new Date(item.timestamp).toISOString();
@@ -234,158 +142,165 @@ function renderRows(details = activityRecords()) {
234
142
  project.append(name);
235
143
  card.append(project);
236
144
  }
237
- card.addEventListener("click", () => {
238
- if (!item.current) {
239
- const native = [...document.querySelectorAll('#app-shell-sidebar [data-app-action-sidebar-thread-row]')]
240
- .find(row => row.dataset.appActionSidebarThreadId === item.key);
241
- const activate = native ? threadRecord(native).activate : item.activate;
242
- if (!activate) return;
243
- activate();
244
- }
245
- close();
246
- });
247
- list.append(card);
248
- });
249
- if (!details.length) {
250
- const empty = document.createElement("p");
251
- empty.className = "mcx-empty";
252
- empty.textContent = text("You're all caught up", "暂无未读会话");
253
- list.append(empty);
254
- }
255
- if (focusedKey) [...list.children].find(card => card.dataset.threadKey === focusedKey)?.focus({ preventScroll:true });
256
- list.scrollTop = scrollTop;
257
- }
258
- function open() {
259
- if (!trigger) return;
260
- cacheProjectIcons();
261
- captureCurrentRow();
262
- const native = activityButton();
263
- if (!native) return;
264
- const details = activityRecords();
265
- syncNotification(details);
266
- if (!details.length) return;
267
- panel.hidden = false;
268
- trigger.setAttribute("aria-expanded", "true");
269
- renderRows(details); position();
270
- // 先呈现浮层,下一帧之后再更新原生 Activity,避免同步 React 切换阻塞点击反馈。
271
- if (native.getAttribute("aria-pressed") !== "true") {
272
- activityFrame = requestAnimationFrame(() => {
273
- activityFrame = null;
274
- activityTimer = setTimeout(() => {
275
- activityTimer = null;
276
- if (disposed || panel.hidden) return;
277
- const latest = activityButton();
278
- if (latest?.getAttribute("aria-pressed") === "false") {
279
- restoreActivity = true; latest.click();
280
- }
281
- }, 0);
145
+
146
+ if (item.unread && !item.attention && !item.working) {
147
+ const dot=document.createElement("span");
148
+ dot.className="mcx-unread-dot";
149
+ if (!elapsed) dot.style.marginInlineStart="auto";
150
+ dot.setAttribute("aria-label",text("Unread","未读"));
151
+ header.append(dot);
152
+ }
153
+ const dismiss=document.createElement("button");
154
+ dismiss.type="button";
155
+ dismiss.className="mcx-dismiss";
156
+ dismiss.innerHTML=closeGlyph;
157
+ dismiss.setAttribute("aria-label",text("Dismiss notification","关闭通知"));
158
+ dismiss.addEventListener("click",()=>{queue.dismiss(item.key);schedule();});
159
+ card.addEventListener("click",async()=>{
160
+ if (card.disabled) return;
161
+ card.disabled=true;
162
+ try { const navigated=await shell.record.activate(); if(navigated===false){card.disabled=false;return;} queue.dismiss(item.key); schedule(); }
163
+ catch { card.disabled=false; }
282
164
  });
283
- }
165
+ shell.append(card,dismiss);
166
+ shell.record=item;
167
+ return shell;
284
168
  }
285
- function visible(element) {
286
- return element && !element.closest('[aria-hidden="true"], [inert]') && element.checkVisibility();
169
+ const motion = {duration:320,easing:"cubic-bezier(.22,1,.36,1)"};
170
+ function updateCountdown(card,item) {
171
+ const label=card.querySelector(".mcx-attention");
172
+ if(!label)return;
173
+ const value=item.countdown;
174
+ let counter=label.querySelector(".mcx-countdown");
175
+ if(!value){counter?.remove();delete label.dataset.countdown;label.removeAttribute("aria-label");return;}
176
+ if(!counter){counter=document.createElement("span");counter.className="mcx-countdown";label.append(counter);}
177
+ const remaining=Math.max(0,value.deadlineMs-Date.now());
178
+ const seconds=Math.ceil(remaining/1000);
179
+ counter.textContent=seconds+text("s","秒");
180
+ label.dataset.countdown="";
181
+ label.style.setProperty("--countdown-progress",String(Math.min(1,remaining/value.durationMs)));
182
+ label.setAttribute("aria-label",text(`Needs input, ${seconds} seconds remaining`,`需要回复,剩余 ${seconds} 秒`));
287
183
  }
288
- const nativeSidebarIcons = new Map();
289
- function restoreSidebarIcons() {
290
- for (const [original, replacement] of nativeSidebarIcons) {
291
- original.removeAttribute("data-minecodex-sidebar-original"); replacement.remove();
184
+ function updateStackViewport() {
185
+ const rows=[...cards.values()].filter(card=>card.isConnected).sort((a,b)=>a.offsetTop-b.offsetTop);
186
+ const collapsedMore=!expanded&&totalNotifications>3;
187
+ panel.style.maxHeight=`${Math.max(0,stackAvailableHeight-12-(collapsedMore?54:0))}px`;
188
+ const bottom=panel.scrollTop+panel.clientHeight;
189
+ const overflow=panel.scrollHeight>panel.clientHeight+1;
190
+ const atBottom=bottom>=panel.scrollHeight-1;
191
+ const edge=rows.find(card=>card.offsetTop+card.offsetHeight>=bottom)??rows.at(-1);
192
+ const fade=Math.min((edge?.offsetHeight??80)/2,panel.clientHeight/3);
193
+ panel.style.maskImage=expanded&&!panel.hidden&&overflow&&!atBottom ? `linear-gradient(to bottom,black calc(100% - ${fade}px),transparent 100%)` : "none";
194
+ more.hidden=tail.hidden=panel.hidden||!collapsedMore;
195
+ if(collapsedMore&&!panel.hidden&&!collapseAnimation){
196
+ const rect=panel.getBoundingClientRect();
197
+ tail.style.left=`${rect.left+6}px`;tail.style.top=`${rect.bottom-32}px`;tail.style.width=`${rect.width-12}px`;
198
+ more.style.left=`${rect.left+rect.width/2}px`;more.style.top=`${rect.bottom+18}px`;
199
+ const count=totalNotifications-3;
200
+ const label=text(`${count} more ${count===1?"notification":"notifications"}`,`还有 ${count} 条通知`);
201
+ if(more.textContent!==label)more.textContent=label;
202
+ more.setAttribute("aria-expanded","false");
292
203
  }
293
- nativeSidebarIcons.clear();
294
204
  }
295
- function syncSidebarIcons(toggle) {
296
- for (const [original, replacement] of nativeSidebarIcons) {
297
- if (!original.isConnected || !toggle.contains(original)) {
298
- original.removeAttribute("data-minecodex-sidebar-original"); replacement.remove(); nativeSidebarIcons.delete(original);
299
- }
205
+ let stackAvailableHeight=innerHeight;
206
+ panel.addEventListener("scroll",updateStackViewport,{signal,passive:true});
207
+ function renderRows(records) {
208
+ const reducedMotion=window.matchMedia("(prefers-reduced-motion: reduce)").matches;
209
+ 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]);
210
+ const ordered=[...panel.children].filter(card=>cards.get(card.record?.key)===card);
211
+ if(records.length===cards.size && records.every((item,index)=>ordered[index]===cards.get(item.key)&&cards.get(item.key)?.signature===signatureFor(item))){
212
+ records.forEach(item=>{const card=cards.get(item.key);card.record=item;updateCountdown(card,item);});
213
+ updateStackViewport();return;
300
214
  }
301
- for (const original of toggle.querySelectorAll('svg:not([data-minecodex-sidebar-replacement])')) {
302
- if (nativeSidebarIcons.has(original)) continue;
303
- const glyph = sidebarGlyphs[original.getAttribute("width")];
304
- if (!glyph) continue;
305
- const container = document.createElement("span");
306
- container.innerHTML = glyph;
307
- const replacement = container.firstElementChild;
308
- replacement.setAttribute("class", original.getAttribute("class") || "");
309
- replacement.setAttribute("data-minecodex-sidebar-replacement", "");
310
- replacement.setAttribute("data-no-autosize", "true");
311
- // 不移除 React 所有的 SVG,只切换显示并插入独立资源。
312
- original.setAttribute("data-minecodex-sidebar-original", "");
313
- original.after(replacement); nativeSidebarIcons.set(original, replacement);
215
+ const positions=new Map([...cards].map(([key,card])=>[key,card.getBoundingClientRect().top]));
216
+ // 从当前视觉位置接续,避免连续状态更新时叠加 transform 动画。
217
+ for(const card of cards.values()) { card.moveAnimation?.cancel(); card.enterAnimation?.cancel(); }
218
+ const keys=new Set(records.map(item=>item.key));
219
+ for(const [key,card] of cards) if(!keys.has(key)){
220
+ const top=card.offsetTop,width=card.offsetWidth;
221
+ cards.delete(key);
222
+ if(reducedMotion||panel.hidden){card.remove();continue;}
223
+ card.style.cssText+=`;position:absolute;top:${top}px;width:${width}px;pointer-events:none;`;
224
+ card.inert=true;
225
+ card.animate([{opacity:1,transform:"translateX(0)"},{opacity:0,transform:"translateX(-10px)"}],{duration:180,easing:"ease-out"}).finished.then(()=>{card.remove();schedule();},()=>card.remove());
314
226
  }
315
- }
316
- function detachTrigger() {
317
- restoreSidebarIcons();
318
- binding?.abort(); binding = null;
319
- if (standalone) trigger?.remove();
320
- else if (trigger) {
321
- for (const name of ["data-minecodex-task-title", "data-minecodex-unread", "role", "tabindex", "aria-haspopup", "aria-controls", "aria-expanded", "aria-label"]) trigger.removeAttribute(name);
227
+ records.forEach((item,index)=>{
228
+ const signature=signatureFor(item);
229
+ let card=cards.get(item.key),fresh=!card;
230
+ if(!card){card=createCard(item);cards.set(item.key,card);}
231
+ else if(card.signature!==signature){
232
+ // 保留容器、点击目标与键盘焦点;仅更新内容,不重播整卡入场。
233
+ const next=createCard(item),button=card.querySelector("[data-unread-row]");
234
+ button.replaceChildren(...next.querySelector("[data-unread-row]").childNodes);
235
+ if(!reducedMotion)button.animate([{opacity:.65},{opacity:1}],{duration:180,easing:"ease-out"});
236
+ }
237
+ card.signature=signature;card.record=item;
238
+ if(panel.children[index]!==card)panel.insertBefore(card,panel.children[index]??null);
239
+ updateCountdown(card,item);
240
+ if(fresh&&!reducedMotion)card.enterAnimation=card.animate([{opacity:0,transform:"translateX(-12px)"},{opacity:1,transform:"translateX(0)"}],motion);
241
+ });
242
+ updateStackViewport();
243
+ if(!reducedMotion) for(const [key,card] of cards){
244
+ const delta=positions.get(key)-card.getBoundingClientRect().top;
245
+ if(Number.isFinite(delta)&&Math.abs(delta)>.5)card.moveAnimation=card.animate([{transform:`translateY(${delta}px)`},{transform:"translateY(0)"}],motion);
322
246
  }
323
- badge?.remove(); badge = null; trigger = null;
247
+ }
248
+ function animateCollapse(before) {
249
+ if(window.matchMedia("(prefers-reduced-motion: reduce)").matches)return;
250
+ const after=panel.getBoundingClientRect();
251
+ const distance=before.height-after.height;
252
+ if(distance<=1)return;
253
+ panel.style.overflow="hidden";
254
+ 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);
255
+ for(const node of [tail,more]) if(!node.hidden)node.animate([
256
+ {opacity:0,translate:`0 ${distance}px`},
257
+ {opacity:1,translate:"0 0"}
258
+ ],motion);
259
+ const animation=collapseAnimation;
260
+ animation.finished.then(()=>{
261
+ if(collapseAnimation!==animation)return;
262
+ collapseAnimation=null;panel.style.overflow="";updateStackViewport();
263
+ },()=>{});
324
264
  }
325
265
  function sync() {
326
- if (disposed) return;
327
- cacheProjectIcons();
328
- captureCurrentRow();
329
- const sidebarToggle = [...document.querySelectorAll("[data-app-shell-sidebar-trigger]")].find(visible);
330
- if (!sidebarToggle || sidebarToggle.getAttribute("aria-expanded") !== "false") {
331
- close(); detachTrigger(); return;
332
- }
333
- const title = [...document.querySelectorAll('[data-app-shell-titlebar-slot="main"] [data-app-shell-titlebar-content]')]
334
- .find(e => visible(e) && !e.querySelector("button, [role=tab], [data-app-shell-tab-row]"));
335
- const tabs = [...document.querySelectorAll('[data-app-shell-tab-row][role=presentation]')].find(visible);
336
- const target = title || tabs;
337
- if (!target) { close(); detachTrigger(); return; }
338
- const needsStandalone = !title;
339
- if (!trigger?.isConnected || standalone !== needsStandalone || (title ? trigger !== title : trigger.parentElement !== target)) {
340
- close(); detachTrigger(); standalone = needsStandalone;
341
- binding = new AbortController();
342
- if (title) {
343
- trigger = title;
344
- trigger.setAttribute("data-minecodex-task-title", "");
345
- trigger.setAttribute("role", "button");
346
- trigger.tabIndex = 0;
347
- } else {
348
- trigger = document.createElement("button");
349
- trigger.type = "button";
350
- trigger.className = "no-drag pointer-events-auto flex items-center justify-center gap-1 rounded-lg text-tertiary hover:bg-primary-ghost-hover button-toolbar px-2";
351
- target.prepend(trigger);
352
- }
353
- trigger.setAttribute(marker, "trigger");
354
- trigger.setAttribute("aria-haspopup", "dialog");
355
- trigger.setAttribute("aria-controls", panel.id);
356
- trigger.setAttribute("aria-expanded", "false");
357
- badge = document.createElement("span");
358
- badge.setAttribute("data-minecodex-task-badge", "");
359
- badge.innerHTML = taskGlyph;
360
- trigger.prepend(badge);
361
- trigger.addEventListener("click", event => { event.stopPropagation(); if (panel.hidden) open(); }, {signal:binding.signal});
362
- trigger.addEventListener("pointerenter", scheduleHoverOpen, { signal:binding.signal });
363
- trigger.addEventListener("pointerleave", scheduleHoverClose, { signal:binding.signal });
364
- trigger.addEventListener("keydown", event => {
365
- if (["Enter", " ", "ArrowDown"].includes(event.key)) {
366
- event.preventDefault(); event.stopPropagation();
367
- if (event.key === "ArrowDown" || panel.hidden) open(); else close();
368
- }
369
- }, {signal:binding.signal});
266
+ if(disposed)return;
267
+ const layout=native.layout(),snapshot=native.snapshot();
268
+ const changedPage=layout.pageKey!=null&&pageKey!=null&&layout.pageKey!==pageKey;
269
+ const collapseFrom=changedPage&&expanded&&!panel.hidden?panel.getBoundingClientRect():null;
270
+ if(layout.pageKey!=null)pageKey=layout.pageKey;
271
+ if(changedPage){
272
+ expanded=false;collapseAnimation?.cancel();collapseAnimation=null;panel.style.overflow="";
273
+ panel.scrollTop=0;
370
274
  }
371
- syncSidebarIcons(sidebarToggle);
372
- const details = activityRecords();
373
- syncNotification(details);
374
- const label = text("Recent activity", "最近活动");
375
- if (trigger.getAttribute("aria-label") !== label) trigger.setAttribute("aria-label", label);
376
- if (!panel.hidden) { renderRows(details); position(); }
275
+ if(!snapshot){queue.clear();panel.replaceChildren();cards.clear();panel.hidden=true;more.hidden=true;tail.hidden=true;expanded=false;return;}
276
+ if(account!==snapshot.account){queue.clear();expanded=false;account=snapshot.account;}
277
+ cacheProjectIcons();
278
+ const details=queue.update(snapshot.records.slice().sort((a,b)=>(a.timestamp??0)-(b.timestamp??0)),snapshot.currentKey);
279
+ totalNotifications=details.length;
280
+ if(!details.length)expanded=false;
281
+ panel.hidden=!layout.visible||(!details.length&&!cards.size);
282
+ if(!details.length){renderRows([]);return;}
283
+ if(panel.hidden){more.hidden=true;tail.hidden=true;return;}
284
+ const width=Math.min(320,Math.max(0,innerWidth-layout.left-12));
285
+ panel.style.left=`${layout.left}px`;
286
+ panel.style.top=`${layout.top}px`;
287
+ panel.style.width=`${width}px`;
288
+ stackAvailableHeight=Math.max(0,innerHeight-layout.top-12);
289
+ panel.setAttribute("aria-label",text("Chat notifications","会话通知"));
290
+ renderRows(expanded?details:details.slice(0,3));
291
+ if(collapseFrom)animateCollapse(collapseFrom);
292
+ }
293
+ function schedule() {
294
+ if(disposed||queued)return;
295
+ queued=true;
296
+ queueMicrotask(()=>{queued=false;sync();});
377
297
  }
378
- const observer = new MutationObserver(() => {
379
- if (queued) return;
380
- queued = true;
381
- queueMicrotask(() => { queued = false; sync(); });
298
+ const observer=new MutationObserver(mutations=>{
299
+ if(mutations.some(m=>!panel.contains(m.target)&&m.target!==style&&!more.contains(m.target)&&!tail.contains(m.target)))schedule();
382
300
  });
383
- observer.observe(document.documentElement, { childList:true, subtree:true, attributes:true, attributeFilter:["aria-expanded", "aria-pressed", "aria-hidden", "data-app-shell-header-layout", "data-app-action-sidebar-thread-selected", "lang"] });
384
- document.addEventListener("pointerdown", event => { if (!trigger?.contains(event.target) && !panel.contains(event.target)) close(); }, { signal });
385
- document.addEventListener("keydown", event => { if (event.key === "Escape" && !panel.hidden) { event.preventDefault(); close(true); } }, { signal });
386
- window.addEventListener("resize", position, { signal });
387
- const refreshTimer = setInterval(sync, 30000);
301
+ observer.observe(document.documentElement,{subtree:true,childList:true,attributes:true,attributeFilter:["inert","aria-hidden","aria-expanded","data-app-shell-sidebar-open","data-slate-sidebar-peeking","lang"]});
302
+ window.addEventListener("resize",schedule,{signal});
303
+ // 隐藏的原生视图不一定重绘;每秒读取 store,不发送请求或切换 Activity。
304
+ const timer=setInterval(schedule,1000);
388
305
  sync();
389
- return () => {
390
- disposed = true; clearInterval(refreshTimer); observer.disconnect(); close(); detachTrigger(); panel.remove(); style.remove();
391
- };
306
+ return ()=>{disposed=true;clearInterval(timer);observer.disconnect();native.dispose();queue.clear();panel.remove();more.remove();tail.remove();style.remove();};
@@ -4,8 +4,8 @@
4
4
  "version": "0.1.0",
5
5
  "label": { "en": "Usage Meter", "zh-CN": "额度环" },
6
6
  "description": {
7
- "en": "Show remaining weekly and 5-hour limits above Avatar and open Usage settings.",
8
- "zh-CN": "在 Avatar 上方显示周与五小时剩余额度,点击进入 Usage 设置。"
7
+ "en": "Check your remaining weekly and five-hour allowance.",
8
+ "zh-CN": "随时查看每周与五小时剩余额度。"
9
9
  },
10
10
  "entry": { "kind": "page-script", "script": "src/usage-meter.mjs", "sourceFactory": "src/injection-source.mjs" },
11
11
  "icon": { "viewBox": "0 0 24 24", "markup": "<circle cx=\"12\" cy=\"12\" r=\"9\"></circle><circle cx=\"12\" cy=\"12\" r=\"5\"></circle>" },