minecodex 1.2.2 → 2.0.0

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 (87) hide show
  1. package/README.md +9 -3
  2. package/features/images/README.md +16 -98
  3. package/features/images/codex-feature.json +6 -23
  4. package/features/images/src/library-prompt-page.mjs +239 -0
  5. package/features/model-slider/THIRD_PARTY_NOTICES.md +1 -1
  6. package/features/notes/DESIGN.md +66 -0
  7. package/features/notes/README.md +18 -99
  8. package/features/notes/assets/icons/book-open-nav.svg +15 -0
  9. package/features/notes/assets/icons/book-open.svg +16 -0
  10. package/features/notes/assets/native-emojis.json +1 -0
  11. package/features/notes/assets/native-prompt-icons.json +28 -0
  12. package/features/notes/codex-feature.json +12 -70
  13. package/features/notes/src/main.mjs +2 -2
  14. package/features/notes/src/product.mjs +287 -613
  15. package/features/notes/src/prompt-schema.mjs +117 -0
  16. package/features/notes/src/quick-prompts-page.mjs +1559 -0
  17. package/features/notes/src/quick-prompts-server.mjs +167 -0
  18. package/features/session-status/DESIGN.md +10 -2
  19. package/features/session-status/README.md +8 -4
  20. package/features/session-status/src/session-status.mjs +268 -24
  21. package/features/sidebar-focus/DESIGN.md +16 -0
  22. package/features/sidebar-focus/codex-feature.json +20 -0
  23. package/features/sidebar-focus/src/sidebar-focus.mjs +134 -0
  24. package/features/sidebar-focus/vendor/lucide-LICENSE +43 -0
  25. package/features/unread-inbox/DESIGN.md +28 -0
  26. package/features/unread-inbox/codex-feature.json +20 -0
  27. package/features/unread-inbox/src/unread-inbox.mjs +360 -0
  28. package/features/usage-meter/DESIGN.md +49 -0
  29. package/features/usage-meter/README.md +18 -0
  30. package/features/usage-meter/codex-feature.json +13 -0
  31. package/features/usage-meter/src/usage-meter.mjs +200 -0
  32. package/package.json +17 -5
  33. package/packages/cli/src/config.mjs +4 -1
  34. package/packages/cli/src/control-server.mjs +2 -2
  35. package/packages/runtime-host/src/codex-injection.mjs +38 -3
  36. package/packages/runtime-host/src/codex-runtime.mjs +41 -20
  37. package/packages/runtime-host/src/feature-registry.mjs +1 -0
  38. package/packages/runtime-host/src/session-status-reader.mjs +40 -16
  39. package/features/notes/assets/icons/circle.svg +0 -15
  40. package/features/notes/assets/icons/ellipsis.svg +0 -17
  41. package/features/notes/assets/icons/external-link.svg +0 -17
  42. package/features/notes/assets/icons/list-todo.svg +0 -19
  43. package/features/notes/assets/icons/message-square-quote.svg +0 -17
  44. package/features/notes/assets/icons/paperclip.svg +0 -15
  45. package/features/notes/assets/icons/star-off.svg +0 -17
  46. package/features/notes/assets/icons/star.svg +0 -15
  47. package/features/notes/assets/icons/sticky-note.svg +0 -16
  48. package/features/notes/assets/icons/trash-2.svg +0 -19
  49. package/features/notes/src/http-server.mjs +0 -580
  50. package/features/notes/web/app.js +0 -1202
  51. package/features/notes/web/codex-artifact-document.svg +0 -1
  52. package/features/notes/web/codex-attachment.svg +0 -1
  53. package/features/notes/web/codex-build.svg +0 -1
  54. package/features/notes/web/codex-code.svg +0 -1
  55. package/features/notes/web/codex-cplusplus.svg +0 -1
  56. package/features/notes/web/codex-css.svg +0 -1
  57. package/features/notes/web/codex-document.svg +0 -1
  58. package/features/notes/web/codex-file.svg +0 -1
  59. package/features/notes/web/codex-folder.svg +0 -1
  60. package/features/notes/web/codex-hashes.svg +0 -1
  61. package/features/notes/web/codex-html.svg +0 -1
  62. package/features/notes/web/codex-image.svg +0 -1
  63. package/features/notes/web/codex-java.svg +0 -1
  64. package/features/notes/web/codex-javascript.svg +0 -1
  65. package/features/notes/web/codex-json.svg +0 -1
  66. package/features/notes/web/codex-notebook.svg +0 -1
  67. package/features/notes/web/codex-pdf.svg +0 -1
  68. package/features/notes/web/codex-pencil.svg +0 -3
  69. package/features/notes/web/codex-php.svg +0 -1
  70. package/features/notes/web/codex-plus.svg +0 -3
  71. package/features/notes/web/codex-presentation.svg +0 -1
  72. package/features/notes/web/codex-python.svg +0 -1
  73. package/features/notes/web/codex-react.svg +0 -1
  74. package/features/notes/web/codex-rust.svg +0 -1
  75. package/features/notes/web/codex-shell.svg +0 -1
  76. package/features/notes/web/codex-skill.svg +0 -1
  77. package/features/notes/web/codex-spreadsheet.svg +0 -1
  78. package/features/notes/web/codex-task.svg +0 -3
  79. package/features/notes/web/codex-terminal.svg +0 -1
  80. package/features/notes/web/codex-toml.svg +0 -1
  81. package/features/notes/web/codex-typescript.svg +0 -1
  82. package/features/notes/web/codex-view-all.svg +0 -3
  83. package/features/notes/web/file-icons.mjs +0 -68
  84. package/features/notes/web/i18n.mjs +0 -237
  85. package/features/notes/web/index.html +0 -15
  86. package/features/notes/web/styles.css +0 -1155
  87. package/features/notes/web/todo-drag.mjs +0 -18
@@ -0,0 +1,1559 @@
1
+ const ICONS = {
2
+ prompt:
3
+ '<svg\n class="lucide lucide-book-open"\n xmlns="http://www.w3.org/2000/svg"\n width="24"\n height="24"\n viewBox="0 0 24 24"\n fill="none"\n stroke="currentColor"\n stroke-width="2.1"\n stroke-linecap="round"\n stroke-linejoin="round"\n>\n <path d="M12 7v14" />\n <path d="M3 18a1 1 0 0 1-1-1V4a1 1 0 0 1 1-1h5a4 4 0 0 1 4 4 4 4 0 0 1 4-4h5a1 1 0 0 1 1 1v13a1 1 0 0 1-1 1h-6a3 3 0 0 0-3 3 3 3 0 0 0-3-3z" />\n</svg>',
4
+ chevron:
5
+ '<!-- @license lucide-static v0.577.0 - ISC -->\n<svg\n class="lucide lucide-chevron-down"\n xmlns="http://www.w3.org/2000/svg"\n width="24"\n height="24"\n viewBox="0 0 24 24"\n fill="none"\n stroke="currentColor"\n stroke-width="2"\n stroke-linecap="round"\n stroke-linejoin="round"\n>\n <path d="m6 9 6 6 6-6" />\n</svg>\n',
6
+ search:
7
+ '<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16" fill="none" aria-hidden="true"><path fill-rule="evenodd" clip-rule="evenodd" d="M7.32849 1.91016C10.2877 1.91016 12.6867 4.30938 12.6869 7.26855C12.6869 8.57363 12.2192 9.7688 11.4437 10.6982L14.0433 13.2979C14.2481 13.5028 14.2481 13.8351 14.0433 14.04C13.8384 14.245 13.5062 14.2449 13.3011 14.04L10.6957 11.4346C9.77536 12.1793 8.60462 12.627 7.32849 12.627C4.36931 12.6268 1.97009 10.2278 1.97009 7.26855C1.97027 4.30949 4.36942 1.91033 7.32849 1.91016ZM7.32849 2.96094C4.94932 2.96111 3.02105 4.88939 3.02087 7.26855C3.02087 9.64787 4.94921 11.577 7.32849 11.5771C9.70792 11.5771 11.6371 9.64798 11.6371 7.26855C11.6369 4.88928 9.70781 2.96094 7.32849 2.96094Z" fill="currentColor"/></svg>',
8
+ "arrow-down-wide-narrow":
9
+ '<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16" fill="none" aria-hidden="true"><path d="M4.66722 3.14218C4.95711 3.14225 5.19261 3.37767 5.19261 3.66757V11.6295L6.95824 9.82578C7.16093 9.61896 7.49327 9.61573 7.70043 9.81796C7.90753 10.0208 7.91092 10.353 7.70824 10.5602L5.27953 13.0406L5.2141 13.1012C4.89729 13.3642 4.43507 13.3645 4.1184 13.1012L4.05297 13.0406L1.62523 10.5602C1.4228 10.353 1.42608 10.0207 1.63304 9.81796C1.8401 9.61558 2.17245 9.61912 2.37523 9.82578L4.14183 11.6295V3.66757C4.14183 3.37762 4.37727 3.14218 4.66722 3.14218Z" fill="currentColor"/> <path d="M10.7854 2.98203C11.1021 2.71876 11.5643 2.719 11.8811 2.98203L11.9465 3.04257L14.3752 5.52304C14.5779 5.73021 14.5745 6.06242 14.3674 6.26523C14.1603 6.46786 13.828 6.46437 13.6252 6.25742L11.8586 4.45175V12.3336C11.8584 12.6233 11.623 12.859 11.3332 12.859C11.0436 12.8588 10.8081 12.6232 10.8078 12.3336V4.45371L9.04222 6.25742C8.83948 6.46447 8.50723 6.46775 8.30004 6.26523C8.093 6.06236 8.08941 5.73017 8.29222 5.52304L10.72 3.04257L10.7854 2.98203Z" fill="currentColor"/></svg>',
10
+ check:
11
+ '<!-- @license lucide-static v0.577.0 - ISC -->\n<svg\n class="lucide lucide-check"\n xmlns="http://www.w3.org/2000/svg"\n width="24"\n height="24"\n viewBox="0 0 24 24"\n fill="none"\n stroke="currentColor"\n stroke-width="2"\n stroke-linecap="round"\n stroke-linejoin="round"\n>\n <path d="M20 6 9 17l-5-5" />\n</svg>\n',
12
+ x: '<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>',
13
+ back: '<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16" fill="none" aria-hidden="true"><path d="M9.87117 11.6289C10.0762 11.8339 10.0762 12.166 9.87117 12.3711C9.66614 12.5761 9.334 12.5761 9.12898 12.3711L5.36433 8.60641C5.0296 8.27129 5.0296 7.72863 5.36433 7.39351L9.12898 3.62887C9.334 3.42384 9.66614 3.42384 9.87117 3.62887C10.0762 3.83389 10.0762 4.16603 9.87117 4.37105L6.24226 7.99996L9.87117 11.6289Z" fill="currentColor"/></svg>',
14
+ settings:
15
+ '<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16" fill="none" aria-hidden="true"><path fill-rule="evenodd" clip-rule="evenodd" d="M6.33374 8.97461C7.54786 8.97484 8.56163 9.83236 8.80347 10.9746H13.3337C13.6235 10.9749 13.8591 11.2102 13.8591 11.5C13.8591 11.7898 13.6235 12.0251 13.3337 12.0254H8.80347C8.56163 13.1676 7.54786 14.0252 6.33374 14.0254C5.11907 14.0254 4.10455 13.1673 3.86304 12.0244L2.66675 12.0254C2.37691 12.0253 2.14138 11.7899 2.14136 11.5C2.14136 11.2102 2.37693 10.9748 2.66675 10.9746H3.86304C4.10491 9.83221 5.11941 8.97461 6.33374 8.97461ZM6.33374 10.0254C5.51912 10.0254 4.85913 10.6854 4.85913 11.5C4.85913 12.3146 5.51912 12.9746 6.33374 12.9746C7.14814 12.9743 7.80835 12.3145 7.80835 11.5C7.80835 10.6855 7.14814 10.0257 6.33374 10.0254Z" fill="currentColor"/> <path fill-rule="evenodd" clip-rule="evenodd" d="M9.66675 1.97461C10.881 1.97461 11.8935 2.83236 12.1355 3.97461H13.3328C13.6227 3.97459 13.8581 4.21005 13.8582 4.5C13.8581 4.78993 13.6227 5.02537 13.3328 5.02539H12.1365C11.8946 6.16774 10.881 7.02539 9.66675 7.02539C8.45245 7.02539 7.43893 6.16774 7.19702 5.02539H2.66675C2.3768 5.02539 2.14136 4.78995 2.14136 4.5C2.14136 4.21005 2.3768 3.97461 2.66675 3.97461H7.19702C7.43893 2.83226 8.45245 1.97461 9.66675 1.97461ZM9.66675 3.02539C8.85213 3.02539 8.19214 3.68538 8.19214 4.5C8.19214 5.31462 8.85213 5.97461 9.66675 5.97461C10.4814 5.97461 11.1414 5.31462 11.1414 4.5C11.1414 3.68538 10.4814 3.02539 9.66675 3.02539Z" fill="currentColor"/></svg>',
16
+ drag: '<svg\n xmlns="http://www.w3.org/2000/svg"\n width="24"\n height="24"\n viewBox="0 0 24 24"\n fill="none"\n stroke="currentColor"\n stroke-width="2"\n stroke-linecap="round"\n stroke-linejoin="round"\n>\n <path d="M4 5h16" />\n <path d="M4 12h16" />\n <path d="M4 19h16" />\n</svg>\n',
17
+ "prompt-nav":
18
+ '<svg\n class="lucide lucide-book-open"\n xmlns="http://www.w3.org/2000/svg"\n width="20"\n height="20"\n viewBox="-1.5 -1.5 27 27"\n fill="none"\n stroke="currentColor"\n stroke-width="1.8"\n stroke-linecap="round"\n stroke-linejoin="round"\n>\n <path d="M12 7v14 M3 18a1 1 0 0 1-1-1V4a1 1 0 0 1 1-1h5a4 4 0 0 1 4 4 4 4 0 0 1 4-4h5a1 1 0 0 1 1 1v13a1 1 0 0 1-1 1h-6a3 3 0 0 0-3 3 3 3 0 0 0-3-3z" />\n</svg>',
19
+ };
20
+ const MARKER = "data-minecodex-quick-prompts";
21
+ const zh = () => (document.documentElement.lang || navigator.language || "en").toLowerCase().startsWith("zh");
22
+ const text = (en, cn) => (zh() ? cn : en);
23
+ const style = document.createElement("style");
24
+ style.textContent = `
25
+ [${MARKER}=page] .pm-title-control { display:flex; align-items:stretch; min-height:40px; border:1px solid var(--color-border-primary-outline); border-radius:var(--radius-lg); background:var(--color-background-primary-soft); overflow:hidden; }
26
+ [${MARKER}=page] .pm-title-control:focus-within { border-color:var(--color-ring); }
27
+ [${MARKER}=page] .pm-title-control>.pm-emoji-trigger { display:flex; flex:none; align-items:center; justify-content:center; width:40px; padding:0; border-inline-end:1px solid var(--color-border); }
28
+ [${MARKER}=page] .pm-title-control>.pm-emoji-trigger:hover { background:var(--color-background-primary-ghost-hover); }
29
+ [${MARKER}=page] .pm-title-control>.pm-emoji-trigger:focus-visible { outline-offset:-3px; }
30
+ [${MARKER}=page] .pm-editor .pm-title-control>input { flex:1; min-width:0; border:0; border-radius:0; background:transparent; padding:8px 10px; outline:none; box-shadow:none; }
31
+
32
+ [${MARKER}=panel] { position:fixed; z-index:50; zoom:var(--codex-window-zoom,1); transform:translateY(-100%); box-sizing:border-box; display:flex; flex-direction:column; overflow:hidden; padding:var(--spacing); border:1px solid var(--color-border); border-radius:var(--radius-suggestion-menu,var(--radius-2xl)); background:var(--color-surface-elevated-secondary); color:var(--color-text-primary); font-family:var(--font-sans-default); font-size:var(--text-sm); line-height:1.43; }
33
+ [${MARKER}=panel] *, [${MARKER}=button] { box-sizing:border-box; }
34
+ [${MARKER}=panel] button { cursor:var(--cursor-interaction,pointer); }
35
+ [${MARKER}=panel] button:disabled { opacity:.4; cursor:default; }
36
+ [${MARKER}=panel] button:focus-visible, [${MARKER}=panel] input:focus-visible, [${MARKER}=panel] textarea:focus-visible { outline:2px solid var(--color-ring,var(--color-text-secondary)); outline-offset:-2px; }
37
+ [${MARKER}=panel] svg { width:16px; height:16px; flex:none; }
38
+ [${MARKER}=panel] .qp-header { display:flex; align-items:center; justify-content:space-between; padding:var(--spacing) var(--padding-row-x,8px); color:var(--color-text-tertiary); flex:none; }
39
+ [${MARKER}=panel] .qp-scroll { overflow-y:auto; min-height:0; padding-bottom:var(--spacing); }
40
+ [${MARKER}=panel] .qp-row { display:flex; align-items:center; min-width:0; border-radius:var(--suggestion-menu-item-radius,var(--radius-xl)); }
41
+ [${MARKER}=panel] .qp-row:hover, [${MARKER}=panel] .qp-row:focus-within { background:var(--color-background-primary-ghost-hover); }
42
+ [${MARKER}=panel] .qp-select { display:flex; flex:1; align-items:center; min-width:0; gap:calc(var(--spacing)*1.5); text-align:left; padding:var(--suggestion-menu-item-padding,var(--app-menu-item-padding,var(--padding-row-y,5px) var(--padding-row-x,8px))); border-radius:inherit; }
43
+ [${MARKER}=panel] .qp-title { white-space:nowrap; overflow:hidden; text-overflow:ellipsis; }
44
+ [${MARKER}=panel] .qp-preview { flex:1; min-width:0; white-space:nowrap; overflow:hidden; text-overflow:ellipsis; color:var(--color-text-tertiary); }
45
+ [${MARKER}=panel] .qp-empty { padding:calc(var(--spacing)*3) var(--padding-row-x,8px); color:var(--color-text-tertiary); }
46
+ [${MARKER}=panel] .qp-command { padding:var(--spacing) calc(var(--spacing)*2); border-radius:var(--radius-xl); }
47
+ [${MARKER}=panel] .qp-command:hover { background:var(--color-background-primary-ghost-hover); }
48
+ [${MARKER}=panel] .qp-error { color:var(--color-text-danger,var(--color-text-primary)); padding:var(--spacing) var(--padding-row-x,8px); }
49
+ .qp-symbol { display:inline-flex; width:20px; height:20px; flex:none; align-items:center; justify-content:center; font-size:18px; line-height:1; color:var(--prompt-icon-light,var(--color-text-secondary)); }
50
+ .qp-symbol svg { width:20px; height:20px; }
51
+ .dark .qp-symbol, [data-theme=dark] .qp-symbol { color:var(--prompt-icon-dark,var(--color-text-secondary)); }
52
+ [${MARKER}=page] { position:fixed; z-index:20; overflow:auto; border-radius:var(--radius-xl-base); background:var(--color-surface); color:var(--color-text-primary); font-family:var(--font-sans-default); font-size:var(--text-sm); line-height:var(--text-sm--line-height); }
53
+ [${MARKER}=page] * { box-sizing:border-box; }
54
+ [${MARKER}=page] button { cursor:var(--cursor-interaction,pointer); }
55
+ [${MARKER}=page] button:disabled { opacity:.35; cursor:default; }
56
+ [${MARKER}=page] button:focus-visible, [${MARKER}=page] input:focus-visible, [${MARKER}=page] textarea:focus-visible { outline:2px solid var(--color-text-secondary); outline-offset:2px; }
57
+ [${MARKER}=page] .pm-wrap { max-width:1440px; margin:auto; padding:28px 48px 48px; }
58
+ [${MARKER}=page] .pm-heading { display:flex; align-items:center; justify-content:space-between; gap:16px; min-height:36px; margin-bottom:24px; }
59
+ [${MARKER}=page] h1 { font-size:var(--text-heading-lg); font-weight:var(--font-weight-medium); line-height:1.2; margin:0; }
60
+ [${MARKER}=page] h2 { font-size:var(--text-base,14px); font-weight:600; margin:24px 0 12px; }
61
+ [${MARKER}=page] .pm-header-actions { display:flex; align-items:center; gap:8px; }
62
+ [${MARKER}=page] .pm-tab-row { display:flex; align-items:center; gap:8px; margin-bottom:24px; }
63
+ [${MARKER}=page] .pm-tab-row .pm-tabs { margin-bottom:0; min-width:0; }
64
+ [${MARKER}=page] .pm-icon { display:flex; align-items:center; justify-content:center; width:32px; height:32px; flex:none; padding:0; border-radius:var(--radius-full); color:var(--color-text-secondary); }
65
+ [${MARKER}=page] .pm-icon[hidden] { display:none; }
66
+ [${MARKER}=page] .pm-icon svg { width:16px; height:16px; }
67
+ [${MARKER}=page] .pm-icon:hover, [${MARKER}=page] .pm-icon[aria-expanded=true], [${MARKER}=page] .pm-icon[aria-pressed=true] { background:var(--color-background-primary-ghost-hover); color:var(--color-text-primary); }
68
+ [${MARKER}=page] .pm-popup { position:fixed; z-index:30; width:200px; padding:var(--app-menu-gutter,var(--spacing)); border-radius:var(--radius-2xl); background:color-mix(in srgb,var(--color-surface-elevated-secondary) 90%,transparent); box-shadow:0 0 0 .5px var(--color-border),var(--shadow-xl-spread,0 8px 16px -4px rgba(0,0,0,.12)); backdrop-filter:blur(4px); }
69
+ [${MARKER}=page] .pm-popup button { width:100%; min-height:var(--app-menu-item-height,30px); display:flex; align-items:center; justify-content:space-between; gap:16px; padding:var(--app-menu-item-padding,5px 8px); border-radius:var(--radius-xl); text-align:left; }
70
+ [${MARKER}=page] .pm-popup button:hover, [${MARKER}=page] .pm-popup button:focus-visible { background:var(--color-background-primary-ghost-hover); }
71
+ [${MARKER}=page] .pm-popup svg { width:16px; height:16px; }
72
+ [${MARKER}=page] .pm-command { display:inline-flex; align-items:center; justify-content:center; gap:6px; min-height:32px; padding:6px 12px; border-radius:var(--radius-full,999px); color:inherit; white-space:nowrap; }
73
+ [${MARKER}=page] .pm-command:hover { background:var(--color-background-primary-ghost-hover); }
74
+ [${MARKER}=page] .pm-primary { background:var(--color-background-primary-solid); color:var(--color-text-primary-solid); }
75
+ [${MARKER}=page] .pm-primary:hover { background:var(--color-background-primary-solid-hover); }
76
+ [${MARKER}=page] .pm-tabs { display:flex; gap:4px; overflow:auto; padding-bottom:4px; margin-bottom:24px; }
77
+ [${MARKER}=page] .pm-tab { display:flex; align-items:center; gap:6px; flex:none; height:36px; border-radius:999px; padding:0 16px; font-weight:500; color:var(--color-text-secondary); }
78
+ [${MARKER}=page] .pm-tab:hover, [${MARKER}=page] .pm-tab[aria-selected=true] { background:var(--color-background-primary-ghost-hover); color:var(--color-text-primary); }
79
+ [${MARKER}=page] input, [${MARKER}=page] textarea { font:inherit; color:inherit; background:transparent; border:1px solid var(--color-border); border-radius:var(--radius-lg,12px); padding:8px 12px; }
80
+ [${MARKER}=page] .pm-list { display:flex; flex-direction:column; gap:4px; }
81
+ [${MARKER}=page] .pm-row { display:flex; align-items:center; gap:12px; padding:16px 12px; border-radius:var(--radius-xl,16px); }
82
+ [${MARKER}=page] .pm-row:hover { background:var(--color-background-primary-ghost-hover); }
83
+ [${MARKER}=page] .pm-info { flex:1; min-width:0; text-align:left; }
84
+ [${MARKER}=page] .pm-title { font-weight:500; overflow:hidden; white-space:nowrap; text-overflow:ellipsis; }
85
+ [${MARKER}=page] .pm-preview { color:var(--color-text-secondary); overflow:hidden; white-space:nowrap; text-overflow:ellipsis; margin-top:3px; }
86
+ [${MARKER}=page] .pm-stats { color:var(--color-text-tertiary); min-width:130px; text-align:right; font-variant-numeric:tabular-nums; font-size:var(--text-xs,12px); }
87
+ [${MARKER}=page] .pm-count { color:var(--color-text-secondary); }
88
+ [${MARKER}=page] .pm-empty { color:var(--color-text-tertiary); padding:48px 0; text-align:center; }
89
+ [${MARKER}=page] .pm-form { display:flex; flex-direction:column; gap:16px; }
90
+ [${MARKER}=page] .pm-field { display:flex; flex-direction:column; gap:6px; color:var(--color-text-secondary); }
91
+ [${MARKER}=page] .pm-field textarea { min-height:220px; resize:vertical; color:var(--color-text-primary); }
92
+ [${MARKER}=page] .pm-footer { display:flex; justify-content:flex-end; gap:8px; margin-top:8px; }
93
+ [${MARKER}=page] .pm-grid { display:flex; flex-wrap:wrap; gap:4px; }
94
+ [${MARKER}=page] .pm-choice { width:36px; height:36px; border-radius:10px; display:flex; justify-content:center; align-items:center; }
95
+ [${MARKER}=page] .pm-choice[aria-pressed=true] { background:var(--color-background-primary-ghost-hover); outline:1px solid var(--color-text-secondary); }
96
+ [${MARKER}=page] .pm-swatch { width:18px; height:18px; border-radius:50%; background:currentColor; }
97
+ [${MARKER}=page] .pm-error { color:var(--color-text-danger,var(--color-text-primary)); margin:12px 0; }
98
+ [${MARKER}=page] .pm-editor h2 { margin-top:0; }
99
+ @media(max-width:800px) { [${MARKER}=page] .pm-wrap { padding:24px 30px; } [${MARKER}=page] .pm-stats { min-width:90px; } }
100
+ [${MARKER}=page] .pm-modal-layer { position:fixed; inset:0; z-index:60; display:grid; place-items:center; padding:24px; background:var(--color-background-overlay,rgba(0,0,0,.16)); }
101
+ [${MARKER}=page] .pm-editor { position:relative; top:auto; width:440px; max-width:calc(100vw - 48px); max-height:calc(100vh - 64px); margin:0; padding:20px; overflow:auto; border:0; border-radius:var(--radius-3xl,25px); background:color-mix(in srgb,var(--color-surface-elevated-secondary) 90%,transparent); box-shadow:0 0 0 .5px var(--color-border),var(--shadow-lg); backdrop-filter:blur(24px); }
102
+ [${MARKER}=page] .pm-prompt-editor { width:520px; }
103
+ [${MARKER}=page] .pm-modal-heading { display:flex; align-items:center; gap:8px; margin:0 0 16px; }
104
+ [${MARKER}=page] .pm-modal-heading h2 { flex:1; font-size:var(--text-lg,18px); font-weight:600; line-height:28px; margin:0; }
105
+ [${MARKER}=page] .pm-modal-heading .pm-icon { width:28px; height:28px; }
106
+ [${MARKER}=page] .pm-editor .pm-row { min-height:44px; padding:8px; gap:10px; border-radius:var(--radius-lg); }
107
+ [${MARKER}=page] .pm-editor .pm-form { gap:12px; }
108
+ [${MARKER}=page] .pm-editor .pm-field { gap:8px; color:var(--color-text-primary); }
109
+ [${MARKER}=page] .pm-editor input, [${MARKER}=page] .pm-editor textarea { border-radius:var(--radius-md); padding:8px 10px; background:var(--color-background-primary-soft,rgba(0,0,0,.025)); }
110
+ [${MARKER}=page] .pm-editor .pm-footer { margin-top:20px; gap:12px; }
111
+ [${MARKER}=page] .pm-editor .pm-footer .pm-command { border-radius:var(--radius-button-action,999px); min-height:32px; padding:6px 16px; background:var(--color-background-primary-ghost-hover); }
112
+ [${MARKER}=page] .pm-editor .pm-footer .pm-primary { background:var(--color-background-primary-solid); color:var(--color-text-primary-solid); }
113
+ [${MARKER}=page] .pm-editor .pm-footer .pm-primary:hover { background:var(--color-background-primary-solid-hover); }
114
+ [${MARKER}=page] .pm-grid { gap:4px; }
115
+ [${MARKER}=page] .pm-choice { width:36px; height:36px; border-radius:var(--radius-lg); }
116
+ [${MARKER}=page] .pm-choice svg { width:20px; height:20px; }
117
+ [${MARKER}=page] .pm-choice[aria-pressed=true] { outline:none; box-shadow:inset 0 0 0 1px var(--color-border); }
118
+ [${MARKER}=page] .pm-search-inline { display:flex; align-items:center; width:clamp(150px,22vw,300px); height:32px; padding:0 12px; gap:8px; border:1px solid var(--color-border-primary-outline,var(--color-border)); border-radius:var(--radius-full); background:transparent; }
119
+ [${MARKER}=page] .pm-search-inline input { min-width:0; width:100%; padding:0; border:0; background:transparent; outline:none; }
120
+ [${MARKER}=page] .pm-search-inline>svg { width:16px; height:16px; flex:none; color:var(--color-text-tertiary); }
121
+ [${MARKER}=page] .pm-list>h2 { color:var(--color-text-tertiary); font-size:var(--text-sm,13px); font-weight:500; }
122
+ [${MARKER}=page] .pm-stats { min-width:64px; }
123
+ [${MARKER}=page] .pm-modal-heading>.pm-icon:first-child { margin-left:-11px; }
124
+ [${MARKER}=page] .pm-modal-heading>.pm-icon:last-child { margin-right:-10px; }
125
+ [${MARKER}=page] .pm-editor input:focus, [${MARKER}=page] .pm-editor textarea:focus, [${MARKER}=page] .pm-popup input:focus { outline:none; box-shadow:none; border-color:var(--color-ring,var(--color-border-primary-outline)); }
126
+ [${MARKER}=page] .pm-editor .pm-footer .pm-danger { color:var(--color-text-danger); background:var(--color-background-danger-soft-alpha); }
127
+ [${MARKER}=page] .pm-editor .pm-footer .pm-danger:hover { background:var(--color-background-danger-soft-alpha-hover); }
128
+ [${MARKER}=page] .pm-confirm-description { color:var(--color-text-secondary); line-height:1.6; }
129
+ [${MARKER}=page] .pm-select { display:flex; align-items:center; gap:8px; min-height:36px; padding:8px 10px; background:var(--color-background-primary-soft); border:1px solid var(--color-border); border-radius:var(--radius-md); text-align:left; }
130
+ [${MARKER}=page] .pm-select>svg { width:16px; height:16px; }
131
+ [${MARKER}=page] .pm-option-label { flex:1; }
132
+ [${MARKER}=page] .pm-empty-state { display:flex; flex-direction:column; align-items:center; justify-content:center; min-height:340px; gap:8px; text-align:center; }
133
+ [${MARKER}=page] .pm-empty-art { font-size:var(--text-5xl,48px); line-height:1; margin-bottom:8px; }
134
+ [${MARKER}=page] .pm-empty-title { font-size:var(--text-lg,18px); line-height:24px; font-weight:500; }
135
+ [${MARKER}=page] .pm-empty-description { color:var(--color-text-secondary); }
136
+ [${MARKER}=page] .pm-empty-state>.pm-command { margin-top:24px; }
137
+ [${MARKER}=page] .pm-drag { cursor:grab; color:var(--color-text-tertiary); }
138
+ [${MARKER}=page] .pm-drag:active { cursor:grabbing; }
139
+ [${MARKER}=page] .pm-drop-placeholder { flex:none; border:1px dashed var(--color-border-primary-outline,var(--color-border)); border-radius:var(--radius-xl); background:var(--color-background-primary-ghost-hover); box-sizing:border-box; }
140
+ [${MARKER}=page] .pm-dragging { position:absolute; opacity:0; pointer-events:none; }
141
+ [${MARKER}=page] .pm-popup { z-index:70; max-height:calc(100vh - 32px); overflow:auto; }
142
+ [${MARKER}=page] .pm-symbol-picker { width:320px; padding:12px; display:flex; flex-direction:column; gap:12px; }
143
+ [${MARKER}=page] .pm-symbol-picker>input { width:100%; flex:none; border-radius:var(--radius-full); }
144
+ [${MARKER}=page] .pm-emoji-grid { display:grid; grid-template-columns:repeat(8,minmax(0,1fr)); gap:4px; max-height:264px; overflow:auto; }
145
+ [${MARKER}=page] .pm-emoji-grid .pm-emoji-choice { width:32px; height:32px; min-height:32px; padding:0; display:flex; justify-content:center; font-size:22px; border-radius:var(--radius-lg); }
146
+ [${MARKER}=page] .pm-emoji-choice[aria-pressed=true] { background:var(--color-background-primary-ghost-hover); }
147
+ `;
148
+ document.head.append(style);
149
+ let button = null;
150
+ let addButton = null;
151
+ let composerCard = null;
152
+ let composerThread = null;
153
+ let panel = null;
154
+ let items = [];
155
+ let generation = 0;
156
+ let syncQueued = false;
157
+ let disposed = false;
158
+ let categories = [];
159
+ let nativeIcons = {};
160
+ let nativeEmojis = [];
161
+
162
+ const appearanceLabels = {"bar-chart": "柱状图", "book": "书籍", "brain": "大脑", "currency-dollar": "美元", "customize": "自定义", "dumbbell": "哑铃", "edit": "编辑", "flask": "烧瓶", "folder": "文件夹", "function": "函数", "globe": "地球", "graduation-cap": "学士帽", "heart": "爱心", "kettlebell": "壶铃", "logs": "笔记", "lotus": "莲花", "music": "音乐", "palette": "调色盘", "paw": "爪印", "plant": "植物", "popcorn": "爆米花", "scale": "天平", "stethoscope": "听诊器", "terminal": "终端", "wrench": "扳手", "writing": "写作", "blue": "蓝色", "green": "绿色", "orange": "橙色", "pink": "粉色", "purple": "紫色", "red": "红色", "yellow": "黄色", "black": "中性色"};
163
+ function appearanceLabel(key) {
164
+ return text(key.replaceAll("-", " "), appearanceLabels[key] ?? key);
165
+ }
166
+ function errorMessage(error) {
167
+ const messages = {
168
+ "Category was not found": "未找到该分类",
169
+ "Prompt was not found": "未找到该提示词",
170
+ "Choose one emoji or leave it empty": "请选择一个 Emoji,或留空以继承分类图标",
171
+ "Invalid sort order": "排序无效,请重试",
172
+ "Invalid order": "排序无效,请重试",
173
+ "Invalid category": "分类无效,请重试",
174
+ "Items changed; refresh and try again": "条目已发生变化,请刷新后重试",
175
+ };
176
+ return text(error.message || "Something went wrong. Please try again.", messages[error.message] ?? "操作失败,请重试。");
177
+ }
178
+
179
+ // DOM 与图标工具
180
+ function element(tag, className, content) {
181
+ const node = document.createElement(tag);
182
+ if (className) node.className = className;
183
+ if (content != null) node.textContent = content;
184
+ return node;
185
+ }
186
+ function icon(name) {
187
+ return new DOMParser()
188
+ .parseFromString(ICONS[name], "image/svg+xml")
189
+ .documentElement.cloneNode(true);
190
+ }
191
+ function control(label, handler, className = "qp-command", iconName = null) {
192
+ const node = element("button", className, iconName ? null : label);
193
+ node.type = "button";
194
+ node.setAttribute("aria-label", label);
195
+ if (iconName) {
196
+ node.append(icon(iconName));
197
+ node.title = label;
198
+ }
199
+ node.addEventListener("click", handler);
200
+ return node;
201
+ }
202
+ async function api(route, { method = "GET", body } = {}) {
203
+ return config.requestHostAction("quick-prompts", { route, method, body });
204
+ }
205
+ function showError(error) {
206
+ if (!panel) return;
207
+ panel.querySelector(".qp-error")?.remove();
208
+ const message = element("div", "qp-error", errorMessage(error));
209
+ message.setAttribute("role", "alert");
210
+ panel.append(message);
211
+ }
212
+ function position() {
213
+ if (!panel || !composerCard?.isConnected) return;
214
+ const rect = composerCard.getBoundingClientRect();
215
+ const zoom = Number.parseFloat(getComputedStyle(panel).zoom) || 1;
216
+ panel.style.left = `${rect.left / zoom}px`;
217
+ panel.style.top = `${(rect.top - 4) / zoom}px`;
218
+ panel.style.width = `${rect.width / zoom}px`;
219
+ panel.style.maxHeight = `${Math.max(0, Math.min(400, (rect.top - 16) / zoom))}px`;
220
+ }
221
+ function close(restoreFocus = false) {
222
+ generation++;
223
+ panel?.remove();
224
+ panel = null;
225
+
226
+ button?.setAttribute("aria-expanded", "false");
227
+ button?.setAttribute("data-state", "closed");
228
+ if (restoreFocus) button?.focus({ preventScroll: true });
229
+ }
230
+ // 输入框 Popover:只浏览与插入
231
+ async function loadList() {
232
+ const current = ++generation;
233
+
234
+ panel.replaceChildren();
235
+
236
+ panel.append(element("div", "qp-empty", text("Loading…", "加载中…")));
237
+ try {
238
+ const state = await api("/api/state");
239
+ if (!panel || current !== generation) return;
240
+ items = state.prompts;
241
+ categories = state.categories;
242
+ nativeIcons = state.icons;
243
+ nativeEmojis = state.emojis ?? [];
244
+ renderList();
245
+ } catch (error) {
246
+ if (panel && current === generation) {
247
+ panel.querySelector(".qp-empty")?.remove();
248
+ showError(error);
249
+ panel.append(control(text("Retry", "重试"), loadList));
250
+ }
251
+ }
252
+ }
253
+ const COLORS = {
254
+ black: ["var(--color-text-secondary)", "var(--color-text-secondary)"],
255
+ blue: ["#0285ff", "#339cff"],
256
+ green: ["#04b84c", "#40c977"],
257
+ orange: ["#fb6a22", "#ff8549"],
258
+ pink: ["#ff66ad", "#ff8cc1"],
259
+ purple: ["#924ff7", "#ad7bf9"],
260
+ red: ["#fa423e", "#ff6764"],
261
+ yellow: ["#ffc300", "#ffd240"],
262
+ };
263
+ function categoryIcon(item) {
264
+ const node = element("span", "qp-symbol");
265
+ if (item.emoji) {
266
+ node.textContent = item.emoji;
267
+ return node;
268
+ }
269
+ const category = categories.find((entry) => entry.id === item.categoryId);
270
+ const svg = new DOMParser().parseFromString(
271
+ nativeIcons[category?.icon ?? "book"] ?? ICONS.prompt,
272
+ "image/svg+xml",
273
+ ).documentElement;
274
+ node.append(svg.cloneNode(true));
275
+ const colors = COLORS[category?.color ?? "black"] ?? COLORS.black;
276
+ node.style.setProperty("--prompt-icon-light", colors[0]);
277
+ node.style.setProperty("--prompt-icon-dark", colors[1]);
278
+ return node;
279
+ }
280
+ function orderedItems(categoryId) {
281
+ return items
282
+ .filter((item) => item.categoryId === categoryId)
283
+ .sort(
284
+ (a, b) =>
285
+ a.sortOrder - b.sortOrder ||
286
+ a.createdAt - b.createdAt ||
287
+ a.id.localeCompare(b.id),
288
+ );
289
+ }
290
+ function renderList() {
291
+ panel.replaceChildren();
292
+ const list = element("div", "qp-scroll");
293
+ if (!items.length)
294
+ list.append(
295
+ element("div", "qp-empty", text("No prompts yet", "还没有提示词")),
296
+ );
297
+ const recent = items
298
+ .filter((item) => item.useCount > 0)
299
+ .sort((a, b) => b.calledAt - a.calledAt || a.id.localeCompare(b.id))
300
+ .slice(0, 3);
301
+ const sections = [
302
+ { name: text("Recently used", "最近使用"), prompts: recent },
303
+ ...categories.map((category) => ({
304
+ name: category.name,
305
+ prompts: orderedItems(category.id),
306
+ })),
307
+ { name: text("Other", "其他"), prompts: orderedItems(null) },
308
+ ];
309
+ for (const section of sections.filter((section) => section.prompts.length)) {
310
+ list.append(element("div", "qp-header", section.name));
311
+ for (const item of section.prompts) {
312
+ const row = element("div", "qp-row");
313
+ const insert = control(
314
+ item.summary,
315
+ async () => {
316
+ const current = generation;
317
+ try {
318
+ window.__codexPersonalRuntime.insertText(item.body);
319
+ // 插入同步成功后再记一次调用;失败和单纯浏览不计数。
320
+ close();
321
+ await api(`/api/prompts/${encodeURIComponent(item.id)}/use`, {
322
+ method: "POST",
323
+ body: { action: "insert", success: true },
324
+ });
325
+ } catch (error) {
326
+ if (generation === current) showError(error);
327
+ else console.warn("[Prompts] Usage could not be saved", error);
328
+ }
329
+ },
330
+ "qp-select",
331
+ );
332
+ insert.replaceChildren(
333
+ categoryIcon(item),
334
+ element("span", "qp-title", item.summary),
335
+ );
336
+ if (item.title)
337
+ insert.append(
338
+ element("span", "qp-preview", item.body.replace(/\s+/g, " ")),
339
+ );
340
+ row.append(insert);
341
+ list.append(row);
342
+ }
343
+ }
344
+ panel.append(list);
345
+ position();
346
+ }
347
+ function open() {
348
+ if (panel) {
349
+ close(true);
350
+ return;
351
+ }
352
+ if (addButton.getAttribute("aria-expanded") === "true") addButton.click();
353
+ panel = element("div");
354
+ panel.setAttribute(MARKER, "panel");
355
+ panel.setAttribute("role", "dialog");
356
+ panel.setAttribute("aria-label", text("Quick Prompts", "快捷提示词"));
357
+ document.body.append(panel);
358
+ button.setAttribute("aria-expanded", "true");
359
+ button.setAttribute("data-state", "open");
360
+ position();
361
+ void loadList();
362
+ }
363
+ function sync() {
364
+ if (disposed || signal.aborted) return;
365
+ syncNavigation();
366
+ const add = [
367
+ ...document.querySelectorAll("[data-codex-composer-root] button"),
368
+ ].find(
369
+ (node) =>
370
+ node.getAttribute("data-composer-navigation-target") === "add-context",
371
+ );
372
+ const card = add?.closest("[data-codex-composer-root]");
373
+ const thread =
374
+ card
375
+ ?.querySelector("[data-above-composer-conversation-id]")
376
+ ?.getAttribute("data-above-composer-conversation-id") ?? null;
377
+ if (thread !== composerThread) {
378
+ close();
379
+ composerThread = thread;
380
+ }
381
+ if (add !== addButton || card !== composerCard) {
382
+ close();
383
+ button?.remove();
384
+ button = null;
385
+ addButton = add;
386
+ composerCard = card;
387
+ }
388
+ if (!add) return;
389
+ if (
390
+ panel &&
391
+ [...card.querySelectorAll("button[aria-expanded=true]")].some(
392
+ (node) => node !== button,
393
+ )
394
+ )
395
+ close();
396
+ if (!button) {
397
+ button = add.cloneNode(false);
398
+ button.removeAttribute("id");
399
+ button.removeAttribute("data-composer-navigation-target");
400
+ button.setAttribute(MARKER, "button");
401
+ button.setAttribute("aria-label", text("Quick Prompts", "快捷提示词"));
402
+ button.title = text("Quick Prompts", "快捷提示词");
403
+ button.setAttribute("aria-haspopup", "dialog");
404
+ button.setAttribute("aria-expanded", "false");
405
+ button.setAttribute("data-state", "closed");
406
+ // 保留 Add 的紧凑与常规图标容器,以同一规则响应窗口尺寸。
407
+ for (const child of add.children) {
408
+ const slot = child.cloneNode(true);
409
+ for (const svg of slot.querySelectorAll("svg")) {
410
+ const replacement = icon("prompt");
411
+ for (const attr of [
412
+ "class",
413
+ "width",
414
+ "height",
415
+ "aria-hidden",
416
+ "focusable",
417
+ ]) {
418
+ if (svg.hasAttribute(attr))
419
+ replacement.setAttribute(attr, svg.getAttribute(attr));
420
+ }
421
+ svg.replaceWith(replacement);
422
+ }
423
+ button.append(slot);
424
+ }
425
+ button.addEventListener("click", open);
426
+ add.after(button);
427
+ }
428
+ if (button.className !== add.className) button.className = add.className;
429
+ position();
430
+ }
431
+ // 页面与快捷调用面板共享数据;所有编辑只存在于页面。
432
+ // 管理页面与编辑弹窗
433
+ let navButton = null;
434
+ let ownNavGroup = null;
435
+ let page = null;
436
+ let pageGeneration = 0;
437
+ let activeCategory = "all";
438
+ let pageSort = "manual";
439
+ let searchText = "";
440
+ let editor = null;
441
+ let pageFocus = null;
442
+ const hiddenSurfaces = new Map();
443
+ const nativeNavigationSelection = new Map();
444
+
445
+ function pageControl(label, handler, primary = false) {
446
+ return control(label, handler, `pm-command${primary ? " pm-primary" : ""}`);
447
+ }
448
+ function pageError(error) {
449
+ if (!page) return;
450
+ page.querySelector(".pm-error")?.remove();
451
+ const message = element("div", "pm-error", errorMessage(error));
452
+ message.setAttribute("role", "alert");
453
+ (editor ?? page.querySelector(".pm-wrap") ?? page).append(message);
454
+ }
455
+ function positionPage() {
456
+ if (!page) return;
457
+ const rail = document.querySelector("[data-app-navigation-rail]");
458
+ if (!rail) {
459
+ closePage();
460
+ return;
461
+ }
462
+ const rect = rail.getBoundingClientRect();
463
+ page.style.left = `${rect.right}px`;
464
+ page.style.top = `${rect.top}px`;
465
+ page.style.right = "4px";
466
+ page.style.bottom = "4px";
467
+ }
468
+ function syncNavigationSelection() {
469
+ if (!page) return;
470
+ for (const node of document.querySelectorAll('[data-app-navigation-rail] [data-sidebar-destination][data-selected], [data-app-navigation-rail] [data-sidebar-destination][aria-current="page"]')) {
471
+ if (!nativeNavigationSelection.has(node)) {
472
+ nativeNavigationSelection.set(node, new Map(
473
+ ["data-selected", "aria-current", "data-suppress-active-style"].map(name => [name, node.getAttribute(name)]),
474
+ ));
475
+ }
476
+ node.removeAttribute("data-selected");
477
+ node.removeAttribute("aria-current");
478
+ node.setAttribute("data-suppress-active-style", "");
479
+ }
480
+ if (navButton && !navButton.hasAttribute("data-selected")) navButton.setAttribute("data-selected", "");
481
+ if (navButton?.getAttribute("aria-current") !== "page") navButton?.setAttribute("aria-current", "page");
482
+ }
483
+ function restoreNavigationSelection() {
484
+ // 退出覆盖页时还原原生状态,随后由原生路由接管新的选择。
485
+ for (const [node, attributes] of nativeNavigationSelection) {
486
+ if (!node.isConnected) continue;
487
+ for (const [name, value] of attributes) {
488
+ if (value === null) node.removeAttribute(name);
489
+ else node.setAttribute(name, value);
490
+ }
491
+ }
492
+ nativeNavigationSelection.clear();
493
+ }
494
+ function closePage(restoreFocus = false) {
495
+ dragState?.cancel();
496
+ pageGeneration++;
497
+ closePagePopup();
498
+ closeEditor(false);
499
+ page?.remove();
500
+ page = null;
501
+ restoreNavigationSelection();
502
+ editor = null;
503
+ for (const [surface, inert] of hiddenSurfaces) surface.inert = inert;
504
+ hiddenSurfaces.clear();
505
+ navButton?.removeAttribute("data-selected");
506
+ navButton?.removeAttribute("aria-current");
507
+ document.documentElement.removeAttribute("data-mcx-prompts-open");
508
+ if (restoreFocus && pageFocus?.isConnected)
509
+ pageFocus.focus({ preventScroll: true });
510
+ }
511
+ async function loadPage() {
512
+ const current = ++pageGeneration;
513
+ try {
514
+ const state = await api("/api/state");
515
+ if (!page || current !== pageGeneration) return;
516
+ items = state.prompts;
517
+ categories = state.categories;
518
+ nativeIcons = state.icons;
519
+ nativeEmojis = state.emojis ?? [];
520
+ renderPage();
521
+ } catch (error) {
522
+ if (current === pageGeneration) pageError(error);
523
+ }
524
+ }
525
+ function openPage() {
526
+ close();
527
+ if (page) return;
528
+ pageFocus = document.activeElement;
529
+ page = element("section");
530
+ page.setAttribute(MARKER, "page");
531
+ page.setAttribute("aria-label", text("Prompts", "提示词"));
532
+ document.body.append(page);
533
+ // 只隔离主内容的焦点,保留导航栏与原生窗口控件。
534
+ for (const surface of document.querySelectorAll(
535
+ '[class*="_PageSurface_"], [class*="_Workspace_"]',
536
+ )) {
537
+ hiddenSurfaces.set(surface, surface.inert);
538
+ surface.inert = true;
539
+ }
540
+ document.documentElement.setAttribute("data-mcx-prompts-open", "");
541
+ navButton?.setAttribute("data-selected", "");
542
+ navButton?.setAttribute("aria-current", "page");
543
+ syncNavigationSelection();
544
+ positionPage();
545
+ page.append(element("div", "pm-empty", text("Loading…", "加载中…")));
546
+ void loadPage();
547
+ }
548
+ let pagePopup = null;
549
+ let popupTrigger = null;
550
+ function pageIcon(label, name, handler) {
551
+ const button = control(label, handler, "pm-icon", name);
552
+ button.title = label;
553
+ return button;
554
+ }
555
+ function closePagePopup(restoreFocus = false) {
556
+ pagePopup?.remove();
557
+ pagePopup = null;
558
+ popupTrigger?.setAttribute("aria-expanded", "false");
559
+ if (restoreFocus && popupTrigger?.isConnected)
560
+ popupTrigger.focus({ preventScroll: true });
561
+ popupTrigger = null;
562
+ }
563
+ function makePagePopup(trigger, className, role, matchTriggerWidth = false) {
564
+ if (popupTrigger === trigger) {
565
+ closePagePopup(true);
566
+ return null;
567
+ }
568
+ closePagePopup();
569
+ popupTrigger = trigger;
570
+ trigger.setAttribute("aria-expanded", "true");
571
+ pagePopup = element("div", `pm-popup ${className}`);
572
+ pagePopup.setAttribute("role", role);
573
+ pagePopup.setAttribute("aria-label", trigger.getAttribute("aria-label"));
574
+ (editorLayer ?? page).append(pagePopup);
575
+ const r = trigger.getBoundingClientRect();
576
+ if (matchTriggerWidth) {
577
+ pagePopup.style.boxSizing = "border-box";
578
+ pagePopup.style.width = `${r.width}px`;
579
+ }
580
+ const width = pagePopup.getBoundingClientRect().width;
581
+ pagePopup.style.top = `${r.bottom + 6}px`;
582
+ pagePopup.style.left = `${Math.max(8, Math.min(r.right - width, innerWidth - width - 8))}px`;
583
+ return pagePopup;
584
+ }
585
+ function makeSearchField() {
586
+ const container = element("div", "pm-search-inline");
587
+ const input = element("input");
588
+ input.type = "text";
589
+ input.autocomplete = "off";
590
+ input.placeholder = text("Search prompts", "搜索提示词");
591
+ input.setAttribute("aria-label", input.placeholder);
592
+ input.value = searchText;
593
+ const update = () => {
594
+ searchText = input.value;
595
+ renderRows(page.querySelector(".pm-list"));
596
+ };
597
+ input.addEventListener("input", update);
598
+ input.addEventListener("keydown", (event) => {
599
+ if (event.key === "Escape") {
600
+ event.stopPropagation();
601
+ event.preventDefault();
602
+ input.value = "";
603
+ update();
604
+ }
605
+ });
606
+ container.append(icon("search"), input);
607
+ return container;
608
+ }
609
+ function openSort(trigger) {
610
+ const popup = makePagePopup(trigger, "", "menu");
611
+ if (!popup) return;
612
+ for (const [value, label] of [
613
+ ["manual", text("Manual order", "手动排序")],
614
+ ["usage", text("Most used", "调用次数")],
615
+ ["recent", text("Recently used", "最近调用")],
616
+ ]) {
617
+ const choice = control(
618
+ label,
619
+ () => {
620
+ pageSort = value;
621
+ closePagePopup(true);
622
+ renderRows(page.querySelector(".pm-list"));
623
+ },
624
+ "",
625
+ );
626
+ choice.setAttribute("role", "menuitemradio");
627
+ choice.setAttribute("aria-checked", String(value === pageSort));
628
+ if (value === pageSort) choice.append(icon("check"));
629
+ popup.append(choice);
630
+ }
631
+ popup.querySelector("[aria-checked=true]")?.focus();
632
+ }
633
+ document.addEventListener(
634
+ "pointerdown",
635
+ (event) => {
636
+ if (
637
+ pagePopup &&
638
+ !pagePopup.contains(event.target) &&
639
+ !popupTrigger?.contains(event.target)
640
+ )
641
+ closePagePopup();
642
+ },
643
+ { signal },
644
+ );
645
+ document.addEventListener(
646
+ "keydown",
647
+ (event) => {
648
+ if (!pagePopup) return;
649
+ if (event.key === "Escape") {
650
+ event.preventDefault();
651
+ event.stopImmediatePropagation();
652
+ closePagePopup(true);
653
+ } else if (
654
+ pagePopup.getAttribute("role") === "menu" &&
655
+ ["ArrowDown", "ArrowUp", "Home", "End"].includes(event.key)
656
+ ) {
657
+ event.preventDefault();
658
+ const options = [...pagePopup.querySelectorAll("button")],
659
+ index = options.indexOf(document.activeElement);
660
+ const next =
661
+ event.key === "Home"
662
+ ? 0
663
+ : event.key === "End"
664
+ ? options.length - 1
665
+ : (index + (event.key === "ArrowDown" ? 1 : -1) + options.length) %
666
+ options.length;
667
+ options[next]?.focus();
668
+ }
669
+ },
670
+ { signal, capture: true },
671
+ );
672
+ document.addEventListener(
673
+ "focusin",
674
+ (event) => {
675
+ if (
676
+ pagePopup &&
677
+ !pagePopup.contains(event.target) &&
678
+ !popupTrigger?.contains(event.target)
679
+ )
680
+ closePagePopup();
681
+ },
682
+ { signal },
683
+ );
684
+ window.addEventListener("resize", () => closePagePopup(), { signal });
685
+
686
+ function renderPage() {
687
+ if (!page) return;
688
+ closeEditor(false);
689
+ closePagePopup();
690
+ editor = null;
691
+ page.replaceChildren();
692
+ const wrap = element("div", "pm-wrap");
693
+ const heading = element("div", "pm-heading");
694
+ const actions = element("div", "pm-header-actions");
695
+ const searchField = makeSearchField();
696
+ const sortButton = pageIcon(
697
+ text("Sort prompts", "提示词排序"),
698
+ "arrow-down-wide-narrow",
699
+ () => openSort(sortButton),
700
+ );
701
+ sortButton.setAttribute("aria-haspopup", "menu");
702
+ actions.append(
703
+ sortButton,
704
+ pageIcon(text("Edit categories", "编辑分类"), "settings", manageCategories),
705
+ searchField,
706
+ pageControl(text("New prompt", "新建提示词"), () => editPrompt(), true),
707
+ );
708
+ heading.append(element("h1", "", text("Prompts", "提示词")), actions);
709
+ const tabs = element("div", "pm-tabs");
710
+ tabs.setAttribute("role", "tablist");
711
+ tabs.setAttribute("aria-label", text("Categories", "分类"));
712
+ const choices = [
713
+ { id: "all", name: "All" },
714
+ ...categories,
715
+ { id: "uncategorized", name: text("Other", "其他") },
716
+ ];
717
+ if (!choices.some((c) => c.id === activeCategory)) activeCategory = "all";
718
+ choices.forEach((category) => {
719
+ const tab = control(
720
+ category.name,
721
+ () => {
722
+ activeCategory = category.id;
723
+ renderPage();
724
+ },
725
+ "pm-tab",
726
+ );
727
+ if (category.id !== "all")
728
+ tab.prepend(categoryIcon({ categoryId: category.id === "uncategorized" ? null : category.id }));
729
+ tab.setAttribute("role", "tab");
730
+ tab.setAttribute("aria-selected", String(activeCategory === category.id));
731
+ tab.tabIndex = activeCategory === category.id ? 0 : -1;
732
+ tab.addEventListener("keydown", (event) => {
733
+ if (!["ArrowLeft", "ArrowRight", "Home", "End"].includes(event.key))
734
+ return;
735
+ event.preventDefault();
736
+ const index = choices.indexOf(category);
737
+ const next =
738
+ event.key === "Home"
739
+ ? 0
740
+ : event.key === "End"
741
+ ? choices.length - 1
742
+ : (index + (event.key === "ArrowRight" ? 1 : -1) + choices.length) %
743
+ choices.length;
744
+ activeCategory = choices[next].id;
745
+ renderPage();
746
+ page.querySelector("[aria-selected=true]")?.focus();
747
+ });
748
+ tabs.append(tab);
749
+ });
750
+ const tabRow = element("div", "pm-tab-row");
751
+ tabRow.append(tabs);
752
+ const list = element("div", "pm-list");
753
+ wrap.append(heading, tabRow, list);
754
+ page.append(wrap);
755
+ renderRows(list);
756
+ }
757
+ function renderRows(list) {
758
+ list.replaceChildren();
759
+ const query = searchText.trim().toLocaleLowerCase();
760
+ const groups = [...categories, { id: null, name: text("Other", "其他") }];
761
+ let shown = items.filter(
762
+ (item) =>
763
+ (activeCategory === "all" ||
764
+ item.categoryId ===
765
+ (activeCategory === "uncategorized" ? null : activeCategory)) &&
766
+ (!query ||
767
+ `${item.title}\n${item.body}`.toLocaleLowerCase().includes(query)),
768
+ );
769
+ if (!shown.length) {
770
+ list.append(emptyState(Boolean(query)));
771
+ return;
772
+ }
773
+ if (pageSort === "usage")
774
+ shown.sort(
775
+ (a, b) =>
776
+ b.useCount - a.useCount || (b.calledAt ?? 0) - (a.calledAt ?? 0),
777
+ );
778
+ if (pageSort === "recent")
779
+ shown.sort((a, b) => (b.calledAt ?? 0) - (a.calledAt ?? 0));
780
+ const sections =
781
+ pageSort === "manual"
782
+ ? groups
783
+ .map((c) => ({
784
+ name: c.name,
785
+ items: orderedItems(c.id).filter((p) => shown.includes(p)),
786
+ }))
787
+ .filter((c) => c.items.length)
788
+ : [{ items: shown }];
789
+ for (const section of sections) {
790
+ if (section.name && activeCategory === "all")
791
+ list.append(element("h2", "", section.name));
792
+ for (const item of section.items) {
793
+ const row = element("div", "pm-row");
794
+ row.dataset.promptId = item.id;
795
+ const info = control(item.summary, () => editPrompt(item), "pm-info");
796
+ info.replaceChildren(
797
+ element("div", "pm-title", item.summary),
798
+ element("div", "pm-preview", item.body.replace(/\s+/g, " ")),
799
+ );
800
+ const stats = element("div", "pm-stats");
801
+ stats.append(
802
+ element(
803
+ "div",
804
+ "pm-count",
805
+ pageSort === "recent"
806
+ ? item.calledAt == null
807
+ ? text("Never used", "尚未调用")
808
+ : new Intl.DateTimeFormat(zh() ? "zh-CN" : "en", {
809
+ month: "short", day: "numeric", hour: "2-digit", minute: "2-digit",
810
+ }).format(new Date(item.calledAt))
811
+ : text(`${item.useCount} ${item.useCount === 1 ? "call" : "calls"}`, `${item.useCount} 次调用`),
812
+ ),
813
+ );
814
+ row.append(categoryIcon(item), info, stats);
815
+ if (pageSort === "manual" && !query)
816
+ row.append(
817
+ orderControls(item, orderedItems(item.categoryId), false, row),
818
+ );
819
+ list.append(row);
820
+ }
821
+ }
822
+ }
823
+ function emptyState(searching) {
824
+ const empty = element("div", "pm-empty-state");
825
+ const art = element("span", "pm-empty-art", searching ? "🔎" : "📖");
826
+ art.setAttribute("role", "img");
827
+ art.setAttribute("aria-label", text("Prompts", "提示词"));
828
+ empty.append(
829
+ art,
830
+ element(
831
+ "div",
832
+ "pm-empty-title",
833
+ searching
834
+ ? text("No prompts found", "未找到提示词")
835
+ : text("No prompts yet", "暂无提示词"),
836
+ ),
837
+ element(
838
+ "div",
839
+ "pm-empty-description",
840
+ searching
841
+ ? text("Try a different search.", "试试其他关键词。")
842
+ : text(
843
+ "Save a prompt to use it again in any chat.",
844
+ "收藏提示词,在任意对话中快速调用。",
845
+ ),
846
+ ),
847
+ );
848
+ if (!searching)
849
+ empty.append(
850
+ pageControl(text("New prompt", "新建提示词"), () => editPrompt(), true),
851
+ );
852
+ return empty;
853
+ }
854
+ let dragState = null;
855
+ function orderControls(item, ordered, isCategory, row) {
856
+ const handle = pageIcon(
857
+ text("Drag to reorder", "拖拽排序"),
858
+ "drag",
859
+ () => {},
860
+ );
861
+ handle.classList.add("pm-drag");
862
+ handle.title = text(
863
+ "Drag to reorder · Alt + ↑ / ↓",
864
+ "拖拽排序 · Alt + ↑ / ↓",
865
+ );
866
+ row.dataset.orderId = item.id;
867
+ const group = isCategory ? "categories" : `prompts:${item.categoryId ?? ""}`;
868
+ row.dataset.orderGroup = group;
869
+ async function save(ids) {
870
+ try {
871
+ await api("/api/reorder", {
872
+ method: "POST",
873
+ body: {
874
+ ids,
875
+ categoryId: item.categoryId ?? null,
876
+ categories: isCategory,
877
+ },
878
+ });
879
+ await loadPage();
880
+ if (isCategory && page) manageCategories();
881
+ } catch (error) {
882
+ pageError(error);
883
+ }
884
+ }
885
+ // 原生拖影跟随指针,占位块负责布局;仅同组条目参与让位。
886
+ handle.addEventListener("pointerdown", () => { row.draggable = true; });
887
+ handle.addEventListener("pointerup", () => { row.draggable = false; });
888
+ row.addEventListener("dragstart", (event) => {
889
+ if (dragState) return;
890
+ const parent = row.parentElement;
891
+ const siblings = [...parent.children].filter(node => node.dataset.orderGroup === group);
892
+ const originalNext = row.nextSibling;
893
+ const placeholder = element("div", "pm-drop-placeholder");
894
+ placeholder.style.height = `${row.getBoundingClientRect().height}px`;
895
+ placeholder.setAttribute("aria-hidden", "true");
896
+ const lifetime = new AbortController();
897
+ const reduced = matchMedia("(prefers-reduced-motion: reduce)").matches;
898
+ const animations = new Map();
899
+ let started = false;
900
+ let finished = false;
901
+ const duration = reduced ? 0 : 180;
902
+ function movePlaceholder(before) {
903
+ if (placeholder.nextSibling === before || before === placeholder) return;
904
+ const positions = new Map(siblings.filter(node => node !== row).map(node => [node, node.getBoundingClientRect().top]));
905
+ for (const animation of animations.values()) animation.cancel();
906
+ animations.clear();
907
+ parent.insertBefore(placeholder, before);
908
+ for (const [node, top] of positions) {
909
+ const delta = top - node.getBoundingClientRect().top;
910
+ if (delta && duration) animations.set(node, node.animate([
911
+ { transform: `translateY(${delta}px)` }, { transform: "translateY(0)" },
912
+ ], { duration, easing: "cubic-bezier(.2,.8,.2,1)" }));
913
+ }
914
+ }
915
+ function finish(commit) {
916
+ if (finished) return;
917
+ finished = true;
918
+ lifetime.abort();
919
+ dragState = null;
920
+ row.draggable = false;
921
+ if (!started) return;
922
+ if (!commit) movePlaceholder(originalNext);
923
+ parent.insertBefore(row, placeholder);
924
+ row.classList.remove("pm-dragging");
925
+ placeholder.remove();
926
+ const ids = [...parent.children].filter(node => node.dataset.orderGroup === group).map(node => node.dataset.orderId);
927
+ const landing = row.animate([{ opacity: .45 }, { opacity: 1 }], { duration: reduced ? 0 : 140, easing: "ease-out" });
928
+ if (commit && ids.some((id, index) => id !== ordered[index].id)) {
929
+ void landing.finished.catch(() => {}).then(() => save(ids));
930
+ }
931
+ }
932
+ dragState = { cancel: () => finish(false) };
933
+ event.dataTransfer.effectAllowed = "move";
934
+ event.dataTransfer.setData("text/plain", item.id);
935
+ // 等浏览器截取拖影后再隐藏原行,避免拖影变成空白。
936
+ requestAnimationFrame(() => {
937
+ if (finished || !row.isConnected) return;
938
+ row.before(placeholder);
939
+ row.classList.add("pm-dragging");
940
+ started = true;
941
+ });
942
+ parent.addEventListener("dragover", event => {
943
+ if (!started) return;
944
+ const target = event.target.closest("[data-order-group], .pm-drop-placeholder");
945
+ if (target !== placeholder && (!siblings.includes(target) || target === row)) return;
946
+ event.preventDefault();
947
+ event.dataTransfer.dropEffect = "move";
948
+ if (target === placeholder) return;
949
+ // 使用布局位置判断插入点,不受让位动画的 transform 影响。
950
+ const rect = target.getBoundingClientRect();
951
+ const transform = getComputedStyle(target).transform;
952
+ const translateY = transform === "none" ? 0 : new DOMMatrixReadOnly(transform).m42;
953
+ const after = event.clientY > rect.top - translateY + rect.height / 2;
954
+ movePlaceholder(after ? target.nextSibling : target);
955
+ }, { signal: lifetime.signal });
956
+ parent.addEventListener("drop", event => {
957
+ const target = event.target.closest("[data-order-group], .pm-drop-placeholder");
958
+ if (target !== placeholder && !siblings.includes(target)) return;
959
+ event.preventDefault();
960
+ finish(true);
961
+ }, { signal: lifetime.signal });
962
+ row.addEventListener("dragend", () => finish(false), { once: true, signal: lifetime.signal });
963
+ signal.addEventListener("abort", () => finish(false), { once: true, signal: lifetime.signal });
964
+ });
965
+ handle.addEventListener("keydown", (event) => {
966
+ if (!event.altKey || !["ArrowUp", "ArrowDown"].includes(event.key)) return;
967
+ event.preventDefault();
968
+ const ids = ordered.map((x) => x.id),
969
+ from = ids.indexOf(item.id),
970
+ to = from + (event.key === "ArrowUp" ? -1 : 1);
971
+ if (to < 0 || to >= ids.length) return;
972
+ [ids[from], ids[to]] = [ids[to], ids[from]];
973
+ void save(ids);
974
+ });
975
+ return handle;
976
+ }
977
+ function field(label, input) {
978
+ const node = element("label", "pm-field", label);
979
+ node.append(input);
980
+ return node;
981
+ }
982
+ let editorLayer = null;
983
+ let editorReturnFocus = null;
984
+ function closeEditor(restoreFocus = true) {
985
+ dragState?.cancel();
986
+ closePagePopup();
987
+ editorLayer?.remove();
988
+ editorLayer = null;
989
+ editor = null;
990
+ const wrap = page?.querySelector(".pm-wrap");
991
+ if (wrap) wrap.inert = false;
992
+ if (restoreFocus && editorReturnFocus?.isConnected)
993
+ editorReturnFocus.focus({ preventScroll: true });
994
+ }
995
+ function editorShell(title, back = null) {
996
+ closePagePopup();
997
+ if (!editorLayer) {
998
+ editorReturnFocus = document.activeElement;
999
+ editorLayer = element("div", "pm-modal-layer");
1000
+ page.append(editorLayer);
1001
+ page.querySelector(".pm-wrap").inert = true;
1002
+ }
1003
+ editorLayer.replaceChildren();
1004
+ editor = element("div", "pm-editor");
1005
+ editor.setAttribute("role", "dialog");
1006
+ editor.setAttribute("aria-modal", "true");
1007
+ editor.setAttribute("aria-label", title);
1008
+ editor.tabIndex = -1;
1009
+ const heading = element("div", "pm-modal-heading");
1010
+ if (back) heading.append(pageIcon(text("Back", "返回"), "back", back));
1011
+ heading.append(
1012
+ element("h2", "", title),
1013
+ pageIcon(text("Close", "关闭"), "x", () => closeEditor()),
1014
+ );
1015
+ editor.append(heading);
1016
+ editorLayer.append(editor);
1017
+ editor.focus();
1018
+ return editor;
1019
+ }
1020
+ document.addEventListener(
1021
+ "keydown",
1022
+ (event) => {
1023
+ if (!editorLayer || !editor || pagePopup) return;
1024
+ if (event.key === "Escape") {
1025
+ event.preventDefault();
1026
+ event.stopImmediatePropagation();
1027
+ closeEditor();
1028
+ }
1029
+ if (event.key === "Tab") {
1030
+ const controls = [
1031
+ ...editor.querySelectorAll(
1032
+ 'button:not(:disabled),input,select,textarea,[tabindex="0"]',
1033
+ ),
1034
+ ].filter((x) => x.getBoundingClientRect().width);
1035
+ const first = controls[0],
1036
+ last = controls.at(-1);
1037
+ if (
1038
+ event.shiftKey &&
1039
+ (document.activeElement === first || document.activeElement === editor)
1040
+ ) {
1041
+ event.preventDefault();
1042
+ last?.focus();
1043
+ } else if (!event.shiftKey && document.activeElement === last) {
1044
+ event.preventDefault();
1045
+ first?.focus();
1046
+ }
1047
+ }
1048
+ },
1049
+ { signal, capture: true },
1050
+ );
1051
+ function editPrompt(item = null) {
1052
+ const target = editorShell(
1053
+ item ? text("Edit prompt", "编辑提示词") : text("New prompt", "新建提示词"),
1054
+ );
1055
+ target.classList.add("pm-prompt-editor");
1056
+ const form = element("form", "pm-form");
1057
+ const title = element("input");
1058
+ title.value = item?.title ?? "";
1059
+ title.maxLength = 200;
1060
+ const category = {
1061
+ value:
1062
+ item?.categoryId ??
1063
+ (categories.some((c) => c.id === activeCategory) ? activeCategory : ""),
1064
+ };
1065
+ const emoji = { value: item?.emoji ?? "" };
1066
+ const categoryButton = control(
1067
+ "",
1068
+ () => {
1069
+ const popup = makePagePopup(categoryButton, "pm-category-menu", "menu", true);
1070
+ if (!popup) return;
1071
+ for (const c of [
1072
+ ...categories,
1073
+ { id: "", name: text("Other", "其他") },
1074
+ ]) {
1075
+ const choice = control(
1076
+ c.name,
1077
+ () => {
1078
+ category.value = c.id;
1079
+ refreshAppearance();
1080
+ closePagePopup(true);
1081
+ },
1082
+ "",
1083
+ );
1084
+ choice.replaceChildren(
1085
+ categoryIcon({ categoryId: c.id || null }),
1086
+ element("span", "pm-option-label", c.name),
1087
+ );
1088
+ choice.setAttribute("role", "menuitemradio");
1089
+ choice.setAttribute("aria-checked", String(category.value === c.id));
1090
+ if (category.value === c.id) choice.append(icon("check"));
1091
+ popup.append(choice);
1092
+ }
1093
+ popup.querySelector("[aria-checked=true]")?.focus();
1094
+ },
1095
+ "pm-select",
1096
+ );
1097
+ categoryButton.setAttribute("aria-haspopup", "menu");
1098
+ const emojiButton = control(
1099
+ text("Choose icon", "选择图标"),
1100
+ () => openEmojiPicker(emojiButton, emoji, refreshAppearance),
1101
+ "pm-emoji-trigger",
1102
+ );
1103
+ emojiButton.setAttribute("aria-haspopup", "dialog");
1104
+ function refreshAppearance() {
1105
+ const c = categories.find((c) => c.id === category.value);
1106
+ categoryButton.replaceChildren(
1107
+ categoryIcon({ categoryId: category.value || null }),
1108
+ element("span", "pm-option-label", c?.name ?? text("Other", "其他")),
1109
+ icon("chevron"),
1110
+ );
1111
+ categoryButton.setAttribute(
1112
+ "aria-label",
1113
+ text("Category: ", "分类:") + (c?.name ?? text("Other", "其他")),
1114
+ );
1115
+ emojiButton.replaceChildren(
1116
+ categoryIcon({ categoryId: category.value || null, emoji: emoji.value }),
1117
+ );
1118
+ emojiButton.title = text("Choose icon", "选择图标");
1119
+ }
1120
+ refreshAppearance();
1121
+ const body = element("textarea");
1122
+ body.value = item?.body ?? "";
1123
+ body.required = true;
1124
+ const titleControl = element("div", "pm-title-control");
1125
+ titleControl.setAttribute("role", "group");
1126
+ titleControl.setAttribute("aria-label", text("Prompt title and icon", "提示词标题与图标"));
1127
+ title.setAttribute("aria-label", text("Title", "标题"));
1128
+ title.placeholder = text("Title", "标题");
1129
+ titleControl.append(emojiButton, title);
1130
+ form.append(
1131
+ titleControl,
1132
+ field(text("Category", "分类"), categoryButton),
1133
+ field(text("Prompt", "提示词"), body),
1134
+ );
1135
+ const footer = element("div", "pm-footer");
1136
+ if (item) {
1137
+ const remove = pageControl(text("Delete", "删除"), async () => {
1138
+ if (remove.dataset.confirm !== "true") {
1139
+ remove.dataset.confirm = "true";
1140
+ remove.textContent = text("Confirm delete", "确认删除");
1141
+ return;
1142
+ }
1143
+ remove.disabled = true;
1144
+ try {
1145
+ await api(`/api/prompts/${item.id}`, { method: "DELETE" });
1146
+ if (editor === target) await loadPage();
1147
+ } catch (error) {
1148
+ remove.disabled = false;
1149
+ pageError(error);
1150
+ }
1151
+ });
1152
+ remove.classList.add("pm-danger");
1153
+ remove.style.marginRight = "auto";
1154
+ footer.append(remove);
1155
+ }
1156
+ const save = pageControl(text("Save", "保存"), () => {}, true);
1157
+ save.type = "submit";
1158
+ footer.append(pageControl(text("Cancel", "取消"), renderPage), save);
1159
+ form.append(footer);
1160
+ target.append(form);
1161
+ form.addEventListener("submit", async (event) => {
1162
+ event.preventDefault();
1163
+ if (save.disabled || !body.value.trim()) return;
1164
+ save.disabled = true;
1165
+ try {
1166
+ await api(item ? `/api/prompts/${item.id}` : "/api/prompts", {
1167
+ method: item ? "PATCH" : "POST",
1168
+ body: {
1169
+ title: title.value,
1170
+ body: body.value,
1171
+ categoryId: category.value || null,
1172
+ emoji: emoji.value.trim(),
1173
+ },
1174
+ });
1175
+ if (editor === target) await loadPage();
1176
+ } catch (error) {
1177
+ if (editor === target) {
1178
+ save.disabled = false;
1179
+ pageError(error);
1180
+ }
1181
+ }
1182
+ });
1183
+ title.focus();
1184
+ }
1185
+ function openEmojiPicker(trigger, selected, refresh) {
1186
+ const popup = makePagePopup(trigger, "pm-symbol-picker", "dialog");
1187
+ if (!popup) return;
1188
+ const search = element("input");
1189
+ search.placeholder = text("Search emoji", "搜索 Emoji");
1190
+ search.setAttribute("aria-label", search.placeholder);
1191
+ const grid = element("div", "pm-emoji-grid");
1192
+ grid.setAttribute("role", "group");
1193
+ grid.setAttribute("aria-label", text("Emojis", "Emoji"));
1194
+ const render = () => {
1195
+ grid.replaceChildren();
1196
+ const query = search.value.trim().toLocaleLowerCase();
1197
+ for (const item of nativeEmojis.filter(
1198
+ (e) => !query || e[0].includes(query) || e[1].includes(query),
1199
+ )) {
1200
+ const choice = control(
1201
+ item[1],
1202
+ () => {
1203
+ selected.value = item[0];
1204
+ refresh();
1205
+ closePagePopup(true);
1206
+ },
1207
+ "pm-emoji-choice",
1208
+ );
1209
+ choice.textContent = item[0];
1210
+ choice.setAttribute("aria-pressed", String(selected.value === item[0]));
1211
+ grid.append(choice);
1212
+ }
1213
+ if (!grid.children.length)
1214
+ grid.append(
1215
+ element(
1216
+ "div",
1217
+ "pm-empty-description",
1218
+ text("No emoji found", "未找到 Emoji"),
1219
+ ),
1220
+ );
1221
+ };
1222
+ search.addEventListener("input", render);
1223
+ const reset = control(
1224
+ text("Use category icon", "使用分类图标"),
1225
+ () => {
1226
+ selected.value = "";
1227
+ refresh();
1228
+ closePagePopup(true);
1229
+ },
1230
+ "pm-command",
1231
+ );
1232
+ popup.append(search, grid, reset);
1233
+ render();
1234
+ search.focus();
1235
+ const r = popup.getBoundingClientRect();
1236
+ popup.style.left = `${Math.max(8, Math.min(trigger.getBoundingClientRect().left, innerWidth - r.width - 8))}px`;
1237
+ if (r.bottom > innerHeight - 16)
1238
+ popup.style.top = `${Math.max(16, innerHeight - r.height - 16)}px`;
1239
+ popup.addEventListener("keydown", (event) => {
1240
+ const choices = [...grid.querySelectorAll("button")],
1241
+ index = choices.indexOf(document.activeElement);
1242
+ if (
1243
+ ["ArrowLeft", "ArrowRight", "ArrowUp", "ArrowDown"].includes(event.key)
1244
+ ) {
1245
+ event.preventDefault();
1246
+ const step = { ArrowLeft: -1, ArrowRight: 1, ArrowUp: -8, ArrowDown: 8 }[
1247
+ event.key
1248
+ ];
1249
+ choices[Math.max(0, Math.min(choices.length - 1, index + step))]?.focus();
1250
+ }
1251
+ if (event.key === "Tab") {
1252
+ const buttons = [search, ...choices, reset],
1253
+ i = buttons.indexOf(document.activeElement);
1254
+ if (event.shiftKey && i === 0) {
1255
+ event.preventDefault();
1256
+ reset.focus();
1257
+ } else if (!event.shiftKey && i === buttons.length - 1) {
1258
+ event.preventDefault();
1259
+ search.focus();
1260
+ }
1261
+ }
1262
+ });
1263
+ }
1264
+ function manageCategories() {
1265
+ const target = editorShell(text("Categories", "分类"));
1266
+ const footer = element("div", "pm-footer");
1267
+ footer.append(
1268
+ pageControl(text("New category", "新建分类"), () => editCategory(), true),
1269
+ );
1270
+ for (const category of categories) {
1271
+ const row = element("div", "pm-row");
1272
+ row.append(
1273
+ categoryIcon({ categoryId: category.id }),
1274
+ control(category.name, () => editCategory(category), "pm-info"),
1275
+ orderControls(category, categories, true, row),
1276
+ );
1277
+ target.append(row);
1278
+ }
1279
+ target.append(footer);
1280
+ }
1281
+ function editCategory(category = null) {
1282
+ const target = editorShell(
1283
+ category
1284
+ ? text("Edit category", "编辑分类")
1285
+ : text("New category", "新建分类"),
1286
+ manageCategories,
1287
+ );
1288
+ const form = element("form", "pm-form");
1289
+ const name = element("input");
1290
+ name.value = category?.name ?? "";
1291
+ name.required = true;
1292
+ name.maxLength = 60;
1293
+ let selectedIcon = category?.icon ?? "book",
1294
+ selectedColor = category?.color ?? "black";
1295
+ const icons = element("div", "pm-grid");
1296
+ icons.setAttribute("aria-label", text("Category icon", "分类图标"));
1297
+ function refreshChoices() {
1298
+ icons.replaceChildren();
1299
+ for (const key of Object.keys(nativeIcons)) {
1300
+ const btn = control(
1301
+ key,
1302
+ () => {
1303
+ selectedIcon = key;
1304
+ refreshChoices();
1305
+ },
1306
+ "pm-choice",
1307
+ );
1308
+ const svg = new DOMParser().parseFromString(
1309
+ nativeIcons[key],
1310
+ "image/svg+xml",
1311
+ ).documentElement;
1312
+ btn.replaceChildren(svg);
1313
+ btn.setAttribute("aria-pressed", String(key === selectedIcon));
1314
+ btn.title = appearanceLabel(key);
1315
+ btn.setAttribute("aria-label", appearanceLabel(key));
1316
+ icons.append(btn);
1317
+ }
1318
+ }
1319
+ refreshChoices();
1320
+ const colors = element("div", "pm-grid");
1321
+ for (const key of Object.keys(COLORS)) {
1322
+ const btn = control(
1323
+ key,
1324
+ () => {
1325
+ selectedColor = key;
1326
+ for (const child of colors.children)
1327
+ child.setAttribute("aria-pressed", String(child === btn));
1328
+ },
1329
+ "pm-choice",
1330
+ );
1331
+ const swatch = element("span", "qp-symbol");
1332
+ swatch.style.setProperty("--prompt-icon-light", COLORS[key][0]);
1333
+ swatch.style.setProperty("--prompt-icon-dark", COLORS[key][1]);
1334
+ swatch.append(element("span", "pm-swatch"));
1335
+ btn.replaceChildren(swatch);
1336
+ btn.setAttribute("aria-pressed", String(key === selectedColor));
1337
+ btn.title = appearanceLabel(key);
1338
+ btn.setAttribute("aria-label", appearanceLabel(key));
1339
+ colors.append(btn);
1340
+ }
1341
+ form.append(
1342
+ field(text("Name", "名称"), name),
1343
+ element("div", "", text("Icon", "图标")),
1344
+ icons,
1345
+ element("div", "", text("Color", "颜色")),
1346
+ colors,
1347
+ );
1348
+ const footer = element("div", "pm-footer");
1349
+ if (category) {
1350
+ const remove = pageControl(text("Delete", "删除"), () => {
1351
+ const count = items.filter(
1352
+ (item) => item.categoryId === category.id,
1353
+ ).length;
1354
+ const destroy = async (button) => {
1355
+ button.disabled = true;
1356
+ try {
1357
+ await api(`/api/categories/${category.id}`, { method: "DELETE" });
1358
+ await loadPage();
1359
+ if (page) manageCategories();
1360
+ } catch (error) {
1361
+ button.disabled = false;
1362
+ pageError(error);
1363
+ }
1364
+ };
1365
+ if (!count) {
1366
+ void destroy(remove);
1367
+ return;
1368
+ }
1369
+ const confirmation = editorShell(text("Delete category?", "删除分类?"));
1370
+ confirmation.append(
1371
+ element(
1372
+ "p",
1373
+ "pm-confirm-description",
1374
+ text(
1375
+ `${count} prompts will move to Other. Their content and call counts will be kept.`,
1376
+ `${count} 条提示词将移到 Other,内容和调用次数都会保留。`,
1377
+ ),
1378
+ ),
1379
+ );
1380
+ const actions = element("div", "pm-footer");
1381
+ const confirm = pageControl(
1382
+ text("Delete", "删除"),
1383
+ () => void destroy(confirm),
1384
+ );
1385
+ confirm.classList.add("pm-danger");
1386
+ actions.append(
1387
+ pageControl(text("Cancel", "取消"), () => editCategory(category)),
1388
+ confirm,
1389
+ );
1390
+ confirmation.append(actions);
1391
+ });
1392
+ remove.classList.add("pm-danger");
1393
+ remove.style.marginRight = "auto";
1394
+ footer.append(remove);
1395
+ }
1396
+ const save = pageControl(text("Save", "保存"), () => {}, true);
1397
+ save.type = "submit";
1398
+ footer.append(pageControl(text("Cancel", "取消"), manageCategories), save);
1399
+ form.append(footer);
1400
+ target.append(form);
1401
+ form.addEventListener("submit", async (event) => {
1402
+ event.preventDefault();
1403
+ if (save.disabled) return;
1404
+ save.disabled = true;
1405
+ try {
1406
+ await api(
1407
+ category ? `/api/categories/${category.id}` : "/api/categories",
1408
+ {
1409
+ method: category ? "PATCH" : "POST",
1410
+ body: { name: name.value, icon: selectedIcon, color: selectedColor },
1411
+ },
1412
+ );
1413
+ if (editor === target) {
1414
+ await loadPage();
1415
+ if (page) manageCategories();
1416
+ }
1417
+ } catch (error) {
1418
+ if (editor === target) {
1419
+ save.disabled = false;
1420
+ pageError(error);
1421
+ }
1422
+ }
1423
+ });
1424
+ name.focus();
1425
+ }
1426
+ // 原生导航挂载与卸载
1427
+ function syncNavigation() {
1428
+ const rail = document.querySelector("[data-app-navigation-rail]");
1429
+ if (!rail) {
1430
+ navButton?.remove();
1431
+ navButton = null;
1432
+ if (page) closePage();
1433
+ return;
1434
+ }
1435
+ const source =
1436
+ rail.querySelector('[data-sidebar-destination="builtin:projects"]') ??
1437
+ rail.querySelector('[data-sidebar-destination="builtin:library"]');
1438
+ if (!source) return;
1439
+ // 固定扩展区是独立容器;不依赖 Projects 的固定状态或文字。
1440
+ const scroll = rail.querySelector(".overflow-y-auto");
1441
+ if (!scroll) return;
1442
+ let group = [...scroll.children].find(
1443
+ (node) =>
1444
+ node !== ownNavGroup &&
1445
+ node.classList.contains("flex-col") &&
1446
+ node.classList.contains("shrink-0"),
1447
+ );
1448
+ if (!group) {
1449
+ if (!ownNavGroup) {
1450
+ ownNavGroup = element("div", "flex shrink-0 flex-col items-center gap-2");
1451
+ ownNavGroup.setAttribute(MARKER, "nav-group");
1452
+ }
1453
+ if (ownNavGroup.parentElement !== scroll) scroll.append(ownNavGroup);
1454
+ group = ownNavGroup;
1455
+ }
1456
+ if (!navButton?.isConnected) {
1457
+ navButton = source.cloneNode(false);
1458
+ navButton.removeAttribute("data-sidebar-destination");
1459
+ navButton.removeAttribute("data-selected");
1460
+ navButton.removeAttribute("aria-current");
1461
+ navButton.removeAttribute("data-suppress-active-style");
1462
+ navButton.setAttribute(MARKER, "nav");
1463
+ navButton.setAttribute("aria-label", text("Prompts", "提示词"));
1464
+ navButton.title = text("Prompts", "提示词");
1465
+ const inner = element("span", source.firstElementChild?.className ?? "");
1466
+ const glyph = icon("prompt-nav");
1467
+ glyph.setAttribute("aria-hidden", "true");
1468
+ inner.append(glyph, element("span", "sr-only", text("Prompts", "提示词")));
1469
+ navButton.append(inner);
1470
+ navButton.addEventListener("click", openPage);
1471
+ }
1472
+ if (navButton.parentElement !== group || navButton !== group.lastElementChild)
1473
+ group.append(navButton);
1474
+ if (group !== ownNavGroup) ownNavGroup?.remove();
1475
+ syncNavigationSelection();
1476
+ positionPage();
1477
+ }
1478
+ document.addEventListener(
1479
+ "click",
1480
+ (event) => {
1481
+ if (
1482
+ page &&
1483
+ event.target.closest("[data-app-navigation-rail] button") &&
1484
+ !navButton?.contains(event.target)
1485
+ )
1486
+ closePage();
1487
+ },
1488
+ { signal, capture: true },
1489
+ );
1490
+ window.addEventListener("popstate", () => closePage(), { signal });
1491
+ window.addEventListener("resize", positionPage, { signal });
1492
+
1493
+ const observer = new MutationObserver(() => {
1494
+ if (syncQueued) return;
1495
+ syncQueued = true;
1496
+ queueMicrotask(() => {
1497
+ syncQueued = false;
1498
+ sync();
1499
+ });
1500
+ });
1501
+ observer.observe(document.documentElement, {
1502
+ childList: true,
1503
+ subtree: true,
1504
+ attributes: true,
1505
+ attributeFilter: [
1506
+ "data-composer-placement",
1507
+ "data-above-composer-conversation-id",
1508
+ "aria-expanded",
1509
+ "data-selected",
1510
+ "aria-current",
1511
+ ],
1512
+ });
1513
+ document.addEventListener(
1514
+ "pointerdown",
1515
+ (event) => {
1516
+ if (
1517
+ panel &&
1518
+ !panel.contains(event.target) &&
1519
+ !button?.contains(event.target)
1520
+ )
1521
+ close();
1522
+ },
1523
+ { signal },
1524
+ );
1525
+ document.addEventListener(
1526
+ "keydown",
1527
+ (event) => {
1528
+ if (!panel) return;
1529
+ if (event.key === "Escape") {
1530
+ event.preventDefault();
1531
+ event.stopImmediatePropagation();
1532
+ close(true);
1533
+ } else if (["ArrowDown", "ArrowUp"].includes(event.key)) {
1534
+ event.preventDefault();
1535
+ event.stopImmediatePropagation();
1536
+ const choices = [...panel.querySelectorAll(".qp-select")];
1537
+ const current = choices.indexOf(document.activeElement);
1538
+ const next =
1539
+ event.key === "ArrowDown"
1540
+ ? (current + 1) % choices.length
1541
+ : (current <= 0 ? choices.length : current) - 1;
1542
+ choices[next]?.focus();
1543
+ }
1544
+ },
1545
+ { signal, capture: true },
1546
+ );
1547
+ window.addEventListener("resize", position, { signal });
1548
+ window.addEventListener("scroll", position, { signal, capture: true });
1549
+ sync();
1550
+ return () => {
1551
+ disposed = true;
1552
+ observer.disconnect();
1553
+ close();
1554
+ button?.remove();
1555
+ closePage();
1556
+ navButton?.remove();
1557
+ ownNavGroup?.remove();
1558
+ style.remove();
1559
+ };